@oh-my-pi/omp-stats 14.9.3 → 14.9.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "14.9.3",
4
+ "version": "14.9.5",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -37,22 +37,22 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-ai": "14.9.3",
41
- "@oh-my-pi/pi-utils": "14.9.3",
40
+ "@oh-my-pi/pi-ai": "14.9.5",
41
+ "@oh-my-pi/pi-utils": "14.9.5",
42
42
  "@tailwindcss/node": "^4.2.4",
43
43
  "chart.js": "^4.5.1",
44
44
  "date-fns": "^4.1.0",
45
45
  "lucide-react": "^1.14.0",
46
46
  "react": "19.2.5",
47
47
  "react-chartjs-2": "^5.3.1",
48
- "react-dom": "19.2.5"
48
+ "react-dom": "19.2.5",
49
+ "tailwindcss": "^4.2.4"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/bun": "^1.3.13",
52
53
  "@types/react": "^19.2.14",
53
54
  "@types/react-dom": "^19.2.3",
54
- "postcss": "^8.5.14",
55
- "tailwindcss": "^4.2.4"
55
+ "postcss": "^8.5.14"
56
56
  },
57
57
  "engines": {
58
58
  "bun": ">=1.3.7"
package/src/aggregator.ts CHANGED
@@ -2,6 +2,9 @@ import * as fs from "node:fs";
2
2
  import {
3
3
  getRecentErrors as dbGetRecentErrors,
4
4
  getRecentRequests as dbGetRecentRequests,
5
+ getBehaviorByModel,
6
+ getBehaviorOverall,
7
+ getBehaviorTimeSeries,
5
8
  getCostTimeSeries,
6
9
  getFileOffset,
7
10
  getMessageById,
@@ -14,10 +17,11 @@ import {
14
17
  getTimeSeries,
15
18
  initDb,
16
19
  insertMessageStats,
20
+ insertUserMessageStats,
17
21
  setFileOffset,
18
22
  } from "./db";
19
23
  import { getSessionEntry, listAllSessionFiles, parseSessionFile } from "./parser";
20
- import type { DashboardStats, MessageStats, RequestDetails } from "./types";
24
+ import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails } from "./types";
21
25
 
22
26
  /**
23
27
  * Sync a single session file to the database.
@@ -42,16 +46,19 @@ async function syncSessionFile(sessionFile: string): Promise<number> {
42
46
 
43
47
  // Parse file from last offset
44
48
  const fromOffset = stored?.offset ?? 0;
45
- const { stats, newOffset } = await parseSessionFile(sessionFile, fromOffset);
49
+ const { stats, userStats, newOffset } = await parseSessionFile(sessionFile, fromOffset);
46
50
 
47
51
  if (stats.length > 0) {
48
52
  insertMessageStats(stats);
49
53
  }
54
+ if (userStats.length > 0) {
55
+ insertUserMessageStats(userStats);
56
+ }
50
57
 
51
58
  // Update offset tracker
52
59
  setFileOffset(sessionFile, newOffset, lastModified);
53
60
 
54
- return stats.length;
61
+ return stats.length + userStats.length;
55
62
  }
56
63
 
57
64
  /**
@@ -76,20 +83,130 @@ export async function syncAllSessions(): Promise<{ processed: number; files: num
76
83
  return { processed: totalProcessed, files: filesProcessed };
77
84
  }
78
85
 
86
+ const HOUR_MS = 60 * 60 * 1000;
87
+ const DAY_MS = 24 * HOUR_MS;
88
+
89
+ type TimeRange = "1h" | "24h" | "7d" | "30d" | "90d" | "all";
90
+
91
+ interface TimeRangeConfig {
92
+ timeSeriesHours: number;
93
+ timeSeriesBucketMs: number;
94
+ modelSeriesDays: number;
95
+ modelPerformanceDays: number;
96
+ costSeriesDays: number;
97
+ cutoff: number | null;
98
+ }
99
+
100
+ const DEFAULT_TIME_RANGE: TimeRange = "24h";
101
+
102
+ const TIME_RANGE_TO_CONFIG: Record<TimeRange, Omit<TimeRangeConfig, "cutoff">> = {
103
+ "1h": {
104
+ timeSeriesHours: 1,
105
+ timeSeriesBucketMs: HOUR_MS,
106
+ modelSeriesDays: 1,
107
+ modelPerformanceDays: 1,
108
+ costSeriesDays: 1,
109
+ },
110
+ "24h": {
111
+ timeSeriesHours: 24,
112
+ timeSeriesBucketMs: HOUR_MS,
113
+ modelSeriesDays: 1,
114
+ modelPerformanceDays: 1,
115
+ costSeriesDays: 1,
116
+ },
117
+ "7d": {
118
+ timeSeriesHours: 24 * 7,
119
+ timeSeriesBucketMs: DAY_MS,
120
+ modelSeriesDays: 7,
121
+ modelPerformanceDays: 7,
122
+ costSeriesDays: 7,
123
+ },
124
+ "30d": {
125
+ timeSeriesHours: 24 * 30,
126
+ timeSeriesBucketMs: DAY_MS,
127
+ modelSeriesDays: 30,
128
+ modelPerformanceDays: 30,
129
+ costSeriesDays: 30,
130
+ },
131
+ "90d": {
132
+ timeSeriesHours: 24 * 90,
133
+ timeSeriesBucketMs: DAY_MS,
134
+ modelSeriesDays: 90,
135
+ modelPerformanceDays: 90,
136
+ costSeriesDays: 90,
137
+ },
138
+ all: {
139
+ timeSeriesHours: 24 * 3650,
140
+ timeSeriesBucketMs: DAY_MS,
141
+ modelSeriesDays: 3650,
142
+ modelPerformanceDays: 3650,
143
+ costSeriesDays: 3650,
144
+ },
145
+ };
146
+
147
+ function getTimeRangeConfig(range?: string | null): TimeRangeConfig {
148
+ const normalized = range?.trim().toLowerCase() ?? DEFAULT_TIME_RANGE;
149
+ const config = TIME_RANGE_TO_CONFIG[normalized as TimeRange];
150
+ if (config) {
151
+ const cutoff = normalized === "all" ? null : Date.now() - Math.max(1, config.timeSeriesHours * 60 * 60 * 1000);
152
+ return { ...config, cutoff };
153
+ }
154
+
155
+ const fallbackConfig = TIME_RANGE_TO_CONFIG[DEFAULT_TIME_RANGE];
156
+ return {
157
+ ...fallbackConfig,
158
+ cutoff: Date.now() - fallbackConfig.timeSeriesHours * 60 * 60 * 1000,
159
+ };
160
+ }
161
+
79
162
  /**
80
163
  * Get all dashboard stats.
81
164
  */
82
- export async function getDashboardStats(): Promise<DashboardStats> {
165
+ export async function getDashboardStats(range?: string | null): Promise<DashboardStats> {
166
+ await initDb();
167
+ const { timeSeriesHours, timeSeriesBucketMs, modelSeriesDays, modelPerformanceDays, costSeriesDays, cutoff } =
168
+ getTimeRangeConfig(range);
169
+
170
+ return {
171
+ overall: getOverallStats(cutoff ?? undefined),
172
+ byModel: getStatsByModel(cutoff ?? undefined),
173
+ byFolder: getStatsByFolder(cutoff ?? undefined),
174
+ timeSeries: getTimeSeries(timeSeriesHours, cutoff, timeSeriesBucketMs),
175
+ modelSeries: getModelTimeSeries(modelSeriesDays, cutoff),
176
+ modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff),
177
+ costSeries: getCostTimeSeries(costSeriesDays, cutoff),
178
+ };
179
+ }
180
+
181
+ export async function getOverviewStats(range?: string | null): Promise<Pick<DashboardStats, "overall" | "timeSeries">> {
182
+ await initDb();
183
+ const { timeSeriesHours, timeSeriesBucketMs, cutoff } = getTimeRangeConfig(range);
184
+
185
+ return {
186
+ overall: getOverallStats(cutoff ?? undefined),
187
+ timeSeries: getTimeSeries(timeSeriesHours, cutoff, timeSeriesBucketMs),
188
+ };
189
+ }
190
+
191
+ export async function getModelDashboardStats(
192
+ range?: string | null,
193
+ ): Promise<Pick<DashboardStats, "byModel" | "modelSeries" | "modelPerformanceSeries">> {
194
+ await initDb();
195
+ const { modelSeriesDays, modelPerformanceDays, cutoff } = getTimeRangeConfig(range);
196
+
197
+ return {
198
+ byModel: getStatsByModel(cutoff ?? undefined),
199
+ modelSeries: getModelTimeSeries(modelSeriesDays, cutoff),
200
+ modelPerformanceSeries: getModelPerformanceSeries(modelPerformanceDays, cutoff),
201
+ };
202
+ }
203
+
204
+ export async function getCostDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "costSeries">> {
83
205
  await initDb();
206
+ const { costSeriesDays, cutoff } = getTimeRangeConfig(range);
84
207
 
85
208
  return {
86
- overall: getOverallStats(),
87
- byModel: getStatsByModel(),
88
- byFolder: getStatsByFolder(),
89
- timeSeries: getTimeSeries(24),
90
- modelSeries: getModelTimeSeries(14),
91
- modelPerformanceSeries: getModelPerformanceSeries(14),
92
- costSeries: getCostTimeSeries(90),
209
+ costSeries: getCostTimeSeries(costSeriesDays, cutoff),
93
210
  };
94
211
  }
95
212
  export async function getRecentRequests(limit?: number): Promise<MessageStats[]> {
@@ -128,3 +245,13 @@ export async function getTotalMessageCount(): Promise<number> {
128
245
  await initDb();
129
246
  return getMessageCount();
130
247
  }
248
+
249
+ export async function getBehaviorDashboardStats(range?: string | null): Promise<BehaviorDashboardStats> {
250
+ await initDb();
251
+ const { cutoff } = getTimeRangeConfig(range);
252
+ return {
253
+ overall: getBehaviorOverall(cutoff),
254
+ byModel: getBehaviorByModel(cutoff),
255
+ behaviorSeries: getBehaviorTimeSeries(cutoff),
256
+ };
257
+ }
@@ -1,5 +1,16 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
- import { getRecentErrors, getRecentRequests, getStats, sync } from "./api";
2
+ import {
3
+ getBehaviorDashboardStats,
4
+ getCostDashboardStats,
5
+ getModelDashboardStats,
6
+ getOverviewStats,
7
+ getRecentErrors,
8
+ getRecentRequests,
9
+ sync,
10
+ } from "./api";
11
+ import { BehaviorChart } from "./components/BehaviorChart";
12
+ import { BehaviorModelsTable } from "./components/BehaviorModelsTable";
13
+ import { BehaviorSummary } from "./components/BehaviorSummary";
3
14
  import { ChartsContainer } from "./components/ChartsContainer";
4
15
  import { CostChart } from "./components/CostChart";
5
16
  import { CostSummary } from "./components/CostSummary";
@@ -8,64 +19,102 @@ import { ModelsTable } from "./components/ModelsTable";
8
19
  import { RequestDetail } from "./components/RequestDetail";
9
20
  import { RequestList } from "./components/RequestList";
10
21
  import { StatsGrid } from "./components/StatsGrid";
11
- import type { DashboardStats, MessageStats } from "./types";
22
+ import type {
23
+ BehaviorDashboardStats,
24
+ CostDashboardStats,
25
+ MessageStats,
26
+ ModelDashboardStats,
27
+ OverviewStats,
28
+ TimeRange,
29
+ } from "./types";
12
30
 
13
- type Tab = "overview" | "requests" | "errors" | "models" | "costs";
31
+ type Tab = "overview" | "requests" | "errors" | "models" | "costs" | "behavior";
14
32
 
15
33
  export default function App() {
16
- const [stats, setStats] = useState<DashboardStats | null>(null);
34
+ const [overviewStats, setOverviewStats] = useState<OverviewStats | null>(null);
35
+ const [modelStats, setModelStats] = useState<ModelDashboardStats | null>(null);
36
+ const [costStats, setCostStats] = useState<CostDashboardStats | null>(null);
37
+ const [behaviorStats, setBehaviorStats] = useState<BehaviorDashboardStats | null>(null);
17
38
  const [recentRequests, setRecentRequests] = useState<MessageStats[]>([]);
18
39
  const [recentErrors, setRecentErrors] = useState<MessageStats[]>([]);
19
40
  const [selectedRequest, setSelectedRequest] = useState<number | null>(null);
20
41
  const [syncing, setSyncing] = useState(false);
21
42
  const [activeTab, setActiveTab] = useState<Tab>("overview");
43
+ const [timeRange, setTimeRange] = useState<TimeRange>("24h");
22
44
 
23
- const loadData = useCallback(async () => {
45
+ const loadRecentLists = useCallback(async () => {
24
46
  try {
25
- const [s, r, e] = await Promise.all([getStats(), getRecentRequests(50), getRecentErrors(50)]);
26
- setStats(s);
27
- setRecentRequests(r);
28
- setRecentErrors(e);
47
+ const [requests, errors] = await Promise.all([getRecentRequests(50), getRecentErrors(50)]);
48
+ setRecentRequests(requests);
49
+ setRecentErrors(errors);
29
50
  } catch (err) {
30
51
  console.error(err);
31
52
  }
32
53
  }, []);
33
54
 
55
+ const loadActiveTabStats = useCallback(async () => {
56
+ try {
57
+ if (activeTab === "models") {
58
+ setModelStats(await getModelDashboardStats(timeRange));
59
+ return;
60
+ }
61
+ if (activeTab === "costs") {
62
+ setCostStats(await getCostDashboardStats(timeRange));
63
+ return;
64
+ }
65
+ if (activeTab === "behavior") {
66
+ setBehaviorStats(await getBehaviorDashboardStats(timeRange));
67
+ return;
68
+ }
69
+ if (activeTab === "overview") {
70
+ setOverviewStats(await getOverviewStats(timeRange));
71
+ }
72
+ } catch (err) {
73
+ console.error(err);
74
+ }
75
+ }, [activeTab, timeRange]);
76
+
34
77
  const handleSync = async () => {
35
78
  setSyncing(true);
36
79
  try {
37
80
  await sync();
38
- await loadData();
81
+ await Promise.all([loadActiveTabStats(), loadRecentLists()]);
39
82
  } finally {
40
83
  setSyncing(false);
41
84
  }
42
85
  };
43
86
 
44
87
  useEffect(() => {
45
- loadData();
46
- const interval = setInterval(loadData, 30000);
88
+ loadRecentLists();
89
+ const interval = setInterval(loadRecentLists, 30000);
47
90
  return () => clearInterval(interval);
48
- }, [loadData]);
49
-
50
- if (!stats) {
51
- return (
52
- <div className="min-h-screen flex items-center justify-center">
53
- <div className="flex items-center gap-3 text-[var(--text-muted)]">
54
- <div className="w-5 h-5 border-2 border-[var(--border-default)] border-t-[var(--accent-cyan)] rounded-full spin" />
55
- <span className="text-sm">Loading analytics...</span>
56
- </div>
57
- </div>
58
- );
59
- }
91
+ }, [loadRecentLists]);
92
+
93
+ useEffect(() => {
94
+ loadActiveTabStats();
95
+ const interval = setInterval(loadActiveTabStats, 30000);
96
+ return () => clearInterval(interval);
97
+ }, [loadActiveTabStats]);
60
98
 
61
99
  return (
62
100
  <div className="min-h-screen">
63
101
  <div className="max-w-[1600px] mx-auto px-6 py-6">
64
- <Header activeTab={activeTab} onTabChange={setActiveTab} onSync={handleSync} syncing={syncing} />
102
+ <Header
103
+ activeTab={activeTab}
104
+ onTabChange={setActiveTab}
105
+ onSync={handleSync}
106
+ syncing={syncing}
107
+ timeRange={timeRange}
108
+ onTimeRangeChange={setTimeRange}
109
+ />
65
110
 
66
111
  {activeTab === "overview" && (
67
112
  <div className="space-y-6 animate-fade-in">
68
- <StatsGrid stats={stats.overall} />
113
+ {overviewStats ? (
114
+ <StatsGrid stats={overviewStats.overall} />
115
+ ) : (
116
+ <LoadingState label="Loading overview..." />
117
+ )}
69
118
 
70
119
  <div className="grid lg:grid-cols-2 gap-6">
71
120
  <RequestList
@@ -104,15 +153,50 @@ export default function App() {
104
153
 
105
154
  {activeTab === "models" && (
106
155
  <div className="space-y-6 animate-fade-in">
107
- <ChartsContainer modelSeries={stats.modelSeries} />
108
- <ModelsTable models={stats.byModel} performanceSeries={stats.modelPerformanceSeries} />
156
+ {modelStats ? (
157
+ <>
158
+ <ChartsContainer modelSeries={modelStats.modelSeries} />
159
+ <ModelsTable
160
+ models={modelStats.byModel}
161
+ performanceSeries={modelStats.modelPerformanceSeries}
162
+ />
163
+ </>
164
+ ) : (
165
+ <LoadingState label="Loading models..." />
166
+ )}
109
167
  </div>
110
168
  )}
111
169
 
112
170
  {activeTab === "costs" && (
113
171
  <div className="space-y-6 animate-fade-in">
114
- <CostSummary costSeries={stats.costSeries} />
115
- <CostChart costSeries={stats.costSeries} />
172
+ {costStats ? (
173
+ <>
174
+ <CostSummary costSeries={costStats.costSeries} />
175
+ <CostChart costSeries={costStats.costSeries} />
176
+ </>
177
+ ) : (
178
+ <LoadingState label="Loading costs..." />
179
+ )}
180
+ </div>
181
+ )}
182
+
183
+ {activeTab === "behavior" && (
184
+ <div className="space-y-6 animate-fade-in">
185
+ {behaviorStats ? (
186
+ <>
187
+ <BehaviorSummary
188
+ overall={behaviorStats.overall}
189
+ behaviorSeries={behaviorStats.behaviorSeries}
190
+ />
191
+ <BehaviorChart behaviorSeries={behaviorStats.behaviorSeries} />
192
+ <BehaviorModelsTable
193
+ models={behaviorStats.byModel}
194
+ behaviorSeries={behaviorStats.behaviorSeries}
195
+ />
196
+ </>
197
+ ) : (
198
+ <LoadingState label="Loading behavior..." />
199
+ )}
116
200
  </div>
117
201
  )}
118
202
 
@@ -123,3 +207,14 @@ export default function App() {
123
207
  </div>
124
208
  );
125
209
  }
210
+
211
+ function LoadingState({ label }: { label: string }) {
212
+ return (
213
+ <div className="min-h-[180px] flex items-center justify-center">
214
+ <div className="flex items-center gap-3 text-[var(--text-muted)]">
215
+ <div className="w-5 h-5 border-2 border-[var(--border-default)] border-t-[var(--accent-cyan)] rounded-full spin" />
216
+ <span className="text-sm">{label}</span>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
package/src/client/api.ts CHANGED
@@ -1,13 +1,39 @@
1
- import type { DashboardStats, MessageStats, RequestDetails } from "./types";
1
+ import type {
2
+ BehaviorDashboardStats,
3
+ CostDashboardStats,
4
+ DashboardStats,
5
+ MessageStats,
6
+ ModelDashboardStats,
7
+ OverviewStats,
8
+ RequestDetails,
9
+ } from "./types";
2
10
 
3
11
  const API_BASE = "/api";
4
12
 
5
- export async function getStats(): Promise<DashboardStats> {
6
- const res = await fetch(`${API_BASE}/stats`);
13
+ export async function getStats(range = "24h"): Promise<DashboardStats> {
14
+ const res = await fetch(`${API_BASE}/stats?range=${encodeURIComponent(range)}`);
7
15
  if (!res.ok) throw new Error("Failed to fetch stats");
8
16
  return res.json() as Promise<DashboardStats>;
9
17
  }
10
18
 
19
+ export async function getOverviewStats(range = "24h"): Promise<OverviewStats> {
20
+ const res = await fetch(`${API_BASE}/stats/overview?range=${encodeURIComponent(range)}`);
21
+ if (!res.ok) throw new Error("Failed to fetch overview stats");
22
+ return res.json() as Promise<OverviewStats>;
23
+ }
24
+
25
+ export async function getModelDashboardStats(range = "24h"): Promise<ModelDashboardStats> {
26
+ const res = await fetch(`${API_BASE}/stats/model-dashboard?range=${encodeURIComponent(range)}`);
27
+ if (!res.ok) throw new Error("Failed to fetch model stats");
28
+ return res.json() as Promise<ModelDashboardStats>;
29
+ }
30
+
31
+ export async function getCostDashboardStats(range = "24h"): Promise<CostDashboardStats> {
32
+ const res = await fetch(`${API_BASE}/stats/costs?range=${encodeURIComponent(range)}`);
33
+ if (!res.ok) throw new Error("Failed to fetch cost stats");
34
+ return res.json() as Promise<CostDashboardStats>;
35
+ }
36
+
11
37
  export async function getRecentRequests(limit = 50): Promise<MessageStats[]> {
12
38
  const res = await fetch(`${API_BASE}/stats/recent?limit=${limit}`);
13
39
  if (!res.ok) throw new Error("Failed to fetch recent requests");
@@ -31,3 +57,9 @@ export async function sync(): Promise<any> {
31
57
  if (!res.ok) throw new Error("Failed to sync");
32
58
  return res.json();
33
59
  }
60
+
61
+ export async function getBehaviorDashboardStats(range = "24h"): Promise<BehaviorDashboardStats> {
62
+ const res = await fetch(`${API_BASE}/stats/behavior?range=${encodeURIComponent(range)}`);
63
+ if (!res.ok) throw new Error("Failed to fetch behavior stats");
64
+ return res.json() as Promise<BehaviorDashboardStats>;
65
+ }