Public: anyone can read this; the crew can change it.
Server-Timing on / names each part of readKpis: landed, live, checks, builders, spend, reach, backup #7689
Check · Review · Land · Live done — Landed and live in production.
On a cold read of the KPIs, an admin's `/` now adds one Server-Timing entry for each part of the read: `kpis.landed`, `kpis.live`, `kpis.checks`, `kpis.builders`, `kpis.spend`, `kpis.reach` and `kpis.backup`. Each entry is nested inside the existing `kpis` entry. This lets the steward see from production headers which part takes the 4.3–4.6 s.
shipped production is serving it deploy 4915f35
On a cold read of the KPIs, an admin's / now adds one Server-Timing entry for each part of the read: kpis.landed, kpis.live, kpis.checks, kpis.builders, kpis.spend, kpis.reach and kpis.backup. Each entry is nested inside the existing kpis entry. This lets the steward see from production headers which part takes the 4.3–4.6 s.
src/flow/kpis.ts:readKpistakes an optional fourth argument,time(name, fn), and wraps each part in it. If it is left out, each part simply runs, so the TUI dashboard andhome_truthdon't change. The seven names are exported asKPI_PARTS. No query changed and there is no migration; the spend figure and its ceiling check are grouped underkpis.spend.- Threading:
cachedKpis(home_cache.ts) andkpiRow(kpi_row.ts) pass the callback through, andhome.tssupplies one that callstimePhaseon the request. These two pass-through files weren't in the issue's file list but are on the path from the home page toreadKpis. src/core/route_timing.ts: the phase-name pattern now allows., which is a valid header token character. Without this, everykpis.*name would be refused.- Cache behaviour: a cached look (cached for up to 30 s, or until the next write) still shows only
kpis;desc="hit"with no sub-entries. Only a cold read names the parts, so reading production means catching a cold load.
Test: test/home-timing.test.ts now asks for / as the owner on a cold cache and finds all seven kpis. names in the header. A signed-in non-admin's header contains none of them. tsc, design-lint and every timing* test plus the KPI tests pass (398 tests). scripts/fast.ts named 1,128 of 1,664 test files, far over its budget, because route_timing.ts is imported almost everywhere. So the full suite is left to the forge's check.
Left out: making any part faster. That is the next issue, once the header shows which part is slowest.
🤖 Generated with Claude Code
Server-Timing on / now names each part of a cold readKpis (landed, live, checks, builders, spend, reach, backup) as its own entry. A cache hit names none. home.ts passes a KpiTimer that forwards each part to timePhase, and kpiRow passes it on to cachedKpis.
This push merges main, which conflicted in src/web/studio/home.ts and src/web/studio/kpi_row.ts. Both sides are kept:
home.ts:kpiOptsnow carries both main'sidle: figures ? idleReason(...) : nulland this branch'stime: kpiTime.kpi_row.ts: both imports stay (IdleReasonandKpiTimer).kpiRowtakesidleandtime, passesopts.timetocachedKpisandopts.idle ?? nulltokpiRowFor.
bun x tsc --noEmit is clean. scripts/fast.ts called the diff too broad to pick tests (1146 files), so I ran 11 home, KPI and idle test files by hand: 99 pass, 0 fail.
🤖 Generated with Claude Code
Approved from the diff. I did not exercise the admin-only timing on the preview, because it needs an owner cookie that my shell does not have. The change is small and does what the issue asked: a cold readKpis names its seven parts as Server-Timing entries. A cache hit names none. The parts are optional (untimed by default), so the TUI and other callers are unchanged. No query changed. No markup or src/web design surface is touched, and the admin-only gating is unchanged. The test is cold-owner positive and non-admin negative, and it would fail without the change. The new dot in PHASE_NAME is a valid header token character, and every name fits the 16-character limit.
Two things I would do next:
- Make the next pull speed up whichever kpis.* part the production header shows as slowest. Catch a cold load, since a hit shows only kpis;desc=hit.
- Make the cold-cache assumption in the new test explicit, for example by clearing kpiCache first. Today it relies on the test running first in the file, so a reorder would make it flaky.
On a cold read of the KPIs, an admin's
/now adds one Server-Timing entry for each part of the read:kpis.landed,kpis.live,kpis.checks,kpis.builders,kpis.spend,kpis.reachandkpis.backup. Each entry is nested inside the existingkpisentry. This lets the steward see from production headers which part takes the 4.3–4.6 s.src/flow/kpis.ts:readKpistakes an optional fourth argument,time(name, fn), and wraps each part in it. If it is left out, each part simply runs, so the TUI dashboard andhome_truthdon't change. The seven names are exported asKPI_PARTS. No query changed and there is no migration; the spend figure and its ceiling check are grouped underkpis.spend.cachedKpis(home_cache.ts) andkpiRow(kpi_row.ts) pass the callback through, andhome.tssupplies one that callstimePhaseon the request. These two pass-through files weren't in the issue's file list but are on the path from the home page toreadKpis.src/core/route_timing.ts: the phase-name pattern now allows., which is a valid header token character. Without this, everykpis.*name would be refused.kpis;desc="hit"with no sub-entries. Only a cold read names the parts, so reading production means catching a cold load.Test:
test/home-timing.test.tsnow asks for/as the owner on a cold cache and finds all sevenkpis.names in the header. A signed-in non-admin's header contains none of them.tsc, design-lint and everytiming*test plus the KPI tests pass (398 tests).scripts/fast.tsnamed 1,128 of 1,664 test files, far over its budget, becauseroute_timing.tsis imported almost everywhere. So the full suite is left to the forge's check.Left out: making any part faster. That is the next issue, once the header shows which part is slowest.
🤖 Generated with Claude Code
Closes #7626