summaryrefslogtreecommitdiff
path: root/web/style.css
diff options
context:
space:
mode:
authorClaude Sonnet 5 <noreply@anthropic.com>2026-07-04 08:50:46 +0000
committerClaude Sonnet 5 <noreply@anthropic.com>2026-07-04 08:50:46 +0000
commitf8ae821240f33d615a9e91cdfeb6c026b7970782 (patch)
tree458ce067db47a92248f163c26ce5fcf71299f3b0 /web/style.css
parentd105eca610b0e737c3313e4978d6a917b4f55d10 (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.css299
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;
+}