Restyle staff and console UIs; add UI-Docs walkthrough and review PDF.
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
Match CS/sales/accounting/access-staff to the portal indigo system with dollar amounts, skip links, and empty states. Fleet replica actions move into overflow menus, roles become chips, Docs become cards, and the header copy reflects the 0.0.0.0 bind. Simulator uses the same shell (orange only for faults). Portal API keys are masked; admin customers edit in a drawer. Catalog uses system-ui. New UI-Docs repo holds screenshots, usage notes, and UI-REVIEW.pdf.
This commit is contained in:
parent
ddf772454b
commit
cb07f5b321
72 changed files with 3284 additions and 242 deletions
|
|
@ -112,6 +112,12 @@ function say(msg, isError = false) {
|
|||
|
||||
const fmt = (cents) => (cents < 0 ? '-$' : '$') + (Math.abs(cents) / 100).toFixed(2);
|
||||
const fmtDate = (ms) => (ms ? new Date(ms).toISOString().slice(0, 10) : '—');
|
||||
const EMPTY_SVG =
|
||||
'<svg width="80" height="64" viewBox="0 0 80 64" fill="none" aria-hidden="true"><rect x="16" y="8" width="48" height="48" rx="8" fill="#eef0fe"/><rect x="24" y="20" width="32" height="4" rx="2" fill="#4f46e5" opacity=".35"/><rect x="24" y="30" width="24" height="4" rx="2" fill="#4f46e5" opacity=".2"/><rect x="24" y="40" width="28" height="4" rx="2" fill="#4f46e5" opacity=".2"/></svg>';
|
||||
function maskKey(k) {
|
||||
if (!k || k.length < 8) return '••••••••';
|
||||
return `${k.slice(0, 4)}••••••••${k.slice(-4)}`;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
state.me = await api('/me');
|
||||
|
|
@ -145,16 +151,26 @@ function renderDashboard() {
|
|||
<div class="card">
|
||||
<h3>Your API key</h3>
|
||||
<div class="apikey-row">
|
||||
<code id="api-key">${me.apiKey}</code>
|
||||
<button class="btn ghost" onclick="copyKey()">Copy</button>
|
||||
<button class="btn" onclick="regenerateKey()">Regenerate</button>
|
||||
<code id="api-key" data-key="${me.apiKey}" data-masked="1">${maskKey(me.apiKey)}</code>
|
||||
<button class="btn ghost" type="button" onclick="toggleKey()">Reveal</button>
|
||||
<button class="btn ghost" type="button" onclick="copyKey()">Copy</button>
|
||||
<button class="btn" type="button" onclick="regenerateKey()">Regenerate</button>
|
||||
</div>
|
||||
<p class="hint">Send it as the <span class="id">x-api-key</span> header. Regenerating invalidates the old key immediately. Per-endpoint usage: ${Object.entries(u.byEndpoint ?? {}).map(([k, v]) => `${k}: ${v.calls} calls`).join(', ') || 'none yet this month'}.</p>
|
||||
<p class="hint">Send it as the <span class="id">x-api-key</span> header. The key stays masked on screen. Copy puts the full key on the clipboard. Regenerating invalidates the old key immediately. Per-endpoint usage: ${Object.entries(u.byEndpoint ?? {}).map(([k, v]) => `${k}: ${v.calls} calls`).join(', ') || 'none yet this month'}.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function toggleKey() {
|
||||
const el = document.getElementById('api-key');
|
||||
const btn = el.parentElement.querySelector('button');
|
||||
const masked = el.dataset.masked === '1';
|
||||
el.textContent = masked ? el.dataset.key : maskKey(el.dataset.key);
|
||||
el.dataset.masked = masked ? '0' : '1';
|
||||
btn.textContent = masked ? 'Hide' : 'Reveal';
|
||||
}
|
||||
|
||||
function copyKey() {
|
||||
navigator.clipboard.writeText(document.getElementById('api-key').textContent);
|
||||
navigator.clipboard.writeText(document.getElementById('api-key').dataset.key);
|
||||
say('API key copied.');
|
||||
}
|
||||
|
||||
|
|
@ -187,10 +203,14 @@ function renderInvoices() {
|
|||
document.getElementById('invoices').innerHTML = `
|
||||
<h2>Invoices</h2>
|
||||
<p class="lede">Your invoice history. “View / print” opens a print-ready page — use the browser’s Print → Save as PDF.</p>
|
||||
<div class="card"><table>
|
||||
<div class="card">${
|
||||
rows
|
||||
? `<table>
|
||||
<thead><tr><th>Invoice</th><th>Period</th><th>Status</th><th>Total</th><th>Credit</th><th>Amount due</th><th>Due date</th><th></th></tr></thead>
|
||||
<tbody>${rows || '<tr><td colspan="8" style="color:var(--muted)">No invoices yet.</td></tr>'}</tbody>
|
||||
</table></div>`;
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`
|
||||
: `<div class="empty">${EMPTY_SVG}<h3>No invoices yet</h3><p>After the first billing period is generated, invoices appear here. Use View / print to save a PDF.</p></div>`
|
||||
}</div>`;
|
||||
}
|
||||
|
||||
async function viewInvoice(id) {
|
||||
|
|
@ -206,10 +226,22 @@ async function viewInvoice(id) {
|
|||
|
||||
function renderStatement() {
|
||||
const st = state.statement || { prepaidCents: 0, credits: [], usage: [], payments: [] };
|
||||
const row = (list, cols) =>
|
||||
(list || [])
|
||||
.map((r) => `<tr>${cols.map((c) => `<td>${r[c] ?? ''}</td>`).join('')}</tr>`)
|
||||
.join('') || `<tr><td colspan="${cols.length}" style="color:var(--muted)">None yet.</td></tr>`;
|
||||
const row = (list, cols) => {
|
||||
if (!list || !list.length) {
|
||||
return `<tr><td colspan="${cols.length}"><div class="empty" style="padding:1rem">${EMPTY_SVG}<h3>Nothing here yet</h3></div></td></tr>`;
|
||||
}
|
||||
return list
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr>${cols
|
||||
.map((c) => {
|
||||
const v = c === 'cents' ? fmt(r.cents) : c === 'at' ? fmtDate(r.at) : (r[c] ?? '—');
|
||||
return `<td class="${c === 'cents' ? 'money' : ''}">${v}</td>`;
|
||||
})
|
||||
.join('')}</tr>`,
|
||||
)
|
||||
.join('');
|
||||
};
|
||||
document.getElementById('statement').innerHTML = `
|
||||
<h2>Statement</h2>
|
||||
<p class="lede">Prepaid balance, customer-service credits, metered usage, and payments. ${st.source === 'nats' ? 'Live from account-balance over NATS.' : 'From this portal’s ledger.'}</p>
|
||||
|
|
@ -220,13 +252,13 @@ function renderStatement() {
|
|||
<div class="stat"><div class="k">Payments</div><div class="v">${st.payments?.length || 0}</div></div>
|
||||
</div>
|
||||
<div class="card"><h3>Credits</h3><table>
|
||||
<thead><tr><th>Cents</th><th>Reason</th><th>Agent</th><th>When</th></tr></thead>
|
||||
<thead><tr><th>Amount</th><th>Reason</th><th>Agent</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.credits, ['cents', 'reason', 'agent', 'at'])}</tbody></table></div>
|
||||
<div class="card"><h3>Usage</h3><table>
|
||||
<thead><tr><th>Endpoint</th><th>Cents</th><th>When</th></tr></thead>
|
||||
<thead><tr><th>Endpoint</th><th>Amount</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.usage, ['endpointId', 'cents', 'at'])}</tbody></table></div>
|
||||
<div class="card"><h3>Payments</h3><table>
|
||||
<thead><tr><th>Cents</th><th>Kind</th><th>Reason</th><th>When</th></tr></thead>
|
||||
<thead><tr><th>Amount</th><th>Kind</th><th>Reason</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.payments, ['cents', 'kind', 'reason', 'at'])}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +387,7 @@ function renderDocs() {
|
|||
.join('');
|
||||
document.getElementById('docs').innerHTML = `
|
||||
<h2>API & pricing</h2>
|
||||
<p class="lede">Interactive API reference lives at <a href="/docs" target="_blank" style="color:var(--accent)">/docs</a> (Swagger UI) — use your API key as <span class="id">x-api-key</span>.</p>
|
||||
<p class="lede">Interactive API reference lives at <a href="/docs" target="_blank" style="color:var(--accent)">/docs</a>. That page is stock Swagger UI — not a designed customer surface. Use your API key as <span class="id">x-api-key</span>.</p>
|
||||
<div class="card">
|
||||
<h3>Rate card (list prices)</h3>
|
||||
<table><thead><tr><th>Endpoint</th><th>Kind</th><th>List price</th></tr></thead><tbody>${rows}</tbody></table>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Zappier Portal</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%234f46e5'/%3E%3C/svg%3E"/>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f5fb;
|
||||
|
|
@ -103,8 +104,14 @@
|
|||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
nav button { min-height: 40px; }
|
||||
nav button:hover { background: var(--bg); color: var(--ink); }
|
||||
nav button.active { background: var(--accent-soft); color: var(--accent); font-weight: 700; }
|
||||
nav button:focus-visible, #logout:focus-visible, button.btn:focus-visible, a:focus-visible {
|
||||
outline: 2px solid var(--accent); outline-offset: 2px;
|
||||
}
|
||||
.skip { position:absolute; left:-999px; }
|
||||
.skip:focus { left:1rem; top:1rem; z-index:30; background:#fff; color:var(--accent); padding:.5rem .9rem; border-radius:8px; }
|
||||
aside .spacer { flex: 1; }
|
||||
#logout {
|
||||
border: 1px solid var(--line); background: none; border-radius: 8px;
|
||||
|
|
@ -184,12 +191,15 @@
|
|||
.kv { display: grid; grid-template-columns: 170px 1fr; row-gap: 0.5rem; align-items: center; }
|
||||
.kv dt { color: var(--muted); font-size: 0.82rem; }
|
||||
.kv dd { margin: 0; font-weight: 600; }
|
||||
.apikey-row { display: flex; gap: 0.5rem; align-items: center; }
|
||||
.apikey-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
.apikey-row code {
|
||||
background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
|
||||
padding: 0.45rem 0.7rem; font-size: 0.85rem; flex: 1;
|
||||
font-family: "SF Mono", Menlo, monospace;
|
||||
padding: 0.45rem 0.7rem; font-size: 0.85rem; flex: 1; min-width: 12rem;
|
||||
font-family: "SF Mono", Menlo, monospace; letter-spacing: 0.04em;
|
||||
}
|
||||
.empty { text-align:center; padding:1.6rem 1rem; color:var(--muted); }
|
||||
.empty svg { display:block; margin:0 auto .6rem; }
|
||||
.empty h3 { margin:0 0 .25rem; color:var(--ink); font-size:1rem; }
|
||||
.qr-box { display: flex; gap: 1.25rem; align-items: flex-start; margin: 0.8rem 0; }
|
||||
.qr-box img { border: 1px solid var(--line); border-radius: 10px; }
|
||||
.qr-box .secret {
|
||||
|
|
@ -220,6 +230,7 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<section id="auth">
|
||||
<form class="card" id="auth-form">
|
||||
<h1>Zappier Portal</h1>
|
||||
|
|
@ -256,7 +267,7 @@
|
|||
<div class="spacer"></div>
|
||||
<button id="logout">Sign out</button>
|
||||
</aside>
|
||||
<main>
|
||||
<main id="main">
|
||||
<section id="dashboard"></section>
|
||||
<section id="invoices" hidden></section>
|
||||
<section id="statement" hidden></section>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue