@oh-my-pi/omp-stats 16.0.4 → 16.0.6
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 +15 -0
- package/build.ts +11 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +11 -0
- package/dist/client/index.js +108 -108
- package/dist/client/styles.css +1070 -631
- package/dist/types/client/api.d.ts +19 -10
- package/dist/types/client/app/AppLayout.d.ts +16 -0
- package/dist/types/client/app/NavRail.d.ts +7 -0
- package/dist/types/client/app/RangeControl.d.ts +7 -0
- package/dist/types/client/app/SyncButton.d.ts +14 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/TopBar.d.ts +15 -0
- package/dist/types/client/app/routes.d.ts +12 -0
- package/dist/types/client/components/chart-shared.d.ts +26 -40
- package/dist/types/client/components/models-table-shared.d.ts +20 -40
- package/dist/types/client/data/charts.d.ts +1 -0
- package/dist/types/client/data/formatters.d.ts +7 -0
- package/dist/types/client/data/useHashRoute.d.ts +8 -0
- package/dist/types/client/data/useResource.d.ts +13 -0
- package/dist/types/client/data/view-models.d.ts +37 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
- package/dist/types/client/routes/CostsRoute.d.ts +7 -0
- package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
- package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
- package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
- package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
- package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
- package/dist/types/client/routes/index.d.ts +7 -0
- package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
- package/dist/types/client/ui/DataTable.d.ts +17 -0
- package/dist/types/client/ui/EmptyState.d.ts +7 -0
- package/dist/types/client/ui/ErrorState.d.ts +6 -0
- package/dist/types/client/ui/JsonBlock.d.ts +7 -0
- package/dist/types/client/ui/MetricCluster.d.ts +5 -0
- package/dist/types/client/ui/Panel.d.ts +7 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
- package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
- package/dist/types/client/ui/Skeleton.d.ts +8 -0
- package/dist/types/client/ui/StatusPill.d.ts +7 -0
- package/dist/types/client/ui/index.d.ts +11 -0
- package/dist/types/client/useSystemTheme.d.ts +9 -0
- package/package.json +4 -4
- package/src/aggregator.ts +4 -3
- package/src/client/App.tsx +89 -207
- package/src/client/api.ts +55 -37
- package/src/client/app/AppLayout.tsx +93 -0
- package/src/client/app/NavRail.tsx +44 -0
- package/src/client/app/RangeControl.tsx +39 -0
- package/src/client/app/SyncButton.tsx +75 -0
- package/src/client/app/ThemeToggle.tsx +37 -0
- package/src/client/app/TopBar.tsx +73 -0
- package/src/client/app/routes.ts +50 -0
- package/src/client/components/chart-shared.tsx +28 -91
- package/src/client/components/models-table-shared.tsx +9 -29
- package/src/client/components/range-meta.ts +3 -2
- package/src/client/data/charts.ts +14 -0
- package/src/client/data/formatters.ts +38 -0
- package/src/client/data/useHashRoute.ts +85 -0
- package/src/client/data/useResource.ts +154 -0
- package/src/client/data/view-models.ts +178 -0
- package/src/client/index.tsx +4 -0
- package/src/client/routes/BehaviorRoute.tsx +623 -0
- package/src/client/routes/CostsRoute.tsx +234 -0
- package/src/client/routes/ErrorsRoute.tsx +118 -0
- package/src/client/routes/ModelsRoute.tsx +430 -0
- package/src/client/routes/OverviewRoute.tsx +332 -0
- package/src/client/routes/ProjectsRoute.tsx +163 -0
- package/src/client/routes/RequestsRoute.tsx +123 -0
- package/src/client/routes/index.ts +7 -0
- package/src/client/styles.css +1242 -225
- package/src/client/ui/AsyncBoundary.tsx +54 -0
- package/src/client/ui/DataTable.tsx +122 -0
- package/src/client/ui/EmptyState.tsx +16 -0
- package/src/client/ui/ErrorState.tsx +25 -0
- package/src/client/ui/JsonBlock.tsx +75 -0
- package/src/client/ui/MetricCluster.tsx +67 -0
- package/src/client/ui/Panel.tsx +24 -0
- package/src/client/ui/RequestDrawer.tsx +208 -0
- package/src/client/ui/SegmentedControl.tsx +36 -0
- package/src/client/ui/Skeleton.tsx +17 -0
- package/src/client/ui/StatusPill.tsx +15 -0
- package/src/client/ui/index.ts +11 -0
- package/src/client/useSystemTheme.ts +73 -17
- package/dist/types/client/components/BehaviorChart.d.ts +0 -6
- package/dist/types/client/components/BehaviorModelsTable.d.ts +0 -7
- package/dist/types/client/components/BehaviorSummary.d.ts +0 -7
- package/dist/types/client/components/ChartsContainer.d.ts +0 -7
- package/dist/types/client/components/CostChart.d.ts +0 -6
- package/dist/types/client/components/CostSummary.d.ts +0 -6
- package/dist/types/client/components/Header.d.ts +0 -12
- package/dist/types/client/components/ModelsTable.d.ts +0 -8
- package/dist/types/client/components/RequestDetail.d.ts +0 -6
- package/dist/types/client/components/RequestList.d.ts +0 -8
- package/dist/types/client/components/StatsGrid.d.ts +0 -6
- package/src/client/components/BehaviorChart.tsx +0 -189
- package/src/client/components/BehaviorModelsTable.tsx +0 -342
- package/src/client/components/BehaviorSummary.tsx +0 -95
- package/src/client/components/ChartsContainer.tsx +0 -221
- package/src/client/components/CostChart.tsx +0 -171
- package/src/client/components/CostSummary.tsx +0 -53
- package/src/client/components/Header.tsx +0 -72
- package/src/client/components/ModelsTable.tsx +0 -265
- package/src/client/components/RequestDetail.tsx +0 -172
- package/src/client/components/RequestList.tsx +0 -73
- package/src/client/components/StatsGrid.tsx +0 -135
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
export type SystemTheme = "light" | "dark";
|
|
2
|
+
export type ThemePreference = "system" | "light" | "dark";
|
|
3
|
+
export declare function setThemePreference(next: ThemePreference): void;
|
|
4
|
+
/** Reader for the active resolved theme. Reflects system default and overrides. */
|
|
2
5
|
export declare function useSystemTheme(): SystemTheme;
|
|
6
|
+
/** Reader + writer for the theme preference (powers the toggle). */
|
|
7
|
+
export declare function useThemePreference(): {
|
|
8
|
+
preference: ThemePreference;
|
|
9
|
+
resolved: SystemTheme;
|
|
10
|
+
setPreference: (next: ThemePreference) => void;
|
|
11
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.6",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-ai": "16.0.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
40
|
+
"@oh-my-pi/pi-ai": "16.0.6",
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.0.6",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.0.6",
|
|
43
43
|
"@tailwindcss/node": "^4.3.0",
|
|
44
44
|
"chart.js": "^4.5.1",
|
|
45
45
|
"date-fns": "^4.4.0",
|
package/src/aggregator.ts
CHANGED
|
@@ -255,6 +255,7 @@ export async function syncAllSessions(opts?: SyncOptions): Promise<{ processed:
|
|
|
255
255
|
|
|
256
256
|
const HOUR_MS = 60 * 60 * 1000;
|
|
257
257
|
const DAY_MS = 24 * HOUR_MS;
|
|
258
|
+
const FIVE_MIN_MS = 5 * 60 * 1000;
|
|
258
259
|
|
|
259
260
|
type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
|
|
260
261
|
|
|
@@ -274,11 +275,11 @@ const DEFAULT_TIME_RANGE: TimeRange = "24h";
|
|
|
274
275
|
const TIME_RANGE_TO_CONFIG: Record<TimeRange, Omit<TimeRangeConfig, "cutoff">> = {
|
|
275
276
|
"1h": {
|
|
276
277
|
timeSeriesHours: 1,
|
|
277
|
-
timeSeriesBucketMs:
|
|
278
|
+
timeSeriesBucketMs: FIVE_MIN_MS,
|
|
278
279
|
modelSeriesDays: 1,
|
|
279
|
-
modelSeriesBucketMs:
|
|
280
|
+
modelSeriesBucketMs: FIVE_MIN_MS,
|
|
280
281
|
modelPerformanceDays: 1,
|
|
281
|
-
modelPerformanceBucketMs:
|
|
282
|
+
modelPerformanceBucketMs: FIVE_MIN_MS,
|
|
282
283
|
costSeriesDays: 1,
|
|
283
284
|
},
|
|
284
285
|
"24h": {
|
package/src/client/App.tsx
CHANGED
|
@@ -1,221 +1,103 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { AppLayout } from "./app/AppLayout";
|
|
3
|
+
import type { DashboardSection } from "./app/routes";
|
|
4
|
+
import { useHashRoute } from "./data/useHashRoute";
|
|
2
5
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "./
|
|
11
|
-
import {
|
|
12
|
-
import { BehaviorModelsTable } from "./components/BehaviorModelsTable";
|
|
13
|
-
import { BehaviorSummary } from "./components/BehaviorSummary";
|
|
14
|
-
import { ChartsContainer } from "./components/ChartsContainer";
|
|
15
|
-
import { CostChart } from "./components/CostChart";
|
|
16
|
-
import { CostSummary } from "./components/CostSummary";
|
|
17
|
-
import { Header } from "./components/Header";
|
|
18
|
-
import { ModelsTable } from "./components/ModelsTable";
|
|
19
|
-
import { RequestDetail } from "./components/RequestDetail";
|
|
20
|
-
import { RequestList } from "./components/RequestList";
|
|
21
|
-
import { StatsGrid } from "./components/StatsGrid";
|
|
22
|
-
import type {
|
|
23
|
-
BehaviorDashboardStats,
|
|
24
|
-
CostDashboardStats,
|
|
25
|
-
MessageStats,
|
|
26
|
-
ModelDashboardStats,
|
|
27
|
-
OverviewStats,
|
|
28
|
-
TimeRange,
|
|
29
|
-
} from "./types";
|
|
30
|
-
|
|
31
|
-
type Tab = "overview" | "requests" | "errors" | "models" | "costs" | "behavior";
|
|
6
|
+
BehaviorRoute,
|
|
7
|
+
CostsRoute,
|
|
8
|
+
ErrorsRoute,
|
|
9
|
+
ModelsRoute,
|
|
10
|
+
OverviewRoute,
|
|
11
|
+
ProjectsRoute,
|
|
12
|
+
RequestsRoute,
|
|
13
|
+
} from "./routes";
|
|
14
|
+
import { RequestDrawer } from "./ui/RequestDrawer";
|
|
32
15
|
|
|
33
16
|
export default function App() {
|
|
34
|
-
const
|
|
35
|
-
const [
|
|
36
|
-
const [
|
|
37
|
-
const [
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const [timeRange, setTimeRange] = useState<TimeRange>("24h");
|
|
44
|
-
|
|
45
|
-
const loadRecentLists = useCallback(async () => {
|
|
46
|
-
try {
|
|
47
|
-
const [requests, errors] = await Promise.all([getRecentRequests(50), getRecentErrors(50)]);
|
|
48
|
-
setRecentRequests(requests);
|
|
49
|
-
setRecentErrors(errors);
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.error(err);
|
|
17
|
+
const { section, setSection, range, setRange } = useHashRoute();
|
|
18
|
+
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
19
|
+
const [selectedRequestId, setSelectedRequestId] = useState<number | null>(null);
|
|
20
|
+
const [updatedAt, setUpdatedAt] = useState<number | null>(() => Date.now());
|
|
21
|
+
|
|
22
|
+
const handleSyncComplete = useCallback((result: { success: boolean }) => {
|
|
23
|
+
if (result.success) {
|
|
24
|
+
setRefreshTrigger(prev => prev + 1);
|
|
25
|
+
setUpdatedAt(Date.now());
|
|
52
26
|
}
|
|
53
27
|
}, []);
|
|
54
28
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
29
|
+
// Stable identity so the drawer's effects don't tear down on every App render.
|
|
30
|
+
const closeDrawer = useCallback(() => setSelectedRequestId(null), []);
|
|
31
|
+
|
|
32
|
+
const active = section;
|
|
33
|
+
|
|
34
|
+
// Keep every visited section mounted and just toggle visibility. Remounting a
|
|
35
|
+
// route on each navigation replays the chart entry animations (a visible
|
|
36
|
+
// flicker); keeping it alive makes revisits instant while the live chart
|
|
37
|
+
// instances still animate in place on data/range updates. Only the active
|
|
38
|
+
// route fetches/polls (enabled), so hidden routes don't keep hitting the API.
|
|
39
|
+
const mountedRef = useRef<Set<DashboardSection>>(new Set());
|
|
40
|
+
mountedRef.current.add(active);
|
|
41
|
+
|
|
42
|
+
const renderRoute = (target: DashboardSection) => {
|
|
43
|
+
const isActive = target === active;
|
|
44
|
+
switch (target) {
|
|
45
|
+
case "overview":
|
|
46
|
+
return (
|
|
47
|
+
<OverviewRoute
|
|
48
|
+
active={isActive}
|
|
49
|
+
range={range}
|
|
50
|
+
refreshTrigger={refreshTrigger}
|
|
51
|
+
onRequestClick={setSelectedRequestId}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
case "requests":
|
|
55
|
+
return (
|
|
56
|
+
<RequestsRoute
|
|
57
|
+
active={isActive}
|
|
58
|
+
range={range}
|
|
59
|
+
refreshTrigger={refreshTrigger}
|
|
60
|
+
onRequestClick={setSelectedRequestId}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
case "errors":
|
|
64
|
+
return (
|
|
65
|
+
<ErrorsRoute
|
|
66
|
+
active={isActive}
|
|
67
|
+
range={range}
|
|
68
|
+
refreshTrigger={refreshTrigger}
|
|
69
|
+
onRequestClick={setSelectedRequestId}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
case "models":
|
|
73
|
+
return <ModelsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
74
|
+
case "costs":
|
|
75
|
+
return <CostsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
76
|
+
case "behavior":
|
|
77
|
+
return <BehaviorRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
78
|
+
case "projects":
|
|
79
|
+
return <ProjectsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
84
80
|
}
|
|
85
81
|
};
|
|
86
82
|
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
loadRecentLists();
|
|
89
|
-
const interval = setInterval(loadRecentLists, 30000);
|
|
90
|
-
return () => clearInterval(interval);
|
|
91
|
-
}, [loadRecentLists]);
|
|
92
|
-
|
|
93
|
-
useEffect(() => {
|
|
94
|
-
loadActiveTabStats();
|
|
95
|
-
const interval = setInterval(loadActiveTabStats, 30000);
|
|
96
|
-
return () => clearInterval(interval);
|
|
97
|
-
}, [loadActiveTabStats]);
|
|
98
|
-
|
|
99
83
|
return (
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<div className="space-y-6 animate-fade-in">
|
|
113
|
-
{overviewStats ? (
|
|
114
|
-
<StatsGrid stats={overviewStats.overall} />
|
|
115
|
-
) : (
|
|
116
|
-
<LoadingState label="Loading overview..." />
|
|
117
|
-
)}
|
|
118
|
-
|
|
119
|
-
<div className="grid lg:grid-cols-2 gap-6">
|
|
120
|
-
<RequestList
|
|
121
|
-
title="Recent Requests"
|
|
122
|
-
requests={recentRequests.slice(0, 10)}
|
|
123
|
-
onSelect={r => r.id && setSelectedRequest(r.id)}
|
|
124
|
-
/>
|
|
125
|
-
<RequestList
|
|
126
|
-
title="Recent Errors"
|
|
127
|
-
requests={recentErrors.slice(0, 10)}
|
|
128
|
-
onSelect={r => r.id && setSelectedRequest(r.id)}
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
)}
|
|
133
|
-
|
|
134
|
-
{activeTab === "requests" && (
|
|
135
|
-
<div className="h-[calc(100vh-140px)] animate-fade-in">
|
|
136
|
-
<RequestList
|
|
137
|
-
title="All Recent Requests"
|
|
138
|
-
requests={recentRequests}
|
|
139
|
-
onSelect={r => r.id && setSelectedRequest(r.id)}
|
|
140
|
-
/>
|
|
141
|
-
</div>
|
|
142
|
-
)}
|
|
143
|
-
|
|
144
|
-
{activeTab === "errors" && (
|
|
145
|
-
<div className="h-[calc(100vh-140px)] animate-fade-in">
|
|
146
|
-
<RequestList
|
|
147
|
-
title="Failed Requests"
|
|
148
|
-
requests={recentErrors}
|
|
149
|
-
onSelect={r => r.id && setSelectedRequest(r.id)}
|
|
150
|
-
/>
|
|
151
|
-
</div>
|
|
152
|
-
)}
|
|
153
|
-
|
|
154
|
-
{activeTab === "models" && (
|
|
155
|
-
<div className="space-y-6 animate-fade-in">
|
|
156
|
-
{modelStats ? (
|
|
157
|
-
<>
|
|
158
|
-
<ChartsContainer modelSeries={modelStats.modelSeries} timeRange={timeRange} />
|
|
159
|
-
<ModelsTable
|
|
160
|
-
models={modelStats.byModel}
|
|
161
|
-
performanceSeries={modelStats.modelPerformanceSeries}
|
|
162
|
-
timeRange={timeRange}
|
|
163
|
-
/>
|
|
164
|
-
</>
|
|
165
|
-
) : (
|
|
166
|
-
<LoadingState label="Loading models..." />
|
|
167
|
-
)}
|
|
84
|
+
<>
|
|
85
|
+
<AppLayout
|
|
86
|
+
activeSection={active}
|
|
87
|
+
onSectionChange={setSection}
|
|
88
|
+
range={range}
|
|
89
|
+
onRangeChange={setRange}
|
|
90
|
+
updatedAt={updatedAt}
|
|
91
|
+
onSyncComplete={handleSyncComplete}
|
|
92
|
+
>
|
|
93
|
+
{[...mountedRef.current].map(target => (
|
|
94
|
+
<div key={target} hidden={target !== active}>
|
|
95
|
+
{renderRoute(target)}
|
|
168
96
|
</div>
|
|
169
|
-
)}
|
|
97
|
+
))}
|
|
98
|
+
</AppLayout>
|
|
170
99
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{costStats ? (
|
|
174
|
-
<>
|
|
175
|
-
<CostSummary costSeries={costStats.costSeries} />
|
|
176
|
-
<CostChart costSeries={costStats.costSeries} />
|
|
177
|
-
</>
|
|
178
|
-
) : (
|
|
179
|
-
<LoadingState label="Loading costs..." />
|
|
180
|
-
)}
|
|
181
|
-
</div>
|
|
182
|
-
)}
|
|
183
|
-
|
|
184
|
-
{activeTab === "behavior" && (
|
|
185
|
-
<div className="space-y-6 animate-fade-in">
|
|
186
|
-
{behaviorStats ? (
|
|
187
|
-
<>
|
|
188
|
-
<BehaviorSummary
|
|
189
|
-
overall={behaviorStats.overall}
|
|
190
|
-
behaviorSeries={behaviorStats.behaviorSeries}
|
|
191
|
-
/>
|
|
192
|
-
<BehaviorChart behaviorSeries={behaviorStats.behaviorSeries} />
|
|
193
|
-
<BehaviorModelsTable
|
|
194
|
-
models={behaviorStats.byModel}
|
|
195
|
-
behaviorSeries={behaviorStats.behaviorSeries}
|
|
196
|
-
/>
|
|
197
|
-
</>
|
|
198
|
-
) : (
|
|
199
|
-
<LoadingState label="Loading behavior..." />
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
)}
|
|
203
|
-
|
|
204
|
-
{selectedRequest !== null && (
|
|
205
|
-
<RequestDetail id={selectedRequest} onClose={() => setSelectedRequest(null)} />
|
|
206
|
-
)}
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function LoadingState({ label }: { label: string }) {
|
|
213
|
-
return (
|
|
214
|
-
<div className="min-h-[180px] flex items-center justify-center">
|
|
215
|
-
<div className="flex items-center gap-3 text-[var(--text-muted)]">
|
|
216
|
-
<div className="w-5 h-5 border-2 border-[var(--border-default)] border-t-[var(--accent-cyan)] rounded-full spin" />
|
|
217
|
-
<span className="text-sm">{label}</span>
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
100
|
+
<RequestDrawer id={selectedRequestId} onClose={closeDrawer} />
|
|
101
|
+
</>
|
|
220
102
|
);
|
|
221
103
|
}
|
package/src/client/api.ts
CHANGED
|
@@ -1,65 +1,83 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BehaviorDashboardStats,
|
|
3
3
|
CostDashboardStats,
|
|
4
|
-
|
|
4
|
+
FolderStats,
|
|
5
5
|
MessageStats,
|
|
6
6
|
ModelDashboardStats,
|
|
7
7
|
OverviewStats,
|
|
8
8
|
RequestDetails,
|
|
9
|
+
TimeRange,
|
|
9
10
|
} from "./types";
|
|
10
11
|
|
|
11
12
|
const API_BASE = "/api";
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
export class ApiError extends Error {
|
|
15
|
+
status: number;
|
|
16
|
+
endpoint: string;
|
|
17
|
+
|
|
18
|
+
constructor(status: number, endpoint: string, message: string) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ApiError";
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.endpoint = endpoint;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fetchJson<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
|
27
|
+
const res = await fetch(endpoint, options);
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw new ApiError(res.status, endpoint, `HTTP error ${res.status} on ${endpoint}`);
|
|
30
|
+
}
|
|
31
|
+
return res.json() as Promise<T>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function getOverviewStats(range: TimeRange = "24h", signal?: AbortSignal): Promise<OverviewStats> {
|
|
35
|
+
return fetchJson<OverviewStats>(`${API_BASE}/stats/overview?range=${encodeURIComponent(range)}`, {
|
|
36
|
+
signal,
|
|
37
|
+
});
|
|
17
38
|
}
|
|
18
39
|
|
|
19
|
-
export async function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
export async function getModelDashboardStats(
|
|
41
|
+
range: TimeRange = "24h",
|
|
42
|
+
signal?: AbortSignal,
|
|
43
|
+
): Promise<ModelDashboardStats> {
|
|
44
|
+
return fetchJson<ModelDashboardStats>(`${API_BASE}/stats/model-dashboard?range=${encodeURIComponent(range)}`, {
|
|
45
|
+
signal,
|
|
46
|
+
});
|
|
23
47
|
}
|
|
24
48
|
|
|
25
|
-
export async function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
export async function getCostDashboardStats(
|
|
50
|
+
range: TimeRange = "24h",
|
|
51
|
+
signal?: AbortSignal,
|
|
52
|
+
): Promise<CostDashboardStats> {
|
|
53
|
+
return fetchJson<CostDashboardStats>(`${API_BASE}/stats/costs?range=${encodeURIComponent(range)}`, { signal });
|
|
29
54
|
}
|
|
30
55
|
|
|
31
|
-
export async function
|
|
32
|
-
|
|
33
|
-
if (!res.ok) throw new Error("Failed to fetch cost stats");
|
|
34
|
-
return res.json() as Promise<CostDashboardStats>;
|
|
56
|
+
export async function getRecentRequests(limit = 50, signal?: AbortSignal): Promise<MessageStats[]> {
|
|
57
|
+
return fetchJson<MessageStats[]>(`${API_BASE}/stats/recent?limit=${limit}`, { signal });
|
|
35
58
|
}
|
|
36
59
|
|
|
37
|
-
export async function
|
|
38
|
-
|
|
39
|
-
if (!res.ok) throw new Error("Failed to fetch recent requests");
|
|
40
|
-
return res.json() as Promise<MessageStats[]>;
|
|
60
|
+
export async function getRecentErrors(limit = 50, signal?: AbortSignal): Promise<MessageStats[]> {
|
|
61
|
+
return fetchJson<MessageStats[]>(`${API_BASE}/stats/errors?limit=${limit}`, { signal });
|
|
41
62
|
}
|
|
42
63
|
|
|
43
|
-
export async function
|
|
44
|
-
|
|
45
|
-
if (!res.ok) throw new Error("Failed to fetch recent errors");
|
|
46
|
-
return res.json() as Promise<MessageStats[]>;
|
|
64
|
+
export async function getRequestDetails(id: number, signal?: AbortSignal): Promise<RequestDetails> {
|
|
65
|
+
return fetchJson<RequestDetails>(`${API_BASE}/request/${id}`, { signal });
|
|
47
66
|
}
|
|
48
67
|
|
|
49
|
-
export async function
|
|
50
|
-
|
|
51
|
-
if (!res.ok) throw new Error("Failed to fetch request details");
|
|
52
|
-
return res.json() as Promise<RequestDetails>;
|
|
68
|
+
export async function sync(signal?: AbortSignal): Promise<{ processed: number; files: number; totalMessages: number }> {
|
|
69
|
+
return fetchJson<{ processed: number; files: number; totalMessages: number }>(`${API_BASE}/sync`, { signal });
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
export async function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
export async function getBehaviorDashboardStats(
|
|
73
|
+
range: TimeRange = "24h",
|
|
74
|
+
signal?: AbortSignal,
|
|
75
|
+
): Promise<BehaviorDashboardStats> {
|
|
76
|
+
return fetchJson<BehaviorDashboardStats>(`${API_BASE}/stats/behavior?range=${encodeURIComponent(range)}`, {
|
|
77
|
+
signal,
|
|
78
|
+
});
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
export async function
|
|
62
|
-
|
|
63
|
-
if (!res.ok) throw new Error("Failed to fetch behavior stats");
|
|
64
|
-
return res.json() as Promise<BehaviorDashboardStats>;
|
|
81
|
+
export async function getFolderStats(range: TimeRange = "24h", signal?: AbortSignal): Promise<FolderStats[]> {
|
|
82
|
+
return fetchJson<FolderStats[]>(`${API_BASE}/stats/folders?range=${encodeURIComponent(range)}`, { signal });
|
|
65
83
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { TimeRange } from "../types";
|
|
5
|
+
import { NavRail } from "./NavRail";
|
|
6
|
+
import type { DashboardSection } from "./routes";
|
|
7
|
+
import { TopBar } from "./TopBar";
|
|
8
|
+
|
|
9
|
+
export interface AppLayoutProps {
|
|
10
|
+
activeSection: DashboardSection;
|
|
11
|
+
onSectionChange: (section: DashboardSection) => void;
|
|
12
|
+
range: TimeRange;
|
|
13
|
+
onRangeChange: (range: TimeRange) => void;
|
|
14
|
+
updatedAt: number | null;
|
|
15
|
+
onSyncStart?: () => void;
|
|
16
|
+
onSyncComplete?: (result: { success: boolean }) => void;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AppLayout({
|
|
21
|
+
activeSection,
|
|
22
|
+
onSectionChange,
|
|
23
|
+
range,
|
|
24
|
+
onRangeChange,
|
|
25
|
+
updatedAt,
|
|
26
|
+
onSyncStart,
|
|
27
|
+
onSyncComplete,
|
|
28
|
+
children,
|
|
29
|
+
}: AppLayoutProps) {
|
|
30
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
31
|
+
|
|
32
|
+
const handleSectionChange = (section: DashboardSection) => {
|
|
33
|
+
onSectionChange(section);
|
|
34
|
+
setMenuOpen(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="stats-app-container">
|
|
39
|
+
{/* Desktop Rail */}
|
|
40
|
+
<NavRail activeSection={activeSection} onSectionChange={handleSectionChange} className="stats-desktop-nav" />
|
|
41
|
+
|
|
42
|
+
{/* Mobile Nav Drawer */}
|
|
43
|
+
{menuOpen && (
|
|
44
|
+
<div className="stats-mobile-drawer-overlay" onClick={() => setMenuOpen(false)} role="presentation">
|
|
45
|
+
<div
|
|
46
|
+
className="stats-mobile-drawer"
|
|
47
|
+
onClick={e => e.stopPropagation()}
|
|
48
|
+
role="dialog"
|
|
49
|
+
aria-modal="true"
|
|
50
|
+
aria-label="Navigation menu"
|
|
51
|
+
>
|
|
52
|
+
<div className="stats-mobile-drawer-header">
|
|
53
|
+
<div className="stats-logo-container">
|
|
54
|
+
<span className="stats-logo-text">OH MY PI</span>
|
|
55
|
+
<span className="stats-logo-subtext">Observability</span>
|
|
56
|
+
</div>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => setMenuOpen(false)}
|
|
60
|
+
className="stats-drawer-close-btn"
|
|
61
|
+
aria-label="Close navigation menu"
|
|
62
|
+
>
|
|
63
|
+
<X size={18} />
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
<NavRail
|
|
67
|
+
activeSection={activeSection}
|
|
68
|
+
onSectionChange={handleSectionChange}
|
|
69
|
+
className="stats-mobile-nav"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{/* Main Layout Pane */}
|
|
76
|
+
<div className="stats-main-pane">
|
|
77
|
+
<TopBar
|
|
78
|
+
activeSection={activeSection}
|
|
79
|
+
range={range}
|
|
80
|
+
onRangeChange={onRangeChange}
|
|
81
|
+
updatedAt={updatedAt}
|
|
82
|
+
onSyncStart={onSyncStart}
|
|
83
|
+
onSyncComplete={onSyncComplete}
|
|
84
|
+
onMenuToggle={() => setMenuOpen(true)}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<main className="stats-content-area">
|
|
88
|
+
<div className="stats-content-inner">{children}</div>
|
|
89
|
+
</main>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type DashboardSection, routes } from "./routes";
|
|
2
|
+
|
|
3
|
+
export interface NavRailProps {
|
|
4
|
+
activeSection: DashboardSection;
|
|
5
|
+
onSectionChange: (section: DashboardSection) => void;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function NavRail({ activeSection, onSectionChange, className = "" }: NavRailProps) {
|
|
10
|
+
return (
|
|
11
|
+
<aside className={`stats-nav-rail ${className}`}>
|
|
12
|
+
<div className="stats-nav-rail-header">
|
|
13
|
+
<div className="stats-logo-container">
|
|
14
|
+
<span className="stats-logo-text">OH MY PI</span>
|
|
15
|
+
<span className="stats-logo-subtext">Observability</span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<nav className="stats-nav-rail-menu">
|
|
20
|
+
{routes.map(route => {
|
|
21
|
+
const isActive = route.id === activeSection;
|
|
22
|
+
const Icon = route.icon;
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
key={route.id}
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => onSectionChange(route.id)}
|
|
28
|
+
className="stats-nav-rail-item"
|
|
29
|
+
data-active={isActive ? "true" : "false"}
|
|
30
|
+
aria-current={isActive ? "page" : undefined}
|
|
31
|
+
>
|
|
32
|
+
<Icon size={16} className="stats-nav-rail-item-icon" />
|
|
33
|
+
<span className="stats-nav-rail-item-label">{route.label}</span>
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
37
|
+
</nav>
|
|
38
|
+
|
|
39
|
+
<div className="stats-nav-rail-footer">
|
|
40
|
+
<span className="stats-version-tag">OMP Stats v1.0.0</span>
|
|
41
|
+
</div>
|
|
42
|
+
</aside>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TimeRange } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface RangeControlProps {
|
|
4
|
+
value: TimeRange;
|
|
5
|
+
onChange: (value: TimeRange) => void;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const RANGE_OPTIONS: { value: TimeRange; label: string }[] = [
|
|
10
|
+
{ value: "1h", label: "1h" },
|
|
11
|
+
{ value: "24h", label: "24h" },
|
|
12
|
+
{ value: "7d", label: "7d" },
|
|
13
|
+
{ value: "30d", label: "30d" },
|
|
14
|
+
{ value: "90d", label: "90d" },
|
|
15
|
+
{ value: "all", label: "All" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function RangeControl({ value, onChange, className = "" }: RangeControlProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={`stats-range-control ${className}`} role="radiogroup" aria-label="Select time range">
|
|
21
|
+
{RANGE_OPTIONS.map(opt => {
|
|
22
|
+
const isActive = opt.value === value;
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
key={opt.value}
|
|
26
|
+
type="button"
|
|
27
|
+
role="radio"
|
|
28
|
+
aria-checked={isActive}
|
|
29
|
+
data-active={isActive ? "true" : "false"}
|
|
30
|
+
className="stats-range-control-btn"
|
|
31
|
+
onClick={() => onChange(opt.value)}
|
|
32
|
+
>
|
|
33
|
+
{opt.label}
|
|
34
|
+
</button>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|