Files
d4rkbot/tools/panel-abgleich-pruefen.mjs
T
D4rkst3r c81e862992
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Wo der Bot nicht hinkommt, fragt das Panel -- Typ panel
Bisher entschied `x.gamedig` darueber, ob aus einem Panel-Server ein
Monitor-Eintrag wird: "kennt unsere Bibliothek dieses Spiel?" Das hat
einen Fall ausgesperrt, den es wirklich gibt -- TeamSpeak.

UND ZWAR NICHT, WEIL GAMEDIG ES NICHT KOENNTE. Am 16.08.2026
nachgeschlagen, nachdem die Annahme "gamedig kennt TeamSpeak nicht"
sich als falsch erwies: gamedig 5.3.3 hat `teamspeak3`, DiscordGSM
fuehrt es ebenfalls. Der Grund ist ein anderer und ein besserer:

    node_modules/gamedig/protocols/teamspeak3.js
      queryPort = options.teamspeakQueryPort || 10011
      'use port=<voice>' -> 'serverinfo' -> 'clientlist'

Es braucht den SERVERQUERY-Port. Beim Betreiber liegt der hier:

    9987/udp  -> 0.0.0.0:9987      Sprache, oeffentlich
    10011/tcp -> 127.0.0.1:9988    ServerQuery, NUR localhost
    30033/tcp -> 0.0.0.0:9989      Dateien, oeffentlich

Der Bot laeuft im Container und erreicht 127.0.0.1 des Hosts nie. Zwei
Gegenproben vom Host aus, beide gescheitert (die zweite auch deshalb,
weil gamedig ohne `teamspeakQueryPort` stur 10011 nimmt -- den es dort
gar nicht gibt). Eine Kennung einzutragen brauechte also einen OFFENEN
Verwaltungsport, und das ist genau das, was die Regel "kein Port geht
ohne Eintrag ins Netz" verhindert.

Das Panel dagegen laeuft auf dem Host, darf hin und hat die
Zugangsdaten -- und schickt seine eigene Messung seit heute im
Statusbericht mit. Also:

  - `taugt` fragt `zeigbar` statt `gamedig`: "kann UEBERHAUPT JEMAND
    diesen Server abfragen -- wir oder das Panel fuer uns?" Mit
    Rueckfall auf `gamedig` fuer ein aelteres Panel; ohne den
    verschwaenden nach diesem Update alle Eintraege, und zwar lautlos.
  - Neuer eigener Typ `panel` neben `fivem` und `http`. Er ersetzt
    nichts: wo gamedig kann, bleibt es bei gamedig -- dessen Antwort
    bringt Karte, Fassung und Ping mit, die das Panel nicht liefert.
  - `spieler === null` heisst KEINE ANTWORT, nicht null Spieler. Das
    Panel sichert dazu einen Grund zu; wir werfen damit und landen im
    selben Zweig wie ein gescheiterter gamedig-Query, statt eine 0 zu
    erfinden. Und kein Ping: die Zahl ist eine gespeicherte Messung des
    Panels, keine Antwortzeit von hier.

EINE FALLE BEIM ZWEITEN PARAMETER: `servers.map(queryServer)` haette
`panelItems` den INDEX untergeschoben -- der erste Server saehe null,
alle weiteren eine Zahl, an der `panelEintrag` still scheitert. Also
`map((s) => queryServer(s, panelItems))`.

Probe erweitert und im neu gebauten Abbild gelaufen, alles gruen --
einschliesslich "null Spieler sind eine Antwort, kein Ausfall".
2026-08-16 12:01:24 +02:00

192 lines
10 KiB
JavaScript

// 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;