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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Проверка состояния...
+
+
+
+
+
+
+
Сервисы
+
+
+
+
Сервер
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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);