tokentrace 0.7.0 → 0.8.1
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 +36 -0
- package/README.md +12 -30
- package/app/api/prices/refresh/route.ts +6 -1
- package/app/api/prices/route.ts +36 -13
- package/app/api/repair-items/route.ts +86 -0
- package/app/api/scan/route.ts +6 -1
- package/app/api/settings/route.ts +6 -1
- 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 +552 -285
- package/dist/runtime/doctor.mjs +2489 -384
- package/dist/runtime/evidence.mjs +781 -0
- package/dist/runtime/insights.mjs +826 -263
- package/dist/runtime/pricing-refresh.mjs +1162 -967
- package/dist/runtime/repair.mjs +863 -0
- package/dist/runtime/reset.mjs +74 -1
- package/dist/runtime/scan.mjs +1261 -702
- package/dist/runtime/status.mjs +842 -542
- 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 +2 -2
- package/scripts/build-cli-runtime.mjs +2 -0
- package/scripts/digest.ts +21 -3
- package/scripts/doctor.ts +25 -5
- package/scripts/evidence.ts +92 -0
- package/scripts/insights.ts +18 -2
- package/scripts/pricing-refresh.ts +25 -9
- package/scripts/repair.ts +57 -0
- package/scripts/scan.ts +21 -8
- package/scripts/smoke-cli.mjs +60 -0
- package/scripts/status.ts +37 -43
- package/src/db/migrate-core.ts +76 -0
- package/src/db/schema.ts +16 -0
- package/src/ingestion/adapters/claude-code.ts +51 -20
- package/src/ingestion/adapters/codex-cli.ts +203 -17
- package/src/ingestion/adapters/generic-json.ts +1 -1
- package/src/ingestion/adapters/generic-jsonl.ts +1 -1
- package/src/ingestion/adapters/generic-log.ts +60 -10
- package/src/ingestion/adapters/helpers.ts +107 -42
- package/src/ingestion/discovery.ts +11 -2
- package/src/ingestion/persist.ts +43 -4
- package/src/ingestion/scan.ts +45 -18
- package/src/lib/analytics.ts +38 -22
- package/src/lib/api-json.ts +21 -0
- 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/pricing-refresh-cli.ts +57 -0
- package/src/lib/report-cli.ts +36 -0
- package/src/lib/scan-cli.ts +50 -0
- package/src/lib/scan-diff.ts +204 -0
- package/src/lib/status-cli.ts +150 -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,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to TokenTrace are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.8.1] - 2026-05-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Codex CLI imports now read exact `token_count` totals, including cached input and reasoning output tokens, so cleared Codex sessions are no longer undercounted.
|
|
10
|
+
- Claude Code and Codex session artifacts can exceed the generic file-size cap without being skipped during discovery.
|
|
11
|
+
- OpenAI-style, Claude-style, and generic usage parsers now normalize cached, cache-write, and reasoning token fields without double-counting them.
|
|
12
|
+
- Parser-version-aware rescans now reprocess stale imports, and source-file replacement is atomic so a failed replacement cannot delete prior trusted sessions.
|
|
13
|
+
- Unknown-cost cause summaries now assign each interaction to one primary cause instead of overlapping buckets.
|
|
14
|
+
- Pricing, scan, settings, repair, and pricing-refresh APIs now reject malformed JSON with clean 400 responses.
|
|
15
|
+
- Manual pricing saves now reject blank model/provider names and invalid numeric prices instead of silently storing unknown prices.
|
|
16
|
+
- CLI commands now reject unknown flags across scan, pricing refresh, status, doctor, digest, and insights commands.
|
|
17
|
+
- Local release checks now run ProjScan through `projscan@latest`, matching the GitHub security workflow guardrail.
|
|
18
|
+
|
|
19
|
+
## [0.8.0] - 2026-05-12
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Evidence detail pages and `tokentrace evidence --json` for tracing metric totals back to sessions, source files, parser status, and pricing context.
|
|
24
|
+
- Unknown Cost Repair workbench and `tokentrace repair --json` for grouped local repair state, alias hints, parser review links, and pricing follow-up.
|
|
25
|
+
- Parser Trust Report and Scan History Diff panels in Diagnostics for latest scan parser coverage and scan-to-scan import changes.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Overview metric cards now link major totals to evidence trails and route unknown cost work to Unknown Cost Repair.
|
|
30
|
+
- 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.
|
|
31
|
+
- Dense evidence, repair, parser trust, and scan diff tables preserve horizontal scrolling and stable source-path truncation.
|
|
32
|
+
- Evidence and repair copy now states local-first behavior, support-file ignores, and parser-review requirements for unsupported files.
|
|
33
|
+
- README screenshots now use public-safe synthetic Evidence + Repair views, and obsolete screenshot assets were removed from the package payload.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- 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.
|
|
38
|
+
- The app shell now constrains page width on small screens so wide toolbars scroll internally instead of widening the whole page.
|
|
39
|
+
- `tokentrace statusline setup claude` and piped Claude status-line input no longer touch the TokenTrace app database or start the dashboard.
|
|
40
|
+
|
|
5
41
|
## [0.7.0] - 2026-05-12
|
|
6
42
|
|
|
7
43
|
### 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.
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { readJsonObject } from "@/src/lib/api-json";
|
|
2
3
|
import { refreshPricing } from "@/src/lib/pricing-refresh";
|
|
3
4
|
|
|
4
5
|
export const dynamic = "force-dynamic";
|
|
5
6
|
|
|
6
7
|
export async function POST(request: Request) {
|
|
7
|
-
const
|
|
8
|
+
const parsed = await readJsonObject(request);
|
|
9
|
+
if (!parsed.ok) {
|
|
10
|
+
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
const body = parsed.body;
|
|
8
13
|
const result = await refreshPricing({
|
|
9
14
|
source: body?.source === "bundled" ? "bundled" : "remote",
|
|
10
15
|
force: Boolean(body?.force)
|
package/app/api/prices/route.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { readJsonObject } from "@/src/lib/api-json";
|
|
2
3
|
import { getPricingRows, upsertPricing } from "@/src/lib/pricing";
|
|
3
4
|
|
|
4
5
|
export const dynamic = "force-dynamic";
|
|
5
6
|
|
|
6
|
-
function
|
|
7
|
-
|
|
7
|
+
function requiredText(value: unknown) {
|
|
8
|
+
return typeof value === "string" ? value.trim() : "";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nullablePrice(value: unknown, field: string) {
|
|
12
|
+
if (value == null) return { ok: true as const, value: null };
|
|
13
|
+
if (typeof value === "string" && value.trim() === "") return { ok: true as const, value: null };
|
|
8
14
|
const number = Number(value);
|
|
9
|
-
|
|
15
|
+
if (!Number.isFinite(number) || number < 0) {
|
|
16
|
+
return { ok: false as const, error: `${field} must be a non-negative number or empty` };
|
|
17
|
+
}
|
|
18
|
+
return { ok: true as const, value: number };
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
export async function GET() {
|
|
@@ -14,20 +23,34 @@ export async function GET() {
|
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
export async function POST(request: Request) {
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
26
|
+
const parsed = await readJsonObject(request);
|
|
27
|
+
if (!parsed.ok) {
|
|
28
|
+
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
const body = parsed.body;
|
|
31
|
+
const providerId = requiredText(body.providerId);
|
|
32
|
+
const model = requiredText(body.model);
|
|
33
|
+
if (!providerId || !model) {
|
|
19
34
|
return NextResponse.json({ error: "providerId and model are required" }, { status: 400 });
|
|
20
35
|
}
|
|
36
|
+
const inputTokenPrice = nullablePrice(body.inputTokenPrice, "inputTokenPrice");
|
|
37
|
+
const outputTokenPrice = nullablePrice(body.outputTokenPrice, "outputTokenPrice");
|
|
38
|
+
const cachedInputTokenPrice = nullablePrice(body.cachedInputTokenPrice, "cachedInputTokenPrice");
|
|
39
|
+
const cacheWriteTokenPrice = nullablePrice(body.cacheWriteTokenPrice, "cacheWriteTokenPrice");
|
|
40
|
+
if (!inputTokenPrice.ok) return NextResponse.json({ error: inputTokenPrice.error }, { status: 400 });
|
|
41
|
+
if (!outputTokenPrice.ok) return NextResponse.json({ error: outputTokenPrice.error }, { status: 400 });
|
|
42
|
+
if (!cachedInputTokenPrice.ok) return NextResponse.json({ error: cachedInputTokenPrice.error }, { status: 400 });
|
|
43
|
+
if (!cacheWriteTokenPrice.ok) return NextResponse.json({ error: cacheWriteTokenPrice.error }, { status: 400 });
|
|
21
44
|
|
|
22
45
|
const id = upsertPricing({
|
|
23
|
-
providerId
|
|
24
|
-
providerName:
|
|
25
|
-
model
|
|
26
|
-
inputTokenPrice:
|
|
27
|
-
outputTokenPrice:
|
|
28
|
-
cachedInputTokenPrice:
|
|
29
|
-
cacheWriteTokenPrice:
|
|
30
|
-
currency:
|
|
46
|
+
providerId,
|
|
47
|
+
providerName: requiredText(body.providerName) || undefined,
|
|
48
|
+
model,
|
|
49
|
+
inputTokenPrice: inputTokenPrice.value,
|
|
50
|
+
outputTokenPrice: outputTokenPrice.value,
|
|
51
|
+
cachedInputTokenPrice: cachedInputTokenPrice.value,
|
|
52
|
+
cacheWriteTokenPrice: cacheWriteTokenPrice.value,
|
|
53
|
+
currency: requiredText(body.currency) || "USD"
|
|
31
54
|
});
|
|
32
55
|
|
|
33
56
|
return NextResponse.json({ id, costsRecalculated: true });
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
import { readJsonObject } from "@/src/lib/api-json";
|
|
9
|
+
|
|
10
|
+
export const dynamic = "force-dynamic";
|
|
11
|
+
|
|
12
|
+
const reviewStates = new Set<UnknownCostRepairStatus>([
|
|
13
|
+
"unresolved",
|
|
14
|
+
"ignored",
|
|
15
|
+
"resolved",
|
|
16
|
+
"needs-parser-review"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function text(value: unknown, maxLength: number) {
|
|
20
|
+
return typeof value === "string" ? value.trim().slice(0, maxLength) : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function reviewState(value: unknown): UnknownCostRepairStatus | null {
|
|
24
|
+
return typeof value === "string" && reviewStates.has(value as UnknownCostRepairStatus)
|
|
25
|
+
? (value as UnknownCostRepairStatus)
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function workbenchGroupForKey(key: string) {
|
|
30
|
+
return buildUnknownCostRepairWorkbench().groups.find((group) => group.key === key) ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function GET() {
|
|
34
|
+
return NextResponse.json(buildUnknownCostRepairWorkbench());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function PUT(request: Request) {
|
|
38
|
+
const parsed = await readJsonObject(request);
|
|
39
|
+
if (!parsed.ok) {
|
|
40
|
+
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
const body = parsed.body;
|
|
43
|
+
const key = text(body.key, 1000);
|
|
44
|
+
const status = reviewState(body.status ?? body.state);
|
|
45
|
+
|
|
46
|
+
if (!key) {
|
|
47
|
+
return NextResponse.json({ error: "key is required" }, { status: 400 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!status) {
|
|
51
|
+
return NextResponse.json({ error: "status must be unresolved, ignored, resolved, or needs-parser-review" }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const group = workbenchGroupForKey(key);
|
|
55
|
+
const existing = group ? null : getUnknownCostReview(key);
|
|
56
|
+
if (!group && (!existing || existing.updatedAt == null)) {
|
|
57
|
+
return NextResponse.json({ error: "repair key was not found in current workbench evidence" }, { status: 404 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const metadata = {
|
|
61
|
+
sourceFile: "",
|
|
62
|
+
model: "",
|
|
63
|
+
cause: ""
|
|
64
|
+
};
|
|
65
|
+
if (group) {
|
|
66
|
+
metadata.sourceFile = group.sourceFile;
|
|
67
|
+
metadata.model = group.model;
|
|
68
|
+
metadata.cause = group.cause;
|
|
69
|
+
} else if (existing) {
|
|
70
|
+
metadata.sourceFile = existing.sourceFile;
|
|
71
|
+
metadata.model = existing.model;
|
|
72
|
+
metadata.cause = existing.cause;
|
|
73
|
+
}
|
|
74
|
+
const review = saveUnknownCostReview({
|
|
75
|
+
key,
|
|
76
|
+
status,
|
|
77
|
+
notes: text(body.notes ?? body.note, 500),
|
|
78
|
+
sourceFile: metadata.sourceFile,
|
|
79
|
+
model: metadata.model,
|
|
80
|
+
cause: metadata.cause
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return NextResponse.json({ review });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const PATCH = PUT;
|
package/app/api/scan/route.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { runScan } from "@/src/ingestion/scan";
|
|
3
|
+
import { readJsonObject } from "@/src/lib/api-json";
|
|
3
4
|
|
|
4
5
|
export const dynamic = "force-dynamic";
|
|
5
6
|
|
|
6
7
|
export async function POST(request: Request) {
|
|
7
|
-
const
|
|
8
|
+
const parsed = await readJsonObject(request);
|
|
9
|
+
if (!parsed.ok) {
|
|
10
|
+
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
const body = parsed.body;
|
|
8
13
|
const result = await runScan({
|
|
9
14
|
folders: Array.isArray(body.folders)
|
|
10
15
|
? body.folders.filter((folder: unknown): folder is string => typeof folder === "string")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { getDatabasePath } from "@/src/db/client";
|
|
3
3
|
import { getAppSettings, normalizeUsageGuardrails, saveAppSettings } from "@/src/db/settings";
|
|
4
|
+
import { readJsonObject } from "@/src/lib/api-json";
|
|
4
5
|
|
|
5
6
|
export const dynamic = "force-dynamic";
|
|
6
7
|
|
|
@@ -12,7 +13,11 @@ export async function GET() {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export async function PUT(request: Request) {
|
|
15
|
-
const
|
|
16
|
+
const parsed = await readJsonObject(request);
|
|
17
|
+
if (!parsed.ok) {
|
|
18
|
+
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
const body = parsed.body;
|
|
16
21
|
const customFolders = Array.isArray(body.customFolders)
|
|
17
22
|
? body.customFolders.filter((folder: unknown): folder is string => typeof folder === "string")
|
|
18
23
|
: [];
|
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} />
|