Werte: 19 feste Zahlen im Panel einstellbar
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:
@@ -5,8 +5,7 @@ import { birthdayChannelId, birthdayRoleId, discordGuildId, brandColor2 } from '
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { renderTemplate } from '../templates.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 15 * 60 * 1000;
|
||||
import { tuning, everyTuned } from '../tuning.js';
|
||||
|
||||
/** Aktuelles Datum/Stunde in Europe/Berlin */
|
||||
function berlinNow() {
|
||||
@@ -20,7 +19,7 @@ function berlinNow() {
|
||||
export async function birthdayTick(client) {
|
||||
if (!moduleEnabled('birthdays')) return;
|
||||
const { day, month, hour, dateKey } = berlinNow();
|
||||
if (hour < 9) return; // erst ab 09:00
|
||||
if (hour < tuning('birthday_hour')) return;
|
||||
if (getSetting('last_birthday_run') === dateKey) return; // heute schon gelaufen
|
||||
setSetting('last_birthday_run', dateKey);
|
||||
|
||||
@@ -66,6 +65,6 @@ export async function birthdayTick(client) {
|
||||
}
|
||||
|
||||
export function startBirthdays(client) {
|
||||
setInterval(() => birthdayTick(client).catch((e) => console.error('[birthday]', e)), CHECK_INTERVAL_MS);
|
||||
everyTuned('birthday_interval', 'minutes', () => birthdayTick(client), 'birthday');
|
||||
birthdayTick(client).catch(() => {});
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ import { Client, Collection, Events, GatewayIntentBits, MessageFlags, Partials,
|
||||
import { config } from '../config.js';
|
||||
import { devlogChannelId, devlogPingRoleId, playtesterRoleId, ticketChannelId, brandColor, brandFooter, discordGuildId } from '../runtime-settings.js';
|
||||
import { renderTemplate } from '../templates.js';
|
||||
import { tuning } from '../tuning.js';
|
||||
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
||||
import { registerCommunityListeners } from './community.js';
|
||||
import { registerModTools } from './mod-tools.js';
|
||||
@@ -210,7 +211,7 @@ export async function startBot() {
|
||||
name: `🎫 ${interaction.user.username}`,
|
||||
type: ChannelType.PrivateThread,
|
||||
invitable: false,
|
||||
autoArchiveDuration: 10080,
|
||||
autoArchiveDuration: tuning('thread_archive_days') * 1440,
|
||||
});
|
||||
await thread.members.add(interaction.user.id);
|
||||
saveTicket(thread.id, interaction.user.id);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Baut aus einem Gitea-Push ein hübsches Embed und postet es in den Commit-Kanal
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { commitChannelId } from '../runtime-settings.js';
|
||||
import { tuning } from '../tuning.js';
|
||||
|
||||
const GITEA_GREEN = 0x609926;
|
||||
const MAX_COMMITS_SHOWN = 10;
|
||||
|
||||
/** Erste Zeile der Commit-Message, auf maxLen gekürzt */
|
||||
function firstLine(message, maxLen = 72) {
|
||||
@@ -29,11 +29,12 @@ export async function postPushEmbed(client, push) {
|
||||
}
|
||||
|
||||
const count = push.commits.length;
|
||||
const lines = push.commits.slice(0, MAX_COMMITS_SHOWN).map(
|
||||
const maxShown = tuning('commits_shown');
|
||||
const lines = push.commits.slice(0, maxShown).map(
|
||||
(c) => `[\`${c.sha.slice(0, 7)}\`](${c.url}) ${firstLine(c.message)} — ${c.author_name}`
|
||||
);
|
||||
if (count > MAX_COMMITS_SHOWN) {
|
||||
lines.push(`… und ${count - MAX_COMMITS_SHOWN} weitere`);
|
||||
if (count > maxShown) {
|
||||
lines.push(`… und ${count - maxShown} weitere`);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
} from '../db.js';
|
||||
import { starboardChannelId, starboardThreshold, screenshotChannelId, brandColor } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { tuning } from '../tuning.js';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const MAX_GALLERY_IMAGES = 4;
|
||||
|
||||
// Galerie-Bilder liegen neben der SQLite: data/gallery/
|
||||
export const galleryDir = join(dirname(resolve(config.dbPath)), 'gallery');
|
||||
@@ -29,7 +29,7 @@ export async function archiveGalleryMessage(message) {
|
||||
|
||||
const files = [];
|
||||
for (const att of message.attachments?.values?.() ?? []) {
|
||||
if (files.length >= MAX_GALLERY_IMAGES) break;
|
||||
if (files.length >= tuning('gallery_max_images')) break;
|
||||
if (!att.contentType?.startsWith('image/')) continue;
|
||||
const ext = (att.name?.split('.').pop() || 'png').toLowerCase().replace(/[^a-z0-9]/g, '') || 'png';
|
||||
const file = `${message.id}_${files.length}.${ext}`;
|
||||
|
||||
+2
-2
@@ -13,11 +13,11 @@ import {
|
||||
} from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { renderTemplate } from '../templates.js';
|
||||
import { tuningMs } from '../tuning.js';
|
||||
|
||||
/* ── Triggers ──────────────────────────────────────── */
|
||||
|
||||
const triggerCooldown = new Map(); // `${channelId}:${keyword}` → ts
|
||||
const TRIGGER_COOLDOWN_MS = 30_000;
|
||||
|
||||
async function handleTriggers(message) {
|
||||
if (!moduleEnabled('triggers')) return;
|
||||
@@ -26,7 +26,7 @@ async function handleTriggers(message) {
|
||||
for (const t of listTriggers()) {
|
||||
if (!content.includes(t.keyword.toLowerCase())) continue;
|
||||
const key = `${message.channelId}:${t.keyword}`;
|
||||
if (Date.now() - (triggerCooldown.get(key) ?? 0) < TRIGGER_COOLDOWN_MS) continue;
|
||||
if (Date.now() - (triggerCooldown.get(key) ?? 0) < tuningMs.seconds('trigger_cooldown')) continue;
|
||||
triggerCooldown.set(key, Date.now());
|
||||
await message.reply({ content: t.reply.slice(0, 2000), allowedMentions: { parse: [] } }).catch(() => {});
|
||||
break; // max. ein Trigger pro Nachricht
|
||||
|
||||
+4
-6
@@ -4,10 +4,7 @@ import { Events } from 'discord.js';
|
||||
import { getLevelRow, setLevelRow } from '../db.js';
|
||||
import { levelsEnabled, levelsAnnounce, levelRewards } from '../runtime-settings.js';
|
||||
import { renderTemplate } from '../templates.js';
|
||||
|
||||
const XP_COOLDOWN_MS = 60_000;
|
||||
const XP_MIN = 15;
|
||||
const XP_SPREAD = 11; // 15–25 XP pro gewerteter Nachricht
|
||||
import { tuningMs, xpRange } from '../tuning.js';
|
||||
|
||||
/** Kumulierte XP-Schwelle, ab der ein Level erreicht ist (MEE6-Formel pro Stufe) */
|
||||
export function xpForLevel(level) {
|
||||
@@ -28,9 +25,10 @@ export function levelFromXp(xp) {
|
||||
*/
|
||||
export function awardXp(userId, username, now = Date.now()) {
|
||||
const row = getLevelRow(userId) ?? { user_id: userId, username, xp: 0, level: 0, last_xp_ms: 0 };
|
||||
if (now - row.last_xp_ms < XP_COOLDOWN_MS) return null;
|
||||
if (now - row.last_xp_ms < tuningMs.seconds('xp_cooldown')) return null;
|
||||
|
||||
const xp = row.xp + XP_MIN + Math.floor(Math.random() * XP_SPREAD);
|
||||
const { min, max } = xpRange();
|
||||
const xp = row.xp + min + Math.floor(Math.random() * (max - min + 1));
|
||||
const level = levelFromXp(xp);
|
||||
const leveledUp = level > row.level;
|
||||
setLevelRow({ user_id: userId, username, xp, level, last_xp_ms: now });
|
||||
|
||||
@@ -4,6 +4,7 @@ import { saveModmail, modmailByUser, modmailByThread, saveStickyRoles, stickyRol
|
||||
import { modmailChannelId, welcomeChannelId, modlogChannelId, publicUrl, autoroleId, stickyRolesEnabled, brandColor, brandColor2, brandFooter, welcomeCard } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { renderTemplate } from '../templates.js';
|
||||
import { tuning } from '../tuning.js';
|
||||
|
||||
|
||||
/* ── Modmail ───────────────────────────────────────── */
|
||||
@@ -26,7 +27,7 @@ async function ensureModmailThread(client, user) {
|
||||
|
||||
const thread = await channel.threads.create({
|
||||
name: `📬 ${user.username}`,
|
||||
autoArchiveDuration: 10080, // 7 Tage
|
||||
autoArchiveDuration: tuning('thread_archive_days') * 1440,
|
||||
type: ChannelType.PublicThread,
|
||||
});
|
||||
saveModmail(user.id, thread.id, user.username);
|
||||
|
||||
@@ -7,10 +7,8 @@ import { getSetting, listGameservers, setGameserverMessage, recordPlayerSample,
|
||||
import { brandFooter, botStatusText } from '../runtime-settings.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const TIMEOUT_MS = 8000;
|
||||
const FAILS_BEFORE_ALERT = 2;
|
||||
const GREEN = 0x23a55a;
|
||||
const RED = 0xf23f43;
|
||||
|
||||
@@ -39,7 +37,7 @@ export async function queryServer(server) {
|
||||
try {
|
||||
if (server.type === 'fivem') {
|
||||
const res = await fetch(`${server.query_url.replace(/\/$/, '')}/dynamic.json`, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
signal: AbortSignal.timeout(tuningMs.seconds('monitor_timeout')),
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/3.0' },
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
@@ -53,7 +51,7 @@ export async function queryServer(server) {
|
||||
}
|
||||
if (server.type === 'http') {
|
||||
const res = await fetch(server.query_url, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
signal: AbortSignal.timeout(tuningMs.seconds('monitor_timeout')),
|
||||
redirect: 'follow',
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/3.0' },
|
||||
});
|
||||
@@ -65,7 +63,7 @@ export async function queryServer(server) {
|
||||
host: server.host,
|
||||
port: server.port || undefined,
|
||||
maxRetries: 1,
|
||||
socketTimeout: TIMEOUT_MS,
|
||||
socketTimeout: tuningMs.seconds('monitor_timeout'),
|
||||
});
|
||||
return {
|
||||
...base, online: true, ping: state.ping ?? Date.now() - started,
|
||||
@@ -137,7 +135,7 @@ async function handleAlerts(client, r) {
|
||||
return;
|
||||
}
|
||||
const fails = s.fails + 1;
|
||||
if (!s.down && fails >= FAILS_BEFORE_ALERT) {
|
||||
if (!s.down && fails >= tuning('monitor_fails')) {
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
await channel?.send({
|
||||
embeds: [
|
||||
@@ -207,6 +205,6 @@ export async function monitorTick(client) {
|
||||
}
|
||||
|
||||
export function startServerMonitor(client) {
|
||||
setInterval(() => monitorTick(client).catch((e) => console.error('[monitor] Fehler:', e)), CHECK_INTERVAL_MS);
|
||||
everyTuned('monitor_interval', 'minutes', () => monitorTick(client), 'monitor');
|
||||
client.once(Events.ClientReady, () => monitorTick(client).catch(() => {}));
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
brandColor, brandColor2, brandFooter,
|
||||
} from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { everyTuned } from '../tuning.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
let twitchToken = null; // { token, expiresAt }
|
||||
|
||||
async function announce(client, embed) {
|
||||
@@ -102,5 +102,5 @@ export function startSocialNotify(client) {
|
||||
try { await checkYouTube(client); } catch (e) { console.error('[social] YouTube:', e.message); }
|
||||
try { await checkTwitch(client); } catch (e) { console.error('[social] Twitch:', e.message); }
|
||||
};
|
||||
setInterval(tick, CHECK_INTERVAL_MS);
|
||||
everyTuned('social_interval', 'minutes', tick, 'social');
|
||||
}
|
||||
|
||||
+3
-2
@@ -4,14 +4,15 @@ import { AttachmentBuilder, EmbedBuilder, MessageFlags } from 'discord.js';
|
||||
import { getTicket, closeTicket } from '../db.js';
|
||||
import { modlogChannelId, brandColor, brandFooter } from '../runtime-settings.js';
|
||||
import { renderTemplate } from '../templates.js';
|
||||
import { tuning } from '../tuning.js';
|
||||
|
||||
const MAX_MESSAGES = 500;
|
||||
|
||||
/** Alle Thread-Nachrichten chronologisch als Text-Transcript */
|
||||
async function fetchTranscript(channel) {
|
||||
const all = [];
|
||||
let before;
|
||||
while (all.length < MAX_MESSAGES) {
|
||||
const maxMessages = tuning('ticket_transcript_max');
|
||||
while (all.length < maxMessages) {
|
||||
const batch = await channel.messages.fetch({ limit: 100, before });
|
||||
if (batch.size === 0) break;
|
||||
all.push(...batch.values());
|
||||
|
||||
+4
-7
@@ -3,10 +3,7 @@
|
||||
import { getSetting } from '../db.js';
|
||||
import { config } from '../config.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const FAILS_BEFORE_ALERT = 2; // 2 Fehlschläge in Folge → Alarm (gegen Flattern)
|
||||
const TIMEOUT_MS = 10_000;
|
||||
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
||||
|
||||
// URL → { fails, down, since }
|
||||
const state = new Map();
|
||||
@@ -24,7 +21,7 @@ async function dmAdmin(client, content) {
|
||||
}
|
||||
|
||||
/** Ein Prüfdurchlauf — exportiert für Tests und den Intervall-Timer */
|
||||
export async function watchdogTick(client, { failsBeforeAlert = FAILS_BEFORE_ALERT } = {}) {
|
||||
export async function watchdogTick(client, { failsBeforeAlert = tuning('watchdog_fails') } = {}) {
|
||||
if (!moduleEnabled('watchdog')) return;
|
||||
for (const url of watchedUrls()) {
|
||||
const s = state.get(url) ?? { fails: 0, down: false, since: null };
|
||||
@@ -35,7 +32,7 @@ export async function watchdogTick(client, { failsBeforeAlert = FAILS_BEFORE_ALE
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
redirect: 'follow',
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
signal: AbortSignal.timeout(tuningMs.seconds('watchdog_timeout')),
|
||||
headers: { 'User-Agent': 'd4rkbot-watchdog/1.0' },
|
||||
});
|
||||
ok = res.status < 500;
|
||||
@@ -65,5 +62,5 @@ export async function watchdogTick(client, { failsBeforeAlert = FAILS_BEFORE_ALE
|
||||
}
|
||||
|
||||
export function startWatchdog(client) {
|
||||
setInterval(() => watchdogTick(client), CHECK_INTERVAL_MS);
|
||||
everyTuned('watchdog_interval', 'minutes', () => watchdogTick(client), 'watchdog');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { weeklyStats, getSetting, setSetting } from '../db.js';
|
||||
import { devlogChannelId, publicUrl, brandColor, brandFooter } from '../runtime-settings.js';
|
||||
import { config } from '../config.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { tuning } from '../tuning.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
@@ -70,7 +71,8 @@ export function scheduleWeeklyRecap(client) {
|
||||
if (getSetting('weekly_recap_enabled') === '0') return;
|
||||
|
||||
const now = new Date();
|
||||
if (now.getDay() !== 0 || now.getHours() < 20) return; // Sonntag ab 20:00 (lokale TZ)
|
||||
// Wochentag und Uhrzeit kommen aus den Stellwerten (lokale TZ des Containers)
|
||||
if (now.getDay() !== tuning('recap_weekday') || now.getHours() < tuning('recap_hour')) return;
|
||||
|
||||
const today = now.toISOString().slice(0, 10);
|
||||
if (getSetting('last_weekly_recap') === today) return;
|
||||
|
||||
+215
@@ -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) };
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import crypto from 'node:crypto';
|
||||
import { moduleStates, setModuleEnabled, MODULE_GROUPS, MODULES } from '../modules.js';
|
||||
import { templateStates, setTemplate, TEMPLATE_GROUPS } from '../templates.js';
|
||||
import { tuningStates, setTuning, TUNING_GROUPS } from '../tuning.js';
|
||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||
import { sanitizeEmbed } from './api-v1.js';
|
||||
@@ -185,6 +186,31 @@ export function registerApiRoutes(app, client) {
|
||||
return { ok: true, modules: moduleStates() };
|
||||
});
|
||||
|
||||
// --- Stellwerte: Zahlen, die vorher fest im Code standen ---
|
||||
|
||||
app.get('/api/tuning', async (request, reply) => {
|
||||
if (requireAnyScope(request, reply)) return;
|
||||
return { values: tuningStates(), groups: TUNING_GROUPS };
|
||||
});
|
||||
|
||||
app.put('/api/tuning', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'settings')) return;
|
||||
const body = request.body ?? {};
|
||||
// Erst alles prüfen, dann schreiben — sonst bleibt bei einem Fehler
|
||||
// die Hälfte gesetzt und die andere nicht.
|
||||
const pending = [];
|
||||
for (const [id, value] of Object.entries(body)) {
|
||||
const check = setTuning(id, value, { dryRun: true });
|
||||
if (!check.ok) return reply.code(400).send({ error: check.error });
|
||||
pending.push([id, value]);
|
||||
}
|
||||
for (const [id, value] of pending) setTuning(id, value);
|
||||
if (pending.length > 0) {
|
||||
logAudit(getSessionUser(request), 'werte geändert', pending.map(([id]) => id).join(', '));
|
||||
}
|
||||
return { ok: true, values: tuningStates() };
|
||||
});
|
||||
|
||||
// Vorschau der Willkommens-Karte. Nimmt das Aussehen als Query entgegen,
|
||||
// damit man im Panel sieht, was man einstellt, bevor gespeichert wird.
|
||||
app.get('/api/welcome-preview.png', async (request, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user