Zwei Nachzügler zu den Modul-Seiten. Die Thread-Archivdauer gilt für Tickets und Modmail, hing aber nur an Tickets — auf der Modmail-Seite fehlte sie also. `module` darf jetzt eine Liste sein; nach außen kommt immer eine, damit niemand zwischen einem und mehreren unterscheiden muss. Und beim Anlegen aus dem Panel wurde nur die Einstellung gesetzt, nicht die Kanalliste nachgeladen. Der frische Kanal stand damit zwar in der Einstellung, fehlte aber in der Auswahl — das Feld sah leer aus, obwohl es gesetzt war. Aufgefallen im Browser-Test: „Einsatzbereit" stand da, die Auswahl daneben war leer. Jetzt holt reloadSettings Kanäle, Rollen und Einstellungen frisch; das gilt für den Einzelknopf, den Assistenten und die Modul-Seiten gleichermaßen. Nachgeprüft: Kanal über die Status-Liste anlegen, dann ohne Neuladen nach Feeds wechseln — dort steht #dev-log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
225 lines
9.3 KiB
JavaScript
225 lines
9.3 KiB
JavaScript
// Stellwerte: alle Zahlen, die vorher fest im Code standen — XP-Beträge,
|
||
// Wartezeiten, Prüf-Intervalle, Obergrenzen.
|
||
//
|
||
// `module` verbindet einen Wert mit seiner Funktion — die Modul-Seite zeigt ihn
|
||
// dann dort, wo er hingehört, statt nur in der flachen Werte-Liste. Ein Wert
|
||
// darf zu mehreren gehören (die Thread-Archivdauer gilt für Tickets UND
|
||
// Modmail); dann steht dort eine Liste.
|
||
//
|
||
// 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', module: 'levels', group: 'level', label: 'XP pro Nachricht — mindestens',
|
||
unit: 'XP', default: 15, min: 1, max: 500,
|
||
},
|
||
{
|
||
id: 'xp_max', module: 'levels', 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', module: 'levels', 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', module: 'triggers', 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', module: 'birthdays', 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', module: 'weekly_recap', 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', module: 'weekly_recap', group: 'zeiten', label: 'Wochen-Rückblick ab',
|
||
unit: 'Uhr', default: 20, min: 0, max: 23,
|
||
},
|
||
|
||
/* ── Prüf-Intervalle ──────────────────────────────── */
|
||
{
|
||
id: 'monitor_interval', module: 'server_monitor', group: 'pruefung', label: 'Game-Server prüfen alle',
|
||
unit: 'Minuten', default: 2, min: 1, max: 120,
|
||
},
|
||
{
|
||
id: 'monitor_timeout', module: 'server_monitor', group: 'pruefung', label: 'Game-Server — Antwort abwarten',
|
||
unit: 'Sekunden', default: 8, min: 2, max: 60,
|
||
},
|
||
{
|
||
id: 'monitor_fails', module: 'server_monitor', 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', module: 'watchdog', group: 'pruefung', label: 'Adressen prüfen alle',
|
||
unit: 'Minuten', default: 2, min: 1, max: 120,
|
||
},
|
||
{
|
||
id: 'watchdog_timeout', module: 'watchdog', group: 'pruefung', label: 'Adressen — Antwort abwarten',
|
||
unit: 'Sekunden', default: 10, min: 2, max: 60,
|
||
},
|
||
{
|
||
id: 'watchdog_fails', module: 'watchdog', group: 'pruefung', label: 'Adressen — Alarm nach',
|
||
unit: 'Fehlversuchen', default: 2, min: 1, max: 10,
|
||
},
|
||
{
|
||
id: 'social_interval', module: 'social', 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', module: 'birthdays', group: 'pruefung', label: 'Geburtstage prüfen alle',
|
||
unit: 'Minuten', default: 15, min: 1, max: 120,
|
||
},
|
||
|
||
/* ── Obergrenzen ──────────────────────────────────── */
|
||
{
|
||
id: 'ticket_transcript_max', module: 'tickets', 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', module: 'gallery', group: 'grenzen', label: 'Bilder je Galerie-Beitrag',
|
||
unit: 'Bilder', default: 4, min: 1, max: 10,
|
||
},
|
||
{
|
||
id: 'commits_shown', module: 'commit_feed', 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', module: ['tickets', 'modmail'], 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,
|
||
// Immer als Liste nach außen — der Aufrufer muss dann nicht
|
||
// zwischen einem Wert und mehreren unterscheiden
|
||
modules: t.module ? [t.module].flat() : [],
|
||
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) };
|
||
}
|