ezpm2gui 1.6.0 → 1.9.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.
Files changed (33) hide show
  1. package/README.md +40 -6
  2. package/dist/server/config/project-configs.json +3 -4
  3. package/dist/server/index.js +44 -5
  4. package/dist/server/routes/deployApplication.js +43 -41
  5. package/dist/server/routes/logStreaming.js +66 -28
  6. package/dist/server/routes/modules.js +55 -0
  7. package/dist/server/routes/pageAuth.js +3 -3
  8. package/dist/server/routes/remoteConnections.js +57 -21
  9. package/dist/server/routes/remoteMetrics.d.ts +3 -0
  10. package/dist/server/routes/remoteMetrics.js +84 -0
  11. package/dist/server/services/ProjectSetupService.d.ts +1 -1
  12. package/dist/server/services/ProjectSetupService.js +25 -9
  13. package/dist/server/utils/encryption.d.ts +22 -0
  14. package/dist/server/utils/encryption.js +53 -0
  15. package/dist/server/utils/metrics-history.d.ts +21 -0
  16. package/dist/server/utils/metrics-history.js +68 -0
  17. package/dist/server/utils/remote-connection.js +3 -3
  18. package/dist/server/utils/remote-metrics-db.d.ts +29 -0
  19. package/dist/server/utils/remote-metrics-db.js +134 -0
  20. package/dist/server/utils/remote-metrics-poller.d.ts +8 -0
  21. package/dist/server/utils/remote-metrics-poller.js +67 -0
  22. package/package.json +15 -2
  23. package/src/client/build/asset-manifest.json +6 -6
  24. package/src/client/build/index.html +1 -1
  25. package/src/client/build/static/css/main.2836d066.css +5 -0
  26. package/src/client/build/static/css/main.2836d066.css.map +1 -0
  27. package/src/client/build/static/js/main.d5c19622.js +3 -0
  28. package/src/client/build/static/js/main.d5c19622.js.map +1 -0
  29. package/src/client/build/static/css/main.775772ee.css +0 -5
  30. package/src/client/build/static/css/main.775772ee.css.map +0 -1
  31. package/src/client/build/static/js/main.cbcb09c9.js +0 -3
  32. package/src/client/build/static/js/main.cbcb09c9.js.map +0 -1
  33. /package/src/client/build/static/js/{main.cbcb09c9.js.LICENSE.txt → main.d5c19622.js.LICENSE.txt} +0 -0
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = __importDefault(require("express"));
7
7
  const remote_connection_1 = require("../utils/remote-connection");
8
+ const encryption_1 = require("../utils/encryption");
8
9
  const router = express_1.default.Router();
9
10
  // @group Security : Validate remote log file paths before shell interpolation
10
11
  const SHELL_UNSAFE_CHARS = /['"`;$|&<>(){}\\\n\r\0]/;
@@ -26,6 +27,29 @@ const safeLogLines = (raw) => {
26
27
  return 200;
27
28
  return Math.min(n, MAX_LOG_LINES);
28
29
  };
30
+ /**
31
+ * Decrypts a field that may be either a plain string (legacy/internal)
32
+ * or an EncryptedPayload object produced by the client-side hybrid encryptor.
33
+ */
34
+ function decryptField(value) {
35
+ if (!value)
36
+ return undefined;
37
+ if (typeof value === 'string')
38
+ return value;
39
+ try {
40
+ return (0, encryption_1.decryptTransitPayload)(value);
41
+ }
42
+ catch {
43
+ throw new Error('Failed to decrypt field — possible tampering or key mismatch');
44
+ }
45
+ }
46
+ /**
47
+ * Expose RSA public key so the client can encrypt sensitive fields before sending.
48
+ * GET /api/remote/public-key
49
+ */
50
+ router.get('/public-key', (_req, res) => {
51
+ res.json({ publicKey: (0, encryption_1.getRSAPublicKey)() });
52
+ });
29
53
  /**
30
54
  * Connect to an existing remote server
31
55
  * POST /api/remote/:connectionId/connect
@@ -819,15 +843,19 @@ router.get('/:connectionId/log-file/download', async (req, res) => {
819
843
  router.get('/connections', async (req, res) => {
820
844
  try {
821
845
  const connections = remote_connection_1.remoteConnectionManager.getAllConnections();
822
- const connectionsList = Array.from(connections.entries()).map(([id, conn]) => ({
823
- id,
824
- name: conn.name || `${conn.username}@${conn.host}`,
825
- host: conn.host,
826
- port: conn.port,
827
- username: conn.username,
828
- connected: conn.isConnected(),
829
- isPM2Installed: conn.isPM2Installed
830
- }));
846
+ const connectionsList = Array.from(connections.entries()).map(([id, conn]) => {
847
+ const connected = conn.isConnected();
848
+ return {
849
+ id,
850
+ name: conn.name || `${conn.username}@${conn.host}`,
851
+ host: conn.host,
852
+ port: conn.port,
853
+ username: conn.username,
854
+ connected,
855
+ isPM2Installed: conn.isPM2Installed,
856
+ status: connected ? 'connected' : 'disconnected',
857
+ };
858
+ });
831
859
  res.json(connectionsList);
832
860
  }
833
861
  catch (error) {
@@ -844,17 +872,21 @@ router.get('/connections', async (req, res) => {
844
872
  */
845
873
  router.post('/connections', (req, res) => {
846
874
  try {
847
- const connectionConfig = req.body;
848
- if (!connectionConfig.name || !connectionConfig.host || !connectionConfig.username) {
875
+ const body = req.body;
876
+ if (!body.name || !body.host || !body.username) {
849
877
  return res.status(400).json({
850
878
  success: false,
851
879
  error: 'Missing required connection parameters: name, host, or username'
852
880
  });
853
881
  }
854
- // Ensure port is set
855
- if (!connectionConfig.port) {
856
- connectionConfig.port = 22;
857
- }
882
+ // Decrypt sensitive fields that may have been encrypted by the client
883
+ const connectionConfig = {
884
+ ...body,
885
+ port: body.port || 22,
886
+ password: decryptField(body.password),
887
+ privateKey: decryptField(body.privateKey),
888
+ passphrase: decryptField(body.passphrase),
889
+ };
858
890
  // Create the connection
859
891
  const connectionId = remote_connection_1.remoteConnectionManager.createConnection(connectionConfig);
860
892
  res.status(201).json({
@@ -877,17 +909,21 @@ router.post('/connections', (req, res) => {
877
909
  router.put('/connections/:connectionId', async (req, res) => {
878
910
  try {
879
911
  const { connectionId } = req.params;
880
- const connectionConfig = req.body;
881
- if (!connectionConfig.name || !connectionConfig.host || !connectionConfig.username) {
912
+ const body = req.body;
913
+ if (!body.name || !body.host || !body.username) {
882
914
  return res.status(400).json({
883
915
  success: false,
884
916
  error: 'Missing required connection parameters: name, host, or username'
885
917
  });
886
918
  }
887
- // Ensure port is set
888
- if (!connectionConfig.port) {
889
- connectionConfig.port = 22;
890
- }
919
+ // Decrypt sensitive fields that may have been encrypted by the client
920
+ const connectionConfig = {
921
+ ...body,
922
+ port: body.port || 22,
923
+ password: decryptField(body.password),
924
+ privateKey: decryptField(body.privateKey),
925
+ passphrase: decryptField(body.passphrase),
926
+ };
891
927
  // Update the connection
892
928
  const success = await remote_connection_1.remoteConnectionManager.updateConnection(connectionId, connectionConfig);
893
929
  if (!success) {
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ declare const router: Router;
3
+ export default router;
@@ -0,0 +1,84 @@
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
+ const express_1 = __importDefault(require("express"));
7
+ const remote_metrics_db_1 = require("../utils/remote-metrics-db");
8
+ const router = express_1.default.Router();
9
+ // @group Constants : Default and maximum query bounds
10
+ const DEFAULT_RANGE_MS = 3600000; // 1 hour
11
+ const MAX_RANGE_MS = 7 * 24 * 3600000; // 7 days
12
+ const MAX_POINTS = 500;
13
+ // @group Utilities : Parse and clamp a query-string integer
14
+ const parseIntParam = (raw, fallback, min, max) => {
15
+ const n = parseInt(raw, 10);
16
+ if (!Number.isFinite(n))
17
+ return fallback;
18
+ return Math.max(min, Math.min(max, n));
19
+ };
20
+ /**
21
+ * List all remote connections that have recorded metrics
22
+ * GET /api/remote-metrics/connections
23
+ */
24
+ router.get('/connections', (_req, res) => {
25
+ try {
26
+ const connections = remote_metrics_db_1.remoteMetricsDB.getConnectionsWithData();
27
+ res.json({ success: true, connections });
28
+ }
29
+ catch (err) {
30
+ res.status(500).json({ success: false, error: err.message });
31
+ }
32
+ });
33
+ /**
34
+ * List all process names recorded for a connection
35
+ * GET /api/remote-metrics/:connectionId/processes
36
+ */
37
+ router.get('/:connectionId/processes', (req, res) => {
38
+ try {
39
+ const { connectionId } = req.params;
40
+ const names = remote_metrics_db_1.remoteMetricsDB.getProcessNames(connectionId);
41
+ res.json({ success: true, processes: names });
42
+ }
43
+ catch (err) {
44
+ res.status(500).json({ success: false, error: err.message });
45
+ }
46
+ });
47
+ /**
48
+ * Latest snapshot (most recent metric point per process) for a connection
49
+ * GET /api/remote-metrics/:connectionId/snapshot
50
+ */
51
+ router.get('/:connectionId/snapshot', (req, res) => {
52
+ try {
53
+ const { connectionId } = req.params;
54
+ const snapshot = remote_metrics_db_1.remoteMetricsDB.getLatestSnapshot(connectionId);
55
+ res.json({ success: true, snapshot });
56
+ }
57
+ catch (err) {
58
+ res.status(500).json({ success: false, error: err.message });
59
+ }
60
+ });
61
+ /**
62
+ * Time-series metrics for one process on a connection
63
+ * GET /api/remote-metrics/:connectionId/:processName
64
+ * ?from=<unix-ms> (default: now - 1h)
65
+ * ?to=<unix-ms> (default: now)
66
+ * ?maxPoints=<n> (default: 500, max: 1000)
67
+ */
68
+ router.get('/:connectionId/:processName', (req, res) => {
69
+ try {
70
+ const { connectionId, processName } = req.params;
71
+ const now = Date.now();
72
+ const to = parseIntParam(req.query.to, now, 0, now + 60000);
73
+ const from = parseIntParam(req.query.from, to - DEFAULT_RANGE_MS, 0, now);
74
+ // Clamp range to max window
75
+ const clampedFrom = Math.max(from, to - MAX_RANGE_MS);
76
+ const maxPoints = parseIntParam(req.query.maxPoints, MAX_POINTS, 50, 1000);
77
+ const metrics = remote_metrics_db_1.remoteMetricsDB.getMetrics(connectionId, processName, clampedFrom, to, maxPoints);
78
+ res.json({ success: true, metrics });
79
+ }
80
+ catch (err) {
81
+ res.status(500).json({ success: false, error: err.message });
82
+ }
83
+ });
84
+ exports.default = router;
@@ -62,7 +62,7 @@ export declare class ProjectSetupService {
62
62
  private ensureLogDirectory;
63
63
  private log;
64
64
  detectProjectType(projectPath: string): string | null;
65
- setupProject(projectPath: string, projectType: string): Promise<SetupResult>;
65
+ setupProject(projectPath: string, projectType: string, onLog?: (msg: string) => void): Promise<SetupResult>;
66
66
  private shouldSkipStep;
67
67
  private executeStep;
68
68
  private validateSetup;
@@ -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
- const stepResult = await this.executeStep(step, projectPath, result.environment);
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.success = false;
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
- // Log real-time output for debugging
249
- console.log(`[${step.name}] ${text.trim()}`);
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
- console.error(`[${step.name}] ERROR: ${text.trim()}`);
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;
@@ -10,3 +10,25 @@ export declare function encrypt(text: string): string;
10
10
  * @returns The decrypted text
11
11
  */
12
12
  export declare function decrypt(encryptedText: string): string;
13
+ /**
14
+ * Returns the server's RSA public key in PEM (SPKI) format.
15
+ * Expose this via a GET endpoint so clients can encrypt before sending.
16
+ */
17
+ export declare function getRSAPublicKey(): string;
18
+ /**
19
+ * Hybrid-encrypted payload sent by the client.
20
+ * - encryptedKey : RSA-OAEP encrypted 256-bit AES key (base64)
21
+ * - iv : 12-byte AES-GCM IV (base64)
22
+ * - data : AES-256-GCM ciphertext + 16-byte auth-tag (base64)
23
+ */
24
+ export interface EncryptedPayload {
25
+ encryptedKey: string;
26
+ iv: string;
27
+ data: string;
28
+ }
29
+ /**
30
+ * Decrypts a hybrid-encrypted payload produced by the client.
31
+ * 1. Unwraps the AES key with RSA-OAEP (SHA-256)
32
+ * 2. Decrypts the data with AES-256-GCM
33
+ */
34
+ export declare function decryptTransitPayload(payload: EncryptedPayload): string;
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.encrypt = encrypt;
7
7
  exports.decrypt = decrypt;
8
+ exports.getRSAPublicKey = getRSAPublicKey;
9
+ exports.decryptTransitPayload = decryptTransitPayload;
8
10
  /**
9
11
  * Utility for encrypting and decrypting sensitive data
10
12
  */
@@ -58,3 +60,54 @@ function decrypt(encryptedText) {
58
60
  return '';
59
61
  }
60
62
  }
63
+ // ---------------------------------------------------------------------------
64
+ // RSA-OAEP key pair for in-transit encryption
65
+ // A fresh key pair is generated once per server process and kept in memory.
66
+ // The public key is shared with clients so they can encrypt sensitive fields
67
+ // before sending them over the network. The private key never leaves the server.
68
+ // ---------------------------------------------------------------------------
69
+ let _rsaPrivateKey = null;
70
+ let _rsaPublicKeyPem = null;
71
+ function getOrCreateRSAKeyPair() {
72
+ if (!_rsaPrivateKey || !_rsaPublicKeyPem) {
73
+ const { privateKey, publicKey } = crypto_1.default.generateKeyPairSync('rsa', {
74
+ modulusLength: 2048,
75
+ });
76
+ _rsaPrivateKey = privateKey;
77
+ _rsaPublicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
78
+ }
79
+ return { publicKeyPem: _rsaPublicKeyPem, privateKey: _rsaPrivateKey };
80
+ }
81
+ /**
82
+ * Returns the server's RSA public key in PEM (SPKI) format.
83
+ * Expose this via a GET endpoint so clients can encrypt before sending.
84
+ */
85
+ function getRSAPublicKey() {
86
+ return getOrCreateRSAKeyPair().publicKeyPem;
87
+ }
88
+ /**
89
+ * Decrypts a hybrid-encrypted payload produced by the client.
90
+ * 1. Unwraps the AES key with RSA-OAEP (SHA-256)
91
+ * 2. Decrypts the data with AES-256-GCM
92
+ */
93
+ function decryptTransitPayload(payload) {
94
+ const { privateKey } = getOrCreateRSAKeyPair();
95
+ // Step 1 — decrypt the AES-256 key with RSA-OAEP / SHA-256
96
+ const encryptedAesKey = Buffer.from(payload.encryptedKey, 'base64');
97
+ const aesKey = crypto_1.default.privateDecrypt({
98
+ key: privateKey,
99
+ padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
100
+ oaepHash: 'sha256',
101
+ }, encryptedAesKey);
102
+ // Step 2 — decrypt data with AES-256-GCM
103
+ // WebCrypto appends the 16-byte auth tag at the end of the ciphertext buffer
104
+ const iv = Buffer.from(payload.iv, 'base64');
105
+ const encryptedBuf = Buffer.from(payload.data, 'base64');
106
+ const authTag = encryptedBuf.slice(encryptedBuf.length - 16);
107
+ const ciphertext = encryptedBuf.slice(0, encryptedBuf.length - 16);
108
+ const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', aesKey, iv);
109
+ decipher.setAuthTag(authTag);
110
+ let decrypted = decipher.update(ciphertext, undefined, 'utf8');
111
+ decrypted += decipher.final('utf8');
112
+ return decrypted;
113
+ }
@@ -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();
@@ -78,9 +78,9 @@ class RemoteConnection extends events_1.EventEmitter {
78
78
  host: this.config.host,
79
79
  port: this.config.port || 22,
80
80
  username: this.config.username,
81
- // Add reasonable timeouts
82
- readyTimeout: 10000,
83
- keepaliveInterval: 30000
81
+ // Add reasonable timeouts optimized for VPN/remote scenarios
82
+ readyTimeout: 20000, // 20 seconds to establish connection (was 10s)
83
+ keepaliveInterval: 10000 // Send keepalive every 10 seconds (was 30s) for aggressive firewalls
84
84
  };
85
85
  // Debug output
86
86
  console.log(`Connecting to ${this.config.host}:${this.config.port} as ${this.config.username}`);
@@ -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 {};