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
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # EZ PM2 GUI
2
2
 
3
+ [![Discord](https://img.shields.io/discord/1234567890?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/ttgc2zqK7b)
4
+
3
5
  A modern web-based graphical user interface for the PM2 process manager, built with TypeScript, Tailwind CSS, and React.
4
6
 
5
7
  ## Screenshots
@@ -12,6 +14,14 @@ A modern web-based graphical user interface for the PM2 process manager, built w
12
14
 
13
15
  ![Process Monitor](ezpm2gui/screenshots/02-monitoring.png)
14
16
 
17
+ **Metrics (Live)** — rolling 1-hour sparklines per process, updated every 3 seconds:
18
+
19
+ ![Metrics Live](ezpm2gui/screenshots/12-metrics-live.png)
20
+
21
+ **Metrics (History)** — SQLite-backed CPU and memory charts with selectable time range:
22
+
23
+ ![Metrics History](ezpm2gui/screenshots/13-metrics-history.png)
24
+
15
25
  **Deploy App** — start new PM2 processes from a structured form:
16
26
 
17
27
  ![Deploy App](ezpm2gui/screenshots/04-deploy-app.png)
@@ -30,18 +40,24 @@ A modern web-based graphical user interface for the PM2 process manager, built w
30
40
 
31
41
  - **Real-time process monitoring** - Keep track of all your PM2 processes in real-time
32
42
  - **Process management** - Start, stop, restart, and delete processes with one click
43
+ - **Sidebar quick-actions** *(v1.9.0)* - Per-process restart, start/stop, and logs buttons revealed on hover in the sidebar
33
44
  - **System metrics dashboard** - Monitor CPU, memory usage, and uptime
45
+ - **Metrics page with live sparklines** - Per-process rolling 1-hour CPU and memory micro-graphs updated every 3s; switch to History tab for SQLite-backed long-term charts
34
46
  - **Enhanced log streaming** - View and filter logs from multiple processes simultaneously
47
+ - **Log search highlighting** *(v1.9.0)* - Search terms are visually highlighted in the log viewer
48
+ - **Log timestamp range filter** *(v1.9.0)* - Filter log output by start/end timestamp with snapshot mode
49
+ - **Remote log polling** *(v1.9.0)* - Logs from remote servers fetched and displayed in real-time
35
50
  - **WebSocket for live updates** - Get instant updates without refreshing
36
51
  - **Process CPU and memory charts** - Visualize performance metrics over time
37
52
  - **Filter processes by status or name** - Quickly find the processes you need
38
- - **Dark/light mode** - Fully supported across all pages with Tailwind CSS
53
+ - **Dark/light mode** - Fully supported across all pages with Tailwind CSS; preference persisted across sessions
39
54
  - **Cluster management** - Easily scale your Node.js applications
40
55
  - **Application deployment** - Deploy new applications directly from the UI
41
56
  - **Ecosystem configuration** - Create and manage your PM2 ecosystem files
42
57
  - **PM2 modules support** - Manage and configure PM2 modules
43
58
  - **Cron Jobs** - Schedule and manage automated tasks with visual cron expression builder
44
59
  - **Remote Server Management** - Connect and manage PM2 on remote servers via SSH
60
+ - **End-to-end encrypted credentials** *(v1.9.0)* - Remote server passwords encrypted in-browser with RSA-OAEP + AES-256-GCM before transmission
45
61
  - **Advanced Monitoring Dashboard** - Real-time performance charts with health scoring
46
62
  - **Tailwind CSS UI** - Sleek, compact, and responsive design with consistent dark/light theming
47
63
  - **Fully typed with TypeScript** - Robust and maintainable codebase
@@ -55,9 +71,9 @@ Monitor all your PM2 processes in real-time with detailed information on CPU usa
55
71
  Connect to and manage PM2 processes on remote servers via secure SSH connections:
56
72
  - Add multiple remote server connections with SSH credentials
57
73
  - View and manage processes on remote servers
58
- - Stream logs from remote processes in real-time
74
+ - Stream logs from remote processes in real-time with polling
59
75
  - Execute PM2 commands on remote machines
60
- - Encrypted credential storage for security
76
+ - **End-to-end credential encryption** passwords are encrypted client-side (RSA-OAEP + AES-256-GCM hybrid scheme) before transmission; the server never sees plaintext passwords in transit
61
77
 
62
78
  ### Cron Jobs
63
79
  Schedule and automate tasks using PM2's cron restart feature:
@@ -91,10 +107,13 @@ Easily scale your Node.js applications with the cluster management interface. Ad
91
107
  ### Log Streaming
92
108
  View and filter logs from multiple processes simultaneously with the enhanced log streaming interface. Features include:
93
109
  - Real-time log updates via WebSocket
110
+ - **Search with visual highlighting** — matched terms are highlighted inline
111
+ - **Timestamp range filter** — narrow logs to a start/end time window with snapshot mode (polling pauses while filter is active)
94
112
  - Filtering by process, log level, or content
95
113
  - Pausing and resuming log streams
96
114
  - Download logs for offline analysis
97
115
  - Floating log panel for remote process logs
116
+ - Remote server log polling
98
117
 
99
118
  ### Ecosystem Configuration
100
119
  Generate and manage PM2 ecosystem configuration files directly from the UI. This makes it easy to set up complex application deployments and share configurations across your team.
@@ -169,7 +188,7 @@ ezpm2gui.start({
169
188
  Once started, open your browser and navigate to:
170
189
 
171
190
  ```
172
- http://localhost:3001
191
+ http://localhost:3101
173
192
  ```
174
193
 
175
194
  ## Requirements
@@ -181,8 +200,23 @@ http://localhost:3001
181
200
 
182
201
  EZ PM2 GUI uses environment variables for configuration:
183
202
 
184
- - `PORT`: The port to run the server on (default: 3001)
185
- - `HOST`: The host to bind to (default: localhost)
203
+ - `PORT`: The port to run the server on (default: `3101`)
204
+ - `HOST`: The host to bind to (default: `localhost`)
205
+
206
+ You can set these in a `.env` file at the project root (create it if it doesn't exist):
207
+
208
+ ```env
209
+ # .env
210
+ PORT=3102
211
+ HOST=localhost
212
+ ```
213
+
214
+ For the React client to connect to the correct port during a production build, also set:
215
+
216
+ ```env
217
+ # src/client/.env
218
+ REACT_APP_API_URL=http://localhost:3102
219
+ ```
186
220
 
187
221
  ## Load Balancing with PM2
188
222
 
@@ -38,9 +38,7 @@
38
38
  "workingDirectory": "project"
39
39
  }
40
40
  ],
41
- "environment": {
42
- "NODE_ENV": "production"
43
- }
41
+ "environment": {}
44
42
  },
45
43
  "validation": {
46
44
  "checks": [
@@ -50,7 +48,8 @@
50
48
  },
51
49
  {
52
50
  "name": "node_modules exists after install",
53
- "directory": "node_modules"
51
+ "directory": "node_modules",
52
+ "optional": true
54
53
  }
55
54
  ]
56
55
  },
@@ -26,6 +26,9 @@ const pageAuth_1 = __importDefault(require("./routes/pageAuth"));
26
26
  const logStreaming_1 = require("./routes/logStreaming");
27
27
  const pm2_connection_1 = require("./utils/pm2-connection");
28
28
  const remote_connection_1 = require("./utils/remote-connection");
29
+ const metrics_history_1 = require("./utils/metrics-history");
30
+ const remoteMetrics_1 = __importDefault(require("./routes/remoteMetrics"));
31
+ const remote_metrics_poller_1 = require("./utils/remote-metrics-poller");
29
32
  /**
30
33
  * Create and configure the express server
31
34
  */
@@ -38,15 +41,15 @@ function createServer() {
38
41
  origin: '*',
39
42
  methods: ['GET', 'POST']
40
43
  },
41
- // Increase ping timeout to prevent false positive disconnections
42
- pingTimeout: 10000, // How long to wait for a pong response (default: 5000ms)
44
+ // Increase ping timeout to prevent false positive disconnections in VPN/remote scenarios
45
+ pingTimeout: 20000, // How long to wait for a pong response (increased from 10s to 20s)
43
46
  pingInterval: 25000, // How often to send ping packets (default: 25000ms)
44
47
  // Allow reconnection attempts
45
48
  allowEIO3: true,
46
49
  // Transport configuration
47
50
  transports: ['websocket', 'polling'],
48
51
  // Upgrade timeout
49
- upgradeTimeout: 10000
52
+ upgradeTimeout: 20000 // Increased from 10s to 20s for slow VPN connections
50
53
  });
51
54
  // Configure middleware
52
55
  app.use(express_1.default.json());
@@ -68,6 +71,9 @@ function createServer() {
68
71
  app.use('/api/cron-jobs', cronJobs_1.default);
69
72
  app.use('/api/update', updates_1.default);
70
73
  app.use('/api/auth', pageAuth_1.default);
74
+ app.use('/api/remote-metrics', remoteMetrics_1.default);
75
+ // Start remote metrics poller (30-second interval, polls all connected servers)
76
+ remote_metrics_poller_1.remoteMetricsPoller.start();
71
77
  // Setup log streaming with Socket.IO
72
78
  (0, logStreaming_1.setupLogStreaming)(io); // PM2 API endpoints
73
79
  app.get('/api/processes', async (req, res) => {
@@ -137,6 +143,24 @@ function createServer() {
137
143
  };
138
144
  res.json(metrics);
139
145
  });
146
+ // @group MetricsHistory : Return full history for all processes
147
+ app.get('/api/metrics/history', (_req, res) => {
148
+ res.json(metrics_history_1.metricsHistory.getAll());
149
+ });
150
+ // @group MetricsHistory : Return history for a single process by pm_id
151
+ app.get('/api/metrics/history/:processId', (req, res) => {
152
+ const pm_id = parseInt(req.params.processId, 10);
153
+ if (isNaN(pm_id)) {
154
+ res.status(400).json({ error: 'Invalid processId' });
155
+ return;
156
+ }
157
+ const entry = metrics_history_1.metricsHistory.getOne(pm_id);
158
+ if (!entry) {
159
+ res.status(404).json({ error: 'No history found for this process' });
160
+ return;
161
+ }
162
+ res.json(entry);
163
+ });
140
164
  // @group LogHistory : Resolve log path from PM2 process descriptor
141
165
  const resolveLocalLogPath = async (id, logType) => {
142
166
  var _a, _b, _c;
@@ -355,6 +379,19 @@ function createServer() {
355
379
  res.status(500).json({ error: 'Failed to download file' });
356
380
  }
357
381
  });
382
+ // @group MetricsHistory : Server-level polling — records PM2 process metrics into the history store
383
+ // This runs once regardless of how many clients are connected so the ring buffer fills consistently.
384
+ const historyPollInterval = setInterval(async () => {
385
+ try {
386
+ const processList = await (0, pm2_connection_1.executePM2Command)((callback) => {
387
+ pm2_1.default.list(callback);
388
+ });
389
+ metrics_history_1.metricsHistory.record(processList);
390
+ }
391
+ catch {
392
+ // silent — best-effort recording; don't crash the server
393
+ }
394
+ }, 3000);
358
395
  // WebSocket for real-time updates
359
396
  io.on('connection', (socket) => {
360
397
  console.log('Client connected');
@@ -403,15 +440,17 @@ function createServer() {
403
440
  res.status(404).send('File not found. Please check server configuration.');
404
441
  }
405
442
  });
443
+ // Clean up history poll on server close
444
+ server.on('close', () => clearInterval(historyPollInterval));
406
445
  // Return the server instance
407
446
  return server;
408
447
  }
409
448
  // Only start the server if this file is run directly
410
449
  if (require.main === module) {
411
- const PORT = process.env.PORT || 3101;
450
+ const PORT = parseInt(process.env.PORT || '3101', 10);
412
451
  const HOST = process.env.HOST || 'localhost';
413
452
  const server = createServer();
414
- server.listen(PORT, () => {
453
+ server.listen(PORT, HOST, () => {
415
454
  console.log(`Server running on http://${HOST}:${PORT}`);
416
455
  });
417
456
  // Handle shutdown gracefully
@@ -9,60 +9,69 @@ const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const ProjectSetupService_1 = require("../services/ProjectSetupService");
11
11
  const router = (0, express_1.Router)();
12
- // Deploy a new application
12
+ // Deploy a new application (SSE streaming)
13
13
  router.post('/', async (req, res) => {
14
14
  const { name, script, cwd, namespace, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
15
- // Validate required fields
15
+ // Validate required fields before starting SSE
16
16
  if (!name || !script) {
17
17
  return res.status(400).json({ error: 'Name and script path are required' });
18
18
  }
19
- // Validate script path exists
20
19
  if (!fs_1.default.existsSync(script)) {
21
20
  return res.status(400).json({ error: `Script file not found: ${script}` });
22
21
  }
22
+ // Switch to SSE
23
+ res.setHeader('Content-Type', 'text/event-stream');
24
+ res.setHeader('Cache-Control', 'no-cache');
25
+ res.setHeader('Connection', 'keep-alive');
26
+ res.flushHeaders();
27
+ const send = (type, data) => {
28
+ if (!res.writableEnded) {
29
+ res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
30
+ }
31
+ };
32
+ const log = (message) => send('log', { message });
33
+ const done = (success, extra = {}) => {
34
+ send('done', { success, ...extra });
35
+ res.end();
36
+ };
23
37
  try {
24
38
  const projectPath = cwd || path_1.default.dirname(script);
25
- let setupResult = null;
26
39
  let finalEnv = env || {};
27
40
  let interpreterPath = '';
28
- // Auto-detect project type if not provided
41
+ let setupResult = null;
42
+ // Auto-detect project type
29
43
  let detectedType = appType;
30
44
  if (!detectedType) {
31
45
  detectedType = ProjectSetupService_1.projectSetupService.detectProjectType(projectPath);
32
46
  if (detectedType) {
33
- console.log(`Auto-detected project type: ${detectedType}`);
47
+ log(`Auto-detected project type: ${detectedType}`);
34
48
  }
35
49
  }
36
- // Run project setup if auto-setup is enabled and project type is detected
50
+ // Run project setup
37
51
  if (autoSetup && detectedType && ['node', 'python', 'dotnet'].includes(detectedType)) {
38
- console.log(`Running setup for ${detectedType} project...`);
52
+ log(`Running ${detectedType} project setup...`);
39
53
  try {
40
- setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType);
54
+ setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType, log);
41
55
  if (!setupResult.success) {
42
- return res.status(500).json({
56
+ return done(false, {
43
57
  error: 'Project setup failed',
44
58
  details: setupResult.errors,
45
- warnings: setupResult.warnings,
46
- steps: setupResult.steps
59
+ warnings: setupResult.warnings
47
60
  });
48
61
  }
49
- // Merge environment variables from setup
50
62
  finalEnv = { ...setupResult.environment, ...finalEnv };
51
- // Set interpreter path for Python projects
52
63
  if (setupResult.interpreterPath) {
53
64
  interpreterPath = setupResult.interpreterPath;
54
65
  }
55
- console.log('Project setup completed successfully');
56
66
  }
57
67
  catch (setupError) {
58
- console.error('Setup error:', setupError);
59
- return res.status(500).json({
68
+ return done(false, {
60
69
  error: 'Project setup failed',
61
70
  details: setupError instanceof Error ? setupError.message : 'Unknown setup error'
62
71
  });
63
72
  }
64
73
  }
65
- // Create deployment configuration
74
+ // Build PM2 config
66
75
  const appConfig = {
67
76
  name,
68
77
  script,
@@ -75,45 +84,38 @@ router.post('/', async (req, res) => {
75
84
  max_memory_restart: max_memory_restart || '150M',
76
85
  env: finalEnv
77
86
  };
78
- // Set interpreter for Python projects
79
87
  if (detectedType === 'python' && interpreterPath) {
80
88
  appConfig.interpreter = interpreterPath;
81
89
  }
82
90
  else if (detectedType === 'dotnet') {
83
91
  appConfig.interpreter = 'dotnet';
84
- // For .NET projects, update script to point to the published DLL if available
85
92
  const publishedDll = path_1.default.join(projectPath, 'publish', `${path_1.default.basename(projectPath)}.dll`);
86
93
  if (fs_1.default.existsSync(publishedDll)) {
87
94
  appConfig.script = publishedDll;
88
95
  }
89
96
  }
90
- pm2_1.default.connect((err) => {
91
- if (err) {
92
- console.error(err);
93
- return res.status(500).json({ error: 'Failed to connect to PM2' });
94
- }
95
- pm2_1.default.start(appConfig, (err) => {
96
- pm2_1.default.disconnect();
97
- if (err) {
98
- console.error('PM2 start error:', err);
99
- return res.status(500).json({
100
- error: `Failed to deploy application: ${err.message || 'Unknown error'}`
101
- });
102
- }
103
- res.json({
104
- success: true,
105
- message: `Application ${name} deployed successfully`,
106
- setupResult: setupResult ? {
107
- steps: setupResult.steps,
108
- warnings: setupResult.warnings
109
- } : null
97
+ log(`Starting PM2 process: ${name}`);
98
+ await new Promise((resolve, reject) => {
99
+ pm2_1.default.connect((err) => {
100
+ if (err)
101
+ return reject(err);
102
+ pm2_1.default.start(appConfig, (startErr) => {
103
+ pm2_1.default.disconnect();
104
+ if (startErr)
105
+ return reject(startErr);
106
+ resolve();
110
107
  });
111
108
  });
112
109
  });
110
+ log(`Process "${name}" started successfully.`);
111
+ done(true, {
112
+ message: `Application ${name} deployed successfully`,
113
+ setupResult: setupResult ? { steps: setupResult.steps, warnings: setupResult.warnings } : null
114
+ });
113
115
  }
114
116
  catch (error) {
115
117
  console.error('Deployment error:', error);
116
- return res.status(500).json({
118
+ done(false, {
117
119
  error: 'Deployment failed',
118
120
  details: error instanceof Error ? error.message : 'Unknown error'
119
121
  });
@@ -7,7 +7,6 @@ exports.setupLogStreaming = void 0;
7
7
  const express_1 = require("express");
8
8
  const pm2_1 = __importDefault(require("pm2"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
- const child_process_1 = require("child_process");
11
10
  const remote_connection_1 = require("../utils/remote-connection");
12
11
  const router = (0, express_1.Router)();
13
12
  // This variable will hold references to active log streams
@@ -18,7 +17,7 @@ const getLogStream = (io, processId, logType) => {
18
17
  const streamKey = `${processId}-${logType}`;
19
18
  // If stream already exists, return it
20
19
  if (activeStreams[streamKey]) {
21
- return activeStreams[streamKey];
20
+ return Promise.resolve(activeStreams[streamKey]);
22
21
  }
23
22
  return new Promise((resolve, reject) => {
24
23
  pm2_1.default.describe(processId, (err, processDesc) => {
@@ -32,30 +31,38 @@ const getLogStream = (io, processId, logType) => {
32
31
  reject(new Error(`Log file not found: ${logPath}`));
33
32
  return;
34
33
  }
35
- // Create a tail process to stream the log file
36
- const tail = (0, child_process_1.spawn)('tail', ['-f', logPath]);
37
- // Setup event handlers
38
- tail.stdout.on('data', (data) => {
39
- const lines = data.toString().split('\n').filter((line) => line.trim() !== '');
40
- lines.forEach((line) => {
41
- // Emit only to clients subscribed to this specific log stream
42
- io.to(streamKey).emit('log-line', {
43
- processId,
44
- logType,
45
- line
34
+ // Cross-platform tail using fs.watch no Unix `tail` binary needed
35
+ let position = fs_1.default.statSync(logPath).size; // start at EOF, don't replay history
36
+ const watcher = fs_1.default.watch(logPath, (eventType) => {
37
+ if (eventType !== 'change')
38
+ return;
39
+ try {
40
+ const stat = fs_1.default.statSync(logPath);
41
+ // Handle log rotation / truncation
42
+ if (stat.size < position)
43
+ position = 0;
44
+ if (stat.size === position)
45
+ return;
46
+ const length = stat.size - position;
47
+ const buffer = Buffer.alloc(length);
48
+ const fd = fs_1.default.openSync(logPath, 'r');
49
+ fs_1.default.readSync(fd, buffer, 0, length, position);
50
+ fs_1.default.closeSync(fd);
51
+ position = stat.size;
52
+ const lines = buffer.toString('utf8').split('\n').filter((l) => l.trim() !== '');
53
+ lines.forEach((line) => {
54
+ io.to(streamKey).emit('log-line', { processId, logType, line });
46
55
  });
47
- });
48
- });
49
- tail.stderr.on('data', (data) => {
50
- console.error(`Tail error: ${data}`);
51
- });
52
- tail.on('close', (code) => {
53
- console.log(`Tail process exited with code ${code}`);
54
- delete activeStreams[streamKey];
56
+ }
57
+ catch (e) {
58
+ console.error('Error reading log file:', e);
59
+ }
55
60
  });
56
- // Store the tail process
57
- activeStreams[streamKey] = tail;
58
- resolve(tail);
61
+ watcher.on('error', (e) => console.error(`Watcher error for ${streamKey}:`, e));
62
+ // Expose a kill() so existing cleanup code works unchanged
63
+ const streamObj = { kill: () => { watcher.close(); delete activeStreams[streamKey]; } };
64
+ activeStreams[streamKey] = streamObj;
65
+ resolve(streamObj);
59
66
  });
60
67
  });
61
68
  };
@@ -67,8 +74,19 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
67
74
  return activeRemoteStreams[streamKey];
68
75
  }
69
76
  const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
70
- if (!connection || !connection.isConnected()) {
71
- throw new Error('Connection not found or not connected');
77
+ if (!connection) {
78
+ throw new Error('Connection not found');
79
+ }
80
+ // Ensure connection is active before starting log stream
81
+ if (!connection.isConnected()) {
82
+ console.log(`Connection ${connectionId} not active, attempting to connect...`);
83
+ try {
84
+ await connection.connect();
85
+ console.log(`Successfully connected to ${connectionId} for log streaming`);
86
+ }
87
+ catch (error) {
88
+ throw new Error(`Failed to connect: ${error instanceof Error ? error.message : 'Unknown error'}`);
89
+ }
72
90
  }
73
91
  // Get process info using the multi-path fallback so pm2 is found regardless of PATH
74
92
  console.log(`Getting process info for: ${processId}`);
@@ -178,9 +196,24 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
178
196
  processName,
179
197
  error: error.message
180
198
  });
199
+ // Clean up on error
200
+ if (activeRemoteStreams[streamKey]) {
201
+ delete activeRemoteStreams[streamKey];
202
+ }
181
203
  });
182
204
  logStream.on('close', (code) => {
183
205
  console.log(`PM2 logs stream closed for ${processName} with code:`, code);
206
+ // Notify clients that the stream has closed
207
+ io.to(streamKey).emit('remote-log-closed', {
208
+ connectionId,
209
+ processId,
210
+ processName,
211
+ code
212
+ });
213
+ // Clean up the stream reference
214
+ if (activeRemoteStreams[streamKey]) {
215
+ delete activeRemoteStreams[streamKey];
216
+ }
184
217
  });
185
218
  streams.combined = logStream;
186
219
  }
@@ -267,10 +300,15 @@ const setupLogStreaming = (io) => {
267
300
  const streams = activeRemoteStreams[streamKey];
268
301
  if (streams) {
269
302
  console.log(`Stopping remote log streams: ${streamKey}`);
270
- if (streams.stdout && streams.stdout.kill) {
303
+ // Kill the log stream properly
304
+ if (streams.combined && typeof streams.combined.kill === 'function') {
305
+ streams.combined.kill();
306
+ }
307
+ // Also handle legacy stdout/stderr streams if they exist
308
+ if (streams.stdout && typeof streams.stdout.kill === 'function') {
271
309
  streams.stdout.kill();
272
310
  }
273
- if (streams.stderr && streams.stderr.kill) {
311
+ if (streams.stderr && typeof streams.stderr.kill === 'function') {
274
312
  streams.stderr.kill();
275
313
  }
276
314
  delete activeRemoteStreams[streamKey];
@@ -5,6 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = require("express");
7
7
  const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
8
11
  const pm2_1 = __importDefault(require("pm2"));
9
12
  const pm2_connection_1 = require("../utils/pm2-connection");
10
13
  const router = (0, express_1.Router)();
@@ -123,4 +126,56 @@ router.delete('/:moduleName', async (req, res) => {
123
126
  });
124
127
  }
125
128
  });
129
+ // @group Utilities : Resolve ~/.pm2/module_conf.json path
130
+ const moduleConfPath = () => path_1.default.join(os_1.default.homedir(), '.pm2', 'module_conf.json');
131
+ // @group APIEndpoints : Get config for a specific installed module
132
+ router.get('/:moduleName/config', (req, res) => {
133
+ var _a;
134
+ const { moduleName } = req.params;
135
+ if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
136
+ return res.status(400).json({ error: 'Invalid module name' });
137
+ }
138
+ const confFile = moduleConfPath();
139
+ if (!fs_1.default.existsSync(confFile)) {
140
+ return res.json({ config: {} });
141
+ }
142
+ try {
143
+ const raw = JSON.parse(fs_1.default.readFileSync(confFile, 'utf8'));
144
+ // PM2 stores module config under the module name key
145
+ const config = (_a = raw[moduleName]) !== null && _a !== void 0 ? _a : {};
146
+ res.json({ config });
147
+ }
148
+ catch {
149
+ res.status(500).json({ error: 'Failed to read module configuration' });
150
+ }
151
+ });
152
+ // @group APIEndpoints : Set one or more config keys for an installed module
153
+ router.put('/:moduleName/config', async (req, res) => {
154
+ const { moduleName } = req.params;
155
+ const { config } = req.body;
156
+ if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
157
+ return res.status(400).json({ error: 'Invalid module name' });
158
+ }
159
+ if (!config || typeof config !== 'object') {
160
+ return res.status(400).json({ error: 'config object is required' });
161
+ }
162
+ // Validate all keys — no shell metacharacters
163
+ for (const key of Object.keys(config)) {
164
+ if (!/^[a-zA-Z0-9_\-.]+$/.test(key)) {
165
+ return res.status(400).json({ error: `Invalid config key: ${key}` });
166
+ }
167
+ }
168
+ try {
169
+ // Run pm2 set for each key sequentially
170
+ for (const [key, value] of Object.entries(config)) {
171
+ // Wrap value in quotes to handle spaces; strip any embedded quotes first
172
+ const safeValue = String(value).replace(/"/g, '');
173
+ await runPM2CLI(`set ${moduleName}:${key} "${safeValue}"`);
174
+ }
175
+ res.json({ success: true, message: `Configuration updated for ${moduleName}` });
176
+ }
177
+ catch (error) {
178
+ res.status(500).json({ error: 'Failed to update configuration', details: error.message });
179
+ }
180
+ });
126
181
  exports.default = router;
@@ -96,14 +96,14 @@ router.post('/verify', (req, res) => {
96
96
  const config = loadAuthConfig();
97
97
  if (!config) {
98
98
  // No password set — treat as unlocked
99
- return res.json({ success: true });
99
+ return res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
100
100
  }
101
101
  const hash = hashPassword(password, config.salt);
102
102
  const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
103
103
  if (!match) {
104
104
  return res.status(401).json({ success: false, error: 'Incorrect password' });
105
105
  }
106
- res.json({ success: true });
106
+ res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
107
107
  });
108
108
  // @group Endpoints : DELETE /api/auth/remove — remove the password (requires current password)
109
109
  router.delete('/remove', (req, res) => {
@@ -153,7 +153,7 @@ router.post('/pin/verify', (req, res) => {
153
153
  if (!match) {
154
154
  return res.status(401).json({ success: false, error: 'Incorrect PIN' });
155
155
  }
156
- res.json({ success: true });
156
+ res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
157
157
  });
158
158
  // @group Endpoints : DELETE /api/auth/pin/remove — remove PIN (requires current password)
159
159
  router.delete('/pin/remove', (req, res) => {