diff --git a/demo requests/1_foodsharing.de_api_stores_44975_pickups_Archive [26-01-02 20-04-15].har b/demo requests/1_foodsharing.de_api_stores_44975_pickups_Archive [26-01-02 20-04-15].har
new file mode 100644
index 0000000..b262859
--- /dev/null
+++ b/demo requests/1_foodsharing.de_api_stores_44975_pickups_Archive [26-01-02 20-04-15].har
@@ -0,0 +1,184 @@
+{
+ "log": {
+ "version": "1.2",
+ "creator": {
+ "name": "Firefox",
+ "version": "146.0.1"
+ },
+ "browser": {
+ "name": "Firefox",
+ "version": "146.0.1"
+ },
+ "pages": [
+ {
+ "id": "page_1",
+ "pageTimings": {
+ "onContentLoad": 721,
+ "onLoad": 988
+ },
+ "startedDateTime": "2026-01-02T20:03:04.535+01:00",
+ "title": "https://foodsharing.de/store/44975"
+ }
+ ],
+ "entries": [
+ {
+ "startedDateTime": "2026-01-02T20:03:04.535+01:00",
+ "request": {
+ "bodySize": 0,
+ "method": "GET",
+ "url": "https://foodsharing.de/api/stores/44975/pickups",
+ "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-b50389f4180bcd0f-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": "TE",
+ "value": "trailers"
+ }
+ ],
+ "cookies": [
+ {
+ "name": "FS_SESSID",
+ "value": "h63ree5vn0sip5rdkdkdiisva5"
+ },
+ {
+ "name": "FS_CSRF_TOKEN",
+ "value": "ee0a04d48f80a5abae3be7b25da22164"
+ }
+ ],
+ "queryString": [],
+ "headersSize": 999
+ },
+ "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:04 GMT"
+ },
+ {
+ "name": "x-nginx-cache",
+ "value": "BYPASS"
+ },
+ {
+ "name": "content-encoding",
+ "value": "gzip"
+ },
+ {
+ "name": "X-Firefox-Spdy",
+ "value": "h2"
+ }
+ ],
+ "cookies": [],
+ "content": {
+ "mimeType": "application/json",
+ "size": 5464,
+ "text": "{\"pickups\":[{\"date\":\"2026-01-02T19:25:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":836860,\"name\":\"Martina Rohrer\",\"avatar\":\"\\/api\\/uploads\\/5360ed79-70a0-44d5-ab6b-1c6e24a69fcf\",\"isSleeping\":0,\"mobile\":\"+49 173 6879116\",\"landline\":\"+49 7223 60631\",\"isManager\":false}},{\"isConfirmed\":true,\"profile\":{\"id\":761925,\"name\":\"Monika Krause\",\"avatar\":\"\\/api\\/uploads\\/1d8c9b79-74fa-4176-b5b0-a917ad689eb5\",\"isSleeping\":0,\"mobile\":\"+49 171 1271466\",\"landline\":\"\",\"isManager\":false}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-03T19:25:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":931341,\"name\":\"Silke Pollice\",\"avatar\":\"\\/api\\/uploads\\/8b8b7d8a-3fef-4407-b516-9a45c763cdbb\",\"isSleeping\":0,\"mobile\":\"+49 176 23474446\",\"landline\":\"+49 176 23474446\",\"isManager\":false}},{\"isConfirmed\":true,\"profile\":{\"id\":966870,\"name\":\"Kristina Freund\",\"avatar\":\"\\/api\\/uploads\\/8fbe41d2-ae44-434a-950e-92fdc7aea147\",\"isSleeping\":0,\"mobile\":\"+49 1525 4137943\",\"landline\":\"\",\"isManager\":false}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-05T19:55:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":631065,\"name\":\"Jana Rehn\",\"avatar\":\"\\/api\\/uploads\\/536cbd6d-3798-4e4b-bda3-331c7908a1fa\",\"isSleeping\":0,\"mobile\":\"+4917670247773\",\"landline\":\"+4972225949244\",\"isManager\":false}},{\"isConfirmed\":true,\"profile\":{\"id\":863731,\"name\":\"Sandra R\\u00f6\\u00dfler\",\"avatar\":\"\\/api\\/uploads\\/7f0cbd03-a318-4610-b066-334f3555934a\",\"isSleeping\":0,\"mobile\":\"+49 1575 5899474\",\"landline\":\"\",\"isManager\":false}}],\"isAvailable\":false,\"description\":\"Abholung 19:25h inkl O&G Tafelersatz\"},{\"date\":\"2026-01-07T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":935397,\"name\":\"Ramona Werner\",\"avatar\":\"\\/api\\/uploads\\/61989f48-009d-4ff1-8577-433949311f7f\",\"isSleeping\":0,\"mobile\":\"+49 1516 5137680\",\"landline\":\"+49 7222 5948766\",\"isManager\":false}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-08T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-09T19:25:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":839246,\"name\":\"Meik Drechsler\",\"avatar\":\"\\/api\\/uploads\\/a66e10af-3fa2-47c6-9089-99a122aa2c4d\",\"isSleeping\":0,\"mobile\":\"+49 176 27186806\",\"landline\":\"\",\"isManager\":false}},{\"isConfirmed\":true,\"profile\":{\"id\":680539,\"name\":\"Sabrina Helmst\\u00e4tter\",\"avatar\":\"\\/api\\/uploads\\/d538951b-e0db-4b44-aee6-2f4e3eb08cb0\",\"isSleeping\":0,\"mobile\":\"+49 176 60025988\",\"landline\":\"+49 7204 9479442\",\"isManager\":false}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-10T19:25:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":769765,\"name\":\"Michaela Strickfaden\",\"avatar\":\"\\/api\\/uploads\\/7259f465-37d5-41e2-a770-5951fcf915f3\",\"isSleeping\":0,\"mobile\":\"+49 176 20700924\",\"landline\":\"\",\"isManager\":false}},{\"isConfirmed\":true,\"profile\":{\"id\":935397,\"name\":\"Ramona Werner\",\"avatar\":\"\\/api\\/uploads\\/61989f48-009d-4ff1-8577-433949311f7f\",\"isSleeping\":0,\"mobile\":\"+49 1516 5137680\",\"landline\":\"+49 7222 5948766\",\"isManager\":false}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-12T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-13T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-14T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-15T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":935397,\"name\":\"Ramona Werner\",\"avatar\":\"\\/api\\/uploads\\/61989f48-009d-4ff1-8577-433949311f7f\",\"isSleeping\":0,\"mobile\":\"+49 1516 5137680\",\"landline\":\"+49 7222 5948766\",\"isManager\":false}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-16T19:25:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":818789,\"name\":\"Vera Jonke\",\"avatar\":\"\\/api\\/uploads\\/b244c5e3-9e9a-42b0-8a50-b2bcf355c2ae\",\"isSleeping\":0,\"mobile\":\"+49 1578 8797966\",\"landline\":\"+49 7222 401605\",\"isManager\":false}},{\"isConfirmed\":true,\"profile\":{\"id\":704922,\"name\":\"Marion Barth\",\"avatar\":\"\\/api\\/uploads\\/f2e6f669-895b-45ad-b4b7-bbdb40804e7a\",\"isSleeping\":0,\"mobile\":\"+49 162 9877749\",\"landline\":\"\",\"isManager\":true}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-17T19:25:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[{\"isConfirmed\":true,\"profile\":{\"id\":876794,\"name\":\"Jessica Szpyra\",\"avatar\":\"\\/api\\/uploads\\/698029dd-787e-4ba4-953e-6fb07dc054ec\",\"isSleeping\":0,\"mobile\":\"+49 177 6317270\",\"landline\":\"\",\"isManager\":false}},{\"isConfirmed\":true,\"profile\":{\"id\":877074,\"name\":\"Alexander Bauer\",\"avatar\":\"\\/api\\/uploads\\/34e2c2a8-e639-40e1-ac11-af00f38d940a\",\"isSleeping\":0,\"mobile\":\"+49 1577 2896743\",\"landline\":\"\",\"isManager\":false}}],\"isAvailable\":false,\"description\":null},{\"date\":\"2026-01-19T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-20T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-21T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-22T19:55:00+01:00\",\"totalSlots\":1,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null},{\"date\":\"2026-01-23T19:25:00+01:00\",\"totalSlots\":2,\"occupiedSlots\":[],\"isAvailable\":true,\"description\":null}]}"
+ },
+ "redirectURL": "",
+ "headersSize": 237,
+ "bodySize": 1446
+ },
+ "cache": {},
+ "timings": {
+ "blocked": -1,
+ "dns": 0,
+ "connect": 0,
+ "ssl": 0,
+ "send": 0,
+ "wait": 69,
+ "receive": 0
+ },
+ "time": 69,
+ "_securityState": "secure",
+ "serverIPAddress": "89.238.64.239",
+ "connection": "443",
+ "pageref": "page_1"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/demo requests/2_foodsharing.de_api_foodsaver_839246_agenda_2026-01-21T18-55-00.000Z_Archive [26-01-02 20-04-28].har b/demo requests/2_foodsharing.de_api_foodsaver_839246_agenda_2026-01-21T18-55-00.000Z_Archive [26-01-02 20-04-28].har
new file mode 100644
index 0000000..2871e29
--- /dev/null
+++ b/demo requests/2_foodsharing.de_api_foodsaver_839246_agenda_2026-01-21T18-55-00.000Z_Archive [26-01-02 20-04-28].har
@@ -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"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/demo requests/3_foodsharing.de_api_stores_44975_pickupRuleCheck_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-42].har b/demo requests/3_foodsharing.de_api_stores_44975_pickupRuleCheck_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-42].har
new file mode 100644
index 0000000..e761a7c
--- /dev/null
+++ b/demo requests/3_foodsharing.de_api_stores_44975_pickupRuleCheck_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-42].har
@@ -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"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/demo requests/4_foodsharing.de_api_stores_44975_pickups_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-51].har b/demo requests/4_foodsharing.de_api_stores_44975_pickups_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-51].har
new file mode 100644
index 0000000..b5361f7
--- /dev/null
+++ b/demo requests/4_foodsharing.de_api_stores_44975_pickups_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-51].har
@@ -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"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/server.js b/server.js
index 5df453d..f19d4d4 100644
--- a/server.js
+++ b/server.js
@@ -7,7 +7,12 @@ const sessionStore = require('./services/sessionStore');
const credentialStore = require('./services/credentialStore');
const { readConfig, writeConfig } = require('./services/configStore');
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 { readNotificationSettings, writeNotificationSettings } = require('./services/userSettingsStore');
const notificationService = require('./services/notificationService');
@@ -92,6 +97,7 @@ app.use((req, res, next) => {
durationMs: Date.now() - startedAt,
sessionId: req.session?.id || null,
profileId: req.session?.profile?.id || null,
+ profileName: req.session?.profile?.name || null,
responseBody: responseBodySnippet
});
} catch (error) {
@@ -120,7 +126,7 @@ async function fetchProfileWithCache(session, { force = false } = {}) {
try {
const details = await withSessionRetry(
session,
- () => foodsharingClient.fetchProfile(session.cookieHeader, { throwOnError: true }),
+ () => foodsharingClient.fetchProfile(session.cookieHeader, { throwOnError: true }, session),
{ label: 'fetchProfile' }
);
sessionStore.update(session.id, {
@@ -191,6 +197,15 @@ function mergeStoresIntoConfig(config = [], stores = []) {
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) {
const entry = regionStoreCache.get(String(regionId));
if (!entry) {
@@ -263,7 +278,7 @@ async function ensureStoreLocationIndex(session, { force = false } = {}) {
if (!payload) {
const result = await withSessionRetry(
session,
- () => foodsharingClient.fetchRegionStores(region.id, session.cookieHeader),
+ () => foodsharingClient.fetchRegionStores(region.id, session.cookieHeader, session),
{ label: 'fetchRegionStores' }
);
payload = {
@@ -371,7 +386,7 @@ async function refreshStoreStatus(
try {
const details = await withSessionRetry(
session,
- () => foodsharingClient.fetchStoreDetails(storeId, session.cookieHeader),
+ () => foodsharingClient.fetchStoreDetails(storeId, session.cookieHeader, session),
{ label: 'fetchStoreDetails' }
);
const status = Number(details?.teamSearchStatus);
@@ -542,14 +557,19 @@ async function runStoreRefreshJob(session, job) {
const stores = await withSessionRetry(
session,
() =>
- foodsharingClient.fetchStores(session.cookieHeader, session.profile.id, {
- delayBetweenRequestsMs: settings.storePickupCheckDelayMs,
- onStoreCheck: (store, processed, total) => {
- job.processed = processed;
- job.total = total;
- job.currentStore = store.name || `Store ${store.id}`;
- }
- }),
+ foodsharingClient.fetchStores(
+ session.cookieHeader,
+ session.profile.id,
+ {
+ delayBetweenRequestsMs: settings.storePickupCheckDelayMs,
+ onStoreCheck: (store, processed, total) => {
+ job.processed = processed;
+ job.total = total;
+ job.currentStore = store.name || `Store ${store.id}`;
+ }
+ },
+ session
+ ),
{ label: 'fetchStores' }
);
job.processed = stores.length;
@@ -567,6 +587,17 @@ async function runStoreRefreshJob(session, job) {
writeConfig(session.profile.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.finishedAt = Date.now();
@@ -828,7 +859,7 @@ app.get('/api/store-watch/regions/:regionId/stores', requireAuth, async (req, re
try {
const result = await withSessionRetry(
req.session,
- () => foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader),
+ () => foodsharingClient.fetchRegionStores(regionId, req.session.cookieHeader, req.session),
{ label: 'fetchRegionStores' }
);
basePayload = {
@@ -973,6 +1004,15 @@ app.post('/api/config', requireAuth, (req, res) => {
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) => {
const userSettings = readNotificationSettings(req.session.profile.id);
const adminSettings = adminConfig.readSettings();
diff --git a/services/foodsharingClient.js b/services/foodsharingClient.js
index ce17746..84b0740 100644
--- a/services/foodsharingClient.js
+++ b/services/foodsharingClient.js
@@ -13,13 +13,15 @@ const client = axios.create({
});
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;
});
client.interceptors.response.use(
(response) => {
const startedAt = response?.config?.metadata?.startedAt || Date.now();
+ const metadata = response?.config?.metadata || {};
try {
requestLogStore.add({
direction: 'outgoing',
@@ -28,6 +30,9 @@ client.interceptors.response.use(
path: response.config?.url || '',
status: response.status,
durationMs: Date.now() - startedAt,
+ sessionId: metadata.sessionId ?? null,
+ profileId: metadata.profileId ?? null,
+ profileName: metadata.profileName ?? null,
responseBody: response.data
});
} catch (error) {
@@ -37,6 +42,7 @@ client.interceptors.response.use(
},
(error) => {
const startedAt = error?.config?.metadata?.startedAt || Date.now();
+ const metadata = error?.config?.metadata || {};
try {
requestLogStore.add({
direction: 'outgoing',
@@ -45,6 +51,9 @@ client.interceptors.response.use(
path: error.config?.url || '',
status: error?.response?.status || null,
durationMs: Date.now() - startedAt,
+ sessionId: metadata.sessionId ?? null,
+ profileId: metadata.profileId ?? null,
+ profileName: metadata.profileName ?? null,
error: error?.message || 'Unbekannter Fehler',
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) {
if (!Array.isArray(cookies) || !name) {
@@ -114,10 +123,46 @@ function buildHeaders(cookieHeader, csrfToken) {
return headers;
}
-async function getCurrentUserDetails(cookieHeader) {
- const response = await client.get('/api/user/current/details', {
- headers: buildHeaders(cookieHeader)
- });
+function buildRequestMetadata(context) {
+ if (!context) {
+ 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;
}
@@ -165,9 +210,9 @@ async function login(email, password) {
};
}
-async function fetchProfile(cookieHeader, { throwOnError = false } = {}) {
+async function fetchProfile(cookieHeader, { throwOnError = false } = {}, context) {
try {
- return await getCurrentUserDetails(cookieHeader);
+ return await getCurrentUserDetails(cookieHeader, context);
} catch (error) {
if (throwOnError) {
throw error;
@@ -181,7 +226,7 @@ function wait(ms = 0) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
-async function fetchStores(cookieHeader, profileId, options = {}) {
+async function fetchStores(cookieHeader, profileId, options = {}, context) {
if (!profileId) {
return [];
}
@@ -193,10 +238,10 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
? options.onStoreCheck
: null;
try {
- const response = await client.get(`/api/user/${profileId}/stores`, {
- headers: buildHeaders(cookieHeader),
- params: { activeStores: 1 }
- });
+ const response = await client.get(
+ `/api/user/${profileId}/stores`,
+ buildRequestConfig({ cookieHeader, params: { activeStores: 1 }, context })
+ );
const stores = Array.isArray(response.data) ? response.data : [];
const normalized = stores.map((store) => ({
id: String(store.id),
@@ -209,14 +254,26 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
zip: store.zip || ''
}));
- return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs, onStoreCheck);
+ return annotateStoresWithPickupSlots(
+ normalized,
+ cookieHeader,
+ delayBetweenRequestsMs,
+ onStoreCheck,
+ context
+ );
} catch (error) {
console.warn('Stores konnten nicht geladen werden:', error.message);
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) {
return [];
}
@@ -238,7 +295,7 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
}
let hasPickupSlots = null;
try {
- const pickups = await fetchPickups(store.id, cookieHeader);
+ const pickups = await fetchPickups(store.id, cookieHeader, context);
hasPickupSlots = Array.isArray(pickups) && pickups.length > 0;
} catch (error) {
const status = error?.response?.status;
@@ -255,50 +312,59 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
return annotated;
}
-async function fetchPickups(storeId, cookieHeader) {
- const response = await client.get(`/api/stores/${storeId}/pickups`, {
- headers: buildHeaders(cookieHeader)
- });
+async function fetchPickups(storeId, cookieHeader, context) {
+ const response = await client.get(
+ `/api/stores/${storeId}/pickups`,
+ buildRequestConfig({ cookieHeader, context })
+ );
return response.data?.pickups || [];
}
-async function fetchRegionStores(regionId, cookieHeader) {
+async function fetchRegionStores(regionId, cookieHeader, context) {
if (!regionId) {
return { total: 0, stores: [] };
}
- const response = await client.get(`/api/region/${regionId}/stores`, {
- headers: buildHeaders(cookieHeader)
- });
+ const response = await client.get(
+ `/api/region/${regionId}/stores`,
+ buildRequestConfig({ cookieHeader, context })
+ );
return {
total: Number(response.data?.total) || 0,
stores: Array.isArray(response.data?.stores) ? response.data.stores : []
};
}
-async function fetchStoreDetails(storeId, cookieHeader) {
+async function fetchStoreDetails(storeId, cookieHeader, context) {
if (!storeId) {
return null;
}
- const response = await client.get(`/api/map/stores/${storeId}`, {
- headers: buildHeaders(cookieHeader)
- });
+ const response = await client.get(
+ `/api/map/stores/${storeId}`,
+ buildRequestConfig({ cookieHeader, context })
+ );
return response.data || null;
}
async function pickupRuleCheck(storeId, utcDate, profileId, session) {
- const response = await client.get(`/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`, {
- headers: buildHeaders(session.cookieHeader, session.csrfToken)
- });
+ const response = await client.get(
+ `/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`,
+ buildRequestConfig({
+ cookieHeader: session.cookieHeader,
+ csrfToken: session.csrfToken,
+ context: session
+ })
+ );
return response.data?.result === true;
}
-async function fetchStoreMembers(storeId, cookieHeader) {
+async function fetchStoreMembers(storeId, cookieHeader, context) {
if (!storeId) {
return [];
}
- const response = await client.get(`/api/stores/${storeId}/member`, {
- headers: buildHeaders(cookieHeader)
- });
+ const response = await client.get(
+ `/api/stores/${storeId}/member`,
+ buildRequestConfig({ cookieHeader, context })
+ );
return Array.isArray(response.data) ? response.data : [];
}
@@ -307,7 +373,11 @@ async function bookSlot(storeId, utcDate, profileId, session) {
`/api/stores/${storeId}/pickups/${utcDate}/${profileId}`,
{},
{
- headers: buildHeaders(session.cookieHeader, session.csrfToken)
+ ...buildRequestConfig({
+ cookieHeader: session.cookieHeader,
+ csrfToken: session.csrfToken,
+ context: session
+ })
}
);
}
diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js
index 58209eb..c093ea6 100644
--- a/services/pickupScheduler.js
+++ b/services/pickupScheduler.js
@@ -357,7 +357,7 @@ async function checkEntry(sessionId, entry, settings) {
try {
const pickups = await withSessionRetry(
session,
- () => foodsharingClient.fetchPickups(entry.id, session.cookieHeader),
+ () => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
{ label: 'fetchPickups' }
);
let hasProfileId = false;
@@ -428,7 +428,7 @@ async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, option
try {
const details = await withSessionRetry(
session,
- () => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader),
+ () => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader, session),
{ label: 'fetchStoreDetails' }
);
const status = details?.teamSearchStatus === 1 ? 1 : 0;
@@ -578,6 +578,19 @@ async function runStoreWatchCheck(sessionId, settings, 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) {
const copy = new Date(date.getTime());
copy.setMonth(copy.getMonth() + offset);
@@ -593,11 +606,14 @@ function getMissingLastPickupStoreIds(config = []) {
.map((entry) => String(entry.id));
}
-async function checkDormantMembers(sessionId) {
+async function checkDormantMembers(sessionId, options = {}) {
const session = sessionStore.get(sessionId);
if (!session?.profile?.id) {
return;
}
+ const storeIdSet = Array.isArray(options.storeIds)
+ ? new Set(options.storeIds.map((storeId) => String(storeId)))
+ : null;
const profileId = session.profile.id;
const ensured = await ensureSession(session);
if (!ensured) {
@@ -615,26 +631,54 @@ async function checkDormantMembers(sessionId) {
});
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 : [];
if (stores.length === 0) {
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 hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000;
- for (const store of stores) {
- const storeId = store?.id ? String(store.id) : null;
- if (!storeId) {
- continue;
- }
- if (skipMap.get(storeId)) {
- continue;
- }
+ for (const target of storeTargets.values()) {
+ const storeId = target.storeId;
let members = [];
try {
members = await withSessionRetry(
session,
- () => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader),
+ () => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader, session),
{ label: 'fetchStoreMembers' }
);
} catch (error) {
@@ -671,7 +715,7 @@ async function checkDormantMembers(sessionId) {
try {
await sendDormantPickupWarning({
profileId,
- storeName: store.name || `Store ${storeId}`,
+ storeName: target.storeName,
storeId,
reasonLines: reasons
});
@@ -722,7 +766,13 @@ function scheduleDormantMembershipCheck(sessionId) {
setTimeout(() => checkDormantMembers(sessionId), randomDelayMs(30, 180));
}
+async function runDormantMembershipCheck(sessionId, options = {}) {
+ await checkDormantMembers(sessionId, options);
+}
+
module.exports = {
scheduleConfig,
- runStoreWatchCheck
+ runStoreWatchCheck,
+ runImmediatePickupCheck,
+ runDormantMembershipCheck
};
diff --git a/src/components/DebugPage.js b/src/components/DebugPage.js
index 8a95625..561b85b 100644
--- a/src/components/DebugPage.js
+++ b/src/components/DebugPage.js
@@ -135,6 +135,7 @@ const DebugPage = ({ authorizedFetch }) => {
durationMs: entry.durationMs ?? null,
timestamp: entry.timestamp,
profileId: entry.profileId || null,
+ profileName: entry.profileName || null,
sessionId: entry.sessionId || null,
target: entry.target || null,
error: entry.error || null,
@@ -241,12 +242,24 @@ const DebugPage = ({ authorizedFetch }) => {
sortingFn: 'alphanumeric',
filterFn: 'includesString'
}),
- columnHelper.accessor('profileId', {
- header: ({ column }) => ,
- cell: ({ getValue }) => {getValue() || '—'},
+ columnHelper.accessor(
+ (row) => row.profileName || row.profileId || '',
+ {
+ id: 'profile',
+ header: ({ column }) => ,
+ cell: ({ row }) => {
+ const name = row.original.profileName;
+ const value = row.original.profileId;
+ return (
+
+ {name || value || '—'}
+
+ );
+ },
sortingFn: 'alphanumeric',
filterFn: 'includesString'
- }),
+ }
+ ),
columnHelper.accessor('sessionId', {
header: ({ column }) => ,
cell: ({ getValue }) => {
diff --git a/src/hooks/useConfigManager.js b/src/hooks/useConfigManager.js
index 42c59bd..97af1b9 100644
--- a/src/hooks/useConfigManager.js
+++ b/src/hooks/useConfigManager.js
@@ -54,6 +54,24 @@ const useConfigManager = ({ sessionToken, authorizedFetch, setStatus, setError,
[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 () => {
if (!sessionToken) {
return false;
@@ -74,9 +92,10 @@ const useConfigManager = ({ sessionToken, authorizedFetch, setStatus, setError,
if (!result.success) {
throw new Error(result.error || 'Unbekannter Fehler beim Speichern');
}
- setStatus('Konfiguration erfolgreich gespeichert!');
+ setStatus('Konfiguration gespeichert. Sofortprüfung gestartet.');
setTimeout(() => setStatus(''), 3000);
setIsDirty(false);
+ triggerImmediateCheck();
return true;
} catch (error) {
setError(`Fehler beim Speichern: ${error.message}`);
@@ -85,7 +104,7 @@ const useConfigManager = ({ sessionToken, authorizedFetch, setStatus, setError,
} finally {
setLoading(false);
}
- }, [authorizedFetch, sessionToken, setError, setLoading, setStatus]);
+ }, [authorizedFetch, sessionToken, setError, setLoading, setStatus, triggerImmediateCheck]);
return {
config,