feat(alerts): enhance alert settings with customizable durations for server offline and resource usage notifications
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m59s

This commit is contained in:
2026-02-24 01:04:29 +07:00
parent b35f787626
commit 2e02d550f6
2 changed files with 232 additions and 155 deletions
+9 -7
View File
@@ -11,11 +11,11 @@ const { getUptimeCacheData } = require('./mikrotikConfigRoutes');
/** Значения по умолчанию для настроек оповещений. */ /** Значения по умолчанию для настроек оповещений. */
const DEFAULT_ALERT_SETTINGS = { const DEFAULT_ALERT_SETTINGS = {
serverOffline: { enabled: true }, serverOffline: { enabled: true, offlineMinutes: 5 },
mikrotikUnreachable: { enabled: true }, mikrotikUnreachable: { enabled: true, durationMinutes: 5 },
highCpu: { enabled: true, thresholdPercent: 85 }, highCpu: { enabled: true, thresholdPercent: 85, durationMinutes: 10 },
highRam: { enabled: true, thresholdPercent: 85 }, highRam: { enabled: true, thresholdPercent: 85, durationMinutes: 10 },
highHdd: { enabled: true, thresholdPercent: 90 }, highHdd: { enabled: true, thresholdPercent: 90, durationMinutes: 10 },
}; };
function getAlertSettings(uiSettings) { function getAlertSettings(uiSettings) {
@@ -64,9 +64,11 @@ async function getAlerts(req, res) {
if (isRouter) { if (isRouter) {
const entry = uptimeResults[serverId]; const entry = uptimeResults[serverId];
offline = entry ? entry.ok === false : false; const offlineMinutes = Math.max(1, Math.min(1440, Number(settings.serverOffline.offlineMinutes) || 5));
const offlineSinceMs = entry && entry.lastCheckTs ? (Date.now() - entry.lastCheckTs) : 0;
offline = entry && entry.ok === false && offlineSinceMs >= offlineMinutes * 60 * 1000;
description = offline description = offline
? `${name} недоступен (по проверке Uptime Monitor).` ? `${name} недоступен более ${offlineMinutes} мин (по проверке Uptime Monitor).`
: ''; : '';
} else { } else {
const idx = nonRouterServers.findIndex( const idx = nonRouterServers.findIndex(
+223 -148
View File
@@ -102,13 +102,18 @@ export default function SettingsPage() {
const [uptimeMonitorSchedulerEnabled, setUptimeMonitorSchedulerEnabled] = useState(true); const [uptimeMonitorSchedulerEnabled, setUptimeMonitorSchedulerEnabled] = useState(true);
const [uptimeMonitorSchedulerIntervalMinutes, setUptimeMonitorSchedulerIntervalMinutes] = useState('2'); const [uptimeMonitorSchedulerIntervalMinutes, setUptimeMonitorSchedulerIntervalMinutes] = useState('2');
const [alertServerOffline, setAlertServerOffline] = useState(true); const [alertServerOffline, setAlertServerOffline] = useState(true);
const [alertServerOfflineMinutes, setAlertServerOfflineMinutes] = useState('5');
const [alertMikrotikUnreachable, setAlertMikrotikUnreachable] = useState(true); const [alertMikrotikUnreachable, setAlertMikrotikUnreachable] = useState(true);
const [alertMikrotikUnreachableMinutes, setAlertMikrotikUnreachableMinutes] = useState('5');
const [alertHighCpuEnabled, setAlertHighCpuEnabled] = useState(true); const [alertHighCpuEnabled, setAlertHighCpuEnabled] = useState(true);
const [alertHighCpuThreshold, setAlertHighCpuThreshold] = useState('85'); const [alertHighCpuThreshold, setAlertHighCpuThreshold] = useState('85');
const [alertHighCpuDurationMinutes, setAlertHighCpuDurationMinutes] = useState('10');
const [alertHighRamEnabled, setAlertHighRamEnabled] = useState(true); const [alertHighRamEnabled, setAlertHighRamEnabled] = useState(true);
const [alertHighRamThreshold, setAlertHighRamThreshold] = useState('85'); const [alertHighRamThreshold, setAlertHighRamThreshold] = useState('85');
const [alertHighRamDurationMinutes, setAlertHighRamDurationMinutes] = useState('10');
const [alertHighHddEnabled, setAlertHighHddEnabled] = useState(true); const [alertHighHddEnabled, setAlertHighHddEnabled] = useState(true);
const [alertHighHddThreshold, setAlertHighHddThreshold] = useState('90'); const [alertHighHddThreshold, setAlertHighHddThreshold] = useState('90');
const [alertHighHddDurationMinutes, setAlertHighHddDurationMinutes] = useState('10');
const [serversList, setServersList] = useState([]); const [serversList, setServersList] = useState([]);
const [sidebarSearch, setSidebarSearch] = useState(''); const [sidebarSearch, setSidebarSearch] = useState('');
const [activeSection, setActiveSection] = useState(() => { const [activeSection, setActiveSection] = useState(() => {
@@ -316,13 +321,18 @@ export default function SettingsPage() {
); );
const a = data?.alertSettings || {}; const a = data?.alertSettings || {};
setAlertServerOffline(a.serverOffline?.enabled !== false); setAlertServerOffline(a.serverOffline?.enabled !== false);
setAlertServerOfflineMinutes(a.serverOffline?.offlineMinutes != null ? String(a.serverOffline.offlineMinutes) : '5');
setAlertMikrotikUnreachable(a.mikrotikUnreachable?.enabled !== false); setAlertMikrotikUnreachable(a.mikrotikUnreachable?.enabled !== false);
setAlertMikrotikUnreachableMinutes(a.mikrotikUnreachable?.durationMinutes != null ? String(a.mikrotikUnreachable.durationMinutes) : '5');
setAlertHighCpuEnabled(a.highCpu?.enabled !== false); setAlertHighCpuEnabled(a.highCpu?.enabled !== false);
setAlertHighCpuThreshold(a.highCpu?.thresholdPercent != null ? String(a.highCpu.thresholdPercent) : '85'); setAlertHighCpuThreshold(a.highCpu?.thresholdPercent != null ? String(a.highCpu.thresholdPercent) : '85');
setAlertHighCpuDurationMinutes(a.highCpu?.durationMinutes != null ? String(a.highCpu.durationMinutes) : '10');
setAlertHighRamEnabled(a.highRam?.enabled !== false); setAlertHighRamEnabled(a.highRam?.enabled !== false);
setAlertHighRamThreshold(a.highRam?.thresholdPercent != null ? String(a.highRam.thresholdPercent) : '85'); setAlertHighRamThreshold(a.highRam?.thresholdPercent != null ? String(a.highRam.thresholdPercent) : '85');
setAlertHighRamDurationMinutes(a.highRam?.durationMinutes != null ? String(a.highRam.durationMinutes) : '10');
setAlertHighHddEnabled(a.highHdd?.enabled !== false); setAlertHighHddEnabled(a.highHdd?.enabled !== false);
setAlertHighHddThreshold(a.highHdd?.thresholdPercent != null ? String(a.highHdd.thresholdPercent) : '90'); setAlertHighHddThreshold(a.highHdd?.thresholdPercent != null ? String(a.highHdd.thresholdPercent) : '90');
setAlertHighHddDurationMinutes(a.highHdd?.durationMinutes != null ? String(a.highHdd.durationMinutes) : '10');
const e = const e =
settingsRes?.headers?.etag || settingsRes?.headers?.ETag || ''; settingsRes?.headers?.etag || settingsRes?.headers?.ETag || '';
setEtag(e ? String(e) : ''); setEtag(e ? String(e) : '');
@@ -438,19 +448,28 @@ export default function SettingsPage() {
})) }))
: [], : [],
alertSettings: { alertSettings: {
serverOffline: { enabled: alertServerOffline }, serverOffline: {
mikrotikUnreachable: { enabled: alertMikrotikUnreachable }, enabled: alertServerOffline,
offlineMinutes: Math.max(1, Math.min(1440, parseInt(alertServerOfflineMinutes, 10) || 5)),
},
mikrotikUnreachable: {
enabled: alertMikrotikUnreachable,
durationMinutes: Math.max(1, Math.min(1440, parseInt(alertMikrotikUnreachableMinutes, 10) || 5)),
},
highCpu: { highCpu: {
enabled: alertHighCpuEnabled, enabled: alertHighCpuEnabled,
thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighCpuThreshold, 10) || 85)), thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighCpuThreshold, 10) || 85)),
durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighCpuDurationMinutes, 10) || 10)),
}, },
highRam: { highRam: {
enabled: alertHighRamEnabled, enabled: alertHighRamEnabled,
thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighRamThreshold, 10) || 85)), thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighRamThreshold, 10) || 85)),
durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighRamDurationMinutes, 10) || 10)),
}, },
highHdd: { highHdd: {
enabled: alertHighHddEnabled, enabled: alertHighHddEnabled,
thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighHddThreshold, 10) || 90)), thresholdPercent: Math.max(1, Math.min(100, parseInt(alertHighHddThreshold, 10) || 90)),
durationMinutes: Math.max(1, Math.min(1440, parseInt(alertHighHddDurationMinutes, 10) || 10)),
}, },
}, },
}; };
@@ -1088,160 +1107,216 @@ export default function SettingsPage() {
<> <>
<SectionHeading title="Настройки оповещений" icon={IconBell} /> <SectionHeading title="Настройки оповещений" icon={IconBell} />
<p className="text-muted mb-4"> <p className="text-muted mb-4">
Включите или отключите типы оповещений и задайте пороги. Оповещения отображаются на панели и в колокольчике в шапке. Настройте пороги и длительность для каждого типа оповещений. Оповещения отображаются на панели и в колокольчике в шапке.
</p> </p>
<div className="mb-4"> <div className="list-group list-group-flush">
<h4 className="subheader mb-3">Статус и доступность</h4> {/* Статус: сервер недоступен */}
<p className="text-muted small mb-3"> <div className="list-group-item d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 py-3">
Для <strong>jumphost/home</strong> используются те же правила, что и в Uptime Monitor: тип проверки (HTTP, internal-ping, external-ping) и кеш. Настройки — в разделе <a href="#uptime-monitor" onClick={(e) => { e.preventDefault(); goToSection('uptime-monitor'); }}>Uptime Monitor</a>. Для остальных серверов — проверка TCP (80/443). <div className="flex-grow-1 min-w-0">
</p> <div className="fw-semibold">Статус</div>
<div className="form-check form-switch mb-3"> <div className="text-muted small">Система не в сети более указанного времени. Для jumphost/home — правила <a href="#uptime-monitor" onClick={(e) => { e.preventDefault(); goToSection('uptime-monitor'); }}>Uptime Monitor</a>.</div>
<input </div>
className="form-check-input" <div className="d-flex flex-wrap align-items-center gap-2">
type="checkbox" <div className="input-group input-group-sm" style={{ width: 100 }}>
id="alertServerOffline" <input
checked={alertServerOffline} type="number"
onChange={(e) => setAlertServerOffline(e.target.checked)} className="form-control"
disabled={saving} min={1}
/> max={1440}
<label className="form-check-label" htmlFor="alertServerOffline"> value={alertServerOfflineMinutes}
Сервер недоступен — оповещение при недоступности (по правилам Uptime Monitor для роутеров, TCP для остальных). onChange={(e) => setAlertServerOfflineMinutes(e.target.value)}
</label> disabled={saving || !alertServerOffline}
/>
<span className="input-group-text">мин</span>
</div>
<div className="form-check form-switch mb-0">
<input
className="form-check-input"
type="checkbox"
id="alertServerOffline"
checked={alertServerOffline}
onChange={(e) => setAlertServerOffline(e.target.checked)}
disabled={saving}
/>
<label className="form-check-label small" htmlFor="alertServerOffline">Вкл</label>
</div>
</div>
</div> </div>
<div className="form-check form-switch mb-3">
<input
className="form-check-input"
type="checkbox"
id="alertMikrotikUnreachable"
checked={alertMikrotikUnreachable}
onChange={(e) => setAlertMikrotikUnreachable(e.target.checked)}
disabled={saving}
/>
<label className="form-check-label" htmlFor="alertMikrotikUnreachable">
MikroTik недоступен — срабатывает при ошибке доступа к RouterOS (таймаут, неверный пароль и т.п.).
</label>
</div>
</div>
<div className="mb-4"> {/* MikroTik недоступен */}
<h4 className="subheader mb-3">Ресурсы роутеров (MikroTik)</h4> <div className="list-group-item d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 py-3">
<div className="row g-3"> <div className="flex-grow-1 min-w-0">
<div className="col-12 col-md-6"> <div className="fw-semibold">MikroTik недоступен</div>
<div className="card"> <div className="text-muted small">Ошибка доступа к RouterOS (таймаут, неверный пароль) в течение указанного времени.</div>
<div className="card-body"> </div>
<div className="d-flex align-items-center mb-2"> <div className="d-flex flex-wrap align-items-center gap-2">
<span className="avatar avatar-sm me-2 bg-blue-lt text-blue"> <div className="input-group input-group-sm" style={{ width: 100 }}>
<IconCpu size={18} /> <input
</span> type="number"
<span className="fw-medium">Использование CPU</span> className="form-control"
</div> min={1}
<div className="form-check form-switch mb-2"> max={1440}
<input value={alertMikrotikUnreachableMinutes}
className="form-check-input" onChange={(e) => setAlertMikrotikUnreachableMinutes(e.target.value)}
type="checkbox" disabled={saving || !alertMikrotikUnreachable}
id="alertHighCpuEnabled" />
checked={alertHighCpuEnabled} <span className="input-group-text">мин</span>
onChange={(e) => setAlertHighCpuEnabled(e.target.checked)} </div>
disabled={saving} <div className="form-check form-switch mb-0">
/> <input
<label className="form-check-label small" htmlFor="alertHighCpuEnabled"> className="form-check-input"
Включить оповещение type="checkbox"
</label> id="alertMikrotikUnreachable"
</div> checked={alertMikrotikUnreachable}
<div className="input-group input-group-sm"> onChange={(e) => setAlertMikrotikUnreachable(e.target.checked)}
<span className="input-group-text">Порог</span> disabled={saving}
<input />
type="number" <label className="form-check-label small" htmlFor="alertMikrotikUnreachable">Вкл</label>
className="form-control"
min={1}
max={100}
value={alertHighCpuThreshold}
onChange={(e) => setAlertHighCpuThreshold(e.target.value)}
disabled={saving || !alertHighCpuEnabled}
/>
<span className="input-group-text">%</span>
</div>
<div className="form-text small">Среднее превышает указанный %.</div>
</div>
</div> </div>
</div> </div>
<div className="col-12 col-md-6"> </div>
<div className="card">
<div className="card-body"> {/* Использование CPU */}
<div className="d-flex align-items-center mb-2"> <div className="list-group-item d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 py-3">
<span className="avatar avatar-sm me-2 bg-green-lt text-green"> <div className="flex-grow-1 min-w-0">
<IconDeviceDesktop size={18} /> <div className="fw-semibold d-flex align-items-center gap-2">
</span> <IconCpu size={18} className="text-blue" />
<span className="fw-medium">Использование памяти</span> Использование CPU
</div> </div>
<div className="form-check form-switch mb-2"> <div className="text-muted small">Среднее превышает порог в течение указанного времени.</div>
<input </div>
className="form-check-input" <div className="d-flex flex-wrap align-items-center gap-2">
type="checkbox" <div className="input-group input-group-sm" style={{ width: 80 }}>
id="alertHighRamEnabled" <input
checked={alertHighRamEnabled} type="number"
onChange={(e) => setAlertHighRamEnabled(e.target.checked)} className="form-control"
disabled={saving} min={1}
/> max={100}
<label className="form-check-label small" htmlFor="alertHighRamEnabled"> value={alertHighCpuThreshold}
Включить оповещение onChange={(e) => setAlertHighCpuThreshold(e.target.value)}
</label> disabled={saving || !alertHighCpuEnabled}
</div> />
<div className="input-group input-group-sm"> <span className="input-group-text">%</span>
<span className="input-group-text">Порог</span> </div>
<input <div className="input-group input-group-sm" style={{ width: 100 }}>
type="number" <input
className="form-control" type="number"
min={1} className="form-control"
max={100} min={1}
value={alertHighRamThreshold} max={1440}
onChange={(e) => setAlertHighRamThreshold(e.target.value)} value={alertHighCpuDurationMinutes}
disabled={saving || !alertHighRamEnabled} onChange={(e) => setAlertHighCpuDurationMinutes(e.target.value)}
/> disabled={saving || !alertHighCpuEnabled}
<span className="input-group-text">%</span> />
</div> <span className="input-group-text">мин</span>
<div className="form-text small">Среднее превышает указанный %.</div> </div>
</div> <div className="form-check form-switch mb-0">
<input
className="form-check-input"
type="checkbox"
id="alertHighCpuEnabled"
checked={alertHighCpuEnabled}
onChange={(e) => setAlertHighCpuEnabled(e.target.checked)}
disabled={saving}
/>
<label className="form-check-label small" htmlFor="alertHighCpuEnabled">Вкл</label>
</div> </div>
</div> </div>
<div className="col-12 col-md-6"> </div>
<div className="card">
<div className="card-body"> {/* Использование памяти */}
<div className="d-flex align-items-center mb-2"> <div className="list-group-item d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 py-3">
<span className="avatar avatar-sm me-2 bg-orange-lt text-orange"> <div className="flex-grow-1 min-w-0">
<IconDatabase size={18} /> <div className="fw-semibold d-flex align-items-center gap-2">
</span> <IconDeviceDesktop size={18} className="text-green" />
<span className="fw-medium">Использование диска</span> Использование памяти
</div> </div>
<div className="form-check form-switch mb-2"> <div className="text-muted small">Среднее превышает порог в течение указанного времени.</div>
<input </div>
className="form-check-input" <div className="d-flex flex-wrap align-items-center gap-2">
type="checkbox" <div className="input-group input-group-sm" style={{ width: 80 }}>
id="alertHighHddEnabled" <input
checked={alertHighHddEnabled} type="number"
onChange={(e) => setAlertHighHddEnabled(e.target.checked)} className="form-control"
disabled={saving} min={1}
/> max={100}
<label className="form-check-label small" htmlFor="alertHighHddEnabled"> value={alertHighRamThreshold}
Включить оповещение onChange={(e) => setAlertHighRamThreshold(e.target.value)}
</label> disabled={saving || !alertHighRamEnabled}
</div> />
<div className="input-group input-group-sm"> <span className="input-group-text">%</span>
<span className="input-group-text">Порог</span> </div>
<input <div className="input-group input-group-sm" style={{ width: 100 }}>
type="number" <input
className="form-control" type="number"
min={1} className="form-control"
max={100} min={1}
value={alertHighHddThreshold} max={1440}
onChange={(e) => setAlertHighHddThreshold(e.target.value)} value={alertHighRamDurationMinutes}
disabled={saving || !alertHighHddEnabled} onChange={(e) => setAlertHighRamDurationMinutes(e.target.value)}
/> disabled={saving || !alertHighRamEnabled}
<span className="input-group-text">%</span> />
</div> <span className="input-group-text">мин</span>
<div className="form-text small">Среднее превышает указанный %.</div> </div>
</div> <div className="form-check form-switch mb-0">
<input
className="form-check-input"
type="checkbox"
id="alertHighRamEnabled"
checked={alertHighRamEnabled}
onChange={(e) => setAlertHighRamEnabled(e.target.checked)}
disabled={saving}
/>
<label className="form-check-label small" htmlFor="alertHighRamEnabled">Вкл</label>
</div>
</div>
</div>
{/* Использование диска */}
<div className="list-group-item d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-3 py-3">
<div className="flex-grow-1 min-w-0">
<div className="fw-semibold d-flex align-items-center gap-2">
<IconDatabase size={18} className="text-orange" />
Использование диска
</div>
<div className="text-muted small">Среднее превышает порог в течение указанного времени.</div>
</div>
<div className="d-flex flex-wrap align-items-center gap-2">
<div className="input-group input-group-sm" style={{ width: 80 }}>
<input
type="number"
className="form-control"
min={1}
max={100}
value={alertHighHddThreshold}
onChange={(e) => setAlertHighHddThreshold(e.target.value)}
disabled={saving || !alertHighHddEnabled}
/>
<span className="input-group-text">%</span>
</div>
<div className="input-group input-group-sm" style={{ width: 100 }}>
<input
type="number"
className="form-control"
min={1}
max={1440}
value={alertHighHddDurationMinutes}
onChange={(e) => setAlertHighHddDurationMinutes(e.target.value)}
disabled={saving || !alertHighHddEnabled}
/>
<span className="input-group-text">мин</span>
</div>
<div className="form-check form-switch mb-0">
<input
className="form-check-input"
type="checkbox"
id="alertHighHddEnabled"
checked={alertHighHddEnabled}
onChange={(e) => setAlertHighHddEnabled(e.target.checked)}
disabled={saving}
/>
<label className="form-check-label small" htmlFor="alertHighHddEnabled">Вкл</label>
</div> </div>
</div> </div>
</div> </div>