@indiekitai/pg-dash 0.3.1 → 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/dist/cli.js +147 -47
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +81 -19
- package/dist/mcp.js.map +1 -1
- package/dist/ui/assets/index-BI4_c1SD.js +33 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-Lt6O9uL6.js +0 -33
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
else
|
|
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(`
|
|
@@ -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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
667
|
-
import
|
|
668
|
-
import
|
|
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
|
-
|
|
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
|
|
785
|
-
import
|
|
786
|
-
import
|
|
787
|
-
import
|
|
788
|
-
var DEFAULT_DIR =
|
|
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
|
|
868
|
+
if (dbOrDir instanceof Database2) {
|
|
796
869
|
this.db = dbOrDir;
|
|
797
870
|
} else {
|
|
798
871
|
const dir = dbOrDir || DEFAULT_DIR;
|
|
799
|
-
|
|
800
|
-
const dbPath =
|
|
801
|
-
this.db = new
|
|
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");
|
|
@@ -1831,6 +1904,33 @@ function registerAdvisorRoutes(app, pool, longQueryThreshold) {
|
|
|
1831
1904
|
return c.json({ error: err.message }, 500);
|
|
1832
1905
|
}
|
|
1833
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
|
+
});
|
|
1834
1934
|
app.post("/api/fix", async (c) => {
|
|
1835
1935
|
try {
|
|
1836
1936
|
const body = await c.req.json();
|
|
@@ -2411,10 +2511,10 @@ function registerQueryStatsRoutes(app, store) {
|
|
|
2411
2511
|
}
|
|
2412
2512
|
|
|
2413
2513
|
// src/server/index.ts
|
|
2414
|
-
import
|
|
2514
|
+
import Database3 from "better-sqlite3";
|
|
2415
2515
|
import { WebSocketServer, WebSocket } from "ws";
|
|
2416
2516
|
import http from "http";
|
|
2417
|
-
var __dirname =
|
|
2517
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
2418
2518
|
async function startServer(opts) {
|
|
2419
2519
|
const pool = new Pool({ connectionString: opts.connectionString });
|
|
2420
2520
|
try {
|
|
@@ -2442,25 +2542,25 @@ async function startServer(opts) {
|
|
|
2442
2542
|
await pool.end();
|
|
2443
2543
|
process.exit(0);
|
|
2444
2544
|
}
|
|
2445
|
-
const dataDir = opts.dataDir ||
|
|
2446
|
-
|
|
2447
|
-
const metricsDbPath =
|
|
2448
|
-
const metricsDb = new
|
|
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);
|
|
2449
2549
|
metricsDb.pragma("journal_mode = WAL");
|
|
2450
2550
|
const store = new TimeseriesStore(metricsDb, opts.retentionDays);
|
|
2451
2551
|
const intervalMs = (opts.interval || 30) * 1e3;
|
|
2452
2552
|
const collector = new Collector(pool, store, intervalMs);
|
|
2453
2553
|
console.log(` Collecting metrics every ${intervalMs / 1e3}s...`);
|
|
2454
2554
|
collector.start();
|
|
2455
|
-
const schemaDbPath =
|
|
2456
|
-
const schemaDb = new
|
|
2555
|
+
const schemaDbPath = path3.join(dataDir, "schema.db");
|
|
2556
|
+
const schemaDb = new Database3(schemaDbPath);
|
|
2457
2557
|
schemaDb.pragma("journal_mode = WAL");
|
|
2458
2558
|
const snapshotIntervalMs = (opts.snapshotInterval || 6) * 60 * 60 * 1e3;
|
|
2459
2559
|
const schemaTracker = new SchemaTracker(schemaDb, pool, snapshotIntervalMs);
|
|
2460
2560
|
schemaTracker.start();
|
|
2461
2561
|
console.log(" Schema change tracking enabled");
|
|
2462
|
-
const alertsDbPath =
|
|
2463
|
-
const alertsDb = new
|
|
2562
|
+
const alertsDbPath = path3.join(dataDir, "alerts.db");
|
|
2563
|
+
const alertsDb = new Database3(alertsDbPath);
|
|
2464
2564
|
alertsDb.pragma("journal_mode = WAL");
|
|
2465
2565
|
const alertManager = new AlertManager(alertsDb, opts.webhook);
|
|
2466
2566
|
console.log(" Alert monitoring enabled");
|
|
@@ -2516,7 +2616,7 @@ async function startServer(opts) {
|
|
|
2516
2616
|
registerExplainRoutes(app, pool);
|
|
2517
2617
|
registerDiskRoutes(app, pool, store);
|
|
2518
2618
|
registerQueryStatsRoutes(app, queryStatsStore);
|
|
2519
|
-
const uiPath =
|
|
2619
|
+
const uiPath = path3.resolve(__dirname, "ui");
|
|
2520
2620
|
const MIME_TYPES = {
|
|
2521
2621
|
".html": "text/html",
|
|
2522
2622
|
".js": "application/javascript",
|
|
@@ -2531,15 +2631,15 @@ async function startServer(opts) {
|
|
|
2531
2631
|
};
|
|
2532
2632
|
app.get("/*", async (c) => {
|
|
2533
2633
|
const urlPath = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
2534
|
-
const filePath =
|
|
2634
|
+
const filePath = path3.join(uiPath, urlPath);
|
|
2535
2635
|
try {
|
|
2536
|
-
const content = await
|
|
2537
|
-
const ext =
|
|
2636
|
+
const content = await fs3.promises.readFile(filePath);
|
|
2637
|
+
const ext = path3.extname(filePath);
|
|
2538
2638
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2539
2639
|
return new Response(content, { headers: { "content-type": contentType } });
|
|
2540
2640
|
} catch {
|
|
2541
2641
|
try {
|
|
2542
|
-
const html = await
|
|
2642
|
+
const html = await fs3.promises.readFile(path3.join(uiPath, "index.html"));
|
|
2543
2643
|
return new Response(html, { headers: { "content-type": "text/html" } });
|
|
2544
2644
|
} catch (err) {
|
|
2545
2645
|
console.error("[static] Error reading index.html:", err.message);
|
|
@@ -2738,8 +2838,8 @@ async function startServer(opts) {
|
|
|
2738
2838
|
}
|
|
2739
2839
|
|
|
2740
2840
|
// src/cli.ts
|
|
2741
|
-
import
|
|
2742
|
-
import
|
|
2841
|
+
import fs4 from "fs";
|
|
2842
|
+
import path4 from "path";
|
|
2743
2843
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2744
2844
|
process.on("uncaughtException", (err) => {
|
|
2745
2845
|
console.error("Uncaught exception:", err);
|
|
@@ -2778,8 +2878,8 @@ var { values, positionals } = parseArgs({
|
|
|
2778
2878
|
});
|
|
2779
2879
|
if (values.version) {
|
|
2780
2880
|
try {
|
|
2781
|
-
const __dirname2 =
|
|
2782
|
-
const pkg = JSON.parse(
|
|
2881
|
+
const __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
|
|
2882
|
+
const pkg = JSON.parse(fs4.readFileSync(path4.resolve(__dirname2, "../package.json"), "utf-8"));
|
|
2783
2883
|
console.log(`pg-dash v${pkg.version}`);
|
|
2784
2884
|
} catch {
|
|
2785
2885
|
console.log("pg-dash v0.1.0");
|
|
@@ -2884,14 +2984,14 @@ if (subcommand === "check") {
|
|
|
2884
2984
|
}
|
|
2885
2985
|
} else if (subcommand === "schema-diff") {
|
|
2886
2986
|
const connectionString = resolveConnectionString(1);
|
|
2887
|
-
const dataDir = values["data-dir"] ||
|
|
2888
|
-
const schemaDbPath =
|
|
2889
|
-
if (!
|
|
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)) {
|
|
2890
2990
|
console.error("No schema tracking data found. Run pg-dash server first to collect schema snapshots.");
|
|
2891
2991
|
process.exit(1);
|
|
2892
2992
|
}
|
|
2893
|
-
const
|
|
2894
|
-
const db = new
|
|
2993
|
+
const Database4 = (await import("better-sqlite3")).default;
|
|
2994
|
+
const db = new Database4(schemaDbPath, { readonly: true });
|
|
2895
2995
|
const changes = db.prepare("SELECT * FROM schema_changes ORDER BY timestamp DESC LIMIT 50").all();
|
|
2896
2996
|
db.close();
|
|
2897
2997
|
if (changes.length === 0) {
|