diff --git a/services/adminConfig.js b/services/adminConfig.js index 5a6a964..39d547e 100644 --- a/services/adminConfig.js +++ b/services/adminConfig.js @@ -13,6 +13,7 @@ const DEFAULT_SETTINGS = { storeWatchCron: '*/30 * * * *', storeWatchInitialDelayMinSeconds: 10, storeWatchInitialDelayMaxSeconds: 60, + storeWatchRequestDelayMs: 1000, storePickupCheckDelayMs: 400, ignoredSlots: [ { @@ -113,6 +114,10 @@ function readSettings() { parsed.storeWatchInitialDelayMaxSeconds, DEFAULT_SETTINGS.storeWatchInitialDelayMaxSeconds ), + storeWatchRequestDelayMs: sanitizeNumber( + parsed.storeWatchRequestDelayMs, + DEFAULT_SETTINGS.storeWatchRequestDelayMs + ), storePickupCheckDelayMs: sanitizeNumber( parsed.storePickupCheckDelayMs, DEFAULT_SETTINGS.storePickupCheckDelayMs @@ -143,6 +148,10 @@ function writeSettings(patch = {}) { patch.storeWatchInitialDelayMaxSeconds, current.storeWatchInitialDelayMaxSeconds ), + storeWatchRequestDelayMs: sanitizeNumber( + patch.storeWatchRequestDelayMs, + current.storeWatchRequestDelayMs + ), storePickupCheckDelayMs: sanitizeNumber( patch.storePickupCheckDelayMs, current.storePickupCheckDelayMs diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js index 6d0696b..d76b43d 100644 --- a/services/pickupScheduler.js +++ b/services/pickupScheduler.js @@ -6,6 +6,13 @@ const notificationService = require('./notificationService'); const { readConfig, writeConfig } = require('./configStore'); const { readStoreWatch, writeStoreWatch } = require('./storeWatchStore'); +function wait(ms) { + if (!ms || ms <= 0) { + return Promise.resolve(); + } + return new Promise((resolve) => setTimeout(resolve, ms)); +} + const weekdayMap = { Montag: 'Monday', Dienstag: 'Tuesday', @@ -47,6 +54,9 @@ function resolveSettings(settings) { storeWatchInitialDelayMaxSeconds: Number.isFinite(settings.storeWatchInitialDelayMaxSeconds) ? settings.storeWatchInitialDelayMaxSeconds : DEFAULT_SETTINGS.storeWatchInitialDelayMaxSeconds, + storeWatchRequestDelayMs: Number.isFinite(settings.storeWatchRequestDelayMs) + ? settings.storeWatchRequestDelayMs + : DEFAULT_SETTINGS.storeWatchRequestDelayMs, ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots, notifications: { ntfy: { @@ -295,7 +305,7 @@ async function checkEntry(sessionId, entry, settings) { } } -async function checkWatchedStores(sessionId) { +async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS) { const session = sessionStore.get(sessionId); if (!session?.profile?.id) { return; @@ -310,8 +320,10 @@ async function checkWatchedStores(sessionId) { return; } + const perRequestDelay = Math.max(0, Number(settings?.storeWatchRequestDelayMs) || 0); let changed = false; - for (const watcher of watchers) { + for (let index = 0; index < watchers.length; index += 1) { + const watcher = watchers[index]; try { const details = await foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader); const status = details?.teamSearchStatus === 1 ? 1 : 0; @@ -329,6 +341,11 @@ async function checkWatchedStores(sessionId) { } } catch (error) { console.error(`[WATCH] Prüfung für Store ${watcher.storeId} fehlgeschlagen:`, error.message); + } finally { + const hasNext = index < watchers.length - 1; + if (hasNext && perRequestDelay > 0) { + await wait(perRequestDelay); + } } } @@ -338,6 +355,7 @@ async function checkWatchedStores(sessionId) { } function scheduleStoreWatchers(sessionId, settings) { + const effectiveSettings = settings || DEFAULT_SETTINGS; const session = sessionStore.get(sessionId); if (!session?.profile?.id) { return false; @@ -346,11 +364,11 @@ function scheduleStoreWatchers(sessionId, settings) { if (!Array.isArray(watchers) || watchers.length === 0) { return false; } - const cronExpression = settings.storeWatchCron || DEFAULT_SETTINGS.storeWatchCron; + const cronExpression = effectiveSettings.storeWatchCron || DEFAULT_SETTINGS.storeWatchCron; const job = cron.schedule( cronExpression, () => { - checkWatchedStores(sessionId).catch((error) => { + checkWatchedStores(sessionId, effectiveSettings).catch((error) => { console.error('[WATCH] Regelmäßige Prüfung fehlgeschlagen:', error.message); }); }, @@ -358,8 +376,11 @@ function scheduleStoreWatchers(sessionId, settings) { ); sessionStore.attachJob(sessionId, job); setTimeout( - () => checkWatchedStores(sessionId), - randomDelayMs(settings.storeWatchInitialDelayMinSeconds, settings.storeWatchInitialDelayMaxSeconds) + () => checkWatchedStores(sessionId, effectiveSettings), + randomDelayMs( + effectiveSettings.storeWatchInitialDelayMinSeconds, + effectiveSettings.storeWatchInitialDelayMaxSeconds + ) ); console.log( `[WATCH] Überwache ${watchers.length} Betriebe für Session ${sessionId} (Cron: ${cronExpression}).` diff --git a/src/components/AdminSettingsPanel.js b/src/components/AdminSettingsPanel.js index 39d75ba..6902cc0 100644 --- a/src/components/AdminSettingsPanel.js +++ b/src/components/AdminSettingsPanel.js @@ -210,6 +210,20 @@ const AdminSettingsPanel = ({ /> + + + onSettingChange('storeWatchRequestDelayMs', event.target.value, true)} + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="z. B. 1000" + /> + { storeWatchCron: raw.storeWatchCron || '', storeWatchInitialDelayMinSeconds: raw.storeWatchInitialDelayMinSeconds ?? '', storeWatchInitialDelayMaxSeconds: raw.storeWatchInitialDelayMaxSeconds ?? '', + storeWatchRequestDelayMs: raw.storeWatchRequestDelayMs ?? '', ignoredSlots: Array.isArray(raw.ignoredSlots) ? raw.ignoredSlots.map((slot) => ({ storeId: slot?.storeId ? String(slot.storeId) : '', @@ -56,6 +57,7 @@ export const serializeAdminSettings = (adminSettings) => { storeWatchCron: adminSettings.storeWatchCron, storeWatchInitialDelayMinSeconds: toNumberOrUndefined(adminSettings.storeWatchInitialDelayMinSeconds), storeWatchInitialDelayMaxSeconds: toNumberOrUndefined(adminSettings.storeWatchInitialDelayMaxSeconds), + storeWatchRequestDelayMs: toNumberOrUndefined(adminSettings.storeWatchRequestDelayMs), ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({ storeId: slot.storeId || '', description: slot.description || ''