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