codexmeter 1.0.3 → 1.0.4

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/dist/index.html CHANGED
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
- <script type="module" crossorigin src="/assets/index-CBnwoOLi.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-_br3iqvd.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-DhDq9dI8.css">
12
12
  </head>
13
13
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmeter",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Local telemetry dashboard for Codex CLI usage",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "scripts": {
18
18
  "dev": "node dev-runner.js",
19
19
  "build": "vite build",
20
+ "test": "node --test test/*.test.js",
20
21
  "start": "node bin/codexmeter.js",
21
22
  "preview": "npm run build && node bin/codexmeter.js",
22
23
  "prepublishOnly": "npm run build"
@@ -30,7 +30,7 @@ export function beginReplayCapture(replay, ingestId, bootstrapPayload) {
30
30
  replay.events.push({
31
31
  event: 'bootstrap',
32
32
  at_ms: 0,
33
- payload: clonePayload(bootstrapPayload),
33
+ payload: bootstrapPayload,
34
34
  });
35
35
  }
36
36
 
@@ -40,7 +40,7 @@ export function recordReplayEvent(replay, event, payload) {
40
40
  replay.events.push({
41
41
  event,
42
42
  at_ms: Math.max(0, Date.now() - replay.started_at_ms),
43
- payload: clonePayload(payload),
43
+ payload,
44
44
  });
45
45
  if (event === 'complete') {
46
46
  replay.active = false;
@@ -24,6 +24,7 @@ const EXPORT_TOTAL_DURATION_MS =
24
24
  const EXPORT_TAIL_SOURCE_FRACTION = OVERVIEW_INGEST_ANIMATION.videoExport?.tailSourceFraction ?? 0.035;
25
25
  const EXPORT_CRF = OVERVIEW_INGEST_ANIMATION.videoExport?.crf ?? 20;
26
26
  const EXPORT_ENCODER_PRESET = OVERVIEW_INGEST_ANIMATION.videoExport?.encoderPreset ?? 'fast';
27
+ const EXPORT_JOB_TTL_MS = 10 * 60 * 1000;
27
28
  let portableBrowserSupportCache = null;
28
29
  const require = createRequire(import.meta.url);
29
30
 
@@ -83,19 +84,23 @@ export function createExportManager({ getReplay, getSettledEnvelope, getBaseUrl
83
84
  install_portable_browser: Boolean(opts.installPortableBrowser),
84
85
  portable_browser_dir: null,
85
86
  portable_browser_executable: null,
87
+ cleanup_timer: null,
88
+ cleanup_at_ms: 0,
86
89
  error: null,
87
90
  };
88
91
 
89
92
  jobs.set(jobId, job);
90
93
  void runOverviewVideoJob(job, replay, getBaseUrl);
91
- return sanitizeJob(job);
94
+ return createJobSummary(job);
92
95
  },
93
96
 
94
97
  getJob(jobId) {
98
+ pruneExpiredJobs();
95
99
  return jobs.get(jobId) || null;
96
100
  },
97
101
 
98
102
  listJobs() {
103
+ pruneExpiredJobs();
99
104
  return [...jobs.values()];
100
105
  },
101
106
 
@@ -123,26 +128,9 @@ export function createExportManager({ getReplay, getSettledEnvelope, getBaseUrl
123
128
  };
124
129
  },
125
130
 
126
- sanitizeJob,
131
+ pruneExpiredJobs,
127
132
  };
128
133
 
129
- function sanitizeJob(job) {
130
- if (!job) return null;
131
- return {
132
- id: job.id,
133
- type: job.type,
134
- status: job.status,
135
- phase: job.phase,
136
- progress: job.progress,
137
- created_at: job.created_at,
138
- updated_at: job.updated_at,
139
- replay_ingest_id: job.replay_ingest_id,
140
- file_name: job.file_name,
141
- download_url: job.status === 'complete' ? `/api/export/${job.id}/file` : null,
142
- error: job.error,
143
- };
144
- }
145
-
146
134
  async function runOverviewVideoJob(job, replay, getBaseUrlFn) {
147
135
  let portableBrowserDir = null;
148
136
  try {
@@ -309,9 +297,11 @@ export function createExportManager({ getReplay, getSettledEnvelope, getBaseUrl
309
297
  outputHeight: job.height,
310
298
  });
311
299
  updateJob(job, 'complete', 1, 'complete');
300
+ scheduleJobCleanup(job);
312
301
  } catch (err) {
313
302
  job.error = err instanceof Error ? err.message : String(err);
314
303
  updateJob(job, 'failed', job.progress || 0, 'failed');
304
+ scheduleJobCleanup(job);
315
305
  } finally {
316
306
  if (portableBrowserDir) {
317
307
  try {
@@ -320,6 +310,41 @@ export function createExportManager({ getReplay, getSettledEnvelope, getBaseUrl
320
310
  }
321
311
  }
322
312
  }
313
+
314
+ function pruneExpiredJobs() {
315
+ const now = Date.now();
316
+ for (const job of jobs.values()) {
317
+ if (job.status === 'queued' || job.status === 'running') continue;
318
+ if (!job.cleanup_at_ms || job.cleanup_at_ms > now) continue;
319
+ if (job.cleanup_timer) {
320
+ clearTimeout(job.cleanup_timer);
321
+ job.cleanup_timer = null;
322
+ }
323
+ jobs.delete(job.id);
324
+ void cleanupJobArtifacts(job);
325
+ }
326
+ }
327
+
328
+ function scheduleJobCleanup(job) {
329
+ if (!job || job.status === 'queued' || job.status === 'running') return;
330
+ if (job.cleanup_timer) {
331
+ clearTimeout(job.cleanup_timer);
332
+ job.cleanup_timer = null;
333
+ }
334
+ job.cleanup_at_ms = Date.now() + EXPORT_JOB_TTL_MS;
335
+ job.cleanup_timer = setTimeout(() => {
336
+ job.cleanup_timer = null;
337
+ jobs.delete(job.id);
338
+ void cleanupJobArtifacts(job);
339
+ }, EXPORT_JOB_TTL_MS);
340
+ }
341
+ }
342
+
343
+ async function cleanupJobArtifacts(job) {
344
+ if (!job?.temp_dir) return;
345
+ try {
346
+ await fs.rm(job.temp_dir, { recursive: true, force: true });
347
+ } catch {}
323
348
  }
324
349
 
325
350
  function analyzeExportTrace(debugTrace, captureTrace) {
@@ -490,6 +515,7 @@ export function createJobSummary(job) {
490
515
  updated_at: job.updated_at,
491
516
  replay_ingest_id: job.replay_ingest_id,
492
517
  file_name: job.file_name,
518
+ expires_at: job.cleanup_at_ms ? new Date(job.cleanup_at_ms).toISOString() : null,
493
519
  download_url: job.status === 'complete' ? `/api/export/${job.id}/file` : null,
494
520
  error: job.error || null,
495
521
  };
package/server/index.js CHANGED
@@ -92,7 +92,7 @@ export function createServer(codexHome, opts = {}) {
92
92
  return;
93
93
  }
94
94
 
95
- const appBaseUrl = req.get('x-codexmeter-client-base') || opts.frontendBaseUrl || `${req.protocol}://${req.get('host')}`;
95
+ const appBaseUrl = opts.frontendBaseUrl || `${req.protocol}://${req.get('host')}`;
96
96
  const settledEnvelope = state.aggregates ? {
97
97
  overview: { data: state.aggregates.overview },
98
98
  repos: { data: state.aggregates.repos },
@@ -108,7 +108,7 @@ export function createServer(codexHome, opts = {}) {
108
108
  appBaseUrl,
109
109
  installPortableBrowser: Boolean(req.body?.install_portable_browser),
110
110
  });
111
- res.status(202).json(createJobSummary(job, `${req.protocol}://${req.get('host')}`));
111
+ res.status(202).json(createJobSummary(job));
112
112
  } catch (err) {
113
113
  res.status(err.statusCode || 500).json({ error: err.message || String(err) });
114
114
  }
@@ -120,7 +120,7 @@ export function createServer(codexHome, opts = {}) {
120
120
  res.json({ job: null });
121
121
  return;
122
122
  }
123
- res.json({ job: createJobSummary(job, `${req.protocol}://${req.get('host')}`) });
123
+ res.json({ job: createJobSummary(job) });
124
124
  });
125
125
 
126
126
  app.get('/api/export/support', async (_req, res) => {
@@ -133,7 +133,7 @@ export function createServer(codexHome, opts = {}) {
133
133
  res.status(404).json({ error: 'Export job not found.' });
134
134
  return;
135
135
  }
136
- res.json(createJobSummary(job, `${req.protocol}://${req.get('host')}`));
136
+ res.json(createJobSummary(job));
137
137
  });
138
138
 
139
139
  app.get('/api/export/:jobId/render-data', (req, res) => {