ezpm2gui 1.5.0 → 1.8.0
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/LICENSE +661 -0
- package/README.md +330 -295
- package/bin/ezpm2gui.js +10 -10
- package/bin/ezpm2gui.ts +51 -51
- package/bin/generate-ecosystem.js +36 -36
- package/bin/generate-ecosystem.ts +56 -56
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server/config/cron-jobs.json +1 -0
- package/dist/server/config/project-configs.json +235 -236
- package/dist/server/config/remote-connections.json +3 -0
- package/dist/server/index.js +44 -3
- package/dist/server/routes/deployApplication.js +47 -45
- package/dist/server/routes/logStreaming.js +31 -24
- package/dist/server/routes/modules.js +55 -0
- package/dist/server/routes/pageAuth.d.ts +3 -0
- package/dist/server/routes/pageAuth.js +177 -0
- package/dist/server/routes/remoteConnections.js +13 -9
- package/dist/server/routes/remoteMetrics.d.ts +3 -0
- package/dist/server/routes/remoteMetrics.js +84 -0
- package/dist/server/services/ProjectSetupService.d.ts +1 -1
- package/dist/server/services/ProjectSetupService.js +25 -9
- package/dist/server/utils/metrics-history.d.ts +21 -0
- package/dist/server/utils/metrics-history.js +68 -0
- package/dist/server/utils/remote-metrics-db.d.ts +29 -0
- package/dist/server/utils/remote-metrics-db.js +134 -0
- package/dist/server/utils/remote-metrics-poller.d.ts +8 -0
- package/dist/server/utils/remote-metrics-poller.js +67 -0
- package/package.json +86 -73
- package/scripts/postinstall.js +36 -36
- package/src/client/build/asset-manifest.json +6 -6
- package/src/client/build/favicon.ico +2 -2
- package/src/client/build/index.html +1 -1
- package/src/client/build/logo192.svg +7 -7
- package/src/client/build/logo512.svg +7 -7
- package/src/client/build/manifest.json +24 -24
- package/src/client/build/static/css/main.9decb204.css +5 -0
- package/src/client/build/static/css/main.9decb204.css.map +1 -0
- package/src/client/build/static/js/main.28a4a583.js +3 -0
- package/src/client/build/static/js/main.28a4a583.js.map +1 -0
- package/src/client/build/static/css/main.2d095544.css +0 -5
- package/src/client/build/static/css/main.2d095544.css.map +0 -1
- package/src/client/build/static/js/main.17e17668.js +0 -3
- package/src/client/build/static/js/main.17e17668.js.map +0 -1
- /package/src/client/build/static/js/{main.17e17668.js.LICENSE.txt → main.28a4a583.js.LICENSE.txt} +0 -0
|
@@ -78,8 +78,9 @@ class ProjectSetupService {
|
|
|
78
78
|
this.log('Could not detect project type');
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
-
async setupProject(projectPath, projectType) {
|
|
81
|
+
async setupProject(projectPath, projectType, onLog) {
|
|
82
82
|
this.log(`Starting setup for ${projectType} project at: ${projectPath}`);
|
|
83
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`Starting ${projectType} project setup at: ${projectPath}`);
|
|
83
84
|
const config = this.configs[projectType];
|
|
84
85
|
if (!config) {
|
|
85
86
|
throw new Error(`Unknown project type: ${projectType}`);
|
|
@@ -96,6 +97,7 @@ class ProjectSetupService {
|
|
|
96
97
|
try {
|
|
97
98
|
// Check if step should be skipped
|
|
98
99
|
if (this.shouldSkipStep(step, projectPath)) {
|
|
100
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[SKIP] ${step.name}`);
|
|
99
101
|
result.steps.push({
|
|
100
102
|
name: step.name,
|
|
101
103
|
success: true,
|
|
@@ -106,24 +108,31 @@ class ProjectSetupService {
|
|
|
106
108
|
continue;
|
|
107
109
|
}
|
|
108
110
|
this.log(`Executing step: ${step.name}`);
|
|
109
|
-
|
|
111
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`\n[STEP] ${step.name}`);
|
|
112
|
+
const stepResult = await this.executeStep(step, projectPath, result.environment, onLog);
|
|
110
113
|
const duration = Date.now() - stepStart;
|
|
111
114
|
result.steps.push({
|
|
112
115
|
...stepResult,
|
|
113
116
|
duration
|
|
114
117
|
});
|
|
115
118
|
if (!stepResult.success && step.required) {
|
|
119
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] Step failed: ${step.name}`);
|
|
116
120
|
result.success = false;
|
|
117
121
|
result.errors.push(`Required step failed: ${step.name} - ${stepResult.error}`);
|
|
118
122
|
break;
|
|
119
123
|
}
|
|
120
124
|
else if (!stepResult.success) {
|
|
125
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] Optional step failed: ${step.name}`);
|
|
121
126
|
result.warnings.push(`Optional step failed: ${step.name} - ${stepResult.error}`);
|
|
122
127
|
}
|
|
128
|
+
else {
|
|
129
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[OK] ${step.name} completed (${duration}ms)`);
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
catch (error) {
|
|
125
133
|
const duration = Date.now() - stepStart;
|
|
126
134
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
135
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[ERROR] ${step.name}: ${errorMessage}`);
|
|
127
136
|
result.steps.push({
|
|
128
137
|
name: step.name,
|
|
129
138
|
success: false,
|
|
@@ -152,13 +161,13 @@ class ProjectSetupService {
|
|
|
152
161
|
result.environment.PYTHON_INTERPRETER = venvPath;
|
|
153
162
|
}
|
|
154
163
|
}
|
|
155
|
-
// Validate the setup
|
|
164
|
+
// Validate the setup (advisory only — step success/failure is the real gate)
|
|
156
165
|
const validationResult = await this.validateSetup(projectPath, config);
|
|
157
166
|
if (!validationResult.success) {
|
|
158
|
-
result.
|
|
159
|
-
result.errors.push(...validationResult.errors);
|
|
167
|
+
result.warnings.push(...validationResult.errors);
|
|
160
168
|
}
|
|
161
169
|
this.log(`Setup completed. Success: ${result.success}`);
|
|
170
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`\nSetup ${result.success ? 'completed successfully' : 'failed'}.`);
|
|
162
171
|
return result;
|
|
163
172
|
}
|
|
164
173
|
shouldSkipStep(step, projectPath) {
|
|
@@ -216,7 +225,7 @@ class ProjectSetupService {
|
|
|
216
225
|
}
|
|
217
226
|
return false;
|
|
218
227
|
}
|
|
219
|
-
async executeStep(step, projectPath, environment) {
|
|
228
|
+
async executeStep(step, projectPath, environment, onLog) {
|
|
220
229
|
const workingDir = step.workingDirectory === 'project' ? projectPath : process.cwd();
|
|
221
230
|
let command = step.command;
|
|
222
231
|
// Handle virtual environment activation for Python
|
|
@@ -245,13 +254,20 @@ class ProjectSetupService {
|
|
|
245
254
|
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
246
255
|
const text = data.toString();
|
|
247
256
|
output += text;
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
for (const line of text.split('\n')) {
|
|
258
|
+
const trimmed = line.trim();
|
|
259
|
+
if (trimmed)
|
|
260
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(trimmed);
|
|
261
|
+
}
|
|
250
262
|
});
|
|
251
263
|
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
252
264
|
const text = data.toString();
|
|
253
265
|
error += text;
|
|
254
|
-
|
|
266
|
+
for (const line of text.split('\n')) {
|
|
267
|
+
const trimmed = line.trim();
|
|
268
|
+
if (trimmed)
|
|
269
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`[WARN] ${trimmed}`);
|
|
270
|
+
}
|
|
255
271
|
});
|
|
256
272
|
child.on('close', (code) => {
|
|
257
273
|
const success = code === 0;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MetricPoint {
|
|
2
|
+
timestamp: number;
|
|
3
|
+
cpu: number;
|
|
4
|
+
memory: number;
|
|
5
|
+
memoryMB: number;
|
|
6
|
+
memoryPercent: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ProcessHistory {
|
|
9
|
+
pm_id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
status: string;
|
|
12
|
+
history: MetricPoint[];
|
|
13
|
+
}
|
|
14
|
+
declare class MetricsHistoryStore {
|
|
15
|
+
private readonly store;
|
|
16
|
+
record(processList: any[]): void;
|
|
17
|
+
getOne(pm_id: number): ProcessHistory | null;
|
|
18
|
+
getAll(): ProcessHistory[];
|
|
19
|
+
}
|
|
20
|
+
export declare const metricsHistory: MetricsHistoryStore;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.metricsHistory = void 0;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
// @group Constants : Ring-buffer capacity — 60 points × 3 s = 3 minutes of history
|
|
9
|
+
const MAX_POINTS = 60;
|
|
10
|
+
// @group MetricsHistory : In-memory ring buffer; key = pm_id (string)
|
|
11
|
+
class MetricsHistoryStore {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.store = new Map();
|
|
14
|
+
}
|
|
15
|
+
// @group MetricsHistory : Record a snapshot from the PM2 process list
|
|
16
|
+
record(processList) {
|
|
17
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
18
|
+
const totalMem = os_1.default.totalmem();
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const proc of processList) {
|
|
21
|
+
const pm_id = proc.pm_id;
|
|
22
|
+
seen.add(pm_id);
|
|
23
|
+
const cpu = (_b = (_a = proc.monit) === null || _a === void 0 ? void 0 : _a.cpu) !== null && _b !== void 0 ? _b : 0;
|
|
24
|
+
const memory = (_d = (_c = proc.monit) === null || _c === void 0 ? void 0 : _c.memory) !== null && _d !== void 0 ? _d : 0;
|
|
25
|
+
const point = {
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
cpu: parseFloat(cpu.toFixed(2)),
|
|
28
|
+
memory,
|
|
29
|
+
memoryMB: parseFloat((memory / 1048576).toFixed(2)),
|
|
30
|
+
memoryPercent: parseFloat(((memory / totalMem) * 100).toFixed(2)),
|
|
31
|
+
};
|
|
32
|
+
if (!this.store.has(pm_id)) {
|
|
33
|
+
this.store.set(pm_id, {
|
|
34
|
+
pm_id,
|
|
35
|
+
name: (_e = proc.name) !== null && _e !== void 0 ? _e : String(pm_id),
|
|
36
|
+
status: (_g = (_f = proc.pm2_env) === null || _f === void 0 ? void 0 : _f.status) !== null && _g !== void 0 ? _g : 'unknown',
|
|
37
|
+
history: [],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const entry = this.store.get(pm_id);
|
|
41
|
+
// Refresh mutable fields on every poll
|
|
42
|
+
entry.name = (_h = proc.name) !== null && _h !== void 0 ? _h : entry.name;
|
|
43
|
+
entry.status = (_k = (_j = proc.pm2_env) === null || _j === void 0 ? void 0 : _j.status) !== null && _k !== void 0 ? _k : entry.status;
|
|
44
|
+
// Append point, trim to ring-buffer size
|
|
45
|
+
entry.history.push(point);
|
|
46
|
+
if (entry.history.length > MAX_POINTS) {
|
|
47
|
+
entry.history.shift();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Remove processes that no longer exist in PM2
|
|
51
|
+
for (const id of this.store.keys()) {
|
|
52
|
+
if (!seen.has(id)) {
|
|
53
|
+
this.store.delete(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// @group MetricsHistory : Return history for a single process; null if not found
|
|
58
|
+
getOne(pm_id) {
|
|
59
|
+
var _a;
|
|
60
|
+
return (_a = this.store.get(pm_id)) !== null && _a !== void 0 ? _a : null;
|
|
61
|
+
}
|
|
62
|
+
// @group MetricsHistory : Return history for all tracked processes
|
|
63
|
+
getAll() {
|
|
64
|
+
return Array.from(this.store.values());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// @group Exports : Singleton instance used across the server
|
|
68
|
+
exports.metricsHistory = new MetricsHistoryStore();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RemoteMetricRow {
|
|
2
|
+
id: number;
|
|
3
|
+
connection_id: string;
|
|
4
|
+
connection_name: string;
|
|
5
|
+
process_name: string;
|
|
6
|
+
pm_id: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
cpu: number;
|
|
9
|
+
memory_bytes: number;
|
|
10
|
+
memory_mb: number;
|
|
11
|
+
}
|
|
12
|
+
declare class RemoteMetricsDB {
|
|
13
|
+
private db;
|
|
14
|
+
constructor();
|
|
15
|
+
private initSchema;
|
|
16
|
+
private purgeOld;
|
|
17
|
+
insert(row: Omit<RemoteMetricRow, 'id'>): void;
|
|
18
|
+
insertBatch(rows: Omit<RemoteMetricRow, 'id'>[]): void;
|
|
19
|
+
getConnectionsWithData(): {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
}[];
|
|
23
|
+
getProcessNames(connectionId: string): string[];
|
|
24
|
+
getMetrics(connectionId: string, processName: string, from: number, to: number, maxPoints?: number): RemoteMetricRow[];
|
|
25
|
+
getLatestSnapshot(connectionId: string): RemoteMetricRow[];
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare const remoteMetricsDB: RemoteMetricsDB;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.remoteMetricsDB = void 0;
|
|
7
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
// @group Configuration : SQLite DB lives alongside other server config files
|
|
11
|
+
const DB_DIR = path_1.default.join(__dirname, '../config');
|
|
12
|
+
const DB_PATH = path_1.default.join(DB_DIR, 'remote-metrics.db');
|
|
13
|
+
// @group RemoteMetricsDB : SQLite-backed persistent store for remote process metrics
|
|
14
|
+
class RemoteMetricsDB {
|
|
15
|
+
constructor() {
|
|
16
|
+
if (!fs_1.default.existsSync(DB_DIR))
|
|
17
|
+
fs_1.default.mkdirSync(DB_DIR, { recursive: true });
|
|
18
|
+
this.db = new better_sqlite3_1.default(DB_PATH);
|
|
19
|
+
this.db.pragma('journal_mode = WAL');
|
|
20
|
+
this.initSchema();
|
|
21
|
+
this.purgeOld();
|
|
22
|
+
}
|
|
23
|
+
// @group RemoteMetricsDB : Create tables and indexes on first run
|
|
24
|
+
initSchema() {
|
|
25
|
+
this.db.exec(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS remote_metrics (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
connection_id TEXT NOT NULL,
|
|
29
|
+
connection_name TEXT NOT NULL,
|
|
30
|
+
process_name TEXT NOT NULL,
|
|
31
|
+
pm_id INTEGER NOT NULL,
|
|
32
|
+
timestamp INTEGER NOT NULL,
|
|
33
|
+
cpu REAL NOT NULL,
|
|
34
|
+
memory_bytes INTEGER NOT NULL,
|
|
35
|
+
memory_mb REAL NOT NULL
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_rmet_conn_proc_ts
|
|
39
|
+
ON remote_metrics (connection_id, process_name, timestamp);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_rmet_conn_ts
|
|
42
|
+
ON remote_metrics (connection_id, timestamp);
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
// @group RemoteMetricsDB : Delete records older than 30 days to keep DB lean
|
|
46
|
+
purgeOld() {
|
|
47
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
48
|
+
this.db.prepare('DELETE FROM remote_metrics WHERE timestamp < ?').run(cutoff);
|
|
49
|
+
}
|
|
50
|
+
// @group RemoteMetricsDB : Persist a single data-point
|
|
51
|
+
insert(row) {
|
|
52
|
+
this.db.prepare(`
|
|
53
|
+
INSERT INTO remote_metrics
|
|
54
|
+
(connection_id, connection_name, process_name, pm_id, timestamp, cpu, memory_bytes, memory_mb)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
56
|
+
`).run(row.connection_id, row.connection_name, row.process_name, row.pm_id, row.timestamp, row.cpu, row.memory_bytes, row.memory_mb);
|
|
57
|
+
}
|
|
58
|
+
// @group RemoteMetricsDB : Bulk-insert multiple data-points in one transaction (efficient)
|
|
59
|
+
insertBatch(rows) {
|
|
60
|
+
const stmt = this.db.prepare(`
|
|
61
|
+
INSERT INTO remote_metrics
|
|
62
|
+
(connection_id, connection_name, process_name, pm_id, timestamp, cpu, memory_bytes, memory_mb)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
64
|
+
`);
|
|
65
|
+
const tx = this.db.transaction(() => {
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
stmt.run(row.connection_id, row.connection_name, row.process_name, row.pm_id, row.timestamp, row.cpu, row.memory_bytes, row.memory_mb);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
tx();
|
|
71
|
+
}
|
|
72
|
+
// @group RemoteMetricsDB : Return all (connection_id, connection_name) pairs that have data
|
|
73
|
+
getConnectionsWithData() {
|
|
74
|
+
return this.db.prepare(`
|
|
75
|
+
SELECT DISTINCT connection_id AS id, connection_name AS name
|
|
76
|
+
FROM remote_metrics
|
|
77
|
+
ORDER BY name
|
|
78
|
+
`).all();
|
|
79
|
+
}
|
|
80
|
+
// @group RemoteMetricsDB : Return distinct process names recorded for a connection
|
|
81
|
+
getProcessNames(connectionId) {
|
|
82
|
+
const rows = this.db.prepare(`
|
|
83
|
+
SELECT DISTINCT process_name
|
|
84
|
+
FROM remote_metrics
|
|
85
|
+
WHERE connection_id = ?
|
|
86
|
+
ORDER BY process_name
|
|
87
|
+
`).all(connectionId);
|
|
88
|
+
return rows.map(r => r.process_name);
|
|
89
|
+
}
|
|
90
|
+
// @group RemoteMetricsDB : Return metric rows for a specific process within a time range
|
|
91
|
+
getMetrics(connectionId, processName, from, to, maxPoints = 500) {
|
|
92
|
+
// Bucket-reduce to avoid sending thousands of points for long ranges
|
|
93
|
+
const total = this.db.prepare(`
|
|
94
|
+
SELECT COUNT(*) AS cnt FROM remote_metrics
|
|
95
|
+
WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
|
|
96
|
+
`).get(connectionId, processName, from, to).cnt;
|
|
97
|
+
if (total <= maxPoints) {
|
|
98
|
+
return this.db.prepare(`
|
|
99
|
+
SELECT * FROM remote_metrics
|
|
100
|
+
WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
|
|
101
|
+
ORDER BY timestamp ASC
|
|
102
|
+
`).all(connectionId, processName, from, to);
|
|
103
|
+
}
|
|
104
|
+
// Downsample: pick every Nth row so we stay under maxPoints
|
|
105
|
+
const step = Math.ceil(total / maxPoints);
|
|
106
|
+
return this.db.prepare(`
|
|
107
|
+
SELECT * FROM remote_metrics
|
|
108
|
+
WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
|
|
109
|
+
AND (id % ?) = 0
|
|
110
|
+
ORDER BY timestamp ASC
|
|
111
|
+
LIMIT ?
|
|
112
|
+
`).all(connectionId, processName, from, to, step, maxPoints);
|
|
113
|
+
}
|
|
114
|
+
// @group RemoteMetricsDB : Return latest snapshot for all processes on a connection
|
|
115
|
+
getLatestSnapshot(connectionId) {
|
|
116
|
+
return this.db.prepare(`
|
|
117
|
+
SELECT r.*
|
|
118
|
+
FROM remote_metrics r
|
|
119
|
+
INNER JOIN (
|
|
120
|
+
SELECT process_name, MAX(timestamp) AS max_ts
|
|
121
|
+
FROM remote_metrics
|
|
122
|
+
WHERE connection_id = ?
|
|
123
|
+
GROUP BY process_name
|
|
124
|
+
) latest ON r.process_name = latest.process_name AND r.timestamp = latest.max_ts
|
|
125
|
+
WHERE r.connection_id = ?
|
|
126
|
+
ORDER BY r.process_name
|
|
127
|
+
`).all(connectionId, connectionId);
|
|
128
|
+
}
|
|
129
|
+
close() {
|
|
130
|
+
this.db.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// @group Exports : Singleton used across the server
|
|
134
|
+
exports.remoteMetricsDB = new RemoteMetricsDB();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.remoteMetricsPoller = void 0;
|
|
4
|
+
const remote_connection_1 = require("./remote-connection");
|
|
5
|
+
const remote_metrics_db_1 = require("./remote-metrics-db");
|
|
6
|
+
// @group Configuration : Polling interval in milliseconds
|
|
7
|
+
const POLL_INTERVAL_MS = 30000; // 30 seconds
|
|
8
|
+
// @group RemoteMetricsPoller : Background service that samples PM2 metrics for every connected remote server
|
|
9
|
+
class RemoteMetricsPoller {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.timer = null;
|
|
12
|
+
}
|
|
13
|
+
// @group RemoteMetricsPoller : Start the periodic polling loop
|
|
14
|
+
start() {
|
|
15
|
+
if (this.timer)
|
|
16
|
+
return; // already running
|
|
17
|
+
console.log('[RemoteMetricsPoller] Starting — polling every', POLL_INTERVAL_MS / 1000, 's');
|
|
18
|
+
this.timer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
|
|
19
|
+
}
|
|
20
|
+
// @group RemoteMetricsPoller : Stop the polling loop
|
|
21
|
+
stop() {
|
|
22
|
+
if (this.timer) {
|
|
23
|
+
clearInterval(this.timer);
|
|
24
|
+
this.timer = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// @group RemoteMetricsPoller : Poll all currently-connected remote servers and persist metrics
|
|
28
|
+
async poll() {
|
|
29
|
+
var _a;
|
|
30
|
+
const connectionsMap = remote_connection_1.remoteConnectionManager.getAllConnections();
|
|
31
|
+
const connected = Array.from(connectionsMap.entries()).filter(([, c]) => c.isConnected());
|
|
32
|
+
if (connected.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
for (const [connectionId, conn] of connected) {
|
|
36
|
+
try {
|
|
37
|
+
const processes = await conn.getPM2Processes();
|
|
38
|
+
if (!Array.isArray(processes) || processes.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
const rows = processes
|
|
41
|
+
.filter(p => p && p.name)
|
|
42
|
+
.map(p => {
|
|
43
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
44
|
+
return ({
|
|
45
|
+
connection_id: connectionId,
|
|
46
|
+
connection_name: (_a = conn.name) !== null && _a !== void 0 ? _a : conn.host,
|
|
47
|
+
process_name: p.name,
|
|
48
|
+
pm_id: (_b = p.pm_id) !== null && _b !== void 0 ? _b : -1,
|
|
49
|
+
timestamp: now,
|
|
50
|
+
cpu: parseFloat(((_d = (_c = p.monit) === null || _c === void 0 ? void 0 : _c.cpu) !== null && _d !== void 0 ? _d : 0).toFixed(2)),
|
|
51
|
+
memory_bytes: ((_f = (_e = p.monit) === null || _e === void 0 ? void 0 : _e.memory) !== null && _f !== void 0 ? _f : 0),
|
|
52
|
+
memory_mb: parseFloat((((_h = (_g = p.monit) === null || _g === void 0 ? void 0 : _g.memory) !== null && _h !== void 0 ? _h : 0) / 1048576).toFixed(2)),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
if (rows.length > 0) {
|
|
56
|
+
remote_metrics_db_1.remoteMetricsDB.insertBatch(rows);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Non-fatal — log and continue with other servers
|
|
61
|
+
console.warn(`[RemoteMetricsPoller] Failed to poll ${conn.host}:`, (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// @group Exports : Singleton poller started once when the server boots
|
|
67
|
+
exports.remoteMetricsPoller = new RemoteMetricsPoller();
|
package/package.json
CHANGED
|
@@ -1,73 +1,86 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ezpm2gui",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"main": "dist/server/index.js",
|
|
5
|
-
"bin": {
|
|
6
|
-
"ezpm2gui": "bin/ezpm2gui.js",
|
|
7
|
-
"ezpm2gui-generate-ecosystem": "bin/generate-ecosystem.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"start": "node dist/server/index.js",
|
|
11
|
-
"dev": "concurrently --kill-others-on-fail --names \"server,client\" --prefix-colors \"cyan,green\" \"npm run dev:server\" \"npm run dev:client\"",
|
|
12
|
-
"dev:server": "nodemon --exec ts-node src/server/index.ts",
|
|
13
|
-
"dev:client": "cd src/client && npm run dev",
|
|
14
|
-
"build": "node scripts/build.js",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "ezpm2gui",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"main": "dist/server/index.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ezpm2gui": "bin/ezpm2gui.js",
|
|
7
|
+
"ezpm2gui-generate-ecosystem": "bin/generate-ecosystem.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node dist/server/index.js",
|
|
11
|
+
"dev": "concurrently --kill-others-on-fail --names \"server,client\" --prefix-colors \"cyan,green\" \"npm run dev:server\" \"npm run dev:client\"",
|
|
12
|
+
"dev:server": "nodemon --exec ts-node src/server/index.ts",
|
|
13
|
+
"dev:client": "cd src/client && npm run dev",
|
|
14
|
+
"build": "node scripts/build.js",
|
|
15
|
+
"mobile": "node scripts/deploy-mobile.js",
|
|
16
|
+
"mobile:dev": "node scripts/deploy-mobile.js dev",
|
|
17
|
+
"mobile:android": "node scripts/deploy-mobile.js android --debug --install",
|
|
18
|
+
"mobile:android:release": "node scripts/deploy-mobile.js android --release",
|
|
19
|
+
"mobile:debug": "powershell -ExecutionPolicy Bypass -File scripts/Debug-Mobile.ps1",
|
|
20
|
+
"mobile:debug:dump": "powershell -ExecutionPolicy Bypass -File scripts/Debug-Mobile.ps1 -Dump",
|
|
21
|
+
"mobile:ps": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1",
|
|
22
|
+
"mobile:ps:release": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1 -Mode release -Install -Launch",
|
|
23
|
+
"mobile:ps:debug": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1 -Mode debug -Install -Launch",
|
|
24
|
+
"mobile:ios": "node scripts/deploy-mobile.js ios",
|
|
25
|
+
"mobile:ios:release": "node scripts/deploy-mobile.js ios --release",
|
|
26
|
+
"build:server": "tsc",
|
|
27
|
+
"build:client": "cd src/client && npm run build",
|
|
28
|
+
"build:bin": "tsc --project tsconfig.bin.json",
|
|
29
|
+
"prepare": "npm run build",
|
|
30
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
31
|
+
"postinstall": "node scripts/postinstall.js"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"pm2",
|
|
35
|
+
"gui",
|
|
36
|
+
"monitor",
|
|
37
|
+
"process-manager",
|
|
38
|
+
"dashboard"
|
|
39
|
+
],
|
|
40
|
+
"author": "Chandan Bhagat",
|
|
41
|
+
"license": "AGPL-3.0-or-later",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/thechandanbhagat/ezpm2gui.git"
|
|
45
|
+
},
|
|
46
|
+
"description": "A modern web-based GUI for PM2 process manager",
|
|
47
|
+
"files": [
|
|
48
|
+
"dist/",
|
|
49
|
+
"bin/",
|
|
50
|
+
"src/client/build/",
|
|
51
|
+
"scripts/postinstall.js"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@tailwindcss/postcss": "^4.1.14",
|
|
55
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
56
|
+
"@types/node-cron": "^3.0.11",
|
|
57
|
+
"axios": "^1.9.0",
|
|
58
|
+
"better-sqlite3": "^12.9.0",
|
|
59
|
+
"chart.js": "^4.4.9",
|
|
60
|
+
"cron-parser": "^5.4.0",
|
|
61
|
+
"dotenv": "^16.5.0",
|
|
62
|
+
"express": "^4.18.3",
|
|
63
|
+
"node-cron": "^4.2.1",
|
|
64
|
+
"pm2": "^6.0.5",
|
|
65
|
+
"react": "^19.1.0",
|
|
66
|
+
"react-dom": "^19.1.0",
|
|
67
|
+
"react-scripts": "^5.0.1",
|
|
68
|
+
"socket.io": "^4.8.1",
|
|
69
|
+
"ssh2": "^1.16.0",
|
|
70
|
+
"uuid": "^11.0.5"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/express": "^4.17.21",
|
|
74
|
+
"@types/node": "^22.15.17",
|
|
75
|
+
"@types/react": "^19.1.4",
|
|
76
|
+
"@types/react-dom": "^19.1.5",
|
|
77
|
+
"@types/socket.io": "^3.0.2",
|
|
78
|
+
"@types/ssh2": "^1.15.5",
|
|
79
|
+
"@types/uuid": "^10.0.0",
|
|
80
|
+
"concurrently": "^9.1.2",
|
|
81
|
+
"cross-env": "^10.1.0",
|
|
82
|
+
"nodemon": "^3.1.10",
|
|
83
|
+
"ts-node": "^10.9.2",
|
|
84
|
+
"typescript": "^5.8.3"
|
|
85
|
+
}
|
|
86
|
+
}
|