Watch Stores mit Offen-Spalte
This commit is contained in:
163
server.js
163
server.js
@@ -13,6 +13,7 @@ const { readNotificationSettings, writeNotificationSettings } = require('./servi
|
||||
const notificationService = require('./services/notificationService');
|
||||
const { readStoreWatch, writeStoreWatch } = require('./services/storeWatchStore');
|
||||
const { readPreferences, writePreferences } = require('./services/userPreferencesStore');
|
||||
const { readStoreStatus, writeStoreStatus } = require('./services/storeStatusStore');
|
||||
|
||||
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
||||
const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase();
|
||||
@@ -24,6 +25,35 @@ const storeRefreshJobs = new Map();
|
||||
const cachedStoreSnapshots = new Map();
|
||||
const regionStoreCache = new Map();
|
||||
const REGION_STORE_CACHE_MS = 15 * 60 * 1000;
|
||||
const STORE_STATUS_MAX_AGE_MS = 60 * 24 * 60 * 60 * 1000;
|
||||
const storeStatusCache = new Map();
|
||||
|
||||
(function bootstrapStoreStatusCache() {
|
||||
try {
|
||||
const cached = readStoreStatus();
|
||||
Object.entries(cached || {}).forEach(([storeId, entry]) => {
|
||||
if (entry && typeof entry === 'object') {
|
||||
storeStatusCache.set(String(storeId), {
|
||||
teamSearchStatus:
|
||||
entry.teamSearchStatus === null || entry.teamSearchStatus === undefined
|
||||
? null
|
||||
: Number(entry.teamSearchStatus),
|
||||
fetchedAt: Number(entry.fetchedAt) || 0
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[STORE-STATUS] Bootstrap fehlgeschlagen:', error.message);
|
||||
}
|
||||
})();
|
||||
|
||||
function persistStoreStatusCache() {
|
||||
const payload = {};
|
||||
storeStatusCache.forEach((value, key) => {
|
||||
payload[key] = value;
|
||||
});
|
||||
writeStoreStatus(payload);
|
||||
}
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
@@ -111,6 +141,90 @@ function setCachedRegionStores(regionId, payload) {
|
||||
});
|
||||
}
|
||||
|
||||
function getCachedStoreStatus(storeId) {
|
||||
return storeStatusCache.get(String(storeId)) || null;
|
||||
}
|
||||
|
||||
async function refreshStoreStatus(storeIds = [], cookieHeader, { force = false } = {}) {
|
||||
if (!Array.isArray(storeIds) || storeIds.length === 0 || !cookieHeader) {
|
||||
return { refreshed: 0 };
|
||||
}
|
||||
const now = Date.now();
|
||||
let refreshed = 0;
|
||||
for (const id of storeIds) {
|
||||
const storeId = String(id);
|
||||
const entry = storeStatusCache.get(storeId);
|
||||
const ageOk = entry && now - (entry.fetchedAt || 0) <= STORE_STATUS_MAX_AGE_MS;
|
||||
if (!force && ageOk) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const details = await foodsharingClient.fetchStoreDetails(storeId, cookieHeader);
|
||||
const status = Number(details?.teamSearchStatus);
|
||||
const normalized = Number.isFinite(status) ? status : null;
|
||||
storeStatusCache.set(storeId, {
|
||||
teamSearchStatus: normalized,
|
||||
fetchedAt: now
|
||||
});
|
||||
refreshed += 1;
|
||||
} catch (error) {
|
||||
console.error(`[STORE-STATUS] Status für Store ${storeId} konnte nicht aktualisiert werden:`, error.message);
|
||||
}
|
||||
}
|
||||
if (refreshed > 0) {
|
||||
persistStoreStatusCache();
|
||||
}
|
||||
return { refreshed };
|
||||
}
|
||||
|
||||
async function enrichStoresWithTeamStatus(stores = [], cookieHeader, { forceRefresh = false } = {}) {
|
||||
if (!Array.isArray(stores) || stores.length === 0) {
|
||||
return { stores, statusMeta: { total: 0, refreshed: 0, fromCache: 0, missing: 0 } };
|
||||
}
|
||||
const now = Date.now();
|
||||
const staleIds = [];
|
||||
let cacheHits = 0;
|
||||
stores.forEach((store) => {
|
||||
const entry = getCachedStoreStatus(store.id);
|
||||
const fresh = entry && now - (entry.fetchedAt || 0) <= STORE_STATUS_MAX_AGE_MS;
|
||||
if (entry && fresh && !forceRefresh) {
|
||||
store.teamSearchStatus = entry.teamSearchStatus;
|
||||
store.teamStatusUpdatedAt = entry.fetchedAt || null;
|
||||
cacheHits += 1;
|
||||
} else {
|
||||
staleIds.push(store.id);
|
||||
}
|
||||
});
|
||||
|
||||
let refreshed = 0;
|
||||
if (staleIds.length > 0) {
|
||||
const result = await refreshStoreStatus(staleIds, cookieHeader, { force: forceRefresh });
|
||||
refreshed = result.refreshed || 0;
|
||||
}
|
||||
|
||||
stores.forEach((store) => {
|
||||
if (store.teamSearchStatus === undefined) {
|
||||
const entry = getCachedStoreStatus(store.id);
|
||||
if (entry) {
|
||||
store.teamSearchStatus = entry.teamSearchStatus;
|
||||
store.teamStatusUpdatedAt = entry.fetchedAt || null;
|
||||
} else {
|
||||
store.teamSearchStatus = null;
|
||||
store.teamStatusUpdatedAt = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const statusMeta = {
|
||||
total: stores.length,
|
||||
refreshed,
|
||||
fromCache: cacheHits,
|
||||
missing: stores.filter((store) => store.teamStatusUpdatedAt == null).length,
|
||||
generatedAt: Date.now()
|
||||
};
|
||||
return { stores, statusMeta };
|
||||
}
|
||||
|
||||
function isStoreCacheFresh(session) {
|
||||
if (!session?.storesCache?.fetchedAt) {
|
||||
return false;
|
||||
@@ -409,25 +523,46 @@ app.get('/api/store-watch/regions/:regionId/stores', requireAuth, async (req, re
|
||||
if (!regionId) {
|
||||
return res.status(400).json({ error: 'Region-ID fehlt' });
|
||||
}
|
||||
const forceRefresh = req.query.force === '1';
|
||||
if (!forceRefresh) {
|
||||
const forceRegionRefresh = req.query.force === '1';
|
||||
const forceStatusRefresh = req.query.forceStatus === '1';
|
||||
|
||||
let basePayload = null;
|
||||
if (!forceRegionRefresh) {
|
||||
const cached = getCachedRegionStores(regionId);
|
||||
if (cached) {
|
||||
return res.json(cached);
|
||||
basePayload = cached;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader);
|
||||
const payload = {
|
||||
total: Number(result?.total) || 0,
|
||||
stores: Array.isArray(result?.stores) ? result.stores : []
|
||||
};
|
||||
setCachedRegionStores(regionId, payload);
|
||||
res.json(payload);
|
||||
} catch (error) {
|
||||
console.error(`[STORE-WATCH] Stores für Region ${regionId} konnten nicht geladen werden:`, error.message);
|
||||
res.status(500).json({ error: 'Betriebe konnten nicht geladen werden' });
|
||||
|
||||
if (!basePayload) {
|
||||
try {
|
||||
const result = await foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader);
|
||||
basePayload = {
|
||||
total: Number(result?.total) || 0,
|
||||
stores: Array.isArray(result?.stores) ? result.stores : []
|
||||
};
|
||||
setCachedRegionStores(regionId, basePayload);
|
||||
} catch (error) {
|
||||
console.error(`[STORE-WATCH] Stores für Region ${regionId} konnten nicht geladen werden:`, error.message);
|
||||
return res.status(500).json({ error: 'Betriebe konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
const filteredStores = basePayload.stores
|
||||
.filter((store) => Number(store.cooperationStatus) === 5)
|
||||
.map((store) => ({ ...store, id: String(store.id) }));
|
||||
|
||||
const { stores: enrichedStores, statusMeta } = await enrichStoresWithTeamStatus(
|
||||
filteredStores,
|
||||
req.session.cookieHeader,
|
||||
{ forceRefresh: forceStatusRefresh }
|
||||
);
|
||||
|
||||
res.json({
|
||||
total: filteredStores.length,
|
||||
stores: enrichedStores,
|
||||
statusMeta
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/store-watch/subscriptions', requireAuth, (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user