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 * * *', pickupFallbackCron: '0 7,12,17,22 * * *', pickupWindowOffsetsMinutes: [-1, -0.5, 0, 0.5, 1, 1.5], regularPickupRefreshCron: '0 3 * * *', dormantMembershipCron: '0 4 */14 * *', randomDelayMinSeconds: 10, randomDelayMaxSeconds: 120, initialDelayMinSeconds: 5, initialDelayMaxSeconds: 30, storeWatchCron: '*/30 * * * *', storeWatchInitialDelayMinSeconds: 10, storeWatchInitialDelayMaxSeconds: 60, storeWatchRequestDelayMs: 1000, storeWatchStatusCacheMaxAgeMinutes: 120, storePickupCheckDelayMs: 400, ignoredSlots: [ { storeId: '51450', slotName: 'TVS', info: '' } ], notifications: { ntfy: { enabled: false, serverUrl: 'https://ntfy.sh', topicPrefix: '', username: '', password: '' }, telegram: { enabled: false, botToken: '', chatId: '' } } }; 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 sanitizeNumberArray(value, fallback) { if (Array.isArray(value)) { const cleaned = value .map((entry) => Number(entry)) .filter((entry) => Number.isFinite(entry)); return cleaned.length > 0 ? cleaned : fallback; } if (typeof value === 'string') { const cleaned = value .split(',') .map((entry) => Number(entry.trim())) .filter((entry) => Number.isFinite(entry)); return cleaned.length > 0 ? cleaned : fallback; } return fallback; } function sanitizeIgnoredSlots(slots = []) { if (!Array.isArray(slots)) { return DEFAULT_SETTINGS.ignoredSlots; } return slots .map((slot) => { const slotName = slot?.slotName ?? slot?.description; return { storeId: slot?.storeId ? String(slot.storeId) : '', slotName: slotName ? String(slotName) : '', info: slot?.info ? String(slot.info) : '' }; }) .filter((slot) => slot.storeId && slot.slotName); } function sanitizeString(value) { if (typeof value === 'string') { return value.trim(); } if (value === null || value === undefined) { return ''; } return String(value).trim(); } function sanitizeNotifications(input = {}) { const defaults = DEFAULT_SETTINGS.notifications; return { ntfy: { enabled: !!(input?.ntfy?.enabled), serverUrl: sanitizeString(input?.ntfy?.serverUrl || defaults.ntfy.serverUrl), topicPrefix: sanitizeString(input?.ntfy?.topicPrefix || defaults.ntfy.topicPrefix), username: sanitizeString(input?.ntfy?.username || defaults.ntfy.username), password: sanitizeString(input?.ntfy?.password || defaults.ntfy.password) }, telegram: { enabled: !!(input?.telegram?.enabled), botToken: sanitizeString(input?.telegram?.botToken || defaults.telegram.botToken), chatId: sanitizeString(input?.telegram?.chatId || defaults.telegram.chatId) } }; } 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, pickupFallbackCron: parsed.pickupFallbackCron || DEFAULT_SETTINGS.pickupFallbackCron, pickupWindowOffsetsMinutes: sanitizeNumberArray( parsed.pickupWindowOffsetsMinutes, DEFAULT_SETTINGS.pickupWindowOffsetsMinutes ), regularPickupRefreshCron: parsed.regularPickupRefreshCron || DEFAULT_SETTINGS.regularPickupRefreshCron, dormantMembershipCron: parsed.dormantMembershipCron || DEFAULT_SETTINGS.dormantMembershipCron, 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), storeWatchCron: parsed.storeWatchCron || DEFAULT_SETTINGS.storeWatchCron, storeWatchInitialDelayMinSeconds: sanitizeNumber( parsed.storeWatchInitialDelayMinSeconds, DEFAULT_SETTINGS.storeWatchInitialDelayMinSeconds ), storeWatchInitialDelayMaxSeconds: sanitizeNumber( parsed.storeWatchInitialDelayMaxSeconds, DEFAULT_SETTINGS.storeWatchInitialDelayMaxSeconds ), storeWatchRequestDelayMs: sanitizeNumber( parsed.storeWatchRequestDelayMs, DEFAULT_SETTINGS.storeWatchRequestDelayMs ), storeWatchStatusCacheMaxAgeMinutes: sanitizeNumber( parsed.storeWatchStatusCacheMaxAgeMinutes, DEFAULT_SETTINGS.storeWatchStatusCacheMaxAgeMinutes ), storePickupCheckDelayMs: sanitizeNumber( parsed.storePickupCheckDelayMs, DEFAULT_SETTINGS.storePickupCheckDelayMs ), ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots), notifications: sanitizeNotifications(parsed.notifications) }; } 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, pickupFallbackCron: patch.pickupFallbackCron || current.pickupFallbackCron, pickupWindowOffsetsMinutes: sanitizeNumberArray( patch.pickupWindowOffsetsMinutes, current.pickupWindowOffsetsMinutes ), regularPickupRefreshCron: patch.regularPickupRefreshCron || current.regularPickupRefreshCron, dormantMembershipCron: patch.dormantMembershipCron || current.dormantMembershipCron, randomDelayMinSeconds: sanitizeNumber(patch.randomDelayMinSeconds, current.randomDelayMinSeconds), randomDelayMaxSeconds: sanitizeNumber(patch.randomDelayMaxSeconds, current.randomDelayMaxSeconds), initialDelayMinSeconds: sanitizeNumber(patch.initialDelayMinSeconds, current.initialDelayMinSeconds), initialDelayMaxSeconds: sanitizeNumber(patch.initialDelayMaxSeconds, current.initialDelayMaxSeconds), storeWatchCron: patch.storeWatchCron || current.storeWatchCron, storeWatchInitialDelayMinSeconds: sanitizeNumber( patch.storeWatchInitialDelayMinSeconds, current.storeWatchInitialDelayMinSeconds ), storeWatchInitialDelayMaxSeconds: sanitizeNumber( patch.storeWatchInitialDelayMaxSeconds, current.storeWatchInitialDelayMaxSeconds ), storeWatchRequestDelayMs: sanitizeNumber( patch.storeWatchRequestDelayMs, current.storeWatchRequestDelayMs ), storeWatchStatusCacheMaxAgeMinutes: sanitizeNumber( patch.storeWatchStatusCacheMaxAgeMinutes, current.storeWatchStatusCacheMaxAgeMinutes ), storePickupCheckDelayMs: sanitizeNumber( patch.storePickupCheckDelayMs, current.storePickupCheckDelayMs ), ignoredSlots: patch.ignoredSlots !== undefined ? sanitizeIgnoredSlots(patch.ignoredSlots) : current.ignoredSlots, notifications: patch.notifications !== undefined ? sanitizeNotifications(patch.notifications) : current.notifications }; ensureDir(); fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2)); return next; } module.exports = { DEFAULT_SETTINGS, readSettings, writeSettings };