Modul-Felder speichern wieder, und die Feed-Adresse ist einstellbar
Die LS-Kanaele liessen sich im Panel waehlen und waren nach dem Speichern wieder leer. Der Grund: PUT /api/settings fuehrt fuer jede Feldart eine von Hand gepflegte Liste erlaubter Schluessel, und ein Schluessel, der nirgends steht, faellt still durch. Keine Fehlermeldung, kein Log — das Feld sieht funktionsfaehig aus und vergisst. Das ist keine Einzelheit, sondern eine Falle fuer jedes kuenftige Modul: wer eins ergaenzt und diese Listen nicht mitpflegt, baut denselben Fehler nach. Die Modulliste weiss laengst, welche Felder es gibt und welcher Art sie sind — also holt der Handler sie jetzt von dort, statt sie ein zweites Mal aufzuzaehlen. Beim Bauen des Helfers ware ich fast in die naechste Falle gelaufen: ich hatte alle requires-Eintraege als Kanaele eingestuft. Dort stehen aber auch gitea_api_token und watchdog_urls, und die haetten dann die Kanal-Pruefung durchlaufen — Speichern waere mit "Kanal nicht gefunden" gescheitert, sobald jemand seinen Gitea-Token aendert. Uebernommen wird jetzt nur, was der Namenskonvention folgt. Dazu die fest verdrahtete Feed-Adresse. http://host:port/feed war eine Wette auf drei Annahmen gleichzeitig: dass jeder Hoster http nimmt, den Feed unter /feed ablegt und keinen Pfad davorsetzt. Aendern konnte das niemand. Jetzt gibt es ein Feld dafuer, und wer die ganze Abfrage-URL ins Host-Feld einfuegt, bekommt es mitsamt Schema und Pfad automatisch ausgefuellt. Was dahinter kommt (dedicated-server-stats.xml und Geschwister) gibt Giants vor und bleibt im Code. Geprueft: jedes Feld jedes Moduls landet in einem Eimer — der Test faellt kuenftig aus, sobald jemand ein Feld ergaenzt, das nicht speicherbar waere. Dazu, dass nichts faelschlich in die Kanal- oder Rollenpruefung geraet, keine Dubletten, und die Feed-Basis mit eigenem Pfad, https, ueberzaehligen Schraegstrichen sowie sechs Sorten Unbrauchbarem. Das Einfuegen einer https-URL mit Pfad im laufenden Panel angesehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 })}
|
||||
/>
|
||||
<label style={{ marginTop: '.6rem' }}>
|
||||
Feed-Basis <span className="field-hint">optional — leer heißt <code>http://host:port/feed</code>. Nur setzen, wenn dein Hoster https nimmt oder einen Pfad davorsetzt.</span>
|
||||
</label>
|
||||
<input
|
||||
type="text" placeholder={`http://${serverDraft.host || 'host'}:${serverDraft.port || gewaehltesSpiel.port || 8080}/feed`}
|
||||
value={serverDraft.query_url}
|
||||
onChange={(e) => setServerDraft({ ...serverDraft, query_url: e.target.value })}
|
||||
/>
|
||||
<p className="field-hint" style={{ marginTop: '.3rem' }}>
|
||||
{gewaehltesSpiel.id.startsWith('farmingsimulator')
|
||||
? <>Im Spiel heißt er <b>Code</b> und steckt in der Abfrage-URL, die dein Hoster ausgibt: <code>…/feed/dedicated-server-stats.xml?code=<b>DAS_HIER</b></code>. Am einfachsten die ganze URL oben ins Host-Feld einfügen — Host, Port und Code werden herausgezogen.</>
|
||||
|
||||
+14
-2
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
+20
-5
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user