Admin Portal 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)

<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

TokenValueUsage
--navy#050C43Header, icon fills, navy buttons
--blue#007AFFPrimary buttons, links, active tab, focus rings
--bg#F0F3FBPage background, alternating table rows
--surface#FFFFFFCards, modals, inputs
--border#deebfdCard borders
--text#0f172aBody text
--muted#64748bLabels, secondary text
--success#00984EActive badges, success alerts
--error#F15A22Error badges, destructive actions
--warn#FF9600Warning states

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

Tailwind Config (include verbatim)

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:

<!-- 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

// 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

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:

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 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
  • 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