feat(dashboard): one-line per-session savings; drop deleted-UI SessionTotals fields (#22, #24)

This commit is contained in:
teamchong
2026-05-24 18:52:52 -04:00
parent f78acb18f2
commit 713ee6b1bc
3 changed files with 79 additions and 336 deletions
+28 -74
View File
@@ -150,21 +150,16 @@ export interface RecentRow {
*/
interface SessionTotals {
sessionId: string;
firstSeen: number; // unix seconds
lastSeen: number; // unix seconds
requests: number;
// Dollar-weighted accumulators. baseline/actual are the SAME math as the
// global `Totals.allBaselineEquivalentWeighted` / `allActualInputWeighted`
// + `allOutputWeighted`, but scoped to a single session.
// global `Totals.allBaselineEquivalentWeighted` / `allActualInputWeighted`,
// but scoped to a single session. Output is excluded because the proxy
// doesn't touch it.
baselineInputWeighted: number;
actualInputWeighted: number;
outputWeighted: number;
// Per-bucket char attribution from ev.info.bucketChars.
bucketChars: { [bucket: string]: number };
// Passthrough reason histogram from ev.info.passthroughReasons.
passthroughReasons: Record<string, number>;
passthroughRequests: number; // count of requests where !ev.info.compressed
compressedRequests: number;
// Honest denominator for the headline: count of requests that contributed
// to `baselineInputWeighted` (events that carried a baseline measurement,
// matching the global `Totals.baselineMeasuredCount`).
baselineMeasuredCount: number;
}
interface Totals {
@@ -512,59 +507,30 @@ export class DashboardState {
if (!s) {
s = {
sessionId: sid,
firstSeen: Date.now() / 1000,
lastSeen: Date.now() / 1000,
requests: 0,
baselineInputWeighted: 0,
actualInputWeighted: 0,
outputWeighted: 0,
bucketChars: {},
passthroughReasons: {},
passthroughRequests: 0,
compressedRequests: 0,
baselineMeasuredCount: 0,
};
this.sessions.set(sid, s);
// Cap memory — drop the oldest session by lastSeen when over budget.
// Cap memory — drop the first (oldest by insertion order) session
// when over budget. We no longer track lastSeen privately on the
// class — insertion order is a fine proxy because `currentSessionId`
// (set above on every update) is what the serve path uses to pick
// the most-recent session, not a scan of `this.sessions`.
if (this.sessions.size > DashboardState.SESSION_CAP) {
const oldest = [...this.sessions.values()].sort(
(a, b) => a.lastSeen - b.lastSeen,
)[0];
if (oldest) this.sessions.delete(oldest.sessionId);
const firstKey = this.sessions.keys().next().value;
if (firstKey !== undefined) this.sessions.delete(firstKey);
}
}
s.lastSeen = Date.now() / 1000;
s.requests += 1;
// Reuse the same haveUsage / haveBaseline guards + the
// baselineInputEff / actualInputEff / outputEquiv locals computed
// earlier in update() so the lifetime totals block (above) and the
// per-session block (here) read the same values. Re-deriving them
// here would duplicate the cache-aware-baseline math and invite drift.
// baselineInputEff / actualInputEff locals computed earlier in
// update() so the lifetime totals block (above) and the per-session
// block (here) read the same values. Re-deriving them here would
// duplicate the cache-aware-baseline math and invite drift.
if (haveBaseline && haveUsage) {
s.baselineInputWeighted += baselineInputEff;
s.actualInputWeighted += actualInputEff;
s.outputWeighted += outputEquiv;
}
if (compressed) s.compressedRequests += 1;
else s.passthroughRequests += 1;
// Per-bucket char attribution from `info.bucketChars` (flat
// `Partial<Record<BucketName, number>>` — see src/core/tracker.ts).
// Matches the shape the Svelte panel iterates over.
const bc = info?.bucketChars;
if (bc && typeof bc === 'object') {
for (const [key, val] of Object.entries(bc) as [string, unknown][]) {
if (typeof val === 'number' && Number.isFinite(val)) {
s.bucketChars[key] = (s.bucketChars[key] ?? 0) + val;
}
}
}
// Passthrough reason histogram from `info.passthroughReasons`.
const pr = info?.passthroughReasons;
if (pr && typeof pr === 'object') {
for (const [reason, count] of Object.entries(pr as Record<string, unknown>)) {
if (typeof count === 'number' && Number.isFinite(count) && count > 0) {
s.passthroughReasons[reason] = (s.passthroughReasons[reason] ?? 0) + count;
}
}
s.baselineMeasuredCount += 1;
}
}
@@ -695,28 +661,16 @@ export class DashboardState {
if (!s) {
return jsonResponse({ sessionId: null, message: 'no active session yet' });
}
// Dollar-weighted savings ratio. Same math as the global `saved_pct`
// (compressed AND passthrough requests — passthrough has baseline=actual,
// so it correctly drags the ratio down when we leak). Honest denominator:
// includes both compressed and uncompressed flows that the proxy saw.
const baselineUsd = (s.baselineInputWeighted * ASSUMED_INPUT_USD_PER_MTOK) / 1e6;
const actualUsd = (s.actualInputWeighted * ASSUMED_INPUT_USD_PER_MTOK) / 1e6;
const savedUsd = baselineUsd - actualUsd;
const savedPct = baselineUsd > 0 ? (savedUsd / baselineUsd) * 100 : 0;
// Minimal headline payload: the same dollar-weighted baseline/actual
// input accumulators the global Totals block exposes, plus the honest
// denominator (`baselineMeasuredCount` — only requests that carried a
// baseline measurement). The Svelte panel does the savings math itself
// so we don't round-trip dollar values through the wire.
return jsonResponse({
sessionId: s.sessionId,
firstSeen: s.firstSeen,
lastSeen: s.lastSeen,
uptimeSec: Math.max(0, s.lastSeen - s.firstSeen),
requests: s.requests,
compressedRequests: s.compressedRequests,
passthroughRequests: s.passthroughRequests,
baselineUsd: round4(baselineUsd),
actualUsd: round4(actualUsd),
savedUsd: round4(savedUsd),
savedPct: round1(savedPct),
bucketChars: s.bucketChars,
passthroughReasons: s.passthroughReasons,
baselineInputWeighted: s.baselineInputWeighted,
actualInputWeighted: s.actualInputWeighted,
baselineMeasuredCount: s.baselineMeasuredCount,
});
}
+45 -242
View File
@@ -1,263 +1,66 @@
<script lang="ts">
// Top-of-dashboard headline. Dollar-weighted savings ratio scoped to the
// most-recently-active session, with per-bucket breakdown of where the
// savings came from and a list of passthrough reasons for requests we
// didn't compress. Polls 2s via the `currentSession` store.
// One-line headline for the current session: dollar-weighted savings ratio.
//
// Math (no cherry-pick):
// saved_$ = Σ baseline_$ Σ actual_$ (over requests where we measured baseline)
// saved_% = saved_$ / Σ baseline_$
//
// `baseline_$` is the cache-aware bill Anthropic would have charged for the
// uncompressed body with the SAME cache_control markers Claude Code sent —
// measured by the counterfactual baseline probe on a sampled subset of
// requests. Caching savings stay credited to Claude Code; what remains is
// proxy-attributable savings only.
//
// Unmeasured requests (probe skipped, passthroughs) don't enter either sum.
// We only report what we measured.
import { currentSession } from '../stores/index.js';
$: data = $currentSession.data;
$: err = $currentSession.error;
$: hasSession = data && data.sessionId != null;
$: baselineUsd = data?.baselineInputWeighted ?? 0;
$: actualUsd = data?.actualInputWeighted ?? 0;
$: savedUsd = Math.max(0, baselineUsd - actualUsd);
$: savedPct = baselineUsd > 0 ? (savedUsd / baselineUsd) * 100 : 0;
$: measuredReqs = data?.baselineMeasuredCount ?? 0;
function fmtUsd(n: number): string {
return '$' + n.toFixed(2);
}
function fmtPct(n: number): string {
return n.toFixed(1) + '%';
}
function fmtDuration(sec: number): string {
if (sec < 60) return Math.round(sec) + 's';
const m = Math.floor(sec / 60);
if (m < 60) return m + 'm';
const h = Math.floor(m / 60);
return h + 'h ' + (m % 60) + 'm';
}
function fmtChars(n: number): string {
if (n < 1000) return String(n);
if (n < 1_000_000) return (n / 1000).toFixed(1) + 'k';
return (n / 1_000_000).toFixed(1) + 'M';
}
$: bucketTotal = data?.bucketChars
? Object.values(data.bucketChars).reduce((a, b) => a + b, 0)
: 0;
$: bucketEntries = data?.bucketChars
? (Object.entries(data.bucketChars) as [string, number][])
.filter(([, v]) => v > 0)
.sort((a, b) => b[1] - a[1])
: [];
$: passthroughEntries = data?.passthroughReasons
? (Object.entries(data.passthroughReasons) as [string, number][])
.sort((a, b) => b[1] - a[1])
: [];
// Static key→label / key→color maps. The Record type matches the spec's
// `bucketChars` shape so any new bucket added in tracker.ts will show up
// in legend order. Hex codes chosen to match the legacy panel palette.
const BUCKET_LABELS: Record<string, string> = {
static_slab: 'slab',
reminder: 'reminder',
tool_result: 'tool_result',
history: 'history',
billing: 'billing',
dynamic: 'dynamic',
};
const BUCKET_COLORS: Record<string, string> = {
static_slab: '#b9be0',
reminder: '#85c5f6',
tool_result: '#10b981',
history: '#f59e0b',
billing: '#8e7681',
dynamic: '#ef4444',
};
</script>
<div class="panel session-summary">
{#if err}
<div class="error">error: {err}</div>
{:else if !data}
<div class="loading">loading…</div>
{:else if !hasSession}
<h2>This session</h2>
<div class="empty">{data.message ?? 'no active session yet'}</div>
{:else}
<div class="header">
<h2>This session</h2>
<div class="sub">
started {fmtDuration(data.uptimeSec ?? 0)} ago · {data.requests} requests
</div>
</div>
<div class="headline">
<div class="big">
Saved <span class="pct">{fmtPct(data.savedPct ?? 0)}</span>
</div>
<div class="usd">
{fmtUsd(data.savedUsd ?? 0)} of {fmtUsd(data.baselineUsd ?? 0)} baseline
</div>
</div>
<div class="bar">
<div class="bar-fill" style="width: {Math.max(0, Math.min(100, data.savedPct ?? 0))}%"></div>
</div>
{#if bucketEntries.length > 0}
<div class="section">
<div class="section-label">Where it came from</div>
<div class="bucket-bar">
{#each bucketEntries as [key, val]}
{@const w = bucketTotal > 0 ? (val / bucketTotal) * 100 : 0}
<div
class="bucket-seg"
style="width: {w}%; background: {BUCKET_COLORS[key] ?? '#8e7681'}"
title="{BUCKET_LABELS[key] ?? key}: {fmtChars(val)} chars ({w.toFixed(1)}%)"
></div>
{/each}
</div>
<div class="bucket-legend">
{#each bucketEntries as [key, val]}
{@const w = bucketTotal > 0 ? (val / bucketTotal) * 100 : 0}
<span class="legend-item">
<span class="swatch" style="background: {BUCKET_COLORS[key] ?? '#8e7681'}"></span>
{BUCKET_LABELS[key] ?? key} {w.toFixed(0)}%
</span>
{/each}
</div>
</div>
{/if}
{#if passthroughEntries.length > 0}
<div class="section">
<div class="section-label">
Where it didn't · {data.passthroughRequests ?? 0} of {data.requests} requests passed through
</div>
<ul class="passthrough-list">
{#each passthroughEntries as [reason, count]}
<li><span class="reason">{reason}</span> <span class="count">{count}</span></li>
{/each}
</ul>
</div>
{:else if (data.passthroughRequests ?? 0) === 0}
<div class="section">
<div class="section-label-good">✓ Every request compressed</div>
</div>
{/if}
{/if}
</div>
{#if err}
<div class="line muted">session: {err}</div>
{:else if measuredReqs > 0}
<div class="line">
<span class="label">THIS SESSION</span>
— saved <span class="num">{fmtUsd(savedUsd)}</span>
(<span class="num">{savedPct.toFixed(1)}%</span>)
· <span class="muted">{measuredReqs} requests</span>
</div>
{/if}
<style>
.session-summary {
margin-bottom: 22px;
}
.header {
display: flex;
justify-content: space-between;
align-items: baseline;
.line {
font-size: 14px;
color: #c9d1d9;
margin-bottom: 12px;
padding: 8px 12px;
background: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
}
h2 {
margin: 0;
font-size: 13px;
.label {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #8e7681;
color: #8b949e;
letter-spacing: 0.04em;
}
.sub {
font-size: 12px;
color: #8e7681;
}
.headline {
margin-bottom: 10px;
}
.big {
font-size: 18px;
.num {
font-variant-numeric: tabular-nums;
color: #3fb950;
font-weight: 600;
color: #c9d1d9;
}
.pct {
color: #3fb950;
font-variant-numeric: tabular-nums;
}
.usd {
font-size: 12px;
color: #8e7681;
margin-top: 2px;
font-variant-numeric: tabular-nums;
}
.bar {
margin-top: 8px;
height: 6px;
background: #21262d;
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: #3fb950;
transition: width 1s ease;
}
.section {
margin-top: 16px;
}
.section-label {
font-size: 11px;
color: #8e7681;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 6px;
}
.section-label-good {
color: #3fb950;
text-transform: none;
letter-spacing: 0;
font-size: 12px;
}
.bucket-bar {
display: flex;
height: 8px;
border-radius: 4px;
overflow: hidden;
background: #21262d;
}
.bucket-seg {
height: 100%;
transition: width 1s ease;
}
.bucket-legend {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 11px;
color: #c9d1d9;
}
.legend-item {
display: inline-flex;
align-items: center;
gap: 4px;
font-variant-numeric: tabular-nums;
}
.swatch {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 2px;
}
.passthrough-list {
margin: 0;
padding: 0;
list-style: none;
font-size: 12px;
color: #c9d1d9;
}
.passthrough-list li {
display: flex;
justify-content: space-between;
padding: 2px 0;
font-variant-numeric: tabular-nums;
}
.reason {
color: #8e7681;
}
.count {
color: #8e7681;
}
.loading, .empty, .error {
color: #8e7681;
font-size: 12px;
}
.error {
color: #f85149;
.muted {
color: #6e7681;
}
</style>
+6 -20
View File
@@ -153,27 +153,13 @@ export interface CompressionToggleResponse {
compression_enabled: boolean;
}
/** /api/current-session.json payload — per-session aggregates for the most-recently-active Claude Code session. */
/** /api/current-session.json payload per-session aggregates for the most-recently-active Claude Code session.
* Pruned to only the fields the minimal SessionSummary.svelte headline reads:
* dollar-weighted baseline/actual input + the measured-request count. */
export interface CurrentSessionPayload {
sessionId: string | null;
message?: string;
firstSeen?: number;
lastSeen?: number;
uptimeSec?: number;
requests?: number;
compressedRequests?: number;
passthroughRequests?: number;
baselineUsd?: number;
actualUsd?: number;
savedUsd?: number;
savedPct?: number;
bucketChars?: {
static_slab: number;
reminder: number;
tool_result: number;
history: number;
billing: number;
dynamic: number;
};
passthroughReasons?: Record<string, number>;
baselineInputWeighted?: number;
actualInputWeighted?: number;
baselineMeasuredCount?: number;
}