---
updatedAt: 2026-06-02T12:18:56.000Z
agentTools:
  projectIndex: https://developer.eftcorp.com/llms.txt
---

# Admin Portal Portlets

Build self-contained HTML portlet pages for the Eclipse Admin Portal — authentication, design system, API wiring, and component patterns for list, detail, form, and dashboard portlets.

## Overview

This skill enables AI agents to generate complete, production-grade HTML portlet pages for the Eclipse Admin Portal. A portlet is a single self-contained HTML file that is registered with the portal and appears as a native section in the navigation. Eclipse handles authentication automatically — when a portal user navigates to a portlet, Eclipse injects a signed JWT via a `?token=` URL parameter.

It supports:

* List portlets — searchable, paginated tables with stat cards
* Detail portlets — profile header, tab nav, property rows
* Form portlets — split layout with live preview and confirmation modal
* Dashboard portlets — stat cards, activity tables, quick actions

Every portlet is built as a single file with zero backend and zero build step — just open in a browser or register with the portal.

> **Design Reference:** Portlets follow a fixed design system. Do not substitute colours, fonts, or component patterns. The output should match the quality of tools like Lovable.dev.

***

## Trigger

**Trigger phrases:** "build a portlet", "create a portlet", "write a portlet", "generate an Eclipse portlet", "make a portlet for", "new portlet", or any description of a feature that needs an Eclipse Admin Portal page.

**Required inputs:** the portlet name, the primary data domain (wallets, payments, cards, customers, etc.), and the key feature (list, detail view, form, dashboard). If `tenantId` is provided, embed it; otherwise use a `TENANT_ID` constant.

***

## 1. Design System

### CDN Imports (always exactly these three)

```html
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
```

### Colour Palette

| Token       | Value     | Usage                                           |
| ----------- | --------- | ----------------------------------------------- |
| `--navy`    | `#050C43` | Header, icon fills, navy buttons                |
| `--blue`    | `#007AFF` | Primary buttons, links, active tab, focus rings |
| `--bg`      | `#F0F3FB` | Page background, alternating table rows         |
| `--surface` | `#FFFFFF` | Cards, modals, inputs                           |
| `--border`  | `#deebfd` | Card borders                                    |
| `--text`    | `#0f172a` | Body text                                       |
| `--muted`   | `#64748b` | Labels, secondary text                          |
| `--success` | `#00984E` | Active badges, success alerts                   |
| `--error`   | `#F15A22` | Error badges, destructive actions               |
| `--warn`    | `#FF9600` | Warning states                                  |

Primary accent is always `--blue` (`#007AFF`). Never substitute purple, teal, or other accent colours.

### Tailwind Config (include verbatim)

```js
tailwind.config = {
  theme: {
    extend: {
      fontFamily: { sans: ['Inter','system-ui','-apple-system','sans-serif'] },
      colors: {
        navy:        { DEFAULT:'#050C43', hover:'#070f52' },
        accent:      { DEFAULT:'#007AFF', 300:'#4DA2FF', 600:'#0072FF', 900:'#004AFF' },
        'bg-page':   '#F0F3FB',
        'card-border':'#deebfd',
      },
      boxShadow: {
        card: '-8px 12px 18px 0 #dadee8',
        nav:  '5px 7px 26px -5px #cdd4e7',
      },
      borderRadius: { card:'12px', btn:'6px', pill:'9999px' }
    }
  }
}
```

***

## 2. HTML Structure

Every portlet follows this top-level layout:

```html
<!-- PAGE HEADER -->
<div class="page-header" id="page-header">
  <div class="ph-icon"><i data-lucide="ICON_NAME"></i></div>
  <span class="ph-title">Portlet Title</span>
  <div class="breadcrumb" style="margin-left:4px">
    <span class="bc-sep">/</span><span>Section</span>
    <span class="bc-sep">/</span><span class="bc-current">Portlet Title</span>
  </div>
  <div class="ph-spacer"></div>
  <div class="ph-actions">
    <button class="btn btn-ghost btn-sm" onclick="openJwtModal()" title="Auth">
      <i data-lucide="key-round"></i>
    </button>
    <!-- portlet-specific action buttons -->
  </div>
</div>

<!-- APP BODY -->
<div class="app-body" id="app-body">
  <div id="app-banner" class="alert alert-info" style="display:none">
    <i data-lucide="info" id="app-banner-icon-el"></i>
    <span id="app-banner-text"></span>
    <button class="btn btn-ghost btn-sm" onclick="hideBanner()" style="margin-left:auto;min-width:unset;padding:0 4px">
      <i data-lucide="x"></i>
    </button>
  </div>
  <!-- portlet content -->
</div>

<!-- JWT MODAL — copy verbatim, do not modify -->
```

The page header always includes the `key-round` auth button. The `#app-banner` status banner is always present. The JWT modal is always included in full.

***

## 3. Authentication

Eclipse injects authentication via a `?token=` URL parameter when a portal user opens the portlet. The auth infrastructure is fixed — copy it verbatim into every portlet.

### How it works

1. On load, the init IIFE reads `?token=` from the URL
2. If present, `_exchangeToken()` calls the Eclipse login endpoint to exchange it for a full JWT and stores it in `localStorage`
3. If no `?token=` param, the stored JWT is used; if none exists, the JWT modal opens for manual entry
4. All API calls go through `apiFetch()`, which waits for the JWT to be ready before sending

### Key auth functions

```js
// All API calls — use this exclusively, never raw fetch
async function apiFetch(path, options = {}) { /* injects Authorization header */ }

// JWT decode utility
function decodeJWT(token) { /* returns payload object */ }

// Token exchange — called automatically from init() when ?token= is present
async function _exchangeToken(urlToken) { /* exchanges portal token for JWT */ }
```

### Startup pattern

```js
async function onReady() {
  const tenantId = resolveTenantId();
  try {
    showSkeleton();
    const data = await apiFetch(`/tenants/${tenantId}/YOUR_RESOURCE`);
    renderData(data);
  } catch(err) {
    showBanner('error', err.message);
    showEmpty();
  }
}

(function init() {
  const params   = new URLSearchParams(window.location.search);
  const urlToken = params.get('token');
  if (urlToken) {
    _exchangeToken(urlToken);
  } else {
    const token = loadToken();
    if (token) { const p = decodeJWT(token); if (p) _scheduleRenewal(token, p); _signalJwtReady(); }
    else { openJwtModal(); }
  }
  onReady();
})();

lucide.createIcons(); // always last line
```

***

## 4. tenantId Resolution

Eclipse API paths are always tenant-scoped. Resolve `tenantId` in this priority order:

```js
function resolveTenantId() {
  // 1. Path segment: /portlets/{tenantId}/...
  const seg = window.location.pathname.split('/').filter(Boolean);
  const idx = seg.indexOf('portlets');
  if (idx >= 0 && seg[idx+1] && /^\d+$/.test(seg[idx+1])) return seg[idx+1];
  // 2. Query param
  const qp = new URLSearchParams(window.location.search).get('tenantId');
  if (qp) return qp;
  // 3. JWT claim
  const p = decodeJWT(loadToken());
  if (p?.tenant) return String(p.tenant);
  // 4. Fallback constant
  return TENANT_ID;
}
```

Common tenant-scoped API paths:

```
/tenants/{tenantId}/customers
/tenants/{tenantId}/wallets
/tenants/{tenantId}/payments
/tenants/{tenantId}/cards
/tenants/{tenantId}/wallet-types
/tenants/{tenantId}/config-items
```

All paths are relative — `apiFetch()` prepends the base URL automatically.

***

## 5. Component Patterns

### List Portlet

* 4 stat cards across the top (`currentBalance`, counts, etc.)
* Card with search input (pill shape) and filter chips in the header
* Sortable table with status badges, mono-font IDs, and row action buttons
* Skeleton loading rows while fetching, empty state when result set is empty
* Pagination controls for lists exceeding 25 rows

### Detail Portlet

* Profile header card with navy gradient, avatar icon, name, and status badge
* Tab nav (`Overview`, `Transactions`, `History`, etc.)
* Property rows for key fields (`prop-key` / `prop-val` pattern)
* Action buttons (Edit, Suspend, Back to list)

### Form Portlet

* Split layout: form fields left, live summary panel right
* Progressive disclosure — show/hide sections based on selections
* Inline validation feedback below each field
* Confirmation modal before any destructive or financial operation

### Dashboard Portlet

* 4-column stat cards with trend indicators (up/down arrows, colour-coded)
* Recent activity table (last 10–20 rows, no pagination needed)
* Quick-action buttons for the most common operations

***

## 6. UX Requirements

Every portlet must include all of the following — no exceptions:

**Loading states:** show `.skeleton-row` elements with `.skeleton` divs while fetching. Hide and replace with real content on arrival.

**Error handling:** wrap every `apiFetch()` call in try/catch and call `showBanner('error', err.message)` on failure. Surface the Eclipse `traceId` from the error body when present.

**Empty state:** show an `.empty-state` block (icon, title, subtitle) when the API returns an empty list.

**Button loading:** disable the button and show a spinner while an async action is in progress. Re-enable and restore the label when it completes.

**Status badges:** use the fixed badge classes — `badge-active` (green), `badge-pending` (amber), `badge-error` (red), `badge-inactive` (grey), `badge-info` (blue). Never invent new badge colours.

***

## Common Patterns for AI Agents

### Build a portlet from a description

```
Build an Eclipse portlet for [domain]. It should [key feature — list / detail / form / dashboard].
Primary entity: [wallet / customer / payment / card / etc.].
tenantId: [value or leave blank for TENANT_ID constant].
```

The AI generates a single complete `.html` file ready to register with the portal.

### Add a feature to an existing portlet

Share the current `.html` file and describe the addition:

```
Add [feature] to this portlet. Keep the existing design system and auth block unchanged.
```

The auth block and design tokens must never be modified during incremental updates.

### Generate a portlet for a specific API endpoint

```
Build an Eclipse portlet that calls [endpoint] and displays [fields].
Use the portlet skill design system and authentication pattern.
```

Use the [OpenAPI Spec skill](https://developer.eftcorp.com/docs/openapi) alongside this skill to get exact field names from the spec before generating.

***

## Notes

* The auth JavaScript block and CSS design tokens are fixed infrastructure — never condense, alter, or remove any part of them
* `lucide.createIcons()` must always be the last line in `<script>`
* All portlet output must be a single `.html` file with no external dependencies beyond the three CDNs
* The JWT modal (`#jwt-modal`) must always be present in full — it is the fallback auth path for development and direct access
* Token renewal is handled automatically by `_scheduleRenewal()` — do not add manual renewal logic
* Never store credentials in the portlet HTML — authentication flows entirely through the JWT

***

## Best Practices

* Pick the most semantically appropriate Lucide icon for the page header — browse at [lucide.dev](https://lucide.dev)
* Use skeleton rows that match the shape of the real content (same column count and approximate widths)
* Keep action buttons in the page header for global actions; put row-level actions in the table as ghost buttons
* For financial amounts, always display with the currency code and correct decimal places — never format as bare integers
* Confirm before any destructive or financial operation with a modal, not a browser `confirm()` dialog
* When calling list endpoints, always paginate with `limit=25` and an offset-based page control