@oh-my-pi/omp-stats 18.3.5 → 18.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +13 -11
  3. package/build.ts +5 -49
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +2 -2
  6. package/dist/client/index.js +105 -126
  7. package/dist/types/aggregator.d.ts +25 -22
  8. package/dist/types/client/api.d.ts +11 -7
  9. package/dist/types/client/app/LiveChip.d.ts +2 -0
  10. package/dist/types/client/app/Shell.d.ts +16 -0
  11. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  12. package/dist/types/client/app/nav.d.ts +18 -0
  13. package/dist/types/client/charts/BarList.d.ts +17 -0
  14. package/dist/types/client/charts/Chart.d.ts +49 -0
  15. package/dist/types/client/charts/Legend.d.ts +16 -0
  16. package/dist/types/client/charts/ShareBar.d.ts +11 -0
  17. package/dist/types/client/charts/Sparkline.d.ts +11 -0
  18. package/dist/types/client/charts/TimeChart.d.ts +9 -0
  19. package/dist/types/client/charts/index.d.ts +7 -0
  20. package/dist/types/client/charts/types.d.ts +25 -0
  21. package/dist/types/client/charts/useWidth.d.ts +3 -0
  22. package/dist/types/client/data/colors.d.ts +25 -0
  23. package/dist/types/client/data/formatters.d.ts +9 -1
  24. package/dist/types/client/data/live.d.ts +29 -0
  25. package/dist/types/client/data/query.d.ts +35 -0
  26. package/dist/types/client/data/range.d.ts +31 -0
  27. package/dist/types/client/data/series.d.ts +28 -0
  28. package/dist/types/client/data/useHashRoute.d.ts +18 -4
  29. package/dist/types/client/data/view-models.d.ts +111 -28
  30. package/dist/types/client/index.d.ts +0 -1
  31. package/dist/types/client/routes/CostsRoute.d.ts +2 -2
  32. package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
  33. package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
  34. package/dist/types/client/routes/GainRoute.d.ts +1 -2
  35. package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
  36. package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
  37. package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
  38. package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
  39. package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
  40. package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
  41. package/dist/types/client/routes/TracesRoute.d.ts +2 -2
  42. package/dist/types/client/routes/index.d.ts +1 -1
  43. package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
  44. package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
  45. package/dist/types/client/traces/trace-colors.d.ts +18 -12
  46. package/dist/types/client/ui/Badge.d.ts +19 -0
  47. package/dist/types/client/ui/Card.d.ts +25 -0
  48. package/dist/types/client/ui/Drawer.d.ts +25 -0
  49. package/dist/types/client/ui/JsonBlock.d.ts +2 -2
  50. package/dist/types/client/ui/Modal.d.ts +23 -0
  51. package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
  52. package/dist/types/client/ui/SearchInput.d.ts +8 -0
  53. package/dist/types/client/ui/Segmented.d.ts +15 -0
  54. package/dist/types/client/ui/Stat.d.ts +36 -0
  55. package/dist/types/client/ui/States.d.ts +41 -0
  56. package/dist/types/client/ui/Table.d.ts +48 -0
  57. package/dist/types/client/ui/index.d.ts +9 -10
  58. package/dist/types/db.d.ts +90 -106
  59. package/dist/types/frustration.d.ts +70 -0
  60. package/dist/types/index.d.ts +3 -1
  61. package/dist/types/live.d.ts +30 -0
  62. package/dist/types/rollup.d.ts +115 -0
  63. package/dist/types/server.d.ts +6 -1
  64. package/dist/types/shared-types.d.ts +116 -56
  65. package/dist/types/types.d.ts +4 -0
  66. package/dist/types/user-metrics.d.ts +8 -0
  67. package/package.json +7 -13
  68. package/src/aggregator.ts +276 -211
  69. package/src/client/App.tsx +38 -80
  70. package/src/client/api.ts +65 -7
  71. package/src/client/app/LiveChip.tsx +97 -0
  72. package/src/client/app/Shell.tsx +181 -0
  73. package/src/client/app/ThemeToggle.tsx +5 -2
  74. package/src/client/app/nav.ts +78 -0
  75. package/src/client/charts/BarList.tsx +48 -0
  76. package/src/client/charts/Chart.tsx +388 -0
  77. package/src/client/charts/Legend.tsx +61 -0
  78. package/src/client/charts/ShareBar.tsx +26 -0
  79. package/src/client/charts/Sparkline.tsx +38 -0
  80. package/src/client/charts/TimeChart.tsx +22 -0
  81. package/src/client/charts/index.ts +7 -0
  82. package/src/client/charts/types.ts +27 -0
  83. package/src/client/charts/useWidth.ts +19 -0
  84. package/src/client/data/colors.ts +45 -0
  85. package/src/client/data/formatters.ts +30 -9
  86. package/src/client/data/live.tsx +85 -0
  87. package/src/client/data/query.ts +166 -0
  88. package/src/client/data/range.ts +82 -0
  89. package/src/client/data/series.ts +85 -0
  90. package/src/client/data/useHashRoute.ts +41 -83
  91. package/src/client/data/view-models.ts +347 -140
  92. package/src/client/index.tsx +3 -6
  93. package/src/client/routes/CostsRoute.tsx +394 -222
  94. package/src/client/routes/ErrorsRoute.tsx +401 -98
  95. package/src/client/routes/FrustrationRoute.tsx +924 -0
  96. package/src/client/routes/GainRoute.tsx +209 -206
  97. package/src/client/routes/ModelsRoute.tsx +540 -431
  98. package/src/client/routes/OverviewRoute.tsx +283 -311
  99. package/src/client/routes/ProjectsRoute.tsx +298 -156
  100. package/src/client/routes/ProvidersRoute.tsx +948 -416
  101. package/src/client/routes/RequestsRoute.tsx +295 -107
  102. package/src/client/routes/ToolsRoute.tsx +495 -398
  103. package/src/client/routes/TracesRoute.tsx +136 -117
  104. package/src/client/routes/costs.css +24 -0
  105. package/src/client/routes/errors.css +60 -0
  106. package/src/client/routes/frustration.css +123 -0
  107. package/src/client/routes/index.ts +1 -1
  108. package/src/client/routes/models.css +43 -0
  109. package/src/client/routes/projects.css +6 -0
  110. package/src/client/routes/providers.css +28 -0
  111. package/src/client/routes/tools.css +14 -0
  112. package/src/client/styles.css +1396 -1299
  113. package/src/client/traces/AggregatesPanel.tsx +55 -74
  114. package/src/client/traces/Minimap.tsx +6 -15
  115. package/src/client/traces/SpanDrawer.tsx +128 -194
  116. package/src/client/traces/SummaryStrip.tsx +21 -24
  117. package/src/client/traces/TimelineCanvas.tsx +27 -44
  118. package/src/client/traces/TraceView.tsx +175 -130
  119. package/src/client/traces/TranscriptList.tsx +13 -41
  120. package/src/client/traces/trace-colors.ts +60 -53
  121. package/src/client/traces/traces.css +279 -0
  122. package/src/client/ui/Badge.tsx +29 -0
  123. package/src/client/ui/Card.tsx +61 -0
  124. package/src/client/ui/Drawer.tsx +93 -0
  125. package/src/client/ui/JsonBlock.tsx +26 -44
  126. package/src/client/ui/Modal.tsx +106 -0
  127. package/src/client/ui/RequestDrawer.tsx +163 -188
  128. package/src/client/ui/SearchInput.tsx +25 -0
  129. package/src/client/ui/Segmented.tsx +61 -0
  130. package/src/client/ui/Stat.tsx +104 -0
  131. package/src/client/ui/States.tsx +81 -0
  132. package/src/client/ui/Table.tsx +229 -0
  133. package/src/client/ui/index.ts +9 -10
  134. package/src/client/ui/modal.css +54 -0
  135. package/src/client/ui/request-drawer.css +40 -0
  136. package/src/db.ts +379 -983
  137. package/src/frustration.ts +473 -0
  138. package/src/index.ts +22 -22
  139. package/src/live.ts +259 -0
  140. package/src/parser.ts +55 -66
  141. package/src/rollup.ts +951 -0
  142. package/src/server.ts +123 -40
  143. package/src/shared-types.ts +120 -56
  144. package/src/trace.ts +14 -7
  145. package/src/types.ts +4 -0
  146. package/src/user-metrics.ts +13 -0
  147. package/dist/client/styles.css +0 -1976
  148. package/dist/types/client/app/AppLayout.d.ts +0 -16
  149. package/dist/types/client/app/NavRail.d.ts +0 -7
  150. package/dist/types/client/app/RangeControl.d.ts +0 -7
  151. package/dist/types/client/app/SyncButton.d.ts +0 -14
  152. package/dist/types/client/app/TopBar.d.ts +0 -15
  153. package/dist/types/client/app/routes.d.ts +0 -12
  154. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  155. package/dist/types/client/components/chart-shared.d.ts +0 -178
  156. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  157. package/dist/types/client/components/range-meta.d.ts +0 -21
  158. package/dist/types/client/data/charts.d.ts +0 -1
  159. package/dist/types/client/data/useResource.d.ts +0 -13
  160. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  161. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  162. package/dist/types/client/ui/DataTable.d.ts +0 -17
  163. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  164. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  165. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  166. package/dist/types/client/ui/Panel.d.ts +0 -7
  167. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  168. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  169. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  170. package/src/client/app/AppLayout.tsx +0 -93
  171. package/src/client/app/NavRail.tsx +0 -44
  172. package/src/client/app/RangeControl.tsx +0 -39
  173. package/src/client/app/SyncButton.tsx +0 -75
  174. package/src/client/app/TopBar.tsx +0 -73
  175. package/src/client/app/routes.ts +0 -93
  176. package/src/client/components/AgentTokenShare.tsx +0 -68
  177. package/src/client/components/chart-shared.tsx +0 -273
  178. package/src/client/components/models-table-shared.tsx +0 -255
  179. package/src/client/components/range-meta.ts +0 -73
  180. package/src/client/data/charts.ts +0 -14
  181. package/src/client/data/useResource.ts +0 -154
  182. package/src/client/routes/BehaviorRoute.tsx +0 -623
  183. package/src/client/ui/AsyncBoundary.tsx +0 -54
  184. package/src/client/ui/DataTable.tsx +0 -122
  185. package/src/client/ui/EmptyState.tsx +0 -16
  186. package/src/client/ui/ErrorState.tsx +0 -25
  187. package/src/client/ui/MetricCluster.tsx +0 -92
  188. package/src/client/ui/Panel.tsx +0 -24
  189. package/src/client/ui/SegmentedControl.tsx +0 -36
  190. package/src/client/ui/Skeleton.tsx +0 -17
  191. package/src/client/ui/StatusPill.tsx +0 -15
  192. package/tailwind.config.js +0 -40
@@ -1,226 +1,229 @@
1
1
  import { useMemo, useState } from "react";
2
- import { Line } from "react-chartjs-2";
3
2
  import { getGainDashboardStats } from "../api";
4
- import { buildSharedPlugins, buildSharedScales, CHART_THEMES, lineDatasetStyle } from "../components/chart-shared";
3
+ import { Chart, type ChartSeries, Legend } from "../charts";
5
4
  import { formatBytes, formatCompact, formatInteger, formatPercent } from "../data/formatters";
6
- import { useResource } from "../data/useResource";
7
- import type { GainDashboardStats, GainSourceTotals, GainTimeSeriesPoint, TimeRange } from "../types";
8
- import { AsyncBoundary, Panel } from "../ui";
9
- import { useSystemTheme } from "../useSystemTheme";
5
+ import { useQuery } from "../data/query";
6
+ import { bucketAxis, rangeMeta } from "../data/range";
7
+ import { densify } from "../data/series";
8
+ import type { GainSource, GainSourceTotals, TimeRange } from "../types";
9
+ import {
10
+ Card,
11
+ ChartSkeleton,
12
+ type Column,
13
+ EmptyState,
14
+ MeterCell,
15
+ PageHeader,
16
+ QueryView,
17
+ Stat,
18
+ StatGrid,
19
+ Table,
20
+ } from "../ui";
10
21
 
11
22
  export interface GainRouteProps {
12
23
  active: boolean;
13
24
  range: TimeRange;
14
- refreshTrigger: number;
15
25
  }
16
26
 
17
- export function GainRoute({ active, range, refreshTrigger }: GainRouteProps) {
18
- const [project, setProject] = useState<string | null>(null);
19
-
20
- const {
21
- data: stats,
22
- error,
23
- loading,
24
- } = useResource(["gain", range, refreshTrigger, project], signal => getGainDashboardStats(range, project, signal), {
25
- pollMs: 30_000,
26
- enabled: active,
27
- });
28
-
29
- return (
30
- <div className="stats-route-container space-y-6">
31
- <AsyncBoundary loading={loading} error={error} data={stats}>
32
- {stats && (
33
- <>
34
- <GainProjectSelector projects={stats.projects} selected={project} onChange={setProject} />
35
- <GainOverallPanel overall={stats.overall} />
36
- <GainBySourcePanel bySource={stats.bySource} />
37
- <GainTimeSeriesPanel timeSeries={stats.timeSeries} />
38
- </>
39
- )}
40
- </AsyncBoundary>
41
- </div>
42
- );
43
- }
27
+ const DAY_MS = 86_400_000;
44
28
 
45
- // ---------------------------------------------------------------------------
46
- // Project selector
47
- // ---------------------------------------------------------------------------
29
+ const SOURCE_LABEL: Record<GainSource, string> = { snapcompact: "Snapcompact" };
48
30
 
49
- function GainProjectSelector({
50
- projects,
51
- selected,
52
- onChange,
53
- }: {
54
- projects: string[];
55
- selected: string | null;
56
- onChange: (p: string | null) => void;
57
- }) {
58
- if (projects.length === 0) return null;
59
- return (
60
- <div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
61
- <span className="stats-text-secondary" style={{ fontSize: "0.875rem", whiteSpace: "nowrap" }}>
62
- Project
63
- </span>
64
- <select
65
- className="stats-select"
66
- value={selected ?? ""}
67
- onChange={e => onChange(e.target.value || null)}
68
- style={{ maxWidth: "480px", flex: 1 }}
69
- >
70
- <option value="">All projects</option>
71
- {projects.map(p => (
72
- <option key={p} value={p}>
73
- {p}
74
- </option>
75
- ))}
76
- </select>
77
- </div>
78
- );
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Overall metrics panel
83
- // ---------------------------------------------------------------------------
31
+ /** The server buckets gain by UTC calendar day (`YYYY-MM-DD`). */
32
+ const DAY_LABEL = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", timeZone: "UTC" });
84
33
 
85
- function GainOverallPanel({ overall }: { overall: GainSourceTotals }) {
86
- return (
87
- <Panel title="Overall Gain" subtitle="Aggregate snapcompact savings">
88
- <div className="stats-metric-primary-grid">
89
- <div className="stats-metric-card primary">
90
- <div className="stats-metric-label">Saved Tokens</div>
91
- <div className="stats-metric-value">{formatCompact(overall.savedTokens)}</div>
92
- </div>
93
- <div className="stats-metric-card primary">
94
- <div className="stats-metric-label">Saved Bytes</div>
95
- <div className="stats-metric-value">{formatBytes(overall.savedBytes)}</div>
96
- </div>
97
- <div className="stats-metric-card primary">
98
- <div className="stats-metric-label">Reduction</div>
99
- <div className="stats-metric-value">
100
- {overall.reductionPercent !== null ? formatPercent(overall.reductionPercent) : "—"}
101
- </div>
102
- </div>
103
- <div className="stats-metric-card primary">
104
- <div className="stats-metric-label">Total Hits</div>
105
- <div className="stats-metric-value">{formatInteger(overall.hits)}</div>
106
- </div>
107
- </div>
108
- </Panel>
109
- );
34
+ interface SourceRow extends GainSourceTotals {
35
+ source: GainSource;
36
+ /** Share of all saved tokens (0-1). */
37
+ share: number;
110
38
  }
111
39
 
112
- // ---------------------------------------------------------------------------
113
- // By-source breakdown panel
114
- // ---------------------------------------------------------------------------
40
+ export function GainRoute({ active, range }: GainRouteProps) {
41
+ const [project, setProject] = useState<string | null>(null);
42
+ const gain = useQuery(["gain", range, project], () => getGainDashboardStats(range, project), { enabled: active });
43
+ const meta = rangeMeta(range);
44
+ const data = gain.data;
45
+
46
+ const series = useMemo(() => {
47
+ const points = (data?.timeSeries ?? []).map(p => ({ ...p, timestamp: Date.parse(`${p.date}T00:00:00Z`) }));
48
+ const buckets = bucketAxis(
49
+ range,
50
+ points.map(p => p.timestamp),
51
+ DAY_MS,
52
+ );
53
+ const daily = densify(points, buckets, p => p.snapcompact);
54
+ let running = 0;
55
+ const cumulative = daily.map(v => (running += v));
56
+ return { buckets, daily, cumulative };
57
+ }, [data, range]);
58
+
59
+ const sourceRows = useMemo((): SourceRow[] => {
60
+ if (!data) return [];
61
+ const total = data.overall.savedTokens;
62
+ return (Object.keys(data.bySource) as GainSource[]).map(source => ({
63
+ ...data.bySource[source],
64
+ source,
65
+ share: total > 0 ? data.bySource[source].savedTokens / total : 0,
66
+ }));
67
+ }, [data]);
68
+
69
+ const projects = data?.projects ?? [];
70
+ // Keep the chosen project selectable even when the current range never saw it.
71
+ const projectOptions = project !== null && !projects.includes(project) ? [project, ...projects] : projects;
72
+ const scope = project ? ` for ${project}` : "";
73
+
74
+ const chartSeries: ChartSeries[] = [
75
+ { key: "daily", label: "Saved per day", color: "var(--chart-primary)", values: series.daily },
76
+ {
77
+ key: "cumulative",
78
+ label: "Cumulative",
79
+ color: "var(--chart-secondary)",
80
+ values: series.cumulative,
81
+ kind: "line",
82
+ axis: "right",
83
+ },
84
+ ];
115
85
 
116
- function SourceCard({ title, totals }: { title: string; totals: GainSourceTotals }) {
117
86
  return (
118
- <div className="stats-metric-card secondary" style={{ flex: 1 }}>
119
- <div className="stats-metric-label" style={{ fontWeight: 600, marginBottom: 8 }}>
120
- {title}
121
- </div>
122
- <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
123
- <div>
124
- <div className="stats-metric-label">Saved Tokens</div>
125
- <div className="stats-metric-value" style={{ fontSize: "1rem" }}>
126
- {formatCompact(totals.savedTokens)}
127
- </div>
128
- </div>
129
- <div>
130
- <div className="stats-metric-label">Saved Bytes</div>
131
- <div className="stats-metric-value" style={{ fontSize: "1rem" }}>
132
- {formatBytes(totals.savedBytes)}
133
- </div>
134
- </div>
135
- <div>
136
- <div className="stats-metric-label">Hits</div>
137
- <div className="stats-metric-value" style={{ fontSize: "1rem" }}>
138
- {formatInteger(totals.hits)}
139
- </div>
140
- </div>
141
- <div>
142
- <div className="stats-metric-label">Reduction</div>
143
- <div className="stats-metric-value" style={{ fontSize: "1rem" }}>
144
- {totals.reductionPercent !== null ? formatPercent(totals.reductionPercent) : "—"}
145
- </div>
146
- </div>
147
- </div>
87
+ <div className="page">
88
+ <PageHeader
89
+ title="Gain"
90
+ description={`Tokens snapcompact kept out of context${scope} in ${meta.windowLabel}.`}
91
+ actions={
92
+ projectOptions.length > 0 && (
93
+ <select
94
+ className="input"
95
+ aria-label="Project"
96
+ value={project ?? ""}
97
+ onChange={e => setProject(e.target.value || null)}
98
+ style={{ maxWidth: 320 }}
99
+ >
100
+ <option value="">All projects</option>
101
+ {projectOptions.map(p => (
102
+ <option key={p} value={p}>
103
+ {p}
104
+ </option>
105
+ ))}
106
+ </select>
107
+ )
108
+ }
109
+ />
110
+
111
+ <QueryView
112
+ query={gain}
113
+ skeleton={<ChartSkeleton height={96} />}
114
+ isEmpty={stats => stats.overall.hits === 0 && stats.timeSeries.length === 0}
115
+ empty={
116
+ <Card>
117
+ <EmptyState
118
+ title={`No savings recorded${scope} in ${meta.windowLabel}`}
119
+ hint={
120
+ range === "all"
121
+ ? "Savings appear here once snapcompact compacts tool output."
122
+ : "Try a longer range."
123
+ }
124
+ />
125
+ </Card>
126
+ }
127
+ >
128
+ {({ overall }) => (
129
+ <>
130
+ <div data-stale={gain.stale}>
131
+ <StatGrid min={180}>
132
+ <Stat
133
+ label="Saved tokens"
134
+ value={formatCompact(overall.savedTokens)}
135
+ hint={formatInteger(overall.savedTokens)}
136
+ spark={series.daily}
137
+ />
138
+ <Stat label="Saved bytes" value={formatBytes(overall.savedBytes)} />
139
+ <Stat
140
+ label="Reduction"
141
+ title="Saved bytes ÷ original bytes, when the original size is known"
142
+ value={overall.reductionPercent !== null ? formatPercent(overall.reductionPercent) : "–"}
143
+ hint={overall.reductionPercent === null ? "original size not recorded" : undefined}
144
+ />
145
+ <Stat label="Hits" value={formatInteger(overall.hits)} />
146
+ <Stat
147
+ label="Saved per hit"
148
+ value={overall.hits > 0 ? formatCompact(overall.savedTokens / overall.hits) : "–"}
149
+ hint="tokens"
150
+ />
151
+ </StatGrid>
152
+ </div>
153
+
154
+ <Card
155
+ index={1}
156
+ title="Savings over time"
157
+ description="Tokens saved per UTC day, with the running total"
158
+ actions={<Legend items={chartSeries.map(s => ({ key: s.key, label: s.label, color: s.color }))} />}
159
+ stale={gain.stale}
160
+ >
161
+ <Chart
162
+ slots={series.buckets.length}
163
+ tickLabel={i => DAY_LABEL.format(series.buckets[i])}
164
+ series={chartSeries}
165
+ height={260}
166
+ formatTooltip={formatInteger}
167
+ formatRight={formatCompact}
168
+ />
169
+ </Card>
170
+
171
+ <Card index={2} title="By source" description="Savings per subsystem" flush stale={gain.stale}>
172
+ <Table rows={sourceRows} rowKey={row => row.source} columns={SOURCE_COLUMNS} />
173
+ </Card>
174
+ </>
175
+ )}
176
+ </QueryView>
148
177
  </div>
149
178
  );
150
179
  }
151
180
 
152
- function GainBySourcePanel({ bySource }: { bySource: GainDashboardStats["bySource"] }) {
153
- return (
154
- <Panel title="By Source" subtitle="Savings breakdown per subsystem">
155
- <div style={{ display: "flex", gap: 16, flexWrap: "wrap" }}>
156
- <SourceCard title="Snapcompact" totals={bySource.snapcompact} />
157
- </div>
158
- </Panel>
159
- );
160
- }
161
-
162
- // ---------------------------------------------------------------------------
163
- // Time series chart (stacked area, daily)
164
- // ---------------------------------------------------------------------------
165
-
166
- const GAIN_COLORS = {
167
- snapcompact: "rgb(34, 197, 94)",
168
- } as const;
169
-
170
- function GainTimeSeriesPanel({ timeSeries }: { timeSeries: GainTimeSeriesPoint[] }) {
171
- const theme = useSystemTheme();
172
- const chartTheme = CHART_THEMES[theme];
173
-
174
- const { data, options } = useMemo(() => {
175
- const labelFormatter = new Intl.DateTimeFormat(undefined, {
176
- month: "short",
177
- day: "numeric",
178
- timeZone: "UTC",
179
- });
180
- const labels = timeSeries.map(p => labelFormatter.format(new Date(`${p.date}T00:00:00.000Z`)));
181
- const chartData = {
182
- labels,
183
- datasets: [
184
- {
185
- label: "Snapcompact",
186
- data: timeSeries.map(p => p.snapcompact),
187
- ...lineDatasetStyle(GAIN_COLORS.snapcompact),
188
- },
189
- ],
190
- };
191
-
192
- const { sharedScaleBase, yScale } = buildSharedScales({
193
- chartTheme,
194
- formatY: n => formatCompact(n),
195
- });
196
-
197
- const chartOptions = {
198
- responsive: true,
199
- maintainAspectRatio: false,
200
- plugins: buildSharedPlugins({
201
- chartTheme,
202
- showLegend: true,
203
- defaultLabel: "Tokens Saved",
204
- formatValue: formatCompact,
205
- }),
206
- scales: {
207
- x: { ...sharedScaleBase, stacked: true },
208
- y: { ...yScale, stacked: true },
209
- },
210
- };
211
-
212
- return { data: chartData, options: chartOptions };
213
- }, [timeSeries, chartTheme]);
214
-
215
- return (
216
- <Panel title="Savings Over Time" subtitle="Daily token savings">
217
- <div style={{ height: 240 }}>
218
- {timeSeries.length === 0 ? (
219
- <div className="stats-table-empty">No time series data yet</div>
220
- ) : (
221
- <Line data={data} options={options as Parameters<typeof Line>[0]["options"]} />
222
- )}
223
- </div>
224
- </Panel>
225
- );
226
- }
181
+ const SOURCE_COLUMNS: Column<SourceRow>[] = [
182
+ {
183
+ key: "source",
184
+ header: "Source",
185
+ sort: row => row.source,
186
+ render: row => <span className="cell-primary">{SOURCE_LABEL[row.source]}</span>,
187
+ },
188
+ {
189
+ key: "tokens",
190
+ header: "Saved tokens",
191
+ align: "right",
192
+ sort: row => row.savedTokens,
193
+ render: row => (
194
+ <span title={formatInteger(row.savedTokens)}>
195
+ <MeterCell value={row.share} max={1} display={formatCompact(row.savedTokens)} />
196
+ </span>
197
+ ),
198
+ },
199
+ {
200
+ key: "share",
201
+ header: "Share",
202
+ align: "right",
203
+ sort: row => row.share,
204
+ render: row => <span className="num muted">{formatPercent(row.share)}</span>,
205
+ },
206
+ {
207
+ key: "bytes",
208
+ header: "Saved bytes",
209
+ align: "right",
210
+ sort: row => row.savedBytes,
211
+ render: row => <span className="num">{formatBytes(row.savedBytes)}</span>,
212
+ },
213
+ {
214
+ key: "hits",
215
+ header: "Hits",
216
+ align: "right",
217
+ sort: row => row.hits,
218
+ render: row => <span className="num">{formatInteger(row.hits)}</span>,
219
+ },
220
+ {
221
+ key: "reduction",
222
+ header: "Reduction",
223
+ align: "right",
224
+ sort: row => row.reductionPercent ?? -1,
225
+ render: row => (
226
+ <span className="num">{row.reductionPercent !== null ? formatPercent(row.reductionPercent) : "–"}</span>
227
+ ),
228
+ },
229
+ ];