Die Panel-Anbindung entfaellt -- das Panel postet jetzt selbst
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s

Entscheidung des Betreibers am 19.08.2026: das d4rk_gameserver-Panel
baut und stellt seine Status-Embeds ab sofort selbst in den Discord.
Damit hat diese Anbindung keinen Zweck mehr.

WARUM SIE UEBERHAUPT WEG SOLL, und nicht bloss ungenutzt liegenbleibt:
sie war die zweite Meinung ueber denselben Zustand. `zustandText()` gab
`null` zurueck, wenn ein Server LIEF UND BEREIT war -- also im
Normalfall -- und der Monitor fiel dann auf seine gamedig-Abfrage
zurueck. Erreichte die den Server nicht, stand im Kanal "Offline",
waehrend dasselbe Embed RAM und CPU aus dem Panel anzeigte. Ein
Widerspruch, den man von keiner der beiden Seiten aufloesen konnte.

ENTFERNT:

    src/panel.js                      die Anbindung
    src/panel-abgleich.js             das Anlegen/Wegraeumen von Eintraegen
    tools/panel-pruefen.mjs           die zugehoerigen Proben
    tools/panel-abgleich-pruefen.mjs
    panelUrl/panelZeichen             in runtime-settings.js
    panel_url/panel_zeichen           als Einstellung und im Formular
    Servertyp `panel`                 er fragte nicht selbst, sondern las ab

WAS DER MONITOR DAMIT VERLIERT, und es gehoert dazugesagt: die
Schonfrist beim Hochfahren. Ein startendes Modpack antwortet minutenlang
nicht -- am ATM10 gemessen 306 Sekunden -- und meldet dadurch wieder
einen Ausfall, wo vorher "startet gerade" stand. Die
Zwei-Fehlschlaege-Schwelle federt das teilweise ab, nicht ganz.

Fuer Server im Panel ist das kein Verlust: dort postet das Panel und
kennt den Unterschied. Fuer FiveM und LS25 hat es ihn nie gegeben.

Geprueft: alle 281 relativen Importe loesen auf, die geaenderten Dateien
sind syntaktisch in Ordnung, das Frontend baut. Der Bot selbst ist NICHT
neu gestartet worden -- das ist eine Entscheidung des Betreibers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 12:01:21 +02:00
co-authored by Claude Opus 5
parent 0d7628f14e
commit 19010e9fde
9 changed files with 37 additions and 775 deletions
-191
View File
@@ -1,191 +0,0 @@
// Der Abgleich mit dem Panel -- vor allem, was er NICHT anfassen darf.
//
// Diese Probe braucht better-sqlite3 und läuft deshalb im Container:
//
// docker exec d4rkbot node tools/panel-abgleich-pruefen.mjs
//
// Sie arbeitet auf einer EIGENEN Datenbank in /tmp, nicht auf der echten --
// eine Probe, die löschen kann, darf nicht dort löschen, wo es zählt.
// VOR dem Import setzen: db.js loest den Pfad beim Laden auf.
const PROBE = '/tmp/abgleich-probe.sqlite';
process.env.DB_PATH = PROBE;
import { unlinkSync } from 'node:fs';
for (const e of ['', '-wal', '-shm']) {
try { unlinkSync(PROBE + e); } catch { /* gab es noch nicht */ }
}
const db = await import('../src/db.js');
const { abgleichen, taugt } = await import('../src/panel-abgleich.js');
let fehler = 0;
const zeig = (gut, was, dazu = '') => {
if (!gut) fehler++;
console.log(` ${gut ? 'ok ' : 'FEHL'} ${was}${dazu ? ' ' + dazu : ''}`);
};
// Die echte Datenbank waere hier fatal -- diese Probe LOESCHT Server. Also
// nachsehen, worauf sie wirklich zeigt, statt es zu glauben.
{
const datei = db.db.prepare('PRAGMA database_list').all()[0]?.file ?? '';
if (!datei.includes('abgleich-probe')) {
console.log(`ABBRUCH: laeuft auf ${datei}, nicht auf der Probedatenbank.`);
process.exit(1);
}
console.log(`(Probedatenbank: ${datei})`);
console.log('');
}
const panel = (o) => ({
name: 'p1', discord: true, gamedig: 'minecraft', adresse: '1.2.3.4', port: 26000, ...o,
});
console.log('=== Was überhaupt taugt');
zeig(taugt(panel()) === true, 'vollständiger Eintrag');
zeig(taugt(panel({ discord: false })) === false, 'ohne Häkchen -> nein');
zeig(taugt(panel({ gamedig: null })) === false, 'nicht abfragbares Spiel -> nein (sonst dauerhaft offline)');
zeig(taugt(panel({ adresse: '' })) === false, 'ohne Adresse für Spieler -> nein');
zeig(taugt(panel({ port: null })) === false, 'ohne Port -> nein');
zeig(taugt(undefined) === false, 'gar nichts -> nein');
console.log('');
console.log('=== Was das Panel selbst abfragen kann (seit 16.08.2026)');
//
// DER FALL, UM DEN ES GEHT: TeamSpeak. gamedig KANN es -- das Protokoll
// `teamspeak3` gibt es, DiscordGSM fuehrt es ebenfalls. Es braucht dafuer
// aber den ServerQuery-Port, und der liegt beim Betreiber absichtlich auf
// 127.0.0.1 (gemessen: 10011/tcp -> 127.0.0.1:9988). Der Bot im Container
// kommt dort nie hin, das Panel auf dem Host schon -- und es hat die
// Zugangsdaten. Also sagt das Panel `zeigbar: true` OHNE gamedig-Kennung,
// und wir lesen seine Zahl ab, statt einen Verwaltungsport zu oeffnen.
zeig(taugt(panel({ gamedig: null, zeigbar: true })) === true, 'ohne gamedig, aber das Panel kann fragen -> ja');
zeig(taugt(panel({ gamedig: 'minecraft', zeigbar: false })) === false, 'Panel sagt nein -> nein, auch mit gamedig-Kennung');
// DER RUECKFALL FUER EIN AELTERES PANEL. Ohne ihn verschwaenden nach einem
// Bot-Update schlagartig alle Eintraege, und zwar lautlos.
zeig(taugt(panel({ gamedig: 'minecraft' })) === true, 'aelteres Panel ohne `zeigbar` -> weiterhin ja');
zeig(taugt(panel({ gamedig: null })) === false, 'aelteres Panel, kein gamedig -> weiterhin nein');
console.log('');
console.log('=== Und welcher Typ dabei entsteht');
{
const vorher = db.listGameservers().length;
const b = abgleichen([panel({ name: 'ts3', gamedig: null, zeigbar: true, port: 9987 })]);
zeig(b.angelegt.includes('ts3'), 'ein Server ohne gamedig-Kennung wird angelegt', b.angelegt.join());
const t = db.listGameservers().find((s) => s.name === 'ts3');
zeig(t?.type === 'panel', 'und zwar mit dem Typ `panel` -- da fragt der Bot nicht selbst', t?.type);
// Wo gamedig kann, bleibt es bei gamedig: dessen Antwort bringt Karte,
// Fassung und Ping mit, die das Panel nicht liefert.
abgleichen([panel({ name: 'ts3', gamedig: null, zeigbar: true, port: 9987 }), panel()]);
zeig(db.listGameservers().find((s) => s.name === 'p1')?.type === 'minecraft', 'mit Kennung bleibt es bei gamedig -- der neue Typ ersetzt nichts');
// Aufraeumen, damit die folgenden Abschnitte auf dem Stand von vorher
// rechnen. Eine Probe, die den Stand veraendert, macht die naechste Zeile
// unlesbar.
abgleichen([]);
zeig(db.listGameservers().length === vorher, 'und danach ist wieder aufgeraeumt');
}
console.log('');
console.log('=== Anlegen');
let a = abgleichen([panel()]);
zeig(a.angelegt.length === 1, 'ein Server mit Häkchen wird angelegt', a.angelegt.join());
const angelegt = db.listGameservers().find((s) => s.name === 'p1');
zeig(angelegt?.type === 'minecraft', 'mit der gamedig-Kennung, nicht der Vorlagen-Kennung', angelegt?.type);
zeig(angelegt?.host === '1.2.3.4' && Number(angelegt?.port) === 26000, 'Adresse und Port stehen drin');
zeig(angelegt?.address === '1.2.3.4:26000', 'und die Adresse zum Kopieren', angelegt?.address);
zeig(angelegt?.panel_name === 'p1', 'die Herkunft ist vermerkt -- daran hängt alles Weitere');
a = abgleichen([panel()]);
zeig(a.angelegt.length === 0, 'ein zweiter Durchlauf legt ihn NICHT noch einmal an');
console.log('');
console.log('=== Was der Abgleich NICHT anfassen darf');
// Von Hand angelegt: kein panel_name.
const handId = db.createGameserver({
name: 'ATM10', type: 'minecraft', host: '9.9.9.9', port: 25565,
address: '9.9.9.9:25565', image_url: 'https://bild', mods_url: 'https://mods',
links: [{ label: 'Regeln', url: 'https://regeln' }],
});
a = abgleichen([]); // Panel meldet gar nichts mehr
const hand = db.getGameserver(handId);
zeig(Boolean(hand), 'ein von Hand angelegter Server überlebt einen leeren Bericht');
zeig(hand?.image_url === 'https://bild', 'samt Bild');
zeig(hand?.links?.length === 1, 'samt Links');
zeig(a.entfernt.length === 1 && a.entfernt[0].name === 'p1', 'entfernt wird nur, was aus dem Panel kam', a.entfernt.map((e) => e.name).join());
zeig(!db.listGameservers().some((s) => s.name === 'p1'), 'und der ist wirklich weg');
console.log('');
console.log('=== Ein bestehender Eintrag wird UEBERNOMMEN, nicht verdoppelt');
// Der Fall aus dem Betrieb: der Server stand schon von Hand im Monitor, und
// dann wurde im Panel der Schalter umgelegt. Vorher passierte sichtbar nichts
// -- kein Embed, keine Meldung, keine Spur -- und der Betreiber hat den Fehler
// bei sich gesucht.
a = abgleichen([panel({ name: 'ATM10', adresse: '9.9.9.9', port: 25565 })]);
zeig(a.angelegt.length === 0, 'gleiche Adresse und gleicher Port -> kein zweites Embed im Kanal');
zeig(a.uebernommen.includes('ATM10'), 'sondern uebernommen -- der Schalter tut sichtbar etwas', a.uebernommen.join());
const nun = db.getGameserver(handId);
zeig(nun.panel_name === 'ATM10', 'die Herkunft steht jetzt auf dem Panel', nun.panel_name);
zeig(nun.image_url === 'https://bild' && nun.links?.length === 1, 'Bild und Links bleiben dabei unangetastet');
a = abgleichen([panel({ name: 'ATM10', adresse: '9.9.9.9', port: 25565 })]);
zeig(a.uebernommen.length === 0, 'ein zweiter Durchlauf uebernimmt ihn nicht noch einmal');
// Und ab jetzt raeumt das Panel ihn auch wieder weg -- das ist der Preis der
// Uebernahme, und er gehoert geprueft.
a = abgleichen([]);
zeig(a.entfernt.some((e) => e.name === 'ATM10'), 'ausgeschaltet -> jetzt raeumt das Panel ihn weg');
// Fuer die folgenden Abschnitte wieder von Hand anlegen.
db.createGameserver({ name: 'ATM10', type: 'minecraft', host: '9.9.9.9', port: 25565, address: '9.9.9.9:25565' });
console.log('');
console.log('=== Bearbeiten im Webinterface nimmt die Herkunft nicht');
abgleichen([panel()]);
const p1 = db.listGameservers().find((s) => s.name === 'p1');
db.updateGameserver({ ...p1, image_url: 'https://neu' });
const danach = db.getGameserver(p1.id);
zeig(danach.panel_name === 'p1', 'panel_name steht nach dem Speichern noch da', danach.panel_name);
zeig(danach.image_url === 'https://neu', 'und die Änderung ist drin');
// Und der nächste Abgleich darf sie nicht überschreiben.
abgleichen([panel()]);
zeig(db.getGameserver(p1.id).image_url === 'https://neu', 'der nächste Durchlauf plättet sie nicht');
console.log('');
console.log('=== Der Typ `panel`: was der Bot daraus macht');
{
const { queryServer } = await import('../src/bot/server-monitor.js');
const eintrag = { id: 99, name: 'ts3', type: 'panel', host: '1.2.3.4', port: 9987 };
const bericht = (o) => [{
name: 'ts3', port: 9987, spiel: 'TeamSpeak 3', zeigbar: true, gamedig: null,
spieler: 3, spieler_max: 32, spieler_grund: null, ...o,
}];
const r = await queryServer(eintrag, bericht());
zeig(r.online === true && r.players === 3 && r.max === 32, 'die Zahl des Panels wird uebernommen', `${r.players}/${r.max}`);
zeig(r.spielname === 'TeamSpeak 3', 'samt Spielname aus dem Bericht', r.spielname);
// KEIN ERFUNDENER PING. Die Zahl ist eine gespeicherte Messung des Panels
// und keine Antwortzeit von hier.
zeig(r.ping === null, 'aber KEIN Ping -- den haben wir nicht gemessen', String(r.ping));
// null HEISST KEINE ANTWORT, NICHT NULL SPIELER. Das ist der ganze Punkt:
// ein leerer Server und ein unerreichbarer duerfen nicht gleich aussehen.
const ohne = await queryServer(eintrag, bericht({ spieler: null, spieler_grund: 'Der Server läuft nicht.' }));
zeig(ohne.online === false, 'ohne Zahl: offline, statt eine 0 zu erfinden');
zeig(ohne.players === null, 'und die Spielerzahl bleibt null', String(ohne.players));
// NULL SPIELER SIND EINE ANTWORT. Der haeufigste Zustand ueberhaupt --
// wer ihn als "offline" liest, faerbt einen laufenden Server rot.
const leer = await queryServer(eintrag, bericht({ spieler: 0 }));
zeig(leer.online === true && leer.players === 0, 'null Spieler sind eine Antwort, kein Ausfall');
// Und die Faelle, in denen gar nichts da ist.
zeig((await queryServer(eintrag, null)).online === false, 'ohne Panel-Bericht: offline statt Absturz');
zeig((await queryServer(eintrag, [])).online === false, 'leerer Bericht: offline statt Absturz');
}
console.log('');
console.log('=== Kaputte Berichte');
zeig(abgleichen(null).angelegt.length === 0, 'null -> nichts passiert');
zeig(abgleichen([]).entfernt.length >= 0, 'leere Liste -> kein Absturz');
zeig(abgleichen([{}]).angelegt.length === 0, 'Eintrag ohne Felder -> nichts angelegt');
console.log('');
console.log(fehler ? `${fehler} FEHLER` : 'alles grün');
process.exitCode = fehler ? 1 : 0;
-92
View File
@@ -1,92 +0,0 @@
// 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