tokentrace 0.19.1 → 0.20.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 (145) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +1 -1
  3. package/TOKENTRACE_AGENT.md +11 -0
  4. package/app/api/evidence-pack/route.ts +3 -40
  5. package/app/api/export/route.ts +11 -9
  6. package/app/api/reports/route.ts +9 -46
  7. package/app/api/saved-views/[id]/route.ts +5 -1
  8. package/app/evidence/evidence-context-panel.tsx +40 -0
  9. package/app/evidence/evidence-page-data.ts +105 -0
  10. package/app/evidence/evidence-summary-cards.tsx +74 -0
  11. package/app/evidence/evidence-tables.tsx +185 -0
  12. package/app/evidence/evidence-workbench.tsx +101 -0
  13. package/app/evidence/page.tsx +47 -437
  14. package/app/guide/command-block.tsx +10 -0
  15. package/app/guide/guide-content.ts +103 -0
  16. package/app/guide/page.tsx +21 -531
  17. package/app/guide/section-title.tsx +22 -0
  18. package/app/guide/sections/agent-handoff-section.tsx +140 -0
  19. package/app/guide/sections/daily-loop-section.tsx +36 -0
  20. package/app/guide/sections/guide-nav.tsx +39 -0
  21. package/app/guide/sections/setup-status-section.tsx +47 -0
  22. package/app/guide/sections/start-section.tsx +52 -0
  23. package/app/guide/sections/status-line-section.tsx +79 -0
  24. package/app/guide/sections/troubleshooting-section.tsx +71 -0
  25. package/app/models/page.tsx +2 -2
  26. package/app/page.tsx +4 -4
  27. package/app/projects/page.tsx +1 -1
  28. package/app/tools/page.tsx +2 -2
  29. package/bin/tokentrace.js +19 -7
  30. package/components/charts/rank-bar-chart-lazy.tsx +3 -1
  31. package/components/charts/rank-bar-chart.tsx +4 -4
  32. package/components/charts/trend-chart.tsx +5 -3
  33. package/components/hooks/use-json-request.ts +46 -0
  34. package/components/overview/current-mix-panel.tsx +1 -1
  35. package/components/overview/summary-cards.tsx +64 -34
  36. package/components/parser-debug/parser-overrides-panel.tsx +32 -30
  37. package/components/pricing/pricing-workflow.ts +6 -3
  38. package/components/repair/repair-guidance.tsx +2 -2
  39. package/components/repair-bulk-actions.tsx +14 -13
  40. package/components/repair-state-control.tsx +13 -17
  41. package/components/reports/saved-reports-panel.tsx +23 -25
  42. package/components/session-explorer/filters-section.tsx +152 -0
  43. package/components/session-explorer/saved-views-section.tsx +99 -0
  44. package/components/session-explorer/sessions-table.tsx +219 -0
  45. package/components/session-explorer/use-saved-views.ts +51 -0
  46. package/components/session-explorer/use-session-filters.ts +121 -0
  47. package/components/session-explorer.tsx +64 -464
  48. package/components/settings/custom-folders-section.tsx +4 -13
  49. package/components/settings/form-values.ts +18 -0
  50. package/components/settings/guardrails-section.tsx +22 -37
  51. package/components/settings/import-profiles-section.tsx +18 -26
  52. package/components/settings/scan-section.tsx +16 -27
  53. package/components/settings/types.ts +3 -0
  54. package/components/settings/use-folders-section.ts +24 -0
  55. package/components/settings/use-guardrails-section.ts +78 -0
  56. package/components/settings/use-import-profiles-section.ts +102 -0
  57. package/components/settings/use-scan-controls-section.ts +70 -0
  58. package/components/settings/use-scan-schedule-section.ts +33 -0
  59. package/components/settings/use-settings-status.ts +24 -0
  60. package/components/settings/use-storage-section.ts +12 -0
  61. package/components/settings-panel.tsx +38 -256
  62. package/components/sidebar.tsx +64 -26
  63. package/dist/cli/main.mjs +746 -0
  64. package/dist/runtime/agent.mjs +43 -7
  65. package/dist/runtime/anomalies.mjs +40 -32
  66. package/dist/runtime/chatgpt-app.mjs +3901 -0
  67. package/dist/runtime/digest.mjs +9 -4
  68. package/dist/runtime/doctor.mjs +3505 -3499
  69. package/dist/runtime/evidence.mjs +10 -12
  70. package/dist/runtime/insights.mjs +7 -4
  71. package/dist/runtime/mcp.mjs +9866 -43
  72. package/dist/runtime/pricing-refresh.mjs +7 -2
  73. package/dist/runtime/query.mjs +4 -0
  74. package/dist/runtime/repair.mjs +20 -16
  75. package/dist/runtime/report.mjs +534 -481
  76. package/dist/runtime/review.mjs +9 -4
  77. package/dist/runtime/scan.mjs +67 -42
  78. package/dist/runtime/status.mjs +4 -2
  79. package/docs/CHATGPT_APP_PROTOTYPE.md +103 -0
  80. package/docs/CHATGPT_APP_RELEASE.md +101 -0
  81. package/docs/chatgpt-app/README.md +24 -0
  82. package/docs/chatgpt-app/assets/icon.svg +10 -0
  83. package/docs/chatgpt-app/assets/listing-card.svg +34 -0
  84. package/docs/chatgpt-app/assets/widget-preview.svg +24 -0
  85. package/docs/chatgpt-app/dashboard-fields.json +39 -0
  86. package/docs/chatgpt-app/manual-release-steps.md +114 -0
  87. package/docs/chatgpt-app/privacy-and-data.md +39 -0
  88. package/docs/chatgpt-app/review-response-template.md +53 -0
  89. package/docs/chatgpt-app/screenshot-checklist.md +46 -0
  90. package/docs/chatgpt-app/submission-copy.md +45 -0
  91. package/docs/chatgpt-app/test-prompts-and-responses.md +61 -0
  92. package/llms.txt +13 -0
  93. package/next.config.mjs +8 -2
  94. package/package.json +14 -5
  95. package/scripts/agent.ts +1 -0
  96. package/scripts/anomalies.ts +2 -32
  97. package/scripts/build-cli-runtime.mjs +33 -19
  98. package/scripts/chatgpt-app-release-check.mjs +489 -0
  99. package/scripts/chatgpt-app.ts +107 -0
  100. package/scripts/doctor.ts +5 -13
  101. package/scripts/evidence.ts +3 -12
  102. package/scripts/mcp.ts +2 -1
  103. package/scripts/package-inspect.mjs +6 -1
  104. package/scripts/repair.ts +3 -12
  105. package/scripts/report.ts +16 -37
  106. package/scripts/scan.ts +3 -28
  107. package/scripts/smoke-cli/runtime.mjs +6 -1
  108. package/scripts/smoke-packed-install.mjs +5 -0
  109. package/server.json +3 -3
  110. package/src/cli/{commands.js → commands.ts} +47 -31
  111. package/src/cli/{context.js → context.ts} +35 -5
  112. package/src/cli/{help.js → help.ts} +5 -2
  113. package/src/cli/main.ts +12 -0
  114. package/src/cli/{runtime.js → runtime.ts} +37 -13
  115. package/src/cli/{serve.js → serve.ts} +36 -13
  116. package/src/ingestion/adapters/claude-code.ts +2 -1
  117. package/src/lib/agent-discovery.ts +32 -1
  118. package/src/lib/analytics-query-helpers.ts +1 -1
  119. package/src/lib/anomaly-detection.ts +38 -2
  120. package/src/lib/chatgpt-app/prototype.ts +452 -0
  121. package/src/lib/chatgpt-app/server.ts +162 -0
  122. package/src/lib/csv.ts +4 -3
  123. package/src/lib/date-range.ts +2 -1
  124. package/src/lib/doctor.ts +15 -0
  125. package/src/lib/evidence-pack.ts +47 -0
  126. package/src/lib/evidence-trail.ts +16 -0
  127. package/src/lib/mcp-server.ts +67 -24
  128. package/src/lib/overview-data.ts +1 -1
  129. package/src/lib/parser-overrides-cli.ts +1 -0
  130. package/src/lib/project-signals.ts +2 -2
  131. package/src/lib/provider-inference.ts +2 -2
  132. package/src/lib/report-cli.ts +2 -0
  133. package/src/lib/report-service.ts +99 -0
  134. package/src/lib/scan-cli.ts +61 -0
  135. package/src/lib/session-comparison.ts +4 -1
  136. package/src/lib/since-filter.ts +1 -0
  137. package/src/lib/status-cli.ts +4 -2
  138. package/src/lib/structured-query-cli.ts +1 -0
  139. package/src/lib/structured-query.ts +3 -0
  140. package/src/lib/unknown-cost-repair/auto-classify.ts +4 -2
  141. package/src/lib/unknown-cost-repair/keys.ts +9 -0
  142. package/src/lib/unknown-cost-repair/workbench.ts +13 -0
  143. package/src/lib/unknown-cost-repair.ts +3 -1
  144. package/tsconfig.json +3 -0
  145. package/src/cli/serve.d.ts +0 -32
package/CHANGELOG.md CHANGED
@@ -4,6 +4,98 @@ All notable changes to TokenTrace are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.20.0] - 2026-06-12
8
+
9
+ ### Changed
10
+
11
+ - **Overview and shell UI polish.** The dashboard shell now groups navigation by
12
+ task area, and the Overview summary surface makes local evidence,
13
+ cached/non-cache token states, and exact/estimated/unknown cost distinctions
14
+ easier to scan.
15
+ - **ChatGPT app feasibility documented.** Added a recommendation to prototype
16
+ only a private, redacted evidence-pack ChatGPT app workflow until public
17
+ distribution can preserve TokenTrace's local-first privacy boundary.
18
+ - **Private ChatGPT app prototype.** Added `tokentrace chatgpt-app` with a
19
+ local HTTP `/mcp` server, one read-only `get_redacted_evidence_pack` Apps SDK
20
+ tool, a compact MCP Apps widget resource, and a developer-mode runbook. The
21
+ prototype uses redacted evidence packs and does not scan files on startup or
22
+ publish a public app.
23
+ - **ChatGPT app release readiness.** Added `npm run release:chatgpt:check`, a
24
+ hosted `/mcp` validator, a release runbook, and optional tag-workflow wiring
25
+ so ChatGPT app readiness can be checked alongside npm and MCP registry
26
+ releases while keeping OpenAI Dashboard submission and publishing manual.
27
+ - **ChatGPT app submission kit.** Added `docs/chatgpt-app/` with step-by-step
28
+ personal-account Dashboard release instructions, ready-to-paste submission
29
+ copy, privacy and review templates, test prompts, screenshot guidance, and
30
+ reusable SVG app assets for the manual ChatGPT app submission flow.
31
+ - **Development dashboard CSP avoids React issue noise.** The local dev server
32
+ now allows React's development-only diagnostics without relaxing the
33
+ production Content Security Policy.
34
+ - **Product website moved to Baseframe Labs.** The package `homepage`, MCP
35
+ registry `websiteUrl`, README link, and in-app guide link now point to
36
+ `https://www.baseframelabs.com/apps/tokentrace`. The previous
37
+ `abhiyoheswaran.com` URL redirects there.
38
+
39
+ ## [0.19.2] - 2026-06-05
40
+
41
+ ### Changed
42
+
43
+ - **MCP tools now run in-process.** The MCP server calls the same library
44
+ functions as the CLI and HTTP API instead of spawning a `tokentrace` CLI
45
+ subprocess per tool call, making tool calls faster and immune to nested
46
+ process-spawn timeouts. Payload shapes are unchanged. `run_scan` invoked over
47
+ MCP now records its agent action with surface `mcp` instead of `cli`.
48
+ `get_report` still shells out to the CLI because its report composition has
49
+ not yet moved into a shared library function.
50
+ - **Stricter compile-time safety.** TypeScript now runs with
51
+ `noUncheckedIndexedAccess`, `noImplicitOverride`, and
52
+ `noFallthroughCasesInSwitch`; ~190 unguarded array/record accesses across the
53
+ app, library, and tests were given explicit guards. ESLint additionally
54
+ enforces type-aware promise rules (`no-floating-promises`,
55
+ `no-misused-promises`, `await-thenable`). No behavior change intended.
56
+ - **Type-safe chart and CSV plumbing.** `RankBarChart` is generic over its row
57
+ type (chart keys are now checked against the data at compile time) and
58
+ `toCsv` accepts typed rows, removing all eight `as unknown as` casts from the
59
+ export route and analytics pages.
60
+
61
+ ### Fixed
62
+
63
+ - **MCP server reports invalid CLI JSON clearly.** If an underlying
64
+ `tokentrace` CLI call returns unparseable JSON, the MCP server now raises a
65
+ descriptive error naming the command instead of a bare `JSON.parse` failure.
66
+ - **`DELETE /api/saved-views/:id` validates the id.** A blank or
67
+ whitespace-only id now returns `400` instead of attempting a delete.
68
+ - **CSV export keeps caller input out of header syntax.** The
69
+ `content-disposition` filename for `/api/export` now strips characters
70
+ outside `[a-z0-9-]` from the `type` parameter instead of interpolating it
71
+ raw into a quoted string.
72
+ - **Settings scan flow no longer renders an error body as a scan result.**
73
+ A failed scan previously stored the `{error}` response where the last-scan
74
+ panel expected scan counts, which could crash the panel.
75
+
76
+ ### Internal
77
+
78
+ - **CLI wrapper is now strict TypeScript.** `src/cli/*` (≈800 lines,
79
+ previously plain JS invisible to the type checker) is typed and compiled to
80
+ `dist/cli/main.mjs` by the runtime build; `bin/tokentrace.js` loads the
81
+ compiled entry and falls back to `tsx` in dev checkouts.
82
+ - **One report engine.** `/api/reports` and `tokentrace report --type` now
83
+ share `src/lib/report-service.ts`; outputs verified byte-identical against
84
+ the previous implementations.
85
+ - **API routes are integration-tested.** 14 new test files (59 tests) cover
86
+ the previously untested HTTP routes — prices, settings, files, export,
87
+ import-profile-preview, saved-views, saved-reports items, repair-items,
88
+ evidence-pack, analytics, data, operating-metadata, reports — exercising
89
+ real handlers against seeded SQLite databases.
90
+ - **Tests-and-coverage CI.** A new `Tests` workflow runs the suite with v8
91
+ coverage, typecheck, and lint on every PR and push to main.
92
+ - **Frontend decomposition.** The four largest UI files were split into
93
+ focused modules with rendering verified byte-identical: guide page
94
+ (658→148 lines), evidence page (590→200), session explorer (549→149),
95
+ settings panel (341→123, props drilling reduced 49→12 scalars). A shared
96
+ `useJsonRequest` hook replaces the per-component fetch/pending/error
97
+ boilerplate across the client components.
98
+
7
99
  ## [0.19.1] - 2026-06-04
8
100
 
9
101
  ### Fixed
package/README.md CHANGED
@@ -8,7 +8,7 @@ Local-first AI CLI usage analytics. TokenTrace scans local CLI logs and local us
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
- [Website](https://www.abhiyoheswaran.com/apps/tokentrace) · [Source](https://github.com/abhiyoheswaran1/tokentrace)
11
+ [Website](https://www.baseframelabs.com/apps/tokentrace) · [Source](https://github.com/abhiyoheswaran1/tokentrace)
12
12
 
13
13
  ![TokenTrace overview dashboard](docs/assets/overview-0.12.0.png)
14
14
 
@@ -44,6 +44,17 @@ The MCP server does not scan on startup. Its `run_scan` tool requires
44
44
  `confirmLocalScan=true` before reading local usage files or writing the local
45
45
  database.
46
46
 
47
+ Private ChatGPT developer-mode prototype:
48
+
49
+ ```bash
50
+ tokentrace chatgpt-app selftest --json
51
+ tokentrace chatgpt-app --port 8787 --hostname 127.0.0.1
52
+ ```
53
+
54
+ This starts a local HTTP `/mcp` server for one read-only redacted evidence-pack
55
+ tool. ChatGPT requires an HTTPS tunnel for developer-mode connector testing.
56
+ See `docs/CHATGPT_APP_PROTOTYPE.md`.
57
+
47
58
  If the local dashboard is already running, the same manifest is available from:
48
59
 
49
60
  ```bash
@@ -1,7 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
- import { getAnalyticsData } from "@/src/lib/analytics";
3
- import { buildEvidencePack, renderEvidencePackMarkdown } from "@/src/lib/evidence-pack";
4
- import { buildEvidenceTrail, parseEvidenceMetric } from "@/src/lib/evidence-trail";
2
+ import { buildMetricEvidencePack, renderEvidencePackMarkdown } from "@/src/lib/evidence-pack";
3
+ import { parseEvidenceMetric } from "@/src/lib/evidence-trail";
5
4
 
6
5
  export const dynamic = "force-dynamic";
7
6
 
@@ -9,43 +8,7 @@ export async function GET(request: Request) {
9
8
  const url = new URL(request.url);
10
9
  const format = url.searchParams.get("format") === "markdown" ? "markdown" : "json";
11
10
  const metric = parseEvidenceMetric(url.searchParams.get("metric"));
12
- const trail = buildEvidenceTrail({ metric });
13
- const analytics = getAnalyticsData();
14
- const pack = buildEvidencePack({
15
- scope: {
16
- type: "metric",
17
- id: metric,
18
- label: trail.title
19
- },
20
- totals: trail.totals,
21
- confidenceDrivers: [
22
- `${trail.confidence.exact.toLocaleString()} exact interactions`,
23
- `${trail.confidence.estimated.toLocaleString()} estimated interactions`,
24
- `${trail.confidence.unknown.toLocaleString()} unknown interactions`,
25
- `Data confidence ${analytics.dataConfidence.score}/100`
26
- ],
27
- sourceFiles: trail.sourceFiles.map((source) => source.sourceFile),
28
- parserNotes: trail.sessions
29
- .map((session) => `${session.parser ?? "unknown parser"}: ${session.parserStatus ?? "unknown status"}`)
30
- .slice(0, 20),
31
- modelRateState: trail.sessions
32
- .map((session) =>
33
- session.pricingHref ? `${session.model}: model-rate link available` : `${session.model}: no model-rate link`
34
- )
35
- .slice(0, 20),
36
- repairLinks: trail.sessions
37
- .filter((session) => session.unknownCostInteractions > 0)
38
- .map((session) => `/repair?source=${encodeURIComponent(session.sourceFile)}`),
39
- records: trail.sessions.map((session) => ({
40
- id: session.id,
41
- role: "session",
42
- model: session.model,
43
- sourceFile: session.sourceFile,
44
- totalTokens: session.totalTokens,
45
- cost: session.cost,
46
- interactions: session.interactions
47
- }))
48
- });
11
+ const pack = buildMetricEvidencePack({ metric });
49
12
 
50
13
  if (format === "markdown") {
51
14
  return new NextResponse(renderEvidencePackMarkdown(pack), {
@@ -9,19 +9,21 @@ export async function GET(request: Request) {
9
9
  const type = url.searchParams.get("type") ?? "sessions";
10
10
  const analytics = getAnalyticsData();
11
11
  const debug = type.startsWith("scan-") ? getDebugData() : null;
12
- let rows: Array<Record<string, unknown>>;
13
12
 
14
- if (type === "scan-files") rows = (debug?.scanFiles ?? []) as Array<Record<string, unknown>>;
15
- else if (type === "scan-runs") rows = (debug?.scanRuns ?? []) as Array<Record<string, unknown>>;
16
- else if (type === "projects") rows = analytics.projects as unknown as Array<Record<string, unknown>>;
17
- else if (type === "models") rows = analytics.models as unknown as Array<Record<string, unknown>>;
18
- else if (type === "tools") rows = analytics.tools as unknown as Array<Record<string, unknown>>;
19
- else rows = analytics.sessions as unknown as Array<Record<string, unknown>>;
13
+ let csv: string;
14
+ if (type === "scan-files") csv = toCsv(debug?.scanFiles ?? []);
15
+ else if (type === "scan-runs") csv = toCsv(debug?.scanRuns ?? []);
16
+ else if (type === "projects") csv = toCsv(analytics.projects);
17
+ else if (type === "models") csv = toCsv(analytics.models);
18
+ else if (type === "tools") csv = toCsv(analytics.tools);
19
+ else csv = toCsv(analytics.sessions);
20
20
 
21
- return new NextResponse(toCsv(rows), {
21
+ // Keep the caller-supplied type out of header syntax: quoted-string safe.
22
+ const filenameType = type.replace(/[^a-z0-9-]/gi, "") || "sessions";
23
+ return new NextResponse(csv, {
22
24
  headers: {
23
25
  "content-type": "text/csv; charset=utf-8",
24
- "content-disposition": `attachment; filename="tokentrace-${type}.csv"`
26
+ "content-disposition": `attachment; filename="tokentrace-${filenameType}.csv"`
25
27
  }
26
28
  });
27
29
  }
@@ -1,69 +1,32 @@
1
1
  import { NextResponse } from "next/server";
2
- import { getAnalyticsData, getScanTrustData } from "@/src/lib/analytics";
3
- import { buildSourceCatalog, summarizeSourceCoverage } from "@/src/lib/source-catalog";
4
- import { buildSavedReportDefinitions, renderSavedReport, type SavedReportFormat } from "@/src/lib/saved-reports";
2
+ import { generateReport } from "@/src/lib/report-service";
3
+ import type { SavedReportFormat } from "@/src/lib/saved-reports";
5
4
 
6
5
  export const dynamic = "force-dynamic";
7
6
 
8
- function reportRows(definitionId: string) {
9
- const analytics = getAnalyticsData();
10
- const trust = getScanTrustData();
11
- const sourceCoverage = summarizeSourceCoverage(trust.scanFiles);
12
- if (definitionId === "source-coverage") {
13
- return [
14
- { label: "Native files", value: sourceCoverage.nativeFiles.toLocaleString(), detail: "First-class adapters" },
15
- { label: "Profile-assisted files", value: sourceCoverage.profileAssistedFiles.toLocaleString(), detail: "Import profile or generic parser" },
16
- { label: "Fallback files", value: sourceCoverage.fallbackFiles.toLocaleString(), detail: "Low-confidence text or generic fallback" },
17
- { label: "Imported records", value: sourceCoverage.importedRecords.toLocaleString(), detail: "Records imported from scan files" }
18
- ];
19
- }
20
- if (definitionId === "guardrail-status") {
21
- return [
22
- { label: "Cost guardrail", value: analytics.usageGuardrails.cost.status, detail: `${analytics.usageGuardrails.cost.used.toFixed(2)} used` },
23
- { label: "Token guardrail", value: analytics.usageGuardrails.tokens.status, detail: `${analytics.usageGuardrails.tokens.used.toLocaleString()} tokens used` },
24
- { label: "Scoped guardrails", value: analytics.usageGuardrails.scoped.length.toLocaleString(), detail: "Project/model/tool limits" },
25
- { label: "Anomalies", value: analytics.usageGuardrails.anomalies.length.toLocaleString(), detail: "Warning or exceeded scoped guardrails" }
26
- ];
27
- }
28
- return [
29
- { label: "Tokens", value: analytics.summary.totalTokens.toLocaleString(), detail: "Selected local data" },
30
- { label: "Cost", value: `$${analytics.summary.totalCost.toFixed(2)}`, detail: "Provider estimate or source cost" },
31
- { label: "Sessions", value: analytics.summary.sessions.toLocaleString(), detail: "Imported sessions" },
32
- { label: "Unknown cost", value: analytics.summary.unknownCostInteractions.toLocaleString(), detail: "Repair queue candidates" },
33
- { label: "Source catalog", value: buildSourceCatalog().entries.length.toLocaleString(), detail: "Known import paths" }
34
- ];
35
- }
36
-
37
7
  export async function GET(request: Request) {
38
8
  const url = new URL(request.url);
39
- const definitions = buildSavedReportDefinitions();
40
9
  const definitionId = url.searchParams.get("type") ?? "weekly-usage";
41
- const definition = definitions.find((item) => item.id === definitionId);
42
- if (!definition) return NextResponse.json({ error: "Unknown report type." }, { status: 400 });
43
10
  const format = (url.searchParams.get("format") ?? "json") as SavedReportFormat;
44
- if (!definition.formats.includes(format)) {
45
- return NextResponse.json({ error: "Unsupported report format." }, { status: 400 });
11
+ const result = generateReport(definitionId, { format });
12
+ if (!result.ok) {
13
+ const error = result.reason === "unknown-type" ? "Unknown report type." : "Unsupported report format.";
14
+ return NextResponse.json({ error }, { status: 400 });
46
15
  }
47
- const rendered = renderSavedReport({
48
- definitionId,
49
- format,
50
- generatedAt: new Date().toISOString(),
51
- rows: reportRows(definitionId)
52
- });
53
16
  if (format === "json") {
54
- return new NextResponse(rendered, {
17
+ return new NextResponse(result.content, {
55
18
  headers: { "content-type": "application/json; charset=utf-8" }
56
19
  });
57
20
  }
58
21
  if (format === "csv") {
59
- return new NextResponse(rendered, {
22
+ return new NextResponse(result.content, {
60
23
  headers: {
61
24
  "content-type": "text/csv; charset=utf-8",
62
25
  "content-disposition": `attachment; filename="tokentrace-${definitionId}.csv"`
63
26
  }
64
27
  });
65
28
  }
66
- return new NextResponse(rendered, {
29
+ return new NextResponse(result.content, {
67
30
  headers: {
68
31
  "content-type": "text/markdown; charset=utf-8",
69
32
  "content-disposition": `attachment; filename="tokentrace-${definitionId}.md"`
@@ -12,5 +12,9 @@ export async function DELETE(
12
12
  }
13
13
  ) {
14
14
  const { id } = await params;
15
- return NextResponse.json({ deleted: deleteSavedView(decodeURIComponent(id)) });
15
+ const viewId = decodeURIComponent(id).trim();
16
+ if (!viewId) {
17
+ return NextResponse.json({ error: "view id is required" }, { status: 400 });
18
+ }
19
+ return NextResponse.json({ deleted: deleteSavedView(viewId) });
16
20
  }
@@ -0,0 +1,40 @@
1
+ import Link from "next/link";
2
+ import { ArrowRight } from "lucide-react";
3
+ import { Card, CardContent } from "@/components/ui/card";
4
+ import { FieldLabel } from "@/components/ui/typography";
5
+
6
+ export type EvidenceContextAction = {
7
+ label: string;
8
+ detail: string;
9
+ href: string;
10
+ };
11
+
12
+ export function EvidenceContextPanel({ actions }: { actions: EvidenceContextAction[] }) {
13
+ return (
14
+ <Card>
15
+ <CardContent className="flex flex-col gap-3 p-3 lg:flex-row lg:items-center lg:justify-between">
16
+ <div className="max-w-[72ch]">
17
+ <FieldLabel>Evidence path</FieldLabel>
18
+ <p className="mt-1 text-sm leading-6 text-muted-foreground">
19
+ Evidence is a contextual drill-down from Overview, Sessions, Repair, and exported packs. If you opened this page directly, start with processed tokens, then pivot by metric or follow the next action that matches what looks incomplete.
20
+ </p>
21
+ </div>
22
+ <div className="grid min-w-0 gap-2 sm:grid-cols-2 xl:grid-cols-4">
23
+ {actions.map((action) => (
24
+ <Link
25
+ key={action.label}
26
+ href={action.href}
27
+ className="group rounded-md border bg-card p-3 text-left transition-colors hover:bg-muted/40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
28
+ >
29
+ <span className="flex items-center gap-1.5 text-xs font-semibold text-foreground">
30
+ {action.label}
31
+ <ArrowRight className="h-3.5 w-3.5 text-primary transition-transform group-hover:translate-x-0.5" aria-hidden="true" />
32
+ </span>
33
+ <span className="mt-1 block text-xs leading-5 text-muted-foreground">{action.detail}</span>
34
+ </Link>
35
+ ))}
36
+ </div>
37
+ </CardContent>
38
+ </Card>
39
+ );
40
+ }
@@ -0,0 +1,105 @@
1
+ import { dateRangeQueryParams, mergeHrefParams, resolveDateRange } from "@/src/lib/date-range";
2
+ import { buildEvidenceTrail, parseEvidenceMetric } from "@/src/lib/evidence-trail";
3
+
4
+ export type EvidencePageSearchParams = Promise<Record<string, string | string[] | undefined>> | undefined;
5
+
6
+ export type EvidenceDrilldownAction = {
7
+ label: string;
8
+ detail: string;
9
+ href: string;
10
+ };
11
+
12
+ function firstSearchValue(value: string | string[] | undefined) {
13
+ return Array.isArray(value) ? value[0] : value;
14
+ }
15
+
16
+ function openedFromLabel(value: string | undefined) {
17
+ if (value === "overview") return "Overview";
18
+ if (value === "sessions") return "Sessions";
19
+ if (value === "repair") return "Repair";
20
+ if (value === "export") return "Evidence pack";
21
+ if (value === "settings") return "Settings";
22
+ return "Direct link";
23
+ }
24
+
25
+ function safeReturnTo(value: string | string[] | undefined, fallback: string) {
26
+ const candidate = firstSearchValue(value);
27
+ if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//")) return fallback;
28
+ if (candidate.includes("\n") || candidate.includes("\r")) return fallback;
29
+ return candidate;
30
+ }
31
+
32
+ export async function getEvidencePageData(searchParams: EvidencePageSearchParams) {
33
+ const params = (await searchParams) ?? {};
34
+ const range = resolveDateRange(params);
35
+ const metric = parseEvidenceMetric(params?.metric);
36
+ const trail = buildEvidenceTrail({ metric, filters: range.filters });
37
+ const rangeLinkParams = dateRangeQueryParams(range);
38
+ const overviewHref = mergeHrefParams("/", rangeLinkParams);
39
+ const openedFrom = openedFromLabel(firstSearchValue(params.openedFrom));
40
+ const fallbackReturnHref =
41
+ openedFrom === "Sessions"
42
+ ? mergeHrefParams("/sessions", rangeLinkParams)
43
+ : openedFrom === "Repair"
44
+ ? mergeHrefParams("/repair", rangeLinkParams)
45
+ : overviewHref;
46
+ const returnHref = safeReturnTo(params.returnTo, fallbackReturnHref);
47
+ const evidenceContextParams = {
48
+ ...rangeLinkParams,
49
+ openedFrom: firstSearchValue(params.openedFrom) ?? "direct",
50
+ returnTo: returnHref
51
+ };
52
+ const currentEvidenceHref = mergeHrefParams(`/evidence?metric=${trail.metric}`, evidenceContextParams);
53
+ const pricingReturnParams = { returnTo: currentEvidenceHref };
54
+ const periodPreserveParams = {
55
+ metric: trail.metric,
56
+ openedFrom: firstSearchValue(params.openedFrom),
57
+ returnTo: firstSearchValue(params.returnTo)
58
+ };
59
+ const sessionsHref = mergeHrefParams("/sessions", rangeLinkParams);
60
+ const repairHref = mergeHrefParams("/repair", rangeLinkParams);
61
+ const modelRatesHref = mergeHrefParams("/pricing", pricingReturnParams);
62
+ const confidenceTotal = Math.max(1, trail.confidence.exact + trail.confidence.estimated + trail.confidence.unknown);
63
+ const leadingSource = trail.sourceFiles[0];
64
+ const leadingSession = trail.sessions[0];
65
+ const drilldownActions: EvidenceDrilldownAction[] = [
66
+ {
67
+ label: "Top source files",
68
+ detail: "Compare the local files contributing most to this metric.",
69
+ href: "#top-source-files"
70
+ },
71
+ {
72
+ label: "Largest sessions",
73
+ detail: "Open the session evidence table and continue into filtered sessions.",
74
+ href: "#session-evidence"
75
+ },
76
+ {
77
+ label: "Parser confidence",
78
+ detail: "Check whether parser status affects the imported records.",
79
+ href: leadingSource ? mergeHrefParams(leadingSource.parserHref, rangeLinkParams) : "/parser-debug"
80
+ },
81
+ {
82
+ label: "Set model rate",
83
+ detail: "Follow provider model rates or unknown-cost repair when cost needs review.",
84
+ href: leadingSession?.pricingHref
85
+ ? mergeHrefParams(leadingSession.pricingHref, pricingReturnParams)
86
+ : mergeHrefParams("/repair", rangeLinkParams)
87
+ }
88
+ ];
89
+
90
+ return {
91
+ range,
92
+ trail,
93
+ rangeLinkParams,
94
+ openedFrom,
95
+ returnHref,
96
+ evidenceContextParams,
97
+ pricingReturnParams,
98
+ periodPreserveParams,
99
+ sessionsHref,
100
+ repairHref,
101
+ modelRatesHref,
102
+ confidenceTotal,
103
+ drilldownActions
104
+ };
105
+ }
@@ -0,0 +1,74 @@
1
+ import { Badge } from "@/components/ui/badge";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { DataValue, FieldLabel } from "@/components/ui/typography";
4
+ import type { EvidenceTrail } from "@/src/lib/evidence-trail";
5
+ import { formatCurrency, formatExactTokens } from "@/src/lib/format";
6
+
7
+ export function MetricTotalsCard({ totals }: { totals: EvidenceTrail["totals"] }) {
8
+ return (
9
+ <Card>
10
+ <CardHeader>
11
+ <CardTitle>Metric Totals</CardTitle>
12
+ <CardDescription>
13
+ Totals use the same filtered metric definition as the session evidence below.
14
+ </CardDescription>
15
+ </CardHeader>
16
+ <CardContent className="p-0">
17
+ <div className="grid divide-y border-t sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-5">
18
+ <div className="p-3">
19
+ <FieldLabel>Tokens</FieldLabel>
20
+ <DataValue className="mt-1" size="md">{formatExactTokens(totals.tokens)}</DataValue>
21
+ </div>
22
+ <div className="p-3">
23
+ <FieldLabel>Cost</FieldLabel>
24
+ <DataValue className="mt-1" size="md">{formatCurrency(totals.cost)}</DataValue>
25
+ </div>
26
+ <div className="p-3">
27
+ <FieldLabel>Sessions</FieldLabel>
28
+ <DataValue className="mt-1" size="md">{totals.sessions.toLocaleString()}</DataValue>
29
+ </div>
30
+ <div className="p-3">
31
+ <FieldLabel>Interactions</FieldLabel>
32
+ <DataValue className="mt-1" size="md">{totals.interactions.toLocaleString()}</DataValue>
33
+ </div>
34
+ <div className="p-3">
35
+ <FieldLabel>Unknown Cost</FieldLabel>
36
+ <DataValue className="mt-1" size="md">{totals.unknownCostInteractions.toLocaleString()}</DataValue>
37
+ </div>
38
+ </div>
39
+ </CardContent>
40
+ </Card>
41
+ );
42
+ }
43
+
44
+ export function ConfidenceSplitCard({
45
+ confidence,
46
+ confidenceTotal
47
+ }: {
48
+ confidence: EvidenceTrail["confidence"];
49
+ confidenceTotal: number;
50
+ }) {
51
+ return (
52
+ <Card>
53
+ <CardHeader>
54
+ <CardTitle>Confidence Split</CardTitle>
55
+ <CardDescription>Interaction-level token confidence for this metric and period.</CardDescription>
56
+ </CardHeader>
57
+ <CardContent className="space-y-3">
58
+ {[
59
+ { label: "Exact", value: confidence.exact, variant: "success" as const },
60
+ { label: "Estimated", value: confidence.estimated, variant: "secondary" as const },
61
+ { label: "Unknown", value: confidence.unknown, variant: "warning" as const }
62
+ ].map((item) => (
63
+ <div key={item.label} className="grid grid-cols-[5.5rem_minmax(0,1fr)_4rem] items-center gap-3 text-sm">
64
+ <Badge variant={item.variant}>{item.label}</Badge>
65
+ <div className="h-2 overflow-hidden rounded-full bg-muted">
66
+ <div className="h-full rounded-full bg-primary" style={{ width: `${(item.value / confidenceTotal) * 100}%` }} />
67
+ </div>
68
+ <div className="text-right tabular-nums text-muted-foreground">{item.value.toLocaleString()}</div>
69
+ </div>
70
+ ))}
71
+ </CardContent>
72
+ </Card>
73
+ );
74
+ }