diff --git a/README.md b/README.md index 2ab0f56..dd53567 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen. | 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)) | ### Server & Technik diff --git a/frontend/src/pages/Server.jsx b/frontend/src/pages/Server.jsx index 594242c..75ff5c8 100644 --- a/frontend/src/pages/Server.jsx +++ b/frontend/src/pages/Server.jsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { apiGet } from '../api.js'; 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 UptimeTrack from '../components/UptimeTrack.jsx'; @@ -100,7 +100,15 @@ function ServerCard({ s }) { ? : {s.icon}}
- {s.name} + + {s.name} + {s.zugang?.zu && ( + + )} + + {/* Wie der Server sich im Spiel nennt — nur wenn es etwas + anderes ist als der Name hier, sonst doppelt es sich. */} + {s.spielname && {s.spielname}} {s.online === null ? <> {t('server.waiting')} @@ -126,7 +134,7 @@ function ServerCard({ s }) { )} {/* 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) && (
{s.map && {s.map}} {s.version && {s.version}} @@ -134,6 +142,7 @@ function ServerCard({ s }) { sähen die beiden Chips wie zusammengehörig aus. */} {s.mods > 0 && {t('server.mods', s.mods)}} {s.ping != null && {s.ping} ms} + {s.zugang?.grund && {s.zugang.grund}}
)} diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index f58f3b9..7ddb4ea 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -406,7 +406,7 @@ export default function Settings({ me }) { const [triggers, setTriggers] = useState([]); const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' }); // 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 [games, setGames] = useState([]); 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 ?? '', 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 ?? '', - links: s.links ?? [], + links: s.links ?? [], zugang: s.zugang ?? 'auto', })}>Bearbeiten
@@ -3415,7 +3415,23 @@ export default function Settings({ me }) { )}
- + + + +
setServerDraft({ ...serverDraft, connect_url: e.target.value })} /> setServerDraft({ ...serverDraft, address: e.target.value })} /> diff --git a/frontend/src/style.css b/frontend/src/style.css index aeb4daa..e6159ec 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -2891,3 +2891,12 @@ button.with-icon, a.with-icon { justify-content: center; } .tune-row { gap: .6rem; } .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; +} diff --git a/src/bot/server-monitor.js b/src/bot/server-monitor.js index fbe880f..cae996c 100644 --- a/src/bot/server-monitor.js +++ b/src/bot/server-monitor.js @@ -76,6 +76,9 @@ export async function queryServer(server) { const base = { ...server, online: false, players: null, max: null, map: 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(); try { @@ -91,6 +94,7 @@ export async function queryServer(server) { players: Number(data.clients) || 0, max: Number(data.sv_maxclients) || 0, map: data.mapname ?? null, + spielname: data.hostname ?? null, }; } if (server.type === 'http') { @@ -118,6 +122,11 @@ export async function queryServer(server) { max: state.maxplayers ?? null, map: state.map || 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 // das Feld leer statt auf 0 zu stehen und eine Aussage zu machen. 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 */ export function istWebLink(url) { return /^https?:\/\/\S+$/i.test(String(url ?? '').trim()); @@ -182,10 +219,15 @@ export function buildServerNachricht(r) { /** GSM-Style-Embed für ein Query-Ergebnis */ export function buildServerEmbed(r) { const icon = GAME_ICONS[r.type] ?? '🎮'; + const zugang = zugangStand(r, r); const embed = new EmbedBuilder() .setColor(r.online ? GREEN : RED) - .setTitle(`${icon} ${r.name}`) - .addFields({ name: 'Status', value: r.online ? '🟢 **Online**' : '🔴 **Offline**', inline: true }) + .setTitle(`${icon} ${r.name}${zugang.zu ? ' 🔒' : ''}`) + .addFields({ + name: 'Status', + value: `${r.online ? '🟢 **Online**' : '🔴 **Offline**'}${zugang.grund ? `\n🔒 ${zugang.grund}` : ''}`, + inline: true, + }) // Intervall aus den Werten lesen, nicht festschreiben — sonst behauptet // das Embed weiter „alle 2 min", nachdem im Panel etwas anderes steht. .setFooter({ @@ -194,6 +236,11 @@ export function buildServerEmbed(r) { }) .setTimestamp(); 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) { const pct = Math.min(1, r.players / r.max); diff --git a/src/db.js b/src/db.js index adafa7a..202e830 100644 --- a/src/db.js +++ b/src/db.js @@ -931,6 +931,9 @@ db.exec(` // weil sonst für jeden neuen Zweck eine Spalte dazukäme — und genau so // sind connect_url und mods_url entstanden. 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. // 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); } const insertGameserver = db.prepare(` - INSERT INTO gameservers (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) + 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, @zugang) `); const updateGameserverStmt = db.prepare(` 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, - mods_url = @mods_url, links = @links + mods_url = @mods_url, links = @links, zugang = @zugang WHERE id = @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. const gameserverFelder = { 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 // heraus. better-sqlite3 kann kein Array binden und würde sonst werfen. diff --git a/src/web/api.js b/src/web/api.js index f21cb8b..30566c3 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -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 { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.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 { listRules, setRuleEnabled } from '../bot/automod.js'; import { @@ -227,6 +227,10 @@ export function registerApiRoutes(app, client) { connect_url: s.connect_url || null, mods_url: s.mods_url || null, 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 players: r?.players ?? null, max: r?.max ?? null, @@ -1611,6 +1615,11 @@ ${rssItems} ? String(body.mods_url).trim().slice(0, 300) : '', 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', }; }