Merge claude/2
This commit is contained in:
commit
8712be2631
@ -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) => {
|
||||
|
||||
105
containers/libreportal/backend/routes/system-routes.js
Normal file
105
containers/libreportal/backend/routes/system-routes.js
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user