diff options
| author | Claude Sonnet 5 <noreply@anthropic.com> | 2026-07-04 08:50:46 +0000 |
|---|---|---|
| committer | Claude Sonnet 5 <noreply@anthropic.com> | 2026-07-04 08:50:46 +0000 |
| commit | f8ae821240f33d615a9e91cdfeb6c026b7970782 (patch) | |
| tree | 458ce067db47a92248f163c26ce5fcf71299f3b0 /web/style.css | |
| parent | d105eca610b0e737c3313e4978d6a917b4f55d10 (diff) | |
feat(web,api): add Budget/Roles dashboard -- final phase of the harness redesign (Phase 9b)
Two new tabs, matching Phase 9a's conventions: Budget (per-provider spend
meters, escalation funnel, spend-over-time) and Roles (version history,
draft activation, readable escalation-ladder view) -- the human-facing
surface for the token-husbanding value proposition and the Phase 5/8
versioned role-config system.
New backend (minimal, additive, matching existing endpoint conventions --
unauthenticated like /api/budget and /api/roles/*):
- internal/storage/dashboard.go: QueryEscalationFunnel (executions grouped
by escalation_rung + agent, count + cost) and QuerySpendTimeseries (cost
per provider bucketed hourly/daily, mirroring QueryDashboardStats'
existing bucketing expressions). Documented honestly: rung 0 is not
exclusively "resolved locally" -- escalation_rung defaults to 0
uniformly, so non-role-typed executions (which never climb a ladder)
land there too, alongside role-typed tasks genuinely resolved at tier 0.
A role-only variant would need a join against tasks.config_json with no
queryable role column on executions -- intentionally out of scope.
- internal/api/dashboard.go: GET /api/escalation-funnel, GET
/api/spend-timeseries (both ?window=5h|24h|7d|<duration>, default 24h).
- internal/storage.ListRoleNames + GET /api/roles: the "which roles exist"
gap -- there was no way to discover role names before this, only to list
versions for a role you already knew the name of.
Chart-form decisions (dataviz skill, invoked before writing chart code):
horizontal stacked bar for the escalation funnel (rung order already
encodes the funnel shape positionally; color only needed for per-provider
segments within each rung); multi-line for spend-over-time; a fixed-order
categorical palette from the skill's validated palette.md slots for
provider identity (re-validated against this app's dark surface, passing);
a separate status (good/warning/critical) palette for budget meters,
deliberately distinct from both --state-* and the provider palette. Caught
and fixed a real bug during visual QA: converging near-zero end-labels on
the spend chart were overlapping (an anti-pattern the skill explicitly
flags) -- fixed with a 14px minimum-gap check before direct-labeling an
endpoint, leaning on the legend/tooltip otherwise.
Verified with a real running server, real seeded data (65 executions across
rungs 0-2 with a realistic provider mix, 2 roles with active/draft/retired
role_configs versions written directly via internal/storage), and a real
headless-browser session (reusing Phase 9a's Chromium/proxy scaffinding):
confirmed correct rung totals/percentages, provider legends, a 3-line spend
chart, live window-selector re-render, correct active-version highlighting
on the role panel, and a real Activate click on a draft version -- verified
via both DOM re-render and a direct backend GET that it truly persisted.
go build/vet/test -race -count=1 all pass, full suite. node --test
web/test/*.mjs: 291/291 passing (16 new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
Diffstat (limited to 'web/style.css')
| -rw-r--r-- | web/style.css | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/web/style.css b/web/style.css index b11faf6..a5fefb7 100644 --- a/web/style.css +++ b/web/style.css @@ -16,6 +16,14 @@ --text: #e2e8f0; --text-muted: #94a3b8; --accent: #38bdf8; + + /* Approximate solid chart surface (this app has no opaque surface token — + --surface/--bg are translucent over the rotating background image). + Used as the SVG chart background, unlabeled figure, and marker rings + (see marks-and-anatomy.md's "surface ring" spec) for the Phase 9b + budget/escalation charts; the dataviz palette validator was run against + this exact hex. */ + --chart-surface: #0f172a; } /* Reset */ @@ -2408,3 +2416,294 @@ dialog label select:focus { border-radius: 0; } } + +/* ── Budget & Escalation dashboard (Phase 9b) ────────────────────────────── + Provider identity uses a fixed categorical palette (PROVIDER_COLORS in + app.js), deliberately distinct from --state-* — see app.js's doc comment + for why and how it was validated. ── */ + +.provider-legend { + display: flex; + flex-wrap: wrap; + gap: 0.4rem 1rem; + margin-top: 0.6rem; + font-size: 0.75rem; + color: var(--text-muted); +} + +.provider-legend-item { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.provider-legend-swatch { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 2px; + flex-shrink: 0; +} + +/* Budget meters */ +.budget-meters { + display: flex; + flex-direction: column; + gap: 0.85rem; +} + +.budget-meter-row { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.budget-meter-label { + display: flex; + justify-content: space-between; + font-size: 0.82rem; +} + +.budget-meter-label > span:first-child { + font-weight: 600; + text-transform: capitalize; +} + +.budget-meter-value { + color: var(--text-muted); + font-size: 0.78rem; +} + +.budget-meter-track { + height: 10px; + border-radius: 999px; + background: var(--border); + overflow: hidden; +} + +.budget-meter-fill { + height: 100%; + border-radius: 999px; + transition: width 0.2s; +} + +/* Window selector shared by the funnel + spend-over-time charts */ +.dashboard-window-row { + margin: 0.5rem 0 1.5rem; + font-size: 0.85rem; + color: var(--text-muted); +} + +.dashboard-window-select { + margin-left: 0.35rem; +} + +.funnel-note { + margin-bottom: 0.75rem; + max-width: 60ch; +} + +/* Escalation funnel */ +.funnel-chart { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.funnel-row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.funnel-row-label { + width: 60px; + flex-shrink: 0; + font-size: 0.8rem; + font-weight: 600; + color: var(--text-muted); +} + +.funnel-track-outer { + flex: 1; + min-width: 0; +} + +.funnel-track { + display: flex; + height: 22px; + border-radius: 4px; + overflow: hidden; + min-width: 4px; +} + +.funnel-seg { + height: 100%; +} + +/* The 2px surface-color gap between stacked segments (see + marks-and-anatomy.md's "surface gap" spec) — border rather than margin so + it doesn't shrink the flex-grow proportions. */ +.funnel-seg + .funnel-seg { + border-left: 2px solid var(--chart-surface); +} + +.funnel-count { + width: 90px; + flex-shrink: 0; + text-align: right; + font-size: 0.78rem; + color: var(--text-muted); + white-space: nowrap; +} + +/* Spend-over-time line chart */ +.spend-chart-wrap { + margin-top: 0.5rem; +} + +.spend-chart-svg { + background: var(--chart-surface); + border-radius: 8px; + display: block; +} + +.spend-chart-axis { + stroke: var(--border); + stroke-width: 1; +} + +.spend-chart-tick { + fill: var(--text-muted); + font-size: 9px; +} + +.spend-chart-label { + fill: var(--text-muted); + font-size: 9px; +} + +.spend-chart-dot { + stroke: var(--chart-surface); + stroke-width: 2; +} + +/* Role/config management panel */ +.role-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1rem 1.25rem; + margin-bottom: 1rem; +} + +.role-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.role-card-header h3 { + font-size: 1rem; + font-weight: 700; + text-transform: capitalize; +} + +.role-active-badge { + font-size: 0.72rem; + font-weight: 600; + padding: 0.2em 0.6em; + border-radius: 999px; + background: var(--state-completed); + color: #0f172a; + white-space: nowrap; +} + +.role-active-badge--none { + background: var(--border); + color: var(--text-muted); +} + +.role-ladder-section { + margin-bottom: 0.85rem; +} + +.escalation-ladder-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.35rem; + margin-top: 0.35rem; +} + +.escalation-ladder-item { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.5rem; + font-size: 0.82rem; + padding: 0.35rem 0.6rem; + background: rgba(148, 163, 184, 0.08); + border-radius: 6px; +} + +.escalation-ladder-tier { + font-weight: 700; + min-width: 3.5rem; +} + +.escalation-ladder-candidates { + font-family: ui-monospace, monospace; + font-size: 0.78rem; +} + +.escalation-ladder-meta { + color: var(--text-muted); + font-size: 0.75rem; + margin-left: auto; +} + +.role-version-list { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.role-version-row { + display: flex; + align-items: center; + gap: 0.6rem; + font-size: 0.82rem; + padding: 0.4rem 0.1rem; + border-top: 1px solid var(--border); +} + +.role-version-num { + font-weight: 700; + min-width: 2.5rem; +} + +.role-version-status { + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 0.15em 0.5em; + border-radius: 999px; + color: #0f172a; +} + +.role-version-status--active { background: var(--state-completed); } +.role-version-status--draft { background: var(--state-queued); } +.role-version-status--retired { background: var(--border); color: var(--text-muted); } + +.role-version-meta { + color: var(--text-muted); + font-size: 0.75rem; +} + +.role-version-row .btn-primary.btn-sm { + margin-left: auto; +} |
