Servername aus dem Spiel und ein Schloss fuer Passwort oder Whitelist
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Der Name im Panel ist der, den DU vergibst — wie der Server sich im Spiel
nennt, stand nirgends. Steht jetzt darunter, aber nur wenn es etwas anderes
ist als der eingetragene Name; sonst haette dieselbe Zeile zweimal dagestanden.

Beim Schloss war die Frage, wie viel die Abfrage ueberhaupt hergibt. Nachgesehen
statt geraten: 31 der 358 gamedig-Protokolle melden ein Passwort. valve
gehoert dazu, deckt also CS2, Rust, ARK, Squad, 7DTD, Zomboid, Gmod und DayZ
ab. Minecraft nicht, Farming Simulator auch nicht — im LS-XML kommt das Wort
Passwort kein einziges Mal vor. Und eine Whitelist meldet grundsaetzlich kein
Spiel, die steht in einer Datei auf dem Server.

Also beides: automatisch, wo es geht, und eine Einstellung, die es schlaegt.
Wer "Whitelist" eintraegt, bekommt sie angezeigt, auch wenn der Server
schweigt.

Wichtig dabei ist der dritte Zustand. "Sagt nichts" ist nicht "offen" —
deshalb `null` und kein `false`. Ein Schloss, das bei jedem schweigenden
Server fehlt, waere eine Aussage, die wir nicht belegen koennen; umgekehrt
waere ein Schloss ohne Grundlage eine Luege. Bei unbekannt steht schlicht
nichts da.

Geprueft: Einstellung schlaegt Messung in allen vier Faellen, automatisch mit
true/false/null, fehlender Server, unbekannter Wert faellt auf automatisch
zurueck statt zu sperren, Schloss im Embed-Titel nur wenn belegt, Spielname
nur bei echtem Unterschied (auch mit Leerzeichen drumherum). Panel und
/server-Seite im laufenden Frontend angesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 14:12:33 +02:00
co-authored by Claude Opus 5
parent 4ce3c10d76
commit ea523b76b3
7 changed files with 107 additions and 14 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen.
| Funktion | Was sie macht | | Funktion | Was sie macht |
|---|---| |---|---|
| 🎮 **Game-Server-Monitor** | DiscordGSM-Stil: pro Server ein Live-Embed (🟢/🔴, 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 | | 🎮 **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 |
| 🚜 **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), Hof-Übersicht mit Hektar und Wert, Meldung bei Landkauf, dazu die Preiskurven übers Jahr mit „jetzt verkaufen oder warten". **Nicht möglich:** welche Frucht auf welchem Feld steht — das gibt der Feed nicht her (siehe [docs/ls-feed.md](docs/ls-feed.md)) | | 🚜 **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), Hof-Übersicht mit Hektar und Wert, Meldung bei Landkauf, dazu die Preiskurven übers Jahr mit „jetzt verkaufen oder warten". **Nicht möglich:** welche Frucht auf welchem Feld steht — das gibt der Feed nicht her (siehe [docs/ls-feed.md](docs/ls-feed.md)) |
### Server & Technik ### Server & Technik
+12 -3
View File
@@ -4,7 +4,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { apiGet } from '../api.js'; import { apiGet } from '../api.js';
import { SkeletonCards } from '../components/Skeleton.jsx'; import { SkeletonCards } from '../components/Skeleton.jsx';
import { IconGame, IconMapPin, IconClock, IconCheck, IconTeam, IconPackage, IconZap, IconDownload, IconArchive } from '../icons.jsx'; import { IconGame, IconMapPin, IconClock, IconCheck, IconTeam, IconPackage, IconZap, IconDownload, IconArchive, IconLock } from '../icons.jsx';
import { useT, useFmt } from '../i18n.jsx'; import { useT, useFmt } from '../i18n.jsx';
import UptimeTrack from '../components/UptimeTrack.jsx'; import UptimeTrack from '../components/UptimeTrack.jsx';
@@ -100,7 +100,15 @@ function ServerCard({ s }) {
? <img className="srv-logo" src={s.image_url} alt="" loading="lazy" /> ? <img className="srv-logo" src={s.image_url} alt="" loading="lazy" />
: <span className="srv-logo srv-logo-emoji">{s.icon}</span>} : <span className="srv-logo srv-logo-emoji">{s.icon}</span>}
<div className="srv-title"> <div className="srv-title">
<span className="srv-name">{s.name}</span> <span className="srv-name">
{s.name}
{s.zugang?.zu && (
<IconLock size={15} className="srv-schloss" aria-label={s.zugang.grund ?? ''} />
)}
</span>
{/* Wie der Server sich im Spiel nennt — nur wenn es etwas
anderes ist als der Name hier, sonst doppelt es sich. */}
{s.spielname && <span className="srv-spielname">{s.spielname}</span>}
<span className={`srv-status ${s.online ? 'on' : s.online === false ? 'off' : ''}`}> <span className={`srv-status ${s.online ? 'on' : s.online === false ? 'off' : ''}`}>
{s.online === null {s.online === null
? <><IconClock size={13} /> {t('server.waiting')}</> ? <><IconClock size={13} /> {t('server.waiting')}</>
@@ -126,7 +134,7 @@ function ServerCard({ s }) {
)} )}
{/* Was der Server über sich verrät — nur, was auch da ist */} {/* Was der Server über sich verrät — nur, was auch da ist */}
{s.online && (s.map || s.ping != null || s.version) && ( {s.online && (s.map || s.ping != null || s.version || s.zugang?.grund) && (
<div className="srv-chips"> <div className="srv-chips">
{s.map && <span className="srv-chip"><IconMapPin size={12} /> {s.map}</span>} {s.map && <span className="srv-chip"><IconMapPin size={12} /> {s.map}</span>}
{s.version && <span className="srv-chip"><IconPackage size={12} /> {s.version}</span>} {s.version && <span className="srv-chip"><IconPackage size={12} /> {s.version}</span>}
@@ -134,6 +142,7 @@ function ServerCard({ s }) {
sähen die beiden Chips wie zusammengehörig aus. */} sähen die beiden Chips wie zusammengehörig aus. */}
{s.mods > 0 && <span className="srv-chip"><IconArchive size={12} /> {t('server.mods', s.mods)}</span>} {s.mods > 0 && <span className="srv-chip"><IconArchive size={12} /> {t('server.mods', s.mods)}</span>}
{s.ping != null && <span className="srv-chip"><IconZap size={12} /> {s.ping} ms</span>} {s.ping != null && <span className="srv-chip"><IconZap size={12} /> {s.ping} ms</span>}
{s.zugang?.grund && <span className="srv-chip zu"><IconLock size={12} /> {s.zugang.grund}</span>}
</div> </div>
)} )}
+19 -3
View File
@@ -406,7 +406,7 @@ export default function Settings({ me }) {
const [triggers, setTriggers] = useState([]); const [triggers, setTriggers] = useState([]);
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' }); const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
// Game-Server — die Spielliste kommt aus gamedig (siehe /api/gamedig-games) // Game-Server — die Spielliste kommt aus gamedig (siehe /api/gamedig-games)
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '', token: '', mods_url: '', links: [] }; const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '', token: '', mods_url: '', links: [], zugang: 'auto' };
const [servers, setServers] = useState([]); const [servers, setServers] = useState([]);
const [games, setGames] = useState([]); const [games, setGames] = useState([]);
const [serverDraft, setServerDraft] = useState(emptyServer); const [serverDraft, setServerDraft] = useState(emptyServer);
@@ -3354,7 +3354,7 @@ export default function Settings({ me }) {
id: s.id, name: s.name, type: s.type, query_url: s.query_url ?? '', id: s.id, name: s.name, type: s.type, query_url: s.query_url ?? '',
address: s.address ?? '', host: s.host ?? '', port: s.port ?? '', connect_url: s.connect_url ?? '', address: s.address ?? '', host: s.host ?? '', port: s.port ?? '', connect_url: s.connect_url ?? '',
image_url: s.image_url ?? '', token: s.token ?? '', mods_url: s.mods_url ?? '', image_url: s.image_url ?? '', token: s.token ?? '', mods_url: s.mods_url ?? '',
links: s.links ?? [], links: s.links ?? [], zugang: s.zugang ?? 'auto',
})}>Bearbeiten</button> })}>Bearbeiten</button>
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}></button> <button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}></button>
</div> </div>
@@ -3415,7 +3415,23 @@ export default function Settings({ me }) {
</div> </div>
)} )}
<div className="field"> <div className="field">
<label>Connect-Link + Anzeige-Adresse <span className="field-hint">https wird zum Knopf; steam:// / fivem:// stehen als Codeblock zum Kopieren. Adresse ebenfalls als Copy-Paste-Block</span></label> <label>
Zugang <span className="field-hint">
zeigt ein 🔒 im Embed und auf der /server-Seite. „Automatisch" nimmt, was die
Abfrage sagt das können aber nur wenige Spiele, und eine Whitelist meldet nie jemand.
</span>
</label>
<select
value={serverDraft.zugang}
onChange={(e) => setServerDraft({ ...serverDraft, zugang: e.target.value })}
>
<option value="auto">Automatisch was der Server meldet</option>
<option value="offen">Offen für alle</option>
<option value="passwort">🔒 Passwort</option>
<option value="whitelist">🔒 Whitelist</option>
</select>
<label style={{ marginTop: '.9rem' }}>Connect-Link + Anzeige-Adresse <span className="field-hint">https wird zum Knopf; steam:// / fivem:// stehen als Codeblock zum Kopieren. Adresse ebenfalls als Copy-Paste-Block</span></label>
<div className="field-row"> <div className="field-row">
<input type="text" placeholder="https://cfx.re/join/abc123" value={serverDraft.connect_url} onChange={(e) => setServerDraft({ ...serverDraft, connect_url: e.target.value })} /> <input type="text" placeholder="https://cfx.re/join/abc123" value={serverDraft.connect_url} onChange={(e) => setServerDraft({ ...serverDraft, connect_url: e.target.value })} />
<input type="text" placeholder="connect play.d4rkst3r.de" value={serverDraft.address} onChange={(e) => setServerDraft({ ...serverDraft, address: e.target.value })} /> <input type="text" placeholder="connect play.d4rkst3r.de" value={serverDraft.address} onChange={(e) => setServerDraft({ ...serverDraft, address: e.target.value })} />
+9
View File
@@ -2891,3 +2891,12 @@ button.with-icon, a.with-icon { justify-content: center; }
.tune-row { gap: .6rem; } .tune-row { gap: .6rem; }
.tune-range { display: none; } .tune-range { display: none; }
} }
/* Schloss am Servernamen + der Grund als Chip */
.srv-schloss { color: var(--neon); margin-left: .45rem; vertical-align: -2px; }
.srv-chip.zu { color: var(--neon); border-color: rgba(245, 197, 24, .35); }
/* Wie der Server sich im Spiel nennt — leiser als der eigene Name */
.srv-spielname {
font-family: var(--mono); font-size: .68rem; color: var(--muted2);
letter-spacing: .04em; margin-top: .1rem;
}
+49 -2
View File
@@ -76,6 +76,9 @@ export async function queryServer(server) {
const base = { const base = {
...server, online: false, players: null, max: null, map: null, ...server, online: false, players: null, max: null, map: null,
playerNames: [], ping: null, version: null, mods: null, playerNames: [], ping: null, version: null, mods: null,
// Wie der Server sich selbst nennt, und ob er ein Passwort verlangt.
// `null` heisst „sagt er nicht" — das ist etwas anderes als „nein".
spielname: null, passwort: null,
}; };
const started = Date.now(); const started = Date.now();
try { try {
@@ -91,6 +94,7 @@ export async function queryServer(server) {
players: Number(data.clients) || 0, players: Number(data.clients) || 0,
max: Number(data.sv_maxclients) || 0, max: Number(data.sv_maxclients) || 0,
map: data.mapname ?? null, map: data.mapname ?? null,
spielname: data.hostname ?? null,
}; };
} }
if (server.type === 'http') { if (server.type === 'http') {
@@ -118,6 +122,11 @@ export async function queryServer(server) {
max: state.maxplayers ?? null, max: state.maxplayers ?? null,
map: state.map || null, map: state.map || null,
version: state.version || null, version: state.version || null,
spielname: state.name || null,
// 31 von 358 Protokollen melden das — valve (CS2, Rust, ARK,
// Squad, Zomboid …) gehoert dazu, Minecraft und Farming Simulator
// nicht. Wo nichts kommt, bleibt es null statt false.
passwort: typeof state.password === 'boolean' ? state.password : null,
// Nur Farming Simulator liefert die Modliste mit; anderswo bleibt // Nur Farming Simulator liefert die Modliste mit; anderswo bleibt
// das Feld leer statt auf 0 zu stehen und eine Aussage zu machen. // das Feld leer statt auf 0 zu stehen und eine Aussage zu machen.
mods: Array.isArray(state.raw?.mods) ? state.raw.mods.length : null, mods: Array.isArray(state.raw?.mods) ? state.raw.mods.length : null,
@@ -128,6 +137,34 @@ export async function queryServer(server) {
} }
} }
/**
* Wer darf auf den Server?
*
* Nur 31 der 358 Protokolle melden überhaupt ein Passwort, und eine Whitelist
* meldet gar keins — die steckt in einer Datei auf dem Server, nicht in der
* Abfrage. Deshalb schlägt die Einstellung die Messung: wer „Whitelist"
* einträgt, bekommt sie angezeigt, auch wenn der Server schweigt.
*
* `null` heisst „unbekannt" und ist etwas anderes als „offen". Ein Schloss,
* das bei jedem schweigenden Server fehlt, wäre eine Aussage, die wir nicht
* belegen können.
*
* @returns {{ zu: boolean|null, grund: string|null }}
*/
export function zugangStand(server, ergebnis = {}) {
switch (server?.zugang) {
case 'passwort': return { zu: true, grund: 'Passwort' };
case 'whitelist': return { zu: true, grund: 'Whitelist' };
case 'offen': return { zu: false, grund: null };
default: {
const p = ergebnis.passwort;
if (p === true) return { zu: true, grund: 'Passwort' };
if (p === false) return { zu: false, grund: null };
return { zu: null, grund: null };
}
}
}
/** Nur http(s) darf in einen Link-Knopf — Discord weist alles andere ab */ /** Nur http(s) darf in einen Link-Knopf — Discord weist alles andere ab */
export function istWebLink(url) { export function istWebLink(url) {
return /^https?:\/\/\S+$/i.test(String(url ?? '').trim()); return /^https?:\/\/\S+$/i.test(String(url ?? '').trim());
@@ -182,10 +219,15 @@ export function buildServerNachricht(r) {
/** GSM-Style-Embed für ein Query-Ergebnis */ /** GSM-Style-Embed für ein Query-Ergebnis */
export function buildServerEmbed(r) { export function buildServerEmbed(r) {
const icon = GAME_ICONS[r.type] ?? '🎮'; const icon = GAME_ICONS[r.type] ?? '🎮';
const zugang = zugangStand(r, r);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(r.online ? GREEN : RED) .setColor(r.online ? GREEN : RED)
.setTitle(`${icon} ${r.name}`) .setTitle(`${icon} ${r.name}${zugang.zu ? ' 🔒' : ''}`)
.addFields({ name: 'Status', value: r.online ? '🟢 **Online**' : '🔴 **Offline**', inline: true }) .addFields({
name: 'Status',
value: `${r.online ? '🟢 **Online**' : '🔴 **Offline**'}${zugang.grund ? `\n🔒 ${zugang.grund}` : ''}`,
inline: true,
})
// Intervall aus den Werten lesen, nicht festschreiben — sonst behauptet // Intervall aus den Werten lesen, nicht festschreiben — sonst behauptet
// das Embed weiter „alle 2 min", nachdem im Panel etwas anderes steht. // das Embed weiter „alle 2 min", nachdem im Panel etwas anderes steht.
.setFooter({ .setFooter({
@@ -194,6 +236,11 @@ export function buildServerEmbed(r) {
}) })
.setTimestamp(); .setTimestamp();
if (r.image_url) embed.setThumbnail(r.image_url); if (r.image_url) embed.setThumbnail(r.image_url);
// Wie der Server sich im Spiel nennt. Nur wenn es etwas anderes ist als
// der Name im Panel — sonst stünde dieselbe Zeile zweimal da.
if (r.spielname && r.spielname.trim() && r.spielname.trim() !== r.name?.trim()) {
embed.setDescription(`*${r.spielname.trim().slice(0, 200)}*`);
}
if (r.online && r.players != null && r.max) { if (r.online && r.players != null && r.max) {
const pct = Math.min(1, r.players / r.max); const pct = Math.min(1, r.players / r.max);
+7 -4
View File
@@ -931,6 +931,9 @@ db.exec(`
// weil sonst für jeden neuen Zweck eine Spalte dazukäme — und genau so // weil sonst für jeden neuen Zweck eine Spalte dazukäme — und genau so
// sind connect_url und mods_url entstanden. // sind connect_url und mods_url entstanden.
if (!cols.includes('links')) db.exec(`ALTER TABLE gameservers ADD COLUMN links TEXT NOT NULL DEFAULT '[]'`); if (!cols.includes('links')) db.exec(`ALTER TABLE gameservers ADD COLUMN links TEXT NOT NULL DEFAULT '[]'`);
// Wer darf drauf: 'auto' nimmt, was die Abfrage sagt. Nur die wenigsten
// Spiele melden das — und eine Whitelist meldet gar keins.
if (!cols.includes('zugang')) db.exec(`ALTER TABLE gameservers ADD COLUMN zugang TEXT NOT NULL DEFAULT 'auto'`);
} }
// Die Auswahl im Panel bot mal Spiel-IDs an, die es bei gamedig gar nicht gibt. // Die Auswahl im Panel bot mal Spiel-IDs an, die es bei gamedig gar nicht gibt.
// Wer eine davon gewählt hat, hatte einen Server, der dauerhaft offline stand, // Wer eine davon gewählt hat, hatte einen Server, der dauerhaft offline stand,
@@ -944,13 +947,13 @@ db.exec(`
for (const [alt, neu] of Object.entries(echteIds)) um.run(neu, alt); for (const [alt, neu] of Object.entries(echteIds)) um.run(neu, alt);
} }
const insertGameserver = db.prepare(` const insertGameserver = db.prepare(`
INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url, image_url, token, mods_url, links) INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url, image_url, token, mods_url, links, zugang)
VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url, @image_url, @token, @mods_url, @links) VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url, @image_url, @token, @mods_url, @links, @zugang)
`); `);
const updateGameserverStmt = db.prepare(` const updateGameserverStmt = db.prepare(`
UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address, UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address,
host = @host, port = @port, connect_url = @connect_url, image_url = @image_url, token = @token, host = @host, port = @port, connect_url = @connect_url, image_url = @image_url, token = @token,
mods_url = @mods_url, links = @links mods_url = @mods_url, links = @links, zugang = @zugang
WHERE id = @id WHERE id = @id
`); `);
const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?'); const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?');
@@ -962,7 +965,7 @@ const deleteGameserverStmt = db.prepare('DELETE FROM gameservers WHERE id = ?');
// auffüllen ist billiger, als das an jeder Aufrufstelle nachzuziehen. // auffüllen ist billiger, als das an jeder Aufrufstelle nachzuziehen.
const gameserverFelder = { const gameserverFelder = {
name: '', type: 'fivem', query_url: '', address: '', host: '', port: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: null,
connect_url: '', image_url: '', token: '', mods_url: '', links: '[]', connect_url: '', image_url: '', token: '', mods_url: '', links: '[]', zugang: 'auto',
}; };
// Links dürfen als Liste hereinkommen — so gibt getGameserver sie ja auch // Links dürfen als Liste hereinkommen — so gibt getGameserver sie ja auch
// heraus. better-sqlite3 kann kein Array binden und würde sonst werfen. // heraus. better-sqlite3 kann kein Array binden und würde sonst werfen.
+10 -1
View File
@@ -32,7 +32,7 @@ import crypto from 'node:crypto';
import { moduleStates, moduleDetail, setModuleEnabled, moduleEnabled, moduleFavorites, setModuleFavorites, moduleSettingKeys, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js'; import { moduleStates, moduleDetail, setModuleEnabled, moduleEnabled, moduleFavorites, setModuleFavorites, moduleSettingKeys, MODULE_GROUPS, MODULES, MAX_FAVORITES } from '../modules.js';
import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js'; import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js'; import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
import { lastResults, GAME_ICONS, spielName, spielListe } from '../bot/server-monitor.js'; import { lastResults, GAME_ICONS, spielName, spielListe, zugangStand } from '../bot/server-monitor.js';
import { watchdogState, watchedServices } from '../bot/watchdog.js'; import { watchdogState, watchedServices } from '../bot/watchdog.js';
import { listRules, setRuleEnabled } from '../bot/automod.js'; import { listRules, setRuleEnabled } from '../bot/automod.js';
import { import {
@@ -227,6 +227,10 @@ export function registerApiRoutes(app, client) {
connect_url: s.connect_url || null, connect_url: s.connect_url || null,
mods_url: s.mods_url || null, mods_url: s.mods_url || null,
links: s.links ?? [], links: s.links ?? [],
zugang: zugangStand(s, r ?? {}),
// Wie der Server sich im Spiel nennt, falls anders als hier
spielname: r?.spielname && r.spielname.trim() !== s.name.trim()
? r.spielname.trim() : null,
online: r?.online ?? null, // null = noch nie gecheckt online: r?.online ?? null, // null = noch nie gecheckt
players: r?.players ?? null, players: r?.players ?? null,
max: r?.max ?? null, max: r?.max ?? null,
@@ -1611,6 +1615,11 @@ ${rssItems}
? String(body.mods_url).trim().slice(0, 300) ? String(body.mods_url).trim().slice(0, 300)
: '', : '',
links: freieLinks(body.links), links: freieLinks(body.links),
// Nur bekannte Werte — sonst stuende irgendwann "asdf" in der
// Spalte und zugangStand faellt still auf 'auto' zurueck.
zugang: ['auto', 'offen', 'passwort', 'whitelist'].includes(String(body.zugang ?? ''))
? String(body.zugang)
: 'auto',
}; };
} }