From 916ca1dbc2c311079af8d4aaf23c300ed23ac3a2 Mon Sep 17 00:00:00 2001 From: Meik Date: Thu, 29 Jan 2026 17:00:24 +0100 Subject: [PATCH] aktueller stand --- services/adminConfig.js | 26 +++++-- services/pickupScheduler.js | 76 ++++++++++++++---- src/App.js | 2 +- src/components/AdminAccessMessage.js | 2 +- src/components/AdminSettingsPanel.js | 62 +++++++++++++-- src/components/DashboardView.js | 12 +-- src/components/NavigationTabs.js | 110 ++++++++++++++++++++++----- src/components/StoreWatchPage.js | 4 +- src/hooks/useAdminSettings.js | 9 ++- src/utils/adminSettings.js | 8 +- 10 files changed, 256 insertions(+), 55 deletions(-) diff --git a/services/adminConfig.js b/services/adminConfig.js index eb57267..cfd6d48 100644 --- a/services/adminConfig.js +++ b/services/adminConfig.js @@ -14,11 +14,13 @@ const DEFAULT_SETTINGS = { storeWatchInitialDelayMinSeconds: 10, storeWatchInitialDelayMaxSeconds: 60, storeWatchRequestDelayMs: 1000, + storeWatchStatusCacheMaxAgeMinutes: 120, storePickupCheckDelayMs: 400, ignoredSlots: [ { storeId: '51450', - description: 'TVS' + slotName: 'TVS', + info: '' } ], notifications: { @@ -56,11 +58,15 @@ function sanitizeIgnoredSlots(slots = []) { return DEFAULT_SETTINGS.ignoredSlots; } return slots - .map((slot) => ({ - storeId: slot?.storeId ? String(slot.storeId) : '', - description: slot?.description ? String(slot.description) : '' - })) - .filter((slot) => slot.storeId); + .map((slot) => { + const slotName = slot?.slotName ?? slot?.description; + return { + storeId: slot?.storeId ? String(slot.storeId) : '', + slotName: slotName ? String(slotName) : '', + info: slot?.info ? String(slot.info) : '' + }; + }) + .filter((slot) => slot.storeId && slot.slotName); } function sanitizeString(value) { @@ -120,6 +126,10 @@ function readSettings() { parsed.storeWatchRequestDelayMs, DEFAULT_SETTINGS.storeWatchRequestDelayMs ), + storeWatchStatusCacheMaxAgeMinutes: sanitizeNumber( + parsed.storeWatchStatusCacheMaxAgeMinutes, + DEFAULT_SETTINGS.storeWatchStatusCacheMaxAgeMinutes + ), storePickupCheckDelayMs: sanitizeNumber( parsed.storePickupCheckDelayMs, DEFAULT_SETTINGS.storePickupCheckDelayMs @@ -154,6 +164,10 @@ function writeSettings(patch = {}) { patch.storeWatchRequestDelayMs, current.storeWatchRequestDelayMs ), + storeWatchStatusCacheMaxAgeMinutes: sanitizeNumber( + patch.storeWatchStatusCacheMaxAgeMinutes, + current.storeWatchStatusCacheMaxAgeMinutes + ), storePickupCheckDelayMs: sanitizeNumber( patch.storePickupCheckDelayMs, current.storePickupCheckDelayMs diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js index f5f8e6f..9932fbe 100644 --- a/services/pickupScheduler.js +++ b/services/pickupScheduler.js @@ -6,7 +6,7 @@ const notificationService = require('./notificationService'); const { readConfig, writeConfig } = require('./configStore'); const { readStoreWatch, writeStoreWatch } = require('./storeWatchStore'); const { readJournal, writeJournal } = require('./journalStore'); -const { setStoreStatus, persistStoreStatusCache } = require('./storeStatusCache'); +const { getStoreStatus, setStoreStatus, persistStoreStatusCache } = require('./storeStatusCache'); const { sendDormantPickupWarning, sendJournalReminderNotification } = require('./notificationService'); const { ensureSession, withSessionRetry } = require('./sessionRefresh'); @@ -17,6 +17,53 @@ function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } +const DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES = 120; +const storeWatchInFlight = new Map(); + +async function fetchSharedStoreStatus(session, storeId, { forceRefresh = false, maxAgeMs } = {}) { + if (!storeId) { + return { status: null, fetchedAt: null, fromCache: false }; + } + const cacheEntry = getStoreStatus(storeId); + const cachedStatus = cacheEntry?.teamSearchStatus; + const hasCachedStatus = cachedStatus === 0 || cachedStatus === 1; + const cachedAt = Number(cacheEntry?.fetchedAt) || 0; + const effectiveMaxAge = + Number.isFinite(maxAgeMs) && maxAgeMs >= 0 + ? maxAgeMs + : DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES * 60 * 1000; + const cacheFresh = hasCachedStatus && Date.now() - cachedAt <= effectiveMaxAge; + if (cacheFresh && !forceRefresh) { + return { status: cachedStatus, fetchedAt: cachedAt, fromCache: true }; + } + + const key = String(storeId); + if (!forceRefresh && storeWatchInFlight.has(key)) { + return storeWatchInFlight.get(key); + } + + const fetchPromise = (async () => { + const details = await withSessionRetry( + session, + () => foodsharingClient.fetchStoreDetails(storeId, session.cookieHeader, session), + { label: 'fetchStoreDetails' } + ); + const status = details?.teamSearchStatus === 1 ? 1 : 0; + const fetchedAt = Date.now(); + setStoreStatus(storeId, { teamSearchStatus: status, fetchedAt }); + return { status, fetchedAt, fromCache: false }; + })(); + + storeWatchInFlight.set(key, fetchPromise); + try { + return await fetchPromise; + } finally { + if (storeWatchInFlight.get(key) === fetchPromise) { + storeWatchInFlight.delete(key); + } + } +} + const weekdayMap = { Montag: 'Monday', Dienstag: 'Tuesday', @@ -103,6 +150,9 @@ function resolveSettings(settings) { storeWatchRequestDelayMs: Number.isFinite(settings.storeWatchRequestDelayMs) ? settings.storeWatchRequestDelayMs : DEFAULT_SETTINGS.storeWatchRequestDelayMs, + storeWatchStatusCacheMaxAgeMinutes: Number.isFinite(settings.storeWatchStatusCacheMaxAgeMinutes) + ? settings.storeWatchStatusCacheMaxAgeMinutes + : DEFAULT_SETTINGS.storeWatchStatusCacheMaxAgeMinutes, ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots, notifications: { ntfy: { @@ -305,10 +355,8 @@ function shouldIgnoreSlot(entry, pickup, settings) { if (String(rule.storeId) !== entry.id) { return false; } - if (rule.description) { - return pickup.description === rule.description; - } - return true; + const slotName = rule.slotName || rule.description; + return slotName ? pickup.description === slotName : true; }); } @@ -469,12 +517,13 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option for (let index = 0; index < watchers.length; index += 1) { const watcher = watchers[index]; try { - const details = await withSessionRetry( - session, - () => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader, session), - { label: 'fetchStoreDetails' } - ); - const status = details?.teamSearchStatus === 1 ? 1 : 0; + const cacheMaxAgeMs = Math.max( + 0, + Number(settings?.storeWatchStatusCacheMaxAgeMinutes ?? DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES) + ) * 60 * 1000; + const { status, fromCache } = await fetchSharedStoreStatus(session, watcher.storeId, { + maxAgeMs: cacheMaxAgeMs + }); const checkedAt = Date.now(); if (status === 1 && watcher.lastTeamSearchStatus !== 1) { await notificationService.sendStoreWatchNotification({ @@ -490,8 +539,9 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option } watcher.lastStatusCheckAt = checkedAt; changed = true; - setStoreStatus(watcher.storeId, { teamSearchStatus: status, fetchedAt: checkedAt }); - statusCacheUpdated = true; + if (!fromCache) { + statusCacheUpdated = true; + } summary.push({ storeId: watcher.storeId, storeName: watcher.storeName, diff --git a/src/App.js b/src/App.js index af2f732..3171c9c 100644 --- a/src/App.js +++ b/src/App.js @@ -784,7 +784,7 @@ function App() { <>
-
+
diff --git a/src/components/AdminAccessMessage.js b/src/components/AdminAccessMessage.js index d345b7c..f79c94f 100644 --- a/src/components/AdminAccessMessage.js +++ b/src/components/AdminAccessMessage.js @@ -1,7 +1,7 @@ import { Link } from 'react-router-dom'; const AdminAccessMessage = () => ( -
+

Kein Zugriff

Dieser Bereich ist nur für Administratoren verfügbar.

{ return ( -
+

Admin-Einstellungen

@@ -224,6 +224,22 @@ const AdminSettingsPanel = ({ placeholder="z. B. 1000" /> + + + + onSettingChange('storeWatchStatusCacheMaxAgeMinutes', 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. 120" + /> + Keine Regeln definiert.

)} + {adminSettings.ignoredSlots?.length > 0 && ( +
+ Store-ID + Slotname + Info + Aktion +
+ )} {adminSettings.ignoredSlots?.map((slot, index) => ( + (() => { + const storeIdMissing = !slot.storeId; + const slotNameMissing = !slot.slotName; + const showErrors = storeIdMissing || slotNameMissing; + return (
onIgnoredSlotChange(index, 'storeId', event.target.value)} placeholder="Store-ID" - className="md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + className={`border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 ${ + storeIdMissing ? 'border-red-400 focus:ring-red-500 focus:border-red-500' : '' + }`} /> onIgnoredSlotChange(index, 'description', event.target.value)} - placeholder="Beschreibung (optional)" + value={slot.slotName} + onChange={(event) => onIgnoredSlotChange(index, 'slotName', event.target.value)} + placeholder="Slotname" + required + className={`md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 ${ + slotNameMissing ? 'border-red-400 focus:ring-red-500 focus:border-red-500' : '' + }`} + /> + onIgnoredSlotChange(index, 'info', event.target.value)} + placeholder="Info (optional)" className="md:col-span-2 border rounded p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" /> + {showErrors && ( +

+ Store-ID und Slotname sind Pflichtfelder. +

+ )}
+ ); + })() ))} +
+
+ {tabs.map((tab) => + renderLink( + tab, + (isActive) => + `px-4 py-3 rounded-md border text-left transition-colors ${ + isActive + ? 'bg-blue-500 text-white border-blue-600' + : 'bg-white text-gray-700 border-gray-300 hover:border-blue-400' + }` + ) + )} +
+
+ {tabs.map((tab) => + renderLink( + tab, + (isActive) => + `group relative rounded-md px-2 pb-2 pt-1 text-sm font-semibold tracking-tight transition-colors ${ + isActive + ? 'text-blue-600 bg-blue-50/70' + : 'text-gray-600 hover:bg-gray-100/80 hover:text-gray-900' + }`, + { showUnderline: true } + ) + )} +
); }; diff --git a/src/components/StoreWatchPage.js b/src/components/StoreWatchPage.js index a3e0859..33868c5 100644 --- a/src/components/StoreWatchPage.js +++ b/src/components/StoreWatchPage.js @@ -1167,14 +1167,14 @@ const StoreWatchPage = ({ if (!authorizedFetch) { return ( -
+

Keine Session aktiv.

); } return ( -
+

Betriebs-Monitoring

diff --git a/src/hooks/useAdminSettings.js b/src/hooks/useAdminSettings.js index 5f47f7b..9b2f6a2 100644 --- a/src/hooks/useAdminSettings.js +++ b/src/hooks/useAdminSettings.js @@ -126,7 +126,7 @@ const useAdminSettings = ({ session, authorizedFetch, setStatus, setError }) => } return { ...prev, - ignoredSlots: [...(prev.ignoredSlots || []), { storeId: '', description: '' }] + ignoredSlots: [...(prev.ignoredSlots || []), { storeId: '', slotName: '', info: '' }] }; }); }, []); @@ -149,6 +149,13 @@ const useAdminSettings = ({ session, authorizedFetch, setStatus, setError }) => if (!session?.token || !session.isAdmin || !adminSettings) { return; } + const invalidIgnoredSlot = (adminSettings.ignoredSlots || []).find( + (slot) => !slot?.storeId || !slot?.slotName + ); + if (invalidIgnoredSlot) { + setError('Ignorierte Slots: Store-ID und Slotname sind Pflichtfelder.'); + return; + } setStatus('Admin-Einstellungen werden gespeichert...'); setError(''); diff --git a/src/utils/adminSettings.js b/src/utils/adminSettings.js index df8997d..ca99e21 100644 --- a/src/utils/adminSettings.js +++ b/src/utils/adminSettings.js @@ -13,10 +13,12 @@ export const normalizeAdminSettings = (raw) => { storeWatchInitialDelayMinSeconds: raw.storeWatchInitialDelayMinSeconds ?? '', storeWatchInitialDelayMaxSeconds: raw.storeWatchInitialDelayMaxSeconds ?? '', storeWatchRequestDelayMs: raw.storeWatchRequestDelayMs ?? '', + storeWatchStatusCacheMaxAgeMinutes: raw.storeWatchStatusCacheMaxAgeMinutes ?? '', ignoredSlots: Array.isArray(raw.ignoredSlots) ? raw.ignoredSlots.map((slot) => ({ storeId: slot?.storeId ? String(slot.storeId) : '', - description: slot?.description || '' + slotName: slot?.slotName || slot?.description || '', + info: slot?.info || '' })) : [], notifications: { @@ -59,9 +61,11 @@ export const serializeAdminSettings = (adminSettings) => { storeWatchInitialDelayMinSeconds: toNumberOrUndefined(adminSettings.storeWatchInitialDelayMinSeconds), storeWatchInitialDelayMaxSeconds: toNumberOrUndefined(adminSettings.storeWatchInitialDelayMaxSeconds), storeWatchRequestDelayMs: toNumberOrUndefined(adminSettings.storeWatchRequestDelayMs), + storeWatchStatusCacheMaxAgeMinutes: toNumberOrUndefined(adminSettings.storeWatchStatusCacheMaxAgeMinutes), ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({ storeId: slot.storeId || '', - description: slot.description || '' + slotName: slot.slotName || '', + info: slot.info || '' })), notifications: { ntfy: {