tokentrace 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +24 -20
  3. package/app/api/repair-items/route.ts +81 -0
  4. package/app/api/settings/route.ts +3 -2
  5. package/app/diagnostics/page.tsx +140 -1
  6. package/app/evidence/page.tsx +170 -0
  7. package/app/globals.css +9 -3
  8. package/app/layout.tsx +3 -2
  9. package/app/optimisation/page.tsx +55 -2
  10. package/app/page.tsx +144 -38
  11. package/app/projects/page.tsx +51 -0
  12. package/app/repair/page.tsx +199 -0
  13. package/app/sessions/page.tsx +55 -0
  14. package/bin/tokentrace.js +39 -4
  15. package/components/period-filter.tsx +29 -10
  16. package/components/repair-state-control.tsx +109 -0
  17. package/components/settings-panel.tsx +73 -9
  18. package/components/ui/typography.tsx +3 -3
  19. package/dist/runtime/db-migrate.mjs +74 -1
  20. package/dist/runtime/db-seed.mjs +74 -1
  21. package/dist/runtime/digest.mjs +2324 -0
  22. package/dist/runtime/doctor.mjs +371 -47
  23. package/dist/runtime/evidence.mjs +781 -0
  24. package/dist/runtime/insights.mjs +671 -75
  25. package/dist/runtime/pricing-refresh.mjs +74 -1
  26. package/dist/runtime/repair.mjs +863 -0
  27. package/dist/runtime/reset.mjs +74 -1
  28. package/dist/runtime/scan.mjs +97 -3
  29. package/dist/runtime/status.mjs +678 -533
  30. package/docs/assets/doctor-parser-trust-0.8.0.png +0 -0
  31. package/docs/assets/evidence-0.8.0.png +0 -0
  32. package/docs/assets/overview-0.8.0.png +0 -0
  33. package/docs/assets/repair-0.8.0.png +0 -0
  34. package/package.json +1 -1
  35. package/scripts/build-cli-runtime.mjs +3 -0
  36. package/scripts/digest.ts +27 -0
  37. package/scripts/doctor.ts +3 -1
  38. package/scripts/evidence.ts +92 -0
  39. package/scripts/insights.ts +2 -1
  40. package/scripts/repair.ts +57 -0
  41. package/scripts/smoke-cli.mjs +63 -0
  42. package/scripts/smoke-packed-install.mjs +1 -1
  43. package/scripts/status.ts +8 -9
  44. package/src/db/migrate-core.ts +76 -0
  45. package/src/db/schema.ts +16 -0
  46. package/src/db/settings.ts +31 -2
  47. package/src/lib/analytics.ts +47 -3
  48. package/src/lib/claude-statusline.ts +202 -0
  49. package/src/lib/daily-digest.ts +101 -0
  50. package/src/lib/doctor.ts +19 -3
  51. package/src/lib/evidence-trail.ts +309 -0
  52. package/src/lib/live-status.ts +7 -205
  53. package/src/lib/parser-trust.ts +188 -0
  54. package/src/lib/project-signals.ts +139 -0
  55. package/src/lib/recommendations.ts +36 -0
  56. package/src/lib/review-queue.ts +222 -0
  57. package/src/lib/scan-diff.ts +204 -0
  58. package/src/lib/session-comparison.ts +94 -0
  59. package/src/lib/unknown-cost-repair.ts +532 -0
  60. package/src/lib/usage-guardrails.ts +113 -0
  61. package/docs/assets/diagnostics.png +0 -0
  62. package/docs/assets/discovery.png +0 -0
  63. package/docs/assets/mobile-overview.png +0 -0
  64. package/docs/assets/overview.png +0 -0
  65. package/docs/assets/pricing.png +0 -0
  66. package/docs/assets/session-explorer.png +0 -0
package/CHANGELOG.md CHANGED
@@ -2,7 +2,50 @@
2
2
 
3
3
  All notable changes to TokenTrace are documented here.
4
4
 
5
- ## Unreleased
5
+ ## [0.8.0] - 2026-05-12
6
+
7
+ ### Added
8
+
9
+ - Evidence detail pages and `tokentrace evidence --json` for tracing metric totals back to sessions, source files, parser status, and pricing context.
10
+ - Unknown Cost Repair workbench and `tokentrace repair --json` for grouped local repair state, alias hints, parser review links, and pricing follow-up.
11
+ - Parser Trust Report and Scan History Diff panels in Diagnostics for latest scan parser coverage and scan-to-scan import changes.
12
+
13
+ ### Changed
14
+
15
+ - Overview metric cards now link major totals to evidence trails and route unknown cost work to Unknown Cost Repair.
16
+ - Overview now places Token Trend and Cost Trend directly after Usage Pulse and the metric cards, with Monthly Guardrails and Recommended Next Actions below the charts.
17
+ - Dense evidence, repair, parser trust, and scan diff tables preserve horizontal scrolling and stable source-path truncation.
18
+ - Evidence and repair copy now states local-first behavior, support-file ignores, and parser-review requirements for unsupported files.
19
+ - README screenshots now use public-safe synthetic Evidence + Repair views, and obsolete screenshot assets were removed from the package payload.
20
+
21
+ ### Fixed
22
+
23
+ - Overview custom period date fields now use an intentionally inset calendar icon while preserving native `type="date"` submission fields and the single-line desktop toolbar.
24
+ - The app shell now constrains page width on small screens so wide toolbars scroll internally instead of widening the whole page.
25
+ - `tokentrace statusline setup claude` and piped Claude status-line input no longer touch the TokenTrace app database or start the dashboard.
26
+
27
+ ## [0.7.0] - 2026-05-12
28
+
29
+ ### Added
30
+
31
+ - 0.7.0 Usage Intelligence roadmap for local guardrails, savings review, session comparison, project intelligence, and daily digest work.
32
+ - Local monthly cost and token guardrails stored in Settings.
33
+ - Overview Monthly Guardrails panel showing month-to-date local usage against configured limits.
34
+ - Recommendation rules for monthly guardrails that are near or over limit.
35
+ - Evidence-backed Review Queue on Insights, ranked from guardrails, unknown cost repair, high-impact sessions, dominant projects, model review, and cache reuse.
36
+ - `tokentrace insights --json` now includes the Review Queue for local automation.
37
+ - `tokentrace digest` and `tokentrace digest --json` for current-month local usage, guardrails, top review item, unknown-cost count, top project, and latest scan status.
38
+ - Session Comparison Flags on Sessions to highlight token and cost outliers compared with matching project, tool, and primary-model peers.
39
+ - Project Signals on Projects for dominant usage, unknown cost, estimated-token confidence, and model concentration patterns.
40
+
41
+ ### Changed
42
+
43
+ - Renamed the Insights page header to Usage Intelligence to match the 0.7.0 product theme.
44
+ - Refreshed README screenshots for Overview, Usage Intelligence, Sessions, Projects, and local guardrail Settings using public-safe synthetic local data.
45
+
46
+ ### Fixed
47
+
48
+ - Increased the forced packed-install smoke test timeout so native SQLite dependency installation is not killed on slower machines.
6
49
 
7
50
  ## [0.6.0] - 2026-05-10
8
51
 
package/README.md CHANGED
@@ -8,7 +8,7 @@ Local-first analytics for AI CLI usage. TokenTrace scans local CLI logs, normali
8
8
 
9
9
  TokenTrace is designed for local development machines first, with macOS-oriented defaults. It does not require a cloud account and does not send telemetry or logs anywhere.
10
10
 
11
- ![TokenTrace overview dashboard](docs/assets/overview.png)
11
+ ![TokenTrace overview dashboard](docs/assets/overview-0.8.0.png)
12
12
 
13
13
  ## Start In Seconds
14
14
 
@@ -37,6 +37,12 @@ tokentrace serve --port 3210 --no-open
37
37
  tokentrace scan # Scan local AI CLI usage logs
38
38
  tokentrace doctor --json
39
39
  # Inspect scan health and repair recommendations
40
+ tokentrace evidence --json
41
+ # Print metric evidence trails as JSON
42
+ tokentrace repair --json
43
+ # Print unknown-cost repair groups as JSON
44
+ tokentrace digest --json
45
+ # Print current-month local usage digest
40
46
  tokentrace insights --json
41
47
  # Print local recommendations as JSON
42
48
  tokentrace status --json
@@ -82,7 +88,7 @@ npm run db:migrate # Create/update local SQLite tables
82
88
  npm run db:seed # Seed editable provider/model prices
83
89
  npm run reset # Clear imported data and scan history
84
90
  npm test # Run parser and cost tests
85
- npm run verify # Run Vitest and TypeScript checks
91
+ npm run verify # Run Vitest, TypeScript, and ESLint checks
86
92
  npm run package:test # Verify, build, and dry-run the npm package
87
93
  npm run package:inspect
88
94
  # Check package transparency guardrails
@@ -129,6 +135,10 @@ Default discovery checks these locations when present:
129
135
 
130
136
  Use **Settings** in the dashboard to add custom folders, toggle raw message storage, and trigger scans. Use **Doctor**, **Discovery**, **Parser Debug**, and **Raw Data** to inspect discovered files, parser decisions, warnings, failures, extracted metadata, and confidence levels.
131
137
 
138
+ Settings also supports optional local monthly usage guardrails. Set a cost
139
+ limit, token limit, or both, and Overview will show month-to-date progress from
140
+ imported local CLI usage.
141
+
132
142
  ## Ingestion Architecture
133
143
 
134
144
  TokenTrace's primary ingestion architecture is direct local filesystem ingestion:
@@ -178,6 +188,8 @@ tokentrace statusline claude
178
188
 
179
189
  Claude Code sends session JSON to the command on stdin. TokenTrace reads the transcript path, model, context usage, and session cost, then prints one compact local line:
180
190
 
191
+ Do not set the Claude Code `statusLine.command` to plain `tokentrace`. Plain `tokentrace` starts the dashboard, while `tokentrace statusline claude` prints exactly one status-line response.
192
+
181
193
  ![TokenTrace Claude Code status line](docs/assets/claude-statusline.svg)
182
194
 
183
195
  You can also inspect the same local status outside Claude Code:
@@ -191,6 +203,16 @@ Codex CLI status-line integration is intentionally deferred until its status-lin
191
203
 
192
204
  ## Screenshots
193
205
 
206
+ Evidence + Repair views:
207
+
208
+ ![TokenTrace overview dashboard](docs/assets/overview-0.8.0.png)
209
+
210
+ ![TokenTrace processed tokens evidence trail](docs/assets/evidence-0.8.0.png)
211
+
212
+ ![TokenTrace unknown cost repair queue](docs/assets/repair-0.8.0.png)
213
+
214
+ ![TokenTrace Scan Doctor parser trust report](docs/assets/doctor-parser-trust-0.8.0.png)
215
+
194
216
  CLI startup and help:
195
217
 
196
218
  ![TokenTrace CLI help](docs/assets/cli-help.gif)
@@ -203,24 +225,6 @@ Optional wrapper diagnostics:
203
225
 
204
226
  ![TokenTrace wrapper command](docs/assets/cli-wrapper.gif)
205
227
 
206
- Session exploration:
207
-
208
- ![TokenTrace session explorer](docs/assets/session-explorer.png)
209
-
210
- Scan Doctor and file discovery:
211
-
212
- ![TokenTrace ingestion diagnostics](docs/assets/diagnostics.png)
213
-
214
- ![TokenTrace file discovery](docs/assets/discovery.png)
215
-
216
- Editable model pricing:
217
-
218
- ![TokenTrace pricing configuration](docs/assets/pricing.png)
219
-
220
- Mobile overview:
221
-
222
- ![TokenTrace mobile overview](docs/assets/mobile-overview.png)
223
-
224
228
  ## Privacy Model
225
229
 
226
230
  - All processing runs locally on your machine.
@@ -0,0 +1,81 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ buildUnknownCostRepairWorkbench,
4
+ getUnknownCostReview,
5
+ saveUnknownCostReview,
6
+ type UnknownCostRepairStatus
7
+ } from "@/src/lib/unknown-cost-repair";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ const reviewStates = new Set<UnknownCostRepairStatus>([
12
+ "unresolved",
13
+ "ignored",
14
+ "resolved",
15
+ "needs-parser-review"
16
+ ]);
17
+
18
+ function text(value: unknown, maxLength: number) {
19
+ return typeof value === "string" ? value.trim().slice(0, maxLength) : "";
20
+ }
21
+
22
+ function reviewState(value: unknown): UnknownCostRepairStatus | null {
23
+ return typeof value === "string" && reviewStates.has(value as UnknownCostRepairStatus)
24
+ ? (value as UnknownCostRepairStatus)
25
+ : null;
26
+ }
27
+
28
+ function workbenchGroupForKey(key: string) {
29
+ return buildUnknownCostRepairWorkbench().groups.find((group) => group.key === key) ?? null;
30
+ }
31
+
32
+ export async function GET() {
33
+ return NextResponse.json(buildUnknownCostRepairWorkbench());
34
+ }
35
+
36
+ export async function PUT(request: Request) {
37
+ const body = await request.json();
38
+ const key = text(body.key, 1000);
39
+ const status = reviewState(body.status ?? body.state);
40
+
41
+ if (!key) {
42
+ return NextResponse.json({ error: "key is required" }, { status: 400 });
43
+ }
44
+
45
+ if (!status) {
46
+ return NextResponse.json({ error: "status must be unresolved, ignored, resolved, or needs-parser-review" }, { status: 400 });
47
+ }
48
+
49
+ const group = workbenchGroupForKey(key);
50
+ const existing = group ? null : getUnknownCostReview(key);
51
+ if (!group && (!existing || existing.updatedAt == null)) {
52
+ return NextResponse.json({ error: "repair key was not found in current workbench evidence" }, { status: 404 });
53
+ }
54
+
55
+ const metadata = {
56
+ sourceFile: "",
57
+ model: "",
58
+ cause: ""
59
+ };
60
+ if (group) {
61
+ metadata.sourceFile = group.sourceFile;
62
+ metadata.model = group.model;
63
+ metadata.cause = group.cause;
64
+ } else if (existing) {
65
+ metadata.sourceFile = existing.sourceFile;
66
+ metadata.model = existing.model;
67
+ metadata.cause = existing.cause;
68
+ }
69
+ const review = saveUnknownCostReview({
70
+ key,
71
+ status,
72
+ notes: text(body.notes ?? body.note, 500),
73
+ sourceFile: metadata.sourceFile,
74
+ model: metadata.model,
75
+ cause: metadata.cause
76
+ });
77
+
78
+ return NextResponse.json({ review });
79
+ }
80
+
81
+ export const PATCH = PUT;
@@ -1,6 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { getDatabasePath } from "@/src/db/client";
3
- import { getAppSettings, saveAppSettings } from "@/src/db/settings";
3
+ import { getAppSettings, normalizeUsageGuardrails, saveAppSettings } from "@/src/db/settings";
4
4
 
5
5
  export const dynamic = "force-dynamic";
6
6
 
@@ -18,7 +18,8 @@ export async function PUT(request: Request) {
18
18
  : [];
19
19
  const saved = saveAppSettings({
20
20
  customFolders,
21
- storeRawMessageContent: Boolean(body.storeRawMessageContent)
21
+ storeRawMessageContent: Boolean(body.storeRawMessageContent),
22
+ usageGuardrails: normalizeUsageGuardrails(body.usageGuardrails)
22
23
  });
23
24
 
24
25
  return NextResponse.json(saved);
@@ -2,6 +2,7 @@ import Link from "next/link";
2
2
  import { AlertTriangle, ArrowRight, CheckCircle2, CircleDashed } from "lucide-react";
3
3
  import { Badge } from "@/components/ui/badge";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
5
6
  import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typography";
6
7
  import { ScanHealthSummary } from "@/components/scan-health-summary";
7
8
  import { getAnalyticsData, getScanTrustData, type DebugScanRun } from "@/src/lib/analytics";
@@ -136,6 +137,7 @@ function TrustChecklist({
136
137
  function DoctorReportPanel({ report }: { report: DoctorReport }) {
137
138
  const statusRows = [
138
139
  ["Imported", report.fileStatus.imported],
140
+ ["With errors", report.fileStatus.importedWithErrors],
139
141
  ["Duplicates", report.fileStatus.duplicates],
140
142
  ["Ignored", report.fileStatus.ignored],
141
143
  ["Unsupported", report.fileStatus.unsupported],
@@ -192,7 +194,7 @@ function DoctorReportPanel({ report }: { report: DoctorReport }) {
192
194
  <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
193
195
  <div className="space-y-3">
194
196
  <div className="mb-3 text-sm font-semibold">File handling</div>
195
- <div className="grid border-y sm:grid-cols-5 sm:divide-x xl:grid-cols-2">
197
+ <div className="grid border-y sm:grid-cols-6 sm:divide-x xl:grid-cols-2">
196
198
  {statusRows.map(([label, value]) => (
197
199
  <div key={label} className="p-2">
198
200
  <FieldLabel>{label}</FieldLabel>
@@ -261,6 +263,139 @@ function DoctorReportPanel({ report }: { report: DoctorReport }) {
261
263
  );
262
264
  }
263
265
 
266
+ function ParserTrustPanel({ report }: { report: DoctorReport["parserTrust"] }) {
267
+ return (
268
+ <Card>
269
+ <CardHeader>
270
+ <CardTitle>Parser Trust Report</CardTitle>
271
+ <CardDescription>
272
+ Latest scan files grouped by parser, source family, version, status, and import yield. Ignored files are known support files, not usage transcripts. Unsupported files need parser review before they become usage.
273
+ </CardDescription>
274
+ </CardHeader>
275
+ <CardContent className="table-scroll">
276
+ {report.parsers.length ? (
277
+ <Table className="min-w-[72rem]">
278
+ <TableHeader>
279
+ <TableRow>
280
+ <TableHead>Parser</TableHead>
281
+ <TableHead>Version</TableHead>
282
+ <TableHead>Source</TableHead>
283
+ <TableHead className="text-right">Imported</TableHead>
284
+ <TableHead className="text-right">With errors</TableHead>
285
+ <TableHead className="text-right">Ignored</TableHead>
286
+ <TableHead className="text-right">Unsupported</TableHead>
287
+ <TableHead className="text-right">Failed</TableHead>
288
+ <TableHead className="text-right">Duplicate</TableHead>
289
+ <TableHead className="text-right">Records</TableHead>
290
+ <TableHead className="min-w-56">Latest reason</TableHead>
291
+ </TableRow>
292
+ </TableHeader>
293
+ <TableBody>
294
+ {report.parsers.map((row) => (
295
+ <TableRow key={`${row.parser}:${row.version}:${row.sourceFamily}`}>
296
+ <TableCell className="font-medium">{row.parser}</TableCell>
297
+ <TableCell>
298
+ <Badge variant="secondary">{row.version}</Badge>
299
+ </TableCell>
300
+ <TableCell>{row.sourceFamily}</TableCell>
301
+ <TableCell className="text-right">{row.imported.toLocaleString()}</TableCell>
302
+ <TableCell className="text-right">{row.importedWithErrors.toLocaleString()}</TableCell>
303
+ <TableCell className="text-right">{row.ignored.toLocaleString()}</TableCell>
304
+ <TableCell className="text-right">{row.unsupported.toLocaleString()}</TableCell>
305
+ <TableCell className="text-right">{row.failed.toLocaleString()}</TableCell>
306
+ <TableCell className="text-right">{row.duplicate.toLocaleString()}</TableCell>
307
+ <TableCell className="text-right">{row.recordsImported.toLocaleString()}</TableCell>
308
+ <TableCell className="max-w-md text-xs text-muted-foreground">
309
+ {row.latestReason || "No parser reason recorded."}
310
+ </TableCell>
311
+ </TableRow>
312
+ ))}
313
+ </TableBody>
314
+ </Table>
315
+ ) : (
316
+ <div className="px-4 py-6 text-sm text-muted-foreground">
317
+ No parser trust data yet. Run `tokentrace scan` to populate the latest scan report.
318
+ </div>
319
+ )}
320
+ </CardContent>
321
+ </Card>
322
+ );
323
+ }
324
+
325
+ function formatDelta(value: number) {
326
+ if (value > 0) return `+${value.toLocaleString()}`;
327
+ return value.toLocaleString();
328
+ }
329
+
330
+ function ScanDiffPanel({ report }: { report: DoctorReport["scanDiff"] }) {
331
+ const rows: Array<[string, keyof DoctorReport["scanDiff"]["current"]]> = [
332
+ ["Files scanned", "filesScanned"],
333
+ ["Records imported", "recordsImported"],
334
+ ["Imported", "imported"],
335
+ ["With errors", "importedWithErrors"],
336
+ ["Duplicates", "duplicates"],
337
+ ["Ignored", "ignored"],
338
+ ["Unsupported", "unsupported"],
339
+ ["Failed", "failed"]
340
+ ];
341
+
342
+ return (
343
+ <Card>
344
+ <CardHeader>
345
+ <CardTitle>Scan History Diff</CardTitle>
346
+ <CardDescription>
347
+ Latest scan compared with the previous scan using deterministic scan ordering. Ignored files are known support files, not usage transcripts.
348
+ </CardDescription>
349
+ </CardHeader>
350
+ <CardContent className="table-scroll space-y-4">
351
+ <div className="grid border-y md:grid-cols-2 md:divide-x">
352
+ <div className="min-w-0 p-3">
353
+ <FieldLabel>Latest scan</FieldLabel>
354
+ <div className="mt-1 text-sm font-semibold">{formatDate(report.latestCompletedAt ?? report.latestStartedAt)}</div>
355
+ <MonoText className="mt-1 block truncate text-xs text-muted-foreground">
356
+ {report.latestScanId ?? "No scan"}
357
+ </MonoText>
358
+ </div>
359
+ <div className="min-w-0 p-3">
360
+ <FieldLabel>Previous scan</FieldLabel>
361
+ <div className="mt-1 text-sm font-semibold">{formatDate(report.previousCompletedAt ?? report.previousStartedAt)}</div>
362
+ <MonoText className="mt-1 block truncate text-xs text-muted-foreground">
363
+ {report.previousScanId ?? "No previous scan"}
364
+ </MonoText>
365
+ </div>
366
+ </div>
367
+
368
+ <Table>
369
+ <TableHeader>
370
+ <TableRow>
371
+ <TableHead>Count</TableHead>
372
+ <TableHead className="text-right">Current</TableHead>
373
+ <TableHead className="text-right">Previous</TableHead>
374
+ <TableHead className="text-right">Delta</TableHead>
375
+ </TableRow>
376
+ </TableHeader>
377
+ <TableBody>
378
+ {rows.map(([label, key]) => (
379
+ <TableRow key={key}>
380
+ <TableCell className="font-medium">{label}</TableCell>
381
+ <TableCell className="text-right">{report.current[key].toLocaleString()}</TableCell>
382
+ <TableCell className="text-right">{report.previous[key].toLocaleString()}</TableCell>
383
+ <TableCell className="text-right">{formatDelta(report.delta[key])}</TableCell>
384
+ </TableRow>
385
+ ))}
386
+ </TableBody>
387
+ </Table>
388
+
389
+ {report.explanation ? (
390
+ <div className="rounded-md border bg-muted/30 p-3 text-sm leading-relaxed text-muted-foreground">
391
+ {report.explanation}
392
+ </div>
393
+ ) : null}
394
+ </CardContent>
395
+ </Card>
396
+ );
397
+ }
398
+
264
399
  function scanRunVariant(scanRun: DebugScanRun) {
265
400
  if (scanRun.errors.length > 0) return "destructive";
266
401
  if (scanRun.warnings.length > 0) return "warning";
@@ -366,6 +501,10 @@ export default async function DiagnosticsPage() {
366
501
 
367
502
  <DoctorReportPanel report={doctorReport} />
368
503
 
504
+ <ParserTrustPanel report={doctorReport.parserTrust} />
505
+
506
+ <ScanDiffPanel report={doctorReport.scanDiff} />
507
+
369
508
  <ScanHistoryPanel scanRuns={data.scanRuns} />
370
509
 
371
510
  <ScanHealthSummary health={data.health} />
@@ -0,0 +1,170 @@
1
+ import Link from "next/link";
2
+ import { ArrowLeft, ArrowRight } from "lucide-react";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
7
+ import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typography";
8
+ import { buildEvidenceTrail, parseEvidenceMetric } from "@/src/lib/evidence-trail";
9
+ import { formatCurrency, formatTokens, percent } from "@/src/lib/format";
10
+
11
+ export const dynamic = "force-dynamic";
12
+
13
+ type EvidencePageProps = {
14
+ searchParams?: Promise<Record<string, string | string[] | undefined>>;
15
+ };
16
+
17
+ function confidenceVariant(value: string) {
18
+ if (value === "exact") return "success";
19
+ if (value === "unknown") return "warning";
20
+ return "secondary";
21
+ }
22
+
23
+ function parserStatusVariant(value: string | null) {
24
+ if (value === "imported") return "success";
25
+ if (value === "imported_with_errors") return "warning";
26
+ if (!value) return "secondary";
27
+ return "outline";
28
+ }
29
+
30
+ export default async function EvidencePage({ searchParams }: EvidencePageProps) {
31
+ const params = (await searchParams) ?? {};
32
+ const trail = buildEvidenceTrail({ metric: parseEvidenceMetric(params?.metric) });
33
+
34
+ return (
35
+ <div className="space-y-6">
36
+ <PageHeader
37
+ title={`${trail.title} Evidence`}
38
+ description={trail.description}
39
+ actions={
40
+ <Button asChild variant="outline" size="sm">
41
+ <Link href="/">
42
+ <ArrowLeft className="h-4 w-4" />
43
+ Overview
44
+ </Link>
45
+ </Button>
46
+ }
47
+ />
48
+
49
+ <Card>
50
+ <CardHeader>
51
+ <CardTitle>Metric Totals</CardTitle>
52
+ <CardDescription>
53
+ Totals use the same filtered metric definition as the session evidence below.
54
+ </CardDescription>
55
+ </CardHeader>
56
+ <CardContent className="p-0">
57
+ <div className="grid divide-y border-t sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-5">
58
+ <div className="p-3">
59
+ <FieldLabel>Tokens</FieldLabel>
60
+ <DataValue className="mt-1" size="md">{formatTokens(trail.totals.tokens)}</DataValue>
61
+ </div>
62
+ <div className="p-3">
63
+ <FieldLabel>Cost</FieldLabel>
64
+ <DataValue className="mt-1" size="md">{formatCurrency(trail.totals.cost)}</DataValue>
65
+ </div>
66
+ <div className="p-3">
67
+ <FieldLabel>Sessions</FieldLabel>
68
+ <DataValue className="mt-1" size="md">{trail.totals.sessions.toLocaleString()}</DataValue>
69
+ </div>
70
+ <div className="p-3">
71
+ <FieldLabel>Interactions</FieldLabel>
72
+ <DataValue className="mt-1" size="md">{trail.totals.interactions.toLocaleString()}</DataValue>
73
+ </div>
74
+ <div className="p-3">
75
+ <FieldLabel>Unknown Cost</FieldLabel>
76
+ <DataValue className="mt-1" size="md">{trail.totals.unknownCostInteractions.toLocaleString()}</DataValue>
77
+ </div>
78
+ </div>
79
+ </CardContent>
80
+ </Card>
81
+
82
+ <Card>
83
+ <CardHeader>
84
+ <CardTitle>Session, Source, Parser, And Pricing Evidence</CardTitle>
85
+ <CardDescription>
86
+ The table is capped at the top 100 contributing sessions; totals above include the full metric set.
87
+ </CardDescription>
88
+ </CardHeader>
89
+ <CardContent className="table-scroll p-0">
90
+ <Table>
91
+ <TableHeader>
92
+ <TableRow>
93
+ <TableHead>Session</TableHead>
94
+ <TableHead>Metric Total</TableHead>
95
+ <TableHead>Cost</TableHead>
96
+ <TableHead>Source</TableHead>
97
+ <TableHead>Parser</TableHead>
98
+ <TableHead>Pricing</TableHead>
99
+ <TableHead>Confidence</TableHead>
100
+ </TableRow>
101
+ </TableHeader>
102
+ <TableBody>
103
+ {trail.sessions.length ? (
104
+ trail.sessions.map((session) => (
105
+ <TableRow key={session.id}>
106
+ <TableCell className="min-w-64">
107
+ <Link href={session.sessionHref} className="font-medium text-primary underline-offset-4 hover:underline">
108
+ {session.title}
109
+ </Link>
110
+ <div className="mt-1 text-xs text-muted-foreground">
111
+ {session.tool} / {session.provider} / {session.project}
112
+ </div>
113
+ </TableCell>
114
+ <TableCell>{formatTokens(session.totalTokens)}</TableCell>
115
+ <TableCell>{formatCurrency(session.cost)}</TableCell>
116
+ <TableCell className="max-w-80">
117
+ <Link href={session.sourceHref} title={session.sourceFile}>
118
+ <MonoText className="block truncate text-muted-foreground underline-offset-4 hover:underline">
119
+ {session.sourceFile}
120
+ </MonoText>
121
+ </Link>
122
+ </TableCell>
123
+ <TableCell className="min-w-44">
124
+ <div className="flex flex-wrap items-center gap-2">
125
+ <Badge variant={parserStatusVariant(session.parserStatus)}>
126
+ {session.parserStatus ?? "not scanned"}
127
+ </Badge>
128
+ <Link href={session.parserHref} className="text-xs font-medium text-primary underline-offset-4 hover:underline">
129
+ Parser <ArrowRight className="inline h-3.5 w-3.5" />
130
+ </Link>
131
+ </div>
132
+ <div className="mt-1 text-xs text-muted-foreground">
133
+ {session.parser ?? "No parser"} / {session.parserConfidence == null ? "confidence unknown" : percent(session.parserConfidence)}
134
+ </div>
135
+ </TableCell>
136
+ <TableCell className="min-w-44">
137
+ {session.pricingHref ? (
138
+ <Link href={session.pricingHref} className="font-medium text-primary underline-offset-4 hover:underline">
139
+ {session.model}
140
+ </Link>
141
+ ) : (
142
+ <span>{session.model}</span>
143
+ )}
144
+ </TableCell>
145
+ <TableCell>
146
+ <div className="flex flex-col items-start gap-1">
147
+ <Badge variant={confidenceVariant(session.tokenConfidence)}>
148
+ {session.tokenConfidence}
149
+ </Badge>
150
+ <span className="text-xs text-muted-foreground">
151
+ {session.interactions.toLocaleString()} interactions
152
+ </span>
153
+ </div>
154
+ </TableCell>
155
+ </TableRow>
156
+ ))
157
+ ) : (
158
+ <TableRow>
159
+ <TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
160
+ No evidence is available for this metric yet.
161
+ </TableCell>
162
+ </TableRow>
163
+ )}
164
+ </TableBody>
165
+ </Table>
166
+ </CardContent>
167
+ </Card>
168
+ </div>
169
+ );
170
+ }
package/app/globals.css CHANGED
@@ -54,11 +54,17 @@ body {
54
54
  }
55
55
 
56
56
  .period-date-input {
57
- padding-inline-end: 0.5rem;
57
+ color-scheme: light;
58
+ position: relative;
58
59
  }
59
60
 
60
61
  .period-date-input::-webkit-calendar-picker-indicator {
61
62
  cursor: pointer;
62
- margin-inline-end: 0.25rem;
63
- opacity: 0.8;
63
+ height: 100%;
64
+ margin: 0;
65
+ opacity: 0;
66
+ padding: 0;
67
+ position: absolute;
68
+ right: 0;
69
+ width: 2.5rem;
64
70
  }
package/app/layout.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import type { Metadata } from "next";
2
3
  import "./globals.css";
3
4
  import { MobileNav, Sidebar } from "@/components/sidebar";
@@ -17,7 +18,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
17
18
  <body>
18
19
  <div className="flex min-h-screen">
19
20
  <Sidebar appVersion={appVersion} />
20
- <main className="min-w-0 flex-1">
21
+ <main className="min-w-0 flex-1 overflow-x-hidden">
21
22
  <div className="border-b bg-card px-4 py-3 md:hidden">
22
23
  <div className="flex items-center justify-between gap-3">
23
24
  <div className="flex min-w-0 items-center gap-2">
@@ -33,7 +34,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
33
34
  </div>
34
35
  </div>
35
36
  <MobileNav />
36
- <div className="mx-auto w-full max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
37
+ <div className="mx-auto w-full min-w-0 max-w-[100vw] px-4 py-6 sm:px-6 md:max-w-7xl lg:px-8">
37
38
  {children}
38
39
  </div>
39
40
  </main>