@oh-my-pi/omp-stats 18.0.0 → 18.0.3

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.0.1] - 2026-08-23
6
+
7
+ ### Fixed
8
+
9
+ - Fixed the Projects dashboard folder endpoint running unrelated dashboard aggregations when loading folder statistics.
10
+ - Fixed stats sync crashing with a NOT NULL constraint error when legacy session files carry a partially-populated usage cost.
11
+
5
12
  ## [17.4.0] - 2026-08-20
6
13
 
7
14
  ### Changed
@@ -1,4 +1,4 @@
1
- import type { BehaviorDashboardStats, DashboardStats, MessageStats, ProviderDashboardStats, RequestDetails, ToolDashboardStats } from "./types.js";
1
+ import type { BehaviorDashboardStats, DashboardStats, FolderStats, MessageStats, ProviderDashboardStats, RequestDetails, ToolDashboardStats } from "./types.js";
2
2
  /**
3
3
  * Serialize stats ingestion and archive reconciliation across processes.
4
4
  * The lock covers file discovery, parsing, and the final SQLite write so a
@@ -79,6 +79,7 @@ export declare function getDashboardStats(range?: string | null): Promise<Dashbo
79
79
  export declare function getOverviewStats(range?: string | null): Promise<Pick<DashboardStats, "overall" | "byAgentType" | "timeSeries">>;
80
80
  export declare function getModelDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "byModel" | "modelSeries" | "modelPerformanceSeries">>;
81
81
  export declare function getCostDashboardStats(range?: string | null): Promise<Pick<DashboardStats, "costSeries">>;
82
+ export declare function getFolderStats(range?: string | null): Promise<FolderStats[]>;
82
83
  export declare function getRecentRequests(limit?: number): Promise<MessageStats[]>;
83
84
  export declare function getRecentErrors(range?: string | null, limit?: number): Promise<MessageStats[]>;
84
85
  export declare function getRequestDetails(id: number): Promise<RequestDetails | null>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "18.0.0",
4
+ "version": "18.0.3",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -39,9 +39,9 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "18.0.0",
43
- "@oh-my-pi/pi-catalog": "18.0.0",
44
- "@oh-my-pi/pi-utils": "18.0.0",
42
+ "@oh-my-pi/pi-ai": "18.0.3",
43
+ "@oh-my-pi/pi-catalog": "18.0.3",
44
+ "@oh-my-pi/pi-utils": "18.0.3",
45
45
  "@tailwindcss/node": "^4.3.2",
46
46
  "chart.js": "^4.5.1",
47
47
  "lucide-react": "^1.24.0",
package/src/aggregator.ts CHANGED
@@ -43,6 +43,7 @@ import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
43
43
  import type {
44
44
  BehaviorDashboardStats,
45
45
  DashboardStats,
46
+ FolderStats,
46
47
  MessageStats,
47
48
  ProviderDashboardStats,
48
49
  RequestDetails,
@@ -478,6 +479,13 @@ export async function getCostDashboardStats(range?: string | null): Promise<Pick
478
479
  costSeries: getCostTimeSeries(costSeriesDays, cutoff),
479
480
  };
480
481
  }
482
+
483
+ export async function getFolderStats(range?: string | null): Promise<FolderStats[]> {
484
+ await initDb();
485
+ const { cutoff } = getTimeRangeConfig(range);
486
+ return getStatsByFolder(cutoff ?? undefined);
487
+ }
488
+
481
489
  export async function getRecentRequests(limit?: number): Promise<MessageStats[]> {
482
490
  await initDb();
483
491
  return dbGetRecentRequests(limit);
package/src/db.ts CHANGED
@@ -353,13 +353,26 @@ function calculateCatalogCost(provider: string, modelId: string, tokens: CostTok
353
353
  };
354
354
  }
355
355
 
356
+ function normalizeUsageCost(cost: UsageCost): UsageCost {
357
+ const input = cost.input ?? 0;
358
+ const output = cost.output ?? 0;
359
+ const cacheRead = cost.cacheRead ?? 0;
360
+ const cacheWrite = cost.cacheWrite ?? 0;
361
+ const total = cost.total ?? input + output + cacheRead + cacheWrite;
362
+ return { input, output, cacheRead, cacheWrite, total };
363
+ }
364
+
356
365
  function resolveStoredCost(stats: MessageStats): UsageCost {
357
- // `usage.cost` was optional in older session files. Although current
358
- // MessageStats requires it, parsed JSONL can still carry that legacy shape.
359
- const storedCost: UsageCost | undefined = stats.usage.cost;
360
- if (storedCost && storedCost.total !== 0) {
361
- return storedCost;
362
- }
366
+ // `usage.cost` was optional in older session files, and legacy payloads
367
+ // can carry a partially-populated cost object (e.g. only `total`). The
368
+ // messages table declares every cost_* column as REAL NOT NULL, so any
369
+ // missing field must be normalised here before binding into SQLite.
370
+ const raw: UsageCost | undefined = stats.usage.cost;
371
+ const storedCost = raw ? normalizeUsageCost(raw) : undefined;
372
+
373
+ // A missing total is derived from the stored components. An explicit zero
374
+ // remains the sentinel for catalog-based correction.
375
+ if (storedCost && (raw?.total ?? storedCost.total) !== 0) return storedCost;
363
376
 
364
377
  return calculateCatalogCost(stats.provider, stats.model, stats.usage) ?? storedCost ?? ZERO_USAGE_COST;
365
378
  }
package/src/server.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  getBehaviorDashboardStats,
9
9
  getCostDashboardStats,
10
10
  getDashboardStats,
11
+ getFolderStats,
11
12
  getModelDashboardStats,
12
13
  getOverviewStats,
13
14
  getProviderDashboardStats,
@@ -253,8 +254,8 @@ export async function handleApi(req: Request): Promise<Response> {
253
254
  }
254
255
 
255
256
  if (path === "/api/stats/folders") {
256
- const stats = await getDashboardStats(range);
257
- return Response.json(stats.byFolder);
257
+ const stats = await getFolderStats(range);
258
+ return Response.json(stats);
258
259
  }
259
260
 
260
261
  if (path === "/api/stats/timeseries") {