aktueller stand

This commit is contained in:
2026-01-04 00:11:29 +01:00
parent 41ef5107aa
commit 1c12bf6bd1
9 changed files with 1013 additions and 69 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,188 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "146.0.1"
},
"browser": {
"name": "Firefox",
"version": "146.0.1"
},
"pages": [
{
"id": "page_2",
"pageTimings": {
"onContentLoad": 721,
"onLoad": 988
},
"startedDateTime": "2026-01-02T20:03:09.325+01:00",
"title": "https://foodsharing.de/store/44975"
}
],
"entries": [
{
"startedDateTime": "2026-01-02T20:03:09.325+01:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://foodsharing.de/api/foodsaver/839246/agenda/2026-01-21T18:55:00.000Z",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "foodsharing.de"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0"
},
{
"name": "Accept",
"value": "application/json, text/plain, */*"
},
{
"name": "Accept-Language",
"value": "de,en-US;q=0.7,en;q=0.3"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br, zstd"
},
{
"name": "Referer",
"value": "https://foodsharing.de/store/44975"
},
{
"name": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
},
{
"name": "Pragma",
"value": "no-cache"
},
{
"name": "Expires",
"value": "0"
},
{
"name": "X-CSRF-Token",
"value": "ee0a04d48f80a5abae3be7b25da22164"
},
{
"name": "sentry-trace",
"value": "437169c7d1dc4761850ba492b55401de-9f89ecfc36ed2bc4-0"
},
{
"name": "baggage",
"value": "sentry-environment=production,sentry-release=ad5e1f003156978d9ba2914bc8b58c75acfb7742,sentry-public_key=88f1f6fc30d10dba9f9459eecd9d3099,sentry-trace_id=437169c7d1dc4761850ba492b55401de,sentry-sampled=false,sentry-sample_rand=0.04415874010402121,sentry-sample_rate=0.01"
},
{
"name": "Sec-Fetch-Dest",
"value": "empty"
},
{
"name": "Sec-Fetch-Mode",
"value": "cors"
},
{
"name": "Sec-Fetch-Site",
"value": "same-origin"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Cookie",
"value": "FS_SESSID=h63ree5vn0sip5rdkdkdiisva5; FS_CSRF_TOKEN=ee0a04d48f80a5abae3be7b25da22164"
},
{
"name": "Priority",
"value": "u=0"
},
{
"name": "TE",
"value": "trailers"
}
],
"cookies": [
{
"name": "FS_SESSID",
"value": "h63ree5vn0sip5rdkdkdiisva5"
},
{
"name": "FS_CSRF_TOKEN",
"value": "ee0a04d48f80a5abae3be7b25da22164"
}
],
"queryString": [],
"headersSize": 1042
},
"response": {
"status": 200,
"statusText": "",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "server",
"value": "nginx"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "cache-control",
"value": "max-age=0, private, must-revalidate"
},
{
"name": "cache-control",
"value": "no-cache, private"
},
{
"name": "date",
"value": "Fri, 02 Jan 2026 19:03:09 GMT"
},
{
"name": "x-nginx-cache",
"value": "BYPASS"
},
{
"name": "content-encoding",
"value": "gzip"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 90,
"text": "[{\"name\":null,\"id\":-1,\"isConfirmed\":false,\"date\":\"2026-01-21 19:55:00\",\"type\":\"proposal\"}]"
},
"redirectURL": "",
"headersSize": 237,
"bodySize": 341
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 42,
"receive": 0
},
"time": 42,
"_securityState": "secure",
"serverIPAddress": "89.238.64.239",
"connection": "443",
"pageref": "page_2"
}
]
}
}

View File

@@ -0,0 +1,188 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "146.0.1"
},
"browser": {
"name": "Firefox",
"version": "146.0.1"
},
"pages": [
{
"id": "page_4",
"pageTimings": {
"onContentLoad": 721,
"onLoad": 988
},
"startedDateTime": "2026-01-02T20:03:09.328+01:00",
"title": "https://foodsharing.de/store/44975"
}
],
"entries": [
{
"startedDateTime": "2026-01-02T20:03:09.328+01:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://foodsharing.de/api/stores/44975/pickupRuleCheck/2026-01-21T18:55:00.000Z/839246",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "foodsharing.de"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0"
},
{
"name": "Accept",
"value": "application/json, text/plain, */*"
},
{
"name": "Accept-Language",
"value": "de,en-US;q=0.7,en;q=0.3"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br, zstd"
},
{
"name": "Referer",
"value": "https://foodsharing.de/store/44975"
},
{
"name": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
},
{
"name": "Pragma",
"value": "no-cache"
},
{
"name": "Expires",
"value": "0"
},
{
"name": "X-CSRF-Token",
"value": "ee0a04d48f80a5abae3be7b25da22164"
},
{
"name": "sentry-trace",
"value": "437169c7d1dc4761850ba492b55401de-ace60c0491b458b9-0"
},
{
"name": "baggage",
"value": "sentry-environment=production,sentry-release=ad5e1f003156978d9ba2914bc8b58c75acfb7742,sentry-public_key=88f1f6fc30d10dba9f9459eecd9d3099,sentry-trace_id=437169c7d1dc4761850ba492b55401de,sentry-sampled=false,sentry-sample_rand=0.04415874010402121,sentry-sample_rate=0.01"
},
{
"name": "Sec-Fetch-Dest",
"value": "empty"
},
{
"name": "Sec-Fetch-Mode",
"value": "cors"
},
{
"name": "Sec-Fetch-Site",
"value": "same-origin"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Cookie",
"value": "FS_SESSID=h63ree5vn0sip5rdkdkdiisva5; FS_CSRF_TOKEN=ee0a04d48f80a5abae3be7b25da22164"
},
{
"name": "Priority",
"value": "u=0"
},
{
"name": "TE",
"value": "trailers"
}
],
"cookies": [
{
"name": "FS_SESSID",
"value": "h63ree5vn0sip5rdkdkdiisva5"
},
{
"name": "FS_CSRF_TOKEN",
"value": "ee0a04d48f80a5abae3be7b25da22164"
}
],
"queryString": [],
"headersSize": 1054
},
"response": {
"status": 200,
"statusText": "",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "server",
"value": "nginx"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "cache-control",
"value": "max-age=0, private, must-revalidate"
},
{
"name": "cache-control",
"value": "no-cache, private"
},
{
"name": "date",
"value": "Fri, 02 Jan 2026 19:03:09 GMT"
},
{
"name": "x-nginx-cache",
"value": "BYPASS"
},
{
"name": "content-encoding",
"value": "gzip"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 15,
"text": "{\"result\":true}"
},
"redirectURL": "",
"headersSize": 237,
"bodySize": 272
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 42,
"receive": 0
},
"time": 42,
"_securityState": "secure",
"serverIPAddress": "89.238.64.239",
"connection": "443",
"pageref": "page_4"
}
]
}
}

View File

@@ -0,0 +1,192 @@
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "146.0.1"
},
"browser": {
"name": "Firefox",
"version": "146.0.1"
},
"pages": [
{
"id": "page_5",
"pageTimings": {
"onContentLoad": 721,
"onLoad": 988
},
"startedDateTime": "2026-01-02T20:03:12.592+01:00",
"title": "https://foodsharing.de/store/44975"
}
],
"entries": [
{
"startedDateTime": "2026-01-02T20:03:12.592+01:00",
"request": {
"bodySize": 0,
"method": "POST",
"url": "https://foodsharing.de/api/stores/44975/pickups/2026-01-21T18:55:00.000Z/839246",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "foodsharing.de"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0"
},
{
"name": "Accept",
"value": "application/json, text/plain, */*"
},
{
"name": "Accept-Language",
"value": "de,en-US;q=0.7,en;q=0.3"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br, zstd"
},
{
"name": "Referer",
"value": "https://foodsharing.de/store/44975"
},
{
"name": "Cache-Control",
"value": "no-cache, no-store, must-revalidate"
},
{
"name": "Pragma",
"value": "no-cache"
},
{
"name": "Expires",
"value": "0"
},
{
"name": "X-CSRF-Token",
"value": "ee0a04d48f80a5abae3be7b25da22164"
},
{
"name": "sentry-trace",
"value": "437169c7d1dc4761850ba492b55401de-a1069f647575db89-0"
},
{
"name": "baggage",
"value": "sentry-environment=production,sentry-release=ad5e1f003156978d9ba2914bc8b58c75acfb7742,sentry-public_key=88f1f6fc30d10dba9f9459eecd9d3099,sentry-trace_id=437169c7d1dc4761850ba492b55401de,sentry-sampled=false,sentry-sample_rand=0.04415874010402121,sentry-sample_rate=0.01"
},
{
"name": "Origin",
"value": "https://foodsharing.de"
},
{
"name": "Sec-Fetch-Dest",
"value": "empty"
},
{
"name": "Sec-Fetch-Mode",
"value": "cors"
},
{
"name": "Sec-Fetch-Site",
"value": "same-origin"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "Cookie",
"value": "FS_SESSID=h63ree5vn0sip5rdkdkdiisva5; FS_CSRF_TOKEN=ee0a04d48f80a5abae3be7b25da22164"
},
{
"name": "Priority",
"value": "u=0"
},
{
"name": "Content-Length",
"value": "0"
},
{
"name": "TE",
"value": "trailers"
}
],
"cookies": [
{
"name": "FS_SESSID",
"value": "h63ree5vn0sip5rdkdkdiisva5"
},
{
"name": "FS_CSRF_TOKEN",
"value": "ee0a04d48f80a5abae3be7b25da22164"
}
],
"queryString": [],
"headersSize": 1098
},
"response": {
"status": 200,
"statusText": "",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "server",
"value": "nginx"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "cache-control",
"value": "max-age=0, private, must-revalidate"
},
{
"name": "cache-control",
"value": "no-cache, private"
},
{
"name": "date",
"value": "Fri, 02 Jan 2026 19:03:13 GMT"
},
{
"name": "content-encoding",
"value": "gzip"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 21,
"text": "{\"isConfirmed\":false}"
},
"redirectURL": "",
"headersSize": 214,
"bodySize": 255
},
"cache": {},
"timings": {
"blocked": -1,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 120,
"receive": 0
},
"time": 120,
"_securityState": "secure",
"serverIPAddress": "89.238.64.239",
"connection": "443",
"pageref": "page_5"
}
]
}
}

View File

@@ -7,7 +7,12 @@ const sessionStore = require('./services/sessionStore');
const credentialStore = require('./services/credentialStore'); const credentialStore = require('./services/credentialStore');
const { readConfig, writeConfig } = require('./services/configStore'); const { readConfig, writeConfig } = require('./services/configStore');
const foodsharingClient = require('./services/foodsharingClient'); const foodsharingClient = require('./services/foodsharingClient');
const { scheduleConfig, runStoreWatchCheck } = require('./services/pickupScheduler'); const {
scheduleConfig,
runStoreWatchCheck,
runImmediatePickupCheck,
runDormantMembershipCheck
} = require('./services/pickupScheduler');
const adminConfig = require('./services/adminConfig'); const adminConfig = require('./services/adminConfig');
const { readNotificationSettings, writeNotificationSettings } = require('./services/userSettingsStore'); const { readNotificationSettings, writeNotificationSettings } = require('./services/userSettingsStore');
const notificationService = require('./services/notificationService'); const notificationService = require('./services/notificationService');
@@ -92,6 +97,7 @@ app.use((req, res, next) => {
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
sessionId: req.session?.id || null, sessionId: req.session?.id || null,
profileId: req.session?.profile?.id || null, profileId: req.session?.profile?.id || null,
profileName: req.session?.profile?.name || null,
responseBody: responseBodySnippet responseBody: responseBodySnippet
}); });
} catch (error) { } catch (error) {
@@ -120,7 +126,7 @@ async function fetchProfileWithCache(session, { force = false } = {}) {
try { try {
const details = await withSessionRetry( const details = await withSessionRetry(
session, session,
() => foodsharingClient.fetchProfile(session.cookieHeader, { throwOnError: true }), () => foodsharingClient.fetchProfile(session.cookieHeader, { throwOnError: true }, session),
{ label: 'fetchProfile' } { label: 'fetchProfile' }
); );
sessionStore.update(session.id, { sessionStore.update(session.id, {
@@ -191,6 +197,15 @@ function mergeStoresIntoConfig(config = [], stores = []) {
return { merged: Array.from(map.values()), changed }; return { merged: Array.from(map.values()), changed };
} }
function getMissingLastPickupStoreIds(config = []) {
if (!Array.isArray(config)) {
return [];
}
return config
.filter((entry) => entry && entry.id && !entry.hidden && !entry.skipDormantCheck && !entry.lastPickupAt)
.map((entry) => String(entry.id));
}
function getCachedRegionStores(regionId) { function getCachedRegionStores(regionId) {
const entry = regionStoreCache.get(String(regionId)); const entry = regionStoreCache.get(String(regionId));
if (!entry) { if (!entry) {
@@ -263,7 +278,7 @@ async function ensureStoreLocationIndex(session, { force = false } = {}) {
if (!payload) { if (!payload) {
const result = await withSessionRetry( const result = await withSessionRetry(
session, session,
() => foodsharingClient.fetchRegionStores(region.id, session.cookieHeader), () => foodsharingClient.fetchRegionStores(region.id, session.cookieHeader, session),
{ label: 'fetchRegionStores' } { label: 'fetchRegionStores' }
); );
payload = { payload = {
@@ -371,7 +386,7 @@ async function refreshStoreStatus(
try { try {
const details = await withSessionRetry( const details = await withSessionRetry(
session, session,
() => foodsharingClient.fetchStoreDetails(storeId, session.cookieHeader), () => foodsharingClient.fetchStoreDetails(storeId, session.cookieHeader, session),
{ label: 'fetchStoreDetails' } { label: 'fetchStoreDetails' }
); );
const status = Number(details?.teamSearchStatus); const status = Number(details?.teamSearchStatus);
@@ -542,14 +557,19 @@ async function runStoreRefreshJob(session, job) {
const stores = await withSessionRetry( const stores = await withSessionRetry(
session, session,
() => () =>
foodsharingClient.fetchStores(session.cookieHeader, session.profile.id, { foodsharingClient.fetchStores(
session.cookieHeader,
session.profile.id,
{
delayBetweenRequestsMs: settings.storePickupCheckDelayMs, delayBetweenRequestsMs: settings.storePickupCheckDelayMs,
onStoreCheck: (store, processed, total) => { onStoreCheck: (store, processed, total) => {
job.processed = processed; job.processed = processed;
job.total = total; job.total = total;
job.currentStore = store.name || `Store ${store.id}`; job.currentStore = store.name || `Store ${store.id}`;
} }
}), },
session
),
{ label: 'fetchStores' } { label: 'fetchStores' }
); );
job.processed = stores.length; job.processed = stores.length;
@@ -567,6 +587,17 @@ async function runStoreRefreshJob(session, job) {
writeConfig(session.profile.id, config); writeConfig(session.profile.id, config);
scheduleWithCurrentSettings(session.id, config); scheduleWithCurrentSettings(session.id, config);
} }
const missingLastPickupStoreIds = getMissingLastPickupStoreIds(config);
if (missingLastPickupStoreIds.length > 0) {
try {
await runDormantMembershipCheck(session.id, { storeIds: missingLastPickupStoreIds });
} catch (error) {
console.warn(
`[DORMANT] Letzte Abholung nach Store-Refresh konnte nicht aktualisiert werden:`,
error.message
);
}
}
job.status = 'done'; job.status = 'done';
job.finishedAt = Date.now(); job.finishedAt = Date.now();
@@ -828,7 +859,7 @@ app.get('/api/store-watch/regions/:regionId/stores', requireAuth, async (req, re
try { try {
const result = await withSessionRetry( const result = await withSessionRetry(
req.session, req.session,
() => foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader), () => foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader, req.session),
{ label: 'fetchRegionStores' } { label: 'fetchRegionStores' }
); );
basePayload = { basePayload = {
@@ -973,6 +1004,15 @@ app.post('/api/config', requireAuth, (req, res) => {
res.json({ success: true }); res.json({ success: true });
}); });
app.post('/api/config/check', requireAuth, (req, res) => {
const config = readConfig(req.session.profile.id);
const settings = adminConfig.readSettings();
runImmediatePickupCheck(req.session.id, config, settings).catch((error) => {
console.error('[PICKUP] Sofortprüfung fehlgeschlagen:', error.message);
});
res.json({ success: true });
});
app.get('/api/notifications/settings', requireAuth, (req, res) => { app.get('/api/notifications/settings', requireAuth, (req, res) => {
const userSettings = readNotificationSettings(req.session.profile.id); const userSettings = readNotificationSettings(req.session.profile.id);
const adminSettings = adminConfig.readSettings(); const adminSettings = adminConfig.readSettings();

View File

@@ -13,13 +13,15 @@ const client = axios.create({
}); });
client.interceptors.request.use((config) => { client.interceptors.request.use((config) => {
config.metadata = { startedAt: Date.now() }; const metadata = config.metadata && typeof config.metadata === 'object' ? config.metadata : {};
config.metadata = { ...metadata, startedAt: Date.now() };
return config; return config;
}); });
client.interceptors.response.use( client.interceptors.response.use(
(response) => { (response) => {
const startedAt = response?.config?.metadata?.startedAt || Date.now(); const startedAt = response?.config?.metadata?.startedAt || Date.now();
const metadata = response?.config?.metadata || {};
try { try {
requestLogStore.add({ requestLogStore.add({
direction: 'outgoing', direction: 'outgoing',
@@ -28,6 +30,9 @@ client.interceptors.response.use(
path: response.config?.url || '', path: response.config?.url || '',
status: response.status, status: response.status,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
sessionId: metadata.sessionId ?? null,
profileId: metadata.profileId ?? null,
profileName: metadata.profileName ?? null,
responseBody: response.data responseBody: response.data
}); });
} catch (error) { } catch (error) {
@@ -37,6 +42,7 @@ client.interceptors.response.use(
}, },
(error) => { (error) => {
const startedAt = error?.config?.metadata?.startedAt || Date.now(); const startedAt = error?.config?.metadata?.startedAt || Date.now();
const metadata = error?.config?.metadata || {};
try { try {
requestLogStore.add({ requestLogStore.add({
direction: 'outgoing', direction: 'outgoing',
@@ -45,6 +51,9 @@ client.interceptors.response.use(
path: error.config?.url || '', path: error.config?.url || '',
status: error?.response?.status || null, status: error?.response?.status || null,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
sessionId: metadata.sessionId ?? null,
profileId: metadata.profileId ?? null,
profileName: metadata.profileName ?? null,
error: error?.message || 'Unbekannter Fehler', error: error?.message || 'Unbekannter Fehler',
responseBody: error?.response?.data responseBody: error?.response?.data
}); });
@@ -55,7 +64,7 @@ client.interceptors.response.use(
} }
); );
const CSRF_COOKIE_NAMES = ['CSRF_TOKEN', 'CSRF-TOKEN', 'XSRF-TOKEN', 'XSRF_TOKEN']; const CSRF_COOKIE_NAMES = ['FS_CSRF_TOKEN', 'CSRF_TOKEN', 'CSRF-TOKEN', 'XSRF-TOKEN', 'XSRF_TOKEN'];
function extractCookieValue(cookies = [], name) { function extractCookieValue(cookies = [], name) {
if (!Array.isArray(cookies) || !name) { if (!Array.isArray(cookies) || !name) {
@@ -114,10 +123,46 @@ function buildHeaders(cookieHeader, csrfToken) {
return headers; return headers;
} }
async function getCurrentUserDetails(cookieHeader) { function buildRequestMetadata(context) {
const response = await client.get('/api/user/current/details', { if (!context) {
headers: buildHeaders(cookieHeader) return {};
}); }
if (context.sessionId || context.profileId) {
return {
sessionId: context.sessionId ?? null,
profileId: context.profileId ?? null,
profileName: context.profileName ?? null
};
}
if (context.id || context.profile?.id) {
return {
sessionId: context.id ?? null,
profileId: context.profile?.id ?? null,
profileName: context.profile?.name ?? null
};
}
return {};
}
function buildRequestConfig({ cookieHeader, csrfToken, context, params } = {}) {
const metadata = buildRequestMetadata(context);
const config = {
headers: buildHeaders(cookieHeader, csrfToken)
};
if (Object.keys(metadata).length > 0) {
config.metadata = metadata;
}
if (params) {
config.params = params;
}
return config;
}
async function getCurrentUserDetails(cookieHeader, context) {
const response = await client.get(
'/api/user/current/details',
buildRequestConfig({ cookieHeader, context })
);
return response.data; return response.data;
} }
@@ -165,9 +210,9 @@ async function login(email, password) {
}; };
} }
async function fetchProfile(cookieHeader, { throwOnError = false } = {}) { async function fetchProfile(cookieHeader, { throwOnError = false } = {}, context) {
try { try {
return await getCurrentUserDetails(cookieHeader); return await getCurrentUserDetails(cookieHeader, context);
} catch (error) { } catch (error) {
if (throwOnError) { if (throwOnError) {
throw error; throw error;
@@ -181,7 +226,7 @@ function wait(ms = 0) {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
async function fetchStores(cookieHeader, profileId, options = {}) { async function fetchStores(cookieHeader, profileId, options = {}, context) {
if (!profileId) { if (!profileId) {
return []; return [];
} }
@@ -193,10 +238,10 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
? options.onStoreCheck ? options.onStoreCheck
: null; : null;
try { try {
const response = await client.get(`/api/user/${profileId}/stores`, { const response = await client.get(
headers: buildHeaders(cookieHeader), `/api/user/${profileId}/stores`,
params: { activeStores: 1 } buildRequestConfig({ cookieHeader, params: { activeStores: 1 }, context })
}); );
const stores = Array.isArray(response.data) ? response.data : []; const stores = Array.isArray(response.data) ? response.data : [];
const normalized = stores.map((store) => ({ const normalized = stores.map((store) => ({
id: String(store.id), id: String(store.id),
@@ -209,14 +254,26 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
zip: store.zip || '' zip: store.zip || ''
})); }));
return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs, onStoreCheck); return annotateStoresWithPickupSlots(
normalized,
cookieHeader,
delayBetweenRequestsMs,
onStoreCheck,
context
);
} catch (error) { } catch (error) {
console.warn('Stores konnten nicht geladen werden:', error.message); console.warn('Stores konnten nicht geladen werden:', error.message);
return []; return [];
} }
} }
async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0, onStoreCheck) { async function annotateStoresWithPickupSlots(
stores,
cookieHeader,
delayBetweenRequestsMs = 0,
onStoreCheck,
context
) {
if (!Array.isArray(stores) || stores.length === 0) { if (!Array.isArray(stores) || stores.length === 0) {
return []; return [];
} }
@@ -238,7 +295,7 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
} }
let hasPickupSlots = null; let hasPickupSlots = null;
try { try {
const pickups = await fetchPickups(store.id, cookieHeader); const pickups = await fetchPickups(store.id, cookieHeader, context);
hasPickupSlots = Array.isArray(pickups) && pickups.length > 0; hasPickupSlots = Array.isArray(pickups) && pickups.length > 0;
} catch (error) { } catch (error) {
const status = error?.response?.status; const status = error?.response?.status;
@@ -255,50 +312,59 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
return annotated; return annotated;
} }
async function fetchPickups(storeId, cookieHeader) { async function fetchPickups(storeId, cookieHeader, context) {
const response = await client.get(`/api/stores/${storeId}/pickups`, { const response = await client.get(
headers: buildHeaders(cookieHeader) `/api/stores/${storeId}/pickups`,
}); buildRequestConfig({ cookieHeader, context })
);
return response.data?.pickups || []; return response.data?.pickups || [];
} }
async function fetchRegionStores(regionId, cookieHeader) { async function fetchRegionStores(regionId, cookieHeader, context) {
if (!regionId) { if (!regionId) {
return { total: 0, stores: [] }; return { total: 0, stores: [] };
} }
const response = await client.get(`/api/region/${regionId}/stores`, { const response = await client.get(
headers: buildHeaders(cookieHeader) `/api/region/${regionId}/stores`,
}); buildRequestConfig({ cookieHeader, context })
);
return { return {
total: Number(response.data?.total) || 0, total: Number(response.data?.total) || 0,
stores: Array.isArray(response.data?.stores) ? response.data.stores : [] stores: Array.isArray(response.data?.stores) ? response.data.stores : []
}; };
} }
async function fetchStoreDetails(storeId, cookieHeader) { async function fetchStoreDetails(storeId, cookieHeader, context) {
if (!storeId) { if (!storeId) {
return null; return null;
} }
const response = await client.get(`/api/map/stores/${storeId}`, { const response = await client.get(
headers: buildHeaders(cookieHeader) `/api/map/stores/${storeId}`,
}); buildRequestConfig({ cookieHeader, context })
);
return response.data || null; return response.data || null;
} }
async function pickupRuleCheck(storeId, utcDate, profileId, session) { async function pickupRuleCheck(storeId, utcDate, profileId, session) {
const response = await client.get(`/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`, { const response = await client.get(
headers: buildHeaders(session.cookieHeader, session.csrfToken) `/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`,
}); buildRequestConfig({
cookieHeader: session.cookieHeader,
csrfToken: session.csrfToken,
context: session
})
);
return response.data?.result === true; return response.data?.result === true;
} }
async function fetchStoreMembers(storeId, cookieHeader) { async function fetchStoreMembers(storeId, cookieHeader, context) {
if (!storeId) { if (!storeId) {
return []; return [];
} }
const response = await client.get(`/api/stores/${storeId}/member`, { const response = await client.get(
headers: buildHeaders(cookieHeader) `/api/stores/${storeId}/member`,
}); buildRequestConfig({ cookieHeader, context })
);
return Array.isArray(response.data) ? response.data : []; return Array.isArray(response.data) ? response.data : [];
} }
@@ -307,7 +373,11 @@ async function bookSlot(storeId, utcDate, profileId, session) {
`/api/stores/${storeId}/pickups/${utcDate}/${profileId}`, `/api/stores/${storeId}/pickups/${utcDate}/${profileId}`,
{}, {},
{ {
headers: buildHeaders(session.cookieHeader, session.csrfToken) ...buildRequestConfig({
cookieHeader: session.cookieHeader,
csrfToken: session.csrfToken,
context: session
})
} }
); );
} }

View File

@@ -357,7 +357,7 @@ async function checkEntry(sessionId, entry, settings) {
try { try {
const pickups = await withSessionRetry( const pickups = await withSessionRetry(
session, session,
() => foodsharingClient.fetchPickups(entry.id, session.cookieHeader), () => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
{ label: 'fetchPickups' } { label: 'fetchPickups' }
); );
let hasProfileId = false; let hasProfileId = false;
@@ -428,7 +428,7 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option
try { try {
const details = await withSessionRetry( const details = await withSessionRetry(
session, session,
() => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader), () => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader, session),
{ label: 'fetchStoreDetails' } { label: 'fetchStoreDetails' }
); );
const status = details?.teamSearchStatus === 1 ? 1 : 0; const status = details?.teamSearchStatus === 1 ? 1 : 0;
@@ -578,6 +578,19 @@ async function runStoreWatchCheck(sessionId, settings, options = {}) {
return checkWatchedStores(sessionId, resolvedSettings, options); return checkWatchedStores(sessionId, resolvedSettings, options);
} }
async function runImmediatePickupCheck(sessionId, config, settings) {
const resolvedSettings = resolveSettings(settings);
const entries = Array.isArray(config) ? config : [];
const activeEntries = entries.filter((entry) => entry?.active);
if (activeEntries.length === 0) {
return { checked: 0 };
}
for (const entry of activeEntries) {
await checkEntry(sessionId, entry, resolvedSettings);
}
return { checked: activeEntries.length };
}
function setMonthOffset(date, offset) { function setMonthOffset(date, offset) {
const copy = new Date(date.getTime()); const copy = new Date(date.getTime());
copy.setMonth(copy.getMonth() + offset); copy.setMonth(copy.getMonth() + offset);
@@ -593,11 +606,14 @@ function getMissingLastPickupStoreIds(config = []) {
.map((entry) => String(entry.id)); .map((entry) => String(entry.id));
} }
async function checkDormantMembers(sessionId) { async function checkDormantMembers(sessionId, options = {}) {
const session = sessionStore.get(sessionId); const session = sessionStore.get(sessionId);
if (!session?.profile?.id) { if (!session?.profile?.id) {
return; return;
} }
const storeIdSet = Array.isArray(options.storeIds)
? new Set(options.storeIds.map((storeId) => String(storeId)))
: null;
const profileId = session.profile.id; const profileId = session.profile.id;
const ensured = await ensureSession(session); const ensured = await ensureSession(session);
if (!ensured) { if (!ensured) {
@@ -615,26 +631,54 @@ async function checkDormantMembers(sessionId) {
}); });
let configChanged = false; let configChanged = false;
const storeTargets = new Map();
config.forEach((entry) => {
if (!entry?.id || entry.hidden) {
return;
}
const storeId = String(entry.id);
if (storeIdSet && !storeIdSet.has(storeId)) {
return;
}
if (skipMap.get(storeId)) {
return;
}
storeTargets.set(storeId, {
storeId,
storeName: entry.label || `Store ${storeId}`
});
});
const stores = Array.isArray(session.storesCache?.data) ? session.storesCache.data : []; const stores = Array.isArray(session.storesCache?.data) ? session.storesCache.data : [];
if (stores.length === 0) { if (stores.length === 0) {
console.warn(`[DORMANT] Keine Stores für Session ${sessionId} im Cache gefunden.`); console.warn(`[DORMANT] Keine Stores für Session ${sessionId} im Cache gefunden.`);
} else {
stores.forEach((store) => {
const storeId = store?.id ? String(store.id) : null;
if (!storeId || !storeTargets.has(storeId)) {
return;
}
const target = storeTargets.get(storeId);
storeTargets.set(storeId, {
...target,
storeName: store.name || target.storeName
});
});
}
if (storeTargets.size === 0) {
return;
} }
const fourMonthsAgo = setMonthOffset(new Date(), -4).getTime(); const fourMonthsAgo = setMonthOffset(new Date(), -4).getTime();
const hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000; const hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000;
for (const store of stores) { for (const target of storeTargets.values()) {
const storeId = store?.id ? String(store.id) : null; const storeId = target.storeId;
if (!storeId) {
continue;
}
if (skipMap.get(storeId)) {
continue;
}
let members = []; let members = [];
try { try {
members = await withSessionRetry( members = await withSessionRetry(
session, session,
() => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader), () => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader, session),
{ label: 'fetchStoreMembers' } { label: 'fetchStoreMembers' }
); );
} catch (error) { } catch (error) {
@@ -671,7 +715,7 @@ async function checkDormantMembers(sessionId) {
try { try {
await sendDormantPickupWarning({ await sendDormantPickupWarning({
profileId, profileId,
storeName: store.name || `Store ${storeId}`, storeName: target.storeName,
storeId, storeId,
reasonLines: reasons reasonLines: reasons
}); });
@@ -722,7 +766,13 @@ function scheduleDormantMembershipCheck(sessionId) {
setTimeout(() => checkDormantMembers(sessionId), randomDelayMs(30, 180)); setTimeout(() => checkDormantMembers(sessionId), randomDelayMs(30, 180));
} }
async function runDormantMembershipCheck(sessionId, options = {}) {
await checkDormantMembers(sessionId, options);
}
module.exports = { module.exports = {
scheduleConfig, scheduleConfig,
runStoreWatchCheck runStoreWatchCheck,
runImmediatePickupCheck,
runDormantMembershipCheck
}; };

View File

@@ -135,6 +135,7 @@ const DebugPage = ({ authorizedFetch }) => {
durationMs: entry.durationMs ?? null, durationMs: entry.durationMs ?? null,
timestamp: entry.timestamp, timestamp: entry.timestamp,
profileId: entry.profileId || null, profileId: entry.profileId || null,
profileName: entry.profileName || null,
sessionId: entry.sessionId || null, sessionId: entry.sessionId || null,
target: entry.target || null, target: entry.target || null,
error: entry.error || null, error: entry.error || null,
@@ -241,12 +242,24 @@ const DebugPage = ({ authorizedFetch }) => {
sortingFn: 'alphanumeric', sortingFn: 'alphanumeric',
filterFn: 'includesString' filterFn: 'includesString'
}), }),
columnHelper.accessor('profileId', { columnHelper.accessor(
header: ({ column }) => <SortableHeader column={column} label="Profil" placeholder="Profil-ID" />, (row) => row.profileName || row.profileId || '',
cell: ({ getValue }) => <span className="text-xs text-gray-600">{getValue() || '—'}</span>, {
id: 'profile',
header: ({ column }) => <SortableHeader column={column} label="Profil" placeholder="Profil-Name" />,
cell: ({ row }) => {
const name = row.original.profileName;
const value = row.original.profileId;
return (
<span className="text-xs text-gray-600" title={value || ''}>
{name || value || '—'}
</span>
);
},
sortingFn: 'alphanumeric', sortingFn: 'alphanumeric',
filterFn: 'includesString' filterFn: 'includesString'
}), }
),
columnHelper.accessor('sessionId', { columnHelper.accessor('sessionId', {
header: ({ column }) => <SortableHeader column={column} label="Session" placeholder="Session-ID" />, header: ({ column }) => <SortableHeader column={column} label="Session" placeholder="Session-ID" />,
cell: ({ getValue }) => { cell: ({ getValue }) => {

View File

@@ -54,6 +54,24 @@ const useConfigManager = ({ sessionToken, authorizedFetch, setStatus, setError,
[authorizedFetch, sessionToken, setError, setStatus] [authorizedFetch, sessionToken, setError, setStatus]
); );
const triggerImmediateCheck = useCallback(async () => {
if (!sessionToken) {
return;
}
try {
const response = await authorizedFetch('/api/config/check', { method: 'POST' });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Unbekannter Fehler bei der Sofortprüfung');
}
} catch (error) {
setError(`Sofortprüfung fehlgeschlagen: ${error.message}`);
}
}, [authorizedFetch, sessionToken, setError]);
const saveConfig = useCallback(async () => { const saveConfig = useCallback(async () => {
if (!sessionToken) { if (!sessionToken) {
return false; return false;
@@ -74,9 +92,10 @@ const useConfigManager = ({ sessionToken, authorizedFetch, setStatus, setError,
if (!result.success) { if (!result.success) {
throw new Error(result.error || 'Unbekannter Fehler beim Speichern'); throw new Error(result.error || 'Unbekannter Fehler beim Speichern');
} }
setStatus('Konfiguration erfolgreich gespeichert!'); setStatus('Konfiguration gespeichert. Sofortprüfung gestartet.');
setTimeout(() => setStatus(''), 3000); setTimeout(() => setStatus(''), 3000);
setIsDirty(false); setIsDirty(false);
triggerImmediateCheck();
return true; return true;
} catch (error) { } catch (error) {
setError(`Fehler beim Speichern: ${error.message}`); setError(`Fehler beim Speichern: ${error.message}`);
@@ -85,7 +104,7 @@ const useConfigManager = ({ sessionToken, authorizedFetch, setStatus, setError,
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [authorizedFetch, sessionToken, setError, setLoading, setStatus]); }, [authorizedFetch, sessionToken, setError, setLoading, setStatus, triggerImmediateCheck]);
return { return {
config, config,