Skip to content

Commit 4eb2eab

Browse files
committed
system info endpoints
1 parent 7e26ecf commit 4eb2eab

4 files changed

Lines changed: 115 additions & 0 deletions

File tree

package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"multer": "^2.0.2",
3939
"node-cron": "^4.2.1",
4040
"simple-git": "^3.30.0",
41+
"systeminformation": "^5.31.5",
4142
"zod": "^4.1.12"
4243
},
4344
"devDependencies": {

src/admin/adminRouter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
triggerPruning,
1717
updateUserName,
1818
} from './adminController.js';
19+
import { getStorageInfo, streamSystemStats } from './systemInfoController.js';
1920

2021
const router = Router();
2122

@@ -31,6 +32,10 @@ router.get('/audit-logs', (_req, res) => {
3132
res.json({ message: 'Admin audit logs endpoint' });
3233
});
3334

35+
// System info (admin only; protected in server.ts)
36+
router.get('/system/stream', streamSystemStats);
37+
router.get('/system/storage', getStorageInfo);
38+
3439
// Resource management route - get all non-pruned projects
3540
router.get('/resources/projects', getAllProjects);
3641

src/admin/systemInfoController.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { Request, Response } from 'express';
2+
import si from 'systeminformation';
3+
4+
const writeSseEvent = (res: Response, event: string, data: unknown) => {
5+
res.write(`event: ${event}\n`);
6+
res.write(`data: ${JSON.stringify(data)}\n\n`);
7+
};
8+
9+
export const streamSystemStats = async (req: Request, res: Response) => {
10+
// Server-Sent Events (SSE)
11+
res.status(200);
12+
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
13+
res.setHeader('Cache-Control', 'no-cache, no-transform');
14+
res.setHeader('Connection', 'keep-alive');
15+
// Prevent nginx from buffering SSE
16+
res.setHeader('X-Accel-Buffering', 'no');
17+
18+
res.flushHeaders();
19+
20+
// Let the client know stream is alive
21+
writeSseEvent(res, 'ready', { ok: true, timestamp: new Date().toISOString() });
22+
23+
const intervalMs = 1000;
24+
25+
const tick = async () => {
26+
try {
27+
const [load, mem] = await Promise.all([si.currentLoad(), si.mem()]);
28+
29+
writeSseEvent(res, 'stats', {
30+
timestamp: new Date().toISOString(),
31+
cpu: {
32+
currentLoad: load.currentLoad, // %
33+
user: load.currentLoadUser, // %
34+
system: load.currentLoadSystem, // %
35+
idle: load.currentLoadIdle, // %
36+
},
37+
memory: {
38+
total: mem.total,
39+
free: mem.free,
40+
used: mem.used,
41+
active: mem.active,
42+
available: mem.available,
43+
swaptotal: mem.swaptotal,
44+
swapused: mem.swapused,
45+
swapfree: mem.swapfree,
46+
},
47+
});
48+
} catch (error) {
49+
writeSseEvent(res, 'error', {
50+
message: error instanceof Error ? error.message : 'Unknown error',
51+
timestamp: new Date().toISOString(),
52+
});
53+
}
54+
};
55+
56+
// send an immediate tick, then every second
57+
await tick();
58+
const timer = setInterval(() => void tick(), intervalMs);
59+
60+
req.on('close', () => {
61+
clearInterval(timer);
62+
});
63+
};
64+
65+
export const getStorageInfo = async (_req: Request, res: Response) => {
66+
const sizes = await si.fsSize();
67+
68+
return res.json({
69+
timestamp: new Date().toISOString(),
70+
filesystems: sizes.map((fs) => ({
71+
fs: fs.fs,
72+
type: fs.type,
73+
mount: fs.mount,
74+
size: fs.size,
75+
used: fs.used,
76+
available: fs.available,
77+
use: fs.use, // %
78+
rw: fs.rw,
79+
})),
80+
});
81+
};
82+

0 commit comments

Comments
 (0)