tokentrace 0.8.2 → 0.8.4

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/app/page.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import Link from "next/link";
2
- import { ArrowRight, Coins, Database, Gauge, MessageSquare, Minus, Sparkles, TrendingDown, TrendingUp } from "lucide-react";
2
+ import { ArrowRight, Coins, Database, Gauge, Layers, MessageSquare, Minus, TrendingDown, TrendingUp } from "lucide-react";
3
3
  import { RankBarChart } from "@/components/charts/rank-bar-chart";
4
4
  import { TrendChart } from "@/components/charts/trend-chart";
5
5
  import { PeriodFilter } from "@/components/period-filter";
@@ -10,11 +10,11 @@ import { HelpTooltip } from "@/components/ui/help-tooltip";
10
10
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
11
11
  import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typography";
12
12
  import { getAnalyticsData, getScanTrustData } from "@/src/lib/analytics";
13
- import { buildDoctorReport } from "@/src/lib/doctor";
13
+ import { buildDoctorReport, type DoctorReport } from "@/src/lib/doctor";
14
14
  import { getDefaultSearchRoots } from "@/src/ingestion/discovery";
15
15
  import { buildFirstRunStatus, type FirstRunStatus } from "@/src/lib/first-run-status";
16
16
  import { buildUnknownCostRepairWorkbench } from "@/src/lib/unknown-cost-repair";
17
- import { resolveDateRange } from "@/src/lib/date-range";
17
+ import { dateRangeQueryParams, mergeHrefParams, resolveDateRange } from "@/src/lib/date-range";
18
18
  import { formatCurrency, formatSignedTokens, formatTokens, percent } from "@/src/lib/format";
19
19
  import { cn } from "@/src/lib/utils";
20
20
  import type { UsageGuardrailMetric } from "@/src/lib/usage-guardrails";
@@ -28,6 +28,8 @@ function MetricCard({
28
28
  description,
29
29
  href,
30
30
  actionLabel = "Inspect sessions",
31
+ secondaryHref,
32
+ secondaryActionLabel,
31
33
  icon: Icon,
32
34
  className,
33
35
  valueClassName
@@ -38,6 +40,8 @@ function MetricCard({
38
40
  description?: string;
39
41
  href?: string;
40
42
  actionLabel?: string;
43
+ secondaryHref?: string;
44
+ secondaryActionLabel?: string;
41
45
  icon: typeof Database;
42
46
  className?: string;
43
47
  valueClassName?: string;
@@ -45,35 +49,127 @@ function MetricCard({
45
49
  const tooltipId = `metric-${label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}-help`;
46
50
 
47
51
  return (
48
- <Card className={cn("h-full", className)}>
49
- <CardHeader className="flex flex-row items-center justify-between gap-3 space-y-0 pb-3">
52
+ <Card className={cn("flex h-full flex-col overflow-hidden", className)}>
53
+ <CardHeader className="flex flex-row items-start justify-between gap-3 space-y-0 pb-3">
50
54
  <div className="flex min-w-0 items-center gap-1.5">
51
55
  <CardTitle className="leading-tight">{label}</CardTitle>
52
56
  {description ? (
53
57
  <HelpTooltip id={tooltipId} label={label} description={description} />
54
58
  ) : null}
55
59
  </div>
56
- <Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
60
+ <span className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
61
+ <Icon className="h-4 w-4" aria-hidden="true" />
62
+ </span>
57
63
  </CardHeader>
58
- <CardContent>
59
- <DataValue size="lg" className={valueClassName}>{value}</DataValue>
64
+ <CardContent className="flex flex-1 flex-col">
65
+ <DataValue size="lg" className={cn("break-words leading-none", valueClassName)}>{value}</DataValue>
60
66
  {detailItems?.length ? (
61
- <div className="mt-3 flex flex-wrap gap-x-2 gap-y-1 text-xs leading-snug text-muted-foreground">
62
- {detailItems.map((item) => (
63
- <span key={item}>{item}</span>
67
+ <div className="mt-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs leading-snug text-muted-foreground">
68
+ {detailItems.map((item, index) => (
69
+ <span key={item} className="inline-flex min-w-0 items-center gap-2">
70
+ <span className="min-w-0">{item}</span>
71
+ {index < detailItems.length - 1 ? <span className="hidden text-border sm:inline">/</span> : null}
72
+ </span>
64
73
  ))}
65
74
  </div>
66
75
  ) : null}
67
- {href ? (
68
- <Link href={href} className="mt-3 inline-flex text-xs font-medium text-primary underline-offset-4 hover:underline">
69
- {actionLabel}
70
- </Link>
76
+ {href || secondaryHref ? (
77
+ <div className="mt-auto flex flex-wrap items-center gap-x-3 gap-y-1 pt-4">
78
+ {href ? (
79
+ <Link href={href} className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-primary underline-offset-4 hover:underline">
80
+ {actionLabel}
81
+ <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
82
+ </Link>
83
+ ) : null}
84
+ {secondaryHref ? (
85
+ <Link href={secondaryHref} className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline">
86
+ {secondaryActionLabel ?? "View details"}
87
+ </Link>
88
+ ) : null}
89
+ </div>
71
90
  ) : null}
72
91
  </CardContent>
73
92
  </Card>
74
93
  );
75
94
  }
76
95
 
96
+ function OverviewTrustStrip({
97
+ latestScan,
98
+ confidence,
99
+ repairHref,
100
+ processedTokensHref
101
+ }: {
102
+ latestScan: { id: string | null; filesScanned: number; recordsImported: number };
103
+ confidence: {
104
+ interactions: number;
105
+ exactTokenInteractions: number;
106
+ exactCostInteractions: number;
107
+ estimatedCostInteractions: number;
108
+ unknownCostInteractions: number;
109
+ };
110
+ repairHref: string;
111
+ processedTokensHref: string;
112
+ }) {
113
+ const exactTokenShare = confidence.interactions > 0 ? percent(confidence.exactTokenInteractions / confidence.interactions) : "0%";
114
+ const costCoverage =
115
+ confidence.interactions > 0
116
+ ? percent((confidence.exactCostInteractions + confidence.estimatedCostInteractions) / confidence.interactions)
117
+ : "0%";
118
+ const trustItems = [
119
+ {
120
+ label: "Latest scan",
121
+ value: latestScan.id ? `${latestScan.recordsImported.toLocaleString()} imports` : "No scan",
122
+ detail: latestScan.id ? `${latestScan.filesScanned.toLocaleString()} files scanned` : "Waiting for imported usage",
123
+ href: "/discovery",
124
+ icon: Database
125
+ },
126
+ {
127
+ label: "Exact tokens",
128
+ value: exactTokenShare,
129
+ detail: `${confidence.exactTokenInteractions.toLocaleString()} interactions with provider counts`,
130
+ href: processedTokensHref,
131
+ icon: Gauge
132
+ },
133
+ {
134
+ label: "Cost coverage",
135
+ value: costCoverage,
136
+ detail:
137
+ confidence.unknownCostInteractions > 0
138
+ ? `${confidence.unknownCostInteractions.toLocaleString()} interactions need repair`
139
+ : "All imported interactions priced",
140
+ href: confidence.unknownCostInteractions > 0 ? repairHref : "/pricing",
141
+ icon: Coins
142
+ }
143
+ ];
144
+
145
+ return (
146
+ <section className="grid overflow-hidden rounded-lg border border-border bg-card md:grid-cols-3" aria-label="Overview data confidence">
147
+ {trustItems.map((item, index) => {
148
+ const Icon = item.icon;
149
+ return (
150
+ <Link
151
+ key={item.label}
152
+ href={item.href}
153
+ className={cn(
154
+ "group flex min-w-0 items-start gap-3 p-4 transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
155
+ index > 0 ? "border-t border-border md:border-l md:border-t-0" : ""
156
+ )}
157
+ >
158
+ <span className="mt-0.5 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
159
+ <Icon className="h-4 w-4" aria-hidden="true" />
160
+ </span>
161
+ <span className="min-w-0 space-y-1">
162
+ <FieldLabel>{item.label}</FieldLabel>
163
+ <span className="block truncate text-base font-semibold text-foreground">{item.value}</span>
164
+ <span className="block text-sm text-muted-foreground">{item.detail}</span>
165
+ </span>
166
+ </Link>
167
+ );
168
+ })}
169
+ </section>
170
+ );
171
+ }
172
+
77
173
  function formatSignedCurrency(value: number) {
78
174
  if (value === 0) return "$0.00";
79
175
  return `${value > 0 ? "+" : "-"}${formatCurrency(Math.abs(value))}`;
@@ -113,15 +209,15 @@ function DeltaMetric({
113
209
  : "text-muted-foreground";
114
210
 
115
211
  return (
116
- <div className="min-w-44 border-t p-3 first:border-t-0 sm:border-l sm:border-t-0 sm:first:border-l-0">
117
- <div className="flex items-center justify-between gap-3">
118
- <FieldLabel>{label}</FieldLabel>
119
- <span className={cn("inline-flex items-center gap-1 text-xs font-semibold", toneClass)}>
120
- <ToneIcon className="h-3.5 w-3.5" />
212
+ <div className="min-w-0 border-t px-4 py-4 first:border-t-0 sm:border-l sm:border-t-0 sm:first:border-l-0">
213
+ <div className="flex min-w-0 items-center justify-between gap-3">
214
+ <FieldLabel className="truncate">{label}</FieldLabel>
215
+ <span className={cn("inline-flex shrink-0 items-center gap-1 text-xs font-semibold tabular-nums", toneClass)}>
216
+ <ToneIcon className="h-3.5 w-3.5" aria-hidden="true" />
121
217
  {formatPercentValue(percentValue)}
122
218
  </span>
123
219
  </div>
124
- <DataValue className="mt-1" size="md">{value}</DataValue>
220
+ <DataValue className="mt-1 truncate" size="md">{value}</DataValue>
125
221
  <div className="mt-1 text-xs text-muted-foreground">
126
222
  {delta} vs {previous}
127
223
  </div>
@@ -254,6 +350,97 @@ function FirstRunPanel({ status }: { status: FirstRunStatus }) {
254
350
  );
255
351
  }
256
352
 
353
+ function readinessVariant(state: "ready" | "review" | "blocked") {
354
+ if (state === "ready") return "success";
355
+ if (state === "blocked") return "destructive";
356
+ return "warning";
357
+ }
358
+
359
+ function DataReadinessPanel({
360
+ report,
361
+ selectedInteractions,
362
+ selectedCachedTokens,
363
+ repairHref,
364
+ cachedEvidenceHref
365
+ }: {
366
+ report: DoctorReport;
367
+ selectedInteractions: number;
368
+ selectedCachedTokens: number;
369
+ repairHref: string;
370
+ cachedEvidenceHref: string;
371
+ }) {
372
+ const parserReviewFiles = report.parserCoverage.parserReviewFiles + report.parserCoverage.failureFiles;
373
+ const items = [
374
+ {
375
+ label: "Local scans",
376
+ value: report.latestScan.id ? `${report.latestScan.recordsImported.toLocaleString()} imports` : "No scan",
377
+ detail: report.latestScan.zeroImportExplanation ?? `${report.latestScan.filesScanned.toLocaleString()} files checked in the latest scan.`,
378
+ state: report.latestScan.id ? "ready" as const : "blocked" as const,
379
+ href: "/discovery"
380
+ },
381
+ {
382
+ label: "Parser coverage",
383
+ value: parserReviewFiles > 0 ? `${parserReviewFiles.toLocaleString()} files need review` : "No parser blockers",
384
+ detail: "Discovery and Parser Debug explain ignored, unsupported, failed, and imported files.",
385
+ state: report.parserCoverage.failureFiles > 0 ? "blocked" as const : parserReviewFiles > 0 ? "review" as const : "ready" as const,
386
+ href: parserReviewFiles > 0 ? "/parser-debug" : "/discovery"
387
+ },
388
+ {
389
+ label: "Pricing",
390
+ value: report.pricing.unknown > 0 ? `${report.pricing.unknown.toLocaleString()} unknown` : "Priced",
391
+ detail: `${report.pricing.priced.toLocaleString()} priced interactions across ${report.pricing.pricedModelCount.toLocaleString()} priced models.`,
392
+ state: report.pricing.unknown > 0 ? "blocked" as const : "ready" as const,
393
+ href: report.pricing.unknown > 0 ? repairHref : "/pricing"
394
+ },
395
+ {
396
+ label: "Cache accounting",
397
+ value: selectedCachedTokens > 0 ? formatTokens(selectedCachedTokens) : "No cache tokens",
398
+ detail: selectedCachedTokens > 0
399
+ ? "Cache read and write tokens are separated from fresh input/output."
400
+ : selectedInteractions > 0 ? "The selected period has no imported cache read/write counts." : "Scan usage before cache accounting appears.",
401
+ state: selectedCachedTokens > 0 ? "ready" as const : "review" as const,
402
+ href: cachedEvidenceHref
403
+ },
404
+ {
405
+ label: "Boundaries",
406
+ value: `${report.supportMatrix.summary.stable} stable, ${report.supportMatrix.summary.bestEffort} best effort`,
407
+ detail: "Unsupported desktop scraping, packet capture, proxying, and telemetry stay out of scope.",
408
+ state: "ready" as const,
409
+ href: "/doctor"
410
+ }
411
+ ];
412
+
413
+ return (
414
+ <Card>
415
+ <CardHeader>
416
+ <CardTitle>Data Readiness</CardTitle>
417
+ <CardDescription>Current trust checks before acting on cost, token, parser, or cache numbers.</CardDescription>
418
+ </CardHeader>
419
+ <CardContent className="p-0">
420
+ <div className="grid border-t md:grid-cols-2 xl:grid-cols-5">
421
+ {items.map((item, index) => (
422
+ <Link
423
+ key={item.label}
424
+ href={item.href}
425
+ className={cn(
426
+ "block min-w-0 p-4 transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
427
+ index > 0 ? "border-t border-border md:border-l md:border-t-0 xl:border-l" : ""
428
+ )}
429
+ >
430
+ <div className="flex min-w-0 items-start justify-between gap-2">
431
+ <FieldLabel className="truncate">{item.label}</FieldLabel>
432
+ <Badge variant={readinessVariant(item.state)}>{item.state}</Badge>
433
+ </div>
434
+ <div className="mt-2 text-sm font-semibold text-foreground">{item.value}</div>
435
+ <div className="mt-1 text-xs leading-relaxed text-muted-foreground">{item.detail}</div>
436
+ </Link>
437
+ ))}
438
+ </div>
439
+ </CardContent>
440
+ </Card>
441
+ );
442
+ }
443
+
257
444
  type OverviewPageProps = {
258
445
  searchParams?: Promise<Record<string, string | string[] | undefined>>;
259
446
  };
@@ -262,10 +449,20 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
262
449
  const params = (await searchParams) ?? {};
263
450
  const range = resolveDateRange(params);
264
451
  const data = getAnalyticsData(range.filters);
265
- const trust = getScanTrustData();
452
+ const rangeLinkParams = dateRangeQueryParams(range);
453
+ const evidenceLinks = Object.fromEntries(
454
+ Object.entries(data.evidenceLinks).map(([key, href]) => [key, mergeHrefParams(href, rangeLinkParams)])
455
+ ) as typeof data.evidenceLinks;
456
+ const trust = getScanTrustData(range.filters);
266
457
  const roots = await getDefaultSearchRoots();
267
458
  const doctorReport = buildDoctorReport({ ...trust, roots });
268
- const repairWorkbench = buildUnknownCostRepairWorkbench();
459
+ const repairWorkbench = buildUnknownCostRepairWorkbench(range.filters);
460
+ const nextRepairGroup =
461
+ repairWorkbench.groups.find((group) => group.review.status !== "ignored" && group.review.status !== "resolved")
462
+ ?? repairWorkbench.groups[0]
463
+ ?? null;
464
+ const repairFocusHref = mergeHrefParams(nextRepairGroup?.itemHref ?? "/repair", rangeLinkParams);
465
+ const unknownCostEvidenceHref = evidenceLinks["unknown-cost"];
269
466
  const firstRunStatus = buildFirstRunStatus({
270
467
  rootCount: roots.length,
271
468
  pricedModelCount: trust.pricedModelCount,
@@ -360,7 +557,7 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
360
557
  `${formatTokens(summary.outputTokens)} output`,
361
558
  `${formatTokens(summary.cachedTokens)} cached`
362
559
  ]}
363
- href={data.evidenceLinks["processed-tokens"]}
560
+ href={evidenceLinks["processed-tokens"]}
364
561
  actionLabel="View evidence"
365
562
  icon={Database}
366
563
  />
@@ -373,7 +570,7 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
373
570
  `${formatTokens(summary.outputTokens)} output`,
374
571
  `${formatTokens(summary.reasoningTokens)} reasoning`
375
572
  ]}
376
- href={data.evidenceLinks["non-cache-tokens"]}
573
+ href={evidenceLinks["non-cache-tokens"]}
377
574
  actionLabel="View evidence"
378
575
  icon={Database}
379
576
  />
@@ -385,21 +582,24 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
385
582
  `${formatTokens(summary.cacheReadTokens)} read`,
386
583
  `${formatTokens(summary.cacheWriteTokens)} write`
387
584
  ]}
388
- href={data.evidenceLinks["cached-tokens"]}
585
+ href={evidenceLinks["cached-tokens"]}
389
586
  actionLabel="View evidence"
390
- icon={Sparkles}
587
+ icon={Layers}
391
588
  />
392
589
  <MetricCard
393
590
  label="Estimated cost"
394
591
  value={formatCurrency(summary.totalCost)}
592
+ valueClassName="break-normal whitespace-nowrap text-2xl"
395
593
  description="Cost is calculated from editable model prices. Unknown means a model price or usable token count is missing."
396
594
  detailItems={[
397
595
  `${formatCurrency(summary.exactCost)} exact`,
398
596
  `${formatCurrency(summary.estimatedCost)} estimated`,
399
597
  `${summary.unknownCostInteractions.toLocaleString()} unknown`
400
598
  ]}
401
- href={summary.unknownCostInteractions > 0 ? "/repair" : data.evidenceLinks["estimated-cost"]}
402
- actionLabel={summary.unknownCostInteractions > 0 ? "Repair unknown costs" : "View evidence"}
599
+ href={summary.unknownCostInteractions > 0 ? repairFocusHref : evidenceLinks["estimated-cost"]}
600
+ actionLabel={summary.unknownCostInteractions > 0 ? "Open next repair item" : "View evidence"}
601
+ secondaryHref={summary.unknownCostInteractions > 0 ? unknownCostEvidenceHref : undefined}
602
+ secondaryActionLabel="View unknown-cost evidence"
403
603
  icon={Coins}
404
604
  />
405
605
  <MetricCard
@@ -407,7 +607,7 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
407
607
  value={summary.sessions.toLocaleString()}
408
608
  description="Imported local CLI sessions and interactions in the selected period."
409
609
  detailItems={[`${summary.interactions.toLocaleString()} interactions`]}
410
- href={data.evidenceLinks.sessions}
610
+ href={evidenceLinks.sessions}
411
611
  actionLabel="View evidence"
412
612
  icon={MessageSquare}
413
613
  />
@@ -434,6 +634,25 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
434
634
  </Card>
435
635
  </div>
436
636
 
637
+ {summary.interactions > 0 ? (
638
+ <OverviewTrustStrip
639
+ latestScan={doctorReport.latestScan}
640
+ confidence={trust.confidence}
641
+ repairHref={repairFocusHref}
642
+ processedTokensHref={evidenceLinks["processed-tokens"]}
643
+ />
644
+ ) : null}
645
+
646
+ {summary.interactions > 0 ? (
647
+ <DataReadinessPanel
648
+ report={doctorReport}
649
+ selectedInteractions={summary.interactions}
650
+ selectedCachedTokens={summary.cachedTokens}
651
+ repairHref={repairFocusHref}
652
+ cachedEvidenceHref={evidenceLinks["cached-tokens"]}
653
+ />
654
+ ) : null}
655
+
437
656
  <UsageGuardrailsPanel progress={data.usageGuardrails} />
438
657
 
439
658
  <Card>
@@ -443,19 +662,34 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
443
662
  Local rules ranked from your scan, pricing, parser, project, and cache data.
444
663
  </CardDescription>
445
664
  </CardHeader>
446
- <CardContent className="grid divide-y overflow-hidden p-0 lg:grid-cols-3 lg:divide-x lg:divide-y-0">
447
- {data.recommendations.slice(0, 3).map((item) => (
448
- <Link key={item.id} href={item.href} className="px-4 py-3 transition-colors hover:bg-muted/30">
449
- <div className="flex flex-wrap items-center gap-2">
450
- <div className="text-sm font-semibold">{item.title}</div>
451
- <Badge variant={item.severity === "high" ? "destructive" : item.severity === "medium" ? "warning" : "secondary"}>
452
- {item.severity}
453
- </Badge>
454
- </div>
455
- <div className="mt-1 text-xs leading-relaxed text-muted-foreground">{item.evidence}</div>
456
- <div className="mt-2 text-xs font-medium text-emerald-800">{item.action}</div>
457
- </Link>
458
- ))}
665
+ <CardContent className="p-0">
666
+ <ol className="grid divide-y overflow-hidden lg:grid-cols-3 lg:divide-x lg:divide-y-0">
667
+ {data.recommendations.slice(0, 3).map((item, index) => (
668
+ <li key={item.id} className="min-w-0">
669
+ <Link
670
+ href={item.href}
671
+ className="group flex h-full min-w-0 gap-3 px-4 py-4 transition-colors hover:bg-muted/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
672
+ >
673
+ <span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border text-xs font-semibold tabular-nums text-muted-foreground">
674
+ {index + 1}
675
+ </span>
676
+ <span className="min-w-0">
677
+ <span className="flex min-w-0 flex-wrap items-center gap-2">
678
+ <span className="text-sm font-semibold text-foreground">{item.title}</span>
679
+ <Badge variant={item.severity === "high" ? "destructive" : item.severity === "medium" ? "warning" : "secondary"}>
680
+ {item.severity}
681
+ </Badge>
682
+ </span>
683
+ <span className="mt-1 block text-xs leading-relaxed text-muted-foreground">{item.evidence}</span>
684
+ <span className="mt-2 inline-flex items-center gap-1.5 text-xs font-medium text-primary">
685
+ {item.action}
686
+ <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
687
+ </span>
688
+ </span>
689
+ </Link>
690
+ </li>
691
+ ))}
692
+ </ol>
459
693
  </CardContent>
460
694
  </Card>
461
695
 
@@ -521,8 +755,8 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
521
755
  </CardDescription>
522
756
  </div>
523
757
  <Button asChild variant="outline" size="sm">
524
- <Link href="/repair">
525
- Open Repair <ArrowRight className="h-4 w-4" />
758
+ <Link href={repairFocusHref}>
759
+ Open next repair <ArrowRight className="h-4 w-4" />
526
760
  </Link>
527
761
  </Button>
528
762
  </CardHeader>
@@ -558,17 +792,23 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
558
792
  <TableCell>{row.interactions.toLocaleString()}</TableCell>
559
793
  <TableCell>{formatTokens(row.totalTokens)}</TableCell>
560
794
  <TableCell className="max-w-72 truncate">
561
- <Link href={row.sourceHref} title={row.sourceFile}>
795
+ <Link href={mergeHrefParams(row.sourceHref, rangeLinkParams)} title={row.sourceFile}>
562
796
  <MonoText className="text-muted-foreground underline-offset-4 hover:underline">{row.sourceFile}</MonoText>
563
797
  </Link>
564
798
  </TableCell>
565
799
  <TableCell>
566
800
  <div className="flex flex-wrap gap-2">
567
- <Link href={row.repairHref} className="font-medium text-primary underline-offset-4 hover:underline">
801
+ <Link href={mergeHrefParams(row.repairHref, rangeLinkParams)} className="font-medium text-primary underline-offset-4 hover:underline">
568
802
  {row.pricingHref ? "Configure price" : "Review parser"}
569
803
  </Link>
804
+ <Link href={mergeHrefParams(row.itemHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
805
+ Item
806
+ </Link>
807
+ <Link href={mergeHrefParams(row.sourceHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
808
+ Evidence
809
+ </Link>
570
810
  {row.pricingHref ? (
571
- <Link href={row.parserHref} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
811
+ <Link href={mergeHrefParams(row.parserHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
572
812
  Parser
573
813
  </Link>
574
814
  ) : null}
@@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
8
8
  export default async function PricingPage({
9
9
  searchParams
10
10
  }: {
11
- searchParams?: Promise<{ model?: string }>;
11
+ searchParams?: Promise<{ model?: string; returnTo?: string }>;
12
12
  }) {
13
13
  const params = await searchParams;
14
14
  const data = getAnalyticsData();
@@ -21,6 +21,7 @@ export default async function PricingPage({
21
21
  <PricingSettings
22
22
  initialRows={getPricingRows()}
23
23
  initialModel={params?.model}
24
+ returnTo={params?.returnTo}
24
25
  aliasSuggestions={data.modelAliasSuggestions}
25
26
  />
26
27
  </div>