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
+1 -1
View File
@@ -102,7 +102,7 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen.
| Funktion | Was sie macht |
|---|---|
| 🎮 **Game-Server-Monitor** | DiscordGSM-Stil: pro Server ein Live-Embed (🟢/🔴, Name im Spiel, 🔒 bei Passwort oder Whitelist, Spieler-Balken, Map, Version, Mod-Anzahl, Spieler-Liste, Knöpfe für Verbinden, Mod-Download und frei eintragbare Links wie Regeln oder TeamSpeak, Ping); **alle Spiele, die gamedig kennt** — die Auswahl im Panel kommt aus der Bibliothek selbst, inklusive Standard-Port und Hinweis auf nötige Zugangs-Codes (Farming Simulator, Terraria) — plus FiveM + HTTP-Check; Down/Up-Alerts mit Ausfalldauer |
| 🖥️ **Panel-Anbindung** *(optional)* | Trägt der Betreiber unter **Config → Game-Server** die Adresse des [d4rk_gameserver](https://git.d4rkst3r.de/D4rkst3r/d4rk_media)-Panels und ein Lese-Zeichen ein, weiß der Monitor mehr als eine Abfrage hergibt. gamedig **fragt** einen Server — ein startendes Modpack antwortet nicht und sieht dadurch aus wie ein Ausfall (am ATM10 gemessen: 306 Sekunden bis zum ersten Spieler). Das Panel **sieht** den Container: 🟡 „Startet gerade" statt 🔴 „Offline", kein Fehlalarm während des Hochfahrens, kein Alarm für einen im Panel gestoppten Server — aber sehr wohl einer für Absturz, OOM-Kill oder fehlenden Container. Dazu RAM und CPU im Embed. Ohne Eintrag ändert sich nichts, und antwortet das Panel nicht, ebenfalls nicht. **Umgekehrt gilt außerdem:** im Panel hat jeder Server einen Schalter *„Im Discord anzeigen"* — wird er umgelegt, legt der Bot den Monitor-Eintrag binnen einer halben Minute selbst an und räumt ihn beim Ausschalten wieder weg. Das Panel schreibt dafür nichts hierher; das Häkchen reist im Statusbericht mit, den der Bot ohnehin abholt. Bild, Links und Zugangs-Code trägt man weiterhin hier ein — der Abgleich legt nur an und löscht, er überschreibt nie |
| 🖥️ **Panel-Anbindung** | **Entfallen am 19.08.2026.** Der Monitor holte beim [d4rk_gameserver](https://git.d4rkst3r.de/D4rkst3r/d4rk_gameserver)-Panel Zustand und Auslastung ab, um „startet gerade" von „offline" zu unterscheiden. Das Panel **baut und postet seine Status-Embeds inzwischen selbst** — es kennt den Unterschied ohnehin, und zwei Programme mit zwei Meinungen über denselben Zustand waren die Ursache dafür, dass im Discord „Offline" stand, während der Container seit sieben Stunden lief. Für Server, die **nicht** im Panel stehen (FiveM, LS25), arbeitet der Monitor unverändert über gamedig. |
| 🚜 **LS — Höfe & Preise** | Für Farming-Simulator-Server: Besitzkarte auf dem Luftbild des Servers (Parzellen nach Hof eingefärbt, Fläche = Hektar, Fahrzeuge und Spieler live), die Preiskurven übers Jahr mit „jetzt verkaufen oder warten", dazu **je Hof ein Embed**, das sich selbst aktualisiert: Land, Fuhrpark mit Wert und Betriebsstunden, was in die Werkstatt muss, was gewaschen gehört und was geladen ist. Dazu die **Modliste**: der Bot meldet, was neu, aktualisiert oder entfernt wurde, und auf `/mods/<server>` steht jeder Mod einzeln zum Laden — durch den Bot hindurch, damit der Zugangs-Code des Spielservers nicht in einem Link landet. **Nicht möglich:** welche Frucht auf welchem Feld steht, und Tierställe — beides gibt der Feed nicht her (siehe [docs/ls-feed.md](docs/ls-feed.md)) |
### Server & Technik
+10 -49
View File
@@ -3458,55 +3458,16 @@ export default function Settings({ me }) {
<button className="btn btn-save" onClick={save}>Kanäle speichern</button>
</div>
{/* Die Panel-Anbindung steht HIER und nicht nur auf der
Modul-Seite. Wer sie einrichten will, sucht sie bei den
Game-Servern -- dort, wo auch die Kanäle stehen. */}
<div className="field" data-setting="panel_url">
<label>
Panel-Anbindung
<span className="field-hint">
optional unterscheidet startet gerade" von „offline"
</span>
</label>
<p className="section-intro" style={{ marginTop: 0 }}>
Der Monitor <b>fragt</b> einen Server ab. Ein startendes Modpack antwortet
nicht und sieht dadurch aus wie ein Ausfall am ATM10 sind das 306 Sekunden
Fehlalarm bei jedem Neustart. Trägst du hier dein{' '}
<b>d4rk_gameserver</b>-Panel ein, <b>sieht</b> der Bot den Container: 🟡
Startet gerade" statt 🔴 „Offline", kein Alarm während des Hochfahrens und
keiner für einen im Panel gestoppten Server aber sehr wohl einer für
Absturz, vollen Speicher oder fehlenden Container. Dazu RAM und CPU im
Embed. Leer lassen heißt: alles bleibt wie bisher.
</p>
<div className="field-row">
<div style={{ flex: 1 }}>
<label>Panel-Adresse</label>
<input
type="text"
placeholder="https://panel.d4rkst3r.de"
value={form.panel_url ?? ''}
onChange={(e) => setForm({ ...form, panel_url: e.target.value })}
/>
</div>
<div style={{ flex: 1 }} data-setting="panel_zeichen">
<label>
Panel-Zeichen{' '}
<span className="field-hint">
im Panel unter Zugang für Programme" anlegen — darf nur lesen
</span>
</label>
<input
type="text"
placeholder="d4rkp_"
value={form.panel_zeichen ?? ''}
onChange={(e) => setForm({ ...form, panel_zeichen: e.target.value })}
/>
</div>
</div>
</div>
<div className="settings-actions" style={{ marginBottom: '1.4rem' }}>
<button className="btn btn-save" onClick={save}>Panel-Anbindung speichern</button>
</div>
{/* HIER STAND DIE PANEL-ANBINDUNG. Entfernt am 19.08.2026:
das d4rk_gameserver-Panel baut und postet seine
Status-Embeds seither selbst -- Adresse und Zeichen braucht
der Bot dafuer nicht mehr.
Was der Monitor damit verliert: er unterschied "startet
gerade" von "offline" und schwieg bei einem im Panel
gestoppten Server. Fuer Server, die im Panel stehen, macht
das jetzt das Panel; fuer die anderen (FiveM, LS25) hat es
diesen Unterschied nie gegeben. */}
{servers.length > 0 && (
<div className="key-list" style={{ marginBottom: '1.2rem' }}>
+26 -114
View File
@@ -8,8 +8,6 @@ import { brandFooter, botStatusText } from '../runtime-settings.js';
import { brandEmbed } from '../embeds.js';
import { moduleEnabled } from '../modules.js';
import { tuning, tuningMs, everyTuned } from '../tuning.js';
import { panelStatus, panelEintrag, zustandText, lastText, startetNoch, gewolltAus } from '../panel.js';
import { abgleichen, embedWegraeumen } from '../panel-abgleich.js';
const GREEN = 0x23a55a;
const RED = 0xf23f43;
@@ -18,7 +16,7 @@ const RED = 0xf23f43;
// standen mal ausgedachte wie "arkse" oder "terraria", die es dort gar nicht
// gibt. Beim Ändern gegen `games` aus gamedig prüfen, nicht gegen das Gefühl.
export const GAME_ICONS = {
fivem: '🚗', http: '🌐', minecraft: '⛏️', mbe: '⛏️', panel: '🛰️',
fivem: '🚗', http: '🌐', minecraft: '⛏️', mbe: '⛏️',
teamfortress2: '🎩', counterstrike2: '🔫', csgo: '🔫', garrysmod: '🔧',
rust: '🪓', valheim: '🛡️', palworld: '🐑', ase: '🦖', asa: '🦖', pixark: '🦖',
dayz: '🧟', projectzomboid: '🧟', sdtd: '🧟',
@@ -27,13 +25,14 @@ export const GAME_ICONS = {
};
// fivem und http sind unsere eigenen Typen — gamedig kennt sie nicht.
// `panel` ist der dritte, und er ist anders als die beiden: da fragt der Bot
// gar nicht, sondern das Panel hat schon gefragt und schickt die Zahl mit.
// Siehe `queryServer`.
//
// `panel` GAB ES HIER ALS DRITTEN, und er ist am 19.08.2026 entfallen: das
// d4rk_gameserver-Panel baut und postet seine Status-Embeds seither selbst.
// Ein Typ, der auf eine Anbindung zeigt, die es nicht mehr gibt, wäre ein
// Eintrag, den man anlegen kann und der dann dauerhaft „offline" meldet.
const EIGENE = {
fivem: { name: 'FiveM', port: 30120 },
http: { name: 'HTTP-Check', port: null },
panel: { name: 'Über das Panel', port: null },
};
// Protokolle, die ohne Zugangs-Code gar nicht antworten. Der Code steht im
@@ -78,14 +77,9 @@ const alertState = new Map(); // id → { fails, down, since }
export const lastResults = new Map(); // id → Query-Ergebnis + checkedAt
/**
* Einen Server abfragen — fivem/http direkt, `panel` gar nicht, alles andere
* über gamedig.
*
* `panelItems` ist der Statusbericht des Panels aus DIESEM Durchlauf. Er ist
* optional: fehlt er, verhält sich alles wie vorher, und ein `panel`-Eintrag
* gilt als offline — mit Grund.
* Einen Server abfragen — fivem/http direkt, alles andere über gamedig.
*/
export async function queryServer(server, panelItems = null) {
export async function queryServer(server) {
const base = {
...server, online: false, players: null, max: null, map: null,
playerNames: [], ping: null, version: null, mods: null,
@@ -95,34 +89,6 @@ export async function queryServer(server, panelItems = null) {
};
const started = Date.now();
try {
// ================== DAS PANEL HAT SCHON GEFRAGT ====================
//
// Kein Netzverkehr von hier. Für TeamSpeak wäre er sogar unmöglich:
// gamedig braucht dafür den ServerQuery-Port, und der liegt beim
// Betreiber absichtlich auf `127.0.0.1` — der Bot im Container kommt
// dort nie hin. Das Panel läuft auf dem Host, darf hin und hat die
// Zugangsdaten. Also fragt es, und wir lesen ab.
//
// `spieler === null` HEISST „KEINE ANTWORT", NICHT NULL SPIELER. Das
// Panel garantiert dazu einen `spieler_grund` — entweder eine Zahl
// oder ein Grund, nie beides leer. Genau diese Zusage macht das hier
// sauber: wir werfen mit dem Grund und landen im selben Zweig wie ein
// gescheiterter gamedig-Query, statt eine 0 zu erfinden.
if (server.type === 'panel') {
const p = panelEintrag(panelItems, server);
if (!p) throw new Error('Das Panel kennt diesen Server nicht (mehr).');
if (p.spieler == null) throw new Error(p.spieler_grund || 'Das Panel nennt keine Zahl.');
return {
...base, online: true,
players: p.spieler,
max: p.spieler_max ?? null,
spielname: p.spiel ?? null,
// KEIN PING. Die Zahl ist eine gespeicherte Messung des Panels
// (bis zu zehn Minuten alt) und keine Antwortzeit von hier.
// Eine Millisekundenzahl hinzuschreiben wäre erfunden.
ping: null,
};
}
if (server.type === 'fivem') {
const res = await fetch(`${server.query_url.replace(/\/$/, '')}/dynamic.json`, {
signal: AbortSignal.timeout(tuningMs.seconds('monitor_timeout')),
@@ -298,14 +264,8 @@ export function buildServerEmbed(r) {
// nicht verlässlich, und dann stünde das Steuerzeichen wörtlich da.
kopf.push(`*${r.spielname.trim().slice(0, 200)}*`);
}
// Wenn GameDig nichts erreicht hat, das Panel aber sagt, dass der
// Container laeuft und noch startet: DAS ist die richtige Auskunft. Ein
// Modpack braucht Minuten bis zum ersten Spieler -- am ATM10 des
// Betreibers 306 Sekunden -- und ein rotes "Offline" waere in dieser Zeit
// schlicht falsch.
const ausPanel = r.online ? null : zustandText(r.panel);
kopf.push([
r.online ? '🟢 **Online**' : (ausPanel ?? '🔴 **Offline**'),
r.online ? '🟢 **Online**' : '🔴 **Offline**',
zugang.grund ? `🔒 ${zugang.grund}` : null,
r.online && r.ping != null ? `${r.ping} ms` : null,
].filter(Boolean).join(' · '));
@@ -323,12 +283,6 @@ export function buildServerEmbed(r) {
r.mods != null ? { name: 'Mods', value: String(r.mods), inline: true } : null,
]));
// Was nur das Panel weiss: was der Server gerade verbraucht.
{
const last = lastText(r.panel);
if (last) embed.addFields({ name: 'Auf dem Wirt', value: last });
}
if (r.online && r.playerNames.length > 0) {
embed.addFields({
name: `Wer drauf ist (${r.playerNames.length})`,
@@ -371,30 +325,19 @@ async function handleAlerts(client, r) {
alertState.set(r.id, { fails: 0, down: false, since: null });
return;
}
// KEIN ALARM, WENN DAS PANEL DEN ZUSTAND ERKLÄREN KANN.
// HIER STAND EINE SCHONFRIST AUS DEM PANEL: solange es meldete, der
// Container starte noch oder sei absichtlich gestoppt, kam kein Alarm.
// Mit der Panel-Anbindung ist sie entfallen (19.08.2026).
//
// Das ist der eigentliche Gewinn der Anbindung. GameDig *fragt* einen
// Server; ein startender antwortet nicht, und nach zwei Fehlversuchen
// stünde hier eine Ausfallmeldung. Bei einem Modpack sind das jedes Mal
// Minuten Fehlalarm — am ATM10 des Betreibers 306 Sekunden bis zum ersten
// Spieler. Das Panel *sieht* den Container und weiß, dass er läuft und
// nur das Fertig-Merkmal noch nicht durch ist.
// WAS DAS KOSTET, und es gehört dazugesagt: ein Modpack, das startet,
// antwortet minutenlang nicht — am ATM10 gemessen 306 Sekunden. Für
// solche Server meldet der Monitor jetzt wieder einen Ausfall, wo vorher
// „startet gerade" stand. Die Zwei-Fehlschläge-Schwelle unten federt das
// teilweise ab, aber nicht ganz.
//
// Zwei Fälle, eine Behandlung: nichts tun. Der Zähler bleibt dabei
// ausdrücklich STEHEN statt zurückgesetzt zu werden, aus zwei Gründen:
//
// - Läuft die Gnadenfrist ab, ohne dass der Server fertig wird, greift
// `startetNoch` nicht mehr und der Alarm kommt sofort statt erst nach
// zwei weiteren Durchläufen.
// - Ein bereits gemeldeter Ausfall bleibt gemeldet. Sonst verschluckt
// ein Stopp im Panel die spätere „✅ wieder online"-Entwarnung, und im
// Alarm-Kanal bliebe ein 🚨 ohne Auflösung stehen.
//
// Und was NICHT unterdrückt wird: ein Absturz, ein OOM-Kill und ein
// fehlender Container. Das sind die Fälle, für die es den Kanal gibt —
// hier hilft das Panel dem Alarm, statt ihn zu ersetzen.
if (startetNoch(r.panel) || gewolltAus(r.panel)) return;
// Für Server, die im d4rk_gameserver-Panel stehen, ist das kein Verlust:
// das Panel postet deren Status selbst und kennt den Unterschied. Für
// Server, die nur hier stehen (FiveM, LS25), hat es ihn nie gegeben.
const fails = s.fails + 1;
if (!s.down && fails >= tuning('monitor_fails')) {
@@ -417,46 +360,15 @@ async function handleAlerts(client, r) {
export async function monitorTick(client) {
if (!moduleEnabled('server_monitor')) return;
// DER PANEL-BERICHT WIRD VOR ALLEM ANDEREN GEHOLT, und der Abgleich läuft
// VOR der Prüfung auf einen leeren Monitor.
//
// Vorher stand hier zuerst `if (servers.length === 0) return`. Damit hätte
// der Schalter im Panel beim ALLERERSTEN Server nichts getan -- der
// Durchlauf wäre ausgestiegen, bevor er das Panel überhaupt fragt. Und
// zwar stillschweigend, was der schlechteste aller Fehlermodi ist: der
// Schalter steht auf an, im Kanal passiert nichts, und es gibt nirgends
// eine Meldung, an der man sich entlanghangeln könnte.
//
// Der Aufruf ist billig: `panelStatus` hält seine Antwort 20 Sekunden und
// gibt ohne eingetragene Adresse sofort null zurück.
const panelItems = await panelStatus();
if (panelItems) {
const { angelegt, uebernommen, entfernt } = abgleichen(panelItems);
if (entfernt.length > 0) await embedWegraeumen(client, entfernt);
if (angelegt.length + uebernommen.length + entfernt.length > 0) {
console.log(
`[panel] neu: ${angelegt.join(', ') || '—'}`
+ ` | uebernommen: ${uebernommen.join(', ') || '—'}`
+ ` | entfernt: ${entfernt.map((e) => e.name).join(', ') || '—'}`,
);
}
}
const servers = listGameservers();
if (servers.length === 0) return;
// NICHT `servers.map(queryServer)` -- so stand es hier, und mit dem
// zweiten Parameter wäre es eine Falle gewesen: `map` reicht (element,
// index, array) weiter, `panelItems` bekäme also die Zahl 0. Der erste
// Server hätte damit `null` gesehen und alle weiteren eine Zahl, an der
// `panelEintrag` still scheitert -- ein Fehler, der nur beim zweiten
// Eintrag auftritt und nach Zufall aussieht.
const results = await Promise.all(servers.map((s) => queryServer(s, panelItems)));
// Die Zusatzauskunft vom Panel: EINMAL je Durchlauf, nicht je Server.
// Sie ist optional -- ist nichts eingetragen oder antwortet das Panel
// nicht, bleibt `panel` schlicht null und alles läuft wie vorher.
for (const r of results) r.panel = panelEintrag(panelItems, r);
// NICHT `servers.map(queryServer)`, sondern der Pfeil drumherum: `map`
// reicht (element, index, array) weiter, und ein zweiter Parameter bekäme
// damit stillschweigend die Zahl 0. Der Aufruf hat heute nur einen —
// trotzdem bleibt der Pfeil stehen, damit ein zweiter niemandem zur Falle
// wird.
const results = await Promise.all(servers.map((s) => queryServer(s)));
// Verlauf für die Web-Seite festhalten + Cache fürs API
for (const r of results) {
-4
View File
@@ -268,10 +268,6 @@ export const MODULES = [
// Optional. GameDig FRAGT einen Server -- ein startender antwortet
// nicht, und das sieht aus wie ein Ausfall. Das Panel SIEHT den
// Container und kann "startet gerade" von "aus" unterscheiden.
{ setting: 'panel_url', label: 'Panel-Adresse', art: 'text',
hint: 'optional, z. B. https://panel.d4rkst3r.de — unterscheidet „startet gerade" von „offline"' },
{ setting: 'panel_zeichen', label: 'Panel-Zeichen', art: 'text',
hint: 'im Panel unter „Zugang für Programme" anlegen; darf nur lesen' },
],
tool: { tab: 'server', label: 'Game-Server verwalten' },
},
-143
View File
@@ -1,143 +0,0 @@
// Server, die im Panel das Häkchen „Im Discord anzeigen" tragen, hier anlegen
// — und wieder wegräumen, wenn es fällt.
//
// WARUM DIESE RICHTUNG. Der naheliegende Weg wäre: das Panel ruft die
// Bot-API und legt selbst an. Dann müsste das Panel aber ein Zeichen halten,
// das SCHREIBEN darf — und damit läge das mächtigere der beiden Geheimnisse
// auf der Seite, die es nicht braucht. So bleibt es bei einem Zeichen, das nur
// lesen kann, und der Bot holt den Statusbericht ohnehin alle 30 Sekunden ab.
// Das Häkchen reist einfach mit.
//
// DER PREIS IST EINE HALBE MINUTE. Deshalb sagt das Panel beim Umlegen des
// Schalters dazu, wie lange es dauert. Ein Knopf, der scheinbar nichts tut,
// wird dreimal gedrückt.
//
// WAS HIER NICHT PASSIERT: kein Feld eines bestehenden Eintrags wird
// überschrieben. Wer im Webinterface ein Bild, einen Verbinden-Link oder Regeln
// ergänzt hat, soll das nicht beim nächsten Durchlauf verlieren. Angelegt,
// übernommen und gelöscht wird — mehr nicht.
//
// „Übernommen" heißt: gab es den Server schon von Hand, bekommt er nur den
// Herkunftsvermerk. Sonst täte der Schalter im Panel sichtbar nichts, und
// genau das ist dem Betreiber passiert.
import { listGameservers, createGameserver, deleteGameserver, getSetting, db } from './db.js';
/** Einen bestehenden Eintrag dem Panel zuschreiben -- ohne ihn sonst anzufassen. */
const uebernehmenStmt = db.prepare('UPDATE gameservers SET panel_name = ? WHERE id = ?');
/**
* Taugt dieser Panel-Eintrag für einen Monitor-Eintrag?
*
* Drei Bedingungen, und alle drei sind im Panel schon geprüft — hier stehen
* sie noch einmal, weil ein Bericht von einem älteren Panel kommen kann und
* ein halber Eintrag schlimmer ist als keiner: er stünde dauerhaft auf
* offline und schlüge Alarm.
*
* ================== WARUM NICHT MEHR `x.gamedig` ==========================
*
* Hier stand `x.gamedig`, also: „kennt unsere Bibliothek dieses Spiel?" Das
* hat einen Fall ausgesperrt, den es wirklich gibt — TeamSpeak. Nicht weil
* gamedig es nicht könnte (es hat `teamspeak3`), sondern weil der
* ServerQuery-Port des Betreibers absichtlich auf `127.0.0.1` liegt: der Bot
* läuft im Container und kommt dort nie hin. Am 16.08.2026 gegengeprüft,
* beide Versuche vom Host aus gescheitert.
*
* Das Panel dagegen sitzt auf dem Host, darf dorthin und hat die Zugangsdaten
* — es schickt seine eigene Messung seither im Statusbericht mit. Gefragt
* wird deshalb `zeigbar`: „kann ÜBERHAUPT JEMAND diesen Server abfragen —
* wir über gamedig oder das Panel für uns?"
*
* `?? Boolean(x.gamedig)` ist der Rückfall für ein ÄLTERES Panel, das
* `zeigbar` noch nicht kennt. Ohne ihn verschwänden nach einem Bot-Update
* schlagartig alle Einträge, und zwar lautlos.
*/
export function taugt(x) {
const zeigbar = x?.zeigbar ?? Boolean(x?.gamedig);
return Boolean(x?.discord && zeigbar && x.adresse && x.port);
}
/**
* Einmal abgleichen.
*
* @returns {{angelegt: string[], uebernommen: string[],
* entfernt: Array<{id: number, name: string, message_id: string|null}>}}
* Die Entfernten kommen mit ihrer Nachrichten-Kennung zurück, damit
* der Aufrufer das Embed löschen kann — hier gibt es keinen Client.
*/
export function abgleichen(items) {
const ergebnis = { angelegt: [], uebernommen: [], entfernt: [] };
if (!Array.isArray(items)) return ergebnis;
const vorhanden = listGameservers();
const ausPanel = new Map(vorhanden.filter((s) => s.panel_name).map((s) => [s.panel_name, s]));
const gewollt = new Set(items.filter(taugt).map((x) => x.name));
for (const x of items) {
if (!taugt(x) || ausPanel.has(x.name)) continue;
// Ein Server auf derselben Adresse und demselben Port ist schon da —
// dann NICHT noch einmal anlegen. Sonst stünde derselbe Server zweimal
// im Kanal, und niemand wüsste, welches Embed das richtige ist.
//
// ABER AUCH NICHT EINFACH ÜBERGEHEN. Vorher stand hier `continue`, und
// damit tat der Schalter im Panel sichtbar nichts, sobald der Server
// schon von Hand eingetragen war — keine Meldung, kein Embed, keine
// Spur. Genau so ist es dem Betreiber gegangen, und er hat den Fehler
// erst bei sich gesucht.
//
// Wer den Schalter umlegt, sagt „dieser Server gehört ans Panel". Also
// wird der bestehende Eintrag übernommen statt ignoriert.
//
// Der Preis gehört benannt: ab dann nimmt ein Ausschalten den Eintrag
// mit, samt Bild und Links, die von Hand daran hängen. Das ist der Sinn
// des Schalters — aber es ist eine Übertragung von Besitz und keine
// Kleinigkeit.
const dasselbe = vorhanden.find(
(s) => Number(s.port) === Number(x.port) && String(s.host) === String(x.adresse),
);
if (dasselbe) {
if (!dasselbe.panel_name) {
uebernehmenStmt.run(x.name, dasselbe.id);
ergebnis.uebernommen.push(x.name);
}
continue;
}
createGameserver({
name: x.name,
// OHNE gamedig-KENNUNG FRAGEN WIR DAS PANEL. Der Typ `panel` ist
// unser dritter eigener neben `fivem` und `http` — siehe
// `queryServer`. Wo gamedig kann, bleibt es bei gamedig: dessen
// Antwort bringt Karte, Fassung und Ping mit, die das Panel nicht
// liefert. Der neue Typ schliesst eine Luecke, er ersetzt nichts.
type: x.gamedig || 'panel',
host: x.adresse,
port: x.port,
address: `${x.adresse}:${x.port}`,
query_url: '',
// Die Herkunft. Nur was hier steht, wird später auch wieder
// weggeräumt — der handgepflegte ATM10 ist davon nicht berührt.
panel_name: x.name,
});
ergebnis.angelegt.push(x.name);
}
for (const [name, s] of ausPanel) {
if (gewollt.has(name)) continue;
deleteGameserver(s.id);
ergebnis.entfernt.push({ id: s.id, name, message_id: s.message_id ?? null });
}
return ergebnis;
}
/** Das Status-Embed eines entfernten Servers mit wegräumen. */
export async function embedWegraeumen(client, weg) {
const channelId = getSetting('status_channel_id');
if (!channelId) return;
for (const e of weg) {
if (!e.message_id) continue;
const channel = await client.channels.fetch(channelId).catch(() => null);
const message = await channel?.messages?.fetch(e.message_id).catch(() => null);
await message?.delete().catch(() => {});
}
}
-164
View File
@@ -1,164 +0,0 @@
// Auskunft vom d4rk_gameserver-Panel.
//
// WARUM ÜBERHAUPT, wo der Monitor doch schon GameDig hat: weil GameDig einen
// Server FRAGT, und ein Server, der noch startet, antwortet nicht. Für den
// Monitor sieht das aus wie „offline" — und genau dann steht im Alarm-Kanal
// eine Ausfallmeldung, obwohl niemand etwas kaputtgemacht hat.
//
// Ein Modpack braucht bis zum ersten Spieler Minuten; am ATM10 des Betreibers
// gemessen sind es 306 Sekunden. Fünf Minuten Fehlalarm bei jedem Neustart.
//
// Das Panel weiß es besser, weil es den Container sieht statt ihn zu fragen:
//
// zustand was Docker sagt (running, exited, weg)
// bereit ob das Fertig-Merkmal im Protokoll stand
//
// Damit lässt sich „läuft noch gar nicht" von „startet gerade" und von
// „hängt" unterscheiden.
//
// ES IST ABSICHTLICH OPTIONAL. Ohne Adresse und Zeichen verhält sich der
// Monitor exakt wie vorher, und wenn das Panel nicht antwortet, ebenfalls.
// Eine Zusatzauskunft darf nie dazu führen, dass die Hauptauskunft ausfällt.
// Kurz zwischenspeichern: der Monitor fragt je Server einmal, und bei acht
// Servern wären das acht Abrufe für dieselbe Antwort.
let zwischen = { zeit: 0, items: null };
const FRISCH_MS = 20_000;
/** Der ganze Panel-Status, oder null. Wirft nie. */
export async function panelStatus() {
// ERST HIER GELADEN und nicht oben im Modul: `runtime-settings` zieht die
// Datenbank mit herein, und dann laesst sich keine der reinen Funktionen
// unten mehr ohne better-sqlite3 pruefen. Eine Probe, die eine Datenbank
// braucht, um eine Zeichenkette zu vergleichen, schreibt niemand.
const { panelUrl, panelZeichen } = await import('./runtime-settings.js');
const basis = panelUrl();
const zeichen = panelZeichen();
if (!basis || !zeichen) return null;
if (zwischen.items && Date.now() - zwischen.zeit < FRISCH_MS) return zwischen.items;
try {
const steuer = AbortSignal.timeout(5000);
const antwort = await fetch(`${basis.replace(/\/$/, '')}/api/oeffentlich/status`, {
headers: { authorization: `Bearer ${zeichen}` },
signal: steuer,
});
if (!antwort.ok) return null;
const daten = await antwort.json();
zwischen = { zeit: Date.now(), items: Array.isArray(daten.items) ? daten.items : [] };
return zwischen.items;
} catch {
// Panel weg, Zeichen zurückgezogen, Netz hakt — der Monitor läuft
// dann einfach ohne diese Zusatzauskunft weiter.
return null;
}
}
/**
* Den Panel-Eintrag zu einem Monitor-Server finden.
*
* ÜBER DEN PORT, nicht über den Namen. Namen sind Geschmackssache und stehen
* in zwei Werkzeugen selten gleich; der Port ist eine Zahl, die beide kennen
* und die nur einmal vergeben ist.
*/
export function panelEintrag(items, server) {
if (!items?.length) return null;
const port = Number(server?.port);
if (Number.isFinite(port) && port > 0) {
const treffer = items.find((x) => Number(x.port) === port);
if (treffer) return treffer;
}
// Zweite Möglichkeit: gleicher Name, klein geschrieben. Hilft, wenn
// jemand den Port im Monitor weggelassen hat.
const name = String(server?.name ?? '').trim().toLowerCase();
return items.find((x) => String(x.name).trim().toLowerCase() === name) ?? null;
}
/**
* Wie lange ein Start dauern darf, bevor er als hängend gilt.
*
* Gemessen: ATM10 braucht 306 Sekunden bis zum ersten Spieler. Fünfzehn
* Minuten sind knapp das Dreifache — genug Luft für ein größeres Pack auf
* kaltem Cache, und kurz genug, dass ein hängender Start noch am selben
* Abend auffällt.
*
* DIESE GRENZE IST NÖTIG, sonst frisst die Anbindung genau den Alarm, für den
* es sie gibt: `bereit` wird nie von allein wahr. Ein Server, der beim Start
* hängt, bliebe für immer „startet gerade" — und für immer ohne Meldung.
*/
export const STARTGNADE_MS = 15 * 60_000;
/** Läuft der Container und ist er noch in der zugestandenen Startzeit? */
export function startetNoch(eintrag) {
if (eintrag?.zustand !== 'running' || eintrag.bereit) return false;
// Ohne Startzeit lässt sich nichts begrenzen. Das passiert nur, wenn
// Dockers Zeitstempel unlesbar ist — dieselbe Antwort, die uns gerade
// „running" gesagt hat. Ein kaputtes Feld ist kein hängender Server,
// deshalb hier Nachsicht.
if (!eintrag.seit) return true;
return Date.now() - eintrag.seit < STARTGNADE_MS;
}
/**
* War das Ende gewollt?
*
* 0 heißt sauber beendet, 143 ist SIGTERM (`docker stop`) und 137 SIGKILL
* nach abgelaufener Frist — alle drei entstehen, wenn jemand im Panel auf
* Stopp drückt. Jeder andere Code ist ein Absturz, und `oom` heißt: der
* Speicher war zu knapp. Genau dafür ist der Alarm-Kanal da.
*/
export function gewolltAus(eintrag) {
if (eintrag?.zustand !== 'exited') return false;
if (eintrag.oom) return false;
// `code` fehlt bei einem älteren Panel. Dann ist die Frage nicht
// beantwortbar, und im Zweifel gilt der Ausfall als echt.
return eintrag.code === 0 || eintrag.code === 143 || eintrag.code === 137;
}
/**
* Was im Embed stehen soll, wenn GameDig nichts erreicht hat.
*
* Gibt null zurück, wenn das Panel nichts beizutragen hat -- dann bleibt es
* beim schlichten „Offline".
*/
export function zustandText(eintrag) {
if (!eintrag) return null;
if (eintrag.zustand === 'running' && !eintrag.bereit) {
const s = eintrag.seit ? Math.round((Date.now() - eintrag.seit) / 1000) : null;
const dauer = s == null ? '' : s < 90 ? ` (seit ${s} s)` : ` (seit ${Math.round(s / 60)} min)`;
// Nach der Gnadenfrist ist „startet gerade" keine Auskunft mehr,
// sondern eine Beschwichtigung. Dann steht hier, was Sache ist.
return startetNoch(eintrag)
? `🟡 **Startet gerade**${dauer}`
: `🟠 **Startet seit ${dauer.replace(/^ \(seit |\)$/g, '')} und ist nicht fertig**`;
}
if (eintrag.zustand === 'exited') {
if (eintrag.oom) return '🔴 **Vom Speicher erschlagen** (out of memory)';
if (gewolltAus(eintrag)) return '⚫ **Gestoppt** (im Panel)';
// Auch ohne `code` (älteres Panel) landet man hier — dann ohne Zahl.
return eintrag.code == null
? '🔴 **Beendet**'
: `🔴 **Abgestürzt** (Code ${eintrag.code})`;
}
if (eintrag.zustand === 'weg') return '🔴 **Container fehlt**';
return null;
}
/** „1,2 GB / 7 GB · 14 %" — nur wenn das Panel Werte hat. */
export function lastText(eintrag) {
if (!eintrag || eintrag.zustand !== 'running') return null;
const teile = [];
if (eintrag.ram) {
const gb = (b) => `${(b / 1024 ** 3).toFixed(1)} GB`;
teile.push(eintrag.ram_grenze ? `${gb(eintrag.ram)} / ${gb(eintrag.ram_grenze)}` : gb(eintrag.ram));
}
// Das Panel liefert Dockers Kern-Prozent (200 = zwei Kerne voll). Für eine
// Statusmeldung ist der Anteil an der Maschine die verständlichere Zahl --
// „187 %" liest sich wie ein Fehler.
if (eintrag.cpu != null && eintrag.kerne) teile.push(`${(eintrag.cpu / eintrag.kerne).toFixed(1)} % CPU`);
return teile.length ? teile.join(' · ') : null;
}
-17
View File
@@ -326,23 +326,6 @@ export function repoIgnored(repo) {
);
}
/**
* d4rk_gameserver-Panel — Adresse und Lesezeichen.
*
* BEIDES NUR HIER UND NICHT IN DER .env: der Betreiber soll es im Webinterface
* eintragen können, so wie alles andere auch. Das Zeichen darf ausschließlich
* lesen; selbst wenn es abhandenkommt, kann damit niemand einen Server stoppen.
*
* Leer = die Zusatzauskunft ist aus, und der Monitor verhält sich wie vorher.
*/
export function panelUrl() {
return (getSetting('panel_url') || '').replace(/\/$/, '');
}
export function panelZeichen() {
return getSetting('panel_zeichen') || '';
}
/**
* Welche Protokoll-Meldungen überhaupt geschrieben werden.
*
-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