From 539759ed60ffa8f6d5b04a513f0c60079fff646d Mon Sep 17 00:00:00 2001 From: Meik Date: Sun, 9 Nov 2025 20:08:14 +0100 Subject: [PATCH] feat: add ntfy and telegram notification workflows --- server.js | 51 ++++ services/adminConfig.js | 51 +++- services/notificationService.js | 128 +++++++++ services/pickupScheduler.js | 30 ++- services/userSettingsStore.js | 108 ++++++++ src/App.js | 458 +++++++++++++++++++++++++++++++- 6 files changed, 820 insertions(+), 6 deletions(-) create mode 100644 services/notificationService.js create mode 100644 services/userSettingsStore.js diff --git a/server.js b/server.js index c415efd..b7e16c5 100644 --- a/server.js +++ b/server.js @@ -9,6 +9,8 @@ const { readConfig, writeConfig } = require('./services/configStore'); const foodsharingClient = require('./services/foodsharingClient'); const { scheduleConfig } = require('./services/pickupScheduler'); const adminConfig = require('./services/adminConfig'); +const { readNotificationSettings, writeNotificationSettings } = require('./services/userSettingsStore'); +const notificationService = require('./services/notificationService'); const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000; const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase(); @@ -388,6 +390,55 @@ app.post('/api/config', requireAuth, (req, res) => { res.json({ success: true }); }); +app.get('/api/notifications/settings', requireAuth, (req, res) => { + const userSettings = readNotificationSettings(req.session.profile.id); + const adminSettings = adminConfig.readSettings(); + res.json({ + settings: userSettings.notifications, + capabilities: { + ntfy: { + enabled: !!( + adminSettings.notifications?.ntfy?.enabled && adminSettings.notifications?.ntfy?.serverUrl + ), + serverUrl: adminSettings.notifications?.ntfy?.serverUrl || '', + topicPrefix: adminSettings.notifications?.ntfy?.topicPrefix || '' + }, + telegram: { + enabled: !!( + adminSettings.notifications?.telegram?.enabled && adminSettings.notifications?.telegram?.botToken + ) + } + } + }); +}); + +app.post('/api/notifications/settings', requireAuth, (req, res) => { + const payload = { + notifications: { + ntfy: { + enabled: !!req.body?.notifications?.ntfy?.enabled, + topic: req.body?.notifications?.ntfy?.topic || '', + serverUrl: req.body?.notifications?.ntfy?.serverUrl || '' + }, + telegram: { + enabled: !!req.body?.notifications?.telegram?.enabled, + chatId: req.body?.notifications?.telegram?.chatId || '' + } + } + }; + const updated = writeNotificationSettings(req.session.profile.id, payload); + res.json(updated.notifications); +}); + +app.post('/api/notifications/test', requireAuth, async (req, res) => { + try { + await notificationService.sendTestNotification(req.session.profile.id, req.body?.channel); + res.json({ success: true }); + } catch (error) { + res.status(400).json({ error: error.message || 'Testbenachrichtigung fehlgeschlagen' }); + } +}); + app.get('/api/stores', requireAuth, async (req, res) => { res.json(req.session.storesCache?.data || []); }); diff --git a/services/adminConfig.js b/services/adminConfig.js index 7038a5e..845f020 100644 --- a/services/adminConfig.js +++ b/services/adminConfig.js @@ -16,7 +16,20 @@ const DEFAULT_SETTINGS = { storeId: '51450', description: 'TVS' } - ] + ], + notifications: { + ntfy: { + enabled: false, + serverUrl: 'https://ntfy.sh', + topicPrefix: '', + username: '', + password: '' + }, + telegram: { + enabled: false, + botToken: '' + } + } }; function ensureDir() { @@ -45,6 +58,33 @@ function sanitizeIgnoredSlots(slots = []) { .filter((slot) => slot.storeId); } +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) + } + }; +} + function readSettings() { ensureDir(); if (!fs.existsSync(SETTINGS_FILE)) { @@ -65,7 +105,8 @@ function readSettings() { parsed.storePickupCheckDelayMs, DEFAULT_SETTINGS.storePickupCheckDelayMs ), - ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots) + ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots), + notifications: sanitizeNotifications(parsed.notifications) }; } catch (error) { console.error('Konnte Admin-Einstellungen nicht lesen:', error.message); @@ -88,7 +129,11 @@ function writeSettings(patch = {}) { ignoredSlots: patch.ignoredSlots !== undefined ? sanitizeIgnoredSlots(patch.ignoredSlots) - : current.ignoredSlots + : current.ignoredSlots, + notifications: + patch.notifications !== undefined + ? sanitizeNotifications(patch.notifications) + : current.notifications }; ensureDir(); diff --git a/services/notificationService.js b/services/notificationService.js new file mode 100644 index 0000000..22ab7cf --- /dev/null +++ b/services/notificationService.js @@ -0,0 +1,128 @@ +const axios = require('axios'); +const adminConfig = require('./adminConfig'); +const { readNotificationSettings } = require('./userSettingsStore'); + +function formatDateLabel(dateInput) { + try { + const date = new Date(dateInput); + if (Number.isNaN(date.getTime())) { + return 'unbekanntes Datum'; + } + return date.toLocaleString('de-DE', { + weekday: 'short', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } catch (_error) { + return 'unbekanntes Datum'; + } +} + +async function sendNtfyNotification(adminNtfy, userNtfy, payload) { + if (!adminNtfy?.enabled || !userNtfy?.enabled || !userNtfy.topic) { + return; + } + const server = (userNtfy.serverUrl || adminNtfy.serverUrl || 'https://ntfy.sh').replace(/\/+$/, ''); + const topicPrefix = adminNtfy.topicPrefix ? `${adminNtfy.topicPrefix.replace(/\/+$/, '')}/` : ''; + const topic = `${topicPrefix}${userNtfy.topic.replace(/^\//, '')}`; + const url = `${server}/${topic}`; + const headers = { + Title: payload.title, + Priority: payload.priority || 'default' + }; + if (adminNtfy.username && adminNtfy.password) { + const credentials = Buffer.from(`${adminNtfy.username}:${adminNtfy.password}`).toString('base64'); + headers.Authorization = `Basic ${credentials}`; + } + await axios.post(url, payload.message, { headers, timeout: 15000 }); +} + +async function sendTelegramNotification(adminTelegram, userTelegram, payload) { + if (!adminTelegram?.enabled || !adminTelegram.botToken || !userTelegram?.enabled || !userTelegram.chatId) { + return; + } + const endpoint = `https://api.telegram.org/bot${adminTelegram.botToken}/sendMessage`; + await axios.post( + endpoint, + { + chat_id: userTelegram.chatId, + text: `${payload.title}\n${payload.message}`, + disable_web_page_preview: true + }, + { timeout: 15000 } + ); +} + +async function notifyChannels(profileId, template) { + const adminSettings = adminConfig.readSettings(); + const userSettings = readNotificationSettings(profileId); + try { + await Promise.allSettled([ + sendNtfyNotification(adminSettings.notifications?.ntfy, userSettings.notifications?.ntfy, template), + sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, template) + ]); + } catch (error) { + console.error('[NOTIFICATIONS] Versand fehlgeschlagen:', error.message); + } +} + +async function sendSlotNotification({ profileId, storeName, pickupDate, onlyNotify, booked }) { + const dateLabel = formatDateLabel(pickupDate); + const title = onlyNotify + ? `Slot verfügbar bei ${storeName}` + : booked + ? `Slot gebucht bei ${storeName}` + : `Slot gefunden bei ${storeName}`; + const message = onlyNotify + ? `Es wurde ein freier Slot am ${dateLabel} entdeckt.` + : booked + ? `Der Slot am ${dateLabel} wurde erfolgreich gebucht.` + : `Es wurde ein Slot am ${dateLabel} gefunden.`; + + await notifyChannels(profileId, { + title, + message, + priority: booked ? 'high' : 'default' + }); +} + +async function sendTestNotification(profileId, channel) { + const title = 'Pickup Benachrichtigung (Test)'; + const message = 'Das ist eine Testnachricht. Bei Fragen wende dich bitte an den Admin.'; + const adminSettings = adminConfig.readSettings(); + const userSettings = readNotificationSettings(profileId); + const tasks = []; + + if (!channel || channel === 'ntfy') { + tasks.push( + sendNtfyNotification(adminSettings.notifications?.ntfy, userSettings.notifications?.ntfy, { + title, + message, + priority: 'default' + }) + ); + } + if (!channel || channel === 'telegram') { + tasks.push( + sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, { + title, + message, + priority: 'default' + }) + ); + } + + if (tasks.length === 0) { + throw new Error('Kein unterstützter Kanal oder Kanal deaktiviert.'); + } + + await Promise.all(tasks); +} + +module.exports = { + sendSlotNotification, + sendTestNotification +}; diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js index 696618b..e344939 100644 --- a/services/pickupScheduler.js +++ b/services/pickupScheduler.js @@ -2,6 +2,7 @@ const cron = require('node-cron'); const foodsharingClient = require('./foodsharingClient'); const sessionStore = require('./sessionStore'); const { DEFAULT_SETTINGS } = require('./adminConfig'); +const notificationService = require('./notificationService'); const weekdayMap = { Montag: 'Monday', @@ -37,7 +38,20 @@ function resolveSettings(settings) { initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds) ? settings.initialDelayMaxSeconds : DEFAULT_SETTINGS.initialDelayMaxSeconds, - ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots + ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots, + notifications: { + ntfy: { + enabled: !!settings.notifications?.ntfy?.enabled, + serverUrl: settings.notifications?.ntfy?.serverUrl || DEFAULT_SETTINGS.notifications.ntfy.serverUrl, + topicPrefix: settings.notifications?.ntfy?.topicPrefix || DEFAULT_SETTINGS.notifications.ntfy.topicPrefix, + username: settings.notifications?.ntfy?.username || DEFAULT_SETTINGS.notifications.ntfy.username, + password: settings.notifications?.ntfy?.password || DEFAULT_SETTINGS.notifications.ntfy.password + }, + telegram: { + enabled: !!settings.notifications?.telegram?.enabled, + botToken: settings.notifications?.telegram?.botToken || DEFAULT_SETTINGS.notifications.telegram.botToken + } + } }; } @@ -147,6 +161,13 @@ async function processBooking(session, entry, pickup) { if (entry.onlyNotify) { console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`); + await notificationService.sendSlotNotification({ + profileId: session.profile.id, + storeName, + pickupDate: pickup.date, + onlyNotify: true, + booked: false + }); return; } @@ -159,6 +180,13 @@ async function processBooking(session, entry, pickup) { } await foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session); console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`); + await notificationService.sendSlotNotification({ + profileId: session.profile.id, + storeName, + pickupDate: pickup.date, + onlyNotify: false, + booked: true + }); } catch (error) { console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message); } diff --git a/services/userSettingsStore.js b/services/userSettingsStore.js new file mode 100644 index 0000000..eca1d25 --- /dev/null +++ b/services/userSettingsStore.js @@ -0,0 +1,108 @@ +const fs = require('fs'); +const path = require('path'); + +const SETTINGS_DIR = path.join(__dirname, '..', 'config'); + +const DEFAULT_USER_SETTINGS = { + notifications: { + ntfy: { + enabled: false, + topic: '', + serverUrl: '' + }, + telegram: { + enabled: false, + chatId: '' + } + } +}; + +function ensureDir() { + if (!fs.existsSync(SETTINGS_DIR)) { + fs.mkdirSync(SETTINGS_DIR, { recursive: true }); + } +} + +function getSettingsPath(profileId = 'shared') { + return path.join(SETTINGS_DIR, `${profileId}-notifications.json`); +} + +function sanitizeBoolean(value) { + return !!value; +} + +function sanitizeString(value) { + if (typeof value !== 'string') { + if (value === null || value === undefined) { + return ''; + } + return String(value); + } + return value.trim(); +} + +function hydrateSettingsFile(profileId) { + ensureDir(); + const filePath = getSettingsPath(profileId); + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, JSON.stringify(DEFAULT_USER_SETTINGS, null, 2)); + } + return filePath; +} + +function readNotificationSettings(profileId) { + const filePath = hydrateSettingsFile(profileId); + try { + const raw = fs.readFileSync(filePath, 'utf8'); + const parsed = JSON.parse(raw); + return { + notifications: { + ntfy: { + enabled: sanitizeBoolean(parsed?.notifications?.ntfy?.enabled), + topic: sanitizeString(parsed?.notifications?.ntfy?.topic), + serverUrl: sanitizeString(parsed?.notifications?.ntfy?.serverUrl) + }, + telegram: { + enabled: sanitizeBoolean(parsed?.notifications?.telegram?.enabled), + chatId: sanitizeString(parsed?.notifications?.telegram?.chatId) + } + } + }; + } catch (error) { + console.error(`[NOTIFICATIONS] Konnte Einstellungen für ${profileId} nicht lesen:`, error.message); + return JSON.parse(JSON.stringify(DEFAULT_USER_SETTINGS)); + } +} + +function writeNotificationSettings(profileId, patch = {}) { + const current = readNotificationSettings(profileId); + const next = { + notifications: { + ntfy: { + enabled: sanitizeBoolean(patch?.notifications?.ntfy?.enabled ?? current.notifications.ntfy.enabled), + topic: sanitizeString(patch?.notifications?.ntfy?.topic ?? current.notifications.ntfy.topic), + serverUrl: sanitizeString( + patch?.notifications?.ntfy?.serverUrl ?? current.notifications.ntfy.serverUrl + ) + }, + telegram: { + enabled: sanitizeBoolean( + patch?.notifications?.telegram?.enabled ?? current.notifications.telegram.enabled + ), + chatId: sanitizeString( + patch?.notifications?.telegram?.chatId ?? current.notifications.telegram.chatId + ) + } + } + }; + + const filePath = hydrateSettingsFile(profileId); + fs.writeFileSync(filePath, JSON.stringify(next, null, 2)); + return next; +} + +module.exports = { + DEFAULT_USER_SETTINGS, + readNotificationSettings, + writeNotificationSettings +}; diff --git a/src/App.js b/src/App.js index 48ddfb0..9b88217 100644 --- a/src/App.js +++ b/src/App.js @@ -57,6 +57,16 @@ const buildSelectionRange = (start, end, minDate) => { }; }; +const defaultNotificationSettings = { + ntfy: { enabled: false, topic: '', serverUrl: '' }, + telegram: { enabled: false, chatId: '' } +}; + +const defaultNotificationCapabilities = { + ntfy: { enabled: false, serverUrl: '', topicPrefix: '' }, + telegram: { enabled: false } +}; + function App() { const [session, setSession] = useState(null); const [credentials, setCredentials] = useState({ email: '', password: '' }); @@ -83,6 +93,13 @@ function App() { const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false); const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null }); const [activeRangePicker, setActiveRangePicker] = useState(null); + const [notificationSettings, setNotificationSettings] = useState(defaultNotificationSettings); + const [notificationCapabilities, setNotificationCapabilities] = useState(defaultNotificationCapabilities); + const [notificationDirty, setNotificationDirty] = useState(false); + const [notificationLoading, setNotificationLoading] = useState(false); + const [notificationSaving, setNotificationSaving] = useState(false); + const [notificationMessage, setNotificationMessage] = useState(''); + const [notificationError, setNotificationError] = useState(''); const minSelectableDate = useMemo(() => startOfDay(new Date()), []); const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']; @@ -192,7 +209,20 @@ function App() { storeId: slot?.storeId ? String(slot.storeId) : '', description: slot?.description || '' })) - : [] + : [], + notifications: { + ntfy: { + enabled: !!raw.notifications?.ntfy?.enabled, + serverUrl: raw.notifications?.ntfy?.serverUrl || 'https://ntfy.sh', + topicPrefix: raw.notifications?.ntfy?.topicPrefix || '', + username: raw.notifications?.ntfy?.username || '', + password: raw.notifications?.ntfy?.password || '' + }, + telegram: { + enabled: !!raw.notifications?.telegram?.enabled, + botToken: raw.notifications?.telegram?.botToken || '' + } + } }; }, []); @@ -206,6 +236,13 @@ function App() { setAdminSettingsLoading(false); setAvailableCollapsed(true); setInitializing(false); + setNotificationSettings(defaultNotificationSettings); + setNotificationCapabilities(defaultNotificationCapabilities); + setNotificationDirty(false); + setNotificationError(''); + setNotificationMessage(''); + setNotificationLoading(false); + setNotificationSaving(false); }, []); const handleUnauthorized = useCallback(() => { @@ -290,6 +327,47 @@ function App() { [handleUnauthorized, session?.token] ); + const loadNotificationSettings = useCallback(async () => { + if (!session?.token) { + return; + } + setNotificationLoading(true); + setNotificationError(''); + try { + const response = await authorizedFetch('/api/notifications/settings'); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + setNotificationSettings({ + ntfy: { + enabled: Boolean(data?.settings?.ntfy?.enabled), + topic: data?.settings?.ntfy?.topic || '', + serverUrl: data?.settings?.ntfy?.serverUrl || '' + }, + telegram: { + enabled: Boolean(data?.settings?.telegram?.enabled), + chatId: data?.settings?.telegram?.chatId || '' + } + }); + setNotificationCapabilities({ + ntfy: { + enabled: Boolean(data?.capabilities?.ntfy?.enabled), + serverUrl: data?.capabilities?.ntfy?.serverUrl || '', + topicPrefix: data?.capabilities?.ntfy?.topicPrefix || '' + }, + telegram: { + enabled: Boolean(data?.capabilities?.telegram?.enabled) + } + }); + setNotificationDirty(false); + } catch (err) { + setNotificationError(`Benachrichtigungseinstellungen konnten nicht geladen werden: ${err.message}`); + } finally { + setNotificationLoading(false); + } + }, [authorizedFetch, session?.token]); + useEffect(() => { if (!session?.token || !session.isAdmin) { setAdminSettings(null); @@ -445,6 +523,13 @@ function App() { } }, [session?.token, authorizedFetch]); + useEffect(() => { + if (!session?.token) { + return; + } + loadNotificationSettings(); + }, [session?.token, loadNotificationSettings]); + const syncStoresWithProgress = useCallback( async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => { const effectiveToken = tokenOverride || session?.token; @@ -951,6 +1036,81 @@ function App() { ); }; + const handleNotificationFieldChange = (channel, field, value) => { + setNotificationSettings((prev) => { + const nextChannel = { + ...prev[channel], + [field]: value + }; + return { + ...prev, + [channel]: nextChannel + }; + }); + setNotificationDirty(true); + }; + + const saveNotificationSettings = async () => { + if (!session?.token) { + return; + } + setNotificationSaving(true); + setNotificationError(''); + setNotificationMessage(''); + try { + const response = await authorizedFetch('/api/notifications/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ notifications: notificationSettings }) + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + setNotificationSettings({ + ntfy: { + enabled: Boolean(data?.ntfy?.enabled), + topic: data?.ntfy?.topic || notificationSettings.ntfy.topic, + serverUrl: data?.ntfy?.serverUrl || notificationSettings.ntfy.serverUrl + }, + telegram: { + enabled: Boolean(data?.telegram?.enabled), + chatId: data?.telegram?.chatId || notificationSettings.telegram.chatId + } + }); + setNotificationDirty(false); + setNotificationMessage('Benachrichtigungseinstellungen gespeichert.'); + setTimeout(() => setNotificationMessage(''), 4000); + } catch (err) { + setNotificationError(`Speichern der Benachrichtigungen fehlgeschlagen: ${err.message}`); + } finally { + setNotificationSaving(false); + } + }; + + const sendNotificationTest = async (channel) => { + if (!session?.token) { + return; + } + setNotificationError(''); + setNotificationMessage(''); + try { + const response = await authorizedFetch('/api/notifications/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channel }) + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + await response.json(); + setNotificationMessage('Testbenachrichtigung gesendet.'); + setTimeout(() => setNotificationMessage(''), 4000); + } catch (err) { + setNotificationError(`Testbenachrichtigung fehlgeschlagen: ${err.message}`); + } + }; + const handleAdminSettingChange = (field, value, isNumber = false) => { setAdminSettings((prev) => { if (!prev) { @@ -967,6 +1127,24 @@ function App() { }); }; + const handleAdminNotificationChange = (channel, field, value) => { + setAdminSettings((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + notifications: { + ...(prev.notifications || {}), + [channel]: { + ...(prev.notifications?.[channel] || {}), + [field]: value + } + } + }; + }); + }; + const handleIgnoredSlotChange = (index, field, value) => { setAdminSettings((prev) => { if (!prev) { @@ -1034,7 +1212,20 @@ function App() { ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({ storeId: slot.storeId || '', description: slot.description || '' - })) + })), + notifications: { + ntfy: { + enabled: !!adminSettings.notifications?.ntfy?.enabled, + serverUrl: adminSettings.notifications?.ntfy?.serverUrl || '', + topicPrefix: adminSettings.notifications?.ntfy?.topicPrefix || '', + username: adminSettings.notifications?.ntfy?.username || '', + password: adminSettings.notifications?.ntfy?.password || '' + }, + telegram: { + enabled: !!adminSettings.notifications?.telegram?.enabled, + botToken: adminSettings.notifications?.telegram?.botToken || '' + } + } }; const response = await authorizedFetch('/api/admin/settings', { @@ -1240,6 +1431,165 @@ function App() { )} +
+
+
+

Benachrichtigungen

+

+ Erhalte Hinweise über ntfy oder Telegram, sobald Slots gefunden oder gebucht wurden. +

+
+ {notificationLoading && Lade Einstellungen…} +
+ {notificationError && ( +
+ {notificationError} +
+ )} + {notificationMessage && ( +
+ {notificationMessage} +
+ )} +
+
+
+
+

ntfy

+

+ Push aufs Handy über die ntfy-App oder Browser (Themenkanal notwendig). +

+
+ +
+ {!notificationCapabilities.ntfy.enabled ? ( +

+ Diese Option wurde vom Admin deaktiviert. Bitte frage nach, wenn du ntfy nutzen möchtest. +

+ ) : ( +
+
+ + handleNotificationFieldChange('ntfy', 'topic', e.target.value)} + placeholder="z. B. mein-pickup-topic" + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={!notificationSettings.ntfy.enabled} + /> +
+
+ + handleNotificationFieldChange('ntfy', 'serverUrl', e.target.value)} + placeholder={notificationCapabilities.ntfy.serverUrl || 'https://ntfy.sh'} + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={!notificationSettings.ntfy.enabled} + /> + {notificationCapabilities.ntfy.topicPrefix && ( +

+ Vom Admin vorgegebenes Präfix: {notificationCapabilities.ntfy.topicPrefix} +

+ )} +
+ +
+ )} +
+ +
+
+
+

Telegram

+

+ Nutze den Bot des Admins, um Nachrichten direkt in Telegram zu erhalten. +

+
+ +
+ {!notificationCapabilities.telegram.enabled ? ( +

+ Telegram-Benachrichtigungen sind derzeit deaktiviert oder der Bot ist nicht konfiguriert. +

+ ) : ( +
+
+ + handleNotificationFieldChange('telegram', 'chatId', e.target.value)} + placeholder="z. B. 123456789" + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + disabled={!notificationSettings.telegram.enabled} + /> +

+ Tipp: Schreibe dem Telegram-Bot und nutze @userinfobot oder ein Chat-ID-Tool. +

+
+ +
+ )} +
+
+ +
+ + +
+
+
+
+

Benachrichtigungen

+

+ Lege fest, welche Kanäle zur Verfügung stehen und welche Zugangsdaten verwendet werden. +

+
+
+
+
+

ntfy

+

Server-basierte Push-Benachrichtigungen

+
+ +
+
+
+ + handleAdminNotificationChange('ntfy', 'serverUrl', e.target.value)} + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="https://ntfy.sh" + /> +
+
+ + handleAdminNotificationChange('ntfy', 'topicPrefix', e.target.value)} + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="optional" + /> +

+ Wird vor jedes User-Topic gesetzt (z. B. pickup/). +

+
+
+
+ + handleAdminNotificationChange('ntfy', 'username', e.target.value)} + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="optional" + /> +
+
+ + handleAdminNotificationChange('ntfy', 'password', e.target.value)} + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="optional" + /> +
+
+
+
+ +
+
+
+

Telegram

+

Benachrichtigungen über einen Bot

+
+ +
+
+ + handleAdminNotificationChange('telegram', 'botToken', e.target.value)} + className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="123456:ABCDEF" + /> +

+ Erstelle einen Bot via @BotFather und trage den Token hier ein. +

+
+
+
+
+

Ignorierte Slots