auto: session-start commit — 2 file(s) at 2026-05-24 20:20:21

This commit is contained in:
librelad 2026-05-24 20:20:21 +01:00
parent a9c83b06b0
commit b9ae512d31
2 changed files with 107 additions and 0 deletions

View File

@ -15,6 +15,7 @@ const authRoutes = require('./auth-routes.js');
const taskRoutes = require('./task-routes.js');
const serviceRoutes = require('./service-routes.js');
const setupRoutes = require('./setup-routes.js');
const systemRoutes = require('./system-routes.js');
const { testConnection } = require('../utils/mail.js');
module.exports = {
@ -30,6 +31,7 @@ module.exports = {
app.use('/api/tasks', taskRoutes); // requireAuth applied per-route inside
app.use('/api/apps', serviceRoutes); // requireAuth applied per-route inside
app.use('/api/setup', setupRoutes); // requireAuth applied per-route inside
app.use('/api/system', requireAuth, systemRoutes); // live host metrics (/proc)
app.post('/api/test-mail-connection', requireAuth, testConnection);
app.post('/api/gluetun/mullvad-wireguard', requireAuth, async (req, res) => {

View File

@ -0,0 +1,105 @@
// Live system metrics — the fast path behind the Admin → System gauges.
//
// The periodic, host-side picture (disk, network, docker, per-app, 24h history)
// is produced by the webui_system_metrics generator into frontend/data/system/.
// This endpoint exists only to make the headline gauges *tick* every couple of
// seconds, so it is deliberately the cheapest, safest thing it can be:
// - reads only /proc (no subprocess spawn, no docker, no privileges)
// - CPU% from an in-memory delta of the previous /proc/stat read
// - a single-flight + short cache so N open tabs cause ~1 /proc read/sec
//
// Namespace note: this runs *inside* the libreportal container. /proc/stat,
// /proc/meminfo and /proc/loadavg are not namespaced, so they report host-wide
// values that match the generator's numbers. /proc/net/dev IS per-netns (it
// would show only this container's traffic), so network is intentionally left
// to the host-side generator and not served here.
const express = require('express');
const fs = require('fs').promises;
const os = require('os');
const router = express.Router();
const CORES = os.cpus().length || 1;
const MIN_INTERVAL_MS = 750; // serve cache to anything faster than this
let prevCpu = null; // { total, idle } from the last read
let cache = null; // { sample, at }
let inflight = null; // dedupe concurrent cache-miss reads
async function readCpu() {
const data = await fs.readFile('/proc/stat', 'utf8');
const first = data.split('\n', 1)[0]; // "cpu u n s i io irq sirq steal ..."
const n = first.trim().split(/\s+/).slice(1).map(Number);
const idle = (n[3] || 0) + (n[4] || 0); // idle + iowait
const total = n.reduce((a, b) => a + (b || 0), 0);
return { total, idle };
}
async function readMem() {
const data = await fs.readFile('/proc/meminfo', 'utf8');
const m = {};
for (const line of data.split('\n')) {
const mm = line.match(/^(\w+):\s+(\d+)/);
if (mm) m[mm[1]] = parseInt(mm[2], 10) * 1024; // kB -> bytes
}
const total = m.MemTotal || 0;
const available = m.MemAvailable || 0;
const used = Math.max(0, total - available);
const swapTotal = m.SwapTotal || 0;
const swapUsed = Math.max(0, swapTotal - (m.SwapFree || 0));
return {
total, used, available,
percent: total ? +(used / total * 100).toFixed(1) : 0,
swap_total: swapTotal, swap_used: swapUsed,
swap_percent: swapTotal ? +(swapUsed / swapTotal * 100).toFixed(1) : 0
};
}
async function readLoad() {
const data = await fs.readFile('/proc/loadavg', 'utf8');
const [l1, l5, l15] = data.trim().split(/\s+/).map(Number);
return { load1: l1 || 0, load5: l5 || 0, load15: l15 || 0 };
}
async function sample() {
const [cpuNow, memory, load] = await Promise.all([readCpu(), readMem(), readLoad()]);
let percent = 0;
if (prevCpu) {
const dt = cpuNow.total - prevCpu.total;
const di = cpuNow.idle - prevCpu.idle;
if (dt > 0) percent = +Math.max(0, Math.min(100, (1 - di / dt) * 100)).toFixed(1);
}
prevCpu = cpuNow;
return {
cpu: {
percent,
cores: CORES,
load1: load.load1, load5: load.load5, load15: load.load15,
load1_percent: +Math.min(100, load.load1 / CORES * 100).toFixed(1)
},
memory,
t: Date.now()
};
}
router.get('/live', async (req, res) => {
const now = Date.now();
if (cache && (now - cache.at) < MIN_INTERVAL_MS) {
res.set('Cache-Control', 'no-store');
return res.json(cache.sample);
}
try {
if (!inflight) {
inflight = sample()
.then((s) => { cache = { sample: s, at: Date.now() }; return s; })
.finally(() => { inflight = null; });
}
const s = await inflight;
res.set('Cache-Control', 'no-store');
res.json(s);
} catch (err) {
res.status(500).json({ error: 'metrics_unavailable' });
}
});
module.exports = router;