wa-dev-tools/routes/status.js
treamz 64bf816e34 Status page: public monitoring at /status
- Real-time checks: web server, MariaDB, FFmpeg, disk usage
- Auto-refresh every 30 seconds
- Overall status banner (operational/degraded/outage)
- Server info: Node version, uptime, memory, PID
- Public page (no auth required)
- Responsive, theme-aware design
2026-03-22 00:35:15 +03:00

160 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const express = require('express');
const { execFile } = require('child_process');
const path = require('path');
const { pool } = require('../lib/db');
const router = express.Router();
/** Server start time — captured once at module load */
const SERVER_START = Date.now();
// ─── Helpers ─────────────────────────────────────────────────────────────────
/**
* Run a shell command and resolve with its stdout, or reject on error.
* @param {string} cmd
* @param {string[]} args
* @param {number} timeoutMs
* @returns {Promise<string>}
*/
function runCommand(cmd, args, timeoutMs = 3000) {
return new Promise((resolve, reject) => {
execFile(cmd, args, { timeout: timeoutMs }, (err, stdout) => {
if (err) return reject(err);
resolve(stdout.trim());
});
});
}
/**
* Parse `df -h` output for a given mount point.
* Returns { used, total, percent } or null.
* @param {string} mountPoint
* @returns {Promise<{ used: string, total: string, percent: string } | null>}
*/
async function checkDisk(mountPoint) {
try {
const out = await runCommand('df', ['-h', mountPoint]);
// Header line + data line
const lines = out.split('\n').filter(Boolean);
const dataLine = lines[lines.length - 1];
// Columns: Filesystem Size Used Avail Use% Mounted
const parts = dataLine.split(/\s+/);
if (parts.length < 6) return null;
return {
used: parts[2],
total: parts[1],
percent: parts[4], // e.g. "72%"
};
} catch {
return null;
}
}
/**
* Determine overall status from individual checks.
* Any "outage" → overall "outage"; any "degraded" → "degraded"; else "operational".
* @param {{ status: string }[]} checks
* @returns {'operational' | 'degraded' | 'outage'}
*/
function computeOverall(checks) {
if (checks.some(c => c.status === 'outage')) return 'outage';
if (checks.some(c => c.status === 'degraded')) return 'degraded';
return 'operational';
}
// ─── Route: GET /status (serve HTML page) ────────────────────────────────────
router.get('/', (req, res) => {
res.sendFile(path.resolve(__dirname, '../public/status.html'));
});
// ─── Route: GET /status/api ───────────────────────────────────────────────────
router.get('/api', async (req, res) => {
const checks = [];
// 1. Web server — always operational if we reached this handler
checks.push({ name: 'Веб-сервер', status: 'operational', latency: 0 });
// 2. Database — SELECT 1
{
const t0 = Date.now();
try {
await pool.execute('SELECT 1');
checks.push({ name: 'База данных', status: 'operational', latency: Date.now() - t0 });
} catch {
checks.push({ name: 'База данных', status: 'outage', latency: Date.now() - t0 });
}
}
// 3. FFmpeg
{
const t0 = Date.now();
try {
await runCommand('which', ['ffmpeg']);
checks.push({ name: 'FFmpeg', status: 'operational', latency: Date.now() - t0 });
} catch {
checks.push({ name: 'FFmpeg', status: 'degraded', latency: Date.now() - t0 });
}
}
// 4. Disk /
{
const disk = await checkDisk('/');
if (disk) {
const pct = parseInt(disk.percent, 10);
const status = pct >= 95 ? 'outage' : pct >= 85 ? 'degraded' : 'operational';
checks.push({
name: 'Диск / (NVMe)',
status,
detail: `${disk.percent} использовано (${disk.used} / ${disk.total})`,
});
} else {
checks.push({ name: 'Диск / (NVMe)', status: 'degraded', detail: 'Не удалось проверить' });
}
}
// 5. Disk /mnt/webdata
{
const disk = await checkDisk('/mnt/webdata');
if (disk) {
const pct = parseInt(disk.percent, 10);
const status = pct >= 95 ? 'outage' : pct >= 85 ? 'degraded' : 'operational';
checks.push({
name: 'Диск /mnt/webdata',
status,
detail: `${disk.percent} использовано (${disk.used} / ${disk.total})`,
});
} else {
// Mount point may not exist on dev — treat as degraded, not outage
checks.push({ name: 'Диск /mnt/webdata', status: 'degraded', detail: 'Не смонтирован' });
}
}
const mem = process.memoryUsage();
res.json({
overall: computeOverall(checks),
uptime: Math.floor((Date.now() - SERVER_START) / 1000),
checks,
server: {
node: process.version,
memory: {
rss: Math.round(mem.rss / 1024 / 1024) + 'MB',
heap:
Math.round(mem.heapUsed / 1024 / 1024) +
'/' +
Math.round(mem.heapTotal / 1024 / 1024) +
'MB',
},
pid: process.pid,
},
timestamp: new Date().toISOString(),
});
});
module.exports = router;