From 1c12bf6bd1b591d315e5a13a783b10c94736b4a7 Mon Sep 17 00:00:00 2001 From: Meik Date: Sun, 4 Jan 2026 00:11:29 +0100 Subject: [PATCH] aktueller stand --- ...75_pickups_Archive [26-01-02 20-04-15].har | 184 +++++++++++++++++ ...55-00.000Z_Archive [26-01-02 20-04-28].har | 188 +++++++++++++++++ ...00Z_839246_Archive [26-01-02 20-04-42].har | 188 +++++++++++++++++ ...00Z_839246_Archive [26-01-02 20-04-51].har | 192 ++++++++++++++++++ server.js | 66 ++++-- services/foodsharingClient.js | 142 +++++++++---- services/pickupScheduler.js | 78 +++++-- src/components/DebugPage.js | 21 +- src/hooks/useConfigManager.js | 23 ++- 9 files changed, 1013 insertions(+), 69 deletions(-) create mode 100644 demo requests/1_foodsharing.de_api_stores_44975_pickups_Archive [26-01-02 20-04-15].har create mode 100644 demo requests/2_foodsharing.de_api_foodsaver_839246_agenda_2026-01-21T18-55-00.000Z_Archive [26-01-02 20-04-28].har create mode 100644 demo requests/3_foodsharing.de_api_stores_44975_pickupRuleCheck_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-42].har create mode 100644 demo requests/4_foodsharing.de_api_stores_44975_pickups_2026-01-21T18-55-00.000Z_839246_Archive [26-01-02 20-04-51].har 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,