@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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli.js +156 -55
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +83 -21
- 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 +5 -2
- package/dist/ui/assets/index-Lt6O9uL6.js +0 -33
package/dist/mcp.js
CHANGED
|
@@ -13,7 +13,7 @@ async function getOverview(pool2) {
|
|
|
13
13
|
try {
|
|
14
14
|
const version = await client.query("SHOW server_version");
|
|
15
15
|
const uptime = await client.query(
|
|
16
|
-
|
|
16
|
+
`SELECT to_char(now() - pg_postmaster_start_time(), 'DD "d" HH24 "h" MI "m"') AS uptime`
|
|
17
17
|
);
|
|
18
18
|
const dbSize = await client.query(
|
|
19
19
|
"SELECT pg_size_pretty(pg_database_size(current_database())) AS size"
|
|
@@ -197,17 +197,28 @@ async function getActivity(pool2) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// src/server/advisor.ts
|
|
200
|
-
|
|
200
|
+
import Database from "better-sqlite3";
|
|
201
|
+
import path from "path";
|
|
202
|
+
import os from "os";
|
|
203
|
+
import fs from "fs";
|
|
204
|
+
var SEVERITY_WEIGHT = { critical: 15, warning: 5, info: 1 };
|
|
205
|
+
var MAX_DEDUCTION = { critical: 60, warning: 30, info: 10 };
|
|
201
206
|
function computeAdvisorScore(issues) {
|
|
202
207
|
let score = 100;
|
|
208
|
+
const deductions = { critical: 0, warning: 0, info: 0 };
|
|
203
209
|
const counts = { critical: 0, warning: 0, info: 0 };
|
|
204
210
|
for (const issue of issues) {
|
|
205
211
|
counts[issue.severity]++;
|
|
206
212
|
const n = counts[issue.severity];
|
|
207
213
|
const weight = SEVERITY_WEIGHT[issue.severity];
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
else
|
|
214
|
+
let penalty;
|
|
215
|
+
if (n <= 3) penalty = weight;
|
|
216
|
+
else if (n <= 10) penalty = weight * 0.5;
|
|
217
|
+
else penalty = weight * 0.25;
|
|
218
|
+
deductions[issue.severity] += penalty;
|
|
219
|
+
}
|
|
220
|
+
for (const sev of ["critical", "warning", "info"]) {
|
|
221
|
+
score -= Math.min(deductions[sev], MAX_DEDUCTION[sev]);
|
|
211
222
|
}
|
|
212
223
|
return Math.max(0, Math.min(100, Math.round(score)));
|
|
213
224
|
}
|
|
@@ -231,7 +242,10 @@ function computeBreakdown(issues) {
|
|
|
231
242
|
async function getAdvisorReport(pool2, longQueryThreshold2 = 5) {
|
|
232
243
|
const client = await pool2.connect();
|
|
233
244
|
const issues = [];
|
|
245
|
+
const skipped = [];
|
|
234
246
|
try {
|
|
247
|
+
const versionResult = await client.query("SHOW server_version_num");
|
|
248
|
+
const pgVersion = parseInt(versionResult.rows[0].server_version_num);
|
|
235
249
|
try {
|
|
236
250
|
const r = await client.query(`
|
|
237
251
|
SELECT schemaname, relname, seq_scan, seq_tup_read, n_live_tup,
|
|
@@ -256,6 +270,7 @@ CREATE INDEX CONCURRENTLY idx_${row.relname}_<column> ON ${row.schemaname}.${row
|
|
|
256
270
|
}
|
|
257
271
|
} catch (err) {
|
|
258
272
|
console.error("[advisor] Error checking seq scans:", err.message);
|
|
273
|
+
skipped.push("seq scans: " + err.message);
|
|
259
274
|
}
|
|
260
275
|
try {
|
|
261
276
|
const r = await client.query(`
|
|
@@ -284,6 +299,7 @@ CREATE INDEX CONCURRENTLY idx_${row.relname}_<column> ON ${row.schemaname}.${row
|
|
|
284
299
|
}
|
|
285
300
|
} catch (err) {
|
|
286
301
|
console.error("[advisor] Error checking bloated indexes:", err.message);
|
|
302
|
+
skipped.push("bloated indexes: " + err.message);
|
|
287
303
|
}
|
|
288
304
|
try {
|
|
289
305
|
const r = await client.query(`
|
|
@@ -309,6 +325,7 @@ CREATE INDEX CONCURRENTLY idx_${row.relname}_<column> ON ${row.schemaname}.${row
|
|
|
309
325
|
}
|
|
310
326
|
} catch (err) {
|
|
311
327
|
console.error("[advisor] Error checking table bloat:", err.message);
|
|
328
|
+
skipped.push("table bloat: " + err.message);
|
|
312
329
|
}
|
|
313
330
|
try {
|
|
314
331
|
const r = await client.query(`
|
|
@@ -338,6 +355,7 @@ SHOW shared_buffers;`,
|
|
|
338
355
|
}
|
|
339
356
|
} catch (err) {
|
|
340
357
|
console.error("[advisor] Error checking cache efficiency:", err.message);
|
|
358
|
+
skipped.push("cache efficiency: " + err.message);
|
|
341
359
|
}
|
|
342
360
|
try {
|
|
343
361
|
const extCheck = await client.query("SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'");
|
|
@@ -366,6 +384,7 @@ SHOW shared_buffers;`,
|
|
|
366
384
|
}
|
|
367
385
|
} catch (err) {
|
|
368
386
|
console.error("[advisor] Error checking slow queries:", err.message);
|
|
387
|
+
skipped.push("slow queries: " + err.message);
|
|
369
388
|
}
|
|
370
389
|
try {
|
|
371
390
|
const r = await client.query(`
|
|
@@ -391,6 +410,7 @@ SHOW shared_buffers;`,
|
|
|
391
410
|
}
|
|
392
411
|
} catch (err) {
|
|
393
412
|
console.error("[advisor] Error checking vacuum overdue:", err.message);
|
|
413
|
+
skipped.push("vacuum overdue: " + err.message);
|
|
394
414
|
}
|
|
395
415
|
try {
|
|
396
416
|
const r = await client.query(`
|
|
@@ -419,6 +439,7 @@ SHOW shared_buffers;`,
|
|
|
419
439
|
}
|
|
420
440
|
} catch (err) {
|
|
421
441
|
console.error("[advisor] Error checking analyze overdue:", err.message);
|
|
442
|
+
skipped.push("analyze overdue: " + err.message);
|
|
422
443
|
}
|
|
423
444
|
try {
|
|
424
445
|
const r = await client.query(`
|
|
@@ -454,6 +475,7 @@ SHOW shared_buffers;`,
|
|
|
454
475
|
}
|
|
455
476
|
} catch (err) {
|
|
456
477
|
console.error("[advisor] Error checking xid wraparound:", err.message);
|
|
478
|
+
skipped.push("xid wraparound: " + err.message);
|
|
457
479
|
}
|
|
458
480
|
try {
|
|
459
481
|
const r = await client.query(`
|
|
@@ -462,9 +484,9 @@ SHOW shared_buffers;`,
|
|
|
462
484
|
extract(epoch from now() - state_change)::int AS idle_seconds
|
|
463
485
|
FROM pg_stat_activity
|
|
464
486
|
WHERE state IN ('idle', 'idle in transaction')
|
|
465
|
-
AND now() - state_change > interval '
|
|
487
|
+
AND now() - state_change > $1 * interval '1 minute'
|
|
466
488
|
AND pid != pg_backend_pid()
|
|
467
|
-
|
|
489
|
+
`, [longQueryThreshold2]);
|
|
468
490
|
for (const row of r.rows) {
|
|
469
491
|
const isIdleTx = row.state === "idle in transaction";
|
|
470
492
|
issues.push({
|
|
@@ -480,6 +502,7 @@ SHOW shared_buffers;`,
|
|
|
480
502
|
}
|
|
481
503
|
} catch (err) {
|
|
482
504
|
console.error("[advisor] Error checking idle connections:", err.message);
|
|
505
|
+
skipped.push("idle connections: " + err.message);
|
|
483
506
|
}
|
|
484
507
|
try {
|
|
485
508
|
const r = await client.query(`
|
|
@@ -505,6 +528,7 @@ SHOW shared_buffers;`,
|
|
|
505
528
|
}
|
|
506
529
|
} catch (err) {
|
|
507
530
|
console.error("[advisor] Error checking missing primary keys:", err.message);
|
|
531
|
+
skipped.push("missing primary keys: " + err.message);
|
|
508
532
|
}
|
|
509
533
|
try {
|
|
510
534
|
const r = await client.query(`
|
|
@@ -531,6 +555,7 @@ SHOW shared_buffers;`,
|
|
|
531
555
|
}
|
|
532
556
|
} catch (err) {
|
|
533
557
|
console.error("[advisor] Error checking unused indexes:", err.message);
|
|
558
|
+
skipped.push("unused indexes: " + err.message);
|
|
534
559
|
}
|
|
535
560
|
try {
|
|
536
561
|
const r = await client.query(`
|
|
@@ -556,6 +581,7 @@ DROP INDEX CONCURRENTLY ${row.indexes.slice(1).join(";\nDROP INDEX CONCURRENTLY
|
|
|
556
581
|
}
|
|
557
582
|
} catch (err) {
|
|
558
583
|
console.error("[advisor] Error checking duplicate indexes:", err.message);
|
|
584
|
+
skipped.push("duplicate indexes: " + err.message);
|
|
559
585
|
}
|
|
560
586
|
try {
|
|
561
587
|
const r = await client.query(`
|
|
@@ -586,6 +612,7 @@ DROP INDEX CONCURRENTLY ${row.indexes.slice(1).join(";\nDROP INDEX CONCURRENTLY
|
|
|
586
612
|
}
|
|
587
613
|
} catch (err) {
|
|
588
614
|
console.error("[advisor] Error checking missing FK indexes:", err.message);
|
|
615
|
+
skipped.push("missing FK indexes: " + err.message);
|
|
589
616
|
}
|
|
590
617
|
try {
|
|
591
618
|
const r = await client.query(`
|
|
@@ -621,6 +648,7 @@ DROP INDEX CONCURRENTLY ${row.indexes.slice(1).join(";\nDROP INDEX CONCURRENTLY
|
|
|
621
648
|
}
|
|
622
649
|
} catch (err) {
|
|
623
650
|
console.error("[advisor] Error checking locks:", err.message);
|
|
651
|
+
skipped.push("locks: " + err.message);
|
|
624
652
|
}
|
|
625
653
|
try {
|
|
626
654
|
const r = await client.query(`
|
|
@@ -644,13 +672,15 @@ SELECT * FROM pg_stat_replication;`,
|
|
|
644
672
|
}
|
|
645
673
|
} catch (err) {
|
|
646
674
|
console.error("[advisor] Error checking replication lag:", err.message);
|
|
675
|
+
skipped.push("replication lag: " + err.message);
|
|
647
676
|
}
|
|
648
677
|
try {
|
|
678
|
+
const checkpointView = pgVersion >= 17e4 ? "pg_stat_checkpointer" : "pg_stat_bgwriter";
|
|
649
679
|
const r = await client.query(`
|
|
650
680
|
SELECT checkpoints_req, checkpoints_timed,
|
|
651
681
|
CASE WHEN (checkpoints_req + checkpoints_timed) = 0 THEN 0
|
|
652
682
|
ELSE round(checkpoints_req::numeric / (checkpoints_req + checkpoints_timed) * 100, 1) END AS req_pct
|
|
653
|
-
FROM
|
|
683
|
+
FROM ${checkpointView}
|
|
654
684
|
`);
|
|
655
685
|
const reqPct = parseFloat(r.rows[0]?.req_pct ?? "0");
|
|
656
686
|
if (reqPct > 50) {
|
|
@@ -669,6 +699,7 @@ SELECT pg_reload_conf();`,
|
|
|
669
699
|
}
|
|
670
700
|
} catch (err) {
|
|
671
701
|
console.error("[advisor] Error checking checkpoint frequency:", err.message);
|
|
702
|
+
skipped.push("checkpoint frequency: " + err.message);
|
|
672
703
|
}
|
|
673
704
|
try {
|
|
674
705
|
const r = await client.query(`SELECT setting FROM pg_settings WHERE name = 'autovacuum'`);
|
|
@@ -687,6 +718,7 @@ SELECT pg_reload_conf();`,
|
|
|
687
718
|
}
|
|
688
719
|
} catch (err) {
|
|
689
720
|
console.error("[advisor] Error checking autovacuum:", err.message);
|
|
721
|
+
skipped.push("autovacuum: " + err.message);
|
|
690
722
|
}
|
|
691
723
|
try {
|
|
692
724
|
const sbRes = await client.query(`SELECT setting, unit FROM pg_settings WHERE name = 'shared_buffers'`);
|
|
@@ -710,6 +742,7 @@ SELECT pg_reload_conf();`,
|
|
|
710
742
|
}
|
|
711
743
|
} catch (err) {
|
|
712
744
|
console.error("[advisor] Error checking shared_buffers:", err.message);
|
|
745
|
+
skipped.push("shared_buffers: " + err.message);
|
|
713
746
|
}
|
|
714
747
|
try {
|
|
715
748
|
const r = await client.query(`SELECT setting, unit FROM pg_settings WHERE name = 'work_mem'`);
|
|
@@ -729,6 +762,7 @@ SELECT pg_reload_conf();`,
|
|
|
729
762
|
}
|
|
730
763
|
} catch (err) {
|
|
731
764
|
console.error("[advisor] Error checking work_mem:", err.message);
|
|
765
|
+
skipped.push("work_mem: " + err.message);
|
|
732
766
|
}
|
|
733
767
|
try {
|
|
734
768
|
const r = await client.query(`
|
|
@@ -754,6 +788,7 @@ SELECT pg_reload_conf();`,
|
|
|
754
788
|
}
|
|
755
789
|
} catch (err) {
|
|
756
790
|
console.error("[advisor] Error checking superuser connections:", err.message);
|
|
791
|
+
skipped.push("superuser connections: " + err.message);
|
|
757
792
|
}
|
|
758
793
|
try {
|
|
759
794
|
const r = await client.query(`SELECT setting FROM pg_settings WHERE name = 'ssl'`);
|
|
@@ -775,6 +810,7 @@ SELECT pg_reload_conf();`,
|
|
|
775
810
|
}
|
|
776
811
|
} catch (err) {
|
|
777
812
|
console.error("[advisor] Error checking SSL check:", err.message);
|
|
813
|
+
skipped.push("SSL check: " + err.message);
|
|
778
814
|
}
|
|
779
815
|
try {
|
|
780
816
|
const r = await client.query(`
|
|
@@ -798,18 +834,44 @@ SELECT pg_reload_conf();`,
|
|
|
798
834
|
}
|
|
799
835
|
} catch (err) {
|
|
800
836
|
console.error("[advisor] Error checking trust auth:", err.message);
|
|
837
|
+
skipped.push("trust auth: " + err.message);
|
|
801
838
|
}
|
|
802
|
-
const
|
|
839
|
+
const ignoredIds = getIgnoredIssues();
|
|
840
|
+
const ignoredSet = new Set(ignoredIds);
|
|
841
|
+
const activeIssues = issues.filter((i) => !ignoredSet.has(i.id));
|
|
842
|
+
const ignoredCount = issues.length - activeIssues.length;
|
|
843
|
+
const score = computeAdvisorScore(activeIssues);
|
|
803
844
|
return {
|
|
804
845
|
score,
|
|
805
846
|
grade: gradeFromScore(score),
|
|
806
|
-
issues,
|
|
807
|
-
breakdown: computeBreakdown(
|
|
847
|
+
issues: activeIssues,
|
|
848
|
+
breakdown: computeBreakdown(activeIssues),
|
|
849
|
+
skipped,
|
|
850
|
+
ignoredCount
|
|
808
851
|
};
|
|
809
852
|
} finally {
|
|
810
853
|
client.release();
|
|
811
854
|
}
|
|
812
855
|
}
|
|
856
|
+
var _ignoredDb = null;
|
|
857
|
+
function getIgnoredDb() {
|
|
858
|
+
if (_ignoredDb) return _ignoredDb;
|
|
859
|
+
const dataDir2 = process.env.PG_DASH_DATA_DIR || path.join(os.homedir(), ".pg-dash");
|
|
860
|
+
fs.mkdirSync(dataDir2, { recursive: true });
|
|
861
|
+
const dbPath = path.join(dataDir2, "alerts.db");
|
|
862
|
+
_ignoredDb = new Database(dbPath);
|
|
863
|
+
_ignoredDb.pragma("journal_mode = WAL");
|
|
864
|
+
_ignoredDb.exec("CREATE TABLE IF NOT EXISTS ignored_issues (issue_id TEXT PRIMARY KEY, ignored_at INTEGER)");
|
|
865
|
+
return _ignoredDb;
|
|
866
|
+
}
|
|
867
|
+
function getIgnoredIssues() {
|
|
868
|
+
try {
|
|
869
|
+
const db = getIgnoredDb();
|
|
870
|
+
return db.prepare("SELECT issue_id FROM ignored_issues").all().map((r) => r.issue_id);
|
|
871
|
+
} catch {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
}
|
|
813
875
|
function isSafeFix(sql) {
|
|
814
876
|
const trimmed = sql.trim();
|
|
815
877
|
if (!trimmed) return false;
|
|
@@ -833,10 +895,10 @@ function isSafeFix(sql) {
|
|
|
833
895
|
}
|
|
834
896
|
|
|
835
897
|
// src/mcp.ts
|
|
836
|
-
import
|
|
837
|
-
import
|
|
838
|
-
import
|
|
839
|
-
import
|
|
898
|
+
import Database2 from "better-sqlite3";
|
|
899
|
+
import path2 from "path";
|
|
900
|
+
import os2 from "os";
|
|
901
|
+
import fs2, { readFileSync } from "fs";
|
|
840
902
|
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
841
903
|
var connString = process.argv[2] || process.env.PG_DASH_CONNECTION_STRING;
|
|
842
904
|
if (!connString) {
|
|
@@ -846,19 +908,19 @@ if (!connString) {
|
|
|
846
908
|
}
|
|
847
909
|
var pool = new Pool({ connectionString: connString });
|
|
848
910
|
var longQueryThreshold = parseInt(process.env.PG_DASH_LONG_QUERY_THRESHOLD || "5", 10);
|
|
849
|
-
var dataDir = process.env.PG_DASH_DATA_DIR ||
|
|
850
|
-
|
|
911
|
+
var dataDir = process.env.PG_DASH_DATA_DIR || path2.join(os2.homedir(), ".pg-dash");
|
|
912
|
+
fs2.mkdirSync(dataDir, { recursive: true });
|
|
851
913
|
var schemaDb = null;
|
|
852
914
|
var alertsDb = null;
|
|
853
915
|
try {
|
|
854
|
-
const schemaPath =
|
|
855
|
-
if (
|
|
916
|
+
const schemaPath = path2.join(dataDir, "schema.db");
|
|
917
|
+
if (fs2.existsSync(schemaPath)) schemaDb = new Database2(schemaPath, { readonly: true });
|
|
856
918
|
} catch (err) {
|
|
857
919
|
console.error("[mcp] Error:", err.message);
|
|
858
920
|
}
|
|
859
921
|
try {
|
|
860
|
-
const alertsPath =
|
|
861
|
-
if (
|
|
922
|
+
const alertsPath = path2.join(dataDir, "alerts.db");
|
|
923
|
+
if (fs2.existsSync(alertsPath)) alertsDb = new Database2(alertsPath, { readonly: true });
|
|
862
924
|
} catch (err) {
|
|
863
925
|
console.error("[mcp] Error:", err.message);
|
|
864
926
|
}
|