fix(dashboard): light-mode backgrounds + aligned savings tables (#1064)

## Description

Several dashboard cards and tables hardcoded a dark background
(`bg-[#141414]`) that ignored the light theme. The most visible case was
the **Agent Usage** section, which rendered black in light mode while
every other surface flipped correctly. This converts those hardcoded
backgrounds to the theme-aware `bg-card-alt` utility so the whole
dashboard responds to the active theme, and tightens table column
alignment.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Replace hardcoded `bg-[#141414]` with the `bg-card-alt` utility
(`var(--card-alt-bg)`: `#f3f4f6` light / `#141414` dark) across Agent
Usage cards, the request-count badge, and per-agent rows. 0 hardcoded
darks remain.
- Per-model and per-project savings tables: `table-layout:fixed` +
explicit `<col>` widths for stable columns.
- Recent Requests: switch table markup to a CSS grid so columns align
regardless of content length.
- Add a per-project savings section (progress bar, Last Active column,
empty state).
- Single file changed: `headroom/dashboard/templates/dashboard.html`.

## Testing

- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ .venv/Scripts/python.exe -m pytest tests/test_dashboard_agent_usage.py -q
======================== 13 passed, 1 warning in 0.89s ========================

$ grep -c "bg-\[#141414\]" headroom/dashboard/templates/dashboard.html
0          # zero hardcoded dark backgrounds remain

$ grep -c "bg-card-alt" headroom/dashboard/templates/dashboard.html
8          # all converted to the theme-aware utility
```

## Real Behavior Proof

- Environment: Windows 11, headroom proxy 0.26.0 serving `/dashboard`
(template read per-request from disk, no cache).
- Exact command / steps: start proxy, open
`http://127.0.0.1:8787/dashboard`, toggle light mode, inspect the Agent
Usage section.
- Observed result: Agent Usage cards (Before/After/Saved/Savings +
per-agent rows) now use the light surface (`#f3f4f6`) in light mode and
`#141414` in dark mode, matching the rest of the dashboard. Confirmed
visually on refresh; `grep` confirms 0 hardcoded darks remain.
- Not tested: `ruff`/`mypy` (HTML template, no Python changed); no
automated visual-regression test added.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

Presentation-only change to a single HTML template; no Python touched,
so `ruff`/`mypy` are N/A. No visual-regression test added (none exists
for the dashboard); existing `test_dashboard_agent_usage.py` data tests
still pass. CHANGELOG not updated (UI fix).
This commit is contained in:
r00t 2026-06-17 22:13:29 +05:30 committed by GitHub
parent c98728363a
commit 5eae32ba47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -99,6 +99,9 @@
html:not(.dark) .text-cyan-200\/80 { color: #0e7490; }
html:not(.dark) .text-cyan-300\/75 { color: #0891b2; }
html:not(.dark) .text-violet-300\/75 { color: #7c3aed; }
/* Light mode: fix hardcoded dark card backgrounds */
.bg-card-alt { background: var(--card-alt-bg); }
.hover-bg-surface:hover { background: var(--color-surface); }
</style>
</head>
<body class="text-gray-200 min-h-screen" x-data="dashboard()" x-init="init()">
@ -324,26 +327,26 @@
</div>
<div class="flex flex-wrap items-center gap-3 text-xs">
<span class="text-gray-500" x-text="'Coverage: ' + agentCoverageLabel"></span>
<span class="px-2 py-0.5 rounded border border-border bg-[#141414] font-mono text-gray-400"
<span class="px-2 py-0.5 rounded border border-border bg-card-alt font-mono text-gray-400"
x-text="formatNumber(stats.agent_usage?.totals?.requests || 0) + ' requests'"></span>
</div>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-5">
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="rounded-lg border border-border bg-card-alt p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Before</div>
<div class="text-2xl font-light tabular-nums" x-text="formatNumber(stats.agent_usage?.totals?.before_tokens || 0)"></div>
</div>
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="rounded-lg border border-border bg-card-alt p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">After</div>
<div class="text-2xl font-light tabular-nums text-gray-200" x-text="formatNumber(stats.agent_usage?.totals?.after_tokens || 0)"></div>
</div>
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="rounded-lg border border-border bg-card-alt p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Saved</div>
<div class="text-2xl font-light tabular-nums text-accent" x-text="formatNumber(stats.agent_usage?.totals?.tokens_saved || 0)"></div>
</div>
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="rounded-lg border border-border bg-card-alt p-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Savings</div>
<div class="text-2xl font-light tabular-nums text-emerald-400" x-text="(stats.agent_usage?.totals?.savings_percent || 0).toFixed(1) + '%'"></div>
</div>
@ -352,7 +355,7 @@
<template x-if="agentRows.length > 0">
<div class="space-y-3">
<template x-for="agent in agentRows" :key="agent.agent">
<div class="rounded-lg border border-border bg-[#141414] p-3">
<div class="rounded-lg border border-border bg-card-alt p-3">
<div class="grid grid-cols-1 gap-3 lg:grid-cols-[minmax(160px,0.9fr)_minmax(260px,1.5fr)_minmax(260px,1.2fr)] lg:items-center">
<div class="min-w-0">
<div class="flex items-center gap-2">
@ -1076,10 +1079,17 @@
<span class="text-xs text-gray-500">Exact tokens saved per model</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<table class="w-full text-sm" style="table-layout:fixed">
<colgroup>
<col style="width:40%">
<col style="width:12%">
<col style="width:18%">
<col style="width:18%">
<col style="width:12%">
</colgroup>
<thead>
<tr class="text-left text-xs text-gray-500 uppercase tracking-wide">
<th class="px-4 py-3 font-medium">Model</th>
<tr class="text-xs text-gray-500 uppercase tracking-wide">
<th class="px-4 py-3 font-medium text-left">Model</th>
<th class="px-4 py-3 font-medium text-right">Requests</th>
<th class="px-4 py-3 font-medium text-right">Tokens Saved</th>
<th class="px-4 py-3 font-medium text-right">Tokens Sent</th>
@ -1089,14 +1099,14 @@
<tbody class="divide-y divide-border">
<template x-for="[model, info] in Object.entries(stats.cost?.per_model || {})" :key="model">
<tr class="hover:bg-border/30 transition-colors">
<td class="px-4 py-3">
<td class="px-4 py-3 text-left">
<span class="px-2 py-0.5 bg-border rounded text-xs" x-text="truncateModel(model)"></span>
</td>
<td class="px-4 py-3 text-right font-mono" x-text="info.requests"></td>
<td class="px-4 py-3 text-right font-mono text-accent" x-text="formatNumber(info.tokens_saved)"></td>
<td class="px-4 py-3 text-right font-mono" x-text="formatNumber(info.tokens_sent)"></td>
<td class="px-4 py-3 text-right font-mono tabular-nums" x-text="info.requests"></td>
<td class="px-4 py-3 text-right font-mono tabular-nums text-accent" x-text="formatNumber(info.tokens_saved)"></td>
<td class="px-4 py-3 text-right font-mono tabular-nums" x-text="formatNumber(info.tokens_sent)"></td>
<td class="px-4 py-3 text-right">
<span class="text-accent font-mono" x-text="info.reduction_pct.toFixed(1) + '%'"></span>
<span class="text-accent font-mono tabular-nums" x-text="info.reduction_pct.toFixed(1) + '%'"></span>
</td>
</tr>
</template>
@ -1107,42 +1117,78 @@
</template>
<!-- Per-Project Savings Breakdown -->
<template x-if="Object.keys(stats.savings?.per_project || {}).length > 0">
<div class="bg-surface rounded-lg border border-border overflow-hidden mb-6">
<div class="px-4 py-3 border-b border-border flex justify-between items-center">
<div class="bg-surface rounded-lg border border-border overflow-hidden mb-6">
<div class="px-4 py-3 border-b border-border flex justify-between items-center">
<div>
<span class="text-sm font-medium text-gray-300">Per-Project Savings</span>
<span class="text-xs text-gray-500">Lifetime totals &mdash; all time</span>
<div class="text-xs text-gray-500 mt-0.5">Lifetime totals — attributed requests only</div>
</div>
<span class="text-xs text-gray-500 font-mono"
x-text="Object.keys(stats.savings?.per_project || {}).length + ' project(s)'"></span>
</div>
<!-- Empty state: no project data yet -->
<template x-if="Object.keys(stats.savings?.per_project || {}).length === 0">
<div class="px-4 py-8 text-center">
<div class="text-xs text-gray-500 mb-3">No per-project data yet.</div>
<div class="text-xs text-gray-600 font-mono leading-relaxed">
Add to each project's <span class="text-gray-400">.claude/settings.local.json</span>:<br>
<span class="text-accent">ANTHROPIC_BASE_URL: http://127.0.0.1:8787/p/&lt;project-name&gt;</span>
</div>
</div>
</template>
<!-- Project rows -->
<template x-if="Object.keys(stats.savings?.per_project || {}).length > 0">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<table class="w-full text-sm" style="table-layout:fixed">
<colgroup>
<col style="width:25%">
<col style="width:10%">
<col style="width:15%">
<col style="width:12%">
<col style="width:18%">
<col style="width:20%">
</colgroup>
<thead>
<tr class="text-left text-xs text-gray-500 uppercase tracking-wide">
<th class="px-4 py-3 font-medium">Project</th>
<tr class="text-xs text-gray-500 uppercase tracking-wide">
<th class="px-4 py-3 font-medium text-left">Project</th>
<th class="px-4 py-3 font-medium text-right">Requests</th>
<th class="px-4 py-3 font-medium text-right">Tokens Saved</th>
<th class="px-4 py-3 font-medium text-right">Saved $</th>
<th class="px-4 py-3 font-medium text-right">Savings %</th>
<th class="px-4 py-3 font-medium text-right hidden md:table-cell">Last Active</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<template x-for="[project, info] in Object.entries(stats.savings?.per_project || {})" :key="project">
<tr class="hover:bg-border/30 transition-colors">
<td class="px-4 py-3">
<span class="px-2 py-0.5 bg-border rounded text-xs" x-text="project"></span>
<td class="px-4 py-3 text-left">
<span class="px-2 py-0.5 bg-border rounded text-xs font-mono" x-text="project"></span>
</td>
<td class="px-4 py-3 text-right font-mono" x-text="info.requests"></td>
<td class="px-4 py-3 text-right font-mono text-accent" x-text="formatNumber(info.tokens_saved)"></td>
<td class="px-4 py-3 text-right font-mono text-emerald-400" x-text="'$' + formatCurrency(info.compression_savings_usd || 0)"></td>
<td class="px-4 py-3 text-right font-mono tabular-nums" x-text="info.requests"></td>
<td class="px-4 py-3 text-right font-mono tabular-nums text-accent" x-text="formatNumber(info.tokens_saved)"></td>
<td class="px-4 py-3 text-right font-mono tabular-nums text-emerald-400"
x-text="'$' + formatCurrency(info.compression_savings_usd || 0)"></td>
<td class="px-4 py-3 text-right">
<span class="text-accent font-mono" x-text="(info.savings_percent || 0).toFixed(1) + '%'"></span>
<div class="flex items-center justify-end gap-2">
<div class="w-16 h-1.5 bg-border rounded-full overflow-hidden">
<div class="h-full bg-accent rounded-full"
:style="'width:' + Math.min(info.savings_percent || 0, 100) + '%'"></div>
</div>
<span class="text-accent font-mono tabular-nums text-xs w-10 text-right"
x-text="(info.savings_percent || 0).toFixed(1) + '%'"></span>
</div>
</td>
<td class="px-4 py-3 text-right text-xs text-gray-500 font-mono hidden md:table-cell"
x-text="info.last_activity_at ? new Date(info.last_activity_at).toLocaleString() : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</template>
</div>
<!-- Recent Requests Table (with expandable rows) -->
<div class="bg-surface rounded-lg border border-border overflow-hidden">
@ -1151,98 +1197,96 @@
<span class="text-xs text-gray-500">Last 10 &mdash; click row to expand</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-left text-xs text-gray-500 uppercase tracking-wide">
<th class="px-4 py-3 font-medium w-6"></th>
<th class="px-4 py-3 font-medium">Time</th>
<th class="px-4 py-3 font-medium">Model</th>
<th class="px-4 py-3 font-medium text-right">Input</th>
<th class="px-4 py-3 font-medium text-right">Output</th>
<th class="px-4 py-3 font-medium text-right">Saved</th>
<th class="px-4 py-3 font-medium text-right">Latency</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<div>
<!-- Header -->
<div class="grid text-xs text-gray-500 uppercase tracking-wide border-b border-border"
style="grid-template-columns: 2rem 12fr 22fr 15fr 12fr 10fr 14fr">
<div class="px-2 py-3"></div>
<div class="px-4 py-3 font-medium">Time</div>
<div class="px-4 py-3 font-medium">Model</div>
<div class="px-4 py-3 font-medium text-right">Input</div>
<div class="px-4 py-3 font-medium text-right">Output</div>
<div class="px-4 py-3 font-medium text-right">Saved</div>
<div class="px-4 py-3 font-medium text-right">Latency</div>
</div>
<!-- Data rows -->
<div class="divide-y divide-border">
<template x-for="req in (stats.recent_requests || [])" :key="req.request_id">
<tr>
<td colspan="7" class="p-0">
<div class="cursor-pointer" @click="toggleExpanded(req.request_id)">
<div class="flex hover:bg-border/30 transition-colors">
<div class="px-4 py-3 w-6 text-gray-500">
<span x-text="expandedRows[req.request_id] ? '-' : '+'"></span>
</div>
<div class="px-4 py-3 font-mono text-gray-400 flex-1" x-text="formatTime(req.timestamp)"></div>
<div class="px-4 py-3 flex-1">
<span class="px-2 py-0.5 bg-border rounded text-xs" x-text="truncateModel(req.model)"></span>
</div>
<div class="px-4 py-3 text-right font-mono flex-1" x-text="formatNumber(req.input_tokens_optimized)"></div>
<div class="px-4 py-3 text-right font-mono flex-1" x-text="formatNumber(req.output_tokens || 0)"></div>
<div class="px-4 py-3 text-right flex-1">
<span class="text-accent font-mono" x-text="req.savings_percent.toFixed(0) + '%'"></span>
</div>
<div class="px-4 py-3 text-right font-mono text-gray-400 flex-1" x-text="(req.total_latency_ms || 0).toFixed(0) + 'ms'"></div>
<div>
<div class="cursor-pointer" @click="toggleExpanded(req.request_id)">
<div class="grid hover:bg-border/30 transition-colors"
style="grid-template-columns: 2rem 12fr 22fr 15fr 12fr 10fr 14fr">
<div class="px-2 py-3 text-gray-500 flex items-center justify-center">
<span x-text="expandedRows[req.request_id] ? '-' : '+'"></span>
</div>
<div class="px-4 py-3 font-mono text-gray-400 truncate" x-text="formatTime(req.timestamp)"></div>
<div class="px-4 py-3 min-w-0">
<span class="px-2 py-0.5 bg-border rounded text-xs truncate" x-text="truncateModel(req.model)"></span>
</div>
<div class="px-4 py-3 text-right font-mono tabular-nums" x-text="formatNumber(req.input_tokens_optimized)"></div>
<div class="px-4 py-3 text-right font-mono tabular-nums" x-text="formatNumber(req.output_tokens || 0)"></div>
<div class="px-4 py-3 text-right">
<span class="text-accent font-mono tabular-nums" x-text="req.savings_percent.toFixed(0) + '%'"></span>
</div>
<div class="px-4 py-3 text-right font-mono tabular-nums text-gray-400" x-text="(req.total_latency_ms || 0).toFixed(0) + 'ms'"></div>
</div>
<!-- Expanded detail row -->
<template x-if="expandedRows[req.request_id]">
<div class="px-8 py-4 border-t border-border" style="background: var(--card-alt-bg);">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-xs">
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Original Tokens</div>
<div class="font-mono" x-text="formatNumber(req.input_tokens_original)"></div>
</div>
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Compressed Tokens</div>
<div class="font-mono" x-text="formatNumber(req.input_tokens_optimized)"></div>
</div>
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Tokens Removed</div>
<div class="font-mono text-accent" x-text="formatNumber(req.tokens_saved)"></div>
</div>
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Optimization Time</div>
<div class="font-mono" x-text="(req.optimization_latency_ms || 0).toFixed(0) + 'ms'"></div>
</div>
<!-- Expanded detail row -->
<template x-if="expandedRows[req.request_id]">
<div class="px-8 py-4 border-t border-border" style="background: var(--card-alt-bg);">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-xs">
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Original Tokens</div>
<div class="font-mono" x-text="formatNumber(req.input_tokens_original)"></div>
</div>
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Compressed Tokens</div>
<div class="font-mono" x-text="formatNumber(req.input_tokens_optimized)"></div>
</div>
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Tokens Removed</div>
<div class="font-mono text-accent" x-text="formatNumber(req.tokens_saved)"></div>
</div>
<div>
<div class="text-gray-500 uppercase tracking-wide mb-1">Optimization Time</div>
<div class="font-mono" x-text="(req.optimization_latency_ms || 0).toFixed(0) + 'ms'"></div>
</div>
</div>
<!-- Transforms Applied -->
<template x-if="(req.transforms_applied || []).length > 0">
<div class="mt-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Transforms Applied</div>
<div class="flex flex-wrap gap-1">
<template x-for="t in req.transforms_applied" :key="t">
<span class="px-2 py-0.5 bg-border rounded text-xs font-mono" x-text="t"></span>
</template>
</div>
</div>
<!-- Transforms Applied -->
<template x-if="(req.transforms_applied || []).length > 0">
<div class="mt-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Transforms Applied</div>
<div class="flex flex-wrap gap-1">
<template x-for="t in req.transforms_applied" :key="t">
<span class="px-2 py-0.5 bg-border rounded text-xs font-mono" x-text="t"></span>
</template>
</div>
</template>
<!-- Waste Signals for this request -->
<template x-if="req.waste_signals && Object.keys(req.waste_signals).length > 0">
<div class="mt-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Waste Detected</div>
<div class="flex flex-wrap gap-2">
<template x-for="[signal, tokens] in Object.entries(req.waste_signals).filter(([,v]) => v > 0)" :key="signal">
<span class="px-2 py-0.5 rounded text-xs font-mono"
:class="wasteSignalBadgeColor(signal)"
x-text="wasteSignalLabel(signal) + ': ' + formatNumber(tokens)"></span>
</template>
</div>
</template>
<!-- Waste Signals for this request -->
<template x-if="req.waste_signals && Object.keys(req.waste_signals).length > 0">
<div class="mt-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Waste Detected</div>
<div class="flex flex-wrap gap-2">
<template x-for="[signal, tokens] in Object.entries(req.waste_signals).filter(([,v]) => v > 0)" :key="signal">
<span class="px-2 py-0.5 rounded text-xs font-mono"
:class="wasteSignalBadgeColor(signal)"
x-text="wasteSignalLabel(signal) + ': ' + formatNumber(tokens)"></span>
</template>
</div>
</div>
</template>
</div>
</template>
</td>
</tr>
</div>
</template>
</div>
</template>
</div>
</template>
<template x-if="(stats.recent_requests || []).length === 0">
<tr>
<td colspan="8" class="px-4 py-8 text-center text-gray-500 italic">
No requests yet. Start using the proxy to see activity here.
</td>
</tr>
<div class="px-4 py-8 text-center text-gray-500 italic">
No requests yet. Start using the proxy to see activity here.
</div>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@ -1443,7 +1487,7 @@
<tbody>
<template x-for="row in historyModelBreakdown" :key="row.model">
<tr class="border-t border-border text-gray-300 cursor-pointer transition-colors"
:class="historySelectedModel === row.model ? 'bg-[#1c1c1c]' : 'hover:bg-[#181818]'"
:class="historySelectedModel === row.model ? 'bg-card-alt' : 'hover-bg-surface'"
@click="toggleHistoryModel(row.model)"
:title="historySelectedModel === row.model
? 'Show all models'