headroom/headroom/dashboard/templates/dashboard.html
Jervis 0537cbfde4
feat(dashboard): persist lifetime proxy metrics (#2198)
## Description

Persist bounded, aggregate-only Lifetime dashboard metrics across proxy
restarts and expose them through a new `/stats-lifetime` endpoint. The
change keeps session/runtime stats separate from durable lifetime stats,
gates sensitive dashboard metadata for loopback or explicitly trusted
dashboard clients, and updates the dashboard Lifetime view to consume
the new endpoint.

Closes #2137

## Type of Change

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

## Changes Made

- Added bounded persistent lifetime metrics state and wired proxy metric
events into it.
- Added `/stats-lifetime` with sensitive project/persistence details
gated behind dashboard metadata access checks.
- Extended loopback/dashboard metadata access policy for trusted
dashboard client CIDRs without widening admin/debug endpoints.
- Reorganized dashboard session/lifetime presentation around runtime
counters versus durable aggregates.
- Added focused tests for persistent aggregation, persistence, endpoint
registration, loopback gating, trusted dashboard CIDRs, and recent
request ordering.
- Fixed current Ruff/mypy issues in the lifetime metrics normalization
code.

## Testing

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

### Test Output

```text
uv run --extra proxy --with pytest --with pytest-asyncio pytest tests/test_persistent_metrics.py tests/test_persistent_metrics_integration.py tests/test_persistent_metrics_persistence.py tests/test_proxy_loopback_gating.py tests/test_proxy_stats_recent_requests.py -q
53 passed, 1 warning

uvx ruff==0.15.17 check headroom/proxy/forwarded_headers.py headroom/proxy/loopback_guard.py headroom/proxy/persistent_metrics.py headroom/proxy/savings_tracker.py headroom/proxy/server.py tests/test_forwarded_headers.py tests/test_persistent_metrics.py tests/test_persistent_metrics_integration.py tests/test_persistent_metrics_persistence.py tests/test_proxy_loopback_gating.py tests/test_proxy_project_savings.py tests/test_proxy_stats_recent_requests.py
All checks passed!

uv run --extra proxy --with mypy mypy headroom/proxy/persistent_metrics.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows review worktree, Python 3.13.3 via uv.
- Exact command / steps: Ran the focused persistent metrics,
persistence, loopback gating, and recent request tests; ran CI-matching
Ruff on touched files; ran mypy on the new persistent metrics module.
- Observed result: `/stats-lifetime` is registered, non-loopback callers
receive only non-sensitive aggregate data, loopback/trusted dashboard
clients receive the full lifetime payload, admin/debug endpoints remain
loopback-only, and persistent metrics normalize malformed stored state
without type/lint errors.
- Not tested: Full repository pytest suite, full dashboard browser
screenshot pass, or live long-running proxy traffic.

## 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
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] 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
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A

## Additional Notes

No changelog entry is required for this dashboard/internal metrics
iteration. The endpoint intentionally exposes only aggregate lifetime
data to ordinary network callers and strips project/persistence details
unless the caller passes the dashboard metadata access policy.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 18:18:13 +00:00

2750 lines
189 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Headroom Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js" defer></script>
<script>
(function() {
const saved = localStorage.getItem('headroom-theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (saved === 'light' || (!saved && !prefersDark)) {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
})();
</script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
surface: 'var(--color-surface)',
border: 'var(--color-border)',
accent: '#22d3ee',
}
}
}
}
</script>
<style>
:root {
--bg: #f9fafb;
--color-surface: #ffffff;
--color-border: #e5e7eb;
--card-alt-bg: #f3f4f6;
--diff-before-header: #fff5f5;
--diff-before-body: #fef2f2;
--diff-after-header: #f0fdf4;
--diff-after-body: #f0fdf4;
--ttl-card-bg: linear-gradient(180deg, rgba(34,211,238,0.10), rgba(224,249,253,0.90));
}
html.dark {
--bg: #0f0f0f;
--color-surface: #1a1a1a;
--color-border: #2a2a2a;
--card-alt-bg: #141414;
--diff-before-header: #1a0808;
--diff-before-body: #120606;
--diff-after-header: #081a08;
--diff-after-body: #0a120a;
--ttl-card-bg: linear-gradient(180deg, rgba(34,211,238,0.10), rgba(12,16,22,0.88));
}
body { background: var(--bg); }
.sparkline { stroke: #22d3ee; stroke-width: 1.5; fill: none; }
.sparkline-area { fill: url(#sparkline-gradient); }
.trend-line { stroke: #22d3ee; stroke-width: 2; fill: none; }
.trend-area { fill: url(#trend-gradient); }
@keyframes pulse-subtle { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
.pulse-live { animation: pulse-subtle 2s ease-in-out infinite; }
#feed-container {
height: calc(100vh - 57px);
scroll-behavior: smooth;
}
#feed-virtual-list {
position: relative;
}
.transformation-card { background: var(--card-alt-bg); }
.transformation-card:hover { background: var(--color-surface); }
.diff-before-header { background: var(--diff-before-header); }
.diff-before { background: var(--diff-before-body); }
.diff-after-header { background: var(--diff-after-header); }
.diff-after { background: var(--diff-after-body); }
.ttl-bucket-gradient-card { background: var(--ttl-card-bg); }
/* Light mode: invert the gray text scale (dark bg grays → light bg grays) */
html:not(.dark) body { color: #1f2937; }
html:not(.dark) .text-gray-200 { color: #1f2937; }
html:not(.dark) .text-gray-300 { color: #374151; }
html:not(.dark) .text-gray-400 { color: #6b7280; }
html:not(.dark) .text-gray-500 { color: #9ca3af; }
html:not(.dark) .text-gray-600 { color: #6b7280; }
html:not(.dark) .hover\:text-gray-200:hover { color: #111827; }
html:not(.dark) .hover\:text-white:hover { color: #111827; }
/* Light mode overrides for dark-specific utility classes */
html:not(.dark) .bg-black\/20 { background-color: rgba(0,0,0,0.04); }
html:not(.dark) .bg-black\/30 { background-color: rgba(0,0,0,0.06); }
html:not(.dark) .border-white\/8 { border-color: rgba(0,0,0,0.10); }
html:not(.dark) .ring-white\/5 { --tw-ring-color: rgba(0,0,0,0.08); }
html:not(.dark) .border-red-900\/30 { border-color: rgba(239,68,68,0.25); }
html:not(.dark) .border-emerald-900\/30 { border-color: rgba(16,185,129,0.25); }
/* Light mode: TTL card color adjustments */
html:not(.dark) .text-cyan-50 { color: #164e63; }
html:not(.dark) .text-violet-50 { color: #4c1d95; }
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()">
<!-- Header -->
<header class="border-b border-border px-6 py-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div class="flex items-center gap-4">
<h1 class="text-xl font-semibold tracking-tight">HEADROOM</h1>
<span class="text-xs text-gray-500 font-mono" x-text="formatVersion(version)"></span>
</div>
<div class="flex flex-col gap-3 md:flex-row md:items-center md:gap-6">
<div class="inline-flex rounded-lg border border-border bg-surface p-1">
<button class="px-3 py-1.5 text-sm rounded-md transition-colors"
:class="viewMode === 'session' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
@click="setViewMode('session')">
Session
</button>
<button class="px-3 py-1.5 text-sm rounded-md transition-colors"
:class="viewMode === 'lifetime' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
@click="setViewMode('lifetime')">
Lifetime
</button>
<button class="px-3 py-1.5 text-sm rounded-md transition-colors"
:class="viewMode === 'history' ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
@click="setViewMode('history')">
Historical
</button>
</div>
<template x-if="stats.anon_telemetry_shipping">
<div class="inline-flex items-center gap-1.5 rounded-full border border-amber-500/40 bg-amber-500/10 px-2.5 py-1"
title="Anonymous aggregate telemetry is enabled. Disable with HEADROOM_TELEMETRY=off or --no-telemetry.">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"
class="w-3 h-3 text-amber-400 shrink-0">
<path fill-rule="evenodd"
d="M6.701 2.25c.577-1 2.02-1 2.598 0l5.196 9a1.5 1.5 0 0 1-1.299 2.25H2.804a1.5 1.5 0 0 1-1.3-2.25l5.197-9ZM8 4a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0v-3A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clip-rule="evenodd"/>
</svg>
<span class="text-xs text-amber-400 font-medium">Anon Telemetry</span>
</div>
</template>
<template x-if="stats.config && stats.config.savings_profile">
<div class="inline-flex items-center gap-1.5 rounded-full border border-cyan-500/40 bg-cyan-500/10 px-2.5 py-1"
:title="'Current proxy profile: ' + stats.config.savings_profile">
<span class="w-1.5 h-1.5 rounded-full bg-cyan-400"></span>
<span class="text-xs text-cyan-100">
<span x-text="stats.config.savings_profile"></span>
<template x-if="stats.config.target_savings_percent !== null">
<span x-text="' · target ' + stats.config.target_savings_percent + '%'"></span>
</template>
</span>
</div>
</template>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500">Status</span>
<span class="flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full pulse-live"
:class="healthy ? 'bg-emerald-400' : 'bg-red-400'"></span>
<span class="text-sm" x-text="healthy ? 'Healthy' : 'Error'"></span>
</span>
</div>
<div class="text-xs text-gray-500">
Updated <span x-text="lastUpdate"></span>
</div>
<button onclick="toggleTheme()" id="theme-toggle"
class="px-3 py-1.5 text-sm rounded-md border border-border bg-surface text-gray-400 hover:text-gray-200 transition-colors"
title="Toggle light/dark mode">
<svg class="w-4 h-4 dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
</svg>
<svg class="w-4 h-4 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
</button>
<button x-show="log_full_messages" id="feed-toggle"
class="px-3 py-1.5 text-sm rounded-md border border-border bg-surface text-gray-300 hover:text-white transition-colors"
@click="toggleFeed()"
:class="feedOpen ? 'bg-accent text-black' : ''">
Live Feed
</button>
</div>
</header>
<main class="p-6 max-w-7xl mx-auto">
<template x-if="viewMode === 'session'">
<div>
<p class="mb-4 text-xs text-gray-500">Current proxy process · runtime counters reset on restart</p>
<!-- Runtime Overview -->
<div class="grid grid-cols-1 gap-4 mb-6 lg:grid-cols-2">
<section class="bg-surface rounded-lg p-4 border border-border">
<h3 class="mb-3 text-sm font-medium text-gray-300">Request Health</h3>
<div class="grid grid-cols-2 gap-3 text-sm"><div>Completed <span class="float-right tabular-nums" x-text="formatNumber(stats.requests?.total || 0)"></span></div><div>Failed <span class="float-right tabular-nums text-red-400" x-text="formatNumber(stats.requests?.failed || 0)"></span></div><div>Rate Limited <span class="float-right tabular-nums text-amber-400" x-text="formatNumber(stats.requests?.rate_limited || 0)"></span></div><div>Cached <span class="float-right tabular-nums text-cyan-400" x-text="formatNumber(stats.requests?.cached || 0)"></span></div></div>
</section>
<section class="bg-surface rounded-lg p-4 border border-border">
<h3 class="mb-3 text-sm font-medium text-gray-300">Live Activity</h3>
<div class="grid grid-cols-2 gap-3 text-sm"><div>Active Requests <span class="float-right tabular-nums" x-text="formatNumber(stats.proxy_inbound?.active || 0)"></span></div><div>Active WebSockets <span class="float-right tabular-nums" x-text="formatNumber(stats.runtime?.websocket_sessions?.active_sessions || 0)"></span></div><div>Relay Tasks <span class="float-right tabular-nums" x-text="formatNumber(stats.runtime?.websocket_sessions?.active_relay_tasks || 0)"></span></div><div>Compression Queued <span class="float-right tabular-nums" x-text="formatNumber(stats.runtime?.compression_executor?.queued || 0)"></span></div></div>
</section>
</div>
<!-- Session Optimization -->
<div class="grid grid-cols-1 gap-4 mb-6 md:grid-cols-2 lg:grid-cols-3">
<!-- Token Savings (%) -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Token Savings</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums text-accent" x-text="formatNumber(stats.tokens?.saved || 0)"></span>
<!-- Active compression ratio: savings as a fraction of tokens we
*attempted* to compress (extracted units + tool schema). When
the attempted denominator is missing (backend-routed paths
that don't yet wire per-message live-zone tracking), fall back
to the whole-request ratio so the headline doesn't collapse to
0% while compression is happening (issue #455). -->
<span class="text-sm text-accent" x-text="headlineSavingsPercent.toFixed(1) + '%'" :title="headlineSavingsTitle"></span>
</div>
<div class="mt-1 text-xs text-gray-500 leading-relaxed">
<span x-text="'Proxy ' + formatNumber(stats.tokens?.proxy_compression_saved || 0) + ' (' + proxyShareOfTotal.toFixed(1) + '%)'"></span>
<span class="mx-1 text-gray-600">/</span>
<span x-text="cliFilteringAvailable ? (cliFilteringLabel + ' ' + formatNumber(cliFilteringSaved) + ' this session (' + cliFilteringSessionPctDisplay.toFixed(1) + '%)') : (cliFilteringLabel + ' not installed')"></span>
</div>
<div class="mt-1 text-xs text-gray-600 leading-relaxed">
<span x-text="'Of total wire: ' + (stats.tokens?.savings_percent || 0).toFixed(2) + '%'" title="Savings as fraction of all input tokens including frozen prefix"></span>
</div>
<div class="mt-2 h-8">
<svg class="w-full h-full" viewBox="0 0 100 32" preserveAspectRatio="none">
<defs>
<linearGradient id="sparkline-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#22d3ee;stop-opacity:0.3"/>
<stop offset="100%" style="stop-color:#22d3ee;stop-opacity:0"/>
</linearGradient>
</defs>
<path class="sparkline-area" :d="getSparklineArea(savingsHistory)"></path>
<path class="sparkline" :d="getSparkline(savingsHistory)"></path>
</svg>
</div>
</div>
<!-- Output Tokens Saved (counterfactual) -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Output Tokens Saved</div>
<template x-if="stats.tokens?.output_reduction?.available">
<div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums text-accent" x-text="formatNumber(stats.tokens?.output_saved || 0)"></span>
<span class="text-sm text-accent" x-text="(stats.tokens?.output_reduction_percent || 0).toFixed(1) + '%'"></span>
</div>
<div class="mt-1 text-xs text-gray-500 leading-relaxed">
<!-- "measured" = A/B holdout (unbiased); "estimated" = vs learned baseline -->
<span class="uppercase tracking-wide"
:class="stats.tokens?.output_reduction?.method === 'measured' ? 'text-emerald-400' : 'text-gray-400'"
x-text="stats.tokens?.output_reduction?.method || ''"></span>
<span x-text="'· 95% CI ' + (stats.tokens?.output_reduction?.ci_low_percent || 0).toFixed(1) + '' + (stats.tokens?.output_reduction?.ci_high_percent || 0).toFixed(1) + '%'"></span>
</div>
<div class="mt-1 text-xs text-gray-600 leading-relaxed"
x-text="formatNumber(stats.tokens?.output_reduction?.requests || 0) + ' shaped responses · counterfactual'">
</div>
</div>
</template>
<template x-if="!stats.tokens?.output_reduction?.available">
<div class="mt-1 text-xs text-gray-500 leading-relaxed">
<span class="text-2xl font-light tabular-nums text-gray-600"></span>
<div class="mt-1">Enable the output shaper (HEADROOM_OUTPUT_SHAPER=1) and run
<code class="text-gray-400">headroom learn --verbosity --apply</code> to start measuring.</div>
</div>
</template>
</div>
<!-- Tool-Schema Deferral (tool search) — only rendered when there's a saving to show -->
<template x-if="(stats.savings?.by_layer?.tool_search?.tokens || 0) > 0">
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Tool-Schema Deferral</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums text-emerald-400" x-text="formatNumber(stats.savings?.by_layer?.tool_search?.tokens || 0)"></span>
<span class="text-sm text-gray-400">tokens</span>
</div>
<div class="mt-1 text-xs text-gray-600 leading-relaxed"
x-text="formatNumber(stats.savings?.by_layer?.tool_search?.requests || 0) + ' calls · tool schemas deferred (recent window)'">
</div>
</div>
</template>
</div>
<!-- Performance -->
<div class="grid grid-cols-1 gap-4 mb-6 lg:grid-cols-3">
<!-- Headroom Overhead -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Overhead</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums" x-text="(stats.overhead?.average_ms || 0).toFixed(0) + 'ms'"></span>
</div>
<div class="mt-2 text-xs text-gray-500">
TTFB <span x-text="((stats.ttfb?.average_ms || 0) / 1000).toFixed(2)"></span>s avg
</div>
</div>
<!-- Token Throughput -->
<div class="bg-surface rounded-lg p-4 border border-border flex flex-col justify-between">
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1.5">Throughput</div>
<div class="flex flex-col gap-1 text-[11px] text-gray-300">
<div class="flex justify-between items-baseline border-b border-border/40 pb-0.5">
<span class="text-gray-500">Input (wall / active p50)</span>
<span class="font-mono text-accent">
<span x-text="(stats.throughput?.rolling?.input_wall_clock || 0).toFixed(1)"></span> /
<span x-text="(stats.throughput?.rolling?.input_active_p50 || 0).toFixed(1)"></span> tok/s
</span>
</div>
<div class="flex justify-between items-baseline border-b border-border/40 pb-0.5" x-show="(stats.throughput?.rolling?.compression_p50 || 0) > 0">
<span class="text-gray-500">Compression (p50 / p95)</span>
<span class="font-mono text-emerald-400">
<span x-text="(stats.throughput?.rolling?.compression_p50 || 0).toFixed(1)"></span> /
<span x-text="(stats.throughput?.rolling?.compression_p95 || 0).toFixed(1)"></span> tok/s
</span>
</div>
<div class="flex justify-between items-baseline border-b border-border/40 pb-0.5">
<span class="text-gray-500">Forward (p50 / p95)</span>
<span class="font-mono text-cyan-400">
<span x-text="(stats.throughput?.rolling?.forward_p50 || 0).toFixed(1)"></span> /
<span x-text="(stats.throughput?.rolling?.forward_p95 || 0).toFixed(1)"></span> tok/s
</span>
</div>
<div class="flex justify-between items-baseline" x-show="(stats.throughput?.rolling?.generation_p50 || 0) > 0">
<span class="text-gray-500">Generation (p50 / p95)</span>
<span class="font-mono text-yellow-400">
<span x-text="(stats.throughput?.rolling?.generation_p50 || 0).toFixed(1)"></span> /
<span x-text="(stats.throughput?.rolling?.generation_p95 || 0).toFixed(1)"></span> tok/s
</span>
</div>
</div>
</div>
<div class="mt-2 pt-1.5 border-t border-border/40 text-[10px] text-gray-500 flex justify-between items-center">
<span>Current 5m (active p50):</span>
<span class="font-mono text-gray-400">
In: <span x-text="(stats.throughput?.current?.input_active_p50 || 0).toFixed(0)"></span> ·
Fwd: <span x-text="(stats.throughput?.current?.forward_p50 || 0).toFixed(0)"></span> tok/s
</span>
</div>
</div>
<!-- Performance -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Performance</div>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Overhead Range</span>
<span class="font-mono text-sm" x-text="(stats.overhead?.min_ms || 0).toFixed(0) + ' - ' + (stats.overhead?.max_ms || 0).toFixed(0) + 'ms'"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">TTFB Range</span>
<span class="font-mono text-sm" x-text="((stats.ttfb?.min_ms || 0) / 1000).toFixed(2) + ' - ' + ((stats.ttfb?.max_ms || 0) / 1000).toFixed(2) + 's'"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Failed Requests</span>
<span class="font-mono text-sm" x-text="stats.requests?.failed || 0"></span>
</div>
<!-- Per-transform timing breakdown -->
<template x-if="Object.keys(stats.pipeline_timing || {}).length > 0">
<div>
<div class="border-t border-border my-2"></div>
<div class="text-xs text-gray-500 uppercase tracking-wide mb-2">Pipeline Breakdown</div>
<template x-for="[name, t] in Object.entries(stats.pipeline_timing || {})" :key="name">
<div class="flex justify-between items-center mb-1">
<span class="text-xs text-gray-400 font-mono truncate mr-2" x-text="name"></span>
<span class="text-xs font-mono whitespace-nowrap"
:class="t.average_ms > 100 ? 'text-amber-400' : t.average_ms > 50 ? 'text-yellow-400' : 'text-gray-400'"
x-text="t.average_ms.toFixed(0) + 'ms avg / ' + t.max_ms.toFixed(0) + 'ms max'"></span>
</div>
</template>
</div>
</template>
</div>
</div>
</div>
<!-- Optimization Breakdown -->
<div class="grid grid-cols-1 gap-4 mb-6 lg:grid-cols-3">
<!-- Token Usage -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Token Usage</div>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Before Compression</span>
<span class="font-mono text-sm" x-text="formatNumber(stats.tokens?.total_before_compression || 0)"></span>
</div>
<div class="flex justify-between items-center" x-show="cliFilteringAvailable">
<span class="text-sm text-gray-400" x-text="cliFilteringLabel + ' Filtered (this session)'"></span>
<span class="font-mono text-sm text-emerald-400" x-text="formatNumber(cliFilteringSaved)"></span>
</div>
<div class="flex justify-between items-center" x-show="!cliFilteringAvailable">
<span class="text-sm text-gray-400" x-text="cliFilteringLabel + ' Filtered (this session)'"></span>
<span class="font-mono text-sm text-gray-600">not installed</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Proxy Removed</span>
<span class="font-mono text-sm text-accent" x-text="formatNumber(stats.tokens?.proxy_compression_saved || 0)"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">After Compression (sent)</span>
<span class="font-mono text-sm" x-text="formatNumber(stats.tokens?.input || 0)"></span>
</div>
<div class="border-t border-border my-2"></div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Output Tokens</span>
<span class="font-mono text-sm" x-text="formatNumber(stats.tokens?.output || 0)"></span>
</div>
</div>
</div>
<!-- What Headroom Removed -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">What Headroom Removed</div>
<template x-if="Object.keys(stats.waste_signals || {}).length > 0">
<div class="space-y-3">
<template x-for="[signal, tokens] in sortedWasteSignals" :key="signal">
<div>
<div class="flex justify-between items-center mb-1">
<span class="text-sm text-gray-400" x-text="wasteSignalLabel(signal)"></span>
<span class="font-mono text-sm text-accent" x-text="formatNumber(tokens) + ' tokens'"></span>
</div>
<div class="w-full h-2 bg-border rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
:class="wasteSignalColor(signal)"
:style="'width: ' + getWastePercent(tokens) + '%'"></div>
</div>
</div>
</template>
</div>
</template>
<template x-if="Object.keys(stats.waste_signals || {}).length === 0">
<div class="text-sm text-gray-500 italic py-8 text-center">
No waste signals detected yet. Data appears after requests are processed.
</div>
</template>
</div>
<!-- Cumulative Savings Trend -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="flex justify-between items-center mb-4">
<span class="text-sm font-medium text-gray-300">Savings Over Time</span>
<span class="text-xs text-gray-500 font-mono" x-text="formatNumber(stats.tokens?.saved || 0) + ' tokens total'"></span>
</div>
<template x-if="(stats.savings_history || []).length >= 2">
<div class="h-32">
<svg class="w-full h-full" viewBox="0 0 200 64" preserveAspectRatio="none">
<defs>
<linearGradient id="trend-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#22d3ee;stop-opacity:0.2"/>
<stop offset="100%" style="stop-color:#22d3ee;stop-opacity:0"/>
</linearGradient>
</defs>
<path class="trend-area" :d="getTrendArea(stats.savings_history)"></path>
<path class="trend-line" :d="getTrendLine(stats.savings_history)"></path>
</svg>
</div>
</template>
<template x-if="(stats.savings_history || []).length < 2">
<div class="h-32 flex items-center justify-center text-sm text-gray-500 italic">
Trend data will appear after multiple requests.
</div>
</template>
</div>
</div>
<!-- Prefix Cache Impact: current process only -->
<template x-if="cacheSessionActive">
<div class="bg-surface rounded-lg p-4 border border-border mb-6">
<div class="flex justify-between items-center mb-3">
<div class="text-sm font-medium text-gray-300">Prefix Cache Impact</div>
<div class="text-xs text-emerald-400 font-mono"
x-show="cacheSessionActive"
x-text="'Net savings: $' + formatCurrency(stats.prefix_cache?.totals?.net_savings_usd || 0)"></div>
<div class="text-xs text-gray-500 font-mono"
x-show="!cacheSessionActive">no activity since restart</div>
</div>
<!-- Totals row -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Cache Writes</div>
<div class="text-2xl font-light tabular-nums text-amber-400"
x-show="cacheSessionActive"
x-text="formatNumber(stats.prefix_cache?.totals?.cache_write_tokens || 0)"></div>
<div class="text-2xl font-light tabular-nums text-gray-600"
x-show="!cacheSessionActive">&mdash;</div>
<div class="text-xs text-amber-400/70"
x-show="cacheSessionActive"
x-text="'$' + formatCurrency(stats.prefix_cache?.totals?.write_premium_usd || 0) + ' write premium'"></div>
<div class="text-xs text-gray-500"
x-show="!cacheSessionActive">no activity since restart</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Hit Rate</div>
<div class="text-2xl font-light tabular-nums" x-show="cacheSessionActive" :class="(stats.prefix_cache?.totals?.hit_rate || 0) > 80 ? 'text-emerald-400' : (stats.prefix_cache?.totals?.hit_rate || 0) > 50 ? 'text-amber-400' : 'text-red-400'" x-text="(stats.prefix_cache?.totals?.hit_rate || 0).toFixed(0) + '%'"></div>
<div class="text-2xl font-light tabular-nums text-gray-600" x-show="!cacheSessionActive">&mdash;</div>
<div class="text-xs text-gray-500" x-show="cacheSessionActive" x-text="(stats.prefix_cache?.totals?.hit_requests || 0) + ' / ' + (stats.prefix_cache?.totals?.requests || 0) + ' requests'"></div>
<div class="text-xs text-gray-500" x-show="!cacheSessionActive">no activity since restart</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Cache Busts</div>
<div class="text-2xl font-light tabular-nums" x-show="cacheSessionActive" :class="(stats.prefix_cache?.totals?.bust_count || 0) > 5 ? 'text-red-400' : (stats.prefix_cache?.totals?.bust_count || 0) > 0 ? 'text-amber-400' : 'text-emerald-400'" x-text="stats.prefix_cache?.totals?.bust_count || 0"></div>
<div class="text-2xl font-light tabular-nums text-gray-600" x-show="!cacheSessionActive">&mdash;</div>
<div class="text-xs text-gray-500" x-show="cacheSessionActive" x-text="formatNumber(stats.prefix_cache?.totals?.bust_write_tokens || 0) + ' tokens re-written'"></div>
<div class="text-xs text-gray-500" x-show="!cacheSessionActive">no activity since restart</div>
</div>
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Providers</div>
<div class="text-2xl font-light tabular-nums text-gray-300" x-show="cacheSessionActive" x-text="Object.keys(stats.prefix_cache?.by_provider || {}).length"></div>
<div class="text-2xl font-light tabular-nums text-gray-600" x-show="!cacheSessionActive">&mdash;</div>
<div class="text-xs text-gray-500" x-show="cacheSessionActive">with cache data</div>
<div class="text-xs text-gray-500" x-show="!cacheSessionActive">no activity since restart</div>
</div>
</div>
<!-- Cache efficiency bar (session-scoped; hidden until traffic arrives) -->
<div x-show="cacheSessionActive">
<div class="flex justify-between text-xs text-gray-500 mb-1">
<span>Cache Efficiency</span>
<span x-text="cacheSavingsPercent + '% of input served from cache'"></span>
</div>
<div class="w-full h-3 bg-border rounded-full overflow-hidden flex">
<div class="h-full bg-emerald-500 transition-all duration-500"
:style="'width: ' + cacheSavingsPercent + '%'"></div>
<div class="h-full bg-amber-500 transition-all duration-500"
:style="'width: ' + cacheWritePercent + '%'"></div>
</div>
<div class="flex gap-4 mt-1 text-xs text-gray-500">
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-emerald-500 inline-block"></span> Reads (discounted)</span>
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-amber-500 inline-block"></span> Writes</span>
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-border inline-block"></span> Uncached</span>
</div>
</div>
<template x-if="hasObservedTtlBuckets">
<div class="mt-5 border-t border-border pt-4">
<div class="flex items-center justify-between mb-3">
<div>
<div class="text-xs text-gray-500 uppercase tracking-[0.22em]">Observed TTL Buckets</div>
<div class="text-sm text-gray-300 mt-1">Provider-reported cache write mix</div>
</div>
<div class="text-xs text-gray-500 font-mono" x-text="observedTtlWindowLabel"></div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-[1.3fr_0.9fr] gap-4">
<div class="ttl-bucket-gradient-card rounded-2xl border border-cyan-500/20 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-[11px] uppercase tracking-[0.22em] text-cyan-300/75">Bucket Mix</div>
<div class="mt-1 text-2xl font-light text-cyan-50">
<span data-testid="ttl-bucket-mix-1h-total" x-text="formatNumber(stats.prefix_cache?.totals?.cache_write_1h_tokens || 0)"></span>
<span class="text-sm text-cyan-300/70">1h</span>
<span class="mx-2 text-cyan-500/60">/</span>
<span data-testid="ttl-bucket-mix-5m-total" x-text="formatNumber(stats.prefix_cache?.totals?.cache_write_5m_tokens || 0)"></span>
<span class="text-sm text-cyan-300/70">5m</span>
</div>
</div>
<div data-testid="ttl-bucket-headline" class="rounded-full border border-cyan-400/20 bg-black/20 px-3 py-1 text-[11px] uppercase tracking-[0.18em] text-cyan-200/80" x-text="observedTtlHeadline"></div>
</div>
<div class="mt-4">
<div class="flex justify-between text-xs text-gray-500 mb-1">
<span>Observed write-token split</span>
<span x-text="(stats.prefix_cache?.totals?.observed_ttl_mix?.active_buckets || []).join(' + ')"></span>
</div>
<div class="h-3 overflow-hidden rounded-full bg-black/30 ring-1 ring-white/5 flex">
<div class="h-full bg-cyan-400 transition-all duration-500" :style="'width:' + (stats.prefix_cache?.totals?.observed_ttl_mix?.['1h_pct'] || 0) + '%'"></div>
<div class="h-full bg-violet-400 transition-all duration-500" :style="'width:' + (stats.prefix_cache?.totals?.observed_ttl_mix?.['5m_pct'] || 0) + '%'"></div>
</div>
<div class="mt-2 flex flex-wrap gap-3 text-xs text-gray-400">
<span class="inline-flex items-center gap-1.5"><span class="inline-block h-2 w-2 rounded-full bg-cyan-400"></span><span data-testid="ttl-bucket-mix-1h-pct" x-text="'1h ' + (stats.prefix_cache?.totals?.observed_ttl_mix?.['1h_pct'] || 0).toFixed(1) + '%'"></span></span>
<span class="inline-flex items-center gap-1.5"><span class="inline-block h-2 w-2 rounded-full bg-violet-400"></span><span data-testid="ttl-bucket-mix-5m-pct" x-text="'5m ' + (stats.prefix_cache?.totals?.observed_ttl_mix?.['5m_pct'] || 0).toFixed(1) + '%'"></span></span>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-1 gap-3">
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-cyan-300/75">1h Cache Writes</div>
<div data-testid="ttl-bucket-1h-value" class="mt-2 text-3xl font-light text-cyan-50" x-text="formatNumber(stats.prefix_cache?.totals?.observed_ttl_buckets?.['1h']?.tokens || 0)"></div>
<div class="mt-1 text-xs text-gray-500" x-text="formatNumber(stats.prefix_cache?.totals?.observed_ttl_buckets?.['1h']?.requests || 0) + ' requests observed'"></div>
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-violet-300/75">5m Cache Writes</div>
<div data-testid="ttl-bucket-5m-value" class="mt-2 text-3xl font-light text-violet-50" x-text="formatNumber(stats.prefix_cache?.totals?.observed_ttl_buckets?.['5m']?.tokens || 0)"></div>
<div class="mt-1 text-xs text-gray-500" x-text="formatNumber(stats.prefix_cache?.totals?.observed_ttl_buckets?.['5m']?.requests || 0) + ' requests observed'"></div>
</div>
</div>
</div>
</div>
</template>
<!-- Compression vs cache: net impact of prefix-mutating layers -->
<template x-if="hasCompressionVsCache">
<div class="mt-5 border-t border-border pt-4">
<div class="flex items-center justify-between mb-3">
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Compression vs Cache</div>
<div class="text-[11px] text-gray-600 mt-0.5">Tokens saved by compression against cached-prefix tokens its mutations invalidated</div>
</div>
<div data-testid="cvc-net-headline" class="rounded-full border bg-black/20 px-3 py-1 text-[11px] uppercase tracking-[0.18em]" :class="compressionVsCacheNet >= 0 ? 'border-emerald-400/20 text-emerald-300/90' : 'border-red-400/25 text-red-300/90'" x-text="compressionVsCacheNet >= 0 ? 'Net positive' : 'Net negative'"></div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-emerald-300/75">Saved by Compression</div>
<div data-testid="cvc-saved-value" class="mt-2 text-3xl font-light text-emerald-50" x-text="formatNumber(stats.prefix_cache?.compression_vs_cache?.tokens_saved_by_compression || 0)"></div>
<div class="mt-1 text-xs text-gray-500">tokens removed before send</div>
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-red-300/75">Lost to Cache Busts</div>
<div data-testid="cvc-bust-value" class="mt-2 text-3xl font-light text-red-50" x-text="formatNumber(stats.prefix_cache?.compression_vs_cache?.tokens_lost_to_cache_bust || 0)"></div>
<div class="mt-1 text-xs text-gray-500" x-text="formatNumber(stats.prefix_cache?.compression_vs_cache?.cache_bust_count || 0) + ' busts observed'"></div>
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-gray-400">Net</div>
<div data-testid="cvc-net-value" class="mt-2 text-3xl font-light" :class="compressionVsCacheNet >= 0 ? 'text-emerald-400' : 'text-red-400'" x-text="(compressionVsCacheNet >= 0 ? '+' : '-') + formatNumber(Math.abs(compressionVsCacheNet))"></div>
<div class="mt-1 text-xs text-gray-500">saved minus bust losses</div>
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-cyan-300/75">Prefix Freeze Net</div>
<div data-testid="freeze-net-value" class="mt-2 text-3xl font-light" :class="prefixFreezeNet >= 0 ? 'text-cyan-50' : 'text-red-400'" x-text="(prefixFreezeNet >= 0 ? '+' : '-') + formatNumber(Math.abs(prefixFreezeNet))"></div>
<div class="mt-1 text-xs text-gray-500" x-text="formatNumber(stats.prefix_cache?.prefix_freeze?.busts_avoided || 0) + ' busts avoided, ' + formatNumber(stats.prefix_cache?.prefix_freeze?.compression_foregone_tokens || 0) + ' foregone'"></div>
</div>
</div>
</div>
</template>
<!-- Cache miss attribution: why expected-cache turns missed (#1313) -->
<template x-if="hasMissAttribution">
<div class="mt-5 border-t border-border pt-4">
<div class="flex items-center justify-between mb-3">
<div>
<div class="text-xs text-gray-500 uppercase tracking-wide">Cache Miss Attribution</div>
<div class="text-[11px] text-gray-600 mt-0.5">Why turns that expected a prompt-cache hit missed — TTL lapse (consider a longer TTL) vs the cacheable prefix changing</div>
</div>
<div data-testid="miss-attr-headline" class="rounded-full border bg-black/20 px-3 py-1 text-[11px] uppercase tracking-[0.18em]"
:class="(missAttribution.ttl_expiry_pct || 0) >= (missAttribution.prefix_change_pct || 0) ? 'border-violet-400/25 text-violet-300/90' : 'border-amber-400/25 text-amber-300/90'"
x-text="(missAttribution.ttl_expiry_pct || 0) >= (missAttribution.prefix_change_pct || 0) ? 'Mostly TTL lapse' : 'Mostly prefix change'"></div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-violet-300/75">TTL Expiry</div>
<div data-testid="miss-attr-ttl-value" class="mt-2 text-3xl font-light text-violet-50" x-text="formatNumber(missAttribution.ttl_expiry || 0)"></div>
<div class="mt-1 text-xs text-gray-500" x-text="(missAttribution.ttl_expiry_pct || 0).toFixed(1) + '% of attributed — idle past cache TTL'"></div>
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-amber-300/75">Prefix Change</div>
<div data-testid="miss-attr-prefix-value" class="mt-2 text-3xl font-light text-amber-50" x-text="formatNumber(missAttribution.prefix_change || 0)"></div>
<div class="mt-1 text-xs text-gray-500" x-text="(missAttribution.prefix_change_pct || 0).toFixed(1) + '% of attributed — cached prefix shifted'"></div>
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-gray-400">Unknown</div>
<div data-testid="miss-attr-unknown-value" class="mt-2 text-3xl font-light text-gray-300" x-text="formatNumber(missAttribution.unknown || 0)"></div>
<div class="mt-1 text-xs text-gray-500">stable prefix, within TTL</div>
</div>
<div class="rounded-2xl border border-white/8 bg-black/20 p-4">
<div class="text-[11px] uppercase tracking-[0.18em] text-gray-400">Total Misses</div>
<div data-testid="miss-attr-total-value" class="mt-2 text-3xl font-light text-gray-100" x-text="formatNumber(missAttribution.total || 0)"></div>
<div class="mt-1 text-xs text-gray-500">expected a cache hit, got none</div>
</div>
</div>
</div>
</template>
<!-- Per-provider breakdown -->
<template x-if="Object.keys(stats.prefix_cache?.by_provider || {}).length > 0">
<div class="mt-4 border-t border-border pt-3">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-2">Per-Provider Breakdown</div>
<div class="space-y-3">
<template x-for="[prov, pc] in Object.entries(stats.prefix_cache?.by_provider || {})" :key="prov">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="px-2 py-0.5 bg-border rounded text-xs font-mono" x-text="prov"></span>
<span class="text-xs text-gray-500" x-text="pc.label"></span>
</div>
<div class="flex flex-wrap items-center justify-end gap-4 text-xs font-mono">
<span class="text-emerald-400" x-text="formatNumber(pc.cache_read_tokens) + ' reads (' + pc.read_discount + ' off)'"></span>
<template x-if="pc.write_premium !== 'none'">
<span class="text-amber-400" x-text="formatNumber(pc.cache_write_tokens) + ' writes (+' + pc.write_premium + ')'"></span>
</template>
<template x-if="pc.observed_ttl_mix">
<span class="text-cyan-300" x-text="'TTL 1h ' + (pc.observed_ttl_mix['1h_pct'] || 0).toFixed(1) + '% / 5m ' + (pc.observed_ttl_mix['5m_pct'] || 0).toFixed(1) + '%'"></span>
</template>
<span :class="pc.bust_count > 0 ? 'text-red-400' : 'text-gray-500'" x-text="pc.bust_count + ' busts'"></span>
<span class="text-emerald-400" x-text="'$' + formatCurrency(pc.net_savings_usd)"></span>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<!-- Traffic & Workload -->
<!-- Agent Usage -->
<div class="bg-surface rounded-lg border border-border overflow-hidden mb-6">
<div class="px-4 py-3 border-b border-border flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
<div>
<div class="text-sm font-medium text-gray-300">Agent Usage</div>
<div class="text-xs text-gray-500">Before and after token usage by detected client</div>
</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-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-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-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-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-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>
</div>
<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-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">
<span class="h-2.5 w-2.5 rounded-full" :class="agentDotClass(agent.agent)"></span>
<span class="text-sm font-medium text-gray-200 truncate" x-text="agent.label"></span>
</div>
<div class="mt-1 text-xs text-gray-500">
<span x-text="formatNumber(agent.requests || 0) + ' requests'"></span>
<span class="mx-1 text-gray-700">/</span>
<span x-text="agent.source"></span>
</div>
</div>
<div>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-gray-500">Token flow</span>
<span class="font-mono text-emerald-400" x-text="(agent.savings_percent || 0).toFixed(1) + '% saved'"></span>
</div>
<div class="h-3 w-full rounded-full bg-border overflow-hidden flex">
<div class="h-full bg-emerald-500 transition-all duration-500"
:style="'width:' + agentSavedWidth(agent) + '%'"></div>
<div class="h-full bg-accent/60 transition-all duration-500"
:style="'width:' + agentAfterWidth(agent) + '%'"></div>
</div>
<div class="mt-1 flex flex-wrap gap-3 text-xs text-gray-500">
<span class="inline-flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-emerald-500"></span>Saved</span>
<span class="inline-flex items-center gap-1"><span class="h-2 w-2 rounded-full bg-accent/60"></span>Sent</span>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-right">
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">Before</div>
<div class="font-mono text-sm" x-text="formatNumber(agent.before_tokens || 0)"></div>
</div>
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">After</div>
<div class="font-mono text-sm" x-text="formatNumber(agent.after_tokens || 0)"></div>
</div>
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">Saved</div>
<div class="font-mono text-sm text-accent" x-text="formatNumber(agent.tokens_saved || 0)"></div>
</div>
<div>
<div class="text-[11px] uppercase tracking-wide text-gray-500">Share</div>
<div class="font-mono text-sm text-gray-300" x-text="(agent.share_of_saved_percent || 0).toFixed(1) + '%'"></div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
<template x-if="agentRows.length === 0">
<div class="rounded-lg border border-dashed border-border p-6 text-center text-sm text-gray-500">
Agent usage appears after Cursor, Claude, Codex, or another client sends traffic through this proxy.
</div>
</template>
</div>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 lg:grid-cols-3">
<!-- Providers -->
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Providers</div>
<div class="space-y-2">
<template x-for="(count, provider) in (stats.requests?.by_provider || {})" :key="provider">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400" x-text="provider"></span>
<div class="flex items-center gap-2">
<div class="w-24 h-1.5 bg-border rounded-full overflow-hidden">
<div class="h-full bg-accent rounded-full"
:style="'width: ' + getProviderPercent(count) + '%'"></div>
</div>
<span class="font-mono text-sm w-12 text-right" x-text="formatNumber(count)"></span>
</div>
</div>
</template>
<template x-if="Object.keys(stats.requests?.by_provider || {}).length === 0">
<div class="text-sm text-gray-500 italic">No requests yet</div>
</template>
</div>
</div>
<!-- Per-Model Savings Breakdown -->
<template x-if="Object.keys(stats.cost?.per_model || {}).length > 0">
<div class="bg-surface rounded-lg border border-border overflow-hidden lg:col-span-2">
<div class="px-4 py-3 border-b border-border flex justify-between items-center">
<span class="text-sm font-medium text-gray-300">Per-Model Token Savings</span>
<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" 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-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>
<th class="px-4 py-3 font-medium text-right">Reduction</th>
</tr>
</thead>
<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 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 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 tabular-nums" x-text="info.reduction_pct.toFixed(1) + '%'"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</div>
<!-- Recent Requests Table (with expandable rows) -->
<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">
<span class="text-sm font-medium text-gray-300">Recent Requests</span>
<span class="text-xs text-gray-500">Last 25 &mdash; click row to expand</span>
</div>
<div class="overflow-x-auto">
<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">
<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>
</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>
</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>
</div>
</template>
<template x-if="(stats.recent_requests || []).length === 0">
<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>
</div>
</div>
</div>
</div>
<!-- Capacity & Quotas -->
<!-- Anthropic Subscription Window -->
<template x-if="stats.subscription_window && stats.subscription_window.latest">
<div class="bg-surface rounded-lg p-4 border border-border mb-6">
<div class="flex justify-between items-center mb-4">
<div class="text-sm font-medium text-gray-300">Anthropic Subscription Window</div>
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full pulse-live"
:class="stats.subscription_window.last_active_at ? 'bg-emerald-400' : 'bg-gray-600'"></span>
<span class="text-xs text-gray-500" x-text="stats.subscription_window.last_active_at ? 'Active' : 'Idle'"></span>
</div>
</div>
<!-- 5h / 7d progress bars -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- 5-hour window -->
<template x-if="stats.subscription_window.latest.five_hour">
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-400 uppercase tracking-wide">5-Hour Window</span>
<span class="tabular-nums font-mono"
:class="stats.subscription_window.latest.five_hour.utilization_pct > 80 ? 'text-red-400' : stats.subscription_window.latest.five_hour.utilization_pct > 60 ? 'text-amber-400' : 'text-emerald-400'"
x-text="(stats.subscription_window.latest.five_hour.utilization_pct || 0).toFixed(1) + '%'"></span>
</div>
<div class="w-full h-3 bg-border rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
:class="stats.subscription_window.latest.five_hour.utilization_pct > 80 ? 'bg-red-500' : stats.subscription_window.latest.five_hour.utilization_pct > 60 ? 'bg-amber-500' : 'bg-emerald-500'"
:style="'width: ' + Math.min(stats.subscription_window.latest.five_hour.utilization_pct || 0, 100) + '%'"></div>
</div>
<div class="text-xs text-gray-500 mt-1"
x-text="'Resets in ' + formatResetTime(stats.subscription_window.latest.five_hour.seconds_to_reset)"></div>
</div>
</template>
<!-- 7-day window -->
<template x-if="stats.subscription_window.latest.seven_day">
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-400 uppercase tracking-wide">7-Day Window</span>
<span class="tabular-nums font-mono"
:class="stats.subscription_window.latest.seven_day.utilization_pct > 80 ? 'text-red-400' : stats.subscription_window.latest.seven_day.utilization_pct > 60 ? 'text-amber-400' : 'text-emerald-400'"
x-text="(stats.subscription_window.latest.seven_day.utilization_pct || 0).toFixed(1) + '%'"></span>
</div>
<div class="w-full h-3 bg-border rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
:class="stats.subscription_window.latest.seven_day.utilization_pct > 80 ? 'bg-red-500' : stats.subscription_window.latest.seven_day.utilization_pct > 60 ? 'bg-amber-500' : 'bg-emerald-500'"
:style="'width: ' + Math.min(stats.subscription_window.latest.seven_day.utilization_pct || 0, 100) + '%'"></div>
</div>
<div class="text-xs text-gray-500 mt-1"
x-text="'Resets in ' + formatResetTime(stats.subscription_window.latest.seven_day.seconds_to_reset)"></div>
</div>
</template>
</div>
<!-- Overage / extra usage -->
<template x-if="stats.subscription_window.latest.extra_usage && stats.subscription_window.latest.extra_usage.is_enabled">
<div class="border-t border-border pt-3 mb-3">
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-400 uppercase tracking-wide">Extra Usage (Overage)</span>
<span class="text-amber-400 tabular-nums font-mono"
x-text="'$' + formatCurrency(stats.subscription_window.latest.extra_usage.used_credits_usd || 0) + (stats.subscription_window.latest.extra_usage.monthly_limit_usd ? ' / $' + formatCurrency(stats.subscription_window.latest.extra_usage.monthly_limit_usd) : '')"></span>
</div>
<template x-if="stats.subscription_window.latest.extra_usage.utilization_pct != null">
<div class="w-full h-2 bg-border rounded-full overflow-hidden">
<div class="h-full bg-amber-500 rounded-full transition-all duration-500"
:style="'width: ' + Math.min(stats.subscription_window.latest.extra_usage.utilization_pct || 0, 100) + '%'"></div>
</div>
</template>
</div>
</template>
<!-- Headroom contribution row -->
<template x-if="stats.subscription_window.contribution && stats.subscription_window.contribution.tokens_submitted > 0">
<div class="border-t border-border pt-3">
<div class="text-xs text-gray-400 uppercase tracking-wide mb-2">Headroom Contribution This Window</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div>
<div class="text-xs text-gray-500 mb-0.5">Efficiency</div>
<div class="text-lg font-light tabular-nums text-accent"
x-text="(stats.subscription_window.contribution.efficiency_pct || 0).toFixed(1) + '%'"></div>
</div>
<div>
<div class="text-xs text-gray-500 mb-0.5">Tokens Saved</div>
<div class="text-lg font-light tabular-nums text-emerald-400"
x-text="formatNumber(stats.subscription_window.contribution.tokens_saved?.total || 0)"></div>
</div>
<div>
<div class="text-xs text-gray-500 mb-0.5">Compression</div>
<div class="text-sm tabular-nums text-gray-300"
x-text="formatNumber(stats.subscription_window.contribution.tokens_saved?.compression || 0) + ' tok'"></div>
</div>
<div>
<div class="text-xs text-gray-500 mb-0.5">Cache Reads</div>
<div class="text-sm tabular-nums text-cyan-400"
x-text="formatNumber(stats.subscription_window.contribution.tokens_saved?.cache_reads || 0) + ' tok'"></div>
</div>
</div>
<!-- Efficiency bar: tokens submitted vs what would have been sent without headroom -->
<template x-if="stats.subscription_window.contribution.raw_without_headroom > 0">
<div class="mt-3">
<div class="flex justify-between text-xs text-gray-500 mb-1">
<span>Raw vs Submitted</span>
<span x-text="formatNumber(stats.subscription_window.contribution.tokens_submitted) + ' / ' + formatNumber(stats.subscription_window.contribution.raw_without_headroom) + ' tokens forwarded'"></span>
</div>
<div class="w-full h-2 bg-border rounded-full overflow-hidden flex">
<div class="h-full bg-accent rounded-l-full"
:style="'width: ' + ((stats.subscription_window.contribution.tokens_submitted / stats.subscription_window.contribution.raw_without_headroom) * 100).toFixed(1) + '%'"></div>
</div>
<div class="flex gap-4 mt-1 text-xs text-gray-500">
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-accent inline-block"></span>Forwarded to Anthropic</span>
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-border inline-block"></span>Saved by Headroom</span>
</div>
</div>
</template>
</div>
</template>
<!-- Discrepancies -->
<template x-if="stats.subscription_window.discrepancies && stats.subscription_window.discrepancies.length > 0">
<div class="border-t border-border pt-3 mt-3">
<div class="text-xs text-amber-400 uppercase tracking-wide mb-2">⚠ Anomalies Detected</div>
<template x-for="d in stats.subscription_window.discrepancies" :key="d.kind + d.description">
<div class="text-xs mb-1"
:class="d.severity === 'alert' ? 'text-red-400' : 'text-amber-400'"
x-text="d.description"></div>
</template>
</div>
</template>
</div>
</template>
<!-- Codex Rate-Limit Window -->
<template x-if="stats.codex_rate_limits && stats.codex_rate_limits.primary">
<div class="bg-surface rounded-lg p-4 border border-border mb-6">
<div class="flex justify-between items-center mb-4">
<div class="text-sm font-medium text-gray-300">OpenAI Codex Rate-Limit Window</div>
<template x-if="stats.codex_rate_limits.limit_name">
<span class="text-xs text-gray-500 font-mono" x-text="stats.codex_rate_limits.limit_name"></span>
</template>
</div>
<!-- Primary / Secondary window grid (mirrors Anthropic 5h/7d layout) -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<!-- Primary window -->
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-400 uppercase tracking-wide">Primary
<template x-if="stats.codex_rate_limits.primary.window_label">
<span class="text-gray-600 normal-case font-mono ml-1"
x-text="'(' + stats.codex_rate_limits.primary.window_label + ')'"></span>
</template>
</span>
<span class="tabular-nums font-mono"
:class="(stats.codex_rate_limits.primary.used_percent||0) > 80 ? 'text-red-400' : (stats.codex_rate_limits.primary.used_percent||0) > 60 ? 'text-amber-400' : 'text-emerald-400'"
x-text="(stats.codex_rate_limits.primary.used_percent||0).toFixed(1) + '%'"></span>
</div>
<div class="w-full h-3 bg-border rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
:class="(stats.codex_rate_limits.primary.used_percent||0) > 80 ? 'bg-red-500' : (stats.codex_rate_limits.primary.used_percent||0) > 60 ? 'bg-amber-500' : 'bg-emerald-500'"
:style="'width: ' + Math.min(stats.codex_rate_limits.primary.used_percent||0, 100) + '%'"></div>
</div>
<template x-if="stats.codex_rate_limits.primary.seconds_until_reset !== null">
<div class="text-xs text-gray-500 mt-1"
x-text="'Resets in ' + formatResetTime(stats.codex_rate_limits.primary.seconds_until_reset)"></div>
</template>
</div>
<!-- Secondary window -->
<template x-if="stats.codex_rate_limits.secondary">
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-400 uppercase tracking-wide">Secondary
<template x-if="stats.codex_rate_limits.secondary.window_label">
<span class="text-gray-600 normal-case font-mono ml-1"
x-text="'(' + stats.codex_rate_limits.secondary.window_label + ')'"></span>
</template>
</span>
<span class="tabular-nums font-mono"
:class="(stats.codex_rate_limits.secondary.used_percent||0) > 80 ? 'text-red-400' : (stats.codex_rate_limits.secondary.used_percent||0) > 60 ? 'text-amber-400' : 'text-emerald-400'"
x-text="(stats.codex_rate_limits.secondary.used_percent||0).toFixed(1) + '%'"></span>
</div>
<div class="w-full h-3 bg-border rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
:class="(stats.codex_rate_limits.secondary.used_percent||0) > 80 ? 'bg-red-500' : (stats.codex_rate_limits.secondary.used_percent||0) > 60 ? 'bg-amber-500' : 'bg-emerald-500'"
:style="'width: ' + Math.min(stats.codex_rate_limits.secondary.used_percent||0, 100) + '%'"></div>
</div>
<template x-if="stats.codex_rate_limits.secondary.seconds_until_reset !== null">
<div class="text-xs text-gray-500 mt-1"
x-text="'Resets in ' + formatResetTime(stats.codex_rate_limits.secondary.seconds_until_reset)"></div>
</template>
</div>
</template>
</div>
<!-- Credits section (mirrors Anthropic extra-usage section) -->
<template x-if="stats.codex_rate_limits.credits">
<div class="border-t border-border pt-3 mb-3">
<div class="flex justify-between items-center">
<span class="text-xs text-gray-400 uppercase tracking-wide">Credits</span>
<span class="text-sm font-mono font-medium"
:class="stats.codex_rate_limits.credits.unlimited ? 'text-purple-400' : stats.codex_rate_limits.credits.has_credits ? 'text-emerald-400' : 'text-red-400'"
x-text="stats.codex_rate_limits.credits.unlimited ? '∞ Unlimited' : stats.codex_rate_limits.credits.balance ? stats.codex_rate_limits.credits.balance : (stats.codex_rate_limits.credits.has_credits ? 'Active' : 'No Credits')"></span>
</div>
<template x-if="stats.codex_rate_limits.credits.balance && !stats.codex_rate_limits.credits.unlimited">
<div class="text-xs text-gray-500 mt-0.5">Pay-as-you-go credits balance</div>
</template>
</div>
</template>
<!-- Promo message -->
<template x-if="stats.codex_rate_limits.promo_message">
<div class="mt-2 text-xs text-blue-400 italic"
x-text="stats.codex_rate_limits.promo_message"></div>
</template>
</div>
</template>
<!-- GitHub Copilot Quota -->
<template x-if="stats.copilot_quota && stats.copilot_quota.latest">
<div class="bg-surface rounded-lg p-4 border border-border mb-6">
<div class="flex justify-between items-center mb-4">
<div class="text-sm font-medium text-gray-300">GitHub Copilot Quota</div>
<div class="flex items-center gap-2">
<template x-if="stats.copilot_quota.latest.copilot_plan">
<span class="text-xs text-gray-500 font-mono capitalize" x-text="stats.copilot_quota.latest.copilot_plan + ' plan'"></span>
</template>
<template x-if="stats.copilot_quota.latest.login">
<span class="text-xs text-gray-400" x-text="'@' + stats.copilot_quota.latest.login"></span>
</template>
</div>
</div>
<!-- Per-category quota grid (3 columns, mirrors Anthropic 5h/7d structure) -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<template x-for="[catKey, catLabel] in [['chat','Chat'],['completions','Completions'],['premium_interactions','Premium']]" :key="catKey">
<template x-if="stats.copilot_quota.latest.categories[catKey]">
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-gray-400 uppercase tracking-wide" x-text="catLabel"></span>
<template x-if="stats.copilot_quota.latest.categories[catKey].unlimited">
<span class="text-purple-400 font-mono"></span>
</template>
<template x-if="!stats.copilot_quota.latest.categories[catKey].unlimited">
<span class="tabular-nums font-mono"
:class="(stats.copilot_quota.latest.categories[catKey].used_percent||0) > 80 ? 'text-red-400' : (stats.copilot_quota.latest.categories[catKey].used_percent||0) > 50 ? 'text-amber-400' : 'text-emerald-400'"
x-text="(stats.copilot_quota.latest.categories[catKey].used_percent||0).toFixed(0) + '%'"></span>
</template>
</div>
<!-- Used / entitlement count (big number like Anthropic window) -->
<template x-if="!stats.copilot_quota.latest.categories[catKey].unlimited">
<div class="flex items-baseline gap-1 mb-1">
<span class="text-lg font-light tabular-nums"
:class="(stats.copilot_quota.latest.categories[catKey].used_percent||0) > 80 ? 'text-red-400' : (stats.copilot_quota.latest.categories[catKey].used_percent||0) > 50 ? 'text-amber-400' : 'text-emerald-400'"
x-text="stats.copilot_quota.latest.categories[catKey].used ?? '-'"></span>
<span class="text-xs text-gray-500 font-mono"
x-text="'/ ' + (stats.copilot_quota.latest.categories[catKey].entitlement ?? '∞')"></span>
<span class="text-xs text-gray-500 ml-auto font-mono"
x-text="(stats.copilot_quota.latest.categories[catKey].remaining ?? '') + ' left'"></span>
</div>
</template>
<template x-if="stats.copilot_quota.latest.categories[catKey].unlimited">
<div class="text-lg font-light text-purple-400 mb-1">Unlimited</div>
</template>
<!-- Progress bar -->
<template x-if="!stats.copilot_quota.latest.categories[catKey].unlimited">
<div class="w-full h-3 bg-border rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-500"
:class="(stats.copilot_quota.latest.categories[catKey].used_percent||0) > 80 ? 'bg-red-500' : (stats.copilot_quota.latest.categories[catKey].used_percent||0) > 50 ? 'bg-amber-500' : 'bg-emerald-500'"
:style="'width: ' + Math.min(stats.copilot_quota.latest.categories[catKey].used_percent||0, 100) + '%'"></div>
</div>
</template>
<!-- Overage info -->
<template x-if="stats.copilot_quota.latest.categories[catKey].overage_count > 0">
<div class="text-xs text-amber-400 mt-1"
x-text="stats.copilot_quota.latest.categories[catKey].overage_count + ' overage uses'"></div>
</template>
<div class="mt-1">
<span class="text-xs"
:class="stats.copilot_quota.latest.categories[catKey].overage_permitted ? 'text-gray-500' : 'text-gray-600'"
x-text="stats.copilot_quota.latest.categories[catKey].overage_permitted ? 'Overage allowed' : 'No overage'"></span>
</div>
</div>
</template>
</template>
</div>
<!-- Monthly reset section (mirrors Anthropic extra-usage border section) -->
<div class="border-t border-border pt-3">
<div class="flex justify-between text-xs mb-2">
<span class="text-gray-400 uppercase tracking-wide">Monthly Reset</span>
<template x-if="stats.copilot_quota.latest.quota_reset_date_utc">
<span class="text-gray-400 font-mono"
x-text="formatMonthlyReset(stats.copilot_quota.latest.quota_reset_date_utc)"></span>
</template>
</div>
<template x-if="stats.copilot_quota.latest.quota_reset_date_utc">
<div>
<!-- Month-elapsed bar: filled = time elapsed, empty = time remaining -->
<div class="w-full h-2 bg-border rounded-full overflow-hidden">
<div class="h-full bg-accent/50 rounded-full transition-all duration-500"
:style="'width: ' + (100 - Math.min(100, Math.max(0, (new Date(stats.copilot_quota.latest.quota_reset_date_utc) - new Date()) / (30 * 86400000) * 100))) + '%'"></div>
</div>
<div class="flex justify-between text-xs text-gray-600 mt-0.5">
<span>Month start</span>
<span x-text="new Date(stats.copilot_quota.latest.quota_reset_date_utc).toLocaleDateString(undefined, {month:'short', day:'numeric'})"></span>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<!-- Lifetime view: durable aggregate metrics only -->
<template x-if="viewMode === 'lifetime'">
<div>
<div class="mb-6 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<p class="text-xs text-gray-500" x-text="'Lifetime data since ' + (lifetimeStats.started_at || '—')"></p>
<p class="text-xs text-gray-500" x-text="'Full metric coverage since ' + (lifetimeStats.full_fidelity_started_at || '—')"></p>
</div>
<div class="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-gray-500">
<span>Durable local savings history from <span class="font-mono">/stats-lifetime</span></span>
<span aria-hidden="true">-</span>
<span :class="lifetimeStats.persistence?.healthy === false ? 'text-amber-400' : 'text-emerald-400'"
x-text="lifetimeStats.persistence?.healthy === false ? ('Persistence degraded: ' + (lifetimeStats.persistence?.error || 'unknown error')) : 'Persistence healthy'"></span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-6 md:grid-cols-4">
<div class="bg-surface rounded-lg p-4 border border-border"><div class="text-xs text-gray-500">Requests</div><div class="text-2xl tabular-nums" x-text="formatNumber(lifetimeStats.requests?.total || 0)"></div></div>
<div class="bg-surface rounded-lg p-4 border border-border"><div class="text-xs text-gray-500">Failed</div><div class="text-2xl tabular-nums text-red-400" x-text="formatNumber(lifetimeStats.requests?.failed || 0)"></div></div>
<div class="bg-surface rounded-lg p-4 border border-border"><div class="text-xs text-gray-500">Rate Limited</div><div class="text-2xl tabular-nums text-amber-400" x-text="formatNumber(lifetimeStats.requests?.rate_limited || 0)"></div></div>
<div class="bg-surface rounded-lg p-4 border border-border"><div class="text-xs text-gray-500">Cached</div><div class="text-2xl tabular-nums text-cyan-400" x-text="formatNumber(lifetimeStats.requests?.cached || 0)"></div></div>
</div>
<div class="grid grid-cols-1 gap-4 mb-6 lg:grid-cols-3">
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Tokens</h3><dl class="space-y-2 text-sm"><div class="flex justify-between"><dt>Input</dt><dd x-text="formatNumber(lifetimeStats.tokens?.input || 0)"></dd></div><div class="flex justify-between"><dt>Output</dt><dd x-text="formatNumber(lifetimeStats.tokens?.output || 0)"></dd></div><div class="flex justify-between"><dt>Attempted Input</dt><dd x-text="formatNumber(lifetimeStats.tokens?.attempted_input || 0)"></dd></div><div class="flex justify-between"><dt>Saved</dt><dd class="text-emerald-400" x-text="formatNumber(lifetimeStats.tokens?.saved || 0)"></dd></div><div class="flex justify-between"><dt>Token Savings</dt><dd x-text="lifetimeStats.tokens?.token_savings_percent == null ? '—' : lifetimeStats.tokens.token_savings_percent.toFixed(1) + '%'"></dd></div></dl></section>
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Cost</h3><dl class="space-y-2 text-sm"><div class="flex justify-between"><dt>Input cost</dt><dd x-text="'$' + formatCurrency(lifetimeStats.cost?.input_usd || 0)"></dd></div><div class="flex justify-between"><dt>Compression saved</dt><dd class="text-emerald-400" x-text="'$' + formatCurrency(lifetimeStats.cost?.compression_savings_usd || 0)"></dd></div><div class="flex justify-between"><dt>Prefix Cache saved</dt><dd class="text-cyan-400" x-text="'$' + formatCurrency(lifetimeStats.cost?.cache_savings_usd || 0)"></dd></div></dl></section>
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Prefix Cache</h3><dl class="space-y-2 text-sm"><div class="flex justify-between"><dt>Hits / requests</dt><dd x-text="formatNumber(lifetimeStats.prefix_cache?.hit_requests || 0) + ' / ' + formatNumber(lifetimeStats.prefix_cache?.requests || 0)"></dd></div><div class="flex justify-between"><dt>Hit rate</dt><dd x-text="lifetimeStats.prefix_cache?.cache_hit_rate == null ? '—' : lifetimeStats.prefix_cache.cache_hit_rate.toFixed(1) + '%'"></dd></div><div class="flex justify-between"><dt>Read / write</dt><dd x-text="formatNumber(lifetimeStats.prefix_cache?.cache_read_tokens || 0) + ' / ' + formatNumber(lifetimeStats.prefix_cache?.cache_write_tokens || 0)"></dd></div><div class="flex justify-between"><dt>TTL 1h / 5m</dt><dd x-text="(lifetimeStats.prefix_cache?.ttl_1h_percent == null ? '—' : lifetimeStats.prefix_cache.ttl_1h_percent.toFixed(0) + '%') + ' / ' + (lifetimeStats.prefix_cache?.ttl_5m_percent == null ? '—' : lifetimeStats.prefix_cache.ttl_5m_percent.toFixed(0) + '%')"></dd></div><div class="flex justify-between"><dt>Cache bust</dt><dd x-text="formatNumber(lifetimeStats.prefix_cache?.bust_count || 0) + ' / ' + formatNumber(lifetimeStats.prefix_cache?.bust_tokens || 0)"></dd></div></dl></section>
</div>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Cache Miss Attribution</h3><template x-for="[name, count] in Object.entries(lifetimeStats.prefix_cache?.misses_by_reason || {})" :key="name"><div class="flex justify-between text-sm"><span x-text="name"></span><span x-text="formatNumber(count)"></span></div></template></section>
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Waste Signals</h3><template x-for="[name, count] in Object.entries(lifetimeStats.waste_signals || {})" :key="name"><div class="flex justify-between text-sm"><span x-text="name"></span><span x-text="formatNumber(count)"></span></div></template></section>
</div>
<div class="grid grid-cols-1 gap-4 mt-4 lg:grid-cols-3">
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Providers</h3><template x-for="[name, count] in Object.entries(lifetimeStats.requests?.by_provider || {})" :key="name"><div class="flex justify-between text-sm"><span x-text="name"></span><span x-text="formatNumber(count)"></span></div></template></section>
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Stacks</h3><template x-for="[name, count] in Object.entries(lifetimeStats.requests?.by_stack || {})" :key="name"><div class="flex justify-between text-sm"><span x-text="name"></span><span x-text="formatNumber(count)"></span></div></template></section>
<section class="bg-surface rounded-lg p-4 border border-border"><h3 class="mb-3 text-sm text-gray-300">Top Models + Other</h3><template x-for="[name, metric] in Object.entries(lifetimeStats.by_model || {})" :key="name"><div class="flex justify-between text-sm"><span class="truncate pr-3" x-text="name"></span><span x-text="formatNumber(metric.input_tokens + metric.output_tokens)"></span></div></template></section>
</div>
<!-- Per-Project Savings Breakdown -->
<section class="bg-surface rounded-lg border border-border overflow-hidden mt-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>
<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(lifetimeStats.projects || {}).length + ' project(s)'"></span>
</div>
<template x-if="Object.keys(lifetimeStats.projects || {}).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">
Route project traffic through
<span class="text-accent" x-text="window.location.origin + '/p/&lt;project-name&gt;'"></span>
</div>
</div>
</template>
<template x-if="Object.keys(lifetimeStats.projects || {}).length > 0">
<div class="overflow-x-auto">
<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 class="text-xs text-gray-500 tracking-wide">
<tr>
<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 md:table-cell">Last Active</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<template x-for="[project, info] in Object.entries(lifetimeStats.projects || {})" :key="project">
<tr class="hover:bg-border/30 transition-colors">
<td class="px-4 py-3 text-left">
<span class="px-2 py-0.5 bg-border text-xs font-mono" x-text="project"></span>
</td>
<td class="px-4 py-3 text-right font-mono tabular-nums"
x-text="formatNumber(info.requests || 0)"></td>
<td class="px-4 py-3 text-right font-mono tabular-nums text-accent"
x-text="formatNumber(info.tokens_saved || 0)"></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">
<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 md:table-cell"
x-text="info.last_activity_at ? new Date(info.last_activity_at).toLocaleString() : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</section>
</div>
</template>
<template x-if="viewMode === 'history'">
<div>
<div class="flex flex-col gap-4 mb-6 lg:flex-row lg:items-center lg:justify-between">
<div>
<div class="text-sm font-medium text-gray-300">Historical Proxy Compression</div>
<div class="text-xs text-gray-500">
Durable local savings history from <span class="font-mono">/stats-history</span>
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<div class="flex items-center gap-2">
<button class="px-3 py-1.5 text-sm rounded-md border border-border bg-surface text-gray-300 hover:text-white transition-colors"
@click="downloadHistory('json')">
Export JSON
</button>
<button class="px-3 py-1.5 text-sm rounded-md border border-border bg-surface text-gray-300 hover:text-white transition-colors"
@click="downloadHistory('csv')">
Export CSV
</button>
</div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4 mb-6">
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Lifetime Compression Savings</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums text-emerald-400"
x-text="'$' + formatCurrency(historyStats.lifetime?.compression_savings_usd || 0)"></span>
</div>
<div class="mt-2 text-xs text-gray-500">
Proxy compression only
</div>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Lifetime Tokens Saved</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums text-accent"
x-text="formatNumber(historyStats.lifetime?.tokens_saved || 0)"></span>
</div>
<div class="mt-2 text-xs text-gray-500"
x-text="formatNumber((historyStats.history || []).length) + ' recorded checkpoints'"></div>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Active Days</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums"
x-text="formatNumber((historyStats.series?.daily || []).length)"></span>
</div>
<div class="mt-2 text-xs text-gray-500" x-text="historyWindowLabel"></div>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Average Saved / Day</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums"
x-text="formatNumber(historyAverageTokensPerDay)"></span>
</div>
<div class="mt-2 text-xs text-gray-500">Based on persisted daily buckets</div>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Average Saved / Week</div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums"
x-text="formatNumber(historyAverageTokensPerWeek)"></span>
</div>
<div class="mt-2 text-xs text-gray-500">Based on persisted weekly buckets</div>
</div>
<template x-if="historyStats.cli_filtering && historyCliFilteringAvailable">
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1"
x-text="(historyStats.cli_filtering?.label || cliFilteringLabel) + ' Lifetime Saved'"></div>
<div class="flex items-baseline gap-2">
<span class="text-3xl font-light tabular-nums text-cyan-400"
x-text="formatNumber(historyStats.cli_filtering?.lifetime?.tokens_saved || 0)"></span>
</div>
<div class="mt-2 text-xs text-gray-500">CLI output filtering (lifetime)</div>
</div>
</template>
</div>
<template x-if="hasHistoricalData">
<div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Daily Savings</div>
<template x-if="(historyStats.series?.daily || []).length > 0">
<div class="space-y-3">
<template x-for="point in recentDailyHistory" :key="point.timestamp">
<div class="flex items-center justify-between">
<div>
<div class="text-sm text-gray-300" x-text="formatDate(point.timestamp)"></div>
<div class="text-xs text-gray-500"
x-text="formatNumber(point.total_tokens_saved) + ' cumulative'"></div>
</div>
<div class="text-right">
<div class="font-mono text-sm text-accent"
x-text="formatNumber(point.tokens_saved) + ' tokens'"></div>
<div class="text-xs text-emerald-400"
x-text="'$' + formatCurrency(point.compression_savings_usd_delta || 0)"></div>
</div>
</div>
</template>
</div>
</template>
<template x-if="(historyStats.series?.daily || []).length === 0">
<div class="text-sm text-gray-500 italic py-8 text-center">
Daily rollups will appear after persisted history spans at least one checkpoint.
</div>
</template>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Weekly Savings</div>
<template x-if="(historyStats.series?.weekly || []).length > 0">
<div class="space-y-3">
<template x-for="point in recentWeeklyHistory" :key="point.timestamp">
<div class="flex items-center justify-between">
<div>
<div class="text-sm text-gray-300" x-text="formatDate(point.timestamp)"></div>
<div class="text-xs text-gray-500"
x-text="formatNumber(point.total_tokens_saved) + ' cumulative'"></div>
</div>
<div class="text-right">
<div class="font-mono text-sm text-accent"
x-text="formatNumber(point.tokens_saved) + ' tokens'"></div>
<div class="text-xs text-emerald-400"
x-text="'$' + formatCurrency(point.compression_savings_usd_delta || 0)"></div>
</div>
</div>
</template>
</div>
</template>
<template x-if="(historyStats.series?.weekly || []).length === 0">
<div class="text-sm text-gray-500 italic py-8 text-center">
Weekly rollups will appear after persisted history spans multiple days.
</div>
</template>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Monthly Savings</div>
<template x-if="(historyStats.series?.monthly || []).length > 0">
<div class="space-y-3">
<template x-for="point in recentMonthlyHistory" :key="point.timestamp">
<div class="flex items-center justify-between">
<div>
<div class="text-sm text-gray-300" x-text="formatMonth(point.timestamp)"></div>
<div class="text-xs text-gray-500"
x-text="formatNumber(point.total_tokens_saved) + ' cumulative'"></div>
</div>
<div class="text-right">
<div class="font-mono text-sm text-accent"
x-text="formatNumber(point.tokens_saved) + ' tokens'"></div>
<div class="text-xs text-emerald-400"
x-text="'$' + formatCurrency(point.compression_savings_usd_delta || 0)"></div>
</div>
</div>
</template>
</div>
</template>
<template x-if="(historyStats.series?.monthly || []).length === 0">
<div class="text-sm text-gray-500 italic py-8 text-center">
Monthly rollups will appear after persisted history spans multiple months.
</div>
</template>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
<div class="bg-surface rounded-lg p-4 border border-border lg:col-span-2">
<div class="flex flex-col gap-3 mb-4 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-300">Historical Savings Trend</span>
<span class="text-xs text-gray-500 font-mono" x-text="historyTrendLabel"></span>
</div>
<div class="flex flex-wrap gap-2 self-start lg:self-auto">
<div class="inline-flex rounded-lg border border-border p-1" style="background: var(--card-alt-bg);">
<template x-for="[label, key] in historyGranularityOptions" :key="key">
<button class="px-3 py-1.5 text-sm rounded-md transition-colors"
:class="historyGranularity === key ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
@click="historyGranularity = key"
x-text="label"></button>
</template>
</div>
<div class="inline-flex rounded-lg border border-border p-1" style="background: var(--card-alt-bg);">
<template x-for="[label, key] in historyChartModeOptions" :key="key">
<button class="px-3 py-1.5 text-sm rounded-md transition-colors"
:class="historyChartMode === key ? 'bg-accent text-black' : 'text-gray-400 hover:text-gray-200'"
@click="historyChartMode = key"
x-text="label"></button>
</template>
</div>
</div>
</div>
<div class="text-xs text-gray-500 mb-4"
x-text="'Showing ' + formatNumber(historySelectedPointCount) + ' ' + historySelectedSeriesLabel.toLowerCase() + ' points'"></div>
<template x-if="historicalTrend.length >= 2">
<div class="h-48">
<svg class="w-full h-full" viewBox="0 0 200 64" preserveAspectRatio="none">
<defs>
<linearGradient id="history-trend-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#22d3ee;stop-opacity:0.2"/>
<stop offset="100%" style="stop-color:#22d3ee;stop-opacity:0"/>
</linearGradient>
</defs>
<path :d="historyChartMode === 'tokens' && !historyActiveModel ? getObjectTrendArea(historicalTrend, 'total_tokens_saved') : ''"
fill="url(#history-trend-gradient)"></path>
<path class="trend-line"
:d="historyChartMode === 'tokens' && !historyActiveModel ? getObjectTrendLine(historicalTrend, 'total_tokens_saved') : ''"></path>
<path fill="none" stroke-width="1"
:stroke="historyModelColor(0)" :d="historyModelLine(0)"></path>
<path fill="none" stroke-width="1"
:stroke="historyModelColor(1)" :d="historyModelLine(1)"></path>
<path fill="none" stroke-width="1"
:stroke="historyModelColor(2)" :d="historyModelLine(2)"></path>
<path fill="none" stroke-width="1"
:stroke="historyModelColor(3)" :d="historyModelLine(3)"></path>
<path fill="none" stroke-width="1"
:stroke="historyModelColor(4)" :d="historyModelLine(4)"></path>
<path fill="none" stroke="#22d3ee" stroke-width="1.5"
:d="historyCostLine('actual')"></path>
<path fill="none" stroke="#fbbf24" stroke-width="1.5" stroke-dasharray="3 2"
:d="historyCostLine('expected')"></path>
</svg>
</div>
</template>
<template x-if="historicalTrend.length < 2">
<div class="h-48 flex items-center justify-center text-sm text-gray-500 italic">
Historical trend data will appear after more saved checkpoints in this granularity.
</div>
</template>
<div class="flex flex-wrap gap-3 mt-3"
x-show="historyChartMode === 'tokens' && historyGranularity !== 'history' && historyModelChartSeries.length > 0">
<template x-for="(series, index) in historyModelChartSeries" :key="series.model">
<button class="flex items-center gap-1.5 text-xs transition-opacity cursor-pointer"
:class="historyActiveModel && historyActiveModel !== series.model
? 'text-gray-600 opacity-50'
: 'text-gray-400 hover:text-gray-200'"
@click="toggleHistoryModel(series.model)"
:title="historySelectedModel === series.model
? 'Show all models'
: 'Show only this model'">
<span class="inline-block w-2 h-2 rounded-full"
:style="'background:' + historyModelColor(index)"></span>
<span :class="historySelectedModel === series.model ? 'underline' : ''"
x-text="truncateModel(series.model)"></span>
</button>
</template>
</div>
<div class="flex flex-wrap gap-4 mt-3" x-show="historyChartMode === 'cost'">
<span class="flex items-center gap-1.5 text-xs text-gray-400">
<span class="inline-block w-3 h-0.5" style="background:#22d3ee"></span>
Actual cost (with Headroom)
</span>
<span class="flex items-center gap-1.5 text-xs text-gray-400">
<span class="inline-block w-3 h-0.5"
style="background:repeating-linear-gradient(90deg,#fbbf24 0 4px,transparent 4px 7px)"></span>
Expected cost (without Headroom)
</span>
</div>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="flex items-center justify-between mb-4">
<span class="text-sm font-medium text-gray-300">Per-Model Breakdown</span>
<span class="text-xs text-gray-500 font-mono"
x-text="historyModelSourceSeriesLabel + ' buckets'"></span>
</div>
<template x-if="historyModelBreakdown.length === 0">
<div class="text-sm text-gray-500 italic">
Per-model attribution appears for checkpoints recorded after upgrading.
</div>
</template>
<template x-if="historyModelBreakdown.length > 0">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="text-xs text-gray-500 text-left">
<th class="py-2 pr-4 font-medium">Model</th>
<th class="py-2 pr-4 font-medium text-right">Tokens saved</th>
<th class="py-2 pr-4 font-medium text-right">Cost with Headroom</th>
<th class="py-2 pr-4 font-medium text-right">Expected cost without Headroom</th>
<th class="py-2 font-medium text-right">Saved</th>
</tr>
</thead>
<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-card-alt' : 'hover-bg-surface'"
@click="toggleHistoryModel(row.model)"
:title="historySelectedModel === row.model
? 'Show all models'
: 'Show only this model in the chart'">
<td class="py-2 pr-4 font-mono text-xs"
:class="historySelectedModel === row.model ? 'underline' : ''"
x-text="truncateModel(row.model)"></td>
<td class="py-2 pr-4 text-right tabular-nums" x-text="formatNumber(row.tokens_saved)"></td>
<td class="py-2 pr-4 text-right tabular-nums" x-text="'$' + formatCurrency(row.input_cost_usd)"></td>
<td class="py-2 pr-4 text-right tabular-nums" x-text="'$' + formatCurrency(row.expected_cost_usd)"></td>
<td class="py-2 text-right tabular-nums text-accent" x-text="'$' + formatCurrency(row.savings_usd)"></td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Historical Summary</div>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Latest total</span>
<span class="font-mono text-sm"
x-text="formatNumber(historyStats.lifetime?.tokens_saved || 0) + ' tokens'"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Selected series</span>
<span class="font-mono text-sm"
x-text="historySelectedSeriesLabel"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Selected points</span>
<span class="font-mono text-sm"
x-text="formatNumber(historySelectedPointCount)"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Average / month</span>
<span class="font-mono text-sm"
x-text="formatNumber(historyAverageTokensPerMonth) + ' tokens'"></span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm text-gray-400">Retention</span>
<span class="font-mono text-sm"
x-text="(historyStats.retention?.max_history_age_days || 0) + 'd / ' + formatNumber(historyStats.retention?.max_history_points || 0)"></span>
</div>
</div>
</div>
</div>
<div class="bg-surface rounded-lg p-4 border border-border">
<div class="text-sm font-medium mb-4 text-gray-300">Recent Historical Checkpoints</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<template x-for="point in recentHistoricalPoints" :key="point.timestamp">
<div class="flex items-center justify-between rounded-lg border border-border px-3 py-3">
<div>
<div class="text-sm text-gray-300" x-text="formatDateTime(point.timestamp)"></div>
<div class="text-xs text-gray-500">Cumulative proxy compression savings</div>
</div>
<div class="text-right">
<div class="font-mono text-sm text-accent"
x-text="formatNumber(point.total_tokens_saved) + ' tokens'"></div>
<div class="text-xs text-emerald-400"
x-text="'$' + formatCurrency(point.compression_savings_usd || 0)"></div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<template x-if="!hasHistoricalData">
<div class="bg-surface rounded-lg border border-border p-10 text-center">
<div class="text-lg text-gray-300 mb-2">No persisted savings history yet</div>
<div class="text-sm text-gray-500 max-w-2xl mx-auto">
Historical data is written locally after proxy requests save tokens. Keep using Headroom and this view will fill in automatically across restarts.
</div>
</div>
</template>
</div>
</template>
</main>
<!-- Live Feed Sidebar Drawer -->
<div x-show="feedOpen"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="translate-x-full"
x-transition:enter-end="translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="translate-x-0"
x-transition:leave-end="translate-x-full"
@click.away="feedOpen = false"
class="fixed top-0 right-0 h-full w-[520px] bg-surface border-l border-border z-50 flex flex-col"
style="display: none;">
<!-- Drawer Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-border">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-200">Message Transformations</span>
<span class="text-xs text-gray-500 font-mono" x-text="transformations.length + ' msgs'"></span>
</div>
<button @click="feedOpen = false" class="text-gray-500 hover:text-gray-200 transition-colors p-1">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 16 16" fill="currentColor">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854z"/>
</svg>
</button>
</div>
<!-- New messages indicator -->
<div x-show="feedScrolled_ && feedNewCount > 0"
@click="scrollToFeedTop()"
class="absolute top-16 right-4 px-3 py-2 bg-accent text-black text-sm rounded-lg shadow-lg cursor-pointer z-50 font-medium">
<span x-text="feedNewCount"></span> new message<span x-show="feedNewCount > 1">s</span>
</div>
<!-- Drawer Content (virtual scroll container) -->
<div class="flex-1 overflow-y-auto" id="feed-container"
@scroll="handleFeedScroll()">
<div id="feed-virtual-list" class="relative"></div>
</div>
</div>
<!-- Footer -->
<footer class="border-t border-border px-6 py-4 mt-8">
<div class="flex justify-between items-center text-xs text-gray-500">
<div>
Press <kbd class="px-1.5 py-0.5 bg-border rounded text-gray-400">R</kbd> to refresh
</div>
<div>
<a href="https://headroom-docs.vercel.app/docs" target="_blank" class="hover:text-gray-300 transition-colors">Documentation</a>
</div>
</div>
</footer>
<script>
function dashboard() {
return {
stats: {},
lifetimeStats: {},
historyStats: {},
healthy: true,
version: 'loading',
lastUpdate: 'never',
viewMode: 'session',
historyGranularity: 'daily',
historyChartMode: 'tokens',
historySelectedModel: null,
requestHistory: [],
savingsHistory: [],
expandedRows: {},
pollInterval: null,
statsPollMs: 5000,
lifetimePollMs: 30000,
historyPollMs: 30000,
feedPollMs: 5000,
lastLifetimeFetchMs: 0,
lastHistoryFetchMs: 0,
lastFeedFetchMs: 0,
feedOpen: false,
transformations: [],
feedScrolled_: false,
feedNewCount: 0,
feedScrollY: 0,
feedItemHeight: 160,
feedBuffer: 5,
log_full_messages: false,
async init() {
await this.fetchStats();
this.pollInterval = setInterval(() => {
this.pollDashboard();
}, this.statsPollMs);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') {
this.pollDashboard(true);
}
});
},
async pollDashboard(force = false) {
if (!force && document.hidden) return;
await this.fetchStats();
const now = Date.now();
if (this.viewMode === 'lifetime' && (force || now - this.lastLifetimeFetchMs >= this.lifetimePollMs)) {
await this.fetchLifetimeStats();
}
if (this.viewMode === 'history' && (force || now - this.lastHistoryFetchMs >= this.historyPollMs)) {
await this.fetchHistoryStats();
}
if (this.feedOpen && (force || now - this.lastFeedFetchMs >= this.feedPollMs)) {
await this.fetchTransformations();
}
},
async setViewMode(mode) {
this.viewMode = mode;
if (mode === 'lifetime') {
await this.fetchLifetimeStats();
}
if (mode === 'history') {
await this.fetchHistoryStats();
}
},
async toggleFeed() {
this.feedOpen = !this.feedOpen;
if (this.feedOpen) {
await this.fetchTransformations();
}
},
formatVersion(value) {
const label = String(value || 'unknown').trim();
if (label === 'loading' || label === 'unknown') return label;
return /^\d+\.\d+\.\d+$/.test(label) ? 'v' + label : label;
},
async fetchStats() {
try {
const [statsRes, healthRes] = await Promise.all([
fetch('/stats?cached=1'),
fetch('/health')
]);
this.stats = await statsRes.json();
const health = await healthRes.json();
this.healthy = health.status === 'healthy';
this.version = health.version || 'unknown';
this.log_full_messages = this.stats.log_full_messages || false;
// Update history for sparklines
this.requestHistory.push(this.stats.requests?.total || 0);
this.savingsHistory.push(this.stats.tokens?.saved || 0);
// Keep last 30 points
if (this.requestHistory.length > 30) this.requestHistory.shift();
if (this.savingsHistory.length > 30) this.savingsHistory.shift();
this.lastUpdate = new Date().toLocaleTimeString();
} catch (e) {
console.error('Failed to fetch stats:', e);
this.healthy = false;
}
},
async fetchLifetimeStats() {
try {
const response = await fetch('/stats-lifetime');
if (response.ok) {
this.lifetimeStats = await response.json();
this.lastLifetimeFetchMs = Date.now();
}
} catch (e) {
console.error('Failed to fetch lifetime stats:', e);
}
},
async fetchHistoryStats() {
try {
const response = await fetch('/stats-history');
if (response.ok) {
this.historyStats = await response.json();
this.lastHistoryFetchMs = Date.now();
}
} catch (e) {
console.error('Failed to fetch history stats:', e);
}
},
async fetchTransformations() {
try {
const prevLen = this.transformations.length;
const response = await fetch('/transformations/feed?limit=50');
if (response.ok) {
const data = await response.json();
const newLen = (data.transformations || []).length;
if (this.feedScrolled_ && newLen > prevLen) {
this.feedNewCount = newLen - prevLen;
}
this.transformations = data.transformations || [];
this.log_full_messages = data.log_full_messages ?? this.log_full_messages;
this.lastFeedFetchMs = Date.now();
this.renderTransformations();
}
} catch (e) {
console.error('Failed to fetch transformations:', e);
}
},
scrollToFeedTop() {
const container = document.getElementById('feed-container');
if (container) {
container.scrollTop = 0;
this.feedScrolled_ = false;
this.feedNewCount = 0;
this.renderTransformations();
}
},
handleFeedScroll() {
const container = document.getElementById('feed-container');
if (!container) return;
this.feedScrollY = container.scrollTop;
this.feedScrolled_ = container.scrollTop > 50;
this.renderTransformations();
},
renderTransformations() {
const container = document.getElementById('feed-virtual-list');
if (!container) return;
const scrollTop = this.feedScrollY;
const viewportHeight = (document.getElementById('feed-container')?.clientHeight || 600);
const totalHeight = this.transformations.length * this.feedItemHeight;
container.style.height = totalHeight + 'px';
// Calculate visible range with buffer
const startIdx = Math.max(0, Math.floor(scrollTop / this.feedItemHeight) - this.feedBuffer);
const endIdx = Math.min(
this.transformations.length,
Math.ceil((scrollTop + viewportHeight) / this.feedItemHeight) + this.feedBuffer
);
const visible = this.transformations.slice(startIdx, endIdx);
const offsetTop = startIdx * this.feedItemHeight;
let html = `<div style="position: absolute; top: ${offsetTop}px; width: 100%;">`;
html += visible.map((t, i) => this.renderTransformationCard(t, startIdx + i)).join('');
html += '</div>';
container.innerHTML = html;
},
renderTransformationCard(t, idx) {
const msgs = (t.request_messages || []).map(m => m.content || '').join('');
const response = t.response_content || '';
const hasMessages = msgs.length > 0 || response.length > 0;
const before = msgs.substring(0, 2000) + (msgs.length > 2000 ? '\n\n[truncated]' : '');
const after = response.substring(0, 2000) + (response.length > 2000 ? '\n\n[truncated]' : '');
const time = t.timestamp ? new Date(t.timestamp).toLocaleTimeString() : '--:--:--';
const model = (t.model || 'unknown').replace(/^(anthropic\.|openai\.)/, '').substring(0, 25);
const tokensSaved = t.tokens_saved || 0;
const savingsPct = ((t.savings_percent || 0)).toFixed(0);
const emptyState = '<span class="text-gray-600 italic">Enable HEADROOM_LOG_MESSAGES=true to see content</span>';
const beforeContent = hasMessages ? this.escapeHtml(before) : emptyState;
const afterContent = hasMessages ? this.escapeHtml(after) : emptyState;
return `
<div class="transformation-card border-b border-border p-3" data-idx="${idx}" style="height: ${this.feedItemHeight}px; box-sizing: border-box;">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2 min-w-0">
<span class="text-xs font-mono text-gray-400 truncate">${this.escapeHtml(model)}</span>
<span class="text-xs text-gray-600 shrink-0">·</span>
<span class="text-xs text-emerald-400 shrink-0">${tokensSaved} tok</span>
<span class="text-xs text-gray-600 shrink-0">(${savingsPct}%)</span>
${(t.tool_schema_saved_tokens || 0) > 0 ? `<span class="text-xs text-cyan-400 shrink-0" title="Tool-definition tokens deferred out of context">+${t.tool_schema_saved_tokens} tool-schema</span>` : ''}
</div>
<span class="text-xs text-gray-600 shrink-0">${time}</span>
</div>
<div class="grid grid-cols-2 gap-2" style="height: 115px;">
<div class="rounded border border-red-900/30 overflow-hidden flex flex-col">
<div class="diff-before-header px-2 py-1 border-b border-red-900/30 shrink-0">
<span class="text-[10px] text-red-400 uppercase tracking-wide font-semibold">Before</span>
</div>
<div class="diff-before p-2 font-mono text-[11px] text-gray-300 overflow-auto flex-1"
>${beforeContent}</div>
</div>
<div class="rounded border border-emerald-900/30 overflow-hidden flex flex-col">
<div class="diff-after-header px-2 py-1 border-b border-emerald-900/30 shrink-0">
<span class="text-[10px] text-emerald-400 uppercase tracking-wide font-semibold">After</span>
</div>
<div class="diff-after p-2 font-mono text-[11px] text-gray-300 overflow-auto flex-1"
>${afterContent}</div>
</div>
</div>
${(t.transforms_applied || []).length > 0 ? `
<div class="flex flex-wrap gap-1 mt-1.5">
${t.transforms_applied.slice(0, 4).map(tr => `<span class="text-[9px] px-1.5 py-0.5 bg-border rounded font-mono text-gray-500">${this.escapeHtml(tr)}</span>`).join('')}
${t.transforms_applied.length > 4 ? `<span class="text-[9px] text-gray-600">+${t.transforms_applied.length - 4}</span>` : ''}
</div>
` : ''}
</div>
`;
},
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
},
// --- Formatting ---
formatNumber(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
return n.toString();
},
formatCurrency(n) {
if (n < 0) return '-' + this.formatCurrency(-n);
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
if (n >= 1) return n.toFixed(2);
if (n >= 0.01) return n.toFixed(3);
if (n > 0) return n.toFixed(4);
return '0.00';
},
formatResetTime(seconds) {
if (seconds == null || seconds <= 0) return 'now';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (h > 0) return h + 'h ' + m + 'm';
if (m > 0) return m + 'm ' + Math.floor(seconds % 60) + 's';
return Math.floor(seconds) + 's';
},
formatTime(ts) {
if (!ts) return '-';
const d = new Date(ts);
const now = new Date();
const diff = (now - d) / 1000;
if (diff < 60) return Math.floor(diff) + 's ago';
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
return d.toLocaleTimeString();
},
formatDate(ts) {
if (!ts) return '-';
return new Date(ts).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
});
},
formatMonthlyReset(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr);
const days = Math.ceil((d - new Date()) / 86400000);
if (days <= 0) return 'today';
if (days === 1) return 'tomorrow';
return 'in ' + days + ' days (' + d.toLocaleDateString(undefined, {month: 'short', day: 'numeric'}) + ')';
},
formatMonth(ts) {
if (!ts) return '-';
return new Date(ts).toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',
});
},
formatDateTime(ts) {
if (!ts) return '-';
return new Date(ts).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
},
truncateModel(model) {
if (!model) return '-';
return model.replace(/^(anthropic\.|openai\.|bedrock\/)/, '')
.replace(/-\d{8}$/, '')
.substring(0, 20);
},
// --- Agent Usage ---
get agentRows() {
return this.stats.agent_usage?.agents || [];
},
get agentCoverageLabel() {
const coverage = this.stats.agent_usage?.coverage || {};
if (coverage.mode === 'request_logs') {
return this.formatNumber(coverage.logged_requests || 0) + ' logged requests';
}
return 'aggregate fallback';
},
agentSavedWidth(agent) {
const before = agent.before_tokens || 0;
if (before <= 0) return 0;
return Math.min(100, Math.max(0, (agent.tokens_saved || 0) / before * 100)).toFixed(1);
},
agentAfterWidth(agent) {
const before = agent.before_tokens || 0;
if (before <= 0) return 0;
return Math.min(100, Math.max(0, (agent.after_tokens || 0) / before * 100)).toFixed(1);
},
agentDotClass(agent) {
const colors = {
'claude-code': 'bg-orange-400',
claude: 'bg-orange-400',
codex: 'bg-emerald-400',
cursor: 'bg-cyan-400',
copilot: 'bg-violet-400',
openai: 'bg-sky-400',
anthropic: 'bg-orange-400',
gemini: 'bg-rose-400',
aider: 'bg-amber-400',
unknown: 'bg-gray-500',
};
return colors[agent] || 'bg-gray-400';
},
// --- Historical View ---
get historyGranularityOptions() {
return [
['Daily', 'daily'],
['Weekly', 'weekly'],
['Monthly', 'monthly'],
['Checkpoints', 'history'],
];
},
get historyChartModeOptions() {
return [
['Tokens', 'tokens'],
['Cost', 'cost'],
];
},
get historyModelSourceSeries() {
// Rollup buckets carrying by_model attribution. Raw checkpoints
// have no by_model, so the checkpoint view falls back to daily.
const key = this.historySelectedSeriesKey === 'history'
? 'daily'
: this.historySelectedSeriesKey;
return this.historyStats.series?.[key] || [];
},
get historyModelChartSeries() {
const buckets = this.historyModelSourceSeries;
const totals = {};
for (const bucket of buckets) {
for (const [model, entry] of Object.entries(bucket.by_model || {})) {
totals[model] = (totals[model] || 0) + (entry.tokens_saved || 0);
}
}
const topModels = Object.entries(totals)
.filter(([, saved]) => saved > 0)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([model]) => model);
// A breakdown-row selection outside the top 5 takes the
// last chart slot so the filter works for every row (the
// template renders a fixed set of line slots).
const selected = this.historySelectedModel;
if (
selected &&
(totals[selected] || 0) > 0 &&
topModels.length > 0 &&
!topModels.includes(selected)
) {
topModels[topModels.length - 1] = selected;
}
return topModels.map(model => {
let running = 0;
return {
model,
values: buckets.map(bucket => {
running += bucket.by_model?.[model]?.tokens_saved || 0;
return running;
}),
};
});
},
get historyModelBreakdown() {
const totals = {};
for (const bucket of this.historyModelSourceSeries) {
for (const [model, entry] of Object.entries(bucket.by_model || {})) {
const row = totals[model] || (totals[model] = {
model,
tokens_saved: 0,
savings_usd: 0,
input_cost_usd: 0,
});
row.tokens_saved += entry.tokens_saved || 0;
row.savings_usd += entry.compression_savings_usd_delta || 0;
row.input_cost_usd += entry.total_input_cost_usd_delta || 0;
}
}
return Object.values(totals)
.map(row => ({
...row,
expected_cost_usd: row.input_cost_usd + row.savings_usd,
}))
.sort((a, b) => b.tokens_saved - a.tokens_saved);
},
get historyActiveModel() {
// A model filter only applies while that model is present in
// the charted series; otherwise fall back to showing all.
// Raw checkpoint view plots no per-model lines (they are
// derived from rollup buckets), so the filter must not
// suppress the aggregate line there.
if (this.historySelectedSeriesKey === 'history') return null;
const model = this.historySelectedModel;
if (!model) return null;
return this.historyModelChartSeries.some(series => series.model === model)
? model
: null;
},
toggleHistoryModel(model) {
this.historySelectedModel = this.historySelectedModel === model ? null : model;
},
get historyCostTrend() {
return this.historicalTrend.map(point => {
const actual = point?.total_input_cost_usd || 0;
const saved = point?.compression_savings_usd || 0;
return { actual, expected: actual + saved };
});
},
historyModelColor(index) {
const palette = ['#a78bfa', '#34d399', '#fbbf24', '#f87171', '#60a5fa'];
return palette[index % palette.length];
},
historyModelLine(index) {
if (this.historyChartMode !== 'tokens') return '';
// Checkpoint view plots raw checkpoints; daily-derived model
// lines would not share its x-axis.
if (this.historySelectedSeriesKey === 'history') return '';
const allSeries = this.historyModelChartSeries;
const series = allSeries[index];
if (!series || series.values.length < 2) return '';
const activeModel = this.historyActiveModel;
if (activeModel && series.model !== activeModel) return '';
// A single filtered model gets its own scale; the full set
// shares one so the lines stay comparable.
const scaleSeries = activeModel ? [series] : allSeries;
const max = Math.max(...scaleSeries.flatMap(s => s.values), 1);
return this.buildTrendPath(series.values, 0, max);
},
historyCostLine(kind) {
if (this.historyChartMode !== 'cost') return '';
const trend = this.historyCostTrend;
if (trend.length < 2) return '';
const all = trend.flatMap(point => [point.actual, point.expected]);
const min = Math.min(...all);
const max = Math.max(...all);
return this.buildTrendPath(trend.map(point => point[kind]), min, max);
},
buildTrendPath(values, min, max) {
if (!values || values.length < 2) return '';
const range = max - min || 1;
const points = values.map((value, index) => {
const x = (index / (values.length - 1)) * 200;
const y = 60 - ((value - min) / range) * 56;
return `${x},${y}`;
});
return 'M' + points.join(' L');
},
get hasHistoricalData() {
return (this.historyStats.history || []).length > 0;
},
get historySelectedSeriesKey() {
return this.historyGranularity === 'history' ? 'history' : this.historyGranularity;
},
get historySelectedSeriesLabel() {
const labels = {
history: 'Checkpoints',
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
};
return labels[this.historySelectedSeriesKey] || 'History';
},
get historyModelSourceSeriesLabel() {
// historyModelSourceSeries substitutes the daily rollup
// at raw checkpoint granularity; label what is shown.
return this.historySelectedSeriesKey === 'history'
? 'Daily'
: this.historySelectedSeriesLabel;
},
get historySelectedPointCount() {
if (this.historySelectedSeriesKey === 'history') {
return (this.historyStats.history || []).length;
}
return (this.historyStats.series?.[this.historySelectedSeriesKey] || []).length;
},
get historicalTrend() {
if (this.historySelectedSeriesKey === 'history') {
return this.historyStats.history || [];
}
return this.historyStats.series?.[this.historySelectedSeriesKey] || [];
},
get historyTrendLabel() {
const labels = {
history: 'Checkpoint history',
daily: 'Daily cumulative savings',
weekly: 'Weekly cumulative savings',
monthly: 'Monthly cumulative savings',
};
return labels[this.historySelectedSeriesKey] || 'Historical savings';
},
get historyWindowLabel() {
const history = this.historyStats.history || [];
if (history.length === 0) return 'Waiting for saved requests';
const first = history[0]?.timestamp;
const last = history[history.length - 1]?.timestamp;
if (!first || !last) return 'Persisted locally';
return this.formatDate(first) + ' to ' + this.formatDate(last);
},
get historyAverageTokensPerDay() {
const daily = this.historyStats.series?.daily || [];
const lifetime = this.historyStats.lifetime?.tokens_saved || 0;
if (daily.length === 0) return 0;
return Math.round(lifetime / daily.length);
},
get historyAverageTokensPerWeek() {
const weekly = this.historyStats.series?.weekly || [];
const lifetime = this.historyStats.lifetime?.tokens_saved || 0;
if (weekly.length === 0) return 0;
return Math.round(lifetime / weekly.length);
},
get historyAverageTokensPerMonth() {
const monthly = this.historyStats.series?.monthly || [];
const lifetime = this.historyStats.lifetime?.tokens_saved || 0;
if (monthly.length === 0) return 0;
return Math.round(lifetime / monthly.length);
},
get recentDailyHistory() {
return [...(this.historyStats.series?.daily || [])].slice(-7).reverse();
},
get recentWeeklyHistory() {
return [...(this.historyStats.series?.weekly || [])].slice(-6).reverse();
},
get recentMonthlyHistory() {
return [...(this.historyStats.series?.monthly || [])].slice(-6).reverse();
},
get recentHistoricalPoints() {
return [...(this.historyStats.history || [])].slice(-8).reverse();
},
async downloadHistory(format = 'json', series = null) {
const selectedSeries = series || this.historySelectedSeriesKey;
const params = new URLSearchParams({ format, series: selectedSeries });
if (format === 'json' && selectedSeries === 'history') {
params.set('history_mode', 'full');
}
const response = await fetch('/stats-history?' + params.toString());
if (!response.ok) throw new Error('Failed to export history');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `headroom-stats-history-${selectedSeries}.${format}`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
},
// --- Prefix Cache ---
get cacheSessionActive() {
return (this.stats.prefix_cache?.totals?.requests || 0) > 0;
},
get cacheSavingsPercent() {
const t = this.stats.prefix_cache?.totals || {};
const total = (t.cache_read_tokens || 0) + (t.cache_write_tokens || 0);
if (total === 0) return 0;
return Math.round((t.cache_read_tokens || 0) / total * 100);
},
get cacheWritePercent() {
const t = this.stats.prefix_cache?.totals || {};
const total = (t.cache_read_tokens || 0) + (t.cache_write_tokens || 0);
if (total === 0) return 0;
return Math.round((t.cache_write_tokens || 0) / total * 100);
},
get hasObservedTtlBuckets() {
const buckets = this.stats.prefix_cache?.totals?.observed_ttl_buckets || {};
return ((buckets['5m']?.tokens || 0) + (buckets['1h']?.tokens || 0)) > 0;
},
get observedTtlHeadline() {
const mix = this.stats.prefix_cache?.totals?.observed_ttl_mix || {};
const oneHour = mix['1h_pct'] || 0;
const fiveMinute = mix['5m_pct'] || 0;
if (oneHour === fiveMinute) return 'Balanced';
return oneHour > fiveMinute ? '1h leaning' : '5m leaning';
},
get observedTtlWindowLabel() {
const mix = this.stats.prefix_cache?.totals?.observed_ttl_mix || {};
const active = mix.active_buckets || [];
if (!active.length) return 'No TTL bucket data';
return active.length === 1 ? active[0] + ' only' : active.join(' / ');
},
get compressionVsCacheNet() {
const cvc = this.stats.prefix_cache?.compression_vs_cache || {};
return cvc.net_tokens ?? ((cvc.tokens_saved_by_compression || 0) - (cvc.tokens_lost_to_cache_bust || 0));
},
get prefixFreezeNet() {
const pf = this.stats.prefix_cache?.prefix_freeze || {};
return pf.net_benefit_tokens ?? ((pf.tokens_preserved || 0) - (pf.compression_foregone_tokens || 0));
},
get hasCompressionVsCache() {
const cvc = this.stats.prefix_cache?.compression_vs_cache || {};
const pf = this.stats.prefix_cache?.prefix_freeze || {};
return (cvc.tokens_saved_by_compression || 0) > 0
|| (cvc.tokens_lost_to_cache_bust || 0) > 0
|| (pf.tokens_preserved || 0) > 0
|| (pf.compression_foregone_tokens || 0) > 0;
},
// --- Cache miss attribution (#1313) ---
get missAttribution() {
return this.stats.prefix_cache?.miss_attribution?.totals || {};
},
get hasMissAttribution() {
return (this.missAttribution.total || 0) > 0;
},
// --- Waste Signals ---
wasteSignalLabel(signal) {
const labels = {
json_bloat: 'JSON Bloat',
html_noise: 'HTML Noise',
base64: 'Base64 Blobs',
whitespace: 'Whitespace',
dynamic_date: 'Dynamic Dates',
repetition: 'Repetition',
reread: 'Re-read Tool Results',
reread_compressed: 'Re-read After Compression',
};
return labels[signal] || signal;
},
wasteSignalColor(signal) {
const colors = {
json_bloat: 'bg-amber-500',
html_noise: 'bg-orange-500',
base64: 'bg-red-500',
whitespace: 'bg-blue-500',
dynamic_date: 'bg-purple-500',
repetition: 'bg-pink-500',
reread: 'bg-teal-500',
reread_compressed: 'bg-rose-500',
};
return colors[signal] || 'bg-gray-500';
},
wasteSignalBadgeColor(signal) {
const colors = {
json_bloat: 'bg-amber-500/20 text-amber-400',
html_noise: 'bg-orange-500/20 text-orange-400',
base64: 'bg-red-500/20 text-red-400',
whitespace: 'bg-blue-500/20 text-blue-400',
dynamic_date: 'bg-purple-500/20 text-purple-400',
repetition: 'bg-pink-500/20 text-pink-400',
reread: 'bg-teal-500/20 text-teal-400',
reread_compressed: 'bg-rose-500/20 text-rose-400',
};
return colors[signal] || 'bg-gray-500/20 text-gray-400';
},
get sortedWasteSignals() {
const signals = this.stats.waste_signals || {};
return Object.entries(signals)
.filter(([, v]) => v > 0)
.sort((a, b) => b[1] - a[1]);
},
getWastePercent(tokens) {
const signals = this.stats.waste_signals || {};
const max = Math.max(...Object.values(signals), 1);
return Math.min((tokens / max) * 100, 100);
},
get compressionTotalBefore() {
return this.stats.tokens?.total_before_compression || 0;
},
get proxyShareOfTotal() {
const total = this.compressionTotalBefore;
if (total <= 0) return 0;
return (this.stats.tokens?.proxy_compression_saved || 0) / total * 100;
},
get cliFilteringLabel() {
const raw = this.stats.savings?.by_layer?.cli_filtering?.label
|| this.stats.context_tool?.label
|| this.stats.context_tool?.configured
|| 'Context Tool';
if (String(raw).toLowerCase() === 'lean-ctx') return 'Lean-ctx';
return String(raw);
},
get cliFilteringSaved() {
return this.stats.tokens?.cli_filtering_saved
?? this.stats.tokens?.cli_tokens_avoided
?? this.stats.tokens?.rtk_saved
?? 0;
},
get cliFilteringShareOfTotal() {
const total = this.compressionTotalBefore;
if (total <= 0) return 0;
return this.cliFilteringSaved / total * 100;
},
get cliFilteringLifetime() {
return this.stats.savings?.by_layer?.cli_filtering?.lifetime?.tokens_saved ?? 0;
},
get cliFilteringSessionPctDisplay() {
const p = this.stats.savings?.by_layer?.cli_filtering?.session_savings_pct;
return (p === null || p === undefined) ? this.cliFilteringShareOfTotal : p;
},
get cliFilteringAvailable() {
const ct = this.stats.context_tool;
if (ct && typeof ct.available === 'boolean') return ct.available;
return true;
},
get historyCliFilteringAvailable() {
const hf = this.historyStats?.cli_filtering;
if (hf && typeof hf.available === 'boolean') return hf.available;
return true;
},
// --- Headline savings percent ---
//
get headlineSavingsPercent() {
return this.stats.tokens?.savings_percent
?? this.stats.tokens?.proxy_savings_percent
?? 0;
},
get headlineSavingsTitle() {
return 'Of total wire input tokens';
},
// --- Expandable Rows ---
toggleExpanded(id) {
this.expandedRows[id] = !this.expandedRows[id];
},
// --- Charts ---
getProviderPercent(count) {
const total = this.stats.requests?.total || 1;
return Math.min((count / total) * 100, 100);
},
getSparkline(data) {
if (!data || data.length < 2) return '';
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
const points = data.map((v, i) => {
const x = (i / (data.length - 1)) * 100;
const y = 32 - ((v - min) / range) * 28;
return `${x},${y}`;
});
return 'M' + points.join(' L');
},
getSparklineArea(data) {
if (!data || data.length < 2) return '';
const line = this.getSparkline(data);
if (!line) return '';
return line + ` L100,32 L0,32 Z`;
},
getTrendLine(history) {
if (!history || history.length < 2) return '';
const values = history.map(h => Array.isArray(h) ? h[1] : (h?.total_tokens_saved || 0));
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const points = values.map((v, i) => {
const x = (i / (values.length - 1)) * 200;
const y = 60 - ((v - min) / range) * 56;
return `${x},${y}`;
});
return 'M' + points.join(' L');
},
getTrendArea(history) {
if (!history || history.length < 2) return '';
const line = this.getTrendLine(history);
if (!line) return '';
return line + ` L200,64 L0,64 Z`;
},
getObjectTrendLine(history, valueKey) {
if (!history || history.length < 2) return '';
const values = history.map(point => point?.[valueKey] || 0);
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const points = values.map((value, index) => {
const x = (index / (values.length - 1)) * 200;
const y = 60 - ((value - min) / range) * 56;
return `${x},${y}`;
});
return 'M' + points.join(' L');
},
getObjectTrendArea(history, valueKey) {
if (!history || history.length < 2) return '';
const line = this.getObjectTrendLine(history, valueKey);
if (!line) return '';
return line + ` L200,64 L0,64 Z`;
},
};
}
function toggleTheme() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('headroom-theme', isDark ? 'dark' : 'light');
}
</script>
</body>
</html>