diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 0e386a9..f858aa5 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -181,7 +181,10 @@ const GAMESEL_MAX = 60; * und die drei Felder von Hand auseinanderzupflücken ist Arbeit, die niemand * machen muss. Der Port steht mit drin — er ist nicht immer 8080. * - * @returns {{host: string, port: string, token?: string} | null} + * Die Feed-Basis kommt mit heraus (alles bis zum Dateinamen), damit auch + * Hoster funktionieren, die https nehmen oder einen Pfad davorsetzen. + * + * @returns {{host: string, port: string, query_url: string, token?: string} | null} */ function feedUrlZerlegen(text) { const roh = String(text).trim(); @@ -190,9 +193,12 @@ function feedUrlZerlegen(text) { try { u = new URL(roh); } catch { return null; } if (!u.hostname) return null; const code = u.searchParams.get('code') || u.searchParams.get('token') || ''; + // …/feed/dedicated-server-stats.xml → …/feed + const ordner = u.pathname.replace(/\/[^/]*$/, '').replace(/\/+$/, ''); return { host: u.hostname, port: u.port || '', + query_url: `${u.origin}${ordner}`, // Ohne Code im Link den vorhandenen stehen lassen, statt ihn zu leeren ...(code ? { token: code } : {}), }; @@ -3388,6 +3394,14 @@ export default function Settings({ me }) { value={serverDraft.token} onChange={(e) => setServerDraft({ ...serverDraft, token: e.target.value })} /> + + setServerDraft({ ...serverDraft, query_url: e.target.value })} + />

{gewaehltesSpiel.id.startsWith('farmingsimulator') ? <>Im Spiel heißt er Code und steckt in der Abfrage-URL, die dein Hoster ausgibt: …/feed/dedicated-server-stats.xml?code=DAS_HIER. Am einfachsten die ganze URL oben ins Host-Feld einfügen — Host, Port und Code werden herausgezogen. diff --git a/src/bot/ls-farm.js b/src/bot/ls-farm.js index 506c02d..090eeda 100644 --- a/src/bot/ls-farm.js +++ b/src/bot/ls-farm.js @@ -361,8 +361,20 @@ export function preisEmbed(client, preise, { jetzt = null, anzahl = 12, sorten = // ── Abrufen und Posten ──────────────────────────────────────────────────── -/** Basis-URL eines LS-Servers aus dem Monitor-Eintrag */ -function feedBasis(server) { +/** + * Wohin die Abfragen gehen. + * + * Bevorzugt die eingetragene Feed-Adresse, sonst aus Host und Port gebaut. + * Fest verdrahtet war das eine Wette darauf, dass jeder Hoster `http` nimmt, + * den Feed unter `/feed` ablegt und keinen Pfad davor hat — drei Annahmen, + * von denen jede einzeln schiefgehen kann und die niemand ändern konnte. + * + * Was hinten dran hängt (`/dedicated-server-stats.xml` und Geschwister), ist + * dagegen von Giants vorgegeben und bleibt im Code. + */ +export function feedBasis(server) { + const eigene = String(server.query_url ?? '').trim(); + if (/^https?:\/\/.+/i.test(eigene)) return eigene.replace(/\/+$/, ''); const port = server.port || 8080; return `http://${server.host}:${port}/feed`; } diff --git a/src/modules.js b/src/modules.js index b7d1800..74b344c 100644 --- a/src/modules.js +++ b/src/modules.js @@ -432,6 +432,43 @@ export function moduleDetail(id) { }; } +/** + * Alle Einstellungs-Schlüssel aus der Modulliste, nach Art sortiert. + * + * Der Speicher-Handler hatte für jede Art eine von Hand gepflegte Liste. Wer + * ein Modul ergänzte und diese Listen nicht mitpflegte, bekam ein Feld, das + * sich bedienen liess und beim Speichern stillschweigend verfiel — ohne + * Fehlermeldung, weil unbekannte Schlüssel einfach durchfielen. Genau das ist + * beim LS-Modul passiert. + * + * Bei `fields` sagt `art`, worum es geht. Bei `requires` steht das nicht dabei + * — dort stehen Kanäle neben Rollen, dem Gitea-Token und der Adressliste des + * Wächters. Deshalb wird dort nur übernommen, was der Namenskonvention folgt: + * `…_channel_id` ist ein Kanal, `…_role_id` eine Rolle. Alles andere bleibt + * aussen vor und wird weiter von seinem eigenen Zweig behandelt. Ein Token in + * die Kanal-Prüfung zu schicken hiesse „Kanal nicht gefunden" beim Speichern. + * + * @returns {{kanal: string[], rolle: string[], text: string[], farbe: string[], zahl: string[], schalter: string[]}} + */ +export function moduleSettingKeys() { + const raus = { kanal: [], rolle: [], text: [], farbe: [], zahl: [], schalter: [] }; + for (const mod of MODULES) { + for (const r of mod.requires ?? []) { + if (!r.setting) continue; + if (r.setting.endsWith('_channel_id')) raus.kanal.push(r.setting); + else if (r.setting.endsWith('_role_id')) raus.rolle.push(r.setting); + } + for (const f of mod.fields ?? []) { + const art = f.art ?? 'text'; + if (f.setting && raus[art]) raus[art].push(f.setting); + } + // Der Modul-Schalter selbst ist auch eine Einstellung + if (mod.setting) raus.schalter.push(mod.setting); + } + for (const art of Object.keys(raus)) raus[art] = [...new Set(raus[art])]; + return raus; +} + /** Alle Module mit Zustand — für das Webinterface */ export function moduleStates() { return MODULES.map((mod) => ({ diff --git a/src/web/api.js b/src/web/api.js index b4d09b8..3843bb2 100644 --- a/src/web/api.js +++ b/src/web/api.js @@ -29,7 +29,7 @@ import { savePage, getPage, listPages, listPublishedPages, deletePage, } from '../db.js'; import crypto from 'node:crypto'; -import { moduleStates, moduleDetail, setModuleEnabled, moduleEnabled, moduleFavorites, setModuleFavorites, 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 { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js'; import { lastResults, GAME_ICONS, spielName, spielListe } from '../bot/server-monitor.js'; @@ -1005,14 +1005,20 @@ ${rssItems} ]) delete body[key]; } + // Was ein Modul an Feldern mitbringt, ist im Register beschrieben — + // von dort geholt statt hier nochmal aufgezählt. Ein Feld, das im + // Panel steht, aber nicht in dieser Datei, verfiel sonst stumm. + const ausRegister = moduleSettingKeys(); + // Kanäle: müssen existierende Textkanäle sein (optionale dürfen leer sein = aus) - const OPTIONAL_CHANNELS = [ + const OPTIONAL_CHANNELS = [...new Set([ 'release_channel_id', 'backup_channel_id', 'starboard_channel_id', 'screenshot_channel_id', 'modmail_channel_id', 'welcome_channel_id', 'modlog_channel_id', 'status_channel_id', 'voting_channel_id', 'ticket_channel_id', 'events_announce_channel_id', 'social_announce_channel_id', 'server_alert_channel_id', 'birthday_channel_id', - ]; + ...ausRegister.kanal, + ])]; // Auch Devlog- und Commit-Kanal dürfen leer sein: leer heißt „noch nicht // eingerichtet", und der Bot kommt damit klar. Vorher scheiterte das // Speichern JEDER Einstellung, solange einer der beiden nicht gesetzt war @@ -1038,7 +1044,10 @@ ${rssItems} setSetting(key, value); } // Rollen: müssen existieren ('' = Feature aus) - for (const key of ['devlog_ping_role_id', 'playtester_role_id', 'autorole_id', 'birthday_role_id']) { + for (const key of [...new Set([ + 'devlog_ping_role_id', 'playtester_role_id', 'autorole_id', 'birthday_role_id', + ...ausRegister.rolle, + ])]) { if (body[key] === undefined) continue; const value = String(body[key] ?? ''); // null = keine Rolle, siehe oben if (unchanged(key, value)) continue; // unverändert → nicht prüfen @@ -1195,7 +1204,13 @@ ${rssItems} setSetting(key, String(body[key]).trim()); } } - for (const key of ['commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls', 'roadmap_repo']) { + // Freitext-Felder. Die Modul-Felder kommen aus dem Register dazu, sonst + // muss jedes neue Modul auch hier eingetragen werden — und wer das + // vergisst, merkt es erst, wenn das Feld beim Speichern verschwindet. + for (const key of [...new Set([ + 'commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls', 'roadmap_repo', + ...ausRegister.text, + ])]) { if (body[key] !== undefined) { setSetting(key, String(body[key]).trim()); }