@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,255 +0,0 @@
1
- /**
2
- * Shared primitives for the per-model breakdown tables. Each table owns its
3
- * column definitions, sort order, and chart type — this module owns the
4
- * surface chrome, expand-row plumbing, theme palette, the mini-sparkline, and
5
- * the shared plugin/scale config consumed by multi-line detail charts.
6
- */
7
-
8
- import { format } from "@oh-my-pi/pi-utils/dates";
9
- import { ChevronDown, ChevronUp } from "lucide-react";
10
- import { Line } from "react-chartjs-2";
11
- import type { ChartTheme } from "./chart-shared";
12
-
13
- // Detail-table charts share the exact OMP chart chrome as the timeline charts;
14
- // re-export rather than duplicate so the palette has a single source of truth.
15
- export { CHART_THEMES as TABLE_CHART_THEMES, MODEL_COLORS } from "./chart-shared";
16
- export type TableChartTheme = ChartTheme;
17
-
18
- /** Style defaults for one line in a non-stacked detail chart. */
19
- export function lineSeriesStyle(color: string) {
20
- return {
21
- borderColor: color,
22
- backgroundColor: "transparent",
23
- tension: 0.4,
24
- pointRadius: 0,
25
- borderWidth: 2,
26
- };
27
- }
28
-
29
- /**
30
- * No-axis, no-legend single-series sparkline used in the trend cell of every
31
- * model row. Caller supplies the already-extracted numeric series so this
32
- * stays agnostic of the row's underlying data shape.
33
- */
34
- export function MiniSparkline({
35
- timestamps,
36
- values,
37
- color,
38
- }: {
39
- timestamps: number[];
40
- values: number[];
41
- color: string;
42
- }) {
43
- const chartData = {
44
- labels: timestamps.map(ts => format(new Date(ts), "MMM d")),
45
- datasets: [{ data: values, ...lineSeriesStyle(color) }],
46
- };
47
-
48
- const options = {
49
- responsive: true,
50
- maintainAspectRatio: false,
51
- plugins: { legend: { display: false }, tooltip: { enabled: false } },
52
- scales: {
53
- x: { display: false },
54
- y: { display: false, min: 0 },
55
- },
56
- };
57
-
58
- return <Line data={chartData} options={options} />;
59
- }
60
-
61
- /**
62
- * Plugin block (legend + tooltip) shared by every multi-series detail chart
63
- * in the table expanded views.
64
- */
65
- export function detailChartPlugins(chartTheme: TableChartTheme) {
66
- return {
67
- legend: {
68
- display: true,
69
- position: "top" as const,
70
- labels: {
71
- color: chartTheme.legendLabel,
72
- usePointStyle: true,
73
- padding: 16,
74
- font: { size: 12 },
75
- },
76
- },
77
- tooltip: {
78
- backgroundColor: chartTheme.tooltipBackground,
79
- titleColor: chartTheme.tooltipTitle,
80
- bodyColor: chartTheme.tooltipBody,
81
- borderColor: chartTheme.tooltipBorder,
82
- borderWidth: 1,
83
- cornerRadius: 8,
84
- },
85
- };
86
- }
87
-
88
- /**
89
- * Single-Y-axis scales for a detail chart (used when every series shares a
90
- * unit, e.g. behavior counts). Min anchored at 0.
91
- */
92
- export function detailChartScalesSingleAxis(chartTheme: TableChartTheme) {
93
- return {
94
- x: {
95
- grid: { color: chartTheme.grid },
96
- ticks: { color: chartTheme.tick, font: { size: 11 } },
97
- },
98
- y: {
99
- grid: { color: chartTheme.grid },
100
- ticks: { color: chartTheme.tick, font: { size: 11 } },
101
- min: 0,
102
- },
103
- };
104
- }
105
-
106
- /**
107
- * Dual-Y-axis scales for a detail chart with mixed units (e.g. TTFT seconds
108
- * on left, tokens/s on right). Right-axis grid is suppressed so it doesn't
109
- * collide with the left.
110
- */
111
- export function detailChartScalesDualAxis(chartTheme: TableChartTheme) {
112
- return {
113
- x: {
114
- grid: { color: chartTheme.grid },
115
- ticks: { color: chartTheme.tick, font: { size: 11 } },
116
- },
117
- y: {
118
- type: "linear" as const,
119
- display: true,
120
- position: "left" as const,
121
- grid: { color: chartTheme.grid },
122
- ticks: { color: chartTheme.tick, font: { size: 11 } },
123
- },
124
- y1: {
125
- type: "linear" as const,
126
- display: true,
127
- position: "right" as const,
128
- grid: { drawOnChartArea: false },
129
- ticks: { color: chartTheme.tick, font: { size: 11 } },
130
- },
131
- };
132
- }
133
-
134
- export interface TableColumn {
135
- label: string;
136
- align?: "left" | "right" | "center";
137
- }
138
-
139
- /** Outer card + section title used by every model table. */
140
- export function ModelTableShell({
141
- title,
142
- subtitle,
143
- children,
144
- }: {
145
- title: string;
146
- subtitle?: string;
147
- children: React.ReactNode;
148
- }) {
149
- return (
150
- <div className="stats-panel overflow-hidden">
151
- <div className="px-5 py-4 border-b border-[var(--border-subtle)]">
152
- <h3 className="text-sm font-semibold text-[var(--text-primary)]">{title}</h3>
153
- {subtitle ? <p className="text-xs text-[var(--text-muted)] mt-1">{subtitle}</p> : null}
154
- </div>
155
- <div className="overflow-x-auto">{children}</div>
156
- </div>
157
- );
158
- }
159
-
160
- function alignClass(align: TableColumn["align"]): string {
161
- if (align === "right") return "text-right";
162
- if (align === "center") return "text-center";
163
- return "";
164
- }
165
-
166
- /** Sticky column-header row for a model table. */
167
- export function ModelTableHeader({ columns, gridTemplate }: { columns: TableColumn[]; gridTemplate: string }) {
168
- return (
169
- <div
170
- className="grid gap-3 px-5 py-3 text-[var(--text-muted)] text-xs uppercase tracking-wider font-semibold"
171
- style={{ gridTemplateColumns: gridTemplate }}
172
- >
173
- {columns.map(col => (
174
- <div key={col.label} className={alignClass(col.align)}>
175
- {col.label}
176
- </div>
177
- ))}
178
- {/* trailing chevron column has no header label */}
179
- <div />
180
- </div>
181
- );
182
- }
183
-
184
- /** Scroll wrapper for the row stack — capped to fit the dashboard viewport. */
185
- export function ModelTableBody({ children }: { children: React.ReactNode }) {
186
- return <div className="max-h-[calc(100vh-300px)] overflow-y-auto">{children}</div>;
187
- }
188
-
189
- /**
190
- * Two-line model identity cell (model name + provider) shared by every
191
- * per-model table. Kept as a stable named contract so callers don't restate
192
- * the same two divs and font-utility classes.
193
- */
194
- export function ModelNameCell({ model, provider }: { model: string; provider: string }) {
195
- return (
196
- <div>
197
- <div className="font-medium text-[var(--text-primary)]">{model}</div>
198
- <div className="text-xs text-[var(--text-muted)]">{provider}</div>
199
- </div>
200
- );
201
- }
202
-
203
- /**
204
- * One expandable model row. `cells` matches the column order from
205
- * `ModelTableHeader` plus the trend cell at the end (caller controls the
206
- * sparkline / placeholder). `expandedContent` is the panel revealed on toggle.
207
- */
208
- export function ExpandableModelRow({
209
- gridTemplate,
210
- cells,
211
- trendCell,
212
- isExpanded,
213
- onToggle,
214
- expandedContent,
215
- }: {
216
- gridTemplate: string;
217
- cells: React.ReactNode[];
218
- trendCell: React.ReactNode;
219
- isExpanded: boolean;
220
- onToggle: () => void;
221
- expandedContent: React.ReactNode;
222
- }) {
223
- return (
224
- <div className="border-t border-[var(--border-subtle)]">
225
- <button
226
- type="button"
227
- onClick={onToggle}
228
- className="w-full bg-transparent border-none text-left px-5 py-3 cursor-pointer hover:bg-[var(--bg-hover)] transition-colors"
229
- >
230
- <div className="grid gap-3 items-center" style={{ gridTemplateColumns: gridTemplate }}>
231
- {cells}
232
- <div className="h-10">{trendCell}</div>
233
- <div className="flex justify-center text-[var(--text-muted)]">
234
- {isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
235
- </div>
236
- </div>
237
- </button>
238
- {isExpanded ? (
239
- <div className="px-5 py-4 bg-[var(--bg-elevated)] border-t border-[var(--border-subtle)]">
240
- {expandedContent}
241
- </div>
242
- ) : null}
243
- </div>
244
- );
245
- }
246
-
247
- /** Placeholder shown in the trend cell when a model has no time-series data. */
248
- export function TrendEmpty() {
249
- return <div className="text-[var(--text-muted)] text-center text-sm">-</div>;
250
- }
251
-
252
- /** Placeholder shown in the expanded detail-chart slot when data is missing. */
253
- export function DetailChartEmpty({ message = "No data available" }: { message?: string }) {
254
- return <div className="h-full flex items-center justify-center text-[var(--text-muted)] text-sm">{message}</div>;
255
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Display metadata for a `TimeRange` — keeps chart labels, sparkline bucket
3
- * counts, and x-axis date formatting in sync with the server-side bucketing
4
- * defined in `aggregator.ts`.
5
- */
6
-
7
- import { format } from "@oh-my-pi/pi-utils/dates";
8
- import type { TimeRange } from "../types";
9
-
10
- const HOUR_MS = 60 * 60 * 1000;
11
- const DAY_MS = 24 * HOUR_MS;
12
- const FIVE_MIN_MS = 5 * 60 * 1000;
13
-
14
- export interface RangeMeta {
15
- /** Human label used in chart subtitles ("the last 24 hours"). */
16
- windowLabel: string;
17
- /** Short prefix used in compact column headers ("24h Trend"). */
18
- trendLabel: string;
19
- /** Bucket size matching the server query for this range. */
20
- bucketMs: number;
21
- /** Number of buckets the server is expected to return for this range. */
22
- bucketCount: number;
23
- /** Date format string for x-axis labels and tooltip headings. */
24
- tickFormat: string;
25
- }
26
-
27
- const RANGE_META: Record<TimeRange, RangeMeta> = {
28
- "1h": {
29
- windowLabel: "the last hour",
30
- trendLabel: "1h Trend",
31
- bucketMs: FIVE_MIN_MS,
32
- bucketCount: 12,
33
- tickFormat: "HH:mm",
34
- },
35
- "24h": {
36
- windowLabel: "the last 24 hours",
37
- trendLabel: "24h Trend",
38
- bucketMs: HOUR_MS,
39
- bucketCount: 24,
40
- tickFormat: "HH:mm",
41
- },
42
- "7d": {
43
- windowLabel: "the last 7 days",
44
- trendLabel: "7d Trend",
45
- bucketMs: DAY_MS,
46
- bucketCount: 7,
47
- tickFormat: "MMM d",
48
- },
49
- "30d": {
50
- windowLabel: "the last 30 days",
51
- trendLabel: "30d Trend",
52
- bucketMs: DAY_MS,
53
- bucketCount: 30,
54
- tickFormat: "MMM d",
55
- },
56
- "90d": {
57
- windowLabel: "the last 90 days",
58
- trendLabel: "90d Trend",
59
- bucketMs: DAY_MS,
60
- bucketCount: 90,
61
- tickFormat: "MMM d",
62
- },
63
- all: { windowLabel: "all time", trendLabel: "Trend", bucketMs: DAY_MS, bucketCount: 0, tickFormat: "MMM d" },
64
- };
65
-
66
- export function rangeMeta(range: TimeRange): RangeMeta {
67
- return RANGE_META[range];
68
- }
69
-
70
- /** Format a bucket timestamp using the active range's tick format. */
71
- export function formatRangeTick(timestamp: number, range: TimeRange): string {
72
- return format(new Date(timestamp), RANGE_META[range].tickFormat);
73
- }
@@ -1,14 +0,0 @@
1
- import {
2
- BarElement,
3
- CategoryScale,
4
- Chart as ChartJS,
5
- Filler,
6
- Legend,
7
- LinearScale,
8
- LineElement,
9
- PointElement,
10
- Title,
11
- Tooltip,
12
- } from "chart.js";
13
-
14
- ChartJS.register(CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend, Filler);
@@ -1,154 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
-
3
- export interface ResourceResult<T> {
4
- data: T | null;
5
- error: Error | null;
6
- loading: boolean;
7
- refreshing: boolean;
8
- refetch: () => Promise<void>;
9
- updatedAt: number | null;
10
- }
11
-
12
- export interface ResourceOptions {
13
- pollMs?: number;
14
- enabled?: boolean;
15
- }
16
-
17
- // Session-scoped cache keyed by the resource key. Lets navigation between
18
- // screens (and back to an already-visited range) render instantly from the
19
- // last value and refresh in the background, instead of unmounting to a
20
- // skeleton on every visit. Cleared only on full page reload.
21
- const resourceCache = new Map<string, { data: unknown; updatedAt: number }>();
22
- const RESOURCE_CACHE_LIMIT = 64;
23
-
24
- export function useResource<T>(
25
- key: readonly unknown[],
26
- fetcher: (signal: AbortSignal) => Promise<T>,
27
- options?: ResourceOptions,
28
- ): ResourceResult<T> {
29
- const keyString = JSON.stringify(key);
30
-
31
- const [data, setData] = useState<T | null>(() => (resourceCache.get(keyString)?.data as T | undefined) ?? null);
32
- const [error, setError] = useState<Error | null>(null);
33
- const [loading, setLoading] = useState(() => !resourceCache.has(keyString));
34
- const [refreshing, setRefreshing] = useState(false);
35
- const [updatedAt, setUpdatedAt] = useState<number | null>(() => resourceCache.get(keyString)?.updatedAt ?? null);
36
-
37
- const fetcherRef = useRef(fetcher);
38
- fetcherRef.current = fetcher;
39
- const keyStringRef = useRef(keyString);
40
- keyStringRef.current = keyString;
41
-
42
- const enabled = options?.enabled ?? true;
43
- const pollMs = options?.pollMs;
44
-
45
- const controllerRef = useRef<AbortController | null>(null);
46
-
47
- // Track whether we already hold data so a key change refreshes in the
48
- // background — keeping the prior view mounted so charts animate to the new
49
- // data instead of flashing a skeleton.
50
- const hasDataRef = useRef(false);
51
- hasDataRef.current = data !== null;
52
-
53
- const executeFetch = useCallback(async (isBackground: boolean) => {
54
- if (controllerRef.current) {
55
- controllerRef.current.abort();
56
- }
57
-
58
- const controller = new AbortController();
59
- controllerRef.current = controller;
60
-
61
- if (isBackground) {
62
- setRefreshing(true);
63
- } else {
64
- setLoading(true);
65
- setData(null);
66
- }
67
- setError(null);
68
-
69
- try {
70
- const result = await fetcherRef.current(controller.signal);
71
- if (controller.signal.aborted) {
72
- return;
73
- }
74
- resourceCache.set(keyStringRef.current, { data: result, updatedAt: Date.now() });
75
- if (resourceCache.size > RESOURCE_CACHE_LIMIT) {
76
- const oldestKey = resourceCache.keys().next().value;
77
- if (oldestKey !== undefined) resourceCache.delete(oldestKey);
78
- }
79
- setData(result);
80
- setUpdatedAt(Date.now());
81
- setError(null);
82
- } catch (err) {
83
- if (controller.signal.aborted) {
84
- return;
85
- }
86
- setError(err instanceof Error ? err : new Error(String(err)));
87
- } finally {
88
- if (!controller.signal.aborted) {
89
- setLoading(false);
90
- setRefreshing(false);
91
- if (controllerRef.current === controller) {
92
- controllerRef.current = null;
93
- }
94
- }
95
- }
96
- }, []);
97
-
98
- useEffect(() => {
99
- if (!enabled) {
100
- setLoading(false);
101
- setRefreshing(false);
102
- return;
103
- }
104
-
105
- const cached = resourceCache.get(keyString);
106
- if (cached) {
107
- // Show the cached value immediately, then revalidate in the background.
108
- setData(cached.data as T);
109
- setUpdatedAt(cached.updatedAt);
110
- setLoading(false);
111
- executeFetch(true);
112
- } else {
113
- // No cache: keep any stale data (range morph) or show a skeleton (first load).
114
- executeFetch(hasDataRef.current);
115
- }
116
-
117
- return () => {
118
- if (controllerRef.current) {
119
- controllerRef.current.abort();
120
- controllerRef.current = null;
121
- }
122
- };
123
- }, [keyString, enabled, executeFetch]);
124
-
125
- useEffect(() => {
126
- if (!enabled || !pollMs) {
127
- return;
128
- }
129
-
130
- const interval = setInterval(() => {
131
- if (document.hidden) {
132
- return;
133
- }
134
- void executeFetch(true);
135
- }, pollMs);
136
-
137
- return () => {
138
- clearInterval(interval);
139
- };
140
- }, [enabled, pollMs, executeFetch]);
141
-
142
- const refetch = useCallback(async () => {
143
- await executeFetch(true);
144
- }, [executeFetch]);
145
-
146
- return {
147
- data,
148
- error,
149
- loading,
150
- refreshing,
151
- refetch,
152
- updatedAt,
153
- };
154
- }