diff --git a/server.js b/server.js index 1388690..c58a55e 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,7 @@ const { scheduleConfig } = require('./services/pickupScheduler'); const adminConfig = require('./services/adminConfig'); const { readNotificationSettings, writeNotificationSettings } = require('./services/userSettingsStore'); const notificationService = require('./services/notificationService'); -const { readStoreWatch, writeStoreWatch } = require('./services/storeWatchStore'); +const { readStoreWatch, writeStoreWatch, listWatcherProfiles } = require('./services/storeWatchStore'); const { readPreferences, writePreferences } = require('./services/userPreferencesStore'); const { readStoreStatus, writeStoreStatus } = require('./services/storeStatusStore'); @@ -145,12 +145,63 @@ function getCachedStoreStatus(storeId) { return storeStatusCache.get(String(storeId)) || null; } -async function refreshStoreStatus(storeIds = [], cookieHeader, { force = false } = {}) { +async function notifyWatchersForStatusChanges(changes = [], storeInfoMap = new Map()) { + if (!Array.isArray(changes) || changes.length === 0) { + return; + } + const changeMap = new Map(); + changes.forEach((change) => { + if (!change) { + return; + } + changeMap.set(String(change.storeId), change); + }); + if (changeMap.size === 0) { + return; + } + const profiles = listWatcherProfiles(); + for (const profileId of profiles) { + const watchers = readStoreWatch(profileId); + if (!Array.isArray(watchers) || watchers.length === 0) { + continue; + } + let changed = false; + for (const watcher of watchers) { + const change = changeMap.get(String(watcher.storeId)); + if (!change) { + continue; + } + if (watcher.lastTeamSearchStatus !== change.newStatus) { + if (change.newStatus === 1 && watcher.lastTeamSearchStatus !== 1) { + const details = storeInfoMap.get(String(watcher.storeId)) || {}; + await notificationService.sendStoreWatchNotification({ + profileId, + storeName: details.name || watcher.storeName, + storeId: watcher.storeId, + regionName: details.region?.name || watcher.regionName + }); + } + watcher.lastTeamSearchStatus = change.newStatus; + changed = true; + } + } + if (changed) { + writeStoreWatch(profileId, watchers); + } + } +} + +async function refreshStoreStatus( + storeIds = [], + cookieHeader, + { force = false, storeInfoMap = new Map() } = {} +) { if (!Array.isArray(storeIds) || storeIds.length === 0 || !cookieHeader) { - return { refreshed: 0 }; + return { refreshed: 0, changes: [] }; } const now = Date.now(); let refreshed = 0; + const changes = []; for (const id of storeIds) { const storeId = String(id); const entry = storeStatusCache.get(storeId); @@ -162,10 +213,22 @@ async function refreshStoreStatus(storeIds = [], cookieHeader, { force = false } const details = await foodsharingClient.fetchStoreDetails(storeId, cookieHeader); const status = Number(details?.teamSearchStatus); const normalized = Number.isFinite(status) ? status : null; + const previous = entry ? entry.teamSearchStatus : null; storeStatusCache.set(storeId, { teamSearchStatus: normalized, fetchedAt: now }); + if (previous !== normalized) { + const info = storeInfoMap.get(storeId) || {}; + changes.push({ + storeId, + previousStatus: previous, + newStatus: normalized, + fetchedAt: now, + storeName: info.name || null, + regionName: info.region?.name || info.regionName || null + }); + } refreshed += 1; } catch (error) { console.error(`[STORE-STATUS] Status für Store ${storeId} konnte nicht aktualisiert werden:`, error.message); @@ -174,7 +237,7 @@ async function refreshStoreStatus(storeIds = [], cookieHeader, { force = false } if (refreshed > 0) { persistStoreStatusCache(); } - return { refreshed }; + return { refreshed, changes }; } async function enrichStoresWithTeamStatus(stores = [], cookieHeader, { forceRefresh = false } = {}) { @@ -184,7 +247,11 @@ async function enrichStoresWithTeamStatus(stores = [], cookieHeader, { forceRefr const now = Date.now(); const staleIds = []; let cacheHits = 0; + const storeInfoMap = new Map(); stores.forEach((store) => { + if (store?.id) { + storeInfoMap.set(String(store.id), store); + } const entry = getCachedStoreStatus(store.id); const fresh = entry && now - (entry.fetchedAt || 0) <= STORE_STATUS_MAX_AGE_MS; if (entry && fresh && !forceRefresh) { @@ -197,9 +264,14 @@ async function enrichStoresWithTeamStatus(stores = [], cookieHeader, { forceRefr }); let refreshed = 0; + let changes = []; if (staleIds.length > 0) { - const result = await refreshStoreStatus(staleIds, cookieHeader, { force: forceRefresh }); + const result = await refreshStoreStatus(staleIds, cookieHeader, { + force: forceRefresh, + storeInfoMap + }); refreshed = result.refreshed || 0; + changes = result.changes || []; } stores.forEach((store) => { @@ -222,6 +294,9 @@ async function enrichStoresWithTeamStatus(stores = [], cookieHeader, { forceRefr missing: stores.filter((store) => store.teamStatusUpdatedAt == null).length, generatedAt: Date.now() }; + if (changes.length > 0) { + await notifyWatchersForStatusChanges(changes, storeInfoMap); + } return { stores, statusMeta }; } diff --git a/services/storeWatchStore.js b/services/storeWatchStore.js index 86f4fe2..e299bb2 100644 --- a/services/storeWatchStore.js +++ b/services/storeWatchStore.js @@ -78,7 +78,16 @@ function writeStoreWatch(profileId, entries = []) { return sanitized; } +function listWatcherProfiles() { + ensureDir(); + return fs + .readdirSync(STORE_WATCH_DIR) + .filter((file) => file.endsWith('-store-watch.json')) + .map((file) => file.replace(/-store-watch\.json$/, '')); +} + module.exports = { readStoreWatch, - writeStoreWatch + writeStoreWatch, + listWatcherProfiles };