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:
@@ -0,0 +1,67 @@
|
|||||||
|
// Erreichbarkeits-Balken: ein Strich pro Stunde über 7 Tage.
|
||||||
|
//
|
||||||
|
// Wird von der öffentlichen Server-Seite und der Watchdog-Übersicht im Panel
|
||||||
|
// benutzt — beide bekommen dieselben Stundeneimer vom Server geliefert
|
||||||
|
// ({ stunde, proben, an }), deshalb liegt das hier und nicht in einer Seite.
|
||||||
|
import { useT, useFmt } from '../i18n.jsx';
|
||||||
|
|
||||||
|
const STUNDEN = 168;
|
||||||
|
|
||||||
|
export default function UptimeTrack({ buckets }) {
|
||||||
|
const { t } = useT();
|
||||||
|
const { tag, datum } = useFmt();
|
||||||
|
if (!buckets?.length) return null;
|
||||||
|
|
||||||
|
const proStunde = new Map(buckets.map((b) => [b.stunde, b]));
|
||||||
|
const jetzt = new Date();
|
||||||
|
jetzt.setMinutes(0, 0, 0);
|
||||||
|
|
||||||
|
const striche = [];
|
||||||
|
let gemessen = 0;
|
||||||
|
let erreichbar = 0;
|
||||||
|
for (let i = STUNDEN - 1; i >= 0; i--) {
|
||||||
|
const d = new Date(jetzt.getTime() - i * 3600000);
|
||||||
|
// Schlüssel wie strftime('%Y-%m-%dT%H') in SQLite — das rechnet in UTC,
|
||||||
|
// hier also bewusst auch. Angezeigt wird gleich die Ortszeit, sonst
|
||||||
|
// stünde im Tooltip eine Stunde, die niemand wiedererkennt.
|
||||||
|
const key = d.toISOString().slice(0, 13);
|
||||||
|
const b = proStunde.get(key);
|
||||||
|
let art = 'leer';
|
||||||
|
if (b) {
|
||||||
|
gemessen += b.proben;
|
||||||
|
erreichbar += b.an;
|
||||||
|
art = b.an === b.proben ? 'gut' : b.an === 0 ? 'aus' : 'teil';
|
||||||
|
}
|
||||||
|
striche.push({ key, zeit: d, art, b });
|
||||||
|
}
|
||||||
|
|
||||||
|
const quote = gemessen > 0 ? (erreichbar / gemessen) * 100 : null;
|
||||||
|
const titel = (s) => {
|
||||||
|
const wann = datum(s.zeit, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
|
if (!s.b) return `${wann} — ${t('server.noData')}`;
|
||||||
|
const anteil = Math.round((s.b.an / s.b.proben) * 100);
|
||||||
|
// Antwortzeit nur, wo sie erhoben wird (Watchdog) und etwas lief
|
||||||
|
return s.b.schnitt != null && s.b.an > 0
|
||||||
|
? `${wann} — ${anteil} % · ${s.b.schnitt} ms`
|
||||||
|
: `${wann} — ${anteil} %`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="up-track-wrap">
|
||||||
|
<div className="up-track">
|
||||||
|
{striche.map((s) => (
|
||||||
|
<span className={`up-tick ${s.art}`} key={s.key} title={titel(s)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="up-track-foot">
|
||||||
|
<span>{t('server.days7')}</span>
|
||||||
|
{quote != null && (
|
||||||
|
<span className={quote >= 99 ? 'up-good' : quote >= 90 ? '' : 'up-bad'}>
|
||||||
|
{quote.toLocaleString(tag, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{t('server.now')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { apiGet } from '../api.js';
|
|||||||
import { SkeletonCards } from '../components/Skeleton.jsx';
|
import { SkeletonCards } from '../components/Skeleton.jsx';
|
||||||
import { IconGame, IconMapPin, IconClock, IconCheck } from '../icons.jsx';
|
import { IconGame, IconMapPin, IconClock, IconCheck } from '../icons.jsx';
|
||||||
import { useT, useFmt } from '../i18n.jsx';
|
import { useT, useFmt } from '../i18n.jsx';
|
||||||
|
import UptimeTrack from '../components/UptimeTrack.jsx';
|
||||||
|
|
||||||
const UHRZEIT = { hour: '2-digit', minute: '2-digit' };
|
const UHRZEIT = { hour: '2-digit', minute: '2-digit' };
|
||||||
|
|
||||||
@@ -38,69 +39,6 @@ function HistoryChart({ history, max }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Erreichbarkeits-Balken: ein Strich pro Stunde der letzten 7 Tage.
|
|
||||||
*
|
|
||||||
* Der Server liefert nur Stunden, in denen wirklich gemessen wurde. Fehlende
|
|
||||||
* werden hier als Lücke gezeichnet statt als „online" — sonst sähe eine
|
|
||||||
* Bot-Auszeit aus wie ein einwandfrei laufender Server.
|
|
||||||
*/
|
|
||||||
function UptimeTrack({ buckets }) {
|
|
||||||
const { t } = useT();
|
|
||||||
const { tag, datum } = useFmt();
|
|
||||||
if (!buckets?.length) return null;
|
|
||||||
|
|
||||||
const proStunde = new Map(buckets.map((b) => [b.stunde, b]));
|
|
||||||
const jetzt = new Date();
|
|
||||||
jetzt.setMinutes(0, 0, 0);
|
|
||||||
|
|
||||||
const striche = [];
|
|
||||||
let gemessen = 0;
|
|
||||||
let erreichbar = 0;
|
|
||||||
for (let i = 167; i >= 0; i--) {
|
|
||||||
const d = new Date(jetzt.getTime() - i * 3600000);
|
|
||||||
// Schlüssel wie strftime('%Y-%m-%dT%H') in SQLite — das rechnet in UTC,
|
|
||||||
// hier also bewusst auch. Angezeigt wird gleich die Ortszeit, sonst
|
|
||||||
// stünde im Tooltip eine Stunde, die niemand wiedererkennt.
|
|
||||||
const key = d.toISOString().slice(0, 13);
|
|
||||||
const b = proStunde.get(key);
|
|
||||||
let art = 'leer';
|
|
||||||
if (b) {
|
|
||||||
gemessen += b.proben;
|
|
||||||
erreichbar += b.an;
|
|
||||||
art = b.an === b.proben ? 'gut' : b.an === 0 ? 'aus' : 'teil';
|
|
||||||
}
|
|
||||||
striche.push({ key, zeit: d, art, b });
|
|
||||||
}
|
|
||||||
|
|
||||||
const quote = gemessen > 0 ? (erreichbar / gemessen) * 100 : null;
|
|
||||||
const titel = (s) => {
|
|
||||||
const wann = datum(s.zeit, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
||||||
return s.b
|
|
||||||
? `${wann} — ${Math.round((s.b.an / s.b.proben) * 100)} %`
|
|
||||||
: `${wann} — ${t('server.noData')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="up-track-wrap">
|
|
||||||
<div className="up-track">
|
|
||||||
{striche.map((s) => (
|
|
||||||
<span className={`up-tick ${s.art}`} key={s.key} title={titel(s)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="up-track-foot">
|
|
||||||
<span>{t('server.days7')}</span>
|
|
||||||
{quote != null && (
|
|
||||||
<span className={quote >= 99 ? 'up-good' : quote >= 90 ? '' : 'up-bad'}>
|
|
||||||
{quote.toLocaleString(tag, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span>{t('server.now')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Spieler-Peak aus den 24h-Samples (Uptime kommt aus dem Balken oben) */
|
/** Spieler-Peak aus den 24h-Samples (Uptime kommt aus dem Balken oben) */
|
||||||
function historyStats(history) {
|
function historyStats(history) {
|
||||||
if (history.length === 0) return null;
|
if (history.length === 0) return null;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '../icons.jsx';
|
} from '../icons.jsx';
|
||||||
import { moduleIcon } from '../module-icons.jsx';
|
import { moduleIcon } from '../module-icons.jsx';
|
||||||
import ModulePage from '../components/ModulePage.jsx';
|
import ModulePage from '../components/ModulePage.jsx';
|
||||||
|
import UptimeTrack from '../components/UptimeTrack.jsx';
|
||||||
import { findSettings, SETTING_INDEX } from '../setting-index.js';
|
import { findSettings, SETTING_INDEX } from '../setting-index.js';
|
||||||
import { Markdown } from '../markdown.jsx';
|
import { Markdown } from '../markdown.jsx';
|
||||||
|
|
||||||
@@ -341,6 +342,7 @@ export default function Settings({ me }) {
|
|||||||
const [team, setTeam] = useState([]);
|
const [team, setTeam] = useState([]);
|
||||||
const [audit, setAudit] = useState([]);
|
const [audit, setAudit] = useState([]);
|
||||||
const [serverStats, setServerStats] = useState(null);
|
const [serverStats, setServerStats] = useState(null);
|
||||||
|
const [watchdog, setWatchdog] = useState(null);
|
||||||
const [owner, setOwner] = useState(null);
|
const [owner, setOwner] = useState(null);
|
||||||
const [auditAlle, setAuditAlle] = useState(false);
|
const [auditAlle, setAuditAlle] = useState(false);
|
||||||
// Module (An/Aus für alle Funktionen)
|
// Module (An/Aus für alle Funktionen)
|
||||||
@@ -467,6 +469,7 @@ export default function Settings({ me }) {
|
|||||||
apiGet('/api/auditlog').then((d) => setAudit(d.entries)).catch(() => {});
|
apiGet('/api/auditlog').then((d) => setAudit(d.entries)).catch(() => {});
|
||||||
// Fuers Dashboard: Mitgliederzahl und Nachrichten pro Tag
|
// Fuers Dashboard: Mitgliederzahl und Nachrichten pro Tag
|
||||||
apiGet('/api/serverstats').then(setServerStats).catch(() => {});
|
apiGet('/api/serverstats').then(setServerStats).catch(() => {});
|
||||||
|
apiGet('/api/watchdog').then(setWatchdog).catch(() => {});
|
||||||
apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {});
|
apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {});
|
||||||
apiGet('/api/services').then((d) => setServices(d.services)).catch(() => {});
|
apiGet('/api/services').then((d) => setServices(d.services)).catch(() => {});
|
||||||
apiGet('/api/pages/all/list').then((d) => setPages(d.pages)).catch(() => {});
|
apiGet('/api/pages/all/list').then((d) => setPages(d.pages)).catch(() => {});
|
||||||
@@ -2337,6 +2340,31 @@ export default function Settings({ me }) {
|
|||||||
<input type="text" placeholder="https://git.d4rkst3r.de, https://bot.d4rkst3r.de/health" value={form.watchdog_urls} onChange={(e) => setForm({ ...form, watchdog_urls: e.target.value })} />
|
<input type="text" placeholder="https://git.d4rkst3r.de, https://bot.d4rkst3r.de/health" value={form.watchdog_urls} onChange={(e) => setForm({ ...form, watchdog_urls: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{watchdog?.adressen?.length > 0 && (
|
||||||
|
<div className="settings-section">
|
||||||
|
<h2 className="settings-title">// Erreichbarkeit</h2>
|
||||||
|
<p className="section-intro">
|
||||||
|
Ein Strich je Stunde über die letzten 7 Tage — grün erreichbar, rot
|
||||||
|
Ausfall, grau keine Messung. Geprüft wird alle {watchdog.intervall} Minuten.
|
||||||
|
{!watchdog.aktiv && ' Der Wächter ist gerade ausgeschaltet, es kommen keine neuen Messpunkte dazu.'}
|
||||||
|
</p>
|
||||||
|
{watchdog.adressen.map((a) => (
|
||||||
|
<div className="wd-card card" key={a.url}>
|
||||||
|
<div className="wd-head">
|
||||||
|
<span className={`wd-dot ${a.online === null ? 'wartet' : a.online ? 'an' : 'aus'}`} />
|
||||||
|
<a className="wd-url" href={a.url} target="_blank" rel="noreferrer">{a.url}</a>
|
||||||
|
<span className="wd-meta">
|
||||||
|
{a.online === null && 'noch nicht geprüft'}
|
||||||
|
{a.online === true && `${a.status ?? 'OK'}${a.ms != null ? ` · ${a.ms} ms` : ''}`}
|
||||||
|
{a.online === false && `seit ${vorWieLange(new Date(a.seit).toISOString())} weg`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<UptimeTrack buckets={a.uptime} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1815,6 +1815,24 @@ label.toggle {
|
|||||||
.dash-group-name { flex-basis: 7rem; }
|
.dash-group-name { flex-basis: 7rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── WATCHDOG-ÜBERSICHT ────────────────────────────── */
|
||||||
|
.wd-card { padding: .9rem 1.1rem; margin-bottom: .7rem; min-width: 0; }
|
||||||
|
.wd-head { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
|
||||||
|
.wd-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
|
||||||
|
.wd-dot.an { background: var(--success, #3ba55d); }
|
||||||
|
.wd-dot.aus { background: #f23f43; }
|
||||||
|
.wd-dot.wartet { background: var(--muted2); }
|
||||||
|
.wd-url {
|
||||||
|
font-family: var(--mono); font-size: .74rem;
|
||||||
|
color: var(--text); text-decoration: none;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
|
||||||
|
}
|
||||||
|
.wd-url:hover { color: var(--neon); }
|
||||||
|
.wd-meta {
|
||||||
|
margin-left: auto; font-family: var(--mono);
|
||||||
|
font-size: .62rem; letter-spacing: .06em; color: var(--muted2); white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── ERREICHBARKEITS-BALKEN ────────────────────────── */
|
/* ── ERREICHBARKEITS-BALKEN ────────────────────────── */
|
||||||
/* Ein Strich pro Stunde, 168 Stück. Auf schmalen Schirmen werden die Striche
|
/* Ein Strich pro Stunde, 168 Stück. Auf schmalen Schirmen werden die Striche
|
||||||
dünner als ein Pixel — deshalb scrollt der Balken dort lieber, statt zu
|
dünner als ein Pixel — deshalb scrollt der Balken dort lieber, statt zu
|
||||||
|
|||||||
+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.
|
// 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 { config } from '../config.js';
|
||||||
import { moduleEnabled } from '../modules.js';
|
import { moduleEnabled } from '../modules.js';
|
||||||
|
import { brandEmbed } from '../embeds.js';
|
||||||
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
||||||
|
|
||||||
// URL → { fails, down, since }
|
// URL → { fails, down, since }
|
||||||
const state = new Map();
|
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') ?? '')
|
return (getSetting('watchdog_urls') ?? '')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter((s) => /^https?:\/\//.test(s));
|
.filter((s) => /^https?:\/\//.test(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dmAdmin(client, content) {
|
async function dmAdmin(client, embed) {
|
||||||
const user = await client.users.fetch(config.adminDiscordId);
|
if (!config.adminDiscordId) return;
|
||||||
await user.send(content);
|
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 */
|
/** 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 ok = false;
|
||||||
let detail = '';
|
let detail = '';
|
||||||
|
let status = null;
|
||||||
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -36,29 +46,56 @@ export async function watchdogTick(client, { failsBeforeAlert = tuning('watchdog
|
|||||||
headers: { 'User-Agent': 'd4rkbot-watchdog/1.0' },
|
headers: { 'User-Agent': 'd4rkbot-watchdog/1.0' },
|
||||||
});
|
});
|
||||||
ok = res.status < 500;
|
ok = res.status < 500;
|
||||||
|
status = res.status;
|
||||||
detail = `HTTP ${res.status}`;
|
detail = `HTTP ${res.status}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
detail = error.cause?.code ?? error.name ?? 'Fehler';
|
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 (ok) {
|
||||||
if (s.down) {
|
if (s.down) {
|
||||||
const minutes = Math.max(1, Math.round((Date.now() - s.since) / 60000));
|
const minuten = Math.max(1, Math.round((Date.now() - s.since) / 60000));
|
||||||
await dmAdmin(client, `✅ **${url}** ist wieder erreichbar (Downtime: ~${minutes} min).`).catch(() => {});
|
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`);
|
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 {
|
} else {
|
||||||
const fails = s.fails + 1;
|
const fails = s.fails + 1;
|
||||||
if (!s.down && fails >= failsBeforeAlert) {
|
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})`);
|
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 {
|
} 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) {
|
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 uptimeBuckets = (serverId, hours = 168) => uptimeBucketsStmt.all(serverId, `-${hours} hours`);
|
||||||
export const prunePlayerHistory = () => prunePlayerHistoryStmt.run();
|
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
|
// Moderation: Verwarnungen + Sticky-Roles
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS warns (
|
CREATE TABLE IF NOT EXISTS warns (
|
||||||
|
|||||||
+30
-3
@@ -15,16 +15,17 @@ import {
|
|||||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||||
saveTemplate, listTemplates, deleteTemplate,
|
saveTemplate, listTemplates, deleteTemplate,
|
||||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||||
logAudit, listAudit, playerHistory, uptimeBuckets, deleteUserData,
|
logAudit, listAudit, playerHistory, uptimeBuckets, watchdogBuckets, deleteUserData,
|
||||||
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
|
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
|
||||||
createService, updateService, listServices, deleteService,
|
createService, updateService, listServices, deleteService,
|
||||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||||
} from '../db.js';
|
} from '../db.js';
|
||||||
import crypto from 'node:crypto';
|
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 { 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 { 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 { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||||
import { sanitizeEmbed } from './api-v1.js';
|
import { sanitizeEmbed } from './api-v1.js';
|
||||||
import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-menus.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 ---
|
// --- Module: zentrale An/Aus-Schalter aller Funktionen ---
|
||||||
|
|
||||||
app.get('/api/modules', async (request, reply) => {
|
app.get('/api/modules', async (request, reply) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user