diff --git a/static/adwright.js b/static/adwright.js index b00659a..404ad90 100644 --- a/static/adwright.js +++ b/static/adwright.js @@ -314,7 +314,7 @@ '
' + _esc(c.started_at || "") + '
' + '
' + _esc(c.title || ("Cycle #" + (c.id || "?"))) + - ' — ' + _esc(c.status || "") + '' + + ' — ' + _esc(_humanStatus(c.status)) + '' + '
' + '' ).join("") + @@ -329,6 +329,23 @@ function _cycleSpend(x) { 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) { const c = cycles || []; const r = recipes || []; @@ -381,7 +398,7 @@ '
' + '
' + '
' + _esc(c.title || ("Cycle #" + c.id)) + '
' + - '
' + _esc(c.status || "") + ' · started ' + _esc(c.started_at || "") + '
' + + '
' + _esc(_humanStatus(c.status)) + ' · started ' + _esc(c.started_at || "") + '
' + '
' + '
$' + _fmt(_cycleSpend(c)) + ' / $' + _fmt(_cycleBudget(c)) + '
' + '
' @@ -412,7 +429,7 @@ variants.map((v) => '
' + '' + _esc(v.name || v.id || "variant") + '' + - '' + _esc(v.status || "") + ' · imp ' + _fmt(v.impressions || 0) + '' + + '' + _esc(_humanStatus(v.status)) + ' · imp ' + _fmt(v.impressions || 0) + '' + '
' ).join("") + ''; @@ -612,12 +629,34 @@ } _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) { if (!_isCmoActive()) { _toast("Switch to CMO profile to run /adwright commands.", "error"); return; } 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); if (!cmd) return; // Dispatch event for any external listeners (audit / tests). @@ -630,6 +669,7 @@ _toast("Couldn't post to chat — is a session open?", "error"); return; } + if (_ACTION_LABELS[action]) _toast(_ACTION_LABELS[action]); _startPolling(); } function _commandFor(action, args) { @@ -670,8 +710,12 @@ // ── Polling: /api/adwright/last-panel-update ────────────────────────── 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(); + if (NS.state.pollTimer) return; NS.state.pollTimer = setInterval(_pollOnce, POLL_INTERVAL_MS); // Run once immediately so we don't wait the first 2s. _pollOnce(); @@ -699,9 +743,16 @@ const u = r.update; if (u.ts && u.ts <= NS.state.lastSeenTs) return; 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); - // Got our update — stop polling, render. - if (u.tool === tool || !tool) { + // Got our update — stop polling only when the response matches the + // 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; _stopPolling(); } diff --git a/static/app.css b/static/app.css index 3cd1368..64c43a3 100644 --- a/static/app.css +++ b/static/app.css @@ -10,10 +10,10 @@ ============================================================================ */ /* ── 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:500;font-display:swap;src:url("/extensions/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:700;font-display:swap;src:url("/extensions/fonts/montserrat-700.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("/plugins/svrnty/fonts/montserrat-500.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("/plugins/svrnty/fonts/montserrat-700.woff2") format("woff2");} /* ── Light (svrnty *.light) ─────────────────────────────────────────────── */ :root {