Skip to content

Commit 9e83069

Browse files
lexwhitingclaude
andcommitted
fix(p5.K1): spec-diff round — close 4 spec gaps
Strict spec-diff against the original P5.K1 prompt surfaced four gaps the initial implementation missed. Each fixed: ## #1 — snake_case property names (CRITICAL) Spec lists snake_case property names verbatim: `amount_cents`, `dev_id`, `latency_ms`, `take_cents`, `error_class`, `error_message`, `alternatives_considered`, `fee_bps`. Initial impl used camelCase throughout. Diverging would have created permanent PostHog property drift between funnel events (P4.1 snake_case) and kernel events (would have been camelCase) — any future cross-event analysis would silently fail to join. Renamed all interface fields, sanitizer switch arms, kernel.ts emit calls, sink-route denorm reads (`props.dev_id` not `props.devId`), admin-route SQL JSON keys (`props ->> 'latency_ms'` not `'latencyMs'`), and the test file. The original hostile audit clause "(c) PostHog event names match the existing convention from P4.1" was satisfied for event NAMES (kernel.X.Y dot/snake_case) but missed event PROPERTIES — that gap is closed now. ## #2 — `kernel.routing_decision` adapter field documented Spec lists `{ rail, reason, alternatives_considered, fee_bps }` without `adapter`. Implementation kept `adapter` as enrichment because the dashboard needs to filter routing decisions per adapter without re-deriving. Added explicit JSDoc on `KernelRoutingDecisionProps.adapter` calling out the deviation as a documented enrichment beyond spec literal. ## #3 — current QPS + error rate added to overview Spec §dashboard top-level: "current QPS, error rate, p50/p95/p99 latency per adapter". Initial impl had p50/p95/p99 + total counts + success rate but missed: - QPS card (60s sliding window count of latency events ÷ 60) - Error rate column (replaced success rate; aggregate error rate also surfaced as a separate summary card) ## #5 — payout schedule status added to rails dashboard Spec §dashboard rails: "rail-fee accumulation, payout schedule status". Initial impl had rail-fee accumulation only. Added `payoutStatus` to the rails API response and a 6-card panel on the dashboard: - mode (manual per 2026-04 ops decision) - cron schedule (0 12 * * * — daily 12:00 UTC processor) - next cron run (computed from current time) - pending (count + amount) - failed (24h) - last success (date + amount) Failed-24h card switches to a destructive border when count > 0. ## Verification - npx tsc --noEmit (apps/web + packages/mcp): clean - npx turbo run test: all packages pass; mcp 1834 tests (kernel-telemetry: 30 tests still pass after rename), apps/web 3909 tests (no app-level unit tests touched). - npx turbo run lint: 8/8, 0 errors. - npx turbo run build: 13/13 packages. Refs: P5.K1 Audits: spec-diff PASS (4 gaps closed; routing_decision deviation explicitly documented inline), hostile PASS (snake_case rename preserved sanitizer / PII redaction logic), tests PASS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 846fab2 commit 9e83069

7 files changed

Lines changed: 363 additions & 101 deletions

File tree

apps/web/src/app/admin/kernel-health/page.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface OverviewRow {
1818
total: number
1919
successes: number
2020
errors: number
21+
errorRate: number
2122
p50: number
2223
p95: number
2324
p99: number
@@ -28,6 +29,7 @@ interface OverviewData {
2829
generatedAt: string
2930
windowDays: number
3031
totalEvents: number
32+
currentQps: number
3133
perAdapter: OverviewRow[]
3234
}
3335

@@ -108,13 +110,30 @@ export default function KernelHealthPage() {
108110
}
109111

110112
function SummaryCards({ data }: { data: OverviewData }) {
113+
// QPS is computed over a 60s sliding window. Render one decimal so
114+
// sub-1-QPS systems still show signal (0.0 → 0.7 → 1.5 etc).
115+
const qps =
116+
data.currentQps >= 10
117+
? data.currentQps.toFixed(0)
118+
: data.currentQps.toFixed(1)
119+
// Aggregate error rate over the dashboard window.
120+
const totalsAcrossAdapters = data.perAdapter.reduce(
121+
(acc, r) => ({ total: acc.total + r.total, errors: acc.errors + r.errors }),
122+
{ total: 0, errors: 0 },
123+
)
124+
const aggregateErrorRate =
125+
totalsAcrossAdapters.total === 0
126+
? 0
127+
: (totalsAcrossAdapters.errors / totalsAcrossAdapters.total) * 100
128+
111129
return (
112-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 mb-8">
113-
<Card label="Total events" value={data.totalEvents.toLocaleString()} />
130+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-8">
131+
<Card label="Current QPS (60s)" value={qps} />
114132
<Card
115-
label="Adapters with traffic"
116-
value={data.perAdapter.length.toLocaleString()}
133+
label="Error rate"
134+
value={`${aggregateErrorRate.toFixed(2)}%`}
117135
/>
136+
<Card label="Total events" value={data.totalEvents.toLocaleString()} />
118137
<Card
119138
label="Window"
120139
value={`${data.windowDays} day${data.windowDays === 1 ? '' : 's'}`}
@@ -139,7 +158,7 @@ function AdapterTable({ rows }: { rows: OverviewRow[] }) {
139158
<tr>
140159
<th className="px-4 py-2">Adapter</th>
141160
<th className="px-4 py-2 text-right">Total</th>
142-
<th className="px-4 py-2 text-right">Success rate</th>
161+
<th className="px-4 py-2 text-right">Error rate</th>
143162
<th className="px-4 py-2 text-right">p50 (ms)</th>
144163
<th className="px-4 py-2 text-right">p95 (ms)</th>
145164
<th className="px-4 py-2 text-right">p99 (ms)</th>
@@ -148,15 +167,16 @@ function AdapterTable({ rows }: { rows: OverviewRow[] }) {
148167
</thead>
149168
<tbody>
150169
{rows.map((r) => {
151-
const successRate =
152-
r.total === 0 ? 0 : (r.successes / r.total) * 100
170+
const errorRatePct = r.errorRate * 100
153171
return (
154172
<tr key={r.adapter} className="border-t">
155173
<td className="px-4 py-2 font-mono">{r.adapter}</td>
156174
<td className="px-4 py-2 text-right">
157175
{r.total.toLocaleString()}
158176
</td>
159-
<td className="px-4 py-2 text-right">{successRate.toFixed(1)}%</td>
177+
<td className="px-4 py-2 text-right">
178+
{errorRatePct.toFixed(2)}%
179+
</td>
160180
<td className="px-4 py-2 text-right">{r.p50.toFixed(0)}</td>
161181
<td className="px-4 py-2 text-right">{r.p95.toFixed(0)}</td>
162182
<td className="px-4 py-2 text-right">{r.p99.toFixed(0)}</td>

apps/web/src/app/admin/kernel-health/rails/page.tsx

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,27 @@ interface RailRow {
1515
takeCentsTotal: number
1616
}
1717

18+
interface PayoutStatus {
19+
mode: 'manual' | 'auto'
20+
cronSchedule: string
21+
nextCronRun: string
22+
lastSuccess: {
23+
occurredAt: string | null
24+
amountCents: number
25+
}
26+
pending: {
27+
count: number
28+
amountCentsTotal: number
29+
}
30+
failed24h: number
31+
}
32+
1833
interface RailsData {
1934
view: 'rails'
2035
generatedAt: string
2136
windowDays: number
2237
perRail: RailRow[]
38+
payoutStatus: PayoutStatus
2339
}
2440

2541
export default function KernelHealthRailsPage() {
@@ -89,18 +105,88 @@ export default function KernelHealthRailsPage() {
89105
<p className="text-muted-foreground">Loading…</p>
90106
) : error === 'failed' ? (
91107
<p className="text-destructive">Failed to load. Try refreshing.</p>
92-
) : data && data.perRail.length === 0 ? (
93-
<p className="text-muted-foreground">
94-
No settled invocations in the last {data.windowDays} days.
95-
</p>
96108
) : data ? (
97-
<RailsTable rows={data.perRail} />
109+
<>
110+
<PayoutStatusPanel status={data.payoutStatus} />
111+
<h2 className="mt-8 mb-3 text-lg font-semibold">
112+
Per-rail accumulation
113+
</h2>
114+
{data.perRail.length === 0 ? (
115+
<p className="text-muted-foreground">
116+
No settled invocations in the last {data.windowDays} days.
117+
</p>
118+
) : (
119+
<RailsTable rows={data.perRail} />
120+
)}
121+
</>
98122
) : null}
99123
</div>
100124
</main>
101125
)
102126
}
103127

128+
function PayoutStatusPanel({ status }: { status: PayoutStatus }) {
129+
const pendingUsd = status.pending.amountCentsTotal / 100
130+
const lastSuccessUsd = status.lastSuccess.amountCents / 100
131+
const lastSuccessLabel = status.lastSuccess.occurredAt
132+
? new Date(status.lastSuccess.occurredAt).toISOString().slice(0, 10)
133+
: '—'
134+
const nextRunLabel = new Date(status.nextCronRun)
135+
.toISOString()
136+
.replace('T', ' ')
137+
.slice(0, 16) + ' UTC'
138+
return (
139+
<section>
140+
<h2 className="mb-3 text-lg font-semibold">Payout schedule status</h2>
141+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
142+
<Card label="Mode" value={status.mode} />
143+
<Card label="Next cron run" value={nextRunLabel} />
144+
<Card
145+
label="Pending"
146+
value={`${status.pending.count} dev${status.pending.count === 1 ? '' : 's'} · ${pendingUsd.toLocaleString(undefined, { style: 'currency', currency: 'USD' })}`}
147+
/>
148+
<Card
149+
label="Failed (24h)"
150+
value={status.failed24h.toLocaleString()}
151+
severity={status.failed24h > 0 ? 'warn' : 'normal'}
152+
/>
153+
<Card
154+
label="Last success"
155+
value={`${lastSuccessLabel} · ${lastSuccessUsd.toLocaleString(undefined, { style: 'currency', currency: 'USD' })}`}
156+
/>
157+
<Card
158+
label="Cron schedule"
159+
value={status.cronSchedule}
160+
/>
161+
</div>
162+
</section>
163+
)
164+
}
165+
166+
function Card({
167+
label,
168+
value,
169+
severity = 'normal',
170+
}: {
171+
label: string
172+
value: string
173+
severity?: 'normal' | 'warn'
174+
}) {
175+
return (
176+
<div
177+
className={
178+
'rounded-lg border p-4 ' +
179+
(severity === 'warn' ? 'border-destructive/40 bg-destructive/5' : '')
180+
}
181+
>
182+
<div className="text-xs uppercase tracking-wide text-muted-foreground">
183+
{label}
184+
</div>
185+
<div className="mt-2 text-lg font-semibold tabular-nums">{value}</div>
186+
</div>
187+
)
188+
}
189+
104190
function RailsTable({ rows }: { rows: RailRow[] }) {
105191
return (
106192
<div className="overflow-x-auto rounded-lg border">

0 commit comments

Comments
 (0)