tokentrace 0.19.0 → 0.19.2
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 +71 -0
- 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/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/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 +4 -2
- package/dist/cli/main.mjs +736 -0
- package/dist/runtime/agent.mjs +12 -6
- package/dist/runtime/anomalies.mjs +40 -32
- 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 +9836 -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/package.json +5 -2
- package/scripts/agent.ts +1 -0
- package/scripts/anomalies.ts +2 -32
- package/scripts/build-cli-runtime.mjs +32 -19
- package/scripts/doctor.ts +5 -13
- package/scripts/evidence.ts +3 -12
- package/scripts/mcp.ts +2 -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/server.json +2 -2
- package/src/cli/{commands.js → commands.ts} +39 -31
- package/src/cli/{context.js → context.ts} +35 -5
- package/src/cli/{help.js → help.ts} +2 -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/analytics-query-helpers.ts +1 -1
- package/src/lib/anomaly-detection.ts +38 -2
- 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-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,77 @@ All notable changes to TokenTrace are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.19.2] - 2026-06-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **MCP tools now run in-process.** The MCP server calls the same library
|
|
12
|
+
functions as the CLI and HTTP API instead of spawning a `tokentrace` CLI
|
|
13
|
+
subprocess per tool call, making tool calls faster and immune to nested
|
|
14
|
+
process-spawn timeouts. Payload shapes are unchanged. `run_scan` invoked over
|
|
15
|
+
MCP now records its agent action with surface `mcp` instead of `cli`.
|
|
16
|
+
`get_report` still shells out to the CLI because its report composition has
|
|
17
|
+
not yet moved into a shared library function.
|
|
18
|
+
- **Stricter compile-time safety.** TypeScript now runs with
|
|
19
|
+
`noUncheckedIndexedAccess`, `noImplicitOverride`, and
|
|
20
|
+
`noFallthroughCasesInSwitch`; ~190 unguarded array/record accesses across the
|
|
21
|
+
app, library, and tests were given explicit guards. ESLint additionally
|
|
22
|
+
enforces type-aware promise rules (`no-floating-promises`,
|
|
23
|
+
`no-misused-promises`, `await-thenable`). No behavior change intended.
|
|
24
|
+
- **Type-safe chart and CSV plumbing.** `RankBarChart` is generic over its row
|
|
25
|
+
type (chart keys are now checked against the data at compile time) and
|
|
26
|
+
`toCsv` accepts typed rows, removing all eight `as unknown as` casts from the
|
|
27
|
+
export route and analytics pages.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **MCP server reports invalid CLI JSON clearly.** If an underlying
|
|
32
|
+
`tokentrace` CLI call returns unparseable JSON, the MCP server now raises a
|
|
33
|
+
descriptive error naming the command instead of a bare `JSON.parse` failure.
|
|
34
|
+
- **`DELETE /api/saved-views/:id` validates the id.** A blank or
|
|
35
|
+
whitespace-only id now returns `400` instead of attempting a delete.
|
|
36
|
+
- **CSV export keeps caller input out of header syntax.** The
|
|
37
|
+
`content-disposition` filename for `/api/export` now strips characters
|
|
38
|
+
outside `[a-z0-9-]` from the `type` parameter instead of interpolating it
|
|
39
|
+
raw into a quoted string.
|
|
40
|
+
- **Settings scan flow no longer renders an error body as a scan result.**
|
|
41
|
+
A failed scan previously stored the `{error}` response where the last-scan
|
|
42
|
+
panel expected scan counts, which could crash the panel.
|
|
43
|
+
|
|
44
|
+
### Internal
|
|
45
|
+
|
|
46
|
+
- **CLI wrapper is now strict TypeScript.** `src/cli/*` (≈800 lines,
|
|
47
|
+
previously plain JS invisible to the type checker) is typed and compiled to
|
|
48
|
+
`dist/cli/main.mjs` by the runtime build; `bin/tokentrace.js` loads the
|
|
49
|
+
compiled entry and falls back to `tsx` in dev checkouts.
|
|
50
|
+
- **One report engine.** `/api/reports` and `tokentrace report --type` now
|
|
51
|
+
share `src/lib/report-service.ts`; outputs verified byte-identical against
|
|
52
|
+
the previous implementations.
|
|
53
|
+
- **API routes are integration-tested.** 14 new test files (59 tests) cover
|
|
54
|
+
the previously untested HTTP routes — prices, settings, files, export,
|
|
55
|
+
import-profile-preview, saved-views, saved-reports items, repair-items,
|
|
56
|
+
evidence-pack, analytics, data, operating-metadata, reports — exercising
|
|
57
|
+
real handlers against seeded SQLite databases.
|
|
58
|
+
- **Tests-and-coverage CI.** A new `Tests` workflow runs the suite with v8
|
|
59
|
+
coverage, typecheck, and lint on every PR and push to main.
|
|
60
|
+
- **Frontend decomposition.** The four largest UI files were split into
|
|
61
|
+
focused modules with rendering verified byte-identical: guide page
|
|
62
|
+
(658→148 lines), evidence page (590→200), session explorer (549→149),
|
|
63
|
+
settings panel (341→123, props drilling reduced 49→12 scalars). A shared
|
|
64
|
+
`useJsonRequest` hook replaces the per-component fetch/pending/error
|
|
65
|
+
boilerplate across the client components.
|
|
66
|
+
|
|
67
|
+
## [0.19.1] - 2026-06-04
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Leaner npm package.** The published tarball no longer includes a set of
|
|
72
|
+
non-product brand assets (~350 KB of images) that had been parked in the
|
|
73
|
+
`public/` directory and were unintentionally swept into the package by the
|
|
74
|
+
`files` allowlist. They are now excluded from the tarball and ignored by git.
|
|
75
|
+
No functional change — installs are simply smaller and contain only product
|
|
76
|
+
files.
|
|
77
|
+
|
|
7
78
|
## [0.19.0] - 2026-06-04
|
|
8
79
|
|
|
9
80
|
### Security
|
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
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { ArrowRight } from "lucide-react";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
5
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
6
|
+
import { MonoText } from "@/components/ui/typography";
|
|
7
|
+
import { mergeHrefParams } from "@/src/lib/date-range";
|
|
8
|
+
import type { EvidenceTrail, EvidenceTrailSession } from "@/src/lib/evidence-trail";
|
|
9
|
+
import { formatCurrency, formatExactTokens, percent } from "@/src/lib/format";
|
|
10
|
+
|
|
11
|
+
function confidenceVariant(value: string) {
|
|
12
|
+
if (value === "exact") return "success";
|
|
13
|
+
if (value === "unknown") return "warning";
|
|
14
|
+
return "secondary";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parserStatusVariant(value: string | null) {
|
|
18
|
+
if (value === "imported") return "success";
|
|
19
|
+
if (value === "imported_with_errors") return "warning";
|
|
20
|
+
if (!value) return "secondary";
|
|
21
|
+
return "outline";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TopSourceFilesCard({
|
|
25
|
+
sourceFiles,
|
|
26
|
+
rangeLinkParams
|
|
27
|
+
}: {
|
|
28
|
+
sourceFiles: EvidenceTrail["sourceFiles"];
|
|
29
|
+
rangeLinkParams: Record<string, string | undefined>;
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<Card id="top-source-files">
|
|
33
|
+
<CardHeader>
|
|
34
|
+
<CardTitle>Top Source Files</CardTitle>
|
|
35
|
+
<CardDescription>Largest contributing local files for the same metric definition.</CardDescription>
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardContent className="table-scroll">
|
|
38
|
+
<Table>
|
|
39
|
+
<TableHeader>
|
|
40
|
+
<TableRow>
|
|
41
|
+
<TableHead>Source</TableHead>
|
|
42
|
+
<TableHead>Tokens</TableHead>
|
|
43
|
+
<TableHead>Interactions</TableHead>
|
|
44
|
+
<TableHead>Unknown cost</TableHead>
|
|
45
|
+
<TableHead>Actions</TableHead>
|
|
46
|
+
</TableRow>
|
|
47
|
+
</TableHeader>
|
|
48
|
+
<TableBody>
|
|
49
|
+
{sourceFiles.length ? (
|
|
50
|
+
sourceFiles.map((source) => (
|
|
51
|
+
<TableRow key={source.sourceFile}>
|
|
52
|
+
<TableCell className="max-w-96">
|
|
53
|
+
<Link href={mergeHrefParams(source.sourceHref, rangeLinkParams)} title={source.sourceFile}>
|
|
54
|
+
<MonoText className="block truncate text-muted-foreground underline-offset-4 hover:underline">
|
|
55
|
+
{source.sourceFile}
|
|
56
|
+
</MonoText>
|
|
57
|
+
</Link>
|
|
58
|
+
</TableCell>
|
|
59
|
+
<TableCell>{formatExactTokens(source.tokens)}</TableCell>
|
|
60
|
+
<TableCell>{source.interactions.toLocaleString()}</TableCell>
|
|
61
|
+
<TableCell>{source.unknownCostInteractions.toLocaleString()}</TableCell>
|
|
62
|
+
<TableCell>
|
|
63
|
+
<div className="flex flex-wrap gap-2">
|
|
64
|
+
<Link href={mergeHrefParams(source.sourceHref, rangeLinkParams)} className="font-medium text-primary underline-offset-4 hover:underline">
|
|
65
|
+
Open Sessions
|
|
66
|
+
</Link>
|
|
67
|
+
<Link href={mergeHrefParams(source.parserHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
|
|
68
|
+
Review parser
|
|
69
|
+
</Link>
|
|
70
|
+
</div>
|
|
71
|
+
</TableCell>
|
|
72
|
+
</TableRow>
|
|
73
|
+
))
|
|
74
|
+
) : (
|
|
75
|
+
<TableRow>
|
|
76
|
+
<TableCell colSpan={5} className="h-20 text-center text-muted-foreground">
|
|
77
|
+
No source-file evidence is available for this metric yet.
|
|
78
|
+
</TableCell>
|
|
79
|
+
</TableRow>
|
|
80
|
+
)}
|
|
81
|
+
</TableBody>
|
|
82
|
+
</Table>
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function SessionEvidenceCard({
|
|
89
|
+
sessions,
|
|
90
|
+
rangeLinkParams,
|
|
91
|
+
pricingReturnParams
|
|
92
|
+
}: {
|
|
93
|
+
sessions: EvidenceTrailSession[];
|
|
94
|
+
rangeLinkParams: Record<string, string | undefined>;
|
|
95
|
+
pricingReturnParams: Record<string, string | undefined>;
|
|
96
|
+
}) {
|
|
97
|
+
return (
|
|
98
|
+
<Card id="session-evidence">
|
|
99
|
+
<CardHeader>
|
|
100
|
+
<CardTitle>Session, Source, Parser, And Model Rate Evidence</CardTitle>
|
|
101
|
+
<CardDescription>
|
|
102
|
+
The table is capped at the top 100 contributing sessions; totals above include the full metric set.
|
|
103
|
+
</CardDescription>
|
|
104
|
+
</CardHeader>
|
|
105
|
+
<CardContent className="table-scroll p-0">
|
|
106
|
+
<Table>
|
|
107
|
+
<TableHeader>
|
|
108
|
+
<TableRow>
|
|
109
|
+
<TableHead>Session</TableHead>
|
|
110
|
+
<TableHead>Metric Total</TableHead>
|
|
111
|
+
<TableHead>Cost</TableHead>
|
|
112
|
+
<TableHead>Source</TableHead>
|
|
113
|
+
<TableHead>Parser</TableHead>
|
|
114
|
+
<TableHead>Model rates</TableHead>
|
|
115
|
+
<TableHead>Confidence</TableHead>
|
|
116
|
+
</TableRow>
|
|
117
|
+
</TableHeader>
|
|
118
|
+
<TableBody>
|
|
119
|
+
{sessions.length ? (
|
|
120
|
+
sessions.map((session) => (
|
|
121
|
+
<TableRow key={session.id}>
|
|
122
|
+
<TableCell className="min-w-64">
|
|
123
|
+
<Link href={mergeHrefParams(session.sessionHref, rangeLinkParams)} className="font-medium text-primary underline-offset-4 hover:underline">
|
|
124
|
+
{session.title}
|
|
125
|
+
</Link>
|
|
126
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
127
|
+
{session.tool} / {session.provider} / {session.project}
|
|
128
|
+
</div>
|
|
129
|
+
</TableCell>
|
|
130
|
+
<TableCell>{formatExactTokens(session.totalTokens)}</TableCell>
|
|
131
|
+
<TableCell>{formatCurrency(session.cost)}</TableCell>
|
|
132
|
+
<TableCell className="max-w-80">
|
|
133
|
+
<Link href={mergeHrefParams(session.sourceHref, rangeLinkParams)} title={session.sourceFile}>
|
|
134
|
+
<MonoText className="block truncate text-muted-foreground underline-offset-4 hover:underline">
|
|
135
|
+
{session.sourceFile}
|
|
136
|
+
</MonoText>
|
|
137
|
+
</Link>
|
|
138
|
+
</TableCell>
|
|
139
|
+
<TableCell className="min-w-44">
|
|
140
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
141
|
+
<Badge variant={parserStatusVariant(session.parserStatus)}>
|
|
142
|
+
{session.parserStatus ?? "not scanned"}
|
|
143
|
+
</Badge>
|
|
144
|
+
<Link href={mergeHrefParams(session.parserHref, rangeLinkParams)} className="text-xs font-medium text-primary underline-offset-4 hover:underline">
|
|
145
|
+
Parser <ArrowRight className="inline h-3.5 w-3.5" />
|
|
146
|
+
</Link>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
149
|
+
{session.parser ?? "No parser"} / {session.parserConfidence == null ? "confidence unknown" : percent(session.parserConfidence)}
|
|
150
|
+
</div>
|
|
151
|
+
</TableCell>
|
|
152
|
+
<TableCell className="min-w-44">
|
|
153
|
+
{session.pricingHref ? (
|
|
154
|
+
<Link href={mergeHrefParams(session.pricingHref, pricingReturnParams)} className="font-medium text-primary underline-offset-4 hover:underline">
|
|
155
|
+
{session.model}
|
|
156
|
+
</Link>
|
|
157
|
+
) : (
|
|
158
|
+
<span>{session.model}</span>
|
|
159
|
+
)}
|
|
160
|
+
</TableCell>
|
|
161
|
+
<TableCell>
|
|
162
|
+
<div className="flex flex-col items-start gap-1">
|
|
163
|
+
<Badge variant={confidenceVariant(session.tokenConfidence)}>
|
|
164
|
+
{session.tokenConfidence}
|
|
165
|
+
</Badge>
|
|
166
|
+
<span className="text-xs text-muted-foreground">
|
|
167
|
+
{session.interactions.toLocaleString()} interactions
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
</TableCell>
|
|
171
|
+
</TableRow>
|
|
172
|
+
))
|
|
173
|
+
) : (
|
|
174
|
+
<TableRow>
|
|
175
|
+
<TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
|
|
176
|
+
No evidence is available for this metric yet.
|
|
177
|
+
</TableCell>
|
|
178
|
+
</TableRow>
|
|
179
|
+
)}
|
|
180
|
+
</TableBody>
|
|
181
|
+
</Table>
|
|
182
|
+
</CardContent>
|
|
183
|
+
</Card>
|
|
184
|
+
);
|
|
185
|
+
}
|