From 934afc667fcb17213e3b888fcd75b0d8f565bd05 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:50:29 +0200 Subject: [PATCH 1/4] fix(roundup): address pre-merge review findings for v1.5.6 - libraryPruner: run once shortly after boot, not only on a 24h setInterval, so frequent restarts (e.g. config saves) don't skip every prune cycle and let seeded dedup keys expire - seen-items TTL raised to match roundup-first-seen's ~5yr backstop; actual removal for deleted items still happens via the daily prune scan, not TTL expiry - roundupScheduler: rebind to the new Discord client on bot restart instead of ignoring the second start() call, matching the existing daily-pick scheduler pattern - configFile: migrate legacy EMBED_SHOW_OVERVIEW to the new _MOVIES/_EPISODES split so users who disabled it (often to avoid episode spoilers) don't get overviews silently re-enabled - interactions: move the Seerr request error messages (including the new quota message) through locales/ instead of hardcoded English - weeklyRoundup: log the underlying parse error instead of an empty catch block in the JELLYFIN_BASE_URL preflight --- app.js | 11 +++++++++++ bot/interactions.js | 13 +++++++------ bot/roundupScheduler.js | 16 ++++++++++------ bot/weeklyRoundup.js | 4 ++-- jellyfin/libraryResolver.js | 7 ++++++- locales/de.json | 8 ++++++++ locales/en.json | 8 ++++++++ locales/sv.json | 8 ++++++++ locales/template.json | 8 ++++++++ utils/configFile.js | 21 +++++++++++++++++++++ 10 files changed, 89 insertions(+), 15 deletions(-) diff --git a/app.js b/app.js index e41b1e0..6164dc1 100644 --- a/app.js +++ b/app.js @@ -1160,6 +1160,17 @@ function startServer() { }); const LIBRARY_PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000; + const LIBRARY_PRUNE_INITIAL_DELAY_MS = 5 * 60 * 1000; + // setInterval alone only fires after a full 24h of uptime — a container + // that restarts more often than that (e.g. on every config save) would + // otherwise never run a prune cycle at all. Run one shortly after boot too + // so seeded/pruned keys get re-asserted regardless of restart frequency. + setTimeout(() => { + pruneLibrary().catch((err) => + logger.error(`libraryPruner: unexpected rejection in initial prune (${err?.message || err})`) + ); + }, LIBRARY_PRUNE_INITIAL_DELAY_MS); + libraryPruneTimer = setInterval(() => { pruneLibrary().catch((err) => logger.error(`libraryPruner: unexpected rejection in prune cycle (${err?.message || err})`) diff --git a/bot/interactions.js b/bot/interactions.js index bac534d..b4b41a0 100644 --- a/bot/interactions.js +++ b/bot/interactions.js @@ -16,6 +16,7 @@ import { } from "./botState.js"; import { getUserMappings } from "../utils/configFile.js"; import { getSeerrApiUrl } from "../utils/seerrUrl.js"; +import { t } from "../utils/i18n.js"; import logger from "../utils/logger.js"; // Convenience accessors — read process.env at call time so config reloads are respected @@ -29,22 +30,22 @@ function getSeerrErrorMessage(err) { const status = err.response.status; const msg = err.response.data?.message || ""; if (/quota/i.test(msg)) { - return `⚠️ ${msg} You've reached your request limit — contact an admin if you think this is a mistake.`; + return t("seerr_request_errors.quota", { msg }); } if (status === 401 || status === 403) { - return "⚠️ Request failed: authentication error. Check the bot's API key configuration."; + return t("seerr_request_errors.auth"); } if (status >= 500) { - return "⚠️ Seerr returned a server error. Try again later."; + return t("seerr_request_errors.server_error"); } if (msg) { - return `⚠️ Seerr error: ${msg}`; + return t("seerr_request_errors.generic", { msg }); } } if (err.code === "ECONNREFUSED" || err.code === "ETIMEDOUT" || err.code === "ENOTFOUND") { - return "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable."; + return t("seerr_request_errors.unreachable"); } - return `⚠️ An error occurred: ${err.message || "unknown error"}`; + return t("seerr_request_errors.unknown", { msg: err.message || "unknown error" }); } // ----------------- COMMON SEARCH LOGIC ----------------- diff --git a/bot/roundupScheduler.js b/bot/roundupScheduler.js index 9ed6cf4..4cbf393 100644 --- a/bot/roundupScheduler.js +++ b/bot/roundupScheduler.js @@ -15,7 +15,7 @@ const TICK_OFFSET_SECONDS = 5; const WEEKDAY_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -let started = false; +let pendingTimer = null; function parseIntInRange(raw, fallback, min, max) { const n = parseInt(raw, 10); @@ -96,11 +96,15 @@ async function runTick(client, now = new Date()) { } export function start(client) { - if (started) { - logger.warn("Roundup scheduler start() called twice; ignoring second call"); - return; + // Restarting the bot from the dashboard destroys the old Discord client + // and constructs a new one (see bot/botManager.js), so this must rebind + // to the fresh client rather than ignore the second call — otherwise the + // scheduler keeps ticking against a destroyed client until every tick + // fails and the failure circuit permanently opens for the week. + if (pendingTimer) { + clearTimeout(pendingTimer); + pendingTimer = null; } - started = true; const installedAt = getInstalledAt(); const now = new Date(); @@ -141,7 +145,7 @@ export function start(client) { // the next hour. setInterval(HOUR_MS) drifts off the boundary across DST // transitions and could push a post a full hour late. const scheduleNext = () => { - setTimeout(() => { + pendingTimer = setTimeout(() => { runTick(client) .catch((err) => logger.error(`Weekly Roundup tick crash: ${err?.message || err}`) diff --git a/bot/weeklyRoundup.js b/bot/weeklyRoundup.js index 0a429ba..423cda4 100644 --- a/bot/weeklyRoundup.js +++ b/bot/weeklyRoundup.js @@ -367,8 +367,8 @@ export async function sendWeeklyRoundup(client, channelId, now, options = {}) { // schemes are allowed, never file:/gopher:/etc. baseUrlOk = parsed.protocol === "http:" || parsed.protocol === "https:"; } - } catch { - /* fall through */ + } catch (err) { + logger.debug(`${logPrefix}: JELLYFIN_BASE_URL failed to parse as URL: ${err?.message}`); } if (!baseUrlOk) { const msg = `JELLYFIN_BASE_URL is missing or not a valid http(s) URL ("${baseUrl ?? ""}")`; diff --git a/jellyfin/libraryResolver.js b/jellyfin/libraryResolver.js index 7f051ae..9639f91 100644 --- a/jellyfin/libraryResolver.js +++ b/jellyfin/libraryResolver.js @@ -2,7 +2,12 @@ import * as jellyfinApi from "../api/jellyfin.js"; import logger from "../utils/logger.js"; import { PersistentMap } from "../utils/persistentMap.js"; -const SEEN_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days — survive Sonarr/Radarr upgrade cycles +// Also backs the library seed's permanent "already existed" markers (see +// jellyfin/librarySeeder.js), which must outlive Sonarr/Radarr upgrade +// cycles indefinitely, not just 7 days. Real removal for deleted items +// happens via the daily libraryPruner.js scan, not TTL expiry — this is a +// backstop TTL, kept in step with roundup-first-seen's. +const SEEN_THRESHOLD_MS = 5 * 365 * 24 * 60 * 60 * 1000; /** * Fetches all libraries from Jellyfin and returns the library array, diff --git a/locales/de.json b/locales/de.json index 8fc7acd..3a3b5d0 100644 --- a/locales/de.json +++ b/locales/de.json @@ -473,5 +473,13 @@ "section_series": "Serien", "field_continued": "(Forts.)", "unknown_title": "Unbekannt" + }, + "seerr_request_errors": { + "quota": "⚠️ {msg} Du hast dein Anfragelimit erreicht — kontaktiere einen Admin, falls das ein Fehler ist.", + "auth": "⚠️ Anfrage fehlgeschlagen: Authentifizierungsfehler. Prüfe die API-Key-Konfiguration des Bots.", + "server_error": "⚠️ Seerr hat einen Serverfehler zurückgegeben. Versuche es später erneut.", + "generic": "⚠️ Seerr-Fehler: {msg}", + "unreachable": "⚠️ Seerr konnte nicht erreicht werden. Prüfe, ob die Seerr-URL korrekt und erreichbar ist.", + "unknown": "⚠️ Ein Fehler ist aufgetreten: {msg}" } } diff --git a/locales/en.json b/locales/en.json index bb6b629..d593d4e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -404,5 +404,13 @@ "section_series": "Series", "field_continued": "(cont.)", "unknown_title": "Unknown" + }, + "seerr_request_errors": { + "quota": "⚠️ {msg} You've reached your request limit — contact an admin if you think this is a mistake.", + "auth": "⚠️ Request failed: authentication error. Check the bot's API key configuration.", + "server_error": "⚠️ Seerr returned a server error. Try again later.", + "generic": "⚠️ Seerr error: {msg}", + "unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.", + "unknown": "⚠️ An error occurred: {msg}" } } diff --git a/locales/sv.json b/locales/sv.json index 088ea5c..f5b95e3 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -403,5 +403,13 @@ "section_series": "Serier", "field_continued": "(forts.)", "unknown_title": "Okänd" + }, + "seerr_request_errors": { + "quota": "⚠️ {msg} Du har nått din förfrågningsgräns — kontakta en admin om detta verkar fel.", + "auth": "⚠️ Förfrågan misslyckades: autentiseringsfel. Kontrollera botens API-nyckelkonfiguration.", + "server_error": "⚠️ Seerr returnerade ett serverfel. Försök igen senare.", + "generic": "⚠️ Seerr-fel: {msg}", + "unreachable": "⚠️ Kunde inte nå Seerr. Kontrollera att Seerr-URL:en är korrekt och nåbar.", + "unknown": "⚠️ Ett fel uppstod: {msg}" } } diff --git a/locales/template.json b/locales/template.json index 95a1e31..b170705 100644 --- a/locales/template.json +++ b/locales/template.json @@ -404,5 +404,13 @@ "section_series": "Series", "field_continued": "(cont.)", "unknown_title": "Unknown" + }, + "seerr_request_errors": { + "quota": "⚠️ {msg} You've reached your request limit — contact an admin if you think this is a mistake.", + "auth": "⚠️ Request failed: authentication error. Check the bot's API key configuration.", + "server_error": "⚠️ Seerr returned a server error. Try again later.", + "generic": "⚠️ Seerr error: {msg}", + "unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.", + "unknown": "⚠️ An error occurred: {msg}" } } diff --git a/utils/configFile.js b/utils/configFile.js index 1d5b053..0701b2c 100644 --- a/utils/configFile.js +++ b/utils/configFile.js @@ -362,6 +362,27 @@ export function loadConfigToEnv() { } } + // 5. Migrate old single EMBED_SHOW_OVERVIEW flag to the new split + // movies/episodes settings, so users who had it disabled (often for + // episode spoilers) don't silently get overviews turned back on. + if ( + config.EMBED_SHOW_OVERVIEW !== undefined && + config.EMBED_SHOW_OVERVIEW_MOVIES === undefined && + config.EMBED_SHOW_OVERVIEW_EPISODES === undefined + ) { + config.EMBED_SHOW_OVERVIEW_MOVIES = config.EMBED_SHOW_OVERVIEW; + config.EMBED_SHOW_OVERVIEW_EPISODES = config.EMBED_SHOW_OVERVIEW; + delete config.EMBED_SHOW_OVERVIEW; + logger.info( + "🔄 Migrated EMBED_SHOW_OVERVIEW → EMBED_SHOW_OVERVIEW_MOVIES/EMBED_SHOW_OVERVIEW_EPISODES" + ); + if (writeConfig(config)) { + logger.info("✅ Embed overview migration saved to config.json"); + } else { + logger.error("❌ Failed to save embed overview migration"); + } + } + // --- LOAD INTO PROCESS.ENV --- for (const [key, value] of Object.entries(config)) { // Convert objects/arrays to JSON strings to avoid "[object Object]" From 06045b8aa71d9b5ed21cc33f0759b8fe1f5b720a Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:55:22 +0200 Subject: [PATCH 2/4] fix(roundup): close scheduler race, add fr.json keys, fix changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - roundupScheduler: clearTimeout alone doesn't stop a chain whose tick already fired and is in flight when start() is called again on restart — its .finally(scheduleNext) would still re-arm using the stale client/closure. Add a generation token so a superseded chain becomes a no-op instead of quietly ticking against the destroyed client. - i18n: t() now falls back to the English value for a key missing in the active locale (instead of leaking the raw key string into Discord messages), for locale files that exist but lag behind on newer keys. - locales/fr.json: add the seerr_request_errors block directly too, consistent with de/sv/template. - CHANGELOG: fix the 1.5.6 EMBED_SHOW_OVERVIEW entry, which told users to manually re-configure the setting — it's now migrated automatically. --- CHANGELOG.md | 2 +- bot/roundupScheduler.js | 10 ++++++++++ locales/fr.json | 17 ++++++++++++++--- utils/i18n.js | 14 ++++++++++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dac7eac..4a3e0d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### ✨ Added -- **Separate overview toggle for episodes**: The embed overview setting is now split into two independent options -- one for movies and series, one for episodes. Episode summaries can be disabled independently to avoid spoilers for shows you haven't caught up on. Both options are on by default. Configurable via the dashboard under "Embed Options". The previous `EMBED_SHOW_OVERVIEW` setting has been replaced; users who had it disabled will need to re-configure the new options. +- **Separate overview toggle for episodes**: The embed overview setting is now split into two independent options -- one for movies and series, one for episodes. Episode summaries can be disabled independently to avoid spoilers for shows you haven't caught up on. Both options are on by default. Configurable via the dashboard under "Embed Options". The previous `EMBED_SHOW_OVERVIEW` setting has been replaced and is migrated automatically to both new options on first start after upgrading; no action needed. - **Weekly Roundup**: Optional scheduled Discord post that summarizes new Jellyfin content from the last 7 days. Disabled by default. Configurable via the dashboard (channel, weekday, hour, embed color). The roundup groups items by library and collapses episodes of the same series into one line (e.g. _"My Show — Seasons 1 & 2 (12 episodes)"_). Item titles link directly to Jellyfin. A hourly scheduler tick with a persisted `WEEKLY_ROUNDUP_LAST_POSTED_AT` timestamp makes the post idempotent across Docker restarts. Sonarr/Radarr quality upgrades are filtered out via a stable-identity first-seen map (`config/dedup-roundup-first-seen.json`) so a re-imported file does not show up as "new". diff --git a/bot/roundupScheduler.js b/bot/roundupScheduler.js index 4cbf393..8e05a4a 100644 --- a/bot/roundupScheduler.js +++ b/bot/roundupScheduler.js @@ -16,6 +16,7 @@ const TICK_OFFSET_SECONDS = 5; const WEEKDAY_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; let pendingTimer = null; +let generation = 0; function parseIntInRange(raw, fallback, min, max) { const n = parseInt(raw, 10); @@ -105,6 +106,13 @@ export function start(client) { clearTimeout(pendingTimer); pendingTimer = null; } + // clearTimeout only helps if the previous tick hasn't fired yet. If a + // restart lands while an old tick is mid-flight (sendWeeklyRoundup does + // Jellyfin + Discord I/O and can take a while), the old chain's finally() + // would otherwise re-arm itself with the stale client after we've already + // set up the new one. The generation token makes any such stale chain a + // no-op instead of silently ticking against a destroyed client. + const myGeneration = ++generation; const installedAt = getInstalledAt(); const now = new Date(); @@ -145,7 +153,9 @@ export function start(client) { // the next hour. setInterval(HOUR_MS) drifts off the boundary across DST // transitions and could push a post a full hour late. const scheduleNext = () => { + if (myGeneration !== generation) return; // superseded by a newer start() pendingTimer = setTimeout(() => { + if (myGeneration !== generation) return; runTick(client) .catch((err) => logger.error(`Weekly Roundup tick crash: ${err?.message || err}`) diff --git a/locales/fr.json b/locales/fr.json index 9096039..1acb371 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -2,7 +2,10 @@ "_meta": { "language_name": "Français", "language_code": "fr", - "contributors": ["purplelines", "GitHub Copilot"], + "contributors": [ + "purplelines", + "GitHub Copilot" + ], "completion": "100%", "last_updated": "2026-06-08", "notes": "Traduction complète et révisée par purplelines avec l'aide de GitHub Copilot. Chaque traduction a été soigneusement vérifiée." @@ -253,7 +256,7 @@ }, "actions": { "start": "Démarrer le bot", - "stop": "Arrêter le bot", + "stop": "Arrêter le bot", "restart": "Redémarrer le bot" }, "messages": { @@ -360,5 +363,13 @@ "copy_secret": "Copier le secret", "webhook_secret_desc": "Votre secret de webhook est utilisé pour sécuriser les requêtes entrantes. Copiez-le ci-dessous — vous devrez l'ajouter en tant qu'en-tête HTTP personnalisé dans le plugin webhook Jellyfin:", "notification_testing_info": "Ces tests contournent le point de terminaison webhook et s'exécutent en interne — ils fonctionnent sans l'en-tête X-Webhook-Secret. Assurez-vous d'avoir encore copié le secret et de l'avoir ajouté à votre plugin webhook Jellyfin afin que les notifications réelles de Jellyfin soient acceptées." + }, + "seerr_request_errors": { + "quota": "⚠️ {msg} You've reached your request limit — contact an admin if you think this is a mistake.", + "auth": "⚠️ Request failed: authentication error. Check the bot's API key configuration.", + "server_error": "⚠️ Seerr returned a server error. Try again later.", + "generic": "⚠️ Seerr error: {msg}", + "unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.", + "unknown": "⚠️ An error occurred: {msg}" } -} \ No newline at end of file +} diff --git a/utils/i18n.js b/utils/i18n.js index af11e3d..91261a6 100644 --- a/utils/i18n.js +++ b/utils/i18n.js @@ -17,6 +17,7 @@ const FALLBACK_LANG = "en"; const LANG_CODE_RE = /^[a-zA-Z]{2,3}(?:[_-][a-zA-Z0-9]{2,8})?$/; let translations = null; +let englishFallback = null; let loadedLang = null; function safeLang(raw) { @@ -54,6 +55,10 @@ function ensureLoaded() { logger.warn(`[i18n] Locale '${lang}' not found, using '${FALLBACK_LANG}'.`); } } + // Per-key fallback: a locale file that exists but is missing a specific + // key (e.g. added after that translation was last updated) should still + // resolve to English instead of leaking the raw key into user-facing text. + englishFallback = lang === FALLBACK_LANG ? translations : loadLocaleFile(FALLBACK_LANG); loadedLang = lang; } @@ -74,12 +79,17 @@ function interpolate(str, vars) { export function t(key, vars) { ensureLoaded(); if (!key || typeof key !== "string") return String(key ?? ""); - const value = lookup(translations, key); - if (typeof value !== "string") return key; + let value = lookup(translations, key); + if (typeof value !== "string") { + value = lookup(englishFallback, key); + if (typeof value !== "string") return key; + logger.warn(`[i18n] Key '${key}' missing in '${loadedLang}', falling back to '${FALLBACK_LANG}'.`); + } return interpolate(value, vars); } export function resetI18nCache() { translations = null; + englishFallback = null; loadedLang = null; } From 4d61bd5a10153ee306b1488d2c11c81ace898a35 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:00:08 +0200 Subject: [PATCH 3/4] fix(roundup): scheduler stop path, drop fake fr.json translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - roundupScheduler: add stop() (bumps generation, clears the pending timer) and call it from both discordClient.destroy() sites (routes/botRoutes.js stop-bot handler, app.js config-triggered restart). Without this, stopping the bot (not restarting it) left the scheduler ticking against a destroyed client until the failure circuit opened for the week — the same failure mode the previous round's restart fix addressed, just for a different trigger. - locales/fr.json: drop the seerr_request_errors block added last round — it was the English source text copy-pasted in, not an actual French translation, and it silently suppressed the new per-key en fallback warning. Leaving the keys absent lets the fallback (and its logger.warn) do its job until someone translates them for real. - utils/i18n.js: minor cleanup — reuse the already-loaded en.json fallback instead of reading it from disk twice, and log an error if en.json itself fails to load while another locale is active (previously a silent no-fallback-left edge case). --- app.js | 2 ++ bot/roundupScheduler.js | 12 ++++++++++++ locales/fr.json | 8 -------- routes/botRoutes.js | 8 ++++++++ utils/i18n.js | 12 +++++++++++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app.js b/app.js index 6164dc1..fc9f630 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,7 @@ import { handleJellyfinWebhook } from "./jellyfinWebhook.js"; import { configTemplate } from "./lib/config.js"; import { sendDailyRandomPick } from "./bot/dailyPick.js"; import { sendWeeklyRoundupTest } from "./bot/weeklyRoundup.js"; +import { stop as stopRoundupScheduler } from "./bot/roundupScheduler.js"; import { seedLibrary } from "./jellyfin/librarySeeder.js"; import { pruneLibrary } from "./jellyfin/libraryPruner.js"; @@ -674,6 +675,7 @@ function configureWebServer() { "Critical Discord settings changed. Restarting bot logic..." ); + stopRoundupScheduler(); await botState.discordClient.destroy(); botState.isBotRunning = false; botState.discordClient = null; diff --git a/bot/roundupScheduler.js b/bot/roundupScheduler.js index 8e05a4a..d89253b 100644 --- a/bot/roundupScheduler.js +++ b/bot/roundupScheduler.js @@ -165,3 +165,15 @@ export function start(client) { }; scheduleNext(); } + +// Called when the bot is stopped (not restarted) so the scheduler doesn't +// keep ticking against a destroyed Discord client — that would fail every +// tick, burn the failure counter, and open the circuit for the rest of the +// week even though nothing about the roundup content actually failed. +export function stop() { + generation++; + if (pendingTimer) { + clearTimeout(pendingTimer); + pendingTimer = null; + } +} diff --git a/locales/fr.json b/locales/fr.json index 1acb371..371a0ce 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -363,13 +363,5 @@ "copy_secret": "Copier le secret", "webhook_secret_desc": "Votre secret de webhook est utilisé pour sécuriser les requêtes entrantes. Copiez-le ci-dessous — vous devrez l'ajouter en tant qu'en-tête HTTP personnalisé dans le plugin webhook Jellyfin:", "notification_testing_info": "Ces tests contournent le point de terminaison webhook et s'exécutent en interne — ils fonctionnent sans l'en-tête X-Webhook-Secret. Assurez-vous d'avoir encore copié le secret et de l'avoir ajouté à votre plugin webhook Jellyfin afin que les notifications réelles de Jellyfin soient acceptées." - }, - "seerr_request_errors": { - "quota": "⚠️ {msg} You've reached your request limit — contact an admin if you think this is a mistake.", - "auth": "⚠️ Request failed: authentication error. Check the bot's API key configuration.", - "server_error": "⚠️ Seerr returned a server error. Try again later.", - "generic": "⚠️ Seerr error: {msg}", - "unreachable": "⚠️ Could not reach Seerr. Check that your Seerr URL is correct and reachable.", - "unknown": "⚠️ An error occurred: {msg}" } } diff --git a/routes/botRoutes.js b/routes/botRoutes.js index cf5104a..8bae5bf 100644 --- a/routes/botRoutes.js +++ b/routes/botRoutes.js @@ -3,6 +3,7 @@ import rateLimit from "express-rate-limit"; import { createRequire } from "module"; import { authenticateToken } from "../utils/auth.js"; import { botState } from "../bot/botState.js"; +import { stop as stopRoundupScheduler } from "../bot/roundupScheduler.js"; import cache from "../utils/cache.js"; import logger from "../utils/logger.js"; @@ -106,6 +107,13 @@ export function createBotRoutes({ startBot, jellyfinPoller }) { logger.error("Error stopping Jellyfin poller:", error); } + try { + stopRoundupScheduler(); + logger.info("Weekly Roundup scheduler stopped"); + } catch (error) { + logger.error("Error stopping Weekly Roundup scheduler:", error); + } + await botState.discordClient.destroy(); botState.isBotRunning = false; botState.discordClient = null; diff --git a/utils/i18n.js b/utils/i18n.js index 91261a6..4051898 100644 --- a/utils/i18n.js +++ b/utils/i18n.js @@ -58,7 +58,17 @@ function ensureLoaded() { // Per-key fallback: a locale file that exists but is missing a specific // key (e.g. added after that translation was last updated) should still // resolve to English instead of leaking the raw key into user-facing text. - englishFallback = lang === FALLBACK_LANG ? translations : loadLocaleFile(FALLBACK_LANG); + // Reuse `fallback` above instead of re-reading en.json a second time. + englishFallback = lang === FALLBACK_LANG ? translations : fallback; + if (lang !== FALLBACK_LANG && !englishFallback) { + // Unlike the "primary locale missing" case above (expected for + // less-maintained translations), en.json missing entirely means + // per-key fallback has nothing to fall back to — every miss in the + // active locale will now silently return the raw key. + logger.error( + `[i18n] English fallback locale ('${FALLBACK_LANG}.json') failed to load; missing keys in '${lang}' will render as raw keys.` + ); + } loadedLang = lang; } From 59985ed3414c636651970c4c910be8030d0828e5 Mon Sep 17 00:00:00 2001 From: retardgerman <78982850+retardgerman@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:03:05 +0200 Subject: [PATCH 4/4] fix(deps): pin body-parser to 1.20.6, bump axios to 1.19.0 (npm audit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm audit --audit-level=high started failing on the fix branch with two newly-published high-severity advisories, unrelated to the review fixes in this PR: - body-parser <1.20.6 (GHSA-v422-hmwv-36x6): DoS via invalid limit value silently disabling size enforcement. body-parser is a transitive dep of express, not a direct one, so pinned via overrides like the existing undici/follow-redirects entries. - axios 1.0.0-1.17.0: several newly-disclosed advisories (prototype pollution, DoS via recursion, maxBodyLength bypasses). Resolved by `npm audit fix`, which bumped the resolved version to 1.19.0 — still satisfies the existing `^1.17.0` range in package.json, so no version constraint change needed there. `npm audit --audit-level=high` now reports 0 vulnerabilities. --- package-lock.json | 14 +++++++------- package.json | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9bdc2f3..edc9916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -382,13 +382,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", - "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -403,9 +403,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", diff --git a/package.json b/package.json index e02813f..b8bfd31 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "overrides": { "follow-redirects": "^1.16.0", - "undici": "^6.26.1" + "undici": "^6.26.1", + "body-parser": "^1.20.6" } }