@indiekitai/pg-dash 0.4.4 → 0.4.6

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 CHANGED
@@ -300,30 +300,44 @@ SHOW shared_buffers;`,
300
300
  }
301
301
  try {
302
302
  const r = await client.query(`
303
- SELECT pid, state, now() - state_change AS idle_duration,
303
+ SELECT pid,
304
304
  client_addr::text, application_name,
305
305
  extract(epoch from now() - state_change)::int AS idle_seconds
306
306
  FROM pg_stat_activity
307
- WHERE state IN ('idle', 'idle in transaction')
307
+ WHERE state = 'idle in transaction'
308
308
  AND now() - state_change > $1 * interval '1 minute'
309
309
  AND pid != pg_backend_pid()
310
+ ORDER BY idle_seconds DESC
310
311
  `, [longQueryThreshold]);
311
- for (const row of r.rows) {
312
- const isIdleTx = row.state === "idle in transaction";
312
+ if (r.rows.length === 1) {
313
+ const row = r.rows[0];
313
314
  issues.push({
314
- id: `maint-idle-${row.pid}`,
315
- severity: isIdleTx ? "warning" : "info",
315
+ id: `maint-idle-tx-${row.pid}`,
316
+ severity: "warning",
316
317
  category: "maintenance",
317
- title: `${isIdleTx ? "Idle in transaction" : "Idle connection"} (PID ${row.pid})`,
318
- description: `PID ${row.pid} from ${row.client_addr || "local"} (${row.application_name || "unknown"}) has been ${row.state} for ${Math.round(row.idle_seconds / 60)} minutes.`,
318
+ title: `Idle-in-transaction connection (PID ${row.pid})`,
319
+ description: `PID ${row.pid} from ${row.client_addr || "local"} (${row.application_name || "unknown"}) has been idle in transaction for ${Math.round(row.idle_seconds / 60)} minutes. This holds locks and blocks VACUUM.`,
319
320
  fix: `SELECT pg_terminate_backend(${row.pid});`,
320
- impact: isIdleTx ? "Idle-in-transaction connections hold locks and prevent VACUUM." : "Idle connections consume connection slots.",
321
+ impact: "Idle-in-transaction connections hold locks and prevent VACUUM.",
322
+ effort: "quick"
323
+ });
324
+ } else if (r.rows.length > 1) {
325
+ const pids = r.rows.map((row) => row.pid);
326
+ const maxMin = Math.round(r.rows[0].idle_seconds / 60);
327
+ issues.push({
328
+ id: `maint-idle-tx-multi`,
329
+ severity: "warning",
330
+ category: "maintenance",
331
+ title: `${r.rows.length} idle-in-transaction connections (longest: ${maxMin}m)`,
332
+ description: `${r.rows.length} connections have been idle in transaction for over ${longQueryThreshold} minutes. These hold locks and prevent VACUUM from running.`,
333
+ fix: `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - state_change > interval '${longQueryThreshold} minutes';`,
334
+ impact: "Idle-in-transaction connections hold locks and prevent VACUUM.",
321
335
  effort: "quick"
322
336
  });
323
337
  }
324
338
  } catch (err) {
325
- console.error("[advisor] Error checking idle connections:", err.message);
326
- skipped.push("idle connections: " + err.message);
339
+ console.error("[advisor] Error checking idle-in-transaction connections:", err.message);
340
+ skipped.push("idle-in-transaction: " + err.message);
327
341
  }
328
342
  try {
329
343
  const r = await client.query(`
@@ -2741,7 +2755,7 @@ var RANGE_MAP = {
2741
2755
  "7d": 7 * 24 * 60 * 60 * 1e3
2742
2756
  };
2743
2757
  function registerMetricsRoutes(app, store, collector) {
2744
- app.get("/api/metrics", (c) => {
2758
+ app.get("/api/metrics", async (c) => {
2745
2759
  try {
2746
2760
  const metric = c.req.query("metric");
2747
2761
  const range = c.req.query("range") || "1h";
@@ -2754,7 +2768,7 @@ function registerMetricsRoutes(app, store, collector) {
2754
2768
  return c.json({ error: err.message }, 500);
2755
2769
  }
2756
2770
  });
2757
- app.get("/api/metrics/latest", (_c) => {
2771
+ app.get("/api/metrics/latest", async (_c) => {
2758
2772
  try {
2759
2773
  const snapshot = collector.getLastSnapshot();
2760
2774
  return _c.json(snapshot);
@@ -2845,7 +2859,7 @@ function registerAdvisorRoutes(app, pool, longQueryThreshold, store) {
2845
2859
  return c.json({ error: err.message }, 500);
2846
2860
  }
2847
2861
  });
2848
- app.get("/api/advisor/ignored", (c) => {
2862
+ app.get("/api/advisor/ignored", async (c) => {
2849
2863
  try {
2850
2864
  return c.json(getIgnoredIssues());
2851
2865
  } catch (err) {
@@ -2863,7 +2877,7 @@ function registerAdvisorRoutes(app, pool, longQueryThreshold, store) {
2863
2877
  return c.json({ error: err.message }, 500);
2864
2878
  }
2865
2879
  });
2866
- app.delete("/api/advisor/ignore/:issueId", (c) => {
2880
+ app.delete("/api/advisor/ignore/:issueId", async (c) => {
2867
2881
  try {
2868
2882
  const issueId = c.req.param("issueId");
2869
2883
  unignoreIssue(issueId);
@@ -2872,7 +2886,7 @@ function registerAdvisorRoutes(app, pool, longQueryThreshold, store) {
2872
2886
  return c.json({ error: err.message }, 500);
2873
2887
  }
2874
2888
  });
2875
- app.get("/api/advisor/history", (c) => {
2889
+ app.get("/api/advisor/history", async (c) => {
2876
2890
  if (!store) return c.json([]);
2877
2891
  try {
2878
2892
  const range = c.req.query("range") || "24h";
@@ -2953,7 +2967,7 @@ function registerSchemaRoutes(app, pool, schemaTracker) {
2953
2967
  return c.json({ error: err.message }, 500);
2954
2968
  }
2955
2969
  });
2956
- app.get("/api/schema/history", (c) => {
2970
+ app.get("/api/schema/history", async (c) => {
2957
2971
  try {
2958
2972
  const limit = parseInt(c.req.query("limit") || "30");
2959
2973
  return c.json(schemaTracker.getHistory(limit));
@@ -2961,7 +2975,7 @@ function registerSchemaRoutes(app, pool, schemaTracker) {
2961
2975
  return c.json({ error: err.message }, 500);
2962
2976
  }
2963
2977
  });
2964
- app.get("/api/schema/changes", (c) => {
2978
+ app.get("/api/schema/changes", async (c) => {
2965
2979
  try {
2966
2980
  const since = c.req.query("since");
2967
2981
  return c.json(schemaTracker.getChanges(since ? parseInt(since) : void 0));
@@ -2969,14 +2983,14 @@ function registerSchemaRoutes(app, pool, schemaTracker) {
2969
2983
  return c.json({ error: err.message }, 500);
2970
2984
  }
2971
2985
  });
2972
- app.get("/api/schema/changes/latest", (c) => {
2986
+ app.get("/api/schema/changes/latest", async (c) => {
2973
2987
  try {
2974
2988
  return c.json(schemaTracker.getLatestChanges());
2975
2989
  } catch (err) {
2976
2990
  return c.json({ error: err.message }, 500);
2977
2991
  }
2978
2992
  });
2979
- app.get("/api/schema/diff", (c) => {
2993
+ app.get("/api/schema/diff", async (c) => {
2980
2994
  try {
2981
2995
  const from = parseInt(c.req.query("from") || "0");
2982
2996
  const to = parseInt(c.req.query("to") || "0");
@@ -3000,7 +3014,7 @@ function registerSchemaRoutes(app, pool, schemaTracker) {
3000
3014
 
3001
3015
  // src/server/routes/alerts.ts
3002
3016
  function registerAlertsRoutes(app, alertManager) {
3003
- app.get("/api/alerts/rules", (c) => {
3017
+ app.get("/api/alerts/rules", async (c) => {
3004
3018
  try {
3005
3019
  return c.json(alertManager.getRules());
3006
3020
  } catch (err) {
@@ -3027,7 +3041,7 @@ function registerAlertsRoutes(app, alertManager) {
3027
3041
  return c.json({ error: err.message }, 500);
3028
3042
  }
3029
3043
  });
3030
- app.delete("/api/alerts/rules/:id", (c) => {
3044
+ app.delete("/api/alerts/rules/:id", async (c) => {
3031
3045
  try {
3032
3046
  const id = parseInt(c.req.param("id"));
3033
3047
  const ok = alertManager.deleteRule(id);
@@ -3037,7 +3051,7 @@ function registerAlertsRoutes(app, alertManager) {
3037
3051
  return c.json({ error: err.message }, 500);
3038
3052
  }
3039
3053
  });
3040
- app.get("/api/alerts/webhook-info", (c) => {
3054
+ app.get("/api/alerts/webhook-info", async (c) => {
3041
3055
  try {
3042
3056
  const url = alertManager.getWebhookUrl();
3043
3057
  const type = alertManager.getWebhookType();
@@ -3055,7 +3069,7 @@ function registerAlertsRoutes(app, alertManager) {
3055
3069
  return c.json({ error: err.message }, 500);
3056
3070
  }
3057
3071
  });
3058
- app.get("/api/alerts/history", (c) => {
3072
+ app.get("/api/alerts/history", async (c) => {
3059
3073
  try {
3060
3074
  const limit = parseInt(c.req.query("limit") || "50");
3061
3075
  return c.json(alertManager.getHistory(limit));
@@ -3335,8 +3349,13 @@ function registerDiskRoutes(app, pool, store) {
3335
3349
  (SELECT setting FROM pg_settings WHERE name = 'data_directory') AS data_dir
3336
3350
  `);
3337
3351
  const { db_size, data_dir } = dbRes.rows[0];
3338
- const tsRes = await client.query(`SELECT spcname, pg_tablespace_size(oid) AS size FROM pg_tablespace`);
3339
- const tablespaces = tsRes.rows.map((r) => ({
3352
+ const tsRes = await client.query(`
3353
+ SELECT spcname,
3354
+ CASE WHEN has_tablespace_privilege(spcname, 'CREATE')
3355
+ THEN pg_tablespace_size(oid) ELSE NULL END AS size
3356
+ FROM pg_tablespace
3357
+ `);
3358
+ const tablespaces = tsRes.rows.filter((r) => r.size !== null).map((r) => ({
3340
3359
  name: r.spcname,
3341
3360
  size: parseInt(r.size)
3342
3361
  }));
@@ -3369,7 +3388,7 @@ function registerDiskRoutes(app, pool, store) {
3369
3388
  return c.json({ error: err.message }, 500);
3370
3389
  }
3371
3390
  });
3372
- app.get("/api/disk/prediction", (c) => {
3391
+ app.get("/api/disk/prediction", async (c) => {
3373
3392
  try {
3374
3393
  const days = parseInt(c.req.query("days") || "30");
3375
3394
  const maxDisk = c.req.query("maxDisk") ? parseInt(c.req.query("maxDisk")) : void 0;
@@ -3379,7 +3398,7 @@ function registerDiskRoutes(app, pool, store) {
3379
3398
  return c.json({ error: err.message }, 500);
3380
3399
  }
3381
3400
  });
3382
- app.get("/api/disk/table-history/:table", (c) => {
3401
+ app.get("/api/disk/table-history/:table", async (c) => {
3383
3402
  try {
3384
3403
  const table = c.req.param("table");
3385
3404
  const range = c.req.query("range") || "24h";
@@ -3391,7 +3410,7 @@ function registerDiskRoutes(app, pool, store) {
3391
3410
  return c.json({ error: err.message }, 500);
3392
3411
  }
3393
3412
  });
3394
- app.get("/api/disk/history", (c) => {
3413
+ app.get("/api/disk/history", async (c) => {
3395
3414
  try {
3396
3415
  const range = c.req.query("range") || "24h";
3397
3416
  const rangeMs = RANGE_MAP3[range] || RANGE_MAP3["24h"];
@@ -3600,7 +3619,7 @@ var RANGE_MAP4 = {
3600
3619
  "7d": 7 * 24 * 60 * 60 * 1e3
3601
3620
  };
3602
3621
  function registerQueryStatsRoutes(app, store) {
3603
- app.get("/api/query-stats/top", (c) => {
3622
+ app.get("/api/query-stats/top", async (c) => {
3604
3623
  try {
3605
3624
  const range = c.req.query("range") || "1h";
3606
3625
  const orderBy = c.req.query("orderBy") || "total_time";
@@ -3613,7 +3632,7 @@ function registerQueryStatsRoutes(app, store) {
3613
3632
  return c.json({ error: err.message }, 500);
3614
3633
  }
3615
3634
  });
3616
- app.get("/api/query-stats/trend/:queryid", (c) => {
3635
+ app.get("/api/query-stats/trend/:queryid", async (c) => {
3617
3636
  try {
3618
3637
  const queryid = c.req.param("queryid");
3619
3638
  const range = c.req.query("range") || "1h";