tokentrace 0.7.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.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -30
- package/app/api/repair-items/route.ts +81 -0
- package/app/diagnostics/page.tsx +140 -1
- package/app/evidence/page.tsx +170 -0
- package/app/globals.css +9 -3
- package/app/layout.tsx +3 -2
- package/app/page.tsx +50 -39
- package/app/repair/page.tsx +199 -0
- package/bin/tokentrace.js +28 -4
- package/components/period-filter.tsx +29 -10
- package/components/repair-state-control.tsx +109 -0
- package/components/ui/typography.tsx +3 -3
- package/dist/runtime/db-migrate.mjs +74 -1
- package/dist/runtime/db-seed.mjs +74 -1
- package/dist/runtime/digest.mjs +110 -13
- package/dist/runtime/doctor.mjs +368 -47
- package/dist/runtime/evidence.mjs +781 -0
- package/dist/runtime/insights.mjs +108 -11
- package/dist/runtime/pricing-refresh.mjs +74 -1
- package/dist/runtime/repair.mjs +863 -0
- package/dist/runtime/reset.mjs +74 -1
- package/dist/runtime/scan.mjs +74 -1
- package/dist/runtime/status.mjs +678 -533
- package/docs/assets/doctor-parser-trust-0.8.0.png +0 -0
- package/docs/assets/evidence-0.8.0.png +0 -0
- package/docs/assets/overview-0.8.0.png +0 -0
- package/docs/assets/repair-0.8.0.png +0 -0
- package/package.json +1 -1
- package/scripts/build-cli-runtime.mjs +2 -0
- package/scripts/doctor.ts +3 -1
- package/scripts/evidence.ts +92 -0
- package/scripts/repair.ts +57 -0
- package/scripts/smoke-cli.mjs +60 -0
- package/scripts/status.ts +8 -9
- package/src/db/migrate-core.ts +76 -0
- package/src/db/schema.ts +16 -0
- package/src/lib/analytics.ts +18 -3
- package/src/lib/claude-statusline.ts +202 -0
- package/src/lib/doctor.ts +19 -3
- package/src/lib/evidence-trail.ts +309 -0
- package/src/lib/live-status.ts +7 -205
- package/src/lib/parser-trust.ts +188 -0
- package/src/lib/scan-diff.ts +204 -0
- package/src/lib/unknown-cost-repair.ts +532 -0
- package/docs/assets/diagnostics.png +0 -0
- package/docs/assets/discovery.png +0 -0
- package/docs/assets/doctor-0.6.0.png +0 -0
- package/docs/assets/mobile-overview.png +0 -0
- package/docs/assets/overview-0.6.0.png +0 -0
- package/docs/assets/overview-0.7.0.png +0 -0
- package/docs/assets/overview.png +0 -0
- package/docs/assets/pricing.png +0 -0
- package/docs/assets/projects-0.7.0.png +0 -0
- package/docs/assets/session-explorer.png +0 -0
- package/docs/assets/sessions-0.7.0.png +0 -0
- package/docs/assets/settings-guardrails-0.7.0.png +0 -0
- package/docs/assets/settings-package-trust-0.6.0.png +0 -0
- package/docs/assets/usage-intelligence-0.7.0.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to TokenTrace are documented here.
|
|
4
4
|
|
|
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
|
+
|
|
5
27
|
## [0.7.0] - 2026-05-12
|
|
6
28
|
|
|
7
29
|
### Added
|
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
|
-

|
|
12
12
|
|
|
13
13
|
## Start In Seconds
|
|
14
14
|
|
|
@@ -37,6 +37,10 @@ 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
|
|
40
44
|
tokentrace digest --json
|
|
41
45
|
# Print current-month local usage digest
|
|
42
46
|
tokentrace insights --json
|
|
@@ -184,6 +188,8 @@ tokentrace statusline claude
|
|
|
184
188
|
|
|
185
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:
|
|
186
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
|
+
|
|
187
193
|

|
|
188
194
|
|
|
189
195
|
You can also inspect the same local status outside Claude Code:
|
|
@@ -197,21 +203,15 @@ Codex CLI status-line integration is intentionally deferred until its status-lin
|
|
|
197
203
|
|
|
198
204
|
## Screenshots
|
|
199
205
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-

|
|
203
|
-
|
|
204
|
-

|
|
205
|
-
|
|
206
|
-

|
|
206
|
+
Evidence + Repair views:
|
|
207
207
|
|
|
208
|
-

|
|
209
209
|
|
|
210
|
-

|
|
211
211
|
|
|
212
|
-
|
|
212
|
+

|
|
213
213
|
|
|
214
|
-

|
|
215
215
|
|
|
216
216
|
CLI startup and help:
|
|
217
217
|
|
|
@@ -225,24 +225,6 @@ Optional wrapper diagnostics:
|
|
|
225
225
|
|
|
226
226
|

|
|
227
227
|
|
|
228
|
-
Session exploration:
|
|
229
|
-
|
|
230
|
-

|
|
231
|
-
|
|
232
|
-
Scan Doctor and file discovery:
|
|
233
|
-
|
|
234
|
-

|
|
235
|
-
|
|
236
|
-

|
|
237
|
-
|
|
238
|
-
Editable model pricing:
|
|
239
|
-
|
|
240
|
-

|
|
241
|
-
|
|
242
|
-
Mobile overview:
|
|
243
|
-
|
|
244
|
-

|
|
245
|
-
|
|
246
228
|
## Privacy Model
|
|
247
229
|
|
|
248
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;
|
package/app/diagnostics/page.tsx
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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-
|
|
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>
|