aktueller stand
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
54
server.js
54
server.js
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user