From 64bf816e341ae3a731cc35251cc8475e3714a7fd Mon Sep 17 00:00:00 2001 From: treamz Date: Sun, 22 Mar 2026 00:35:15 +0300 Subject: [PATCH] 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 --- lib/auth.js | 4 +- public/status.html | 654 +++++++++++++++++++++++++++++++++++++++++++++ routes/status.js | 159 +++++++++++ server.js | 3 + 4 files changed, 818 insertions(+), 2 deletions(-) create mode 100644 public/status.html create mode 100644 routes/status.js diff --git a/lib/auth.js b/lib/auth.js index 6e5e60f..325569a 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -3,9 +3,9 @@ * Public: landing page, auth routes, static assets, health, placeholder API. */ -const PUBLIC_PATHS = ['/', '/health', '/favicon.ico']; +const PUBLIC_PATHS = ['/', '/health', '/favicon.ico', '/status']; const PUBLIC_API = ['/api/settings', '/api/tools', '/api/content/advantages', '/api/content/dashboard']; -const PUBLIC_PREFIXES = ['/auth/', '/vendor/', '/placeholder-img/', '/admin']; +const PUBLIC_PREFIXES = ['/auth/', '/vendor/', '/placeholder-img/', '/admin', '/status/']; const PUBLIC_FILES = ['/shared.css', '/shared.js', '/landing.html']; function authMiddleware(req, res, next) { diff --git a/public/status.html b/public/status.html new file mode 100644 index 0000000..e775409 --- /dev/null +++ b/public/status.html @@ -0,0 +1,654 @@ + + + + + + Статус системы — WA Dev Tools + + + + + + + + + + + + + +
+
+
+ + +
+
Проверка состояния...
+
+ + +
+ + +
Сервисы
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
Сервер
+
+
+
+
Node.js
+
+
+
+
Аптайм
+
+
+
+
Память
+
+
+
+
PID
+
+
+
+
+ + + + +
+ + + +
+ + + + diff --git a/routes/status.js b/routes/status.js new file mode 100644 index 0000000..17a3fe6 --- /dev/null +++ b/routes/status.js @@ -0,0 +1,159 @@ +'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} + */ +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; diff --git a/server.js b/server.js index 0415422..afa7f2a 100755 --- a/server.js +++ b/server.js @@ -29,6 +29,9 @@ app.get('/api/tools', apiRouter); app.get('/api/content/advantages', apiRouter); app.get('/api/content/dashboard', apiRouter); +// Public status page (before auth — no login required) +app.use('/status', require('./routes/status')); + // Auth middleware (protects everything below) app.use(authMiddleware);