@linxiraos/pi-stats 1.0.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.
- package/CHANGELOG.md +276 -0
- package/README.md +82 -0
- package/build.ts +95 -0
- package/dist/client/index.css +1 -0
- package/dist/client/index.html +24 -0
- package/dist/client/index.js +257 -0
- package/dist/client/styles.css +1695 -0
- package/dist/types/aggregator.d.ts +98 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/api.d.ts +22 -0
- 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/AgentTokenShare.d.ts +5 -0
- package/dist/types/client/components/chart-shared.d.ts +173 -0
- package/dist/types/client/components/models-table-shared.d.ts +175 -0
- package/dist/types/client/components/range-meta.d.ts +21 -0
- package/dist/types/client/data/charts.d.ts +1 -0
- package/dist/types/client/data/formatters.d.ts +8 -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 +74 -0
- package/dist/types/client/index.d.ts +2 -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/GainRoute.d.ts +7 -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/ProvidersRoute.d.ts +7 -0
- package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
- package/dist/types/client/routes/ToolsRoute.d.ts +7 -0
- package/dist/types/client/routes/index.d.ts +10 -0
- package/dist/types/client/types.d.ts +63 -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 +11 -0
- package/dist/types/db.d.ts +159 -0
- package/dist/types/embedded-client.d.ts +18 -0
- package/dist/types/gain-aggregator.d.ts +26 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/parser.d.ts +53 -0
- package/dist/types/port-conflict.d.ts +14 -0
- package/dist/types/server.d.ts +12 -0
- package/dist/types/shared-types.d.ts +403 -0
- package/dist/types/sync-worker.d.ts +31 -0
- package/dist/types/types.d.ts +164 -0
- package/dist/types/usage-windows.d.ts +45 -0
- package/dist/types/user-metrics.d.ts +81 -0
- package/package.json +94 -0
- package/src/aggregator.ts +615 -0
- package/src/client/App.tsx +112 -0
- package/src/client/api.ts +118 -0
- 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 +86 -0
- package/src/client/components/AgentTokenShare.tsx +68 -0
- package/src/client/components/chart-shared.tsx +257 -0
- package/src/client/components/models-table-shared.tsx +255 -0
- package/src/client/components/range-meta.ts +73 -0
- package/src/client/css.d.ts +1 -0
- package/src/client/data/charts.ts +14 -0
- package/src/client/data/formatters.ts +45 -0
- package/src/client/data/useHashRoute.ts +88 -0
- package/src/client/data/useResource.ts +154 -0
- package/src/client/data/view-models.ts +259 -0
- package/src/client/index.tsx +10 -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/GainRoute.tsx +226 -0
- package/src/client/routes/ModelsRoute.tsx +430 -0
- package/src/client/routes/OverviewRoute.tsx +342 -0
- package/src/client/routes/ProjectsRoute.tsx +163 -0
- package/src/client/routes/ProvidersRoute.tsx +492 -0
- package/src/client/routes/RequestsRoute.tsx +123 -0
- package/src/client/routes/ToolsRoute.tsx +463 -0
- package/src/client/routes/index.ts +10 -0
- package/src/client/styles.css +1384 -0
- package/src/client/types.ts +80 -0
- 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 +78 -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 +87 -0
- package/src/db.ts +1702 -0
- package/src/embedded-client.generated.txt +0 -0
- package/src/embedded-client.ts +26 -0
- package/src/gain-aggregator.ts +311 -0
- package/src/index.ts +194 -0
- package/src/parser.ts +499 -0
- package/src/port-conflict.ts +263 -0
- package/src/server.ts +396 -0
- package/src/shared-types.ts +432 -0
- package/src/sync-worker.ts +40 -0
- package/src/types.ts +171 -0
- package/src/usage-windows.ts +304 -0
- package/src/user-metrics.ts +695 -0
- package/tailwind.config.js +40 -0
|
@@ -0,0 +1,112 @@
|
|
|
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";
|
|
5
|
+
import {
|
|
6
|
+
BehaviorRoute,
|
|
7
|
+
CostsRoute,
|
|
8
|
+
ErrorsRoute,
|
|
9
|
+
GainRoute,
|
|
10
|
+
ModelsRoute,
|
|
11
|
+
OverviewRoute,
|
|
12
|
+
ProjectsRoute,
|
|
13
|
+
ProvidersRoute,
|
|
14
|
+
RequestsRoute,
|
|
15
|
+
ToolsRoute,
|
|
16
|
+
} from "./routes";
|
|
17
|
+
import { RequestDrawer } from "./ui/RequestDrawer";
|
|
18
|
+
|
|
19
|
+
export default function App() {
|
|
20
|
+
const { section, setSection, range, setRange } = useHashRoute();
|
|
21
|
+
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
22
|
+
const [selectedRequestId, setSelectedRequestId] = useState<number | null>(null);
|
|
23
|
+
const [updatedAt, setUpdatedAt] = useState<number | null>(() => Date.now());
|
|
24
|
+
|
|
25
|
+
const handleSyncComplete = useCallback((result: { success: boolean }) => {
|
|
26
|
+
if (result.success) {
|
|
27
|
+
setRefreshTrigger(prev => prev + 1);
|
|
28
|
+
setUpdatedAt(Date.now());
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
// Stable identity so the drawer's effects don't tear down on every App render.
|
|
33
|
+
const closeDrawer = useCallback(() => setSelectedRequestId(null), []);
|
|
34
|
+
|
|
35
|
+
const active = section;
|
|
36
|
+
|
|
37
|
+
// Keep every visited section mounted and just toggle visibility. Remounting a
|
|
38
|
+
// route on each navigation replays the chart entry animations (a visible
|
|
39
|
+
// flicker); keeping it alive makes revisits instant while the live chart
|
|
40
|
+
// instances still animate in place on data/range updates. Only the active
|
|
41
|
+
// route fetches/polls (enabled), so hidden routes don't keep hitting the API.
|
|
42
|
+
const mountedRef = useRef<Set<DashboardSection>>(new Set());
|
|
43
|
+
mountedRef.current.add(active);
|
|
44
|
+
|
|
45
|
+
const renderRoute = (target: DashboardSection) => {
|
|
46
|
+
const isActive = target === active;
|
|
47
|
+
switch (target) {
|
|
48
|
+
case "overview":
|
|
49
|
+
return (
|
|
50
|
+
<OverviewRoute
|
|
51
|
+
active={isActive}
|
|
52
|
+
range={range}
|
|
53
|
+
refreshTrigger={refreshTrigger}
|
|
54
|
+
onRequestClick={setSelectedRequestId}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
case "requests":
|
|
58
|
+
return (
|
|
59
|
+
<RequestsRoute
|
|
60
|
+
active={isActive}
|
|
61
|
+
range={range}
|
|
62
|
+
refreshTrigger={refreshTrigger}
|
|
63
|
+
onRequestClick={setSelectedRequestId}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
case "errors":
|
|
67
|
+
return (
|
|
68
|
+
<ErrorsRoute
|
|
69
|
+
active={isActive}
|
|
70
|
+
range={range}
|
|
71
|
+
refreshTrigger={refreshTrigger}
|
|
72
|
+
onRequestClick={setSelectedRequestId}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
case "models":
|
|
76
|
+
return <ModelsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
77
|
+
case "providers":
|
|
78
|
+
return <ProvidersRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
79
|
+
case "tools":
|
|
80
|
+
return <ToolsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
81
|
+
case "costs":
|
|
82
|
+
return <CostsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
83
|
+
case "behavior":
|
|
84
|
+
return <BehaviorRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
85
|
+
case "projects":
|
|
86
|
+
return <ProjectsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
87
|
+
case "gain":
|
|
88
|
+
return <GainRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<AppLayout
|
|
95
|
+
activeSection={active}
|
|
96
|
+
onSectionChange={setSection}
|
|
97
|
+
range={range}
|
|
98
|
+
onRangeChange={setRange}
|
|
99
|
+
updatedAt={updatedAt}
|
|
100
|
+
onSyncComplete={handleSyncComplete}
|
|
101
|
+
>
|
|
102
|
+
{[...mountedRef.current].map(target => (
|
|
103
|
+
<div key={target} hidden={target !== active}>
|
|
104
|
+
{renderRoute(target)}
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</AppLayout>
|
|
108
|
+
|
|
109
|
+
<RequestDrawer id={selectedRequestId} onClose={closeDrawer} />
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BehaviorDashboardStats,
|
|
3
|
+
CostDashboardStats,
|
|
4
|
+
FolderStats,
|
|
5
|
+
GainDashboardStats,
|
|
6
|
+
MessageStats,
|
|
7
|
+
ModelDashboardStats,
|
|
8
|
+
OverviewStats,
|
|
9
|
+
ProviderDashboardStats,
|
|
10
|
+
RequestDetails,
|
|
11
|
+
TimeRange,
|
|
12
|
+
ToolDashboardStats,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
const API_BASE = "/api";
|
|
16
|
+
|
|
17
|
+
export class ApiError extends Error {
|
|
18
|
+
status: number;
|
|
19
|
+
endpoint: string;
|
|
20
|
+
|
|
21
|
+
constructor(status: number, endpoint: string, message: string) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "ApiError";
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.endpoint = endpoint;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fetchJson<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
|
30
|
+
const res = await fetch(endpoint, options);
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new ApiError(res.status, endpoint, `HTTP error ${res.status} on ${endpoint}`);
|
|
33
|
+
}
|
|
34
|
+
return res.json() as Promise<T>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getOverviewStats(range: TimeRange = "24h", signal?: AbortSignal): Promise<OverviewStats> {
|
|
38
|
+
return fetchJson<OverviewStats>(`${API_BASE}/stats/overview?range=${encodeURIComponent(range)}`, {
|
|
39
|
+
signal,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getModelDashboardStats(
|
|
44
|
+
range: TimeRange = "24h",
|
|
45
|
+
signal?: AbortSignal,
|
|
46
|
+
): Promise<ModelDashboardStats> {
|
|
47
|
+
return fetchJson<ModelDashboardStats>(`${API_BASE}/stats/model-dashboard?range=${encodeURIComponent(range)}`, {
|
|
48
|
+
signal,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getCostDashboardStats(
|
|
53
|
+
range: TimeRange = "24h",
|
|
54
|
+
signal?: AbortSignal,
|
|
55
|
+
): Promise<CostDashboardStats> {
|
|
56
|
+
return fetchJson<CostDashboardStats>(`${API_BASE}/stats/costs?range=${encodeURIComponent(range)}`, { signal });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getRecentRequests(limit = 50, signal?: AbortSignal): Promise<MessageStats[]> {
|
|
60
|
+
return fetchJson<MessageStats[]>(`${API_BASE}/stats/recent?limit=${limit}`, { signal });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getRecentErrors(
|
|
64
|
+
range: TimeRange = "24h",
|
|
65
|
+
limit = 50,
|
|
66
|
+
signal?: AbortSignal,
|
|
67
|
+
): Promise<MessageStats[]> {
|
|
68
|
+
return fetchJson<MessageStats[]>(`${API_BASE}/stats/errors?range=${encodeURIComponent(range)}&limit=${limit}`, {
|
|
69
|
+
signal,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function getRequestDetails(id: number, signal?: AbortSignal): Promise<RequestDetails> {
|
|
74
|
+
return fetchJson<RequestDetails>(`${API_BASE}/request/${id}`, { signal });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function sync(signal?: AbortSignal): Promise<{ processed: number; files: number; totalMessages: number }> {
|
|
78
|
+
return fetchJson<{ processed: number; files: number; totalMessages: number }>(`${API_BASE}/sync`, { signal });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getBehaviorDashboardStats(
|
|
82
|
+
range: TimeRange = "24h",
|
|
83
|
+
signal?: AbortSignal,
|
|
84
|
+
): Promise<BehaviorDashboardStats> {
|
|
85
|
+
return fetchJson<BehaviorDashboardStats>(`${API_BASE}/stats/behavior?range=${encodeURIComponent(range)}`, {
|
|
86
|
+
signal,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getFolderStats(range: TimeRange = "24h", signal?: AbortSignal): Promise<FolderStats[]> {
|
|
91
|
+
return fetchJson<FolderStats[]>(`${API_BASE}/stats/folders?range=${encodeURIComponent(range)}`, { signal });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getGainDashboardStats(
|
|
95
|
+
range: TimeRange = "24h",
|
|
96
|
+
project?: string | null,
|
|
97
|
+
signal?: AbortSignal,
|
|
98
|
+
): Promise<GainDashboardStats> {
|
|
99
|
+
const params = new URLSearchParams({ range });
|
|
100
|
+
if (project) params.set("project", project);
|
|
101
|
+
return fetchJson<GainDashboardStats>(`${API_BASE}/stats/gain?${params}`, { signal });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function getToolDashboardStats(
|
|
105
|
+
range: TimeRange = "24h",
|
|
106
|
+
signal?: AbortSignal,
|
|
107
|
+
): Promise<ToolDashboardStats> {
|
|
108
|
+
return fetchJson<ToolDashboardStats>(`${API_BASE}/stats/tools?range=${encodeURIComponent(range)}`, { signal });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function getProviderDashboardStats(
|
|
112
|
+
range: TimeRange = "24h",
|
|
113
|
+
signal?: AbortSignal,
|
|
114
|
+
): Promise<ProviderDashboardStats> {
|
|
115
|
+
return fetchJson<ProviderDashboardStats>(`${API_BASE}/stats/providers?range=${encodeURIComponent(range)}`, {
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { RefreshCw } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { sync } from "../api";
|
|
4
|
+
|
|
5
|
+
export interface SyncButtonProps {
|
|
6
|
+
onSyncStart?: () => void;
|
|
7
|
+
onSyncComplete?: (result: {
|
|
8
|
+
success: boolean;
|
|
9
|
+
data?: { processed: number; files: number; totalMessages: number };
|
|
10
|
+
error?: string;
|
|
11
|
+
}) => void;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SyncButton({ onSyncStart, onSyncComplete, className = "" }: SyncButtonProps) {
|
|
16
|
+
const [syncing, setSyncing] = useState(false);
|
|
17
|
+
const [status, setStatus] = useState<{ type: "success" | "error"; message: string } | null>(null);
|
|
18
|
+
|
|
19
|
+
const handleSync = async () => {
|
|
20
|
+
if (syncing) return;
|
|
21
|
+
|
|
22
|
+
setSyncing(true);
|
|
23
|
+
setStatus(null);
|
|
24
|
+
if (onSyncStart) {
|
|
25
|
+
onSyncStart();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const data = await sync();
|
|
30
|
+
const result = {
|
|
31
|
+
processed: typeof data?.processed === "number" ? data.processed : 0,
|
|
32
|
+
files: typeof data?.files === "number" ? data.files : 0,
|
|
33
|
+
totalMessages: typeof data?.totalMessages === "number" ? data.totalMessages : 0,
|
|
34
|
+
};
|
|
35
|
+
setStatus({
|
|
36
|
+
type: "success",
|
|
37
|
+
message: `Synced: ${result.processed} new request${result.processed === 1 ? "" : "s"} found.`,
|
|
38
|
+
});
|
|
39
|
+
if (onSyncComplete) {
|
|
40
|
+
onSyncComplete({ success: true, data: result });
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
44
|
+
setStatus({
|
|
45
|
+
type: "error",
|
|
46
|
+
message: `Sync failed: ${errorMessage}`,
|
|
47
|
+
});
|
|
48
|
+
if (onSyncComplete) {
|
|
49
|
+
onSyncComplete({ success: false, error: errorMessage });
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
setSyncing(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={`stats-sync-container ${className}`}>
|
|
58
|
+
{status && (
|
|
59
|
+
<span className="stats-sync-status-msg" data-type={status.type}>
|
|
60
|
+
{status.message}
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={handleSync}
|
|
66
|
+
disabled={syncing}
|
|
67
|
+
className="stats-button stats-button-primary stats-sync-btn"
|
|
68
|
+
aria-busy={syncing}
|
|
69
|
+
>
|
|
70
|
+
<RefreshCw size={14} className={`stats-sync-icon ${syncing ? "stats-spin" : ""}`} />
|
|
71
|
+
{syncing ? "Syncing..." : "Sync DB"}
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type LucideIcon, Monitor, Moon, Sun } from "lucide-react";
|
|
2
|
+
import { type ThemePreference, useThemePreference } from "../useSystemTheme";
|
|
3
|
+
|
|
4
|
+
const NEXT_PREFERENCE: Record<ThemePreference, ThemePreference> = {
|
|
5
|
+
system: "light",
|
|
6
|
+
light: "dark",
|
|
7
|
+
dark: "system",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const PREFERENCE_ICON: Record<ThemePreference, LucideIcon> = {
|
|
11
|
+
system: Monitor,
|
|
12
|
+
light: Sun,
|
|
13
|
+
dark: Moon,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const PREFERENCE_LABEL: Record<ThemePreference, string> = {
|
|
17
|
+
system: "System theme",
|
|
18
|
+
light: "Light theme",
|
|
19
|
+
dark: "Dark theme",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ThemeToggle() {
|
|
23
|
+
const { preference, setPreference } = useThemePreference();
|
|
24
|
+
const Icon = PREFERENCE_ICON[preference];
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
className="stats-theme-toggle"
|
|
30
|
+
onClick={() => setPreference(NEXT_PREFERENCE[preference])}
|
|
31
|
+
aria-label={`${PREFERENCE_LABEL[preference]} (click to switch)`}
|
|
32
|
+
title={`${PREFERENCE_LABEL[preference]} — click to switch`}
|
|
33
|
+
>
|
|
34
|
+
<Icon size={16} />
|
|
35
|
+
</button>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Menu } from "lucide-react";
|
|
2
|
+
import type { TimeRange } from "../types";
|
|
3
|
+
import { RangeControl } from "./RangeControl";
|
|
4
|
+
import type { DashboardSection } from "./routes";
|
|
5
|
+
import { routes } from "./routes";
|
|
6
|
+
import { SyncButton } from "./SyncButton";
|
|
7
|
+
import { ThemeToggle } from "./ThemeToggle";
|
|
8
|
+
|
|
9
|
+
export interface TopBarProps {
|
|
10
|
+
activeSection: DashboardSection;
|
|
11
|
+
range: TimeRange;
|
|
12
|
+
onRangeChange: (range: TimeRange) => void;
|
|
13
|
+
updatedAt: number | null;
|
|
14
|
+
onSyncStart?: () => void;
|
|
15
|
+
onSyncComplete?: (result: { success: boolean }) => void;
|
|
16
|
+
onMenuToggle?: () => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TopBar({
|
|
21
|
+
activeSection,
|
|
22
|
+
range,
|
|
23
|
+
onRangeChange,
|
|
24
|
+
updatedAt,
|
|
25
|
+
onSyncStart,
|
|
26
|
+
onSyncComplete,
|
|
27
|
+
onMenuToggle,
|
|
28
|
+
className = "",
|
|
29
|
+
}: TopBarProps) {
|
|
30
|
+
const currentRoute = routes.find(r => r.id === activeSection);
|
|
31
|
+
const title = currentRoute?.label || "Observability";
|
|
32
|
+
|
|
33
|
+
const formatLastUpdated = (time: number | null) => {
|
|
34
|
+
if (!time) return "Not updated";
|
|
35
|
+
const date = new Date(time);
|
|
36
|
+
return `Updated ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<header className={`stats-top-bar ${className}`}>
|
|
41
|
+
<div className="stats-top-bar-left">
|
|
42
|
+
{onMenuToggle && (
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={onMenuToggle}
|
|
46
|
+
className="stats-mobile-menu-btn"
|
|
47
|
+
aria-label="Open navigation menu"
|
|
48
|
+
>
|
|
49
|
+
<Menu size={20} />
|
|
50
|
+
</button>
|
|
51
|
+
)}
|
|
52
|
+
<h1 className="stats-page-title">{title}</h1>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="stats-top-bar-right">
|
|
56
|
+
<div className="stats-top-bar-meta">
|
|
57
|
+
<span
|
|
58
|
+
className="stats-last-updated"
|
|
59
|
+
title={updatedAt ? new Date(updatedAt).toLocaleString() : undefined}
|
|
60
|
+
>
|
|
61
|
+
{formatLastUpdated(updatedAt)}
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<RangeControl value={range} onChange={onRangeChange} />
|
|
66
|
+
|
|
67
|
+
<ThemeToggle />
|
|
68
|
+
|
|
69
|
+
<SyncButton onSyncStart={onSyncStart} onSyncComplete={onSyncComplete} />
|
|
70
|
+
</div>
|
|
71
|
+
</header>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Activity,
|
|
3
|
+
AlertCircle,
|
|
4
|
+
Coins,
|
|
5
|
+
Cpu,
|
|
6
|
+
Folder,
|
|
7
|
+
LayoutDashboard,
|
|
8
|
+
Plug,
|
|
9
|
+
Smile,
|
|
10
|
+
TrendingUp,
|
|
11
|
+
Wrench,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import type React from "react";
|
|
14
|
+
|
|
15
|
+
export type DashboardSection =
|
|
16
|
+
| "overview"
|
|
17
|
+
| "requests"
|
|
18
|
+
| "errors"
|
|
19
|
+
| "models"
|
|
20
|
+
| "providers"
|
|
21
|
+
| "tools"
|
|
22
|
+
| "costs"
|
|
23
|
+
| "behavior"
|
|
24
|
+
| "projects"
|
|
25
|
+
| "gain";
|
|
26
|
+
|
|
27
|
+
export interface DashboardRoute {
|
|
28
|
+
id: DashboardSection;
|
|
29
|
+
label: string;
|
|
30
|
+
shortLabel?: string;
|
|
31
|
+
icon: React.ComponentType<{ size?: number; className?: string }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const routes: DashboardRoute[] = [
|
|
35
|
+
{
|
|
36
|
+
id: "overview",
|
|
37
|
+
label: "Overview",
|
|
38
|
+
icon: LayoutDashboard,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "requests",
|
|
42
|
+
label: "Requests",
|
|
43
|
+
icon: Activity,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "errors",
|
|
47
|
+
label: "Errors",
|
|
48
|
+
icon: AlertCircle,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "models",
|
|
52
|
+
label: "Models",
|
|
53
|
+
icon: Cpu,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "providers",
|
|
57
|
+
label: "Providers",
|
|
58
|
+
icon: Plug,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "tools",
|
|
62
|
+
label: "Tools",
|
|
63
|
+
icon: Wrench,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "costs",
|
|
67
|
+
label: "Costs",
|
|
68
|
+
icon: Coins,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "behavior",
|
|
72
|
+
label: "Behavior",
|
|
73
|
+
shortLabel: "Behavior",
|
|
74
|
+
icon: Smile,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "projects",
|
|
78
|
+
label: "Projects",
|
|
79
|
+
icon: Folder,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "gain",
|
|
83
|
+
label: "Gain",
|
|
84
|
+
icon: TrendingUp,
|
|
85
|
+
},
|
|
86
|
+
];
|