const fs = require('fs'); const path = require('path'); const { readJsonFile, writeJsonFile } = require('./jsonFileStore'); 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, messageWatcherSocketEnabled: true, messageWatcherFallbackEnabled: true, messageWatcherFallbackCron: '0 6-23 * * *', messageWatcherDebugLogging: true, maintenanceModeActive: false, maintenanceModeAutoEnabled: true, maintenanceModeActivatedAt: null, maintenanceModeReason: '', maintenanceModeErrorThreshold: 8, maintenanceModeErrorWindowMinutes: 10, 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 sanitizeNullableString(value, fallback = null) { const sanitized = sanitizeString(value); return sanitized || fallback; } 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)) { writeJsonFile(SETTINGS_FILE, DEFAULT_SETTINGS, { backup: false }); return { ...DEFAULT_SETTINGS }; } try { const parsed = readJsonFile( SETTINGS_FILE, DEFAULT_SETTINGS, (value) => value && typeof value === 'object' && !Array.isArray(value) ); 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 ), messageWatcherSocketEnabled: parsed.messageWatcherSocketEnabled === undefined ? DEFAULT_SETTINGS.messageWatcherSocketEnabled : !!parsed.messageWatcherSocketEnabled, messageWatcherFallbackEnabled: parsed.messageWatcherFallbackEnabled === undefined ? DEFAULT_SETTINGS.messageWatcherFallbackEnabled : !!parsed.messageWatcherFallbackEnabled, messageWatcherFallbackCron: parsed.messageWatcherFallbackCron || DEFAULT_SETTINGS.messageWatcherFallbackCron, messageWatcherDebugLogging: parsed.messageWatcherDebugLogging === undefined ? DEFAULT_SETTINGS.messageWatcherDebugLogging : !!parsed.messageWatcherDebugLogging, maintenanceModeActive: !!parsed.maintenanceModeActive, maintenanceModeAutoEnabled: parsed.maintenanceModeAutoEnabled === undefined ? DEFAULT_SETTINGS.maintenanceModeAutoEnabled : !!parsed.maintenanceModeAutoEnabled, maintenanceModeActivatedAt: sanitizeNullableString(parsed.maintenanceModeActivatedAt, null), maintenanceModeReason: sanitizeString(parsed.maintenanceModeReason), maintenanceModeErrorThreshold: sanitizeNumber( parsed.maintenanceModeErrorThreshold, DEFAULT_SETTINGS.maintenanceModeErrorThreshold ), maintenanceModeErrorWindowMinutes: sanitizeNumber( parsed.maintenanceModeErrorWindowMinutes, DEFAULT_SETTINGS.maintenanceModeErrorWindowMinutes ), 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 ), messageWatcherSocketEnabled: patch.messageWatcherSocketEnabled === undefined ? current.messageWatcherSocketEnabled : !!patch.messageWatcherSocketEnabled, messageWatcherFallbackEnabled: patch.messageWatcherFallbackEnabled === undefined ? current.messageWatcherFallbackEnabled : !!patch.messageWatcherFallbackEnabled, messageWatcherFallbackCron: patch.messageWatcherFallbackCron || current.messageWatcherFallbackCron, messageWatcherDebugLogging: patch.messageWatcherDebugLogging === undefined ? current.messageWatcherDebugLogging : !!patch.messageWatcherDebugLogging, maintenanceModeActive: patch.maintenanceModeActive === undefined ? current.maintenanceModeActive : !!patch.maintenanceModeActive, maintenanceModeAutoEnabled: patch.maintenanceModeAutoEnabled === undefined ? current.maintenanceModeAutoEnabled : !!patch.maintenanceModeAutoEnabled, maintenanceModeActivatedAt: patch.maintenanceModeActivatedAt === undefined ? current.maintenanceModeActivatedAt : sanitizeNullableString(patch.maintenanceModeActivatedAt, null), maintenanceModeReason: patch.maintenanceModeReason === undefined ? current.maintenanceModeReason : sanitizeString(patch.maintenanceModeReason), maintenanceModeErrorThreshold: sanitizeNumber( patch.maintenanceModeErrorThreshold, current.maintenanceModeErrorThreshold ), maintenanceModeErrorWindowMinutes: sanitizeNumber( patch.maintenanceModeErrorWindowMinutes, current.maintenanceModeErrorWindowMinutes ), ignoredSlots: patch.ignoredSlots !== undefined ? sanitizeIgnoredSlots(patch.ignoredSlots) : current.ignoredSlots, notifications: patch.notifications !== undefined ? sanitizeNotifications(patch.notifications) : current.notifications }; if (!next.maintenanceModeActive) { next.maintenanceModeActivatedAt = null; next.maintenanceModeReason = ''; } ensureDir(); writeJsonFile(SETTINGS_FILE, next); return next; } module.exports = { DEFAULT_SETTINGS, readSettings, writeSettings };