From 559e657b9032f6c18001f2e70f736e6e64162efd Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 11 May 2026 15:18:09 -0500 Subject: [PATCH] =?UTF-8?q?v0.2.0:28=20=E2=80=94=20Settings=20polish,=20op?= =?UTF-8?q?erator-name=20fix,=20Hide-revoked=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small admin-UI fixes: - Settings page intro card removed. The preamble was redundant with the page title + section headers. - Operator-name save no longer 404s. The JS was POSTing to /v1/admin/operator-name; the daemon mounts the endpoint at /v1/admin/settings/operator-name. Fixed both GET and POST paths. - Licenses page: pill toggle "Hide revoked" between the product filter row and stat cards. Filters rendered rows; stat cards still show the true revoked count so operators don't lose visibility. UI-only; no schema, API, or SDK change. Co-Authored-By: Claude Opus 4.7 (1M context) --- licensing-service/web/index.html | 46 ++++++++++++++++++++++++++------ startos/versions/v0.2.0.ts | 14 +++++++++- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/licensing-service/web/index.html b/licensing-service/web/index.html index 65ba4bb..04f93a3 100644 --- a/licensing-service/web/index.html +++ b/licensing-service/web/index.html @@ -4513,12 +4513,40 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } let products = [] // for product-name lookup + grouping let allLicenses = [] // last fetched set let currentProductFilter = '' // empty = all products + let hideRevoked = false // toggle: hide revoked rows from table (stats unaffected) let lastQuery = '' let lastQueryField = 'email' const productPillRow = el('div', { - style: 'display:flex; gap:8px; flex-wrap:wrap; margin:14px 0', + style: 'display:flex; gap:8px; flex-wrap:wrap; margin:14px 0 4px', }) + // Status-filter row: a single "Hide revoked" toggle for now. Stats + // cards below still show the revoked count so operators don't lose + // visibility — this only filters which rows render in the table. + const statusFilterRow = el('div', { + style: 'display:flex; gap:8px; flex-wrap:wrap; margin:0 0 14px; ' + + 'font-size:12px; color:var(--ink-500);', + }) + const hideRevokedBtn = el('button', { + type: 'button', + style: 'font-size:12px; padding:4px 12px; border-radius:999px; cursor:pointer; ' + + 'font-family:var(--font-body); font-weight:500; transition:all 100ms; ' + + 'background:transparent; color:var(--ink-700); border:1px solid var(--border-2);', + }, 'Hide revoked') + hideRevokedBtn.addEventListener('click', () => { + hideRevoked = !hideRevoked + if (hideRevoked) { + hideRevokedBtn.style.background = 'var(--navy-800)' + hideRevokedBtn.style.color = 'var(--cream-50)' + hideRevokedBtn.style.borderColor = 'var(--navy-800)' + } else { + hideRevokedBtn.style.background = 'transparent' + hideRevokedBtn.style.color = 'var(--ink-700)' + hideRevokedBtn.style.borderColor = 'var(--border-2)' + } + render() + }) + statusFilterRow.appendChild(hideRevokedBtn) const statsRow = el('div', { class: 'stats', style: 'margin:0 0 14px' }) const tableHolder = el('div') @@ -4717,6 +4745,11 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } tableHolder.innerHTML = '' let scoped = allLicenses if (currentProductFilter) scoped = scoped.filter((l) => l.product_id === currentProductFilter) + // Hide-revoked toggle: applied to TABLE rows only. Stats below + // still show the full revoked count from `scoped` (renderStats + // ignores hideRevoked deliberately so the operator never loses + // visibility into how many revoked licenses exist). + if (hideRevoked) scoped = scoped.filter((l) => l.status !== 'revoked') // Single-product or product-filtered: flat table. const inSearchMode = lastQuery.length > 0 @@ -4964,6 +4997,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } issueDisclosure, ])) target.appendChild(productPillRow) + target.appendChild(statusFilterRow) target.appendChild(statsRow) target.appendChild(tableHolder) buildIssueForm() @@ -5442,12 +5476,8 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } routes.settings = async function () { const target = document.getElementById('route-target') target.innerHTML = '' - target.appendChild(plainCard([ - el('p', { class: 'muted', style: 'margin:0' }, - 'Operator-facing configuration. Display name, payment provider connections, and scoped API keys for agent / automation access.'), - ])) - const opNameHost = el('div', { style: 'margin-top:18px' }) + const opNameHost = el('div') target.appendChild(opNameHost) renderOperatorNameCard(opNameHost) @@ -5464,7 +5494,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } host.innerHTML = '' let stored = '', effective = '', fallbackEnv = '' try { - const r = await api('/v1/admin/operator-name').catch(() => null) + const r = await api('/v1/admin/settings/operator-name').catch(() => null) if (r) { stored = r.stored || '' effective = r.effective || '' @@ -5483,7 +5513,7 @@ hr.div { border:none; border-top:1px solid var(--border-1); margin:18px 0; } saveBtn.disabled = true status.textContent = 'Saving…' try { - await api('/v1/admin/operator-name', { + await api('/v1/admin/settings/operator-name', { method: 'POST', body: { name: input.value.trim() }, }) diff --git a/startos/versions/v0.2.0.ts b/startos/versions/v0.2.0.ts index 769968c..27c8dc3 100644 --- a/startos/versions/v0.2.0.ts +++ b/startos/versions/v0.2.0.ts @@ -58,6 +58,18 @@ const RELEASE_NOTES = [ // in RELEASE_NOTES above (the milestone). Subsequent revisions // append here. const ROUTINE_NOTES = [ + '0.2.0:28 — **Settings cleanup, operator-name save fix, Licenses "Hide revoked" toggle.** Three small admin-UI changes.', + '', + '**Settings page intro card removed.** The "Operator-facing configuration. Display name, payment provider connections, …" preamble at the top of the Settings page was redundant with the page title + the section headers below; dropping it tightens the layout and gets the operator straight to the controls.', + '', + '**Operator name save no longer 404s.** The admin UI was POSTing to `/v1/admin/operator-name`, but the daemon mounts the endpoint at `/v1/admin/settings/operator-name`. Mismatch fixed in the JS — both GET (page load) and POST (save) now hit the correct path. The route on the daemon side was already correct; this was a UI typo from the original wiring.', + '', + '**Licenses page: "Hide revoked" toggle.** A pill toggle sits between the product filter row and the stat cards. Off (default) = table shows every license. On = table filters out `status = "revoked"` rows so operators reviewing active state aren\'t scrolling past revoked entries. The Revoked stat card stays accurate either way — the toggle only affects rendered rows, never the stat counts. Per-product grouping, search, and product-filter pills all still work alongside it.', + '', + '**Test count: 87** (unchanged — UI-only release).', + '', + '**Upgrade path.** v0.2.0:27 → v0.2.0:28 is a drop-in. No data, schema, or API change. Existing operator-name settings in the DB are untouched.', + '', '0.2.0:27 — **Tier-card feature list merged into a single ul; "MOST POPULAR" + "Limited: …" no longer collide.** Two remaining buy-page nits from :26.', '', '**Single feature list.** The :26 attempt at zeroing the gap between adjacent `` and `` worked on paper but the boundary was still visible to the eye — probably from list-item bottom-padding accumulating around the ul switch. Solved structurally: the daemon now builds ONE `
    ` server-side, concatenating marketing bullets and entitlements in the operator-controlled order. No list boundary = no gap to fight with CSS. Both groups render with the same ✓ checkmark, indistinguishable to the buyer.', @@ -430,7 +442,7 @@ const ROUTINE_NOTES = [ ].join('\n\n') export const v0_2_0 = VersionInfo.of({ - version: '0.2.0:27', + version: '0.2.0:28', releaseNotes: { en_US: ROUTINE_NOTES }, // No on-disk transformation needed — v0.2.0:0 is a label change. // SQLite-level migrations live separately under