- 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
160 lines
5.0 KiB
JavaScript
160 lines
5.0 KiB
JavaScript
'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;
|