@oh-my-pi/omp-stats 18.3.5 → 18.4.1
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 +22 -0
- package/README.md +13 -11
- package/build.ts +5 -49
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +105 -126
- package/dist/types/aggregator.d.ts +25 -22
- package/dist/types/client/api.d.ts +11 -7
- package/dist/types/client/app/LiveChip.d.ts +2 -0
- package/dist/types/client/app/Shell.d.ts +16 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/nav.d.ts +18 -0
- package/dist/types/client/charts/BarList.d.ts +17 -0
- package/dist/types/client/charts/Chart.d.ts +49 -0
- package/dist/types/client/charts/Legend.d.ts +16 -0
- package/dist/types/client/charts/ShareBar.d.ts +11 -0
- package/dist/types/client/charts/Sparkline.d.ts +11 -0
- package/dist/types/client/charts/TimeChart.d.ts +9 -0
- package/dist/types/client/charts/index.d.ts +7 -0
- package/dist/types/client/charts/types.d.ts +25 -0
- package/dist/types/client/charts/useWidth.d.ts +3 -0
- package/dist/types/client/data/colors.d.ts +25 -0
- package/dist/types/client/data/formatters.d.ts +9 -1
- package/dist/types/client/data/live.d.ts +29 -0
- package/dist/types/client/data/query.d.ts +35 -0
- package/dist/types/client/data/range.d.ts +31 -0
- package/dist/types/client/data/series.d.ts +28 -0
- package/dist/types/client/data/useHashRoute.d.ts +18 -4
- package/dist/types/client/data/view-models.d.ts +111 -28
- package/dist/types/client/index.d.ts +0 -1
- package/dist/types/client/routes/CostsRoute.d.ts +2 -2
- package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
- package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
- package/dist/types/client/routes/GainRoute.d.ts +1 -2
- package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
- package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
- package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
- package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
- package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
- package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
- package/dist/types/client/routes/TracesRoute.d.ts +2 -2
- package/dist/types/client/routes/index.d.ts +1 -1
- package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
- package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
- package/dist/types/client/traces/trace-colors.d.ts +18 -12
- package/dist/types/client/ui/Badge.d.ts +19 -0
- package/dist/types/client/ui/Card.d.ts +25 -0
- package/dist/types/client/ui/Drawer.d.ts +25 -0
- package/dist/types/client/ui/JsonBlock.d.ts +2 -2
- package/dist/types/client/ui/Modal.d.ts +23 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
- package/dist/types/client/ui/SearchInput.d.ts +8 -0
- package/dist/types/client/ui/Segmented.d.ts +15 -0
- package/dist/types/client/ui/Stat.d.ts +36 -0
- package/dist/types/client/ui/States.d.ts +41 -0
- package/dist/types/client/ui/Table.d.ts +48 -0
- package/dist/types/client/ui/index.d.ts +9 -10
- package/dist/types/db.d.ts +90 -106
- package/dist/types/frustration.d.ts +70 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/live.d.ts +30 -0
- package/dist/types/rollup.d.ts +115 -0
- package/dist/types/server.d.ts +6 -1
- package/dist/types/shared-types.d.ts +116 -56
- package/dist/types/types.d.ts +4 -0
- package/dist/types/user-metrics.d.ts +8 -0
- package/package.json +7 -13
- package/src/aggregator.ts +276 -211
- package/src/client/App.tsx +38 -80
- package/src/client/api.ts +65 -7
- package/src/client/app/LiveChip.tsx +97 -0
- package/src/client/app/Shell.tsx +181 -0
- package/src/client/app/ThemeToggle.tsx +5 -2
- package/src/client/app/nav.ts +78 -0
- package/src/client/charts/BarList.tsx +48 -0
- package/src/client/charts/Chart.tsx +388 -0
- package/src/client/charts/Legend.tsx +61 -0
- package/src/client/charts/ShareBar.tsx +26 -0
- package/src/client/charts/Sparkline.tsx +38 -0
- package/src/client/charts/TimeChart.tsx +22 -0
- package/src/client/charts/index.ts +7 -0
- package/src/client/charts/types.ts +27 -0
- package/src/client/charts/useWidth.ts +19 -0
- package/src/client/data/colors.ts +45 -0
- package/src/client/data/formatters.ts +30 -9
- package/src/client/data/live.tsx +85 -0
- package/src/client/data/query.ts +166 -0
- package/src/client/data/range.ts +82 -0
- package/src/client/data/series.ts +85 -0
- package/src/client/data/useHashRoute.ts +41 -83
- package/src/client/data/view-models.ts +347 -140
- package/src/client/index.tsx +3 -6
- package/src/client/routes/CostsRoute.tsx +394 -222
- package/src/client/routes/ErrorsRoute.tsx +401 -98
- package/src/client/routes/FrustrationRoute.tsx +924 -0
- package/src/client/routes/GainRoute.tsx +209 -206
- package/src/client/routes/ModelsRoute.tsx +540 -431
- package/src/client/routes/OverviewRoute.tsx +283 -311
- package/src/client/routes/ProjectsRoute.tsx +298 -156
- package/src/client/routes/ProvidersRoute.tsx +948 -416
- package/src/client/routes/RequestsRoute.tsx +295 -107
- package/src/client/routes/ToolsRoute.tsx +495 -398
- package/src/client/routes/TracesRoute.tsx +136 -117
- package/src/client/routes/costs.css +24 -0
- package/src/client/routes/errors.css +60 -0
- package/src/client/routes/frustration.css +123 -0
- package/src/client/routes/index.ts +1 -1
- package/src/client/routes/models.css +43 -0
- package/src/client/routes/projects.css +6 -0
- package/src/client/routes/providers.css +28 -0
- package/src/client/routes/tools.css +14 -0
- package/src/client/styles.css +1396 -1299
- package/src/client/traces/AggregatesPanel.tsx +55 -74
- package/src/client/traces/Minimap.tsx +6 -15
- package/src/client/traces/SpanDrawer.tsx +128 -194
- package/src/client/traces/SummaryStrip.tsx +21 -24
- package/src/client/traces/TimelineCanvas.tsx +27 -44
- package/src/client/traces/TraceView.tsx +175 -130
- package/src/client/traces/TranscriptList.tsx +13 -41
- package/src/client/traces/trace-colors.ts +60 -53
- package/src/client/traces/traces.css +279 -0
- package/src/client/ui/Badge.tsx +29 -0
- package/src/client/ui/Card.tsx +61 -0
- package/src/client/ui/Drawer.tsx +93 -0
- package/src/client/ui/JsonBlock.tsx +26 -44
- package/src/client/ui/Modal.tsx +106 -0
- package/src/client/ui/RequestDrawer.tsx +163 -188
- package/src/client/ui/SearchInput.tsx +25 -0
- package/src/client/ui/Segmented.tsx +61 -0
- package/src/client/ui/Stat.tsx +104 -0
- package/src/client/ui/States.tsx +81 -0
- package/src/client/ui/Table.tsx +229 -0
- package/src/client/ui/index.ts +9 -10
- package/src/client/ui/modal.css +54 -0
- package/src/client/ui/request-drawer.css +40 -0
- package/src/db.ts +379 -983
- package/src/frustration.ts +473 -0
- package/src/index.ts +22 -22
- package/src/live.ts +259 -0
- package/src/parser.ts +55 -66
- package/src/rollup.ts +951 -0
- package/src/server.ts +143 -87
- package/src/shared-types.ts +120 -56
- package/src/trace.ts +14 -7
- package/src/types.ts +4 -0
- package/src/user-metrics.ts +13 -0
- package/dist/client/styles.css +0 -1976
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/src/client/app/AppLayout.tsx +0 -93
- package/src/client/app/NavRail.tsx +0 -44
- package/src/client/app/RangeControl.tsx +0 -39
- package/src/client/app/SyncButton.tsx +0 -75
- package/src/client/app/TopBar.tsx +0 -73
- package/src/client/app/routes.ts +0 -93
- package/src/client/components/AgentTokenShare.tsx +0 -68
- package/src/client/components/chart-shared.tsx +0 -273
- package/src/client/components/models-table-shared.tsx +0 -255
- package/src/client/components/range-meta.ts +0 -73
- package/src/client/data/charts.ts +0 -14
- package/src/client/data/useResource.ts +0 -154
- package/src/client/routes/BehaviorRoute.tsx +0 -623
- package/src/client/ui/AsyncBoundary.tsx +0 -54
- package/src/client/ui/DataTable.tsx +0 -122
- package/src/client/ui/EmptyState.tsx +0 -16
- package/src/client/ui/ErrorState.tsx +0 -25
- package/src/client/ui/MetricCluster.tsx +0 -92
- package/src/client/ui/Panel.tsx +0 -24
- package/src/client/ui/SegmentedControl.tsx +0 -36
- package/src/client/ui/Skeleton.tsx +0 -17
- package/src/client/ui/StatusPill.tsx +0 -15
- 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
|
-
}
|