Werte: 19 feste Zahlen im Panel einstellbar
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

XP-Beträge, Wartezeiten, Prüf-Intervalle, Obergrenzen und Postzeiten
standen als Konstanten im Code. Wer den Server-Monitor seltener prüfen
lassen wollte oder das Level-System anders austarieren, musste die
Quelldateien anfassen.

src/tuning.js hält sie jetzt an einer Stelle, mit Standard, erlaubtem
Bereich und Erklärung. Der neue Config-Bereich „Werte" zeigt sie nach
Themen gruppiert; neben jedem Feld steht der erlaubte Bereich und der
Standard, angepasste Werte lassen sich einzeln zurücksetzen.

Die Grenzen sind nicht Kosmetik: ein Vertipper beim Prüf-Intervall würde
sonst den Bot in eine Schleife im Sekundentakt schicken. Gespeichert wird
ganz oder gar nicht — ein ungültiger Wert im Formular lässt auch die
gültigen daneben unverändert, statt die Hälfte zu schreiben.

Intervalle laufen nicht mehr über setInterval mit festem Abstand, sondern
über everyTuned: der Wert wird vor jeder Runde neu gelesen. Ein geändertes
Intervall greift damit ab dem nächsten Durchlauf, ohne Neustart. Ein
Fehler in einer Runde beendet die Schleife nicht.

Beim Testen aufgefallen und behoben: Number('') ist 0 und nicht NaN — ein
nie gesetzter Wert wäre dadurch auf sein Minimum gefallen statt auf den
Standard. Das Level-System hätte also 1 XP pro Nachricht vergeben und der
Monitor jede Minute geprüft.

Der Fetch-Wrapper im Frontend hat die Fehlermeldung des Servers verworfen;
jetzt steht im Panel „XP pro Nachricht: 1–500 XP" statt „fehlgeschlagen".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 16:49:34 +02:00
co-authored by Claude Fable 5
parent fb927d4399
commit 197c2a1ada
17 changed files with 453 additions and 41 deletions
+215
View File
@@ -0,0 +1,215 @@
// Stellwerte: alle Zahlen, die vorher fest im Code standen — XP-Beträge,
// Wartezeiten, Prüf-Intervalle, Obergrenzen.
//
// Wie bei den Modulen und Vorlagen lebt der Standard im Code und die
// Datenbank enthält nur Abweichungen. `min`/`max` sind keine Kosmetik: sie
// verhindern, dass ein Vertipper den Bot in eine Prüfschleife im
// Sekundentakt schickt oder das Level-System unbrauchbar macht.
import { getSetting, setSetting } from './db.js';
export const TUNING_GROUPS = [
{ id: 'level', label: 'Level & Aktivität' },
{ id: 'zeiten', label: 'Wann der Bot postet' },
{ id: 'pruefung', label: 'Prüf-Intervalle' },
{ id: 'grenzen', label: 'Obergrenzen' },
];
export const TUNING = [
/* ── Level & Aktivität ────────────────────────────── */
{
id: 'xp_min', group: 'level', label: 'XP pro Nachricht — mindestens',
unit: 'XP', default: 15, min: 1, max: 500,
},
{
id: 'xp_max', group: 'level', label: 'XP pro Nachricht — höchstens',
hint: 'Der Bot würfelt zwischen beiden Werten.',
unit: 'XP', default: 25, min: 1, max: 500,
},
{
id: 'xp_cooldown', group: 'level', label: 'Wartezeit zwischen zwei XP-Gutschriften',
hint: 'Verhindert, dass Vielschreiber das Level-System leerlaufen lassen.',
unit: 'Sekunden', default: 60, min: 5, max: 3600,
},
{
id: 'trigger_cooldown', group: 'level', label: 'Wartezeit für Auto-Antworten',
hint: 'Pro Schlüsselwort und Kanal — sonst antwortet der Bot sich in Grund und Boden.',
unit: 'Sekunden', default: 30, min: 5, max: 3600,
},
/* ── Wann der Bot postet ──────────────────────────── */
{
id: 'birthday_hour', group: 'zeiten', label: 'Geburtstags-Gratulation ab',
hint: 'Uhrzeit in Europe/Berlin. Gratuliert wird einmal am Tag.',
unit: 'Uhr', default: 9, min: 0, max: 23,
},
{
id: 'recap_weekday', group: 'zeiten', label: 'Wochen-Rückblick am',
hint: '0 = Sonntag, 1 = Montag … 6 = Samstag.',
unit: 'Wochentag', default: 0, min: 0, max: 6,
},
{
id: 'recap_hour', group: 'zeiten', label: 'Wochen-Rückblick ab',
unit: 'Uhr', default: 20, min: 0, max: 23,
},
/* ── Prüf-Intervalle ──────────────────────────────── */
{
id: 'monitor_interval', group: 'pruefung', label: 'Game-Server prüfen alle',
unit: 'Minuten', default: 2, min: 1, max: 120,
},
{
id: 'monitor_timeout', group: 'pruefung', label: 'Game-Server — Antwort abwarten',
unit: 'Sekunden', default: 8, min: 2, max: 60,
},
{
id: 'monitor_fails', group: 'pruefung', label: 'Game-Server — Alarm nach',
hint: 'Fehlversuchen in Folge. 1 meldet jeden Aussetzer, höher meldet nur echte Ausfälle.',
unit: 'Fehlversuchen', default: 2, min: 1, max: 10,
},
{
id: 'watchdog_interval', group: 'pruefung', label: 'Adressen prüfen alle',
unit: 'Minuten', default: 2, min: 1, max: 120,
},
{
id: 'watchdog_timeout', group: 'pruefung', label: 'Adressen — Antwort abwarten',
unit: 'Sekunden', default: 10, min: 2, max: 60,
},
{
id: 'watchdog_fails', group: 'pruefung', label: 'Adressen — Alarm nach',
unit: 'Fehlversuchen', default: 2, min: 1, max: 10,
},
{
id: 'social_interval', group: 'pruefung', label: 'Twitch & YouTube prüfen alle',
hint: 'Zu kurz bringt nichts — YouTube-Feeds aktualisieren sich ohnehin nur alle paar Minuten.',
unit: 'Minuten', default: 5, min: 1, max: 180,
},
{
id: 'birthday_interval', group: 'pruefung', label: 'Geburtstage prüfen alle',
unit: 'Minuten', default: 15, min: 1, max: 120,
},
/* ── Obergrenzen ──────────────────────────────────── */
{
id: 'ticket_transcript_max', group: 'grenzen', label: 'Nachrichten im Ticket-Protokoll',
hint: 'Ältere fallen weg. Hoch gesetzt dauert das Schließen länger.',
unit: 'Nachrichten', default: 500, min: 50, max: 2000,
},
{
id: 'gallery_max_images', group: 'grenzen', label: 'Bilder je Galerie-Beitrag',
unit: 'Bilder', default: 4, min: 1, max: 10,
},
{
id: 'commits_shown', group: 'grenzen', label: 'Commits je Push-Embed',
hint: 'Der Rest wird als „… und N weitere" zusammengefasst.',
unit: 'Commits', default: 10, min: 1, max: 20,
},
{
id: 'thread_archive_days', group: 'grenzen', label: 'Threads archivieren nach',
hint: 'Für Ticket- und Modmail-Threads. Discord erlaubt 1, 3 oder 7 Tage.',
unit: 'Tagen', default: 7, min: 1, max: 7, choices: [1, 3, 7],
},
];
const byId = new Map(TUNING.map((t) => [t.id, t]));
/**
* Wert lesen — immer eine gültige Zahl im erlaubten Bereich.
* @param {string} id
* @returns {number}
*/
export function tuning(id) {
const def = byId.get(id);
if (!def) return 0;
// Achtung: Number('') ist 0, nicht NaN — ohne diese Prüfung würde ein
// ungesetzter Wert auf das Minimum fallen statt auf den Standard.
const stored = String(getSetting(`tune_${id}`) ?? '').trim();
if (stored === '') return def.default;
const raw = Number(stored);
if (!Number.isFinite(raw)) return def.default;
return Math.min(def.max, Math.max(def.min, Math.round(raw)));
}
/** Derselbe Wert in Millisekunden — spart die Rechnerei an jeder Aufrufstelle */
export const tuningMs = {
seconds: (id) => tuning(id) * 1000,
minutes: (id) => tuning(id) * 60 * 1000,
};
/**
* Wert setzen. Leer stellt den Standard wieder her.
* Mit `dryRun` wird nur geprüft — damit ein Formular mit mehreren Werten
* entweder ganz oder gar nicht gespeichert wird.
* @returns {{ ok: true } | { ok: false, error: string }}
*/
export function setTuning(id, value, { dryRun = false } = {}) {
const def = byId.get(id);
if (!def) return { ok: false, error: `Unbekannter Wert: ${id}` };
if (value === '' || value === null || value === undefined) {
if (!dryRun) setSetting(`tune_${id}`, '');
return { ok: true };
}
const n = Number(value);
if (!Number.isFinite(n) || !Number.isInteger(n)) {
return { ok: false, error: `${def.label}: ganze Zahl erwartet` };
}
if (n < def.min || n > def.max) {
return { ok: false, error: `${def.label}: ${def.min}${def.max} ${def.unit}` };
}
if (def.choices && !def.choices.includes(n)) {
return { ok: false, error: `${def.label}: erlaubt sind ${def.choices.join(', ')}` };
}
if (!dryRun) setSetting(`tune_${id}`, String(n));
return { ok: true };
}
/** Alle Werte mit Zustand — fürs Webinterface */
export function tuningStates() {
return TUNING.map((t) => {
const custom = getSetting(`tune_${t.id}`);
return {
id: t.id, label: t.label, hint: t.hint ?? null, group: t.group,
unit: t.unit, min: t.min, max: t.max,
choices: t.choices ?? null,
default: t.default,
value: tuning(t.id),
customized: Boolean(String(custom ?? '').trim()),
};
});
}
/**
* Wiederkehrende Aufgabe, deren Abstand aus einem Stellwert kommt. Der Wert
* wird vor jeder Runde neu gelesen — ein geändertes Intervall greift damit ab
* dem nächsten Durchlauf, ohne den Bot neu zu starten. (Ein setInterval mit
* festem Abstand könnte das nicht.)
*
* @param {string} id — Stellwert
* @param {'minutes'|'seconds'} unit
* @param {() => unknown} task
* @param {string} tag — Kennung fürs Fehler-Log
* @returns {() => void} Abbrechen
*/
export function everyTuned(id, unit, task, tag = id) {
let timer;
const delay = () => (unit === 'minutes' ? tuningMs.minutes(id) : tuningMs.seconds(id));
const run = async () => {
try {
await task();
} catch (error) {
console.error(`[${tag}]`, error);
}
timer = setTimeout(run, delay());
};
timer = setTimeout(run, delay());
return () => clearTimeout(timer);
}
/**
* XP-Spanne — sorgt dafür, dass Minimum und Maximum nicht vertauscht sind,
* falls jemand das Maximum unter das Minimum setzt.
*/
export function xpRange() {
const a = tuning('xp_min');
const b = tuning('xp_max');
return { min: Math.min(a, b), max: Math.max(a, b) };
}