@oh-my-pi/omp-stats 17.3.5 → 17.3.7
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 +6 -0
- package/dist/types/index.d.ts +11 -1
- package/dist/types/port-conflict.d.ts +6 -4
- package/dist/types/server.d.ts +3 -1
- package/package.json +4 -4
- package/src/index.ts +33 -10
- package/src/port-conflict.ts +13 -8
- package/src/server.ts +23 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.6] - 2026-08-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed the stats dashboard being unreachable from container hosts by accepting an explicit `--host` bind address while preserving loopback-only binding and same-origin API access by default.
|
|
10
|
+
|
|
5
11
|
## [17.3.0] - 2026-08-13
|
|
6
12
|
|
|
7
13
|
### Added
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
export { getDashboardStats, getToolDashboardStats, getTotalMessageCount, type SyncOptions, type SyncProgress, smokeTestSyncWorker, syncAllSessions, } from "./aggregator.js";
|
|
3
3
|
export { closeDb } from "./db.js";
|
|
4
4
|
export { getGainDashboardStats } from "./gain-aggregator.js";
|
|
5
|
-
export { startServer } from "./server.js";
|
|
5
|
+
export { formatStatsDashboardUrl, startServer } from "./server.js";
|
|
6
6
|
export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint, } from "./shared-types.js";
|
|
7
7
|
export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types.js";
|
|
8
|
+
/** Parsed arguments for the standalone `omp-stats` entry point. */
|
|
9
|
+
export interface StandaloneStatsArgs {
|
|
10
|
+
port: number;
|
|
11
|
+
host: string;
|
|
12
|
+
json: boolean;
|
|
13
|
+
sync: boolean;
|
|
14
|
+
help: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Parse the standalone `omp-stats` arguments used by the production entry point. */
|
|
17
|
+
export declare function parseStandaloneStatsArgs(args: string[]): StandaloneStatsArgs;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Header stamped on every dashboard response so reuse probes can identify us. */
|
|
2
2
|
export declare const STATS_DASHBOARD_HEADER = "x-omp-stats-dashboard";
|
|
3
|
-
/**
|
|
4
|
-
export declare const
|
|
3
|
+
/** Header recording the server's requested bind host so reuse cannot change its exposure scope. */
|
|
4
|
+
export declare const STATS_DASHBOARD_HOSTNAME_HEADER = "x-omp-stats-hostname";
|
|
5
|
+
/** Identity-header value for dashboards enforcing an explicit bind host and same-origin access. */
|
|
6
|
+
export declare const STATS_DASHBOARD_SECURITY_VERSION = "3";
|
|
5
7
|
/** IPv4 loopback address shared by the dashboard server and reuse probe. */
|
|
6
8
|
export declare const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
|
|
7
9
|
/**
|
|
@@ -9,6 +11,6 @@ export declare const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
|
|
|
9
11
|
* The preflight is needed on platforms that permit wildcard and loopback-specific
|
|
10
12
|
* listeners to coexist on one port.
|
|
11
13
|
*/
|
|
12
|
-
export declare function prepareStatsPort(port: number): Promise<"retry" | "reuse">;
|
|
14
|
+
export declare function prepareStatsPort(port: number, hostname?: string): Promise<"retry" | "reuse">;
|
|
13
15
|
/** Reuse or reclaim a listener found after the server bind reports EADDRINUSE. */
|
|
14
|
-
export declare function recoverStatsPort(port: number): Promise<"retry" | "reuse">;
|
|
16
|
+
export declare function recoverStatsPort(port: number, hostname?: string): Promise<"retry" | "reuse">;
|
package/dist/types/server.d.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Handle API requests.
|
|
3
3
|
*/
|
|
4
4
|
export declare function handleApi(req: Request): Promise<Response>;
|
|
5
|
+
/** Format a dashboard origin, including brackets required by IPv6 literals. */
|
|
6
|
+
export declare function formatStatsDashboardUrl(hostname: string, port: number): string;
|
|
5
7
|
/**
|
|
6
8
|
* Start the HTTP server, reusing a live dashboard or reclaiming a stale omp listener.
|
|
7
9
|
*/
|
|
8
|
-
export declare function startServer(port?: number): Promise<{
|
|
10
|
+
export declare function startServer(port?: number, hostname?: string): Promise<{
|
|
9
11
|
hostname: string;
|
|
10
12
|
port: number;
|
|
11
13
|
stop: () => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/omp-stats",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.7",
|
|
5
5
|
"description": "Local observability dashboard for pi AI usage statistics",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fmt": "biome format --write ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "17.3.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
44
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
42
|
+
"@oh-my-pi/pi-ai": "17.3.7",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "17.3.7",
|
|
44
|
+
"@oh-my-pi/pi-utils": "17.3.7",
|
|
45
45
|
"@tailwindcss/node": "^4.3.2",
|
|
46
46
|
"chart.js": "^4.5.1",
|
|
47
47
|
"lucide-react": "^1.24.0",
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { parseArgs } from "node:util";
|
|
|
4
4
|
import { formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { getDashboardStats, getTotalMessageCount, syncAllSessions } from "./aggregator";
|
|
6
6
|
import { closeDb } from "./db";
|
|
7
|
-
import { startServer } from "./server";
|
|
7
|
+
import { formatStatsDashboardUrl, startServer } from "./server";
|
|
8
8
|
|
|
9
9
|
export {
|
|
10
10
|
getDashboardStats,
|
|
@@ -17,7 +17,7 @@ export {
|
|
|
17
17
|
} from "./aggregator";
|
|
18
18
|
export { closeDb } from "./db";
|
|
19
19
|
export { getGainDashboardStats } from "./gain-aggregator";
|
|
20
|
-
export { startServer } from "./server";
|
|
20
|
+
export { formatStatsDashboardUrl, startServer } from "./server";
|
|
21
21
|
export type {
|
|
22
22
|
GainDashboardStats,
|
|
23
23
|
GainSource,
|
|
@@ -96,19 +96,42 @@ async function printStats(): Promise<void> {
|
|
|
96
96
|
console.log("");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
/** Parsed arguments for the standalone `omp-stats` entry point. */
|
|
100
|
+
export interface StandaloneStatsArgs {
|
|
101
|
+
port: number;
|
|
102
|
+
host: string;
|
|
103
|
+
json: boolean;
|
|
104
|
+
sync: boolean;
|
|
105
|
+
help: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Parse the standalone `omp-stats` arguments used by the production entry point. */
|
|
109
|
+
export function parseStandaloneStatsArgs(args: string[]): StandaloneStatsArgs {
|
|
103
110
|
const { values } = parseArgs({
|
|
111
|
+
args,
|
|
104
112
|
options: {
|
|
105
113
|
port: { type: "string", short: "p", default: "3847" },
|
|
114
|
+
host: { type: "string", default: "127.0.0.1" },
|
|
106
115
|
json: { type: "boolean", short: "j", default: false },
|
|
107
116
|
sync: { type: "boolean", short: "s", default: false },
|
|
108
117
|
help: { type: "boolean", short: "h", default: false },
|
|
109
118
|
},
|
|
110
119
|
allowPositionals: true,
|
|
111
120
|
});
|
|
121
|
+
return {
|
|
122
|
+
port: parseInt(values.port || "3847", 10),
|
|
123
|
+
host: values.host || "127.0.0.1",
|
|
124
|
+
json: values.json ?? false,
|
|
125
|
+
sync: values.sync ?? false,
|
|
126
|
+
help: values.help ?? false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Main CLI entry point.
|
|
132
|
+
*/
|
|
133
|
+
async function main(): Promise<void> {
|
|
134
|
+
const values = parseStandaloneStatsArgs(process.argv.slice(2));
|
|
112
135
|
|
|
113
136
|
if (values.help) {
|
|
114
137
|
console.log(`
|
|
@@ -119,6 +142,7 @@ Usage:
|
|
|
119
142
|
|
|
120
143
|
Options:
|
|
121
144
|
-p, --port <port> Port for the dashboard server (default: 3847)
|
|
145
|
+
--host <host> Host to bind (default: 127.0.0.1)
|
|
122
146
|
-j, --json Output stats as JSON and exit
|
|
123
147
|
-s, --sync Sync session files and show summary
|
|
124
148
|
-h, --help Show this help message
|
|
@@ -126,7 +150,7 @@ Options:
|
|
|
126
150
|
Examples:
|
|
127
151
|
omp-stats # Start dashboard server
|
|
128
152
|
omp-stats --json # Print stats as JSON
|
|
129
|
-
omp-stats --
|
|
153
|
+
omp-stats --host 0.0.0.0 # Explicitly expose on all IPv4 interfaces
|
|
130
154
|
omp-stats --sync # Sync and show summary
|
|
131
155
|
`);
|
|
132
156
|
return;
|
|
@@ -171,9 +195,8 @@ Examples:
|
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
// Start server
|
|
174
|
-
const port =
|
|
175
|
-
|
|
176
|
-
console.log(`Dashboard available at: http://${hostname}:${actualPort}`);
|
|
198
|
+
const { port: actualPort } = await startServer(values.port, values.host);
|
|
199
|
+
console.log(`Dashboard available at: ${formatStatsDashboardUrl(values.host, actualPort)}`);
|
|
177
200
|
console.log("Press Ctrl+C to stop\n");
|
|
178
201
|
|
|
179
202
|
// Keep process running
|
package/src/port-conflict.ts
CHANGED
|
@@ -17,23 +17,28 @@ interface PortHolder {
|
|
|
17
17
|
|
|
18
18
|
/** Header stamped on every dashboard response so reuse probes can identify us. */
|
|
19
19
|
export const STATS_DASHBOARD_HEADER = "x-omp-stats-dashboard";
|
|
20
|
+
/** Header recording the server's requested bind host so reuse cannot change its exposure scope. */
|
|
21
|
+
export const STATS_DASHBOARD_HOSTNAME_HEADER = "x-omp-stats-hostname";
|
|
20
22
|
|
|
21
|
-
/** Identity-header value for dashboards enforcing
|
|
22
|
-
export const STATS_DASHBOARD_SECURITY_VERSION = "
|
|
23
|
+
/** Identity-header value for dashboards enforcing an explicit bind host and same-origin access. */
|
|
24
|
+
export const STATS_DASHBOARD_SECURITY_VERSION = "3";
|
|
23
25
|
|
|
24
26
|
/** IPv4 loopback address shared by the dashboard server and reuse probe. */
|
|
25
27
|
export const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
|
|
26
28
|
|
|
27
29
|
type StatsDashboardProbe = "reusable" | "occupied" | "unreachable";
|
|
28
30
|
|
|
29
|
-
async function probeStatsDashboard(port: number): Promise<StatsDashboardProbe> {
|
|
31
|
+
async function probeStatsDashboard(port: number, hostname: string): Promise<StatsDashboardProbe> {
|
|
32
|
+
const probeHostname = hostname === "0.0.0.0" ? STATS_DASHBOARD_HOSTNAME : hostname === "::" ? "::1" : hostname;
|
|
33
|
+
const urlHostname = probeHostname.includes(":") ? `[${probeHostname}]` : probeHostname;
|
|
30
34
|
try {
|
|
31
|
-
const response = await fetch(`http://${
|
|
35
|
+
const response = await fetch(`http://${urlHostname}:${port}/api/stats/models`, {
|
|
32
36
|
signal: AbortSignal.timeout(STATS_PROBE_TIMEOUT_MS),
|
|
33
37
|
});
|
|
34
38
|
const reusable =
|
|
35
39
|
response.status === 200 &&
|
|
36
40
|
response.headers.get(STATS_DASHBOARD_HEADER) === STATS_DASHBOARD_SECURITY_VERSION &&
|
|
41
|
+
response.headers.get(STATS_DASHBOARD_HOSTNAME_HEADER) === hostname &&
|
|
37
42
|
!response.headers.has("Access-Control-Allow-Origin");
|
|
38
43
|
await response.body?.cancel();
|
|
39
44
|
return reusable ? "reusable" : "occupied";
|
|
@@ -248,16 +253,16 @@ async function reclaimStatsPort(port: number): Promise<"retry"> {
|
|
|
248
253
|
* The preflight is needed on platforms that permit wildcard and loopback-specific
|
|
249
254
|
* listeners to coexist on one port.
|
|
250
255
|
*/
|
|
251
|
-
export async function prepareStatsPort(port: number): Promise<"retry" | "reuse"> {
|
|
256
|
+
export async function prepareStatsPort(port: number, hostname = STATS_DASHBOARD_HOSTNAME): Promise<"retry" | "reuse"> {
|
|
252
257
|
if (port === 0) return "retry";
|
|
253
|
-
const probe = await probeStatsDashboard(port);
|
|
258
|
+
const probe = await probeStatsDashboard(port, hostname);
|
|
254
259
|
if (probe === "reusable") return "reuse";
|
|
255
260
|
if (probe === "occupied") return reclaimStatsPort(port);
|
|
256
261
|
return "retry";
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
/** Reuse or reclaim a listener found after the server bind reports EADDRINUSE. */
|
|
260
|
-
export async function recoverStatsPort(port: number): Promise<"retry" | "reuse"> {
|
|
261
|
-
if ((await probeStatsDashboard(port)) === "reusable") return "reuse";
|
|
265
|
+
export async function recoverStatsPort(port: number, hostname = STATS_DASHBOARD_HOSTNAME): Promise<"retry" | "reuse"> {
|
|
266
|
+
if ((await probeStatsDashboard(port, hostname)) === "reusable") return "reuse";
|
|
262
267
|
return reclaimStatsPort(port);
|
|
263
268
|
}
|
package/src/server.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
recoverStatsPort,
|
|
27
27
|
STATS_DASHBOARD_HEADER,
|
|
28
28
|
STATS_DASHBOARD_HOSTNAME,
|
|
29
|
+
STATS_DASHBOARD_HOSTNAME_HEADER,
|
|
29
30
|
STATS_DASHBOARD_SECURITY_VERSION,
|
|
30
31
|
} from "./port-conflict";
|
|
31
32
|
|
|
@@ -306,10 +307,16 @@ async function handleStatic(requestPath: string): Promise<Response> {
|
|
|
306
307
|
return new Response("Not Found", { status: 404 });
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
|
|
310
|
+
/** Format a dashboard origin, including brackets required by IPv6 literals. */
|
|
311
|
+
export function formatStatsDashboardUrl(hostname: string, port: number): string {
|
|
312
|
+
const urlHostname = hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
313
|
+
return `http://${urlHostname}:${port}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createDashboardServer(port: number, hostname: string) {
|
|
310
317
|
const server = Bun.serve({
|
|
311
318
|
port,
|
|
312
|
-
hostname
|
|
319
|
+
hostname,
|
|
313
320
|
async fetch(req) {
|
|
314
321
|
const url = new URL(req.url);
|
|
315
322
|
const path = url.pathname;
|
|
@@ -318,6 +325,7 @@ function createDashboardServer(port: number) {
|
|
|
318
325
|
// recognize this dashboard without allowing cross-origin API reads.
|
|
319
326
|
const dashboardHeaders: Record<string, string> = {
|
|
320
327
|
[STATS_DASHBOARD_HEADER]: STATS_DASHBOARD_SECURITY_VERSION,
|
|
328
|
+
[STATS_DASHBOARD_HOSTNAME_HEADER]: hostname,
|
|
321
329
|
};
|
|
322
330
|
|
|
323
331
|
if (req.method === "OPTIONS") {
|
|
@@ -358,37 +366,40 @@ function createDashboardServer(port: number) {
|
|
|
358
366
|
/**
|
|
359
367
|
* Start the HTTP server, reusing a live dashboard or reclaiming a stale omp listener.
|
|
360
368
|
*/
|
|
361
|
-
export async function startServer(
|
|
369
|
+
export async function startServer(
|
|
370
|
+
port = 3847,
|
|
371
|
+
hostname = STATS_DASHBOARD_HOSTNAME,
|
|
372
|
+
): Promise<{ hostname: string; port: number; stop: () => void }> {
|
|
362
373
|
await ensureClientBuild();
|
|
363
|
-
const preparation = await prepareStatsPort(port);
|
|
374
|
+
const preparation = await prepareStatsPort(port, hostname);
|
|
364
375
|
if (preparation === "reuse") {
|
|
365
|
-
return { hostname
|
|
376
|
+
return { hostname, port, stop: () => {} };
|
|
366
377
|
}
|
|
367
378
|
|
|
368
379
|
try {
|
|
369
|
-
const server = createDashboardServer(port);
|
|
380
|
+
const server = createDashboardServer(port, hostname);
|
|
370
381
|
return {
|
|
371
|
-
hostname
|
|
382
|
+
hostname,
|
|
372
383
|
port: server.port ?? port,
|
|
373
384
|
stop: () => server.stop(),
|
|
374
385
|
};
|
|
375
386
|
} catch (error) {
|
|
376
387
|
if (!(error instanceof Error && "code" in error && error.code === "EADDRINUSE")) throw error;
|
|
377
388
|
|
|
378
|
-
const recovery = await recoverStatsPort(port);
|
|
389
|
+
const recovery = await recoverStatsPort(port, hostname);
|
|
379
390
|
if (recovery === "reuse") {
|
|
380
|
-
return { hostname
|
|
391
|
+
return { hostname, port, stop: () => {} };
|
|
381
392
|
}
|
|
382
393
|
|
|
383
394
|
try {
|
|
384
|
-
const server = createDashboardServer(port);
|
|
395
|
+
const server = createDashboardServer(port, hostname);
|
|
385
396
|
return {
|
|
386
|
-
hostname
|
|
397
|
+
hostname,
|
|
387
398
|
port: server.port ?? port,
|
|
388
399
|
stop: () => server.stop(),
|
|
389
400
|
};
|
|
390
401
|
} catch (retryError) {
|
|
391
|
-
throw new Error(`Failed to start stats dashboard on
|
|
402
|
+
throw new Error(`Failed to start stats dashboard on ${hostname}:${port} after reclaiming it.`, {
|
|
392
403
|
cause: retryError,
|
|
393
404
|
});
|
|
394
405
|
}
|