@indiekitai/pg-dash 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -126,7 +126,7 @@ Options:
126
126
  -i, --interval <sec> Collection interval (default: 30)
127
127
  --threshold <score> Score threshold for check command (default: 70)
128
128
  -f, --format <fmt> Output format: text|json (default: text)
129
- --query-stats-interval <sec> Query stats snapshot interval (default: 300)
129
+ --query-stats-interval <min> Query stats snapshot interval in minutes (default: 5)
130
130
  --slack-webhook <url> Slack webhook URL for alert notifications
131
131
  --discord-webhook <url> Discord webhook URL for alert notifications
132
132
  -v, --version Show version
package/README.zh-CN.md CHANGED
@@ -126,7 +126,7 @@ Options:
126
126
  -i, --interval <sec> 采集间隔(默认:30)
127
127
  --threshold <score> check 命令的分数阈值(默认:70)
128
128
  -f, --format <fmt> 输出格式:text|json(默认:text)
129
- --query-stats-interval <sec> 查询统计快照间隔(默认:300
129
+ --query-stats-interval <min> 查询统计快照间隔,单位分钟(默认:5
130
130
  --slack-webhook <url> Slack webhook URL,用于告警通知
131
131
  --discord-webhook <url> Discord webhook URL,用于告警通知
132
132
  -v, --version 显示版本
package/dist/cli.js CHANGED
@@ -14,19 +14,32 @@ var advisor_exports = {};
14
14
  __export(advisor_exports, {
15
15
  computeAdvisorScore: () => computeAdvisorScore,
16
16
  getAdvisorReport: () => getAdvisorReport,
17
+ getIgnoredIssues: () => getIgnoredIssues,
17
18
  gradeFromScore: () => gradeFromScore,
18
- isSafeFix: () => isSafeFix
19
+ ignoreIssue: () => ignoreIssue,
20
+ isSafeFix: () => isSafeFix,
21
+ unignoreIssue: () => unignoreIssue
19
22
  });
23
+ import Database from "better-sqlite3";
24
+ import path from "path";
25
+ import os from "os";
26
+ import fs from "fs";
20
27
  function computeAdvisorScore(issues) {
21
28
  let score = 100;
29
+ const deductions = { critical: 0, warning: 0, info: 0 };
22
30
  const counts = { critical: 0, warning: 0, info: 0 };
23
31
  for (const issue of issues) {
24
32
  counts[issue.severity]++;
25
33
  const n = counts[issue.severity];
26
34
  const weight = SEVERITY_WEIGHT[issue.severity];
27
- if (n <= 5) score -= weight;
28
- else if (n <= 15) score -= weight * 0.5;
29
- else score -= weight * 0.25;
35
+ let penalty;
36
+ if (n <= 3) penalty = weight;
37
+ else if (n <= 10) penalty = weight * 0.5;
38
+ else penalty = weight * 0.25;
39
+ deductions[issue.severity] += penalty;
40
+ }
41
+ for (const sev of ["critical", "warning", "info"]) {
42
+ score -= Math.min(deductions[sev], MAX_DEDUCTION[sev]);
30
43
  }
31
44
  return Math.max(0, Math.min(100, Math.round(score)));
32
45
  }
@@ -50,7 +63,10 @@ function computeBreakdown(issues) {
50
63
  async function getAdvisorReport(pool, longQueryThreshold = 5) {
51
64
  const client = await pool.connect();
52
65
  const issues = [];
66
+ const skipped = [];
53
67
  try {
68
+ const versionResult = await client.query("SHOW server_version_num");
69
+ const pgVersion = parseInt(versionResult.rows[0].server_version_num);
54
70
  try {
55
71
  const r = await client.query(`
56
72
  SELECT schemaname, relname, seq_scan, seq_tup_read, n_live_tup,
@@ -75,6 +91,7 @@ CREATE INDEX CONCURRENTLY idx_${row.relname}_<column> ON ${row.schemaname}.${row
75
91
  }
76
92
  } catch (err) {
77
93
  console.error("[advisor] Error checking seq scans:", err.message);
94
+ skipped.push("seq scans: " + err.message);
78
95
  }
79
96
  try {
80
97
  const r = await client.query(`
@@ -103,6 +120,7 @@ CREATE INDEX CONCURRENTLY idx_${row.relname}_<column> ON ${row.schemaname}.${row
103
120
  }
104
121
  } catch (err) {
105
122
  console.error("[advisor] Error checking bloated indexes:", err.message);
123
+ skipped.push("bloated indexes: " + err.message);
106
124
  }
107
125
  try {
108
126
  const r = await client.query(`
@@ -128,6 +146,7 @@ CREATE INDEX CONCURRENTLY idx_${row.relname}_<column> ON ${row.schemaname}.${row
128
146
  }
129
147
  } catch (err) {
130
148
  console.error("[advisor] Error checking table bloat:", err.message);
149
+ skipped.push("table bloat: " + err.message);
131
150
  }
132
151
  try {
133
152
  const r = await client.query(`
@@ -157,6 +176,7 @@ SHOW shared_buffers;`,
157
176
  }
158
177
  } catch (err) {
159
178
  console.error("[advisor] Error checking cache efficiency:", err.message);
179
+ skipped.push("cache efficiency: " + err.message);
160
180
  }
161
181
  try {
162
182
  const extCheck = await client.query("SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'");
@@ -185,6 +205,7 @@ SHOW shared_buffers;`,
185
205
  }
186
206
  } catch (err) {
187
207
  console.error("[advisor] Error checking slow queries:", err.message);
208
+ skipped.push("slow queries: " + err.message);
188
209
  }
189
210
  try {
190
211
  const r = await client.query(`
@@ -210,6 +231,7 @@ SHOW shared_buffers;`,
210
231
  }
211
232
  } catch (err) {
212
233
  console.error("[advisor] Error checking vacuum overdue:", err.message);
234
+ skipped.push("vacuum overdue: " + err.message);
213
235
  }
214
236
  try {
215
237
  const r = await client.query(`
@@ -238,6 +260,7 @@ SHOW shared_buffers;`,
238
260
  }
239
261
  } catch (err) {
240
262
  console.error("[advisor] Error checking analyze overdue:", err.message);
263
+ skipped.push("analyze overdue: " + err.message);
241
264
  }
242
265
  try {
243
266
  const r = await client.query(`
@@ -273,6 +296,7 @@ SHOW shared_buffers;`,
273
296
  }
274
297
  } catch (err) {
275
298
  console.error("[advisor] Error checking xid wraparound:", err.message);
299
+ skipped.push("xid wraparound: " + err.message);
276
300
  }
277
301
  try {
278
302
  const r = await client.query(`
@@ -281,9 +305,9 @@ SHOW shared_buffers;`,
281
305
  extract(epoch from now() - state_change)::int AS idle_seconds
282
306
  FROM pg_stat_activity
283
307
  WHERE state IN ('idle', 'idle in transaction')
284
- AND now() - state_change > interval '${longQueryThreshold} minutes'
308
+ AND now() - state_change > $1 * interval '1 minute'
285
309
  AND pid != pg_backend_pid()
286
- `);
310
+ `, [longQueryThreshold]);
287
311
  for (const row of r.rows) {
288
312
  const isIdleTx = row.state === "idle in transaction";
289
313
  issues.push({
@@ -299,6 +323,7 @@ SHOW shared_buffers;`,
299
323
  }
300
324
  } catch (err) {
301
325
  console.error("[advisor] Error checking idle connections:", err.message);
326
+ skipped.push("idle connections: " + err.message);
302
327
  }
303
328
  try {
304
329
  const r = await client.query(`
@@ -324,6 +349,7 @@ SHOW shared_buffers;`,
324
349
  }
325
350
  } catch (err) {
326
351
  console.error("[advisor] Error checking missing primary keys:", err.message);
352
+ skipped.push("missing primary keys: " + err.message);
327
353
  }
328
354
  try {
329
355
  const r = await client.query(`
@@ -350,6 +376,7 @@ SHOW shared_buffers;`,
350
376
  }
351
377
  } catch (err) {
352
378
  console.error("[advisor] Error checking unused indexes:", err.message);
379
+ skipped.push("unused indexes: " + err.message);
353
380
  }
354
381
  try {
355
382
  const r = await client.query(`
@@ -375,6 +402,7 @@ DROP INDEX CONCURRENTLY ${row.indexes.slice(1).join(";\nDROP INDEX CONCURRENTLY
375
402
  }
376
403
  } catch (err) {
377
404
  console.error("[advisor] Error checking duplicate indexes:", err.message);
405
+ skipped.push("duplicate indexes: " + err.message);
378
406
  }
379
407
  try {
380
408
  const r = await client.query(`
@@ -405,6 +433,7 @@ DROP INDEX CONCURRENTLY ${row.indexes.slice(1).join(";\nDROP INDEX CONCURRENTLY
405
433
  }
406
434
  } catch (err) {
407
435
  console.error("[advisor] Error checking missing FK indexes:", err.message);
436
+ skipped.push("missing FK indexes: " + err.message);
408
437
  }
409
438
  try {
410
439
  const r = await client.query(`
@@ -440,6 +469,7 @@ DROP INDEX CONCURRENTLY ${row.indexes.slice(1).join(";\nDROP INDEX CONCURRENTLY
440
469
  }
441
470
  } catch (err) {
442
471
  console.error("[advisor] Error checking locks:", err.message);
472
+ skipped.push("locks: " + err.message);
443
473
  }
444
474
  try {
445
475
  const r = await client.query(`
@@ -463,13 +493,15 @@ SELECT * FROM pg_stat_replication;`,
463
493
  }
464
494
  } catch (err) {
465
495
  console.error("[advisor] Error checking replication lag:", err.message);
496
+ skipped.push("replication lag: " + err.message);
466
497
  }
467
498
  try {
499
+ const checkpointView = pgVersion >= 17e4 ? "pg_stat_checkpointer" : "pg_stat_bgwriter";
468
500
  const r = await client.query(`
469
501
  SELECT checkpoints_req, checkpoints_timed,
470
502
  CASE WHEN (checkpoints_req + checkpoints_timed) = 0 THEN 0
471
503
  ELSE round(checkpoints_req::numeric / (checkpoints_req + checkpoints_timed) * 100, 1) END AS req_pct
472
- FROM pg_stat_bgwriter
504
+ FROM ${checkpointView}
473
505
  `);
474
506
  const reqPct = parseFloat(r.rows[0]?.req_pct ?? "0");
475
507
  if (reqPct > 50) {
@@ -488,6 +520,7 @@ SELECT pg_reload_conf();`,
488
520
  }
489
521
  } catch (err) {
490
522
  console.error("[advisor] Error checking checkpoint frequency:", err.message);
523
+ skipped.push("checkpoint frequency: " + err.message);
491
524
  }
492
525
  try {
493
526
  const r = await client.query(`SELECT setting FROM pg_settings WHERE name = 'autovacuum'`);
@@ -506,6 +539,7 @@ SELECT pg_reload_conf();`,
506
539
  }
507
540
  } catch (err) {
508
541
  console.error("[advisor] Error checking autovacuum:", err.message);
542
+ skipped.push("autovacuum: " + err.message);
509
543
  }
510
544
  try {
511
545
  const sbRes = await client.query(`SELECT setting, unit FROM pg_settings WHERE name = 'shared_buffers'`);
@@ -529,6 +563,7 @@ SELECT pg_reload_conf();`,
529
563
  }
530
564
  } catch (err) {
531
565
  console.error("[advisor] Error checking shared_buffers:", err.message);
566
+ skipped.push("shared_buffers: " + err.message);
532
567
  }
533
568
  try {
534
569
  const r = await client.query(`SELECT setting, unit FROM pg_settings WHERE name = 'work_mem'`);
@@ -548,6 +583,7 @@ SELECT pg_reload_conf();`,
548
583
  }
549
584
  } catch (err) {
550
585
  console.error("[advisor] Error checking work_mem:", err.message);
586
+ skipped.push("work_mem: " + err.message);
551
587
  }
552
588
  try {
553
589
  const r = await client.query(`
@@ -573,6 +609,7 @@ SELECT pg_reload_conf();`,
573
609
  }
574
610
  } catch (err) {
575
611
  console.error("[advisor] Error checking superuser connections:", err.message);
612
+ skipped.push("superuser connections: " + err.message);
576
613
  }
577
614
  try {
578
615
  const r = await client.query(`SELECT setting FROM pg_settings WHERE name = 'ssl'`);
@@ -594,6 +631,7 @@ SELECT pg_reload_conf();`,
594
631
  }
595
632
  } catch (err) {
596
633
  console.error("[advisor] Error checking SSL check:", err.message);
634
+ skipped.push("SSL check: " + err.message);
597
635
  }
598
636
  try {
599
637
  const r = await client.query(`
@@ -617,18 +655,51 @@ SELECT pg_reload_conf();`,
617
655
  }
618
656
  } catch (err) {
619
657
  console.error("[advisor] Error checking trust auth:", err.message);
658
+ skipped.push("trust auth: " + err.message);
620
659
  }
621
- const score = computeAdvisorScore(issues);
660
+ const ignoredIds = getIgnoredIssues();
661
+ const ignoredSet = new Set(ignoredIds);
662
+ const activeIssues = issues.filter((i) => !ignoredSet.has(i.id));
663
+ const ignoredCount = issues.length - activeIssues.length;
664
+ const score = computeAdvisorScore(activeIssues);
622
665
  return {
623
666
  score,
624
667
  grade: gradeFromScore(score),
625
- issues,
626
- breakdown: computeBreakdown(issues)
668
+ issues: activeIssues,
669
+ breakdown: computeBreakdown(activeIssues),
670
+ skipped,
671
+ ignoredCount
627
672
  };
628
673
  } finally {
629
674
  client.release();
630
675
  }
631
676
  }
677
+ function getIgnoredDb() {
678
+ if (_ignoredDb) return _ignoredDb;
679
+ const dataDir = process.env.PG_DASH_DATA_DIR || path.join(os.homedir(), ".pg-dash");
680
+ fs.mkdirSync(dataDir, { recursive: true });
681
+ const dbPath = path.join(dataDir, "alerts.db");
682
+ _ignoredDb = new Database(dbPath);
683
+ _ignoredDb.pragma("journal_mode = WAL");
684
+ _ignoredDb.exec("CREATE TABLE IF NOT EXISTS ignored_issues (issue_id TEXT PRIMARY KEY, ignored_at INTEGER)");
685
+ return _ignoredDb;
686
+ }
687
+ function getIgnoredIssues() {
688
+ try {
689
+ const db = getIgnoredDb();
690
+ return db.prepare("SELECT issue_id FROM ignored_issues").all().map((r) => r.issue_id);
691
+ } catch {
692
+ return [];
693
+ }
694
+ }
695
+ function ignoreIssue(issueId) {
696
+ const db = getIgnoredDb();
697
+ db.prepare("INSERT OR REPLACE INTO ignored_issues (issue_id, ignored_at) VALUES (?, ?)").run(issueId, Date.now());
698
+ }
699
+ function unignoreIssue(issueId) {
700
+ const db = getIgnoredDb();
701
+ db.prepare("DELETE FROM ignored_issues WHERE issue_id = ?").run(issueId);
702
+ }
632
703
  function isSafeFix(sql) {
633
704
  const trimmed = sql.trim();
634
705
  if (!trimmed) return false;
@@ -650,11 +721,13 @@ function isSafeFix(sql) {
650
721
  ];
651
722
  return ALLOWED_PREFIXES.some((p) => upper.startsWith(p));
652
723
  }
653
- var SEVERITY_WEIGHT;
724
+ var SEVERITY_WEIGHT, MAX_DEDUCTION, _ignoredDb;
654
725
  var init_advisor = __esm({
655
726
  "src/server/advisor.ts"() {
656
727
  "use strict";
657
- SEVERITY_WEIGHT = { critical: 20, warning: 8, info: 3 };
728
+ SEVERITY_WEIGHT = { critical: 15, warning: 5, info: 1 };
729
+ MAX_DEDUCTION = { critical: 60, warning: 30, info: 10 };
730
+ _ignoredDb = null;
658
731
  }
659
732
  });
660
733
 
@@ -663,9 +736,9 @@ import { parseArgs } from "util";
663
736
 
664
737
  // src/server/index.ts
665
738
  import { Hono } from "hono";
666
- import path2 from "path";
667
- import fs2 from "fs";
668
- import os2 from "os";
739
+ import path3 from "path";
740
+ import fs3 from "fs";
741
+ import os3 from "os";
669
742
  import { fileURLToPath } from "url";
670
743
  import { Pool } from "pg";
671
744
 
@@ -675,7 +748,7 @@ async function getOverview(pool) {
675
748
  try {
676
749
  const version = await client.query("SHOW server_version");
677
750
  const uptime = await client.query(
678
- "SELECT now() - pg_postmaster_start_time() AS uptime"
751
+ `SELECT to_char(now() - pg_postmaster_start_time(), 'DD "d" HH24 "h" MI "m"') AS uptime`
679
752
  );
680
753
  const dbSize = await client.query(
681
754
  "SELECT pg_size_pretty(pg_database_size(current_database())) AS size"
@@ -781,24 +854,24 @@ async function getActivity(pool) {
781
854
  init_advisor();
782
855
 
783
856
  // src/server/timeseries.ts
784
- import Database from "better-sqlite3";
785
- import path from "path";
786
- import os from "os";
787
- import fs from "fs";
788
- var DEFAULT_DIR = path.join(os.homedir(), ".pg-dash");
857
+ import Database2 from "better-sqlite3";
858
+ import path2 from "path";
859
+ import os2 from "os";
860
+ import fs2 from "fs";
861
+ var DEFAULT_DIR = path2.join(os2.homedir(), ".pg-dash");
789
862
  var DEFAULT_RETENTION_DAYS = 7;
790
863
  var TimeseriesStore = class {
791
864
  db;
792
865
  insertStmt;
793
866
  retentionMs;
794
867
  constructor(dbOrDir, retentionDays = DEFAULT_RETENTION_DAYS) {
795
- if (dbOrDir instanceof Database) {
868
+ if (dbOrDir instanceof Database2) {
796
869
  this.db = dbOrDir;
797
870
  } else {
798
871
  const dir = dbOrDir || DEFAULT_DIR;
799
- fs.mkdirSync(dir, { recursive: true });
800
- const dbPath = path.join(dir, "metrics.db");
801
- this.db = new Database(dbPath);
872
+ fs2.mkdirSync(dir, { recursive: true });
873
+ const dbPath = path2.join(dir, "metrics.db");
874
+ this.db = new Database2(dbPath);
802
875
  }
803
876
  this.retentionMs = retentionDays * 24 * 60 * 60 * 1e3;
804
877
  this.db.pragma("journal_mode = WAL");
@@ -858,8 +931,10 @@ var TimeseriesStore = class {
858
931
  };
859
932
 
860
933
  // src/server/collector.ts
861
- var Collector = class {
934
+ import { EventEmitter } from "events";
935
+ var Collector = class extends EventEmitter {
862
936
  constructor(pool, store, intervalMs = 3e4) {
937
+ super();
863
938
  this.pool = pool;
864
939
  this.store = store;
865
940
  this.intervalMs = intervalMs;
@@ -979,6 +1054,7 @@ var Collector = class {
979
1054
  this.store.insertMany(points);
980
1055
  }
981
1056
  this.lastSnapshot = snapshot;
1057
+ this.emit("collected", snapshot);
982
1058
  return snapshot;
983
1059
  }
984
1060
  };
@@ -1828,6 +1904,33 @@ function registerAdvisorRoutes(app, pool, longQueryThreshold) {
1828
1904
  return c.json({ error: err.message }, 500);
1829
1905
  }
1830
1906
  });
1907
+ app.get("/api/advisor/ignored", (c) => {
1908
+ try {
1909
+ return c.json(getIgnoredIssues());
1910
+ } catch (err) {
1911
+ return c.json({ error: err.message }, 500);
1912
+ }
1913
+ });
1914
+ app.post("/api/advisor/ignore", async (c) => {
1915
+ try {
1916
+ const body = await c.req.json();
1917
+ const issueId = body?.issueId;
1918
+ if (!issueId) return c.json({ error: "issueId required" }, 400);
1919
+ ignoreIssue(issueId);
1920
+ return c.json({ ok: true });
1921
+ } catch (err) {
1922
+ return c.json({ error: err.message }, 500);
1923
+ }
1924
+ });
1925
+ app.delete("/api/advisor/ignore/:issueId", (c) => {
1926
+ try {
1927
+ const issueId = c.req.param("issueId");
1928
+ unignoreIssue(issueId);
1929
+ return c.json({ ok: true });
1930
+ } catch (err) {
1931
+ return c.json({ error: err.message }, 500);
1932
+ }
1933
+ });
1831
1934
  app.post("/api/fix", async (c) => {
1832
1935
  try {
1833
1936
  const body = await c.req.json();
@@ -2017,6 +2120,7 @@ function registerExplainRoutes(app, pool) {
2017
2120
  const query = body?.query?.trim();
2018
2121
  if (!query) return c.json({ error: "Missing query" }, 400);
2019
2122
  if (DDL_PATTERN.test(query)) return c.json({ error: "DDL statements are not allowed" }, 400);
2123
+ if (!/^\s*SELECT\b/i.test(query)) return c.json({ error: "Only SELECT queries can be explained for safety. DELETE/UPDATE/INSERT are blocked." }, 400);
2020
2124
  const client = await pool.connect();
2021
2125
  try {
2022
2126
  await client.query("SET statement_timeout = '30s'");
@@ -2407,10 +2511,10 @@ function registerQueryStatsRoutes(app, store) {
2407
2511
  }
2408
2512
 
2409
2513
  // src/server/index.ts
2410
- import Database2 from "better-sqlite3";
2514
+ import Database3 from "better-sqlite3";
2411
2515
  import { WebSocketServer, WebSocket } from "ws";
2412
2516
  import http from "http";
2413
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
2517
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
2414
2518
  async function startServer(opts) {
2415
2519
  const pool = new Pool({ connectionString: opts.connectionString });
2416
2520
  try {
@@ -2438,25 +2542,25 @@ async function startServer(opts) {
2438
2542
  await pool.end();
2439
2543
  process.exit(0);
2440
2544
  }
2441
- const dataDir = opts.dataDir || path2.join(os2.homedir(), ".pg-dash");
2442
- fs2.mkdirSync(dataDir, { recursive: true });
2443
- const metricsDbPath = path2.join(dataDir, "metrics.db");
2444
- const metricsDb = new Database2(metricsDbPath);
2545
+ const dataDir = opts.dataDir || path3.join(os3.homedir(), ".pg-dash");
2546
+ fs3.mkdirSync(dataDir, { recursive: true });
2547
+ const metricsDbPath = path3.join(dataDir, "metrics.db");
2548
+ const metricsDb = new Database3(metricsDbPath);
2445
2549
  metricsDb.pragma("journal_mode = WAL");
2446
2550
  const store = new TimeseriesStore(metricsDb, opts.retentionDays);
2447
2551
  const intervalMs = (opts.interval || 30) * 1e3;
2448
2552
  const collector = new Collector(pool, store, intervalMs);
2449
2553
  console.log(` Collecting metrics every ${intervalMs / 1e3}s...`);
2450
2554
  collector.start();
2451
- const schemaDbPath = path2.join(dataDir, "schema.db");
2452
- const schemaDb = new Database2(schemaDbPath);
2555
+ const schemaDbPath = path3.join(dataDir, "schema.db");
2556
+ const schemaDb = new Database3(schemaDbPath);
2453
2557
  schemaDb.pragma("journal_mode = WAL");
2454
2558
  const snapshotIntervalMs = (opts.snapshotInterval || 6) * 60 * 60 * 1e3;
2455
2559
  const schemaTracker = new SchemaTracker(schemaDb, pool, snapshotIntervalMs);
2456
2560
  schemaTracker.start();
2457
2561
  console.log(" Schema change tracking enabled");
2458
- const alertsDbPath = path2.join(dataDir, "alerts.db");
2459
- const alertsDb = new Database2(alertsDbPath);
2562
+ const alertsDbPath = path3.join(dataDir, "alerts.db");
2563
+ const alertsDb = new Database3(alertsDbPath);
2460
2564
  alertsDb.pragma("journal_mode = WAL");
2461
2565
  const alertManager = new AlertManager(alertsDb, opts.webhook);
2462
2566
  console.log(" Alert monitoring enabled");
@@ -2512,7 +2616,7 @@ async function startServer(opts) {
2512
2616
  registerExplainRoutes(app, pool);
2513
2617
  registerDiskRoutes(app, pool, store);
2514
2618
  registerQueryStatsRoutes(app, queryStatsStore);
2515
- const uiPath = path2.resolve(__dirname, "ui");
2619
+ const uiPath = path3.resolve(__dirname, "ui");
2516
2620
  const MIME_TYPES = {
2517
2621
  ".html": "text/html",
2518
2622
  ".js": "application/javascript",
@@ -2527,15 +2631,15 @@ async function startServer(opts) {
2527
2631
  };
2528
2632
  app.get("/*", async (c) => {
2529
2633
  const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
2530
- const filePath = path2.join(uiPath, urlPath);
2634
+ const filePath = path3.join(uiPath, urlPath);
2531
2635
  try {
2532
- const content = await fs2.promises.readFile(filePath);
2533
- const ext = path2.extname(filePath);
2636
+ const content = await fs3.promises.readFile(filePath);
2637
+ const ext = path3.extname(filePath);
2534
2638
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
2535
2639
  return new Response(content, { headers: { "content-type": contentType } });
2536
2640
  } catch {
2537
2641
  try {
2538
- const html = await fs2.promises.readFile(path2.join(uiPath, "index.html"));
2642
+ const html = await fs3.promises.readFile(path3.join(uiPath, "index.html"));
2539
2643
  return new Response(html, { headers: { "content-type": "text/html" } });
2540
2644
  } catch (err) {
2541
2645
  console.error("[static] Error reading index.html:", err.message);
@@ -2599,9 +2703,7 @@ async function startServer(opts) {
2599
2703
  ws.on("error", () => clients.delete(ws));
2600
2704
  });
2601
2705
  let collectCycleCount = 0;
2602
- const origCollect = collector.collect.bind(collector);
2603
- collector.collect = async () => {
2604
- const snapshot = await origCollect();
2706
+ collector.on("collected", async (snapshot) => {
2605
2707
  if (clients.size > 0 && Object.keys(snapshot).length > 0) {
2606
2708
  const metricsMsg = JSON.stringify({ type: "metrics", data: snapshot });
2607
2709
  let activityData = [];
@@ -2700,8 +2802,7 @@ async function startServer(opts) {
2700
2802
  console.error("[alerts] Error checking alerts:", err.message);
2701
2803
  }
2702
2804
  }
2703
- return snapshot;
2704
- };
2805
+ });
2705
2806
  const bindAddr = opts.bind || "127.0.0.1";
2706
2807
  server.listen(opts.port, bindAddr, async () => {
2707
2808
  console.log(`
@@ -2737,8 +2838,8 @@ async function startServer(opts) {
2737
2838
  }
2738
2839
 
2739
2840
  // src/cli.ts
2740
- import fs3 from "fs";
2741
- import path3 from "path";
2841
+ import fs4 from "fs";
2842
+ import path4 from "path";
2742
2843
  import { fileURLToPath as fileURLToPath2 } from "url";
2743
2844
  process.on("uncaughtException", (err) => {
2744
2845
  console.error("Uncaught exception:", err);
@@ -2777,8 +2878,8 @@ var { values, positionals } = parseArgs({
2777
2878
  });
2778
2879
  if (values.version) {
2779
2880
  try {
2780
- const __dirname2 = path3.dirname(fileURLToPath2(import.meta.url));
2781
- const pkg = JSON.parse(fs3.readFileSync(path3.resolve(__dirname2, "../package.json"), "utf-8"));
2881
+ const __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
2882
+ const pkg = JSON.parse(fs4.readFileSync(path4.resolve(__dirname2, "../package.json"), "utf-8"));
2782
2883
  console.log(`pg-dash v${pkg.version}`);
2783
2884
  } catch {
2784
2885
  console.log("pg-dash v0.1.0");
@@ -2883,14 +2984,14 @@ if (subcommand === "check") {
2883
2984
  }
2884
2985
  } else if (subcommand === "schema-diff") {
2885
2986
  const connectionString = resolveConnectionString(1);
2886
- const dataDir = values["data-dir"] || path3.join((await import("os")).homedir(), ".pg-dash");
2887
- const schemaDbPath = path3.join(dataDir, "schema.db");
2888
- if (!fs3.existsSync(schemaDbPath)) {
2987
+ const dataDir = values["data-dir"] || path4.join((await import("os")).homedir(), ".pg-dash");
2988
+ const schemaDbPath = path4.join(dataDir, "schema.db");
2989
+ if (!fs4.existsSync(schemaDbPath)) {
2889
2990
  console.error("No schema tracking data found. Run pg-dash server first to collect schema snapshots.");
2890
2991
  process.exit(1);
2891
2992
  }
2892
- const Database3 = (await import("better-sqlite3")).default;
2893
- const db = new Database3(schemaDbPath, { readonly: true });
2993
+ const Database4 = (await import("better-sqlite3")).default;
2994
+ const db = new Database4(schemaDbPath, { readonly: true });
2894
2995
  const changes = db.prepare("SELECT * FROM schema_changes ORDER BY timestamp DESC LIMIT 50").all();
2895
2996
  db.close();
2896
2997
  if (changes.length === 0) {