ezpm2gui 1.6.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/README.md +330 -321
- package/bin/ezpm2gui.js +8 -8
- package/bin/ezpm2gui.ts +51 -51
- package/bin/generate-ecosystem.js +35 -35
- package/bin/generate-ecosystem.ts +56 -56
- 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 +42 -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.js +3 -3
- 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.775772ee.css +0 -5
- package/src/client/build/static/css/main.775772ee.css.map +0 -1
- package/src/client/build/static/js/main.cbcb09c9.js +0 -3
- package/src/client/build/static/js/main.cbcb09c9.js.map +0 -1
- /package/src/client/build/static/js/{main.cbcb09c9.js.LICENSE.txt → main.28a4a583.js.LICENSE.txt} +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
47
|
+
log(`Auto-detected project type: ${detectedType}`);
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
|
-
// Run project setup
|
|
50
|
+
// Run project setup
|
|
37
51
|
if (autoSetup && detectedType && ['node', 'python', 'dotnet'].includes(detectedType)) {
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
118
|
+
done(false, {
|
|
117
119
|
error: 'Deployment failed',
|
|
118
120
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
119
121
|
});
|
|
@@ -159,8 +161,8 @@ router.post('/generate-ecosystem', (req, res) => {
|
|
|
159
161
|
env: pm2Env.env || {}
|
|
160
162
|
};
|
|
161
163
|
});
|
|
162
|
-
const ecosystemConfig = `module.exports = {
|
|
163
|
-
apps: ${JSON.stringify(apps, null, 2)}
|
|
164
|
+
const ecosystemConfig = `module.exports = {
|
|
165
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
164
166
|
};`;
|
|
165
167
|
// Create the file (either at specified path or default location)
|
|
166
168
|
const filePath = req.body.path || path_1.default.join(process.cwd(), 'ecosystem.config.js');
|
|
@@ -213,8 +215,8 @@ router.get('/generate-ecosystem-preview', (req, res) => {
|
|
|
213
215
|
env: pm2Env.env || {}
|
|
214
216
|
};
|
|
215
217
|
});
|
|
216
|
-
const ecosystemConfig = `module.exports = {
|
|
217
|
-
apps: ${JSON.stringify(apps, null, 2)}
|
|
218
|
+
const ecosystemConfig = `module.exports = {
|
|
219
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
218
220
|
};`;
|
|
219
221
|
pm2_1.default.disconnect();
|
|
220
222
|
res.json({
|
|
@@ -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
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
};
|
|
@@ -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) => {
|
|
@@ -819,15 +819,19 @@ router.get('/:connectionId/log-file/download', async (req, res) => {
|
|
|
819
819
|
router.get('/connections', async (req, res) => {
|
|
820
820
|
try {
|
|
821
821
|
const connections = remote_connection_1.remoteConnectionManager.getAllConnections();
|
|
822
|
-
const connectionsList = Array.from(connections.entries()).map(([id, conn]) =>
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
822
|
+
const connectionsList = Array.from(connections.entries()).map(([id, conn]) => {
|
|
823
|
+
const connected = conn.isConnected();
|
|
824
|
+
return {
|
|
825
|
+
id,
|
|
826
|
+
name: conn.name || `${conn.username}@${conn.host}`,
|
|
827
|
+
host: conn.host,
|
|
828
|
+
port: conn.port,
|
|
829
|
+
username: conn.username,
|
|
830
|
+
connected,
|
|
831
|
+
isPM2Installed: conn.isPM2Installed,
|
|
832
|
+
status: connected ? 'connected' : 'disconnected',
|
|
833
|
+
};
|
|
834
|
+
});
|
|
831
835
|
res.json(connectionsList);
|
|
832
836
|
}
|
|
833
837
|
catch (error) {
|
|
@@ -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
|
-
|
|
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 {};
|