@f5-sales-demo/xcsh-stats 21.13.2 → 21.14.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh-stats",
4
- "version": "21.13.2",
4
+ "version": "21.14.0",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@f5-sales-demo/pi-ai": "21.13.2",
42
- "@f5-sales-demo/pi-utils": "21.13.2",
41
+ "@f5-sales-demo/pi-ai": "21.14.0",
42
+ "@f5-sales-demo/pi-utils": "21.14.0",
43
43
  "@tailwindcss/node": "^4.2",
44
44
  "chart.js": "^4.5",
45
45
  "date-fns": "~4.4.0",
package/src/db.ts CHANGED
@@ -101,6 +101,16 @@ export function setFileOffset(sessionFile: string, offset: number, lastModified:
101
101
  stmt.run(sessionFile, offset, lastModified);
102
102
  }
103
103
 
104
+ function normalizeUsageCost(cost: MessageStats["usage"]["cost"]): MessageStats["usage"]["cost"] {
105
+ const partial = cost as Partial<MessageStats["usage"]["cost"]>;
106
+ const input = partial.input ?? 0;
107
+ const output = partial.output ?? 0;
108
+ const cacheRead = partial.cacheRead ?? 0;
109
+ const cacheWrite = partial.cacheWrite ?? 0;
110
+ const total = partial.total ?? input + output + cacheRead + cacheWrite;
111
+ return { input, output, cacheRead, cacheWrite, total };
112
+ }
113
+
104
114
  /**
105
115
  * Insert message stats into the database.
106
116
  */
@@ -119,6 +129,7 @@ export function insertMessageStats(stats: MessageStats[]): number {
119
129
  let inserted = 0;
120
130
  const insert = db.transaction(() => {
121
131
  for (const s of stats) {
132
+ const cost = normalizeUsageCost(s.usage.cost);
122
133
  const result = stmt.run(
123
134
  s.sessionFile,
124
135
  s.entryId,
@@ -137,11 +148,11 @@ export function insertMessageStats(stats: MessageStats[]): number {
137
148
  s.usage.cacheWrite,
138
149
  s.usage.totalTokens,
139
150
  s.usage.premiumRequests ?? 0,
140
- s.usage.cost.input,
141
- s.usage.cost.output,
142
- s.usage.cost.cacheRead,
143
- s.usage.cost.cacheWrite,
144
- s.usage.cost.total,
151
+ cost.input,
152
+ cost.output,
153
+ cost.cacheRead,
154
+ cost.cacheWrite,
155
+ cost.total,
145
156
  );
146
157
  if (result.changes > 0) inserted++;
147
158
  }
package/src/index.ts CHANGED
@@ -130,8 +130,8 @@ Examples:
130
130
 
131
131
  // Start server
132
132
  const port = parseInt(values.port || "3847", 10);
133
- const { port: actualPort } = await startServer(port);
134
- console.log(`Dashboard available at: http://localhost:${actualPort}`);
133
+ const { hostname, port: actualPort } = await startServer(port);
134
+ console.log(`Dashboard available at: http://${hostname}:${actualPort}`);
135
135
  console.log("Press Ctrl+C to stop\n");
136
136
 
137
137
  // Keep process running
package/src/server.ts CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  } from "./aggregator";
13
13
  import embeddedClientArchiveTxt from "./embedded-client.generated.txt";
14
14
 
15
+ const STATS_DASHBOARD_HOSTNAME = "127.0.0.1";
16
+
15
17
  const getEmbeddedClientArchive = (() => {
16
18
  const txt = embeddedClientArchiveTxt.replaceAll(/[\s\r\n]/g, "").trim();
17
19
  if (!txt) return null;
@@ -157,6 +159,8 @@ const ensureClientBuild = async () => {
157
159
  </body>
158
160
  </html>`;
159
161
 
162
+ // STATIC_DIR is derived only from import.meta.dir; no HTML input reaches this filesystem path.
163
+ // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
160
164
  await Bun.write(path.join(STATIC_DIR, "index.html"), indexHtml);
161
165
  };
162
166
 
@@ -244,24 +248,18 @@ async function handleStatic(requestPath: string): Promise<Response> {
244
248
  /**
245
249
  * Start the HTTP server.
246
250
  */
247
- export async function startServer(port = 3847): Promise<{ port: number; stop: () => void }> {
251
+ export async function startServer(port = 3847): Promise<{ hostname: string; port: number; stop: () => void }> {
248
252
  await ensureClientBuild();
249
253
 
250
254
  const server = Bun.serve({
251
255
  port,
256
+ hostname: STATS_DASHBOARD_HOSTNAME,
252
257
  async fetch(req) {
253
258
  const url = new URL(req.url);
254
259
  const path = url.pathname;
255
260
 
256
- // CORS headers for local development
257
- const corsHeaders = {
258
- "Access-Control-Allow-Origin": "*",
259
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
260
- "Access-Control-Allow-Headers": "Content-Type",
261
- };
262
-
263
261
  if (req.method === "OPTIONS") {
264
- return new Response(null, { headers: corsHeaders });
262
+ return new Response(null, { status: 204 });
265
263
  }
266
264
 
267
265
  try {
@@ -273,27 +271,16 @@ export async function startServer(port = 3847): Promise<{ port: number; stop: ()
273
271
  response = await handleStatic(path);
274
272
  }
275
273
 
276
- // Add CORS headers to all responses
277
- const headers = new Headers(response.headers);
278
- for (const [key, value] of Object.entries(corsHeaders)) {
279
- headers.set(key, value);
280
- }
281
-
282
- return new Response(response.body, {
283
- status: response.status,
284
- headers,
285
- });
274
+ return response;
286
275
  } catch (error) {
287
276
  console.error("Server error:", error);
288
- return Response.json(
289
- { error: error instanceof Error ? error.message : "Unknown error" },
290
- { status: 500, headers: corsHeaders },
291
- );
277
+ return Response.json({ error: error instanceof Error ? error.message : "Unknown error" }, { status: 500 });
292
278
  }
293
279
  },
294
280
  });
295
281
 
296
282
  return {
283
+ hostname: STATS_DASHBOARD_HOSTNAME,
297
284
  port: server.port ?? port,
298
285
  stop: () => server.stop(),
299
286
  };