diff --git a/.env b/.env index 41a5f72..674608e 100644 --- a/.env +++ b/.env @@ -1,9 +1,5 @@ # MQTT-Konfiguration -MQTT_BROKER=mqtt://iobroker:1884 -MQTT_TOPIC=foodsharing/pickupCheck/config/meik -MQTT_USER=mqtt -MQTT_PASSWORD=mqtt!1884! - +ADMIN_EMAIL=meikdre@gmx.de # Server-Konfiguration PORT=3000 NODE_ENV=production diff --git a/config/839246-pickup-config.json b/config/839246-pickup-config.json new file mode 100644 index 0000000..8c8afa4 --- /dev/null +++ b/config/839246-pickup-config.json @@ -0,0 +1,170 @@ +[ + { + "id": "44972", + "label": "Aldi Süd RA Biblisweg", + "active": false, + "checkProfileId": true, + "onlyNotify": true + }, + { + "id": "44975", + "label": "Aldi Süd RA Kuppenheim", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "44971", + "label": "Aldi Süd RA LützowerStr.", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "63367", + "label": "Arena Balkan Bäckerei", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "59378", + "label": "Backwaren Kaufland", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "49712", + "label": "Baden-Baden foodsharing", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "42264", + "label": "Bildungshaus St. Bernhard, Rastatt", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "43191", + "label": "Café Böckeler Baden-Baden", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "33875", + "label": "CAP-Markt Sandweier", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "28513", + "label": "denn's Biomarkt", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "42322", + "label": "Edeka Fischer Haueneberstein", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "40082", + "label": "Ernteaktionen rund um Baden-Baden", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "40261", + "label": "Eventbetrieb Rastatt/Baden-Baden", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "51450", + "label": "Hornbach KA-Grünwinkel", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "62551", + "label": "Koordinationsbetrieb Fairteiler Sandweier ", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "53552", + "label": "Koordinationsbetrieb Fairteiler Spitalkirche", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "43754", + "label": "Koordinationsbetrieb Lebensmittelspenden für Geflüchtete bei Sinzheim", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "66125", + "label": "Murgtal Foodsharing", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "62533", + "label": "New Pop-Festival", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "31602", + "label": "Notfallteam Rastatt/Baden-Baden / spontane Abholungen", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "63448", + "label": "Penny Baden-Oos", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "41393", + "label": "Weihnachtsmarkt Baden-Baden", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "22787", + "label": "Weihnachtsmarkt Rastatt", + "active": false, + "checkProfileId": true, + "onlyNotify": false + }, + { + "id": "36100", + "label": "Bäckerei Späth", + "active": false, + "checkProfileId": true, + "onlyNotify": false + } +] \ No newline at end of file diff --git a/config/admin-settings.json b/config/admin-settings.json new file mode 100644 index 0000000..af162f6 --- /dev/null +++ b/config/admin-settings.json @@ -0,0 +1,13 @@ +{ + "scheduleCron": "*/10 7-22 * * *", + "randomDelayMinSeconds": 10, + "randomDelayMaxSeconds": 120, + "initialDelayMinSeconds": 5, + "initialDelayMaxSeconds": 30, + "ignoredSlots": [ + { + "storeId": "51450", + "description": "TVS" + } + ] +} \ No newline at end of file diff --git a/config/credentials.json b/config/credentials.json new file mode 100644 index 0000000..c7589de --- /dev/null +++ b/config/credentials.json @@ -0,0 +1,7 @@ +{ + "839246": { + "email": "meikdre@gmx.de", + "password": "R67aJUj2-wWVfP8", + "token": "1fdccfbe-2182-4749-9f42-ac79345c143d" + } +} \ No newline at end of file diff --git a/data/defaultConfig.js b/data/defaultConfig.js new file mode 100644 index 0000000..e0a30c5 --- /dev/null +++ b/data/defaultConfig.js @@ -0,0 +1 @@ +module.exports = []; diff --git a/iobrokerSkript.js b/iobrokerSkript.js deleted file mode 100644 index 8b35527..0000000 --- a/iobrokerSkript.js +++ /dev/null @@ -1,390 +0,0 @@ -const weekdayMap={Montag:"Monday",Dienstag:"Tuesday",Mittwoch:"Wednesday",Donnerstag:"Thursday",Freitag:"Friday",Samstag:"Saturday",Sonntag:"Sunday"}; - -const configState = 'mqtt.0.foodsharing.pickupCheck.config.meik'; -let scheduledJobs = []; - -// Zeitpläne aufräumen -function clearAllScheduledJobs() { - scheduledJobs.forEach(job => clearSchedule(job)); - scheduledJobs = []; -} - -// Neue Funktion: Config aus dem Datenpunkt parsen -function parseConfigState() { - try { - const raw = getState(configState)?.val; - if (!raw) throw new Error("Leerer Konfigurationswert"); - - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) throw new Error("Konfiguration ist kein Array"); - - return parsed; - } catch (e) { - console.error("Fehler beim Parsen der Konfiguration:", e.message); - sendTelegramMessage(telegramInstance, `? Fehler beim Parsen der Pickup-Konfiguration: ${e.message}`); - return []; - } -} - -// Pickup-Checks ausführen mit aktueller Konfiguration -function setupPickupCheckSchedules() { - clearAllScheduledJobs(); // Vorherige Tasks stoppen - const pickupCheckConfig = parseConfigState(); - - pickupCheckConfig.forEach(({ id, active, onlyNotify, label, desiredWeekday, desiredDate, checkProfileId }) => { - if (active) { - const storeLabel = label || masterStores[id] || "Unbekannter Store"; - const translatedWeekday = desiredWeekday && weekdayMap[desiredWeekday] ? weekdayMap[desiredWeekday] : desiredWeekday; - - const job = scheduleWithRandomDelay('*/10 7-22 * * *', () => - handleLoginAndRequests( - (storeIds, session) => { - console.log(`Prüfung für ${storeLabel} (${id}) – Nur Benachrichtigung: ${onlyNotify}` + - (translatedWeekday ? ` – nur an ${translatedWeekday}` : '') + - ` – checkProfileId: ${checkProfileId}`); - checkPickupData(id, !!checkProfileId, session, onlyNotify, translatedWeekday, desiredDate); - }, - null - ) - ); - scheduledJobs.push(job); // Job merken - } - }); -} -// Zentrale Definition der Stores mit ID und sprechendem Namen -const masterStores = { - "51485": "Hornbach KA - Hagsfeld", - "43694": "Blumen Deniz Rastatt", - "32807": "Claus Reformwaren", - "47510": "BIOLAND GÄRTNEREI LUTZ", - "42141": "Restaurant Poké You", - "37245": "Erdbeerland Enderle GBR", - "35401": "Kalinka - Internationale Lebensmittel", - "31080": "Gemüsebau Gabelmann", - "28513": "denn's Biomarkt", - // Weitere Stores, die nur in Pickup-Checks genutzt werden: - "63448": "Penny Baden-Oos", - "42264": "Bildungshaus St. Bernhard, Rastatt", - "51450": "Hornbach KA - Grünwinkel", // Für diese StoreID gilt die TVS-Ausnahme - "42322": "Edeka Fischer Haueneberstein", - "33875": "Cap Markt", - "44972": "Aldi Biblisweg", - "44975": "Aldi Kuppenheim" - -}; - -// Extrahierte Store-IDs als Array für sendFoodSharingRequests -const storeIds = [ - //"51485", //Hornbach KA -Hagsfeld - //"51450", //Hornbach KA-Grünwinkel - "43694", //Blumen Deniz Rastatt - "32807", //Claus Reformwaren - "47510", //BIOLAND GÄRTNEREI LUTZ - "42141", //Restaurant Poké You - "37245", //Erdbeerland Enderle GBR - "35401", //Kalinka - Internationale Lebensmittel - "31080", //Gemüsebau Gabelmann - "28513", //denn's Biomarkt - //"29374", //Rastatter Wochenmarkt -]; - -// Anmeldedaten -const userEmail = "ddda@gmx.de"; -const userPassword = "a"; - -// Telegram-Bot-Instanz und Profil-ID (für Pickup-Prüfung) -const telegramInstance = 'telegram.0'; -const profileId = 839246; - -// Globale Variablen für Session und CSRF-Token -let sessionCookies = null; -let csrfToken = null; - -// Funktion zum Extrahieren des CSRF-Tokens aus den Cookies -function extractCsrfToken(cookies) { - let token = null; - cookies.forEach(cookieStr => { - if (cookieStr.includes("CSRF_TOKEN=")) { - // Beispiel-Cookie: "CSRF_TOKEN=wert; Path=/; Domain=foodsharing.de; ..." - token = cookieStr.split(';')[0].split('=')[1]; - } - }); - return token; -} - -// Funktion zum zufällig verzögerten Start des Skripts -function startScriptWithRandomDelay(random = false) { - const maxDelay = random ? 10 * 60 * 1000 : 1000; // bis zu 10 Minuten Verzögerung - const delay = Math.floor(Math.random() * maxDelay); - console.log(`Das Skript wird in ${Math.round(delay / 1000)} Sekunden gestartet.`); - setTimeout(() => { - // sendFoodSharingRequests nutzt hier die zentrale masterStores-Liste - handleLoginAndRequests(sendFoodSharingRequests, storeIds); - }, delay); -} - -// Session-Handling: Login und anschließende Requests ausführen -function handleLoginAndRequests(callback, storeIds = null) { - checkSessionValidity().then(isValid => { - if (!isValid) { - login(userEmail, userPassword).then(() => { - callback(storeIds, sessionCookies); - }).catch(error => { - console.error(`Login-Fehler: ${error}`); - sendTelegramMessage(telegramInstance, "Login fehlgeschlagen."); - }); - } else { - callback(storeIds, sessionCookies); - } - }); -} - -// Überprüfung der Session-Gültigkeit -async function checkSessionValidity() { - if (!sessionCookies) { - return false; - } - const axios = require('axios'); - const headers = { - "cookie": sessionCookies - }; - try { - const response = await axios.get('https://foodsharing.de/api/wall/foodsaver/839246?limit=1', { headers }); - return response.status === 200; - } catch (error) { - return false; - } -} - -// Login-Funktion inkl. CSRF-Token-Extraktion -async function login(userEmail, userPassword) { - const axios = require('axios'); - const loginData = { - email: userEmail, - password: userPassword, - remember_me: true - }; - const headers = { - "sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"", - "Referer": "https://foodsharing.de/", - "DNT": "1", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "Content-Type": "application/json; charset=utf-8" - }; - - const response = await axios.post('https://foodsharing.de/api/user/login', loginData, { headers }); - if (response.status === 200) { - sessionCookies = response.headers['set-cookie']; // Alle Cookies speichern - csrfToken = extractCsrfToken(sessionCookies); // CSRF-Token extrahieren - console.log("Login erfolgreich. CSRF Token: " + csrfToken); - } else { - throw new Error("Login fehlgeschlagen."); - } -} - -// Funktion zum Senden der FoodSharing-Anfragen (zeigt Ergebnisse per Telegram an) -function sendFoodSharingRequests(storeIds, session) { - const axios = require('axios'); - let results = []; - let errors = []; - let shouldSendMessage = false; - - storeIds.forEach(storeId => { - const storeUrl = `https://foodsharing.de/api/map/stores/${storeId}`; - const storeHeaders = { - "cookie": session - }; - - axios.get(storeUrl, { headers: storeHeaders }) - .then(storeResponse => { - const storeData = storeResponse.data; - if (storeData.maySendRequest) { - shouldSendMessage = true; - } - results.push({ - ID: storeId, - Name: masterStores[storeId] || storeData.name, - AnmeldungOffen: storeData.maySendRequest ? "Ja" : "Nein" - }); - - if (results.length + errors.length === storeIds.length) { - if (shouldSendMessage || errors.length > 0) { - sendResultsViaTelegram(results, errors); - } - } - }) - .catch(error => { - errors.push(`Fehler bei der Store-Anfrage für Store ${storeId} (${masterStores[storeId] || "unbekannt"}): ${error.message}`); - if (results.length + errors.length === storeIds.length) { - sendResultsViaTelegram(results, errors); - } - }); - }); -} - -// Erweiterte Funktion zur Prüfung der Pickup-Daten mit automatischer Buchung -function checkPickupData(storeId, checkProfileId, session, onlyNotify = false, desiredWeekday = null, desiredDate = null) { - const axios = require('axios'); - const storeUrl = `https://foodsharing.de/api/stores/${storeId}/pickups`; - const headers = { "cookie": session }; - - axios.get(storeUrl, { headers }) - .then(response => { - const pickups = response.data.pickups; - let hasProfileId = false; - let availablePickup = null; - - pickups.forEach(pickup => { - const pickupDate = new Date(pickup.date); - const weekday = pickupDate.toLocaleDateString("en-US", { weekday: 'long' }); - - let matchesFilter = true; - - if (desiredDate) { - const desired = new Date(desiredDate); - matchesFilter = pickupDate.getFullYear() === desired.getFullYear() - && pickupDate.getMonth() === desired.getMonth() - && pickupDate.getDate() === desired.getDate(); - } - - if (desiredWeekday && !matchesFilter) { - matchesFilter = weekday === desiredWeekday; - } - - if (!matchesFilter) return; - - if (checkProfileId && pickup.occupiedSlots.some(slot => slot.profile.id === profileId)) { - hasProfileId = true; - } else if (pickup.isAvailable && !availablePickup) { - availablePickup = pickup; - } - }); - - // Sonderfall TVS für StoreID 51450 - if (storeId === "51450" && availablePickup && availablePickup.description === "TVS") { - console.log(`Slot mit Beschreibung "TVS" gefunden für Store ${storeId}. Buchung wird übersprungen.`); - return; - } - - if ((!checkProfileId || !hasProfileId) && availablePickup) { - const localDate = availablePickup.date; - const utcDate = new Date(localDate).toISOString(); - const readableDate = new Date(localDate).toLocaleString("de-DE"); - - if (onlyNotify) { - console.log(`Benachrichtigung: Freier Slot für ${masterStores[storeId] || storeId} am ${readableDate}`); - sendTelegramMessage(telegramInstance, `?? Freier Slot gefunden für ${masterStores[storeId] || storeId} am ${readableDate} (nur Benachrichtigung).`); - } else { - bookSlot(storeId, utcDate, localDate); - } - } else { - console.log(`Kein passender Slot für Store ${storeId} (${masterStores[storeId] || "unbekannt"}) gefunden.`); - } - }) - .catch(error => { - console.error(`Fehler bei Pickup-Abfrage für Store ${storeId}: ${error.message}`); - }); -} - - -// Funktion zum automatischen Buchen eines freien Slots -function bookSlot(storeId, utcDate, localDate) { - const axios = require('axios'); - console.log(`Starte Buchung für Store ${storeId} (${masterStores[storeId] || "unbekannt"}) am ${localDate} (UTC: ${utcDate})...`); - - // 1. Pickup-Rule Check durchführen - axios.get(`https://foodsharing.de/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`, { - headers: { - "cookie": sessionCookies, - "x-csrf-token": csrfToken - } - }) - .then(ruleResponse => { - if (ruleResponse.data.result === true) { - // 2. Slot buchen, wenn Rule Check erfolgreich war - axios.post(`https://foodsharing.de/api/stores/${storeId}/pickups/${utcDate}/${profileId}`, {}, { - headers: { - "cookie": sessionCookies, - "Content-Type": "application/json; charset=utf-8", - "x-csrf-token": csrfToken - } - }) - .then(bookingResponse => { - console.log("Buchung Response:", bookingResponse.data); - sendTelegramMessage(telegramInstance, `Slot gebucht für Store ${storeId} (${masterStores[storeId] || "unbekannt"}) am ${localDate}`); - }) - .catch(bookingError => { - console.error("Buchung Fehler:", bookingError.message); - sendTelegramMessage(telegramInstance, `Buchungsfehler für Store ${storeId} (${masterStores[storeId] || "unbekannt"}): ${bookingError.message}`); - }); - } else { - console.log("Pickup Rule Check fehlgeschlagen."); - sendTelegramMessage(telegramInstance, `Pickup Rule Check fehlgeschlagen für Store ${storeId} (${masterStores[storeId] || "unbekannt"}) am ${localDate}`); - } - }) - .catch(ruleError => { - console.error("Fehler beim Pickup Rule Check:", ruleError.message); - sendTelegramMessage(telegramInstance, `Fehler beim Pickup Rule Check für Store ${storeId} (${masterStores[storeId] || "unbekannt"}): ${ruleError.message}`); - }); -} - -// Funktion zum Senden der Ergebnisse und Fehler per Telegram -function sendResultsViaTelegram(results, errors) { - let message = "Ergebnisse der FoodSharing-Anfragen:\n\n"; - - if (results.length > 0) { - message += "Erfolg:\n
ID | Name | Anmeldung offen\n";
- message += "--------|------------------------------------|-----------------\n";
- results.forEach(result => {
- message += `${result.ID.padEnd(8)}| ${result.Name.padEnd(36)}| ${result.AnmeldungOffen}\n`;
- });
- message += "\n";
- } else {
- message += "Keine erfolgreichen Anfragen.\n";
- }
-
- if (errors.length > 0) {
- message += "\nFehler:\n";
- errors.forEach(error => {
- message += `${error}\n`;
- });
- }
-
- sendTelegramMessage(telegramInstance, message, true);
-}
-
-// Funktion zum Senden einer Nachricht per Telegram
-function sendTelegramMessage(instance, message, isHtml = false) {
- const options = { text: message };
- if (isHtml) {
- options.parse_mode = 'HTML';
- }
- sendTo(instance, options);
-}
-
-// Funktion zur Planung mit zufälliger Verzögerung
-function scheduleWithRandomDelay(cronExpression, callback) {
- const job = schedule(cronExpression, function () {
- let delay = Math.floor(Math.random() * 110 * 1000) + 10 * 1000; // 10s bis 120s
- setTimeout(callback, delay);
- });
- return job;
-}
-
-// Zufälliger Start des Skripts
-startScriptWithRandomDelay();
-
-// Zeitplan: Täglich um 7 Uhr morgens bzw. 15 Uhr (hier per Cron-Ausdruck)
-schedule('0 7,15 * * *', function () {
- startScriptWithRandomDelay(true);
-});
-
-// Initialisierung
-setupPickupCheckSchedules();
-
-// Trigger bei Änderungen des Datenpunkts
-on({ id: configState, change: "any" }, () => {
- console.log("?? Konfiguration geändert. Pickup-Checks werden neu geladen.");
- setupPickupCheckSchedules();
-});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 3c975e8..d754262 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,12 +12,14 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.7.7",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
- "mqtt": "^5.3.0",
+ "node-cron": "^3.0.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+ "uuid": "^11.0.3",
"web-vitals": "^2.1.4"
},
"devDependencies": {
@@ -87,6 +89,7 @@
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -831,6 +834,7 @@
"integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1753,6 +1757,7 @@
"integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.1",
"@babel/helper-module-imports": "^7.27.1",
@@ -3804,6 +3809,7 @@
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -4167,6 +4173,7 @@
"version": "22.15.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz",
"integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -4217,22 +4224,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/readable-stream": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz",
- "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "safe-buffer": "~5.1.1"
- }
- },
- "node_modules/@types/readable-stream/node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "license": "MIT"
- },
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -4871,18 +4862,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
- "node_modules/abort-controller": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
- "license": "MIT",
- "dependencies": {
- "event-target-shim": "^5.0.0"
- },
- "engines": {
- "node": ">=6.5"
- }
- },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -4902,6 +4881,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5021,6 +5001,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -5429,7 +5410,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@@ -5506,6 +5486,33 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -5793,26 +5800,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
"node_modules/batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -5860,18 +5847,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/bl": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz",
- "integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==",
- "license": "MIT",
- "dependencies": {
- "@types/readable-stream": "^4.0.0",
- "buffer": "^6.0.3",
- "inherits": "^2.0.4",
- "readable-stream": "^4.2.0"
- }
- },
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -5972,6 +5947,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
@@ -5995,34 +5971,11 @@
"node-int64": "^0.4.0"
}
},
- "node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/builtin-modules": {
@@ -6469,7 +6422,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -6488,12 +6440,6 @@
"node": ">= 12"
}
},
- "node_modules/commist": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
- "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
- "license": "MIT"
- },
"node_modules/common-tags": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
@@ -6560,35 +6506,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/concat-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
- "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
- "engines": [
- "node >= 6.0"
- ],
- "license": "MIT",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.0.2",
- "typedarray": "^0.0.6"
- }
- },
- "node_modules/concat-stream/node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/confusing-browser-globals": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -7327,7 +7244,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -7868,7 +7784,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -7980,6 +7895,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -8704,15 +8620,6 @@
"node": ">= 0.6"
}
},
- "node_modules/event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -8724,6 +8631,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.x"
@@ -8875,19 +8783,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-unique-numbers": {
- "version": "8.0.13",
- "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz",
- "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.23.8",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.1.0"
- }
- },
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -9123,7 +9018,6 @@
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
- "dev": true,
"funding": [
{
"type": "individual",
@@ -9779,7 +9673,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -9813,12 +9706,6 @@
"he": "bin/he"
}
},
- "node_modules/help-me": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
- "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
- "license": "MIT"
- },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -10198,26 +10085,6 @@
"node": ">=4"
}
},
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause"
- },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -10345,25 +10212,6 @@
"node": ">= 0.4"
}
},
- "node_modules/ip-address": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
- "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
- "license": "MIT",
- "dependencies": {
- "jsbn": "1.1.0",
- "sprintf-js": "^1.1.3"
- },
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/ip-address/node_modules/sprintf-js": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
- "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
- "license": "BSD-3-Clause"
- },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -11112,6 +10960,7 @@
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@@ -12065,16 +11914,6 @@
"jiti": "bin/jiti.js"
}
},
- "node_modules/js-sdsl": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
- "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/js-sdsl"
- }
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -12095,12 +11934,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/jsbn": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
- "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
- "license": "MIT"
- },
"node_modules/jsdom": {
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
@@ -12513,6 +12346,7 @@
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/lz-string": {
@@ -12754,6 +12588,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -12782,93 +12617,6 @@
"mkdirp": "bin/cmd.js"
}
},
- "node_modules/mqtt": {
- "version": "5.13.0",
- "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.13.0.tgz",
- "integrity": "sha512-pR+z+ChxFl3n8AKLQbTONVOOg/jl4KiKQRBAi78tjd6PksOWvl1nl9L8ZHOZ3MiavZfrUOjok2ddwc1VymGWRg==",
- "license": "MIT",
- "dependencies": {
- "commist": "^3.2.0",
- "concat-stream": "^2.0.0",
- "debug": "^4.4.0",
- "help-me": "^5.0.0",
- "lru-cache": "^10.4.3",
- "minimist": "^1.2.8",
- "mqtt-packet": "^9.0.2",
- "number-allocator": "^1.0.14",
- "readable-stream": "^4.7.0",
- "rfdc": "^1.4.1",
- "socks": "^2.8.3",
- "split2": "^4.2.0",
- "worker-timers": "^7.1.8",
- "ws": "^8.18.0"
- },
- "bin": {
- "mqtt": "build/bin/mqtt.js",
- "mqtt_pub": "build/bin/pub.js",
- "mqtt_sub": "build/bin/sub.js"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/mqtt-packet": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
- "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
- "license": "MIT",
- "dependencies": {
- "bl": "^6.0.8",
- "debug": "^4.3.4",
- "process-nextick-args": "^2.0.1"
- }
- },
- "node_modules/mqtt-packet/node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/mqtt-packet/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/mqtt/node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/mqtt/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -12961,6 +12709,27 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/node-cron": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
+ "license": "ISC",
+ "dependencies": {
+ "uuid": "8.3.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/node-cron/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@@ -13044,39 +12813,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
- "node_modules/number-allocator": {
- "version": "1.0.14",
- "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
- "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.3.1",
- "js-sdsl": "4.3.0"
- }
- },
- "node_modules/number-allocator/node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/number-allocator/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
@@ -13698,6 +13434,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -14956,6 +14693,7 @@
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -15127,19 +14865,11 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/process": {
- "version": "0.11.10",
- "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
- "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
"license": "MIT"
},
"node_modules/promise": {
@@ -15198,6 +14928,12 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -15325,6 +15061,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -15463,6 +15200,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -15489,6 +15227,7 @@
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -15577,22 +15316,6 @@
"pify": "^2.3.0"
}
},
- "node_modules/readable-stream": {
- "version": "4.7.0",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
- "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
- "license": "MIT",
- "dependencies": {
- "abort-controller": "^3.0.0",
- "buffer": "^6.0.3",
- "events": "^3.3.0",
- "process": "^0.11.10",
- "string_decoder": "^1.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- }
- },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -15958,12 +15681,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/rfdc": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
- "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
- "license": "MIT"
- },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -15987,6 +15704,7 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -16242,6 +15960,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -16627,16 +16346,6 @@
"node": ">=8"
}
},
- "node_modules/smart-buffer": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
- "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
- "license": "MIT",
- "engines": {
- "node": ">= 6.0.0",
- "npm": ">= 3.0.0"
- }
- },
"node_modules/sockjs": {
"version": "0.3.24",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@@ -16649,18 +16358,14 @@
"websocket-driver": "^0.7.4"
}
},
- "node_modules/socks": {
- "version": "2.8.4",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
- "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
+ "node_modules/sockjs/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "ip-address": "^9.0.5",
- "smart-buffer": "^4.2.0"
- },
- "engines": {
- "node": ">= 10.0.0",
- "npm": ">= 3.0.0"
+ "bin": {
+ "uuid": "dist/bin/uuid"
}
},
"node_modules/source-list-map": {
@@ -16851,15 +16556,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/split2": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
- "license": "ISC",
- "engines": {
- "node": ">= 10.x"
- }
- },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -17026,6 +16722,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
@@ -18008,6 +17705,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
"license": "0BSD"
},
"node_modules/tsutils": {
@@ -18062,6 +17760,7 @@
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -18160,12 +17859,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
- "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
- "license": "MIT"
- },
"node_modules/typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -18177,9 +17870,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
@@ -18188,7 +17881,7 @@
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=14.17"
+ "node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
@@ -18221,6 +17914,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/unicode-canonical-property-names-ecmascript": {
@@ -18373,6 +18067,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/util.promisify": {
@@ -18408,13 +18103,16 @@
}
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "dev": true,
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"license": "MIT",
"bin": {
- "uuid": "dist/bin/uuid"
+ "uuid": "dist/esm/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {
@@ -18528,6 +18226,7 @@
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
@@ -18600,6 +18299,7 @@
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
@@ -19012,6 +18712,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -19268,40 +18969,6 @@
"workbox-core": "6.6.0"
}
},
- "node_modules/worker-timers": {
- "version": "7.1.8",
- "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz",
- "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.5",
- "tslib": "^2.6.2",
- "worker-timers-broker": "^6.1.8",
- "worker-timers-worker": "^7.0.71"
- }
- },
- "node_modules/worker-timers-broker": {
- "version": "6.1.8",
- "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz",
- "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.5",
- "fast-unique-numbers": "^8.0.13",
- "tslib": "^2.6.2",
- "worker-timers-worker": "^7.0.71"
- }
- },
- "node_modules/worker-timers-worker": {
- "version": "7.0.71",
- "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz",
- "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.24.5",
- "tslib": "^2.6.2"
- }
- },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -19363,6 +19030,7 @@
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/package.json b/package.json
index 62d1c2c..896ef77 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "axios": "^1.7.7",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -10,9 +11,10 @@
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
- "mqtt": "^5.3.0",
+ "node-cron": "^3.0.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+ "uuid": "^11.0.3",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/server.js b/server.js
index bcfabc8..38afb89 100644
--- a/server.js
+++ b/server.js
@@ -1,126 +1,290 @@
const express = require('express');
const path = require('path');
const cors = require('cors');
-const fs = require('fs');
-const bodyParser = require('body-parser');
-const mqtt = require('mqtt');
+
+const sessionStore = require('./services/sessionStore');
+const credentialStore = require('./services/credentialStore');
+const { readConfig, writeConfig } = require('./services/configStore');
+const foodsharingClient = require('./services/foodsharingClient');
+const { scheduleConfig } = require('./services/pickupScheduler');
+const adminConfig = require('./services/adminConfig');
+
+const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
+const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase();
const app = express();
const port = process.env.PORT || 3000;
-const mqttBroker = process.env.MQTT_BROKER || 'mqtt://192.168.1.100:1883';
-const mqttTopic = process.env.MQTT_TOPIC || 'iobroker/pickupCheck/config';
-const mqttUser = process.env.MQTT_USER || 'iobroker';
-const mqttPassword = process.env.MQTT_PASSWORD || 'password';
-const configPath = './config/pickup-config.json';
-// Logger mit Timestamp
-function logWithTimestamp(...args) {
- const timestamp = new Date().toISOString();
- console.log(`[${timestamp}]`, ...args);
+app.use(cors());
+app.use(express.json({ limit: '1mb' }));
+app.use(express.static(path.join(__dirname, 'build')));
+
+function isAdmin(profile) {
+ if (!adminEmail || !profile?.email) {
+ return false;
+ }
+ return profile.email.toLowerCase() === adminEmail;
}
-function errorWithTimestamp(...args) {
- const timestamp = new Date().toISOString();
- console.error(`[${timestamp}]`, ...args);
+function scheduleWithCurrentSettings(sessionId, config) {
+ const settings = adminConfig.readSettings();
+ scheduleConfig(sessionId, config, settings);
}
-// MQTT-Client mit Authentifizierung initialisieren
-const mqttClient = mqtt.connect(mqttBroker, {
- clientId: 'pickup-config-web-' + Math.random().toString(16).substring(2, 8),
- clean: true,
- username: mqttUser,
- password: mqttPassword
+function rescheduleAllSessions() {
+ const settings = adminConfig.readSettings();
+ sessionStore.list().forEach((session) => {
+ if (!session?.profile?.id) {
+ return;
+ }
+ const config = readConfig(session.profile.id);
+ scheduleConfig(session.id, config, settings);
+ });
+}
+
+function mergeStoresIntoConfig(config = [], stores = []) {
+ const entries = Array.isArray(config) ? config : [];
+ const map = new Map();
+ entries.forEach((entry) => {
+ if (!entry || !entry.id) {
+ return;
+ }
+ map.set(String(entry.id), { ...entry, id: String(entry.id) });
+ });
+
+ let changed = false;
+ stores.forEach((store) => {
+ if (!store?.id) {
+ return;
+ }
+ const id = String(store.id);
+ if (!map.has(id)) {
+ map.set(id, {
+ id,
+ label: store.name || `Store ${id}`,
+ active: false,
+ checkProfileId: true,
+ onlyNotify: false,
+ hidden: false
+ });
+ changed = true;
+ return;
+ }
+
+ const existing = map.get(id);
+ if (!existing.label && store.name) {
+ existing.label = store.name;
+ changed = true;
+ }
+ });
+
+ return { merged: Array.from(map.values()), changed };
+}
+
+async function restoreSessionsFromDisk() {
+ const saved = credentialStore.loadAll();
+ const entries = Object.entries(saved);
+ if (entries.length === 0) {
+ return;
+ }
+
+ console.log(`[RESTORE] Versuche ${entries.length} gespeicherte Anmeldung(en) zu laden...`);
+ const schedulerSettings = adminConfig.readSettings();
+
+ for (const [profileId, credentials] of entries) {
+ if (!credentials?.email || !credentials?.password) {
+ continue;
+ }
+ try {
+ const auth = await foodsharingClient.login(credentials.email, credentials.password);
+ const profile = {
+ id: String(auth.profile.id),
+ name: auth.profile.name,
+ email: auth.profile.email || credentials.email
+ };
+ const isAdminUser = isAdmin(profile);
+ let config = readConfig(profile.id);
+ const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id);
+ const { merged, changed } = mergeStoresIntoConfig(config, stores);
+ if (changed) {
+ config = merged;
+ writeConfig(profile.id, config);
+ }
+
+ const session = sessionStore.create({
+ cookieHeader: auth.cookieHeader,
+ csrfToken: auth.csrfToken,
+ profile,
+ credentials,
+ isAdmin: isAdminUser
+ }, credentials.token, ONE_YEAR_MS);
+ credentialStore.save(profile.id, {
+ email: credentials.email,
+ password: credentials.password,
+ token: session.id
+ });
+ scheduleConfig(session.id, config, schedulerSettings);
+ console.log(`[RESTORE] Session fuer Profil ${profile.id} (${profile.name}) reaktiviert.`);
+ } catch (error) {
+ console.error(`[RESTORE] Login fuer Profil ${profileId} fehlgeschlagen:`, error.message);
+ }
+ }
+}
+
+function requireAuth(req, res, next) {
+ const header = req.headers.authorization || '';
+ const [scheme, token] = header.split(' ');
+ if (scheme !== 'Bearer' || !token) {
+ return res.status(401).json({ error: 'Unautorisiert' });
+ }
+
+ const session = sessionStore.get(token);
+ if (!session) {
+ return res.status(401).json({ error: 'Session nicht gefunden oder abgelaufen' });
+ }
+
+ req.session = session;
+ next();
+}
+
+function requireAdmin(req, res, next) {
+ if (!req.session?.isAdmin) {
+ return res.status(403).json({ error: 'Nur für Admins verfügbar' });
+ }
+ next();
+}
+
+app.post('/api/auth/login', async (req, res) => {
+ const { email, password } = req.body || {};
+ if (!email || !password) {
+ return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' });
+ }
+
+ try {
+ const auth = await foodsharingClient.login(email, password);
+ const profile = {
+ id: String(auth.profile.id),
+ name: auth.profile.name,
+ email: auth.profile.email || email
+ };
+ const isAdminUser = isAdmin(profile);
+
+ let config = readConfig(profile.id);
+ const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id);
+ const { merged, changed } = mergeStoresIntoConfig(config, stores);
+ if (changed) {
+ config = merged;
+ writeConfig(profile.id, config);
+ }
+
+ const existingCredentials = credentialStore.get(profile.id);
+ const existingToken = existingCredentials?.token;
+ if (existingToken) {
+ sessionStore.delete(existingToken);
+ }
+
+ const session = sessionStore.create({
+ cookieHeader: auth.cookieHeader,
+ csrfToken: auth.csrfToken,
+ profile,
+ credentials: { email, password },
+ isAdmin: isAdminUser
+ }, existingToken, ONE_YEAR_MS);
+
+ credentialStore.save(profile.id, { email, password, token: session.id });
+ const settings = adminConfig.readSettings();
+ scheduleConfig(session.id, config, settings);
+
+ return res.json({
+ token: session.id,
+ profile,
+ stores,
+ config,
+ isAdmin: isAdminUser,
+ adminSettings: isAdminUser ? settings : undefined
+ });
+ } catch (error) {
+ console.error('Login fehlgeschlagen:', error.message);
+ return res.status(401).json({ error: 'Login fehlgeschlagen' });
+ }
});
-// MQTT-Events
-mqttClient.on('connect', () => {
- logWithTimestamp('Verbunden mit MQTT-Broker:', mqttBroker);
+app.post('/api/auth/logout', requireAuth, (req, res) => {
+ sessionStore.delete(req.session.id);
+ credentialStore.remove(req.session.profile.id);
+ res.json({ success: true });
+});
- mqttClient.subscribe(mqttTopic, (err) => {
- if (!err) {
- logWithTimestamp('Abonniert auf Topic:', mqttTopic);
- mqttClient.publish(mqttTopic + '/get', 'true');
- }
+app.get('/api/auth/session', requireAuth, async (req, res) => {
+ const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
+ let config = readConfig(req.session.profile.id);
+ const { merged, changed } = mergeStoresIntoConfig(config, stores);
+ if (changed) {
+ config = merged;
+ writeConfig(req.session.profile.id, config);
+ }
+ res.json({
+ profile: req.session.profile,
+ stores,
+ isAdmin: !!req.session.isAdmin,
+ adminSettings: req.session.isAdmin ? adminConfig.readSettings() : undefined
});
});
-mqttClient.on('error', (error) => {
- errorWithTimestamp('MQTT-Fehler:', error);
+app.get('/api/profile', requireAuth, async (req, res) => {
+ const details = await foodsharingClient.fetchProfile(req.session.cookieHeader);
+ res.json({
+ profile: details || req.session.profile
+ });
});
-mqttClient.on('message', (topic, message) => {
- logWithTimestamp('Nachricht erhalten auf Topic:', topic);
+app.get('/api/config', requireAuth, (req, res) => {
+ const config = readConfig(req.session.profile.id);
+ res.json(config);
+});
- if (topic === mqttTopic) {
- try {
- const config = JSON.parse(message.toString());
- logWithTimestamp('Konfiguration vom MQTT-Broker erhalten');
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
- } catch (error) {
- errorWithTimestamp('Fehler beim Verarbeiten der MQTT-Nachricht:', error);
- }
+app.post('/api/config', requireAuth, (req, res) => {
+ if (!Array.isArray(req.body)) {
+ return res.status(400).json({ error: 'Konfiguration muss ein Array sein' });
}
+ writeConfig(req.session.profile.id, req.body);
+ scheduleWithCurrentSettings(req.session.id, req.body);
+ res.json({ success: true });
});
-// Middleware
-app.use(cors());
-app.use(bodyParser.json());
-app.use(express.static(path.join(__dirname, 'build')));
-
-// Sicherstellen, dass Konfigurationsordner existiert
-const configDir = path.dirname(configPath);
-if (!fs.existsSync(configDir)) {
- fs.mkdirSync(configDir, { recursive: true });
-}
-
-// Initiale Konfigurationsdatei erstellen, falls nicht vorhanden
-if (!fs.existsSync(configPath)) {
- const initialConfig = [
- { id: "63448", active: false, checkProfileId: true, onlyNotify: true, label: "Penny Baden-Oos" },
- { id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredWeekday: "Samstag" },
- { id: "44972", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Biblisweg", desiredWeekday: "Dienstag" },
- { id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredDate: "2025-05-18" },
- { id: "33875", active: false, checkProfileId: true, onlyNotify: false, label: "Cap Markt", desiredWeekday: "Donnerstag" },
- { id: "42322", active: false, checkProfileId: false, onlyNotify: false, label: "Edeka Haueneberstein" },
- { id: "51450", active: false, checkProfileId: true, onlyNotify: false, label: "Hornbach Grünwinkel" }
- ];
-
- fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2));
- mqttClient.publish(mqttTopic, JSON.stringify(initialConfig));
- logWithTimestamp('Initiale Konfiguration erstellt und an MQTT gesendet');
-}
-
-// API: Konfiguration abrufen
-app.get('/api/iobroker/pickup-config', (req, res) => {
- try {
- const configData = fs.readFileSync(configPath, 'utf8');
- res.json(JSON.parse(configData));
- } catch (error) {
- errorWithTimestamp('Error reading configuration:', error);
- res.status(500).json({ error: 'Failed to read configuration' });
+app.get('/api/stores', requireAuth, async (req, res) => {
+ const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
+ let config = readConfig(req.session.profile.id);
+ const { merged, changed } = mergeStoresIntoConfig(config, stores);
+ if (changed) {
+ config = merged;
+ writeConfig(req.session.profile.id, config);
}
+ res.json(stores);
});
-// API: Konfiguration speichern
-app.post('/api/iobroker/pickup-config', (req, res) => {
- try {
- const newConfig = req.body;
- fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2));
- mqttClient.publish(mqttTopic, JSON.stringify(newConfig));
- logWithTimestamp('Konfiguration über MQTT gesendet');
- res.json({ success: true });
- } catch (error) {
- errorWithTimestamp('Error saving configuration:', error);
- res.status(500).json({ error: 'Failed to save configuration' });
- }
+app.get('/api/admin/settings', requireAuth, requireAdmin, (_req, res) => {
+ res.json(adminConfig.readSettings());
+});
+
+app.post('/api/admin/settings', requireAuth, requireAdmin, (req, res) => {
+ const updated = adminConfig.writeSettings(req.body || {});
+ rescheduleAllSessions();
+ res.json(updated);
+});
+
+app.get('/api/health', (_req, res) => {
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
-// React-App ausliefern
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
-// Server starten
app.listen(port, () => {
- logWithTimestamp(`Server läuft auf Port ${port}`);
+ console.log(`Server läuft auf Port ${port}`);
+});
+
+restoreSessionsFromDisk().catch((error) => {
+ console.error('[RESTORE] Fehler bei der Session-Wiederherstellung:', error.message);
});
diff --git a/services/adminConfig.js b/services/adminConfig.js
new file mode 100644
index 0000000..4bfaf8d
--- /dev/null
+++ b/services/adminConfig.js
@@ -0,0 +1,94 @@
+const fs = require('fs');
+const path = require('path');
+
+const CONFIG_DIR = path.join(__dirname, '..', 'config');
+const SETTINGS_FILE = path.join(CONFIG_DIR, 'admin-settings.json');
+
+const DEFAULT_SETTINGS = {
+ scheduleCron: '*/10 7-22 * * *',
+ randomDelayMinSeconds: 10,
+ randomDelayMaxSeconds: 120,
+ initialDelayMinSeconds: 5,
+ initialDelayMaxSeconds: 30,
+ ignoredSlots: [
+ {
+ storeId: '51450',
+ description: 'TVS'
+ }
+ ]
+};
+
+function ensureDir() {
+ if (!fs.existsSync(CONFIG_DIR)) {
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
+ }
+}
+
+function sanitizeNumber(value, fallback) {
+ const num = Number(value);
+ if (Number.isFinite(num) && num >= 0) {
+ return num;
+ }
+ return fallback;
+}
+
+function sanitizeIgnoredSlots(slots = []) {
+ if (!Array.isArray(slots)) {
+ return DEFAULT_SETTINGS.ignoredSlots;
+ }
+ return slots
+ .map((slot) => ({
+ storeId: slot?.storeId ? String(slot.storeId) : '',
+ description: slot?.description ? String(slot.description) : ''
+ }))
+ .filter((slot) => slot.storeId);
+}
+
+function readSettings() {
+ ensureDir();
+ if (!fs.existsSync(SETTINGS_FILE)) {
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(DEFAULT_SETTINGS, null, 2));
+ return { ...DEFAULT_SETTINGS };
+ }
+
+ try {
+ const raw = fs.readFileSync(SETTINGS_FILE, 'utf8');
+ const parsed = JSON.parse(raw);
+ return {
+ scheduleCron: parsed.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
+ randomDelayMinSeconds: sanitizeNumber(parsed.randomDelayMinSeconds, DEFAULT_SETTINGS.randomDelayMinSeconds),
+ randomDelayMaxSeconds: sanitizeNumber(parsed.randomDelayMaxSeconds, DEFAULT_SETTINGS.randomDelayMaxSeconds),
+ initialDelayMinSeconds: sanitizeNumber(parsed.initialDelayMinSeconds, DEFAULT_SETTINGS.initialDelayMinSeconds),
+ initialDelayMaxSeconds: sanitizeNumber(parsed.initialDelayMaxSeconds, DEFAULT_SETTINGS.initialDelayMaxSeconds),
+ ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots)
+ };
+ } catch (error) {
+ console.error('Konnte Admin-Einstellungen nicht lesen:', error.message);
+ return { ...DEFAULT_SETTINGS };
+ }
+}
+
+function writeSettings(patch = {}) {
+ const current = readSettings();
+ const next = {
+ scheduleCron: patch.scheduleCron || current.scheduleCron,
+ randomDelayMinSeconds: sanitizeNumber(patch.randomDelayMinSeconds, current.randomDelayMinSeconds),
+ randomDelayMaxSeconds: sanitizeNumber(patch.randomDelayMaxSeconds, current.randomDelayMaxSeconds),
+ initialDelayMinSeconds: sanitizeNumber(patch.initialDelayMinSeconds, current.initialDelayMinSeconds),
+ initialDelayMaxSeconds: sanitizeNumber(patch.initialDelayMaxSeconds, current.initialDelayMaxSeconds),
+ ignoredSlots:
+ patch.ignoredSlots !== undefined
+ ? sanitizeIgnoredSlots(patch.ignoredSlots)
+ : current.ignoredSlots
+ };
+
+ ensureDir();
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2));
+ return next;
+}
+
+module.exports = {
+ DEFAULT_SETTINGS,
+ readSettings,
+ writeSettings
+};
diff --git a/services/configStore.js b/services/configStore.js
new file mode 100644
index 0000000..210f257
--- /dev/null
+++ b/services/configStore.js
@@ -0,0 +1,48 @@
+const fs = require('fs');
+const path = require('path');
+const defaultConfig = require('../data/defaultConfig');
+
+const CONFIG_DIR = path.join(__dirname, '..', 'config');
+
+function ensureDir() {
+ if (!fs.existsSync(CONFIG_DIR)) {
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
+ }
+}
+
+function getConfigPath(profileId = 'shared') {
+ return path.join(CONFIG_DIR, `${profileId}-pickup-config.json`);
+}
+
+function hydrateConfigFile(profileId) {
+ ensureDir();
+ const filePath = getConfigPath(profileId);
+ if (!fs.existsSync(filePath)) {
+ fs.writeFileSync(filePath, JSON.stringify(defaultConfig, null, 2));
+ }
+ return filePath;
+}
+
+function readConfig(profileId) {
+ const filePath = hydrateConfigFile(profileId);
+ try {
+ const raw = fs.readFileSync(filePath, 'utf8');
+ const parsed = JSON.parse(raw);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch (err) {
+ console.error(`Failed to read config for ${profileId}:`, err);
+ return [];
+ }
+}
+
+function writeConfig(profileId, payload) {
+ const filePath = hydrateConfigFile(profileId);
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
+ return filePath;
+}
+
+module.exports = {
+ readConfig,
+ writeConfig,
+ getConfigPath
+};
diff --git a/services/credentialStore.js b/services/credentialStore.js
new file mode 100644
index 0000000..5969aa1
--- /dev/null
+++ b/services/credentialStore.js
@@ -0,0 +1,71 @@
+const fs = require('fs');
+const path = require('path');
+
+const CONFIG_DIR = path.join(__dirname, '..', 'config');
+const CREDENTIAL_FILE = path.join(CONFIG_DIR, 'credentials.json');
+
+function ensureDir() {
+ if (!fs.existsSync(CONFIG_DIR)) {
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
+ }
+}
+
+function readStore() {
+ ensureDir();
+ if (!fs.existsSync(CREDENTIAL_FILE)) {
+ fs.writeFileSync(CREDENTIAL_FILE, JSON.stringify({}, null, 2));
+ }
+
+ try {
+ const raw = fs.readFileSync(CREDENTIAL_FILE, 'utf8');
+ const parsed = JSON.parse(raw);
+ return parsed && typeof parsed === 'object' ? parsed : {};
+ } catch (error) {
+ console.error('Konnte Credential-Store nicht lesen:', error.message);
+ return {};
+ }
+}
+
+function writeStore(store) {
+ ensureDir();
+ fs.writeFileSync(CREDENTIAL_FILE, JSON.stringify(store, null, 2));
+}
+
+function save(profileId, credentials) {
+ if (!profileId || !credentials?.email || !credentials?.password) {
+ return;
+ }
+ const store = readStore();
+ store[profileId] = credentials;
+ writeStore(store);
+}
+
+function remove(profileId) {
+ if (!profileId) {
+ return;
+ }
+ const store = readStore();
+ if (store[profileId]) {
+ delete store[profileId];
+ writeStore(store);
+ }
+}
+
+function loadAll() {
+ return readStore();
+}
+
+function get(profileId) {
+ if (!profileId) {
+ return null;
+ }
+ const store = readStore();
+ return store[profileId] || null;
+}
+
+module.exports = {
+ save,
+ remove,
+ loadAll,
+ get
+};
diff --git a/services/foodsharingClient.js b/services/foodsharingClient.js
new file mode 100644
index 0000000..4db2a44
--- /dev/null
+++ b/services/foodsharingClient.js
@@ -0,0 +1,179 @@
+const axios = require('axios');
+
+const BASE_URL = 'https://foodsharing.de';
+
+const client = axios.create({
+ baseURL: BASE_URL,
+ timeout: 20000,
+ headers: {
+ 'User-Agent': 'pickup-config/1.0 (+https://foodsharing.de)',
+ Accept: 'application/json, text/plain, */*'
+ }
+});
+
+function extractCsrfToken(cookies = []) {
+ if (!Array.isArray(cookies)) {
+ return null;
+ }
+ const tokenCookie = cookies.find((cookie) => cookie.startsWith('CSRF_TOKEN='));
+ if (!tokenCookie) {
+ return null;
+ }
+ return tokenCookie.split(';')[0].split('=')[1];
+}
+
+function serializeCookies(cookies = []) {
+ if (!Array.isArray(cookies)) {
+ return '';
+ }
+ return cookies.map((c) => c.split(';')[0]).join('; ');
+}
+
+function buildHeaders(cookieHeader, csrfToken) {
+ const headers = {};
+ if (cookieHeader) {
+ headers.cookie = cookieHeader;
+ }
+ if (csrfToken) {
+ headers['x-csrf-token'] = csrfToken;
+ }
+ return headers;
+}
+
+async function getCurrentUserDetails(cookieHeader) {
+ const response = await client.get('/api/user/current/details', {
+ headers: buildHeaders(cookieHeader)
+ });
+ return response.data;
+}
+
+async function login(email, password) {
+ const payload = {
+ email,
+ password,
+ remember_me: true
+ };
+
+ const headers = {
+ 'sec-ch-ua': '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
+ Referer: BASE_URL,
+ DNT: '1',
+ 'sec-ch-ua-mobile': '?0',
+ 'sec-ch-ua-platform': '"Linux"',
+ 'Content-Type': 'application/json; charset=utf-8'
+ };
+
+ const response = await client.post('/api/user/login', payload, { headers });
+ const cookies = response.headers['set-cookie'] || [];
+ const csrfToken = extractCsrfToken(cookies);
+ const cookieHeader = serializeCookies(cookies);
+ const details = await getCurrentUserDetails(cookieHeader);
+ if (!details?.id) {
+ throw new Error('Profil-ID konnte nicht ermittelt werden.');
+ }
+
+ const nameParts = [];
+ if (details.firstname) {
+ nameParts.push(details.firstname);
+ }
+ if (details.lastname) {
+ nameParts.push(details.lastname);
+ }
+
+ return {
+ csrfToken,
+ cookieHeader,
+ profile: {
+ id: String(details.id),
+ name: nameParts.length > 0 ? nameParts.join(' ') : details.email || email,
+ email: details.email || email
+ }
+ };
+}
+
+async function checkSession(cookieHeader, profileId) {
+ if (!cookieHeader) {
+ return false;
+ }
+
+ try {
+ await client.get(`/api/wall/foodsaver/${profileId}?limit=1`, {
+ headers: buildHeaders(cookieHeader)
+ });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function fetchProfile(cookieHeader) {
+ try {
+ return await getCurrentUserDetails(cookieHeader);
+ } catch (error) {
+ console.warn('Profil konnte nicht geladen werden:', error.message);
+ return null;
+ }
+}
+
+async function fetchStores(cookieHeader, profileId) {
+ if (!profileId) {
+ return [];
+ }
+ try {
+ const response = await client.get(`/api/user/${profileId}/stores`, {
+ headers: buildHeaders(cookieHeader),
+ params: { activeStores: 1 }
+ });
+ const stores = response.data || [];
+ if (!Array.isArray(stores)) {
+ return [];
+ }
+ return stores.map((store) => ({
+ id: String(store.id),
+ name: store.name || `Store ${store.id}`,
+ pickupStatus: store.pickupStatus,
+ membershipStatus: store.membershipStatus,
+ isManaging: !!store.isManaging,
+ city: store.city || '',
+ street: store.street || '',
+ zip: store.zip || ''
+ }));
+ } catch (error) {
+ console.warn('Stores konnten nicht geladen werden:', error.message);
+ return [];
+ }
+}
+
+async function fetchPickups(storeId, cookieHeader) {
+ const response = await client.get(`/api/stores/${storeId}/pickups`, {
+ headers: buildHeaders(cookieHeader)
+ });
+ return response.data?.pickups || [];
+}
+
+async function pickupRuleCheck(storeId, utcDate, profileId, session) {
+ const response = await client.get(`/api/stores/${storeId}/pickupRuleCheck/${utcDate}/${profileId}`, {
+ headers: buildHeaders(session.cookieHeader, session.csrfToken)
+ });
+ return response.data?.result === true;
+}
+
+async function bookSlot(storeId, utcDate, profileId, session) {
+ await client.post(
+ `/api/stores/${storeId}/pickups/${utcDate}/${profileId}`,
+ {},
+ {
+ headers: buildHeaders(session.cookieHeader, session.csrfToken)
+ }
+ );
+}
+
+module.exports = {
+ login,
+ checkSession,
+ fetchProfile,
+ fetchStores,
+ fetchPickups,
+ pickupRuleCheck,
+ bookSlot
+};
diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js
new file mode 100644
index 0000000..b96b0d3
--- /dev/null
+++ b/services/pickupScheduler.js
@@ -0,0 +1,236 @@
+const cron = require('node-cron');
+const foodsharingClient = require('./foodsharingClient');
+const sessionStore = require('./sessionStore');
+const { DEFAULT_SETTINGS } = require('./adminConfig');
+
+const weekdayMap = {
+ Montag: 'Monday',
+ Dienstag: 'Tuesday',
+ Mittwoch: 'Wednesday',
+ Donnerstag: 'Thursday',
+ Freitag: 'Friday',
+ Samstag: 'Saturday',
+ Sonntag: 'Sunday'
+};
+
+function randomDelayMs(minSeconds = 10, maxSeconds = 120) {
+ const min = minSeconds * 1000;
+ const max = maxSeconds * 1000;
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+function resolveSettings(settings) {
+ if (!settings) {
+ return { ...DEFAULT_SETTINGS };
+ }
+ return {
+ scheduleCron: settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
+ randomDelayMinSeconds: Number.isFinite(settings.randomDelayMinSeconds)
+ ? settings.randomDelayMinSeconds
+ : DEFAULT_SETTINGS.randomDelayMinSeconds,
+ randomDelayMaxSeconds: Number.isFinite(settings.randomDelayMaxSeconds)
+ ? settings.randomDelayMaxSeconds
+ : DEFAULT_SETTINGS.randomDelayMaxSeconds,
+ initialDelayMinSeconds: Number.isFinite(settings.initialDelayMinSeconds)
+ ? settings.initialDelayMinSeconds
+ : DEFAULT_SETTINGS.initialDelayMinSeconds,
+ initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
+ ? settings.initialDelayMaxSeconds
+ : DEFAULT_SETTINGS.initialDelayMaxSeconds,
+ ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots
+ };
+}
+
+async function ensureSession(session) {
+ const profileId = session.profile?.id;
+ if (!profileId) {
+ return false;
+ }
+
+ const stillValid = await foodsharingClient.checkSession(session.cookieHeader, profileId);
+ if (stillValid) {
+ return true;
+ }
+
+ if (!session.credentials) {
+ console.warn(`Session ${session.id} kann nicht erneuert werden – keine Zugangsdaten gespeichert.`);
+ return false;
+ }
+
+ try {
+ const refreshed = await foodsharingClient.login(
+ session.credentials.email,
+ session.credentials.password
+ );
+ sessionStore.update(session.id, {
+ cookieHeader: refreshed.cookieHeader,
+ csrfToken: refreshed.csrfToken,
+ profile: {
+ ...session.profile,
+ ...refreshed.profile
+ }
+ });
+ console.log(`Session ${session.id} wurde erfolgreich erneuert.`);
+ return true;
+ } catch (error) {
+ console.error(`Session ${session.id} konnte nicht erneuert werden:`, error.message);
+ return false;
+ }
+}
+
+function matchesDesiredDate(pickupDate, desiredDate) {
+ if (!desiredDate) {
+ return true;
+ }
+
+ const desired = new Date(desiredDate);
+ return (
+ pickupDate.getFullYear() === desired.getFullYear() &&
+ pickupDate.getMonth() === desired.getMonth() &&
+ pickupDate.getDate() === desired.getDate()
+ );
+}
+
+function matchesDesiredWeekday(pickupDate, desiredWeekday) {
+ if (!desiredWeekday) {
+ return true;
+ }
+ const weekday = pickupDate.toLocaleDateString('en-US', { weekday: 'long' });
+ return weekday === desiredWeekday;
+}
+
+function shouldIgnoreSlot(entry, pickup, settings) {
+ const rules = settings.ignoredSlots || [];
+ return rules.some((rule) => {
+ if (!rule?.storeId) {
+ return false;
+ }
+ if (String(rule.storeId) !== entry.id) {
+ return false;
+ }
+ if (rule.description) {
+ return pickup.description === rule.description;
+ }
+ return true;
+ });
+}
+
+async function processBooking(session, entry, pickup) {
+ const readableDate = new Date(pickup.date).toLocaleString('de-DE');
+ const storeName = entry.label || entry.id;
+
+ if (entry.onlyNotify) {
+ console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
+ return;
+ }
+
+ const utcDate = new Date(pickup.date).toISOString();
+ try {
+ const allowed = await foodsharingClient.pickupRuleCheck(entry.id, utcDate, session.profile.id, session);
+ if (!allowed) {
+ console.warn(`[WARN] Rule-Check fehlgeschlagen für ${storeName} am ${readableDate}`);
+ return;
+ }
+ await foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session);
+ console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
+ } catch (error) {
+ console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
+ }
+}
+
+async function checkEntry(sessionId, entry, settings) {
+ const session = sessionStore.get(sessionId);
+ if (!session) {
+ return;
+ }
+
+ const ready = await ensureSession(session);
+ if (!ready) {
+ return;
+ }
+
+ try {
+ const pickups = await foodsharingClient.fetchPickups(entry.id, session.cookieHeader);
+ let hasProfileId = false;
+ let availablePickup = null;
+
+ const desiredWeekday = entry.desiredWeekday ? weekdayMap[entry.desiredWeekday] || entry.desiredWeekday : null;
+
+ pickups.forEach((pickup) => {
+ const pickupDate = new Date(pickup.date);
+ if (!matchesDesiredDate(pickupDate, entry.desiredDate)) {
+ return;
+ }
+ if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
+ return;
+ }
+ if (entry.checkProfileId && pickup.occupiedSlots?.some((slot) => slot.profile?.id === session.profile.id)) {
+ hasProfileId = true;
+ return;
+ }
+ if (pickup.isAvailable && !availablePickup) {
+ availablePickup = pickup;
+ }
+ });
+
+ if (!availablePickup) {
+ console.log(
+ `[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
+ hasProfileId ? 'ja' : 'nein'
+ }`
+ );
+ return;
+ }
+
+ if (shouldIgnoreSlot(entry, availablePickup, settings)) {
+ console.log(`[INFO] Slot für ${entry.id} aufgrund einer Admin-Regel ignoriert.`);
+ return;
+ }
+
+ if (!entry.checkProfileId || !hasProfileId) {
+ await processBooking(session, entry, availablePickup);
+ }
+ } catch (error) {
+ console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
+ }
+}
+
+function scheduleEntry(sessionId, entry, settings) {
+ const cronExpression = settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron;
+ const job = cron.schedule(
+ cronExpression,
+ () => {
+ const delay = randomDelayMs(
+ settings.randomDelayMinSeconds,
+ settings.randomDelayMaxSeconds
+ );
+ setTimeout(() => checkEntry(sessionId, entry, settings), delay);
+ },
+ {
+ timezone: 'Europe/Berlin'
+ }
+ );
+ sessionStore.attachJob(sessionId, job);
+ setTimeout(
+ () => checkEntry(sessionId, entry, settings),
+ randomDelayMs(settings.initialDelayMinSeconds, settings.initialDelayMaxSeconds)
+ );
+}
+
+function scheduleConfig(sessionId, config, settings) {
+ const resolvedSettings = resolveSettings(settings);
+ sessionStore.clearJobs(sessionId);
+ const activeEntries = config.filter((entry) => entry.active);
+ if (activeEntries.length === 0) {
+ console.log(`[INFO] Keine aktiven Einträge für Session ${sessionId} – Scheduler ruht.`);
+ return;
+ }
+ activeEntries.forEach((entry) => scheduleEntry(sessionId, entry, resolvedSettings));
+ console.log(
+ `[INFO] Scheduler für Session ${sessionId} mit ${activeEntries.length} Jobs aktiv (Cron: ${resolvedSettings.scheduleCron}).`
+ );
+}
+
+module.exports = {
+ scheduleConfig
+};
diff --git a/services/sessionStore.js b/services/sessionStore.js
new file mode 100644
index 0000000..338dd8f
--- /dev/null
+++ b/services/sessionStore.js
@@ -0,0 +1,98 @@
+const { v4: uuid } = require('uuid');
+
+class SessionStore {
+ constructor() {
+ this.sessions = new Map();
+ this.profileIndex = new Map();
+ }
+
+ create(payload, customId, ttlMs) {
+ const id = customId || uuid();
+ const profileId = payload?.profile?.id ? String(payload.profile.id) : null;
+
+ if (profileId && this.profileIndex.has(profileId)) {
+ const previousId = this.profileIndex.get(profileId);
+ if (previousId && previousId !== id) {
+ this.delete(previousId);
+ }
+ }
+
+ const session = {
+ id,
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ expiresAt: ttlMs ? Date.now() + ttlMs : null,
+ jobs: [],
+ ...payload
+ };
+ this.sessions.set(id, session);
+
+ if (profileId) {
+ this.profileIndex.set(profileId, id);
+ }
+
+ return session;
+ }
+
+ get(id) {
+ const session = this.sessions.get(id);
+ if (!session) {
+ return null;
+ }
+
+ if (session.expiresAt && session.expiresAt < Date.now()) {
+ this.delete(id);
+ return null;
+ }
+
+ return session;
+ }
+
+ update(id, patch) {
+ const session = this.get(id);
+ if (!session) {
+ return null;
+ }
+ Object.assign(session, patch, { updatedAt: Date.now() });
+ return session;
+ }
+
+ attachJob(id, job) {
+ const session = this.get(id);
+ if (!session) {
+ return;
+ }
+ session.jobs.push(job);
+ }
+
+ clearJobs(id) {
+ const session = this.get(id);
+ if (!session || !Array.isArray(session.jobs)) {
+ return;
+ }
+ session.jobs.forEach((job) => {
+ if (job && typeof job.stop === 'function') {
+ job.stop();
+ }
+ });
+ session.jobs = [];
+ }
+
+ delete(id) {
+ const session = this.sessions.get(id);
+ if (session) {
+ this.clearJobs(id);
+ const profileId = session.profile?.id ? String(session.profile.id) : null;
+ if (profileId && this.profileIndex.get(profileId) === id) {
+ this.profileIndex.delete(profileId);
+ }
+ }
+ this.sessions.delete(id);
+ }
+
+ list() {
+ return Array.from(this.sessions.values());
+ }
+}
+
+module.exports = new SessionStore();
diff --git a/src/App.js b/src/App.js
index 8d7be52..9ea6cb8 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,251 +1,776 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
import './App.css';
+const emptyEntry = {
+ id: '',
+ label: '',
+ active: false,
+ checkProfileId: true,
+ onlyNotify: false
+};
+
+const TOKEN_STORAGE_KEY = 'pickupConfigToken';
+
function App() {
+ const [session, setSession] = useState(null);
+ const [credentials, setCredentials] = useState({ email: '', password: '' });
const [config, setConfig] = useState([]);
- const [loading, setLoading] = useState(true);
+ const [stores, setStores] = useState([]);
+ const [loading, setLoading] = useState(false);
const [status, setStatus] = useState('');
const [error, setError] = useState('');
- const [newEntry, setNewEntry] = useState({
- id: "",
- label: "",
- active: false,
- checkProfileId: true,
- onlyNotify: false
- });
+ const [newEntry, setNewEntry] = useState(emptyEntry);
const [showNewEntryForm, setShowNewEntryForm] = useState(false);
+ const [availableCollapsed, setAvailableCollapsed] = useState(true);
+ const [adminSettings, setAdminSettings] = useState(null);
+ const [adminSettingsLoading, setAdminSettingsLoading] = useState(false);
- // API-URL für Server-Endpunkte
- const API_URL = '/api/iobroker/pickup-config';
+ const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
- // Beim Laden der Komponente die aktuelle Konfiguration abrufen
- useEffect(() => {
- fetchConfig();
+ const normalizeAdminSettings = useCallback((raw) => {
+ if (!raw) {
+ return null;
+ }
+ return {
+ scheduleCron: raw.scheduleCron || '',
+ randomDelayMinSeconds: raw.randomDelayMinSeconds ?? '',
+ randomDelayMaxSeconds: raw.randomDelayMaxSeconds ?? '',
+ initialDelayMinSeconds: raw.initialDelayMinSeconds ?? '',
+ initialDelayMaxSeconds: raw.initialDelayMaxSeconds ?? '',
+ ignoredSlots: Array.isArray(raw.ignoredSlots)
+ ? raw.ignoredSlots.map((slot) => ({
+ storeId: slot?.storeId ? String(slot.storeId) : '',
+ description: slot?.description || ''
+ }))
+ : []
+ };
}, []);
- // Konfiguration vom Server abrufen
- const fetchConfig = async () => {
+ const resetSessionState = useCallback(() => {
+ setSession(null);
+ setConfig([]);
+ setStores([]);
+ setStatus('');
+ setError('');
+ setShowNewEntryForm(false);
+ setNewEntry(emptyEntry);
+ setAdminSettings(null);
+ setAdminSettingsLoading(false);
+ setAvailableCollapsed(true);
+ }, []);
+
+ const handleUnauthorized = useCallback(() => {
+ resetSessionState();
+ try {
+ localStorage.removeItem(TOKEN_STORAGE_KEY);
+ } catch (storageError) {
+ console.warn('Konnte Token nicht aus dem Speicher entfernen:', storageError);
+ }
+ }, [resetSessionState]);
+
+ const bootstrapSession = useCallback(
+ async (token) => {
+ setLoading(true);
+ setError('');
+ try {
+ const response = await fetch('/api/auth/session', {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ if (response.status === 401) {
+ handleUnauthorized();
+ return;
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const data = await response.json();
+ setSession({ token, profile: data.profile, isAdmin: data.isAdmin });
+ setStores(Array.isArray(data.stores) ? data.stores : []);
+ setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null);
+
+ const configResponse = await fetch('/api/config', {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ if (configResponse.status === 401) {
+ handleUnauthorized();
+ return;
+ }
+ if (!configResponse.ok) {
+ throw new Error(`HTTP ${configResponse.status}`);
+ }
+ const configData = await configResponse.json();
+ setConfig(Array.isArray(configData) ? configData : []);
+ } catch (err) {
+ setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`);
+ } finally {
+ setLoading(false);
+ }
+ },
+ [handleUnauthorized, normalizeAdminSettings]
+ );
+
+ useEffect(() => {
+ try {
+ const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
+ if (storedToken) {
+ bootstrapSession(storedToken);
+ }
+ } catch (err) {
+ console.warn('Konnte gespeicherten Token nicht lesen:', err);
+ }
+ }, [bootstrapSession]);
+
+ const authorizedFetch = useCallback(
+ async (url, options = {}, tokenOverride) => {
+ const activeToken = tokenOverride || session?.token;
+ if (!activeToken) {
+ throw new Error('Keine aktive Session');
+ }
+ const headers = {
+ Authorization: `Bearer ${activeToken}`,
+ ...(options.headers || {})
+ };
+ const response = await fetch(url, { ...options, headers });
+ if (response.status === 401) {
+ handleUnauthorized();
+ throw new Error('Nicht autorisiert');
+ }
+ return response;
+ },
+ [handleUnauthorized, session?.token]
+ );
+
+ useEffect(() => {
+ if (!session?.token || !session.isAdmin) {
+ setAdminSettings(null);
+ setAdminSettingsLoading(false);
+ return;
+ }
+
+ let cancelled = false;
+ setAdminSettingsLoading(true);
+
+ (async () => {
+ try {
+ const response = await authorizedFetch('/api/admin/settings');
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const data = await response.json();
+ if (!cancelled) {
+ setAdminSettings(normalizeAdminSettings(data));
+ }
+ } catch (err) {
+ if (!cancelled) {
+ setError(`Admin-Einstellungen konnten nicht geladen werden: ${err.message}`);
+ }
+ } finally {
+ if (!cancelled) {
+ setAdminSettingsLoading(false);
+ }
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [session?.token, session?.isAdmin, authorizedFetch, normalizeAdminSettings]);
+
+ const handleLogin = async (event) => {
+ event.preventDefault();
setLoading(true);
setError('');
-
+ setStatus('');
+
try {
- const response = await fetch(API_URL);
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(credentials)
+ });
+
if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
+ throw new Error(`HTTP ${response.status}`);
}
-
+
const data = await response.json();
- setConfig(data);
- setLoading(false);
+ try {
+ localStorage.setItem(TOKEN_STORAGE_KEY, data.token);
+ } catch (storageError) {
+ console.warn('Konnte Token nicht speichern:', storageError);
+ }
+ setSession({ token: data.token, profile: data.profile, isAdmin: data.isAdmin });
+ setConfig(Array.isArray(data.config) ? data.config : []);
+ setStores(Array.isArray(data.stores) ? data.stores : []);
+ setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null);
+ setStatus('Anmeldung erfolgreich. Konfiguration geladen.');
+ setTimeout(() => setStatus(''), 3000);
} catch (err) {
- console.error("Fehler beim Laden der Konfiguration:", err);
- setError(`Fehler beim Laden der Konfiguration: ${err.message}`);
+ setError(`Login fehlgeschlagen: ${err.message}`);
+ } finally {
setLoading(false);
-
- // Fallback zur statischen Konfiguration bei Fehler
- setConfig([
- { id: "63448", active: false, checkProfileId: true, onlyNotify: true, label: "Penny Baden-Oos" },
- { id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredWeekday: "Samstag" },
- { id: "44972", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Biblisweg", desiredWeekday: "Dienstag" },
- { id: "44975", active: false, checkProfileId: true, onlyNotify: false, label: "Aldi Kuppenheim", desiredDate: "2025-05-18" },
- { id: "33875", active: false, checkProfileId: true, onlyNotify: false, label: "Cap Markt", desiredWeekday: "Donnerstag" },
- { id: "42322", active: false, checkProfileId: false, onlyNotify: false, label: "Edeka Haueneberstein" },
- { id: "51450", active: false, checkProfileId: true, onlyNotify: false, label: "Hornbach Grünwinkel" }
- ]);
}
};
- // Konfiguration auf dem Server speichern
+ const handleLogout = async () => {
+ if (!session?.token) {
+ handleUnauthorized();
+ return;
+ }
+ try {
+ await authorizedFetch('/api/auth/logout', { method: 'POST' });
+ } catch (err) {
+ console.warn('Logout fehlgeschlagen:', err);
+ } finally {
+ handleUnauthorized();
+ }
+ };
+
+ const fetchConfig = async (tokenOverride, { silent = false } = {}) => {
+ const tokenToUse = tokenOverride || session?.token;
+ if (!tokenToUse) {
+ return;
+ }
+ if (!silent) {
+ setStatus('');
+ }
+ setLoading(true);
+ setError('');
+ try {
+ const response = await authorizedFetch('/api/config', {}, tokenToUse);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const data = await response.json();
+ setConfig(Array.isArray(data) ? data : []);
+ if (!silent) {
+ setStatus('Konfiguration aktualisiert.');
+ setTimeout(() => setStatus(''), 3000);
+ }
+ } catch (err) {
+ setError(`Fehler beim Laden der Konfiguration: ${err.message}`);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchStoresList = async () => {
+ if (!session?.token) {
+ return;
+ }
+ setStatus('');
+ setError('');
+ try {
+ const response = await authorizedFetch('/api/stores');
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const data = await response.json();
+ setStores(Array.isArray(data) ? data : []);
+ await fetchConfig(undefined, { silent: true });
+ setStatus('Betriebe aktualisiert.');
+ setTimeout(() => setStatus(''), 3000);
+ } catch (err) {
+ setError(`Fehler beim Laden der Betriebe: ${err.message}`);
+ }
+ };
+
const saveConfig = async () => {
+ if (!session?.token) {
+ return;
+ }
setStatus('Speichere...');
setError('');
-
try {
- const response = await fetch(API_URL, {
+ const response = await authorizedFetch('/api/config', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(config),
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(config)
});
-
if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
+ throw new Error(`HTTP ${response.status}`);
}
-
const result = await response.json();
-
if (result.success) {
setStatus('Konfiguration erfolgreich gespeichert!');
- // Status nach 3 Sekunden wieder zurücksetzen
setTimeout(() => setStatus(''), 3000);
} else {
throw new Error(result.error || 'Unbekannter Fehler beim Speichern');
}
} catch (err) {
- console.error("Fehler beim Speichern:", err);
+ setError(`Fehler beim Speichern: ${err.message}`);
+ setStatus('');
+ }
+ };
+
+ const persistConfigUpdate = async (updater, successMessage) => {
+ if (!session?.token) {
+ return;
+ }
+ let nextConfigState;
+ setConfig((prev) => {
+ nextConfigState = typeof updater === 'function' ? updater(prev) : updater;
+ return nextConfigState;
+ });
+ if (!nextConfigState) {
+ return;
+ }
+ setStatus('Speichere...');
+ setError('');
+ try {
+ const response = await authorizedFetch('/api/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(nextConfigState)
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const result = await response.json();
+ if (!result.success) {
+ throw new Error(result.error || 'Unbekannter Fehler beim Speichern');
+ }
+ const message = successMessage || 'Konfiguration gespeichert.';
+ setStatus(message);
+ setTimeout(() => setStatus(''), 3000);
+ } catch (err) {
setError(`Fehler beim Speichern: ${err.message}`);
}
};
- // Eintrag hinzufügen
const addEntry = () => {
- // Validierung
if (!newEntry.id || !newEntry.label) {
setError('ID und Bezeichnung müssen ausgefüllt werden!');
return;
}
-
- // Neuen Eintrag zur Konfiguration hinzufügen
- const updatedConfig = [...config, newEntry];
+
+ const normalized = {
+ ...newEntry,
+ id: String(newEntry.id),
+ hidden: false
+ };
+
+ const updatedConfig = [...config.filter((item) => item.id !== normalized.id), normalized];
setConfig(updatedConfig);
-
- // Formular zurücksetzen
- setNewEntry({
- id: "",
- label: "",
- active: false,
- checkProfileId: true,
- onlyNotify: false
- });
-
- // Formular ausblenden
+ setNewEntry(emptyEntry);
setShowNewEntryForm(false);
-
- // Erfolgsmeldung anzeigen
setStatus('Neuer Eintrag hinzugefügt!');
setTimeout(() => setStatus(''), 3000);
};
- // Eintrag löschen
- const deleteEntry = (index) => {
- if (window.confirm('Sind Sie sicher, dass Sie diesen Eintrag löschen möchten?')) {
- const updatedConfig = [...config];
- updatedConfig.splice(index, 1);
+ const deleteEntry = (entryId) => {
+ if (window.confirm('Soll dieser Eintrag dauerhaft gelöscht werden?')) {
+ const updatedConfig = config.filter((item) => item.id !== entryId);
setConfig(updatedConfig);
-
setStatus('Eintrag gelöscht!');
setTimeout(() => setStatus(''), 3000);
}
};
- // Event-Handler für verschiedene Änderungen an der Konfiguration
- const handleToggleActive = (index) => {
- const newConfig = [...config];
- newConfig[index].active = !newConfig[index].active;
- setConfig(newConfig);
- };
-
- const handleToggleProfileCheck = (index) => {
- const newConfig = [...config];
- newConfig[index].checkProfileId = !newConfig[index].checkProfileId;
- setConfig(newConfig);
- };
-
- const handleToggleOnlyNotify = (index) => {
- const newConfig = [...config];
- newConfig[index].onlyNotify = !newConfig[index].onlyNotify;
- setConfig(newConfig);
- };
-
- const handleWeekdayChange = (index, value) => {
- const newConfig = [...config];
- newConfig[index].desiredWeekday = value || null;
-
- // Wenn ein Wochentag gesetzt wird, entfernen wir das spezifische Datum
- if (value && newConfig[index].desiredDate) {
- delete newConfig[index].desiredDate;
+ const hideEntry = async (entryId) => {
+ if (!window.confirm('Soll dieser Betrieb ausgeblendet werden?')) {
+ return;
}
-
- setConfig(newConfig);
- };
-
- const handleDateChange = (index, value) => {
- const newConfig = [...config];
- newConfig[index].desiredDate = value || null;
-
- // Wenn ein spezifisches Datum gesetzt wird, entfernen wir den Wochentag
- if (value && newConfig[index].desiredWeekday) {
- delete newConfig[index].desiredWeekday;
- }
-
- setConfig(newConfig);
+ await persistConfigUpdate(
+ (prev) => prev.map((item) => (item.id === entryId ? { ...item, hidden: true } : item)),
+ 'Betrieb ausgeblendet.'
+ );
};
- // Neue Eintragsdaten verwalten
- const handleNewEntryChange = (e) => {
- const { name, value, type, checked } = e.target;
+ const handleToggleActive = (entryId) => {
+ setConfig((prev) =>
+ prev.map((item) =>
+ item.id === entryId ? { ...item, active: !item.active } : item
+ )
+ );
+ };
+
+ const handleToggleProfileCheck = (entryId) => {
+ setConfig((prev) =>
+ prev.map((item) =>
+ item.id === entryId ? { ...item, checkProfileId: !item.checkProfileId } : item
+ )
+ );
+ };
+
+ const handleToggleOnlyNotify = (entryId) => {
+ setConfig((prev) =>
+ prev.map((item) =>
+ item.id === entryId ? { ...item, onlyNotify: !item.onlyNotify } : item
+ )
+ );
+ };
+
+ const handleWeekdayChange = (entryId, value) => {
+ setConfig((prev) =>
+ prev.map((item) => {
+ if (item.id !== entryId) {
+ return item;
+ }
+ const updated = { ...item, desiredWeekday: value || null };
+ if (value && updated.desiredDate) {
+ delete updated.desiredDate;
+ }
+ return updated;
+ })
+ );
+ };
+
+ const handleDateChange = (entryId, value) => {
+ setConfig((prev) =>
+ prev.map((item) => {
+ if (item.id !== entryId) {
+ return item;
+ }
+ const updated = { ...item, desiredDate: value || null };
+ if (value && updated.desiredWeekday) {
+ delete updated.desiredWeekday;
+ }
+ return updated;
+ })
+ );
+ };
+
+ const handleNewEntryChange = (event) => {
+ const { name, value, type, checked } = event.target;
setNewEntry({
...newEntry,
[name]: type === 'checkbox' ? checked : value
});
};
- // Lade-Indikator anzeigen, während die Daten geladen werden
+ const configMap = useMemo(() => {
+ const map = new Map();
+ config.forEach((item) => {
+ if (item?.id) {
+ map.set(String(item.id), item);
+ }
+ });
+ return map;
+ }, [config]);
+
+ const visibleConfig = useMemo(() => config.filter((item) => !item.hidden), [config]);
+
+ const handleStoreSelection = async (store) => {
+ const storeId = String(store.id);
+ const existing = configMap.get(storeId);
+ if (existing && !existing.hidden) {
+ return;
+ }
+ if (!window.confirm(`Soll der Betrieb "${store.name}" zur Liste hinzugefügt werden?`)) {
+ return;
+ }
+ const message = existing ? 'Betrieb wieder eingeblendet.' : 'Betrieb zur Liste hinzugefügt.';
+ await persistConfigUpdate(
+ (prev) => {
+ const already = prev.find((item) => item.id === storeId);
+ if (already) {
+ return prev.map((item) =>
+ item.id === storeId
+ ? {
+ ...item,
+ hidden: false,
+ label: item.label || store.name || `Store ${storeId}`
+ }
+ : item
+ );
+ }
+ return [
+ ...prev,
+ {
+ id: storeId,
+ label: store.name || `Store ${storeId}`,
+ active: false,
+ checkProfileId: true,
+ onlyNotify: false,
+ hidden: false
+ }
+ ];
+ },
+ message
+ );
+ };
+
+ const handleAdminSettingChange = (field, value, isNumber = false) => {
+ setAdminSettings((prev) => {
+ if (!prev) {
+ return prev;
+ }
+ let nextValue = value;
+ if (isNumber) {
+ nextValue = value === '' ? '' : Number(value);
+ }
+ return {
+ ...prev,
+ [field]: nextValue
+ };
+ });
+ };
+
+ const handleIgnoredSlotChange = (index, field, value) => {
+ setAdminSettings((prev) => {
+ if (!prev) {
+ return prev;
+ }
+ const slots = [...(prev.ignoredSlots || [])];
+ slots[index] = {
+ ...slots[index],
+ [field]: field === 'storeId' ? value : value
+ };
+ return {
+ ...prev,
+ ignoredSlots: slots
+ };
+ });
+ };
+
+ const addIgnoredSlot = () => {
+ setAdminSettings((prev) => {
+ if (!prev) {
+ return prev;
+ }
+ return {
+ ...prev,
+ ignoredSlots: [...(prev.ignoredSlots || []), { storeId: '', description: '' }]
+ };
+ });
+ };
+
+ const removeIgnoredSlot = (index) => {
+ setAdminSettings((prev) => {
+ if (!prev) {
+ return prev;
+ }
+ const slots = [...(prev.ignoredSlots || [])];
+ slots.splice(index, 1);
+ return {
+ ...prev,
+ ignoredSlots: slots
+ };
+ });
+ };
+
+ const saveAdminSettings = async () => {
+ if (!session?.token || !session.isAdmin || !adminSettings) {
+ return;
+ }
+ setStatus('Admin-Einstellungen werden gespeichert...');
+ setError('');
+ const toNumber = (value) => {
+ if (value === '' || value === null || value === undefined) {
+ return undefined;
+ }
+ const parsed = Number(value);
+ return Number.isFinite(parsed) ? parsed : undefined;
+ };
+ try {
+ const payload = {
+ scheduleCron: adminSettings.scheduleCron,
+ randomDelayMinSeconds: toNumber(adminSettings.randomDelayMinSeconds),
+ randomDelayMaxSeconds: toNumber(adminSettings.randomDelayMaxSeconds),
+ initialDelayMinSeconds: toNumber(adminSettings.initialDelayMinSeconds),
+ initialDelayMaxSeconds: toNumber(adminSettings.initialDelayMaxSeconds),
+ ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
+ storeId: slot.storeId || '',
+ description: slot.description || ''
+ }))
+ };
+
+ const response = await authorizedFetch('/api/admin/settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const data = await response.json();
+ setAdminSettings(normalizeAdminSettings(data));
+ setStatus('Admin-Einstellungen gespeichert.');
+ setTimeout(() => setStatus(''), 3000);
+ } catch (err) {
+ setError(`Speichern der Admin-Einstellungen fehlgeschlagen: ${err.message}`);
+ }
+ };
+
+ if (!session?.token) {
+ return (
+ Lade Konfiguration...
+Lade Daten...
Angemeldet
+{session.profile.name}
+Profil-ID: {session.profile.id}
+| + Keine sichtbaren Einträge. Nutze „Verfügbare Betriebe“, um Betriebe hinzuzufügen oder ausgeblendete Einträge zurückzuholen. + | +|||||||
| - + handleToggleActive(item.id)} + className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" + /> |
@@ -280,65 +810,191 @@ function App() {
|
- + handleToggleProfileCheck(item.id)} + className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" + /> | - + handleToggleOnlyNotify(item.id)} + className="h-5 w-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500" + /> | - | - handleDateChange(index, e.target.value)} + onChange={(e) => handleDateChange(item.id, e.target.value)} className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" disabled={item.desiredWeekday} /> | -
- |
+
+
|
Lade Admin-Einstellungen...
} + {!adminSettingsLoading && !adminSettings && ( +Keine Admin-Einstellungen verfügbar.
+ )} + {adminSettings && ( + <> +Keine Regeln definiert.
+ )} + {adminSettings.ignoredSlots?.map((slot, index) => ( +
- {JSON.stringify(config, null, 2)}
-
-