fix(adwright + plugin): font 404s + status humanization + compound refresh + poll deadline

Bundles four Tranche-A polish fixes that ship together:

1. **Font 404s** — CSS @font-face URLs were /extensions/fonts/* but the
   plugin loader serves static under /plugins/svrnty/*. Updated paths
   to match register_static prefix.

2. **Humanized status enum** — _humanStatus() maps proto canonical strings
   (CYCLE_STATUS_PREVIEW_READY, VARIANT_STATUS_ACTIVE, etc.) to
   user-facing labels ("Preview ready", "Active"). Applied to Cycles
   row, Overview timeline, and variant detail.

3. **Compound Overview refresh** — refresh-overview now dispatches both
   refresh-cycles AND list-recipes via _COMPOUND_ACTIONS map. Without
   this, KPI Recipes stayed at 0 until user visited Targeting tab.
   Added per-action user-facing toast labels (_ACTION_LABELS) so the
   user sees "Loading recipes…" instead of just hermes-webui's raw
   "Queued: /adwright list-recipes" banner.

4. **Poll deadline race** — compound actions fired two sub-actions
   back-to-back; sub1's poll response was nulling pendingTool before
   sub2 could be ingested. _pollOnce now (a) resets deadline on each
   successful update (sign of progress) and (b) only stops polling
   when the response matches the CURRENT pendingTool (not the captured
   local var from earlier in the recursive fire).

Verified live: Overview now shows Cycles=1, Recipes=10, Spend=$0/$200,
timeline=Cycle #1 — Preview ready. Console clean (0 errors, was 3
font 404s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Svrnty 2026-05-24 17:47:56 -04:00
parent 91c134c309
commit 9ce6bfc08f
2 changed files with 61 additions and 10 deletions

View File

@ -314,7 +314,7 @@
'<div class="svrnty-aw-timeline-time">' + _esc(c.started_at || "") + '</div>' + '<div class="svrnty-aw-timeline-time">' + _esc(c.started_at || "") + '</div>' +
'<div class="svrnty-aw-timeline-text">' + '<div class="svrnty-aw-timeline-text">' +
_esc(c.title || ("Cycle #" + (c.id || "?"))) + _esc(c.title || ("Cycle #" + (c.id || "?"))) +
' — <span style="color:var(--muted)">' + _esc(c.status || "") + '</span>' + ' — <span style="color:var(--muted)">' + _esc(_humanStatus(c.status)) + '</span>' +
'</div>' + '</div>' +
'</div>' '</div>'
).join("") + ).join("") +
@ -329,6 +329,23 @@
function _cycleSpend(x) { function _cycleSpend(x) {
return parseFloat(x.spend || 0) || 0; return parseFloat(x.spend || 0) || 0;
} }
// Backend serializes enum as canonical proto string ("CYCLE_STATUS_PREVIEW_READY",
// "VARIANT_STATUS_ACTIVE", etc.). Drop the prefix + title-case the rest.
const _STATUS_LABELS = {
"CYCLE_STATUS_ANALYZING": "Analyzing",
"CYCLE_STATUS_PREVIEW_READY": "Preview ready",
"CYCLE_STATUS_RUNNING": "Running",
"CYCLE_STATUS_COMPLETE": "Complete",
"CYCLE_STATUS_FAILED": "Failed",
"VARIANT_STATUS_PREVIEW_READY": "Preview ready",
"VARIANT_STATUS_ACTIVE": "Active",
"VARIANT_STATUS_PAUSED": "Paused",
"VARIANT_STATUS_COMPLETE": "Complete",
};
function _humanStatus(s) {
if (!s) return "";
return _STATUS_LABELS[s] || s;
}
function _deriveKpis(cycles, recipes) { function _deriveKpis(cycles, recipes) {
const c = cycles || []; const c = cycles || [];
const r = recipes || []; const r = recipes || [];
@ -381,7 +398,7 @@
'<div class="svrnty-aw-row" data-svrnty-aw-cycle="' + _attr(c.id) + '">' + '<div class="svrnty-aw-row" data-svrnty-aw-cycle="' + _attr(c.id) + '">' +
'<div class="svrnty-aw-row-main">' + '<div class="svrnty-aw-row-main">' +
'<div class="svrnty-aw-row-title">' + _esc(c.title || ("Cycle #" + c.id)) + '</div>' + '<div class="svrnty-aw-row-title">' + _esc(c.title || ("Cycle #" + c.id)) + '</div>' +
'<div class="svrnty-aw-row-sub">' + _esc(c.status || "") + ' · started ' + _esc(c.started_at || "") + '</div>' + '<div class="svrnty-aw-row-sub">' + _esc(_humanStatus(c.status)) + ' · started ' + _esc(c.started_at || "") + '</div>' +
'</div>' + '</div>' +
'<div class="svrnty-aw-row-meta">$' + _fmt(_cycleSpend(c)) + ' / $' + _fmt(_cycleBudget(c)) + '</div>' + '<div class="svrnty-aw-row-meta">$' + _fmt(_cycleSpend(c)) + ' / $' + _fmt(_cycleBudget(c)) + '</div>' +
'</div>' '</div>'
@ -412,7 +429,7 @@
variants.map((v) => variants.map((v) =>
'<div style="display:flex;justify-content:space-between;font-size:12px">' + '<div style="display:flex;justify-content:space-between;font-size:12px">' +
'<span>' + _esc(v.name || v.id || "variant") + '</span>' + '<span>' + _esc(v.name || v.id || "variant") + '</span>' +
'<span style="color:var(--muted)">' + _esc(v.status || "") + ' · imp ' + _fmt(v.impressions || 0) + '</span>' + '<span style="color:var(--muted)">' + _esc(_humanStatus(v.status)) + ' · imp ' + _fmt(v.impressions || 0) + '</span>' +
'</div>' '</div>'
).join("") + ).join("") +
'</div>'; '</div>';
@ -612,12 +629,34 @@
} }
_fireAction(action); _fireAction(action);
} }
// User-facing label for each action — shown via our toast so the user sees
// "Refreshing Overview" rather than the raw `/adwright refresh-cycles` slash
// command that hermes-webui's native Queued banner displays underneath.
const _ACTION_LABELS = {
"refresh-overview": "Refreshing Overview…",
"refresh-cycles": "Refreshing cycles…",
"get-cycle": "Loading cycle…",
"list-segments": "Loading segments…",
"list-recipes": "Loading recipes…",
"connections-status": "Checking connections…",
};
// Compound actions — one user gesture, multiple underlying tool calls.
// Overview KPIs need both cycles (for Cycles count + Spend + timeline)
// AND recipes (for Recipes count). Fire both so KPIs stay coherent.
const _COMPOUND_ACTIONS = {
"refresh-overview": ["refresh-cycles", "list-recipes"],
};
function _fireAction(action, args) { function _fireAction(action, args) {
if (!_isCmoActive()) { if (!_isCmoActive()) {
_toast("Switch to CMO profile to run /adwright commands.", "error"); _toast("Switch to CMO profile to run /adwright commands.", "error");
return; return;
} }
args = args || {}; args = args || {};
if (_COMPOUND_ACTIONS[action]) {
if (_ACTION_LABELS[action]) _toast(_ACTION_LABELS[action]);
_COMPOUND_ACTIONS[action].forEach((sub) => _fireAction(sub, args));
return;
}
const cmd = _commandFor(action, args); const cmd = _commandFor(action, args);
if (!cmd) return; if (!cmd) return;
// Dispatch event for any external listeners (audit / tests). // Dispatch event for any external listeners (audit / tests).
@ -630,6 +669,7 @@
_toast("Couldn't post to chat — is a session open?", "error"); _toast("Couldn't post to chat — is a session open?", "error");
return; return;
} }
if (_ACTION_LABELS[action]) _toast(_ACTION_LABELS[action]);
_startPolling(); _startPolling();
} }
function _commandFor(action, args) { function _commandFor(action, args) {
@ -670,8 +710,12 @@
// ── Polling: /api/adwright/last-panel-update ────────────────────────── // ── Polling: /api/adwright/last-panel-update ──────────────────────────
function _startPolling() { function _startPolling() {
if (NS.state.pollTimer) return; // Reset deadline on every call so back-to-back sub-actions (compound
// refresh-overview = refresh-cycles + list-recipes) each get a fresh
// POLL_MAX_MS window — CMO processes chat messages sequentially and
// each tool can take ~25s.
NS.state.pollStartedAt = Date.now(); NS.state.pollStartedAt = Date.now();
if (NS.state.pollTimer) return;
NS.state.pollTimer = setInterval(_pollOnce, POLL_INTERVAL_MS); NS.state.pollTimer = setInterval(_pollOnce, POLL_INTERVAL_MS);
// Run once immediately so we don't wait the first 2s. // Run once immediately so we don't wait the first 2s.
_pollOnce(); _pollOnce();
@ -699,9 +743,16 @@
const u = r.update; const u = r.update;
if (u.ts && u.ts <= NS.state.lastSeenTs) return; if (u.ts && u.ts <= NS.state.lastSeenTs) return;
NS.state.lastSeenTs = u.ts || Date.now(); NS.state.lastSeenTs = u.ts || Date.now();
// Sign of life — reset deadline. Lets compound actions (two CMO
// calls back-to-back, ~50s total) finish even when each sub-action
// shares one POLL_MAX_MS window with its sibling.
NS.state.pollStartedAt = Date.now();
_ingestUpdate(u); _ingestUpdate(u);
// Got our update — stop polling, render. // Got our update — stop polling only when the response matches the
if (u.tool === tool || !tool) { // CURRENT pendingTool. Compound actions overwrite pendingTool after
// sub1's fetch is already in flight; if sub1's response arrives we
// ingest it but keep polling for sub2.
if (NS.state.pendingTool && u.tool === NS.state.pendingTool) {
NS.state.pendingTool = null; NS.state.pendingTool = null;
_stopPolling(); _stopPolling();
} }

View File

@ -10,10 +10,10 @@
============================================================================ */ ============================================================================ */
/* ── Montserrat (self-hosted, sovereign — no external CDN; font-src 'self') ── */ /* ── Montserrat (self-hosted, sovereign — no external CDN; font-src 'self') ── */
@font-face{font-family:"Montserrat";font-style:normal;font-weight:400;font-display:swap;src:url("/extensions/fonts/montserrat-400.woff2") format("woff2");} @font-face{font-family:"Montserrat";font-style:normal;font-weight:400;font-display:swap;src:url("/plugins/svrnty/fonts/montserrat-400.woff2") format("woff2");}
@font-face{font-family:"Montserrat";font-style:normal;font-weight:500;font-display:swap;src:url("/extensions/fonts/montserrat-500.woff2") format("woff2");} @font-face{font-family:"Montserrat";font-style:normal;font-weight:500;font-display:swap;src:url("/plugins/svrnty/fonts/montserrat-500.woff2") format("woff2");}
@font-face{font-family:"Montserrat";font-style:normal;font-weight:600;font-display:swap;src:url("/extensions/fonts/montserrat-600.woff2") format("woff2");} @font-face{font-family:"Montserrat";font-style:normal;font-weight:600;font-display:swap;src:url("/plugins/svrnty/fonts/montserrat-600.woff2") format("woff2");}
@font-face{font-family:"Montserrat";font-style:normal;font-weight:700;font-display:swap;src:url("/extensions/fonts/montserrat-700.woff2") format("woff2");} @font-face{font-family:"Montserrat";font-style:normal;font-weight:700;font-display:swap;src:url("/plugins/svrnty/fonts/montserrat-700.woff2") format("woff2");}
/* ── Light (svrnty *.light) ─────────────────────────────────────────────── */ /* ── Light (svrnty *.light) ─────────────────────────────────────────────── */
:root { :root {