Frontend now drives Impressions / CTR / Spend from real per-variant
metrics (cycle_metric) rather than hardcoded zeros + fake +12% / -0.3%
deltas. Adds state.data.metricsByCycle keyed by cycle id; populated
via _fetchCycleMetrics which hits the panel-update endpoint directly
(no CMO chat dispatch needed — the route calls gRPC live per request).
After every cycles load (refresh-cycles or list-cycles ingest), fans
out one metrics fetch per cycle. _deriveKpis flattens all metrics
arrays, sums impressions/clicks/spend, computes real CTR from
clicks/impressions. Spend bar now reflects $spend / $budget_total
from real values instead of falling back to the $6,000 default floor.
Plugin route _real_payload_for gains a new branch:
adwright_get_cycle_metrics?cycle_id=N → core.get_cycle_metrics_tool(N).
Fake deltas dropped — real deltas need a previous-window snapshot
the backend doesn't supply yet.
Verified live: metricsByCycle.1 populated with backend's [{variant_id:1,
impressions:0, ...}] record. All metric values still zero in this
sandbox because no ads have dispatched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Backend AdwrightCore.list_cycles preserves proto snake_case via
MessageToDict(preserving_proto_field_name=True), so cycle JSON carries
`budget_total` (decimal string like "200.00"), not `budget`. Frontend
read c.budget which was always undefined → fell back to 0 in both the
Cycles row meta and the Overview spend bar (which then defaulted to
$6,000 budget per the `|| 6000` floor in _deriveKpis).
Added _cycleBudget + _cycleSpend helpers that parseFloat the decimal
strings, with c.budget fallback so any future renamed field still works.
Spend stays 0 until GetCycleMetrics is wired in — it doesn't live in
ListCycles.
Verified live: Cycles row now shows $0 / $200 (was $0 / $0). Overview
spend now shows $0 / $200 (was $0 / $6,000).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Adwright ↗BTE button called SvrntyBTE.open() directly, which sets
overlay.hidden=false but never flips main.svrnty-showing-bte. The CSS
gates BTE overlay visibility on that class, so the click did nothing
visible — overlay was mounted but display:none. Route the click through
window.switchPanel("bte") instead so the canonical nav wrapper handles
class flipping + event dispatch identically to the sidebar BTE button.
Validated via Playwright with 6 gates: V1 sidebar BTE (1 event, class
set, visible), V2 ↗BTE crosslink (1 event, class flip, visible),
V3 Adwright nav (class flip, BTE hidden), V4 main classes correct,
V5 console clean, V6 /api/bte/proxy 200 OK. All pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty-state messages now differentiate "never fetched" (null) from
"fetched but empty" ([]), so the panel reads correctly after a refresh
returns zero rows. Affected renderers: _renderCycles, _renderAudience,
_renderConnections. Targeting unchanged (its gate is "both lists needed",
not single-list empty).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: cycleCount: String(c.length || '—') turned 0 into '—' due to || falsy
coercion. Plan B DB starts empty → Overview KPIs all '—' even after a
successful refresh. Renders '0' now (the actually correct truthful value).
Karpathy 4 rules: smallest change (one expression), no shape change.
Bug: panel _ingestUpdate expected payload.connections array (mock shape).
Real adwright_get_connections_status returns {meta:{...}, woo:{...}}.
Resulted in connections tab always empty + Overview KPIs sourced from
empty arrays even though backend was returning real data.
Now ingest normalises both shapes (real + mock fallback) into the
[{id, name, ok, status, detail}, ...] shape the renderer already expects.
detail field plumbed through for future Connections card expansion.
Verified via curl /api/adwright/last-panel-update?tool=adwright_get_connections_status
returns real Plan B sandbox + Woo. Panel now displays them.
Karpathy 4 rules: surfaced shape mismatch via real backend probe, smallest
shim (one ingest branch), supports both old + new shape (no regression).
JP feedback: panel says "switch to cmo profile" but offers no way to do so.
Also window.S.activeProfile may be empty on /session/* pages before boot.js
finishes initializing, so the banner showed unnecessarily.
This patch:
- Replaces vague text with a [Switch to CMO] button that POSTs to
/api/profile/switch {name: "cmo-planb"} and reloads on success
- Adds _fetchActiveProfile() that reads /api/profile/active directly
(defense against window.S race); cached in NS.state._fetchedProfile
- Background poll every 5s catches profile switches made from the upstream
Profiles panel — no hard reload needed to clear the banner
- Disabled-state refresh fires once on mount + on each poll
Karpathy 4 rules: smallest possible change (one button + one fetch helper),
no abstraction layer, no fallback "smart" detection — authoritative API only.
Header now shows ↗ BTE button next to the profile status pill. Click
invokes window.SvrntyBTE.open() to surface the BTE overlay, satisfying
the "Adwright pulls content from BTE panel" integration goal at the UX
level. Asset-URL-level integration follows automatically once cycles
contain BTE-rendered asset references (post Phase 8).
Themed via existing CSS vars (--accent, --border2, --accent-bg, etc) —
zero hardcoded colors. CONNECTION-MAP regenerated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>