tokentrace 0.15.2 → 0.17.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 +101 -0
- package/app/api/parser-debug/preview/route.ts +92 -0
- package/app/api/parser-overrides/route.ts +65 -0
- package/app/api/saved-reports/[id]/route.ts +20 -0
- package/app/api/saved-reports/route.ts +34 -0
- package/app/models/page.tsx +1 -1
- package/app/page.tsx +72 -47
- package/app/parser-debug/page.tsx +10 -0
- package/app/projects/page.tsx +1 -1
- package/app/reports/page.tsx +19 -0
- package/app/tools/page.tsx +1 -1
- package/components/charts/rank-bar-chart-lazy.tsx +9 -0
- package/components/charts/skeleton.tsx +16 -0
- package/components/charts/trend-section-lazy.tsx +9 -0
- package/components/overview/current-mix-panel.tsx +1 -1
- package/components/overview/section-skeletons.tsx +35 -0
- package/components/parser-debug/parser-overrides-panel.tsx +205 -0
- package/components/reports/saved-reports-panel.tsx +206 -0
- package/dist/runtime/agent.mjs +3903 -6
- package/dist/runtime/db-migrate.mjs +40 -0
- package/dist/runtime/db-seed.mjs +40 -0
- package/dist/runtime/digest.mjs +60 -2
- package/dist/runtime/doctor.mjs +116 -19
- package/dist/runtime/evidence.mjs +40 -0
- package/dist/runtime/insights.mjs +60 -2
- package/dist/runtime/mcp.mjs +30 -0
- package/dist/runtime/pricing-refresh.mjs +62 -4
- package/dist/runtime/repair.mjs +348 -91
- package/dist/runtime/report.mjs +517 -13
- package/dist/runtime/reset.mjs +40 -0
- package/dist/runtime/review.mjs +60 -2
- package/dist/runtime/scan.mjs +408 -128
- package/dist/runtime/status.mjs +40 -0
- package/next.config.mjs +28 -5
- package/package.json +1 -1
- package/scripts/agent.ts +70 -4
- package/scripts/doctor.ts +38 -11
- package/scripts/repair.ts +26 -8
- package/scripts/report.ts +49 -0
- package/scripts/scan.ts +13 -0
- package/server.json +2 -2
- package/src/db/client.ts +5 -0
- package/src/db/migrate-core.ts +35 -0
- package/src/db/prepared.ts +17 -0
- package/src/ingestion/discovery.ts +2 -5
- package/src/ingestion/persist.ts +12 -3
- package/src/ingestion/scan-adapters.ts +59 -2
- package/src/ingestion/scan-files.ts +35 -24
- package/src/ingestion/scan.ts +22 -8
- package/src/lib/agent-actions.ts +138 -0
- package/src/lib/analytics-query-helpers.ts +2 -2
- package/src/lib/doctor-cli.ts +45 -0
- package/src/lib/handoff.ts +129 -0
- package/src/lib/mcp/agent-guide.ts +5 -0
- package/src/lib/mcp/tools.ts +7 -0
- package/src/lib/mcp-server.ts +19 -0
- package/src/lib/overview-data.ts +140 -46
- package/src/lib/parser-overrides-cli.ts +143 -0
- package/src/lib/parser-overrides.ts +110 -0
- package/src/lib/saved-report-runner.ts +171 -0
- package/src/lib/saved-reports-store.ts +189 -0
- package/src/lib/scheduled-scan.ts +4 -4
- package/src/lib/source-catalog.ts +5 -3
- package/src/lib/unknown-cost-repair/suggestions.ts +2 -2
- package/src/lib/unknown-cost-repair/workbench.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,107 @@ All notable changes to TokenTrace are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.17.0] - 2026-05-23
|
|
8
|
+
|
|
9
|
+
### Performance
|
|
10
|
+
|
|
11
|
+
- **Runtime SQLite pragmas tuned for analytics.** `src/db/client.ts` now
|
|
12
|
+
re-applies `journal_mode=WAL` on the live connection and sets
|
|
13
|
+
`synchronous=NORMAL`, `temp_store=MEMORY`, `cache_size=64MB`, and
|
|
14
|
+
`mmap_size=256MB`. Previously only `busy_timeout` and `foreign_keys`
|
|
15
|
+
were set on the runtime connection.
|
|
16
|
+
- **Prepared-statement cache.** `src/db/prepared.ts` adds a tiny
|
|
17
|
+
`prepareCached(sql)` helper keyed by SQL string. The hot analytics,
|
|
18
|
+
unknown-cost-repair, scheduled-scan, and ingestion helpers now skip
|
|
19
|
+
the parse-and-plan cost on repeat queries.
|
|
20
|
+
- **Overview page parallelization.** The independent sub-queries in
|
|
21
|
+
`getOverviewData` (analytics, accounting invariants, scan diff,
|
|
22
|
+
default search roots, repair workbench) now run through `Promise.all`,
|
|
23
|
+
so the async filesystem walk overlaps with the serialized SQLite
|
|
24
|
+
reads.
|
|
25
|
+
- **Render-scoped overview memo.** `getOverviewPageData` is wrapped in
|
|
26
|
+
`React.cache` so any future composition that calls it twice within a
|
|
27
|
+
single server render tree pays the cost once.
|
|
28
|
+
- **Lazy-loaded Recharts.** `TrendSection` and `RankBarChart` are now
|
|
29
|
+
loaded via `next/dynamic({ ssr: false })` on the overview, projects,
|
|
30
|
+
tools, and models routes, splitting the ~80KB Recharts bundle out of
|
|
31
|
+
the initial JS payload. `ChartSkeleton` keeps the chart slot from
|
|
32
|
+
collapsing during client hydration.
|
|
33
|
+
- **Streaming overview with two `<Suspense>` boundaries.** The overview
|
|
34
|
+
page now splits into two cache-wrapped fetchers — `getOverviewPrimaryData`
|
|
35
|
+
(analytics-driven: pulse, summary, trend, trust, guardrails,
|
|
36
|
+
recommendations, mix) and `getOverviewRepairData` (workbench-driven:
|
|
37
|
+
review status, repair items). Each renders inside its own `<Suspense>`
|
|
38
|
+
so the page shell and period filter paint immediately, the primary
|
|
39
|
+
analytics block streams in next, and the repair lane streams in
|
|
40
|
+
independently when the workbench query resolves.
|
|
41
|
+
- **`tokentrace doctor --timings`.** New flag force-enables analytics
|
|
42
|
+
timing capture and emits the analytics timing report (slow queries,
|
|
43
|
+
threshold, sample list). Combine with `--json` for machine-readable
|
|
44
|
+
output. Useful for before/after measurement of performance changes
|
|
45
|
+
since `TOKENTRACE_ANALYTICS_TIMING` is off by default in production.
|
|
46
|
+
- **Scan ingestion throughput.** Adds a (path, size, mtime)-keyed
|
|
47
|
+
file-hash cache so rescans of unchanged files skip the `fs.readFile`
|
|
48
|
+
+ SHA-256 step entirely. The hot scan-side `INSERT INTO scan_files`,
|
|
49
|
+
`INSERT INTO scan_runs`, and `hasImportedFile` lookups also route
|
|
50
|
+
through `prepareCached`.
|
|
51
|
+
- **Next bundle optimizations.** Enables `optimizePackageImports` for
|
|
52
|
+
`lucide-react` (~37 import sites) and `recharts` so Next 16 transforms
|
|
53
|
+
named imports into per-symbol imports and prunes unused-symbol weight
|
|
54
|
+
from the client bundle. Adds an opt-in `@next/bundle-analyzer`
|
|
55
|
+
integration gated on `ANALYZE=true` for follow-up audits.
|
|
56
|
+
- **Hot-path bug-hunt fixes.** `source-catalog.summarizeSourceCoverage`
|
|
57
|
+
was O(rows × entries); now uses a pre-built Map for O(1) parser tier
|
|
58
|
+
lookups. `ingestion/discovery.getDefaultSearchRoots` parallelizes its
|
|
59
|
+
`fs.access` checks via `Promise.all` instead of awaiting them
|
|
60
|
+
sequentially. `ingestion/persist.findProjectRoot` memoizes the
|
|
61
|
+
resolved project root by start directory, eliminating duplicate
|
|
62
|
+
filesystem walks when many imported sessions share the same source
|
|
63
|
+
directory.
|
|
64
|
+
|
|
65
|
+
## [0.16.0] - 2026-05-23
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **Parser overrides.** Force a specific parser for a file or exclude it from
|
|
70
|
+
future scans without editing source code. Override is honored by the next
|
|
71
|
+
scan via `selectAdapter`. New surfaces:
|
|
72
|
+
- Dashboard: `Parser overrides` panel on `/parser-debug` (list, add, clear).
|
|
73
|
+
- REST: `GET / POST / DELETE /api/parser-overrides`.
|
|
74
|
+
- REST: `POST /api/parser-debug/preview` returns predicted parse output for
|
|
75
|
+
an alternate parser without writing to the local database.
|
|
76
|
+
- CLI: `tokentrace repair set-parser <path> --parser <id> [--note "..."]`,
|
|
77
|
+
`--exclude` variant, and `clear-parser <path>` (all support `--json`).
|
|
78
|
+
- File paths are normalized to absolute form so scan-resolved paths match
|
|
79
|
+
user-supplied overrides, regardless of relative vs absolute input.
|
|
80
|
+
- **Saved reports.** Persisted local report templates that can be replayed
|
|
81
|
+
from the dashboard or the CLI. New surfaces:
|
|
82
|
+
- Dashboard: `/reports` page with create / list / delete.
|
|
83
|
+
- REST: `GET / POST /api/saved-reports`, `GET / DELETE /api/saved-reports/[id]`.
|
|
84
|
+
- CLI: `tokentrace report --saved "<name>" --format json|markdown|html`
|
|
85
|
+
and `tokentrace report --list-saved [--json]`.
|
|
86
|
+
- Standalone HTML report format escapes every user-supplied filter value
|
|
87
|
+
so opening or archiving the file is safe.
|
|
88
|
+
- Param keys are validated against an allow-list before persistence.
|
|
89
|
+
- **Agent handoff.** Structured envelope plus action log for multi-agent
|
|
90
|
+
workflows.
|
|
91
|
+
- CLI: `tokentrace agent --handoff [--json]` returns the
|
|
92
|
+
`tokentrace.handoff.v1` envelope (scan state, repair queue, confidence,
|
|
93
|
+
recent actions, suggested next actions). Pure read.
|
|
94
|
+
- CLI: `tokentrace agent --actions [--limit N] [--json]` reads the local
|
|
95
|
+
agent action log.
|
|
96
|
+
- MCP: new `get_handoff` tool returns the same envelope; added to the
|
|
97
|
+
recommended workflow in `get_agent_guide`.
|
|
98
|
+
- Action log is bounded to the last 500 entries (configurable via
|
|
99
|
+
`TOKENTRACE_AGENT_ACTION_LOG_MAX`), token-shaped strings are redacted
|
|
100
|
+
before write, and the writer is best-effort so logging never breaks the
|
|
101
|
+
CLI command it observes. `tokentrace scan` now writes a row on completion.
|
|
102
|
+
|
|
103
|
+
### Internal
|
|
104
|
+
|
|
105
|
+
- Three new local SQLite tables: `file_parser_overrides`, `saved_reports`,
|
|
106
|
+
`agent_actions`. All additive migrations.
|
|
107
|
+
|
|
7
108
|
## [0.15.2] - 2026-05-23
|
|
8
109
|
|
|
9
110
|
### Fixed
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { adapters } from "@/src/ingestion/adapters";
|
|
4
|
+
import type { FileCandidate, NormalizedInteraction } from "@/src/ingestion/types";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
type PreviewBody = {
|
|
9
|
+
path?: unknown;
|
|
10
|
+
parserId?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function POST(request: Request) {
|
|
14
|
+
let body: PreviewBody;
|
|
15
|
+
try {
|
|
16
|
+
body = (await request.json()) as PreviewBody;
|
|
17
|
+
} catch {
|
|
18
|
+
return NextResponse.json({ error: "Request body must be JSON." }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const filePath = typeof body.path === "string" ? body.path.trim() : "";
|
|
22
|
+
const parserId = typeof body.parserId === "string" ? body.parserId.trim() : "";
|
|
23
|
+
|
|
24
|
+
if (!filePath) {
|
|
25
|
+
return NextResponse.json({ error: "path is required" }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
if (!parserId) {
|
|
28
|
+
return NextResponse.json({ error: "parserId is required" }, { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const adapter = adapters.find((candidate) => candidate.id === parserId);
|
|
32
|
+
if (!adapter) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ error: `parser ${parserId} is not registered` },
|
|
35
|
+
{ status: 404 }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let stat;
|
|
40
|
+
try {
|
|
41
|
+
stat = await fs.stat(filePath);
|
|
42
|
+
} catch {
|
|
43
|
+
return NextResponse.json({ error: `file not found: ${filePath}` }, { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const candidate: FileCandidate = {
|
|
47
|
+
path: filePath,
|
|
48
|
+
sizeBytes: stat.size,
|
|
49
|
+
modifiedTime: stat.mtime
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let parseResult;
|
|
53
|
+
try {
|
|
54
|
+
parseResult = await adapter.parse(candidate, { storeRawMessageContent: false });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return NextResponse.json(
|
|
57
|
+
{
|
|
58
|
+
error: `parser ${parserId} threw while parsing: ${
|
|
59
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
60
|
+
}`
|
|
61
|
+
},
|
|
62
|
+
{ status: 422 }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const allInteractions: NormalizedInteraction[] = parseResult.sessions.flatMap(
|
|
67
|
+
(session) => session.interactions
|
|
68
|
+
);
|
|
69
|
+
const predictedTotalTokens = allInteractions.reduce(
|
|
70
|
+
(sum, interaction) => sum + (interaction.totalTokens ?? 0),
|
|
71
|
+
0
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return NextResponse.json({
|
|
75
|
+
parserId,
|
|
76
|
+
path: filePath,
|
|
77
|
+
sessions: parseResult.sessions.map((session) => ({
|
|
78
|
+
externalId: session.externalId,
|
|
79
|
+
provider: session.provider,
|
|
80
|
+
tool: session.tool,
|
|
81
|
+
interactionCount: session.interactions.length,
|
|
82
|
+
totalTokens: session.interactions.reduce(
|
|
83
|
+
(sum, interaction) => sum + (interaction.totalTokens ?? 0),
|
|
84
|
+
0
|
|
85
|
+
)
|
|
86
|
+
})),
|
|
87
|
+
predictedInteractions: allInteractions.length,
|
|
88
|
+
predictedTotalTokens,
|
|
89
|
+
warnings: parseResult.warnings,
|
|
90
|
+
errors: parseResult.errors
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
clearParserOverride,
|
|
4
|
+
listParserOverrides,
|
|
5
|
+
setParserOverride
|
|
6
|
+
} from "@/src/lib/parser-overrides";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
type WriteBody = {
|
|
11
|
+
path?: unknown;
|
|
12
|
+
parserId?: unknown;
|
|
13
|
+
excluded?: unknown;
|
|
14
|
+
note?: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function GET() {
|
|
18
|
+
return NextResponse.json({ overrides: listParserOverrides() });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function POST(request: Request) {
|
|
22
|
+
let body: WriteBody;
|
|
23
|
+
try {
|
|
24
|
+
body = (await request.json()) as WriteBody;
|
|
25
|
+
} catch {
|
|
26
|
+
return NextResponse.json({ error: "Request body must be JSON." }, { status: 400 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const path = typeof body.path === "string" ? body.path.trim() : "";
|
|
30
|
+
if (!path) return NextResponse.json({ error: "path is required" }, { status: 400 });
|
|
31
|
+
|
|
32
|
+
const excluded = body.excluded === true;
|
|
33
|
+
const parserId =
|
|
34
|
+
!excluded && typeof body.parserId === "string" && body.parserId.trim()
|
|
35
|
+
? body.parserId.trim()
|
|
36
|
+
: undefined;
|
|
37
|
+
const note = typeof body.note === "string" && body.note.trim() ? body.note.trim() : undefined;
|
|
38
|
+
|
|
39
|
+
if (!excluded && !parserId) {
|
|
40
|
+
return NextResponse.json({ error: "parserId or excluded must be provided" }, { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const override = setParserOverride({ path, parserId, excluded, note });
|
|
45
|
+
return NextResponse.json({ override });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: error instanceof Error ? error.message : "Failed to set parser override" },
|
|
49
|
+
{ status: 400 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function DELETE(request: Request) {
|
|
55
|
+
let body: { path?: unknown };
|
|
56
|
+
try {
|
|
57
|
+
body = (await request.json()) as { path?: unknown };
|
|
58
|
+
} catch {
|
|
59
|
+
return NextResponse.json({ error: "Request body must be JSON." }, { status: 400 });
|
|
60
|
+
}
|
|
61
|
+
const path = typeof body.path === "string" ? body.path.trim() : "";
|
|
62
|
+
if (!path) return NextResponse.json({ error: "path is required" }, { status: 400 });
|
|
63
|
+
const removed = clearParserOverride(path);
|
|
64
|
+
return NextResponse.json({ removed, path });
|
|
65
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { deleteSavedReport, findSavedReportById } from "@/src/lib/saved-reports-store";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
7
|
+
|
|
8
|
+
export async function GET(_request: Request, context: RouteContext) {
|
|
9
|
+
const { id } = await context.params;
|
|
10
|
+
const report = findSavedReportById(id);
|
|
11
|
+
if (!report) return NextResponse.json({ error: "saved report not found" }, { status: 404 });
|
|
12
|
+
return NextResponse.json({ report });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function DELETE(_request: Request, context: RouteContext) {
|
|
16
|
+
const { id } = await context.params;
|
|
17
|
+
const removed = deleteSavedReport(id);
|
|
18
|
+
if (!removed) return NextResponse.json({ error: "saved report not found" }, { status: 404 });
|
|
19
|
+
return NextResponse.json({ removed: true, id });
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createSavedReport, listSavedReports } from "@/src/lib/saved-reports-store";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return NextResponse.json({ reports: listSavedReports() });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function POST(request: Request) {
|
|
11
|
+
let body: Record<string, unknown>;
|
|
12
|
+
try {
|
|
13
|
+
body = (await request.json()) as Record<string, unknown>;
|
|
14
|
+
} catch {
|
|
15
|
+
return NextResponse.json({ error: "Request body must be JSON." }, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const name = typeof body.name === "string" ? body.name : "";
|
|
19
|
+
const viewType = typeof body.viewType === "string" ? body.viewType : "";
|
|
20
|
+
const format = typeof body.format === "string" ? body.format : undefined;
|
|
21
|
+
const params =
|
|
22
|
+
body.params && typeof body.params === "object" && !Array.isArray(body.params)
|
|
23
|
+
? (body.params as Record<string, string | number | boolean>)
|
|
24
|
+
: undefined;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const report = createSavedReport({ name, viewType, format, params });
|
|
28
|
+
return NextResponse.json({ report }, { status: 201 });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : "Failed to create saved report";
|
|
31
|
+
const status = /already exists/i.test(message) ? 409 : 400;
|
|
32
|
+
return NextResponse.json({ error: message }, { status });
|
|
33
|
+
}
|
|
34
|
+
}
|
package/app/models/page.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { EmptyState } from "@/components/empty-state";
|
|
|
3
3
|
import { ScanNowButton } from "@/components/scan-now-button";
|
|
4
4
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
5
5
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
6
|
-
import { RankBarChart } from "@/components/charts/rank-bar-chart";
|
|
6
|
+
import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
|
|
7
7
|
import { PageHeader } from "@/components/ui/typography";
|
|
8
8
|
import { getAnalyticsData } from "@/src/lib/analytics";
|
|
9
9
|
import { formatCurrency, formatTokens } from "@/src/lib/format";
|
package/app/page.tsx
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
1
2
|
import Link from "next/link";
|
|
2
3
|
import { ArrowRight } from "lucide-react";
|
|
3
|
-
import { TrendSection } from "@/components/charts/trend-section";
|
|
4
|
+
import { TrendSection } from "@/components/charts/trend-section-lazy";
|
|
4
5
|
import { OverviewCurrentMixPanel } from "@/components/overview/current-mix-panel";
|
|
5
6
|
import { DataConfidenceStrip } from "@/components/overview/data-confidence-strip";
|
|
6
7
|
import { FirstRunPanel } from "@/components/overview/first-run-panel";
|
|
7
8
|
import { UsageGuardrailsPanel } from "@/components/overview/guardrails-panel";
|
|
8
9
|
import { OverviewRecommendationsCard } from "@/components/overview/recommendations-card";
|
|
9
10
|
import { OverviewReviewStatusStrip } from "@/components/overview/review-status-strip";
|
|
11
|
+
import {
|
|
12
|
+
OverviewPrimarySkeleton,
|
|
13
|
+
OverviewRepairSkeleton
|
|
14
|
+
} from "@/components/overview/section-skeletons";
|
|
10
15
|
import { CostSessionsCard, TokenAccountingCard } from "@/components/overview/summary-cards";
|
|
11
16
|
import { TopRepairItemsStrip } from "@/components/overview/top-repair-items-strip";
|
|
12
17
|
import { OverviewTrustFooter } from "@/components/overview/trust-footer";
|
|
@@ -14,9 +19,9 @@ import { UsagePulsePanel } from "@/components/overview/usage-pulse-panel";
|
|
|
14
19
|
import { PeriodFilter } from "@/components/period-filter";
|
|
15
20
|
import { Button } from "@/components/ui/button";
|
|
16
21
|
import { PageHeader } from "@/components/ui/typography";
|
|
17
|
-
import { resolveDateRange } from "@/src/lib/date-range";
|
|
22
|
+
import { mergeHrefParams, type ResolvedDateRange, resolveDateRange } from "@/src/lib/date-range";
|
|
18
23
|
import { runDueScheduledScan } from "@/src/lib/scheduled-scan";
|
|
19
|
-
import {
|
|
24
|
+
import { getOverviewPrimaryData, getOverviewRepairData } from "@/src/lib/overview-data";
|
|
20
25
|
|
|
21
26
|
export const dynamic = "force-dynamic";
|
|
22
27
|
|
|
@@ -24,51 +29,24 @@ type OverviewPageProps = {
|
|
|
24
29
|
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
|
25
30
|
};
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
void runDueScheduledScan().catch(() => undefined);
|
|
29
|
-
const params = (await searchParams) ?? {};
|
|
30
|
-
const range = resolveDateRange(params);
|
|
31
|
-
const overview = await getOverviewPageData(range);
|
|
32
|
+
async function OverviewPrimarySection({ range }: { range: ResolvedDateRange }) {
|
|
32
33
|
const {
|
|
33
34
|
data,
|
|
34
35
|
trust,
|
|
35
|
-
accountingReport,
|
|
36
|
-
postSessionReview,
|
|
37
|
-
rangeLinkParams,
|
|
38
36
|
evidenceLinks,
|
|
39
|
-
doctorReport,
|
|
40
|
-
repairWorkbench,
|
|
41
|
-
repairFocusHref,
|
|
42
|
-
unknownCostEvidenceHref,
|
|
43
37
|
firstRunStatus,
|
|
44
38
|
summary,
|
|
45
|
-
trendDefaultWindow
|
|
46
|
-
|
|
39
|
+
trendDefaultWindow,
|
|
40
|
+
unknownCostEvidenceHref,
|
|
41
|
+
rangeLinkParams
|
|
42
|
+
} = await getOverviewPrimaryData(range);
|
|
43
|
+
const repairFocusHref = mergeHrefParams("/repair", rangeLinkParams);
|
|
47
44
|
|
|
48
45
|
return (
|
|
49
46
|
<div className="space-y-8">
|
|
50
|
-
<
|
|
51
|
-
title="Overview"
|
|
52
|
-
description="Local token, cost, model, and session analytics across AI CLI tools."
|
|
53
|
-
actions={
|
|
54
|
-
<Button asChild>
|
|
55
|
-
<Link href="/settings#scan-controls">
|
|
56
|
-
Configure scan <ArrowRight className="h-4 w-4" />
|
|
57
|
-
</Link>
|
|
58
|
-
</Button>
|
|
59
|
-
}
|
|
60
|
-
/>
|
|
61
|
-
|
|
62
|
-
<PeriodFilter range={range} />
|
|
63
|
-
|
|
64
|
-
{summary.interactions === 0 ? (
|
|
65
|
-
<FirstRunPanel status={firstRunStatus} />
|
|
66
|
-
) : null}
|
|
67
|
-
|
|
47
|
+
{summary.interactions === 0 ? <FirstRunPanel status={firstRunStatus} /> : null}
|
|
68
48
|
<UsagePulsePanel comparison={data.comparison} />
|
|
69
|
-
|
|
70
49
|
<DataConfidenceStrip confidence={data.dataConfidence} />
|
|
71
|
-
|
|
72
50
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-6">
|
|
73
51
|
<TokenAccountingCard
|
|
74
52
|
summary={summary}
|
|
@@ -84,11 +62,38 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
|
|
|
84
62
|
sessionsHref={evidenceLinks.sessions}
|
|
85
63
|
/>
|
|
86
64
|
</div>
|
|
87
|
-
|
|
88
65
|
<OverviewTrustFooter health={trust.health} pricedModelCount={trust.pricedModelCount} />
|
|
89
|
-
|
|
90
66
|
<TrendSection data={data.trends} defaultWindow={trendDefaultWindow} />
|
|
67
|
+
<UsageGuardrailsPanel progress={data.usageGuardrails} />
|
|
68
|
+
<OverviewRecommendationsCard recommendations={data.recommendations} />
|
|
69
|
+
<OverviewCurrentMixPanel
|
|
70
|
+
tools={data.tools}
|
|
71
|
+
mostUsedTool={summary.mostUsedTool}
|
|
72
|
+
mostUsedModel={summary.mostUsedModel}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function OverviewRepairSection({ range }: { range: ResolvedDateRange }) {
|
|
79
|
+
const {
|
|
80
|
+
accountingReport,
|
|
81
|
+
postSessionReview,
|
|
82
|
+
doctorReport,
|
|
83
|
+
repairWorkbench,
|
|
84
|
+
repairFocusHref,
|
|
85
|
+
evidenceLinks,
|
|
86
|
+
summary,
|
|
87
|
+
trust,
|
|
88
|
+
rangeLinkParams
|
|
89
|
+
} = await getOverviewRepairData(range);
|
|
91
90
|
|
|
91
|
+
if (summary.interactions === 0 && repairWorkbench.groups.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="space-y-8">
|
|
92
97
|
{summary.interactions > 0 ? (
|
|
93
98
|
<OverviewReviewStatusStrip
|
|
94
99
|
report={doctorReport}
|
|
@@ -102,7 +107,6 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
|
|
|
102
107
|
cachedEvidenceHref={evidenceLinks["cached-tokens"]}
|
|
103
108
|
/>
|
|
104
109
|
) : null}
|
|
105
|
-
|
|
106
110
|
{repairWorkbench.groups.length ? (
|
|
107
111
|
<TopRepairItemsStrip
|
|
108
112
|
groups={repairWorkbench.groups}
|
|
@@ -110,17 +114,38 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
|
|
|
110
114
|
rangeLinkParams={rangeLinkParams}
|
|
111
115
|
/>
|
|
112
116
|
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
113
120
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
export default async function OverviewPage({ searchParams }: OverviewPageProps) {
|
|
122
|
+
void runDueScheduledScan().catch(() => undefined);
|
|
123
|
+
const params = (await searchParams) ?? {};
|
|
124
|
+
const range = resolveDateRange(params);
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
return (
|
|
127
|
+
<div className="space-y-8">
|
|
128
|
+
<PageHeader
|
|
129
|
+
title="Overview"
|
|
130
|
+
description="Local token, cost, model, and session analytics across AI CLI tools."
|
|
131
|
+
actions={
|
|
132
|
+
<Button asChild>
|
|
133
|
+
<Link href="/settings#scan-controls">
|
|
134
|
+
Configure scan <ArrowRight className="h-4 w-4" />
|
|
135
|
+
</Link>
|
|
136
|
+
</Button>
|
|
137
|
+
}
|
|
122
138
|
/>
|
|
123
139
|
|
|
140
|
+
<PeriodFilter range={range} />
|
|
141
|
+
|
|
142
|
+
<Suspense fallback={<OverviewPrimarySkeleton />}>
|
|
143
|
+
<OverviewPrimarySection range={range} />
|
|
144
|
+
</Suspense>
|
|
145
|
+
|
|
146
|
+
<Suspense fallback={<OverviewRepairSkeleton />}>
|
|
147
|
+
<OverviewRepairSection range={range} />
|
|
148
|
+
</Suspense>
|
|
124
149
|
</div>
|
|
125
150
|
);
|
|
126
151
|
}
|
|
@@ -4,7 +4,10 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
|
|
4
4
|
import { MonoText, PageHeader } from "@/components/ui/typography";
|
|
5
5
|
import { EmptyState } from "@/components/empty-state";
|
|
6
6
|
import { ScanNowButton } from "@/components/scan-now-button";
|
|
7
|
+
import { ParserOverridesPanel } from "@/components/parser-debug/parser-overrides-panel";
|
|
7
8
|
import { getScanTrustData } from "@/src/lib/analytics";
|
|
9
|
+
import { adapters } from "@/src/ingestion/adapters";
|
|
10
|
+
import { listParserOverrides } from "@/src/lib/parser-overrides";
|
|
8
11
|
|
|
9
12
|
export const dynamic = "force-dynamic";
|
|
10
13
|
|
|
@@ -16,6 +19,11 @@ export default async function ParserDebugPage({
|
|
|
16
19
|
const params = await searchParams;
|
|
17
20
|
const selectedSource = params?.source;
|
|
18
21
|
const { scanFiles, health } = getScanTrustData();
|
|
22
|
+
const initialOverrides = listParserOverrides();
|
|
23
|
+
const parserOptions = adapters.map((adapter) => ({
|
|
24
|
+
id: adapter.id,
|
|
25
|
+
displayName: adapter.displayName
|
|
26
|
+
}));
|
|
19
27
|
const visibleScanFiles = selectedSource
|
|
20
28
|
? scanFiles.filter((file) => file.path === selectedSource)
|
|
21
29
|
: scanFiles.slice(0, 500);
|
|
@@ -30,6 +38,8 @@ export default async function ParserDebugPage({
|
|
|
30
38
|
description="Inspect adapter selection, parser confidence, extracted tokens, warnings, and failures."
|
|
31
39
|
/>
|
|
32
40
|
|
|
41
|
+
<ParserOverridesPanel initialOverrides={initialOverrides} parsers={parserOptions} />
|
|
42
|
+
|
|
33
43
|
<div className="rounded-md border bg-card p-3">
|
|
34
44
|
<div className="text-sm font-medium">Latest parser mix</div>
|
|
35
45
|
<div className="mt-2 flex flex-wrap gap-2">
|
package/app/projects/page.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
-
import { RankBarChart } from "@/components/charts/rank-bar-chart";
|
|
2
|
+
import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
|
|
3
3
|
import { TrendChart } from "@/components/charts/trend-chart";
|
|
4
4
|
import { EmptyState } from "@/components/empty-state";
|
|
5
5
|
import { ScanNowButton } from "@/components/scan-now-button";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PageHeader } from "@/components/ui/typography";
|
|
2
|
+
import { SavedReportsPanel } from "@/components/reports/saved-reports-panel";
|
|
3
|
+
import { listSavedReports } from "@/src/lib/saved-reports-store";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
export default async function ReportsPage() {
|
|
8
|
+
const reports = listSavedReports();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="space-y-6">
|
|
12
|
+
<PageHeader
|
|
13
|
+
title="Reports"
|
|
14
|
+
description="Save reusable local report templates and replay them from the CLI."
|
|
15
|
+
/>
|
|
16
|
+
<SavedReportsPanel initial={reports} />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
package/app/tools/page.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RankBarChart } from "@/components/charts/rank-bar-chart";
|
|
1
|
+
import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
|
|
2
2
|
import { EmptyState } from "@/components/empty-state";
|
|
3
3
|
import { ScanNowButton } from "@/components/scan-now-button";
|
|
4
4
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
import { ChartSkeleton } from "@/components/charts/skeleton";
|
|
5
|
+
|
|
6
|
+
export const RankBarChart = dynamic(
|
|
7
|
+
() => import("./rank-bar-chart").then((m) => m.RankBarChart),
|
|
8
|
+
{ ssr: false, loading: () => <ChartSkeleton heightClass="h-60" label="Loading chart…" /> }
|
|
9
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type ChartSkeletonProps = {
|
|
2
|
+
heightClass?: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function ChartSkeleton({ heightClass = "h-64", label = "Loading chart…" }: ChartSkeletonProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={`flex items-center justify-center rounded-md border border-dashed border-muted-foreground/30 bg-muted/30 text-xs text-muted-foreground ${heightClass}`}
|
|
10
|
+
role="status"
|
|
11
|
+
aria-live="polite"
|
|
12
|
+
>
|
|
13
|
+
{label}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
import { ChartSkeleton } from "@/components/charts/skeleton";
|
|
5
|
+
|
|
6
|
+
export const TrendSection = dynamic(
|
|
7
|
+
() => import("./trend-section").then((m) => m.TrendSection),
|
|
8
|
+
{ ssr: false, loading: () => <ChartSkeleton heightClass="h-72" label="Loading trend chart…" /> }
|
|
9
|
+
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
-
import { RankBarChart } from "@/components/charts/rank-bar-chart";
|
|
2
|
+
import { RankBarChart } from "@/components/charts/rank-bar-chart-lazy";
|
|
3
3
|
import { Badge } from "@/components/ui/badge";
|
|
4
4
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
5
5
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type SectionSkeletonProps = {
|
|
2
|
+
heightClass?: string;
|
|
3
|
+
label: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function OverviewSectionSkeleton({ heightClass = "h-48", label }: SectionSkeletonProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={`flex items-center justify-center rounded-md border border-dashed border-muted-foreground/30 bg-muted/30 text-xs text-muted-foreground ${heightClass}`}
|
|
10
|
+
role="status"
|
|
11
|
+
aria-live="polite"
|
|
12
|
+
>
|
|
13
|
+
{label}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function OverviewPrimarySkeleton() {
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-6">
|
|
21
|
+
<OverviewSectionSkeleton heightClass="h-20" label="Loading usage pulse…" />
|
|
22
|
+
<OverviewSectionSkeleton heightClass="h-36" label="Loading summary cards…" />
|
|
23
|
+
<OverviewSectionSkeleton heightClass="h-72" label="Loading trend chart…" />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function OverviewRepairSkeleton() {
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-6">
|
|
31
|
+
<OverviewSectionSkeleton heightClass="h-24" label="Loading review status…" />
|
|
32
|
+
<OverviewSectionSkeleton heightClass="h-40" label="Loading repair items…" />
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|