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.
- package/CHANGELOG.md +92 -0
- package/README.md +1 -1
- package/TOKENTRACE_AGENT.md +11 -0
- package/app/api/evidence-pack/route.ts +3 -40
- package/app/api/export/route.ts +11 -9
- package/app/api/reports/route.ts +9 -46
- package/app/api/saved-views/[id]/route.ts +5 -1
- package/app/evidence/evidence-context-panel.tsx +40 -0
- package/app/evidence/evidence-page-data.ts +105 -0
- package/app/evidence/evidence-summary-cards.tsx +74 -0
- package/app/evidence/evidence-tables.tsx +185 -0
- package/app/evidence/evidence-workbench.tsx +101 -0
- package/app/evidence/page.tsx +47 -437
- package/app/guide/command-block.tsx +10 -0
- package/app/guide/guide-content.ts +103 -0
- package/app/guide/page.tsx +21 -531
- package/app/guide/section-title.tsx +22 -0
- package/app/guide/sections/agent-handoff-section.tsx +140 -0
- package/app/guide/sections/daily-loop-section.tsx +36 -0
- package/app/guide/sections/guide-nav.tsx +39 -0
- package/app/guide/sections/setup-status-section.tsx +47 -0
- package/app/guide/sections/start-section.tsx +52 -0
- package/app/guide/sections/status-line-section.tsx +79 -0
- package/app/guide/sections/troubleshooting-section.tsx +71 -0
- package/app/models/page.tsx +2 -2
- package/app/page.tsx +4 -4
- package/app/projects/page.tsx +1 -1
- package/app/tools/page.tsx +2 -2
- package/bin/tokentrace.js +19 -7
- package/components/charts/rank-bar-chart-lazy.tsx +3 -1
- package/components/charts/rank-bar-chart.tsx +4 -4
- package/components/charts/trend-chart.tsx +5 -3
- package/components/hooks/use-json-request.ts +46 -0
- package/components/overview/current-mix-panel.tsx +1 -1
- package/components/overview/summary-cards.tsx +64 -34
- package/components/parser-debug/parser-overrides-panel.tsx +32 -30
- package/components/pricing/pricing-workflow.ts +6 -3
- package/components/repair/repair-guidance.tsx +2 -2
- package/components/repair-bulk-actions.tsx +14 -13
- package/components/repair-state-control.tsx +13 -17
- package/components/reports/saved-reports-panel.tsx +23 -25
- package/components/session-explorer/filters-section.tsx +152 -0
- package/components/session-explorer/saved-views-section.tsx +99 -0
- package/components/session-explorer/sessions-table.tsx +219 -0
- package/components/session-explorer/use-saved-views.ts +51 -0
- package/components/session-explorer/use-session-filters.ts +121 -0
- package/components/session-explorer.tsx +64 -464
- package/components/settings/custom-folders-section.tsx +4 -13
- package/components/settings/form-values.ts +18 -0
- package/components/settings/guardrails-section.tsx +22 -37
- package/components/settings/import-profiles-section.tsx +18 -26
- package/components/settings/scan-section.tsx +16 -27
- package/components/settings/types.ts +3 -0
- package/components/settings/use-folders-section.ts +24 -0
- package/components/settings/use-guardrails-section.ts +78 -0
- package/components/settings/use-import-profiles-section.ts +102 -0
- package/components/settings/use-scan-controls-section.ts +70 -0
- package/components/settings/use-scan-schedule-section.ts +33 -0
- package/components/settings/use-settings-status.ts +24 -0
- package/components/settings/use-storage-section.ts +12 -0
- package/components/settings-panel.tsx +38 -256
- package/components/sidebar.tsx +64 -26
- package/dist/cli/main.mjs +746 -0
- package/dist/runtime/agent.mjs +43 -7
- package/dist/runtime/anomalies.mjs +40 -32
- package/dist/runtime/chatgpt-app.mjs +3901 -0
- package/dist/runtime/digest.mjs +9 -4
- package/dist/runtime/doctor.mjs +3505 -3499
- package/dist/runtime/evidence.mjs +10 -12
- package/dist/runtime/insights.mjs +7 -4
- package/dist/runtime/mcp.mjs +9866 -43
- package/dist/runtime/pricing-refresh.mjs +7 -2
- package/dist/runtime/query.mjs +4 -0
- package/dist/runtime/repair.mjs +20 -16
- package/dist/runtime/report.mjs +534 -481
- package/dist/runtime/review.mjs +9 -4
- package/dist/runtime/scan.mjs +67 -42
- package/dist/runtime/status.mjs +4 -2
- package/docs/CHATGPT_APP_PROTOTYPE.md +103 -0
- package/docs/CHATGPT_APP_RELEASE.md +101 -0
- package/docs/chatgpt-app/README.md +24 -0
- package/docs/chatgpt-app/assets/icon.svg +10 -0
- package/docs/chatgpt-app/assets/listing-card.svg +34 -0
- package/docs/chatgpt-app/assets/widget-preview.svg +24 -0
- package/docs/chatgpt-app/dashboard-fields.json +39 -0
- package/docs/chatgpt-app/manual-release-steps.md +114 -0
- package/docs/chatgpt-app/privacy-and-data.md +39 -0
- package/docs/chatgpt-app/review-response-template.md +53 -0
- package/docs/chatgpt-app/screenshot-checklist.md +46 -0
- package/docs/chatgpt-app/submission-copy.md +45 -0
- package/docs/chatgpt-app/test-prompts-and-responses.md +61 -0
- package/llms.txt +13 -0
- package/next.config.mjs +8 -2
- package/package.json +14 -5
- package/scripts/agent.ts +1 -0
- package/scripts/anomalies.ts +2 -32
- package/scripts/build-cli-runtime.mjs +33 -19
- package/scripts/chatgpt-app-release-check.mjs +489 -0
- package/scripts/chatgpt-app.ts +107 -0
- package/scripts/doctor.ts +5 -13
- package/scripts/evidence.ts +3 -12
- package/scripts/mcp.ts +2 -1
- package/scripts/package-inspect.mjs +6 -1
- package/scripts/repair.ts +3 -12
- package/scripts/report.ts +16 -37
- package/scripts/scan.ts +3 -28
- package/scripts/smoke-cli/runtime.mjs +6 -1
- package/scripts/smoke-packed-install.mjs +5 -0
- package/server.json +3 -3
- package/src/cli/{commands.js → commands.ts} +47 -31
- package/src/cli/{context.js → context.ts} +35 -5
- package/src/cli/{help.js → help.ts} +5 -2
- package/src/cli/main.ts +12 -0
- package/src/cli/{runtime.js → runtime.ts} +37 -13
- package/src/cli/{serve.js → serve.ts} +36 -13
- package/src/ingestion/adapters/claude-code.ts +2 -1
- package/src/lib/agent-discovery.ts +32 -1
- package/src/lib/analytics-query-helpers.ts +1 -1
- package/src/lib/anomaly-detection.ts +38 -2
- package/src/lib/chatgpt-app/prototype.ts +452 -0
- package/src/lib/chatgpt-app/server.ts +162 -0
- package/src/lib/csv.ts +4 -3
- package/src/lib/date-range.ts +2 -1
- package/src/lib/doctor.ts +15 -0
- package/src/lib/evidence-pack.ts +47 -0
- package/src/lib/evidence-trail.ts +16 -0
- package/src/lib/mcp-server.ts +67 -24
- package/src/lib/overview-data.ts +1 -1
- package/src/lib/parser-overrides-cli.ts +1 -0
- package/src/lib/project-signals.ts +2 -2
- package/src/lib/provider-inference.ts +2 -2
- package/src/lib/report-cli.ts +2 -0
- package/src/lib/report-service.ts +99 -0
- package/src/lib/scan-cli.ts +61 -0
- package/src/lib/session-comparison.ts +4 -1
- package/src/lib/since-filter.ts +1 -0
- package/src/lib/status-cli.ts +4 -2
- package/src/lib/structured-query-cli.ts +1 -0
- package/src/lib/structured-query.ts +3 -0
- package/src/lib/unknown-cost-repair/auto-classify.ts +4 -2
- package/src/lib/unknown-cost-repair/keys.ts +9 -0
- package/src/lib/unknown-cost-repair/workbench.ts +13 -0
- package/src/lib/unknown-cost-repair.ts +3 -1
- package/tsconfig.json +3 -0
- 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.
|
|
11
|
+
[Website](https://www.baseframelabs.com/apps/tokentrace) · [Source](https://github.com/abhiyoheswaran1/tokentrace)
|
|
12
12
|
|
|
13
13
|

|
|
14
14
|
|
package/TOKENTRACE_AGENT.md
CHANGED
|
@@ -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 {
|
|
3
|
-
import {
|
|
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
|
|
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), {
|
package/app/api/export/route.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
else if (type === "
|
|
17
|
-
else if (type === "
|
|
18
|
-
else if (type === "
|
|
19
|
-
else
|
|
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
|
-
|
|
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-${
|
|
26
|
+
"content-disposition": `attachment; filename="tokentrace-${filenameType}.csv"`
|
|
25
27
|
}
|
|
26
28
|
});
|
|
27
29
|
}
|
package/app/api/reports/route.ts
CHANGED
|
@@ -1,69 +1,32 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
45
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|