feat: der Monitor fragt das Panel, wenn eine Abfrage nicht ausreicht
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s

gamedig FRAGT einen Server. Ein startendes Modpack antwortet nicht, und
nach zwei Fehlversuchen stand im Alarm-Kanal eine Ausfallmeldung, obwohl
niemand etwas kaputtgemacht hat -- am ATM10 des Betreibers gemessen jedes
Mal 306 Sekunden Fehlalarm.

Das d4rk_gameserver-Panel SIEHT den Container, statt ihn zu fragen. Neu
ist deshalb `src/panel.js`: Zustand, Fertig-Merkmal, RAM und CPU kommen
als Zusatzauskunft dazu, einmal je Durchlauf abgerufen und 20 Sekunden
zwischengespeichert.

Was das im Alarm-Kanal aendert:

  startet gerade   kein Alarm, aber nur innerhalb einer Gnadenfrist von
                   15 Minuten. Ohne diese Grenze fraesse die Anbindung
                   genau den Alarm, fuer den es sie gibt -- `bereit`
                   wird nie von allein wahr, ein haengender Start bliebe
                   sonst fuer immer stumm.
  im Panel gestoppt  kein Alarm (Code 0/143/137)
  abgestuerzt      ALARM, mit Code im Embed
  OOM-Kill         ALARM, und beim Namen genannt statt als Absturz
                   getarnt -- bei Modpacks die haeufigste Ursache
  Container weg    ALARM

Der Zaehler wird beim Unterdruecken NICHT zurueckgesetzt: ein bereits
gemeldeter Ausfall bleibt gemeldet, sonst verschluckt ein Stopp im Panel
die spaetere "wieder online"-Entwarnung und im Kanal bliebe ein Alarm
ohne Aufloesung stehen.

ES IST DURCHGEHEND OPTIONAL. Adresse und Zeichen stehen im Webinterface
unter "Server" und NICHT in der .env; ohne Eintrag -- und ebenso, wenn
das Panel nicht antwortet -- verhaelt sich der Monitor exakt wie vorher.
Das Zeichen darf nur lesen, damit auch ein verlorenes niemandem einen
Server stoppen kann.

`tools/panel-pruefen.mjs` prueft die Logik ohne Discord und ohne
Datenbank; deshalb laedt panel.js die Einstellungen erst beim Aufruf.
This commit is contained in:
2026-08-13 18:05:24 +02:00
parent bce75f8c65
commit d6124fc2cd
6 changed files with 326 additions and 1 deletions
+92
View File
@@ -0,0 +1,92 @@
// Die Panel-Anbindung -- ohne Discord, ohne Datenbank, nur die Logik.
//
// Geprüft wird vor allem, was passieren muss, wenn das Panel NICHT da ist:
// dann darf sich nichts anders verhalten als vorher. Eine Zusatzauskunft, die
// den Monitor mit herunterreißt, wäre schlimmer als gar keine.
//
// node tools/panel-pruefen.mjs
import { panelEintrag, zustandText, lastText, startetNoch, gewolltAus, STARTGNADE_MS } from '../src/panel.js'
let fehler = 0
const zeig = (gut, was, dazu = '') => {
if (!gut) fehler++
console.log(` ${gut ? 'ok ' : 'FEHL'} ${was}${dazu ? ' ' + dazu : ''}`)
}
const items = [
// So sieht ein Modpack in der Startphase aus: Container läuft, das
// Fertig-Merkmal stand noch nicht im Protokoll, GameDig bekommt nichts.
{ name: 'atm11-test', zustand: 'running', bereit: false, port: 26000,
seit: Date.now() - 120_000, cpu: 187, kerne: 20, ram: 5_153_960_755, ram_grenze: 7_516_192_768 },
{ name: 'nallheim', zustand: 'running', bereit: true, port: 26004,
seit: Date.now() - 3_600_000, cpu: 20, kerne: 20, ram: 1_073_741_824, ram_grenze: 5_368_709_120 },
{ name: 'alt', zustand: 'exited', bereit: false, port: 26010, code: 0, oom: false,
seit: null, cpu: null, kerne: null, ram: null, ram_grenze: null },
]
// Ein Start, der die Gnadenfrist gerissen hat -- der Fall, in dem die
// Anbindung NICHT mehr beschwichtigen darf.
const haengt = { ...items[0], seit: Date.now() - (STARTGNADE_MS + 60_000) }
console.log('=== Zuordnen')
zeig(panelEintrag(items, { port: 26000 })?.name === 'atm11-test', 'über den Port')
zeig(panelEintrag(items, { name: 'NALLHEIM' })?.name === 'nallheim', 'ersatzweise über den Namen, groß/klein egal')
zeig(panelEintrag(items, { port: 9999 }) === null, 'unbekannter Port -> nichts')
zeig(panelEintrag(null, { port: 26000 }) === null, 'ohne Panel-Daten -> nichts')
zeig(panelEintrag([], { port: 26000 }) === null, 'leere Liste -> nichts')
console.log('')
console.log('=== Was im Embed steht, wenn GameDig nichts erreicht')
const startet = zustandText(items[0])
zeig(/Startet gerade/.test(startet ?? ''), 'startender Server -> „Startet gerade"', startet)
zeig(/2 min/.test(startet ?? ''), 'mit Dauer', startet)
zeig(/Gestoppt/.test(zustandText(items[2]) ?? ''), 'gestoppter -> „Gestoppt"', zustandText(items[2]))
zeig(zustandText(items[1]) === null, 'ein bereiter Server trägt hier nichts bei — da war GameDig schon erfolgreich')
zeig(zustandText(null) === null, 'ohne Eintrag -> null, dann bleibt es beim schlichten „Offline"')
console.log('')
console.log('=== Wann der Alarm UNTERDRÜCKT wird -- und wann nicht')
zeig(startetNoch(items[0]) === true, 'startender Server in der Frist -> unterdrücken')
zeig(startetNoch(items[1]) === false, 'ein fertiger Server ist kein Startvorgang')
zeig(startetNoch(items[2]) === false, 'ein beendeter erst recht nicht')
zeig(startetNoch(null) === false, 'ohne Panel -> nichts unterdrücken (Monitor wie vorher)')
// Die wichtigste Zeile hier: `bereit` wird nie von allein wahr. Ohne diese
// Grenze fräße die Anbindung genau den Alarm, für den es sie gibt.
zeig(startetNoch(haengt) === false, 'nach der Gnadenfrist NICHT mehr unterdrücken -- sonst kommt der Alarm nie')
zeig(/nicht fertig/.test(zustandText(haengt) ?? ''), 'und im Embed steht dann Klartext', zustandText(haengt))
zeig(startetNoch({ ...items[0], seit: null }) === true, 'ohne Startzeit -> Nachsicht (kaputter Zeitstempel, kein hängender Server)')
const abgestuerzt = { ...items[2], code: 1 }
const oomTot = { ...items[2], code: 137, oom: true }
const altesPanel = { name: 'x', zustand: 'exited', bereit: false, port: 1 } // ohne code/oom
zeig(gewolltAus(items[2]) === true, 'Code 0 -> jemand hat gestoppt, kein Alarm')
zeig(gewolltAus({ ...items[2], code: 143 }) === true, 'Code 143 (SIGTERM) -> auch ein Stopp')
zeig(gewolltAus(abgestuerzt) === false, 'Code 1 -> ABSTURZ, der Alarm muss durch')
zeig(gewolltAus(oomTot) === false, 'OOM-Kill -> Alarm, auch bei Code 137')
zeig(gewolltAus(altesPanel) === false, 'älteres Panel ohne `code` -> im Zweifel echter Ausfall')
zeig(gewolltAus(items[0]) === false, 'ein laufender Server ist nicht "aus"')
zeig(/Abgestürzt.*\(Code 1\)/.test(zustandText(abgestuerzt) ?? ''), 'Absturz steht mit Code im Embed', zustandText(abgestuerzt))
zeig(/Speicher/.test(zustandText(oomTot) ?? ''), 'OOM wird benannt, nicht als Absturz getarnt', zustandText(oomTot))
console.log('')
console.log('=== Last')
const last = lastText(items[0])
zeig(/4\.8 GB \/ 7\.0 GB/.test(last ?? ''), 'RAM mit Grenze', last)
// 187 / 20 = 9.35, und toFixed(1) macht daraus 9.3 -- 9.35 liegt binär knapp
// UNTER 9.35, also wird abgerundet. Hier steht der gemessene Wert, nicht der
// erwartete; für eine Auslastungsanzeige ist das Zehntel ohnehin ohne Belang.
zeig(/9\.3 % CPU/.test(last ?? ''), 'CPU als Anteil der MASCHINE, nicht Dockers 187 %', last)
zeig(lastText(items[2]) === null, 'gestoppt -> keine Last')
zeig(lastText(null) === null, 'ohne Eintrag -> nichts')
console.log('')
console.log('=== Abschaltbarkeit')
// Ohne Panel-Eintrag muss JEDE dieser Funktionen null liefern, sonst steht
// im Embed plötzlich ein leeres Feld.
zeig([zustandText(undefined), lastText(undefined), panelEintrag(undefined, {})].every((x) => x == null),
'undefined überall -> überall null')
console.log('')
console.log(fehler ? `${fehler} FEHLER` : 'alles grün')
process.exitCode = fehler ? 1 : 0