Watchdog: Embeds statt Textzeilen, dazu eine Uebersicht im Panel
Die DM-Alarme waren als einzige Meldung noch reiner Text — direkt neben dem Raid-Alarm, der ein Embed ist. Jetzt beide im selben Look, mit Ausfalldauer bzw. Fehlergrund und Antwortzeit als Felder. Uebersicht im Panel (System, unter den Watchdog-URLs): je Adresse ein Erreichbarkeits-Balken ueber 7 Tage, gruen/rot/grau wie bei den Game-Servern, dazu Status, Antwortzeit und seit wann etwas weg ist. Dafuer schreibt der Waechter jetzt mit — vorher lebte sein Zustand nur im Speicher und war nach jedem Neustart weg. Neue Tabelle watchdog_history, gleiche Bauart und gleiche 7-Tage-Aufbewahrung wie player_history, stundenweise zusammengefasst statt roh ausgeliefert. Wichtig dabei: aufgezeichnet wird jeder Check, nicht nur die Wechsel. Sonst saehen ruhige Stunden im Balken aus wie Messluecken. Der Balken ist aus Server.jsx in components/UptimeTrack.jsx gewandert, damit oeffentliche Seite und Panel denselben benutzen. Er zeigt die Antwortzeit im Tooltip nur, wo sie erhoben wird — bei Game-Servern gibt es keine. Der Endpunkt /api/watchdog liegt hinter der Rechtepruefung, nicht oeffentlich: in der Adressliste koennen interne Dienste stehen. Zwei fehlende Importe in api.js gefunden und ergaenzt (moduleEnabled, tuning) — der erste Aufruf der Route waere sonst mit ReferenceError gestorben. Geprueft: alle 171 SQL-Abfragen gegen das Schema, die drei neuen inklusive. Im Browser mit drei Faellen — laeuft, ist weg, noch nie geprueft: Punktfarbe, Quote, Ausfallstunden und Messluecken stimmen, die ungepruefte Adresse zeigt korrekt gar keinen Balken. Oeffentliche Server-Seite nach dem Umbau gegengeprueft. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+49
-12
@@ -1,23 +1,31 @@
|
||||
// Watchdog: prüft konfigurierte URLs und schickt dem Admin eine DM bei Ausfall.
|
||||
// Watchdog: prüft konfigurierte URLs, meldet Ausfälle per DM und schreibt
|
||||
// jeden Check mit — daraus baut die Übersicht im Panel ihren Uptime-Balken.
|
||||
// URLs kommen aus dem Setting watchdog_urls (Config-Seite), leer = deaktiviert.
|
||||
import { getSetting } from '../db.js';
|
||||
import { getSetting, recordWatchdogSample, pruneWatchdogHistory } from '../db.js';
|
||||
import { config } from '../config.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
||||
|
||||
// URL → { fails, down, since }
|
||||
const state = new Map();
|
||||
|
||||
function watchedUrls() {
|
||||
/** Aktueller Stand je Adresse — für die Übersicht im Panel */
|
||||
export function watchdogState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
export function watchedUrls() {
|
||||
return (getSetting('watchdog_urls') ?? '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => /^https?:\/\//.test(s));
|
||||
}
|
||||
|
||||
async function dmAdmin(client, content) {
|
||||
const user = await client.users.fetch(config.adminDiscordId);
|
||||
await user.send(content);
|
||||
async function dmAdmin(client, embed) {
|
||||
if (!config.adminDiscordId) return;
|
||||
const user = await client.users.fetch(config.adminDiscordId).catch(() => null);
|
||||
await user?.send({ embeds: [embed] }).catch(() => {});
|
||||
}
|
||||
|
||||
/** Ein Prüfdurchlauf — exportiert für Tests und den Intervall-Timer */
|
||||
@@ -28,6 +36,8 @@ export async function watchdogTick(client, { failsBeforeAlert = tuning('watchdog
|
||||
|
||||
let ok = false;
|
||||
let detail = '';
|
||||
let status = null;
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -36,29 +46,56 @@ export async function watchdogTick(client, { failsBeforeAlert = tuning('watchdog
|
||||
headers: { 'User-Agent': 'd4rkbot-watchdog/1.0' },
|
||||
});
|
||||
ok = res.status < 500;
|
||||
status = res.status;
|
||||
detail = `HTTP ${res.status}`;
|
||||
} catch (error) {
|
||||
detail = error.cause?.code ?? error.name ?? 'Fehler';
|
||||
}
|
||||
const dauer = Date.now() - start;
|
||||
|
||||
// Immer mitschreiben, nicht nur bei Wechseln — der Balken braucht
|
||||
// durchgehende Messpunkte, sonst sähen ruhige Stunden aus wie Lücken.
|
||||
try {
|
||||
recordWatchdogSample(url, ok, ok ? dauer : null);
|
||||
} catch (error) {
|
||||
console.error('[watchdog] Messpunkt nicht gespeichert:', error.message);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
if (s.down) {
|
||||
const minutes = Math.max(1, Math.round((Date.now() - s.since) / 60000));
|
||||
await dmAdmin(client, `✅ **${url}** ist wieder erreichbar (Downtime: ~${minutes} min).`).catch(() => {});
|
||||
const minuten = Math.max(1, Math.round((Date.now() - s.since) / 60000));
|
||||
await dmAdmin(client, brandEmbed(client, 'WATCHDOG')
|
||||
.setTitle('✅ Wieder erreichbar')
|
||||
.setDescription(`**${url}**`)
|
||||
.addFields(
|
||||
{ name: 'Ausfall', value: `~${minuten} min`, inline: true },
|
||||
{ name: 'Antwort', value: `${detail} · ${dauer} ms`, inline: true },
|
||||
));
|
||||
console.log(`[watchdog] ${url} wieder ok`);
|
||||
}
|
||||
state.set(url, { fails: 0, down: false, since: null });
|
||||
state.set(url, { fails: 0, down: false, since: null, status, ms: dauer, geprueft: Date.now() });
|
||||
} else {
|
||||
const fails = s.fails + 1;
|
||||
if (!s.down && fails >= failsBeforeAlert) {
|
||||
await dmAdmin(client, `🚨 **${url}** ist DOWN (${detail}) — seit ${failsBeforeAlert} Checks nicht erreichbar!`).catch(() => {});
|
||||
await dmAdmin(client, brandEmbed(client, 'WATCHDOG', { secondary: true })
|
||||
.setTitle('🚨 Nicht erreichbar')
|
||||
.setDescription(`**${url}**`)
|
||||
.addFields(
|
||||
{ name: 'Fehler', value: detail, inline: true },
|
||||
{ name: 'Versuche', value: `${fails} in Folge`, inline: true },
|
||||
));
|
||||
console.warn(`[watchdog] ${url} down (${detail})`);
|
||||
state.set(url, { fails, down: true, since: Date.now() });
|
||||
state.set(url, { fails, down: true, since: Date.now(), status, ms: null, geprueft: Date.now() });
|
||||
} else {
|
||||
state.set(url, { ...s, fails });
|
||||
state.set(url, { ...s, fails, status, ms: null, geprueft: Date.now() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alte Messpunkte wegräumen, gleiche Aufbewahrung wie beim Game-Server
|
||||
try {
|
||||
pruneWatchdogHistory();
|
||||
} catch { /* nicht kritisch, beim nächsten Durchlauf wieder */ }
|
||||
}
|
||||
|
||||
export function startWatchdog(client) {
|
||||
|
||||
@@ -712,6 +712,35 @@ export const playerHistory = (serverId, hours = 24) => playerHistoryStmt.all(ser
|
||||
export const uptimeBuckets = (serverId, hours = 168) => uptimeBucketsStmt.all(serverId, `-${hours} hours`);
|
||||
export const prunePlayerHistory = () => prunePlayerHistoryStmt.run();
|
||||
|
||||
// Watchdog: Erreichbarkeit der überwachten Adressen — gleiche Bauart wie oben,
|
||||
// damit die Übersicht denselben Balken benutzen kann. Adresse als Schlüssel:
|
||||
// die Liste steht als Einstellung und hat keine IDs.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS watchdog_history (
|
||||
url TEXT NOT NULL,
|
||||
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
ok INTEGER NOT NULL,
|
||||
ms INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wd_url_ts ON watchdog_history (url, ts);
|
||||
`);
|
||||
const insertWatchdogSample = db.prepare('INSERT INTO watchdog_history (url, ok, ms) VALUES (?, ?, ?)');
|
||||
const watchdogBucketsStmt = db.prepare(`
|
||||
SELECT strftime('%Y-%m-%dT%H', ts) AS stunde,
|
||||
COUNT(*) AS proben,
|
||||
SUM(ok) AS an,
|
||||
CAST(AVG(ms) AS INTEGER) AS schnitt
|
||||
FROM watchdog_history
|
||||
WHERE url = ? AND ts >= datetime('now', ?)
|
||||
GROUP BY stunde ORDER BY stunde
|
||||
`);
|
||||
// Adressen, die aus der Einstellung entfernt wurden, sollen nicht ewig bleiben
|
||||
const pruneWatchdogStmt = db.prepare(`DELETE FROM watchdog_history WHERE ts < datetime('now', '-7 days')`);
|
||||
export const recordWatchdogSample = (url, ok, ms) =>
|
||||
insertWatchdogSample.run(url, ok ? 1 : 0, Number.isFinite(ms) ? Math.round(ms) : null);
|
||||
export const watchdogBuckets = (url, hours = 168) => watchdogBucketsStmt.all(url, `-${hours} hours`);
|
||||
export const pruneWatchdogHistory = () => pruneWatchdogStmt.run();
|
||||
|
||||
// Moderation: Verwarnungen + Sticky-Roles
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS warns (
|
||||
|
||||
+30
-3
@@ -15,16 +15,17 @@ import {
|
||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||
saveTemplate, listTemplates, deleteTemplate,
|
||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||
logAudit, listAudit, playerHistory, uptimeBuckets, deleteUserData,
|
||||
logAudit, listAudit, playerHistory, uptimeBuckets, watchdogBuckets, deleteUserData,
|
||||
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
|
||||
createService, updateService, listServices, deleteService,
|
||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||
} from '../db.js';
|
||||
import crypto from 'node:crypto';
|
||||
import { moduleStates, moduleDetail, setModuleEnabled, moduleFavorites, setModuleFavorites, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
|
||||
import { moduleStates, moduleDetail, setModuleEnabled, moduleEnabled, moduleFavorites, setModuleFavorites, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
|
||||
import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
|
||||
import { tuningStates, setTuning, TUNING_GROUPS } from '../tuning.js';
|
||||
import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
|
||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||
import { watchdogState, watchedUrls } from '../bot/watchdog.js';
|
||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||
import { sanitizeEmbed } from './api-v1.js';
|
||||
import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-menus.js';
|
||||
@@ -189,6 +190,32 @@ export function registerApiRoutes(app, client) {
|
||||
}),
|
||||
}));
|
||||
|
||||
// Watchdog-Übersicht: Stand und Erreichbarkeit der überwachten Adressen.
|
||||
// Nicht öffentlich — in der Liste können interne Dienste stehen, die
|
||||
// niemand von außen kennen muss.
|
||||
app.get('/api/watchdog', async (request, reply) => {
|
||||
if (requireAnyScope(request, reply)) return;
|
||||
const stand = watchdogState();
|
||||
return {
|
||||
aktiv: moduleEnabled('watchdog'),
|
||||
intervall: tuning('watchdog_interval'),
|
||||
adressen: watchedUrls().map((url) => {
|
||||
const s = stand.get(url) ?? {};
|
||||
return {
|
||||
url,
|
||||
// undefined = seit dem Start noch nicht geprüft
|
||||
online: s.geprueft ? !s.down : null,
|
||||
fails: s.fails ?? 0,
|
||||
seit: s.since ?? null,
|
||||
ms: s.ms ?? null,
|
||||
status: s.status ?? null,
|
||||
geprueft: s.geprueft ?? null,
|
||||
uptime: watchdogBuckets(url, 168),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// --- Module: zentrale An/Aus-Schalter aller Funktionen ---
|
||||
|
||||
app.get('/api/modules', async (request, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user