import type { NotificationProviderKind, NotificationService, NotificationServiceCreate, NotificationServiceUpdate, } from '@uptime/contracts'; import { useEffect, useState } from 'react'; import type { FormEvent } from 'react'; import { api, RequestError } from './api.js'; interface EditorState { id?: string; name: string; provider: NotificationProviderKind; enabled: boolean; botToken: string; chatId: string; webhookUrl: string; apiKey: string; from: string; to: string; subject: string; serverUrl: string; applicationToken: string; priority: string; bearerToken: string; host: string; port: string; security: 'tls' | 'starttls' | 'none'; username: string; password: string; accessToken: string; service: string; } const emptyEditor: EditorState = { name: '', provider: 'telegram', enabled: true, botToken: '', chatId: '', webhookUrl: '', apiKey: '', from: '', to: '', subject: '', serverUrl: '', applicationToken: '', priority: '8', bearerToken: '', host: '', port: '587', security: 'starttls', username: '', password: '', accessToken: '', service: 'notify', }; const providerLabels: Record = { telegram: 'Telegram', discord: 'Discord', resend: 'Resend', gotify: 'Gotify', webhook: 'Webhook', smtp: 'SMTP', 'home-assistant': 'Home Assistant', }; function recipients(value: string) { return value .split(/[\n,]/) .map((item) => item.trim()) .filter(Boolean); } function readableConfig(service: NotificationService, key: string, fallback = '') { const config = service.config; const value = key === 'from' ? config.from : key === 'subject' ? config.subject : key === 'serverUrl' ? config.serverUrl : key === 'host' ? config.host : key === 'username' ? config.username : key === 'service' ? config.service : key === 'security' ? config.security : key === 'priority' ? config.priority : key === 'port' ? config.port : undefined; return value === undefined ? fallback : String(value); } function readableRecipients(service: NotificationService) { return service.config.to?.join(', ') ?? ''; } function editorFromService(service: NotificationService): EditorState { return { id: service.id, name: service.name, provider: service.provider, enabled: service.enabled, botToken: '', chatId: readableConfig(service, 'chatId'), webhookUrl: '', apiKey: '', from: readableConfig(service, 'from'), to: readableRecipients(service), subject: readableConfig(service, 'subject'), serverUrl: readableConfig(service, 'serverUrl'), applicationToken: '', priority: String(readableConfig(service, 'priority', '8')), bearerToken: '', host: readableConfig(service, 'host'), port: readableConfig(service, 'port', '587'), security: readableConfig(service, 'security', 'starttls') as EditorState['security'], username: readableConfig(service, 'username'), password: '', accessToken: '', service: readableConfig(service, 'service', 'notify'), }; } export function NotificationsPage() { const [services, setServices] = useState(null); const [editor, setEditor] = useState(null); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const [busyId, setBusyId] = useState(null); const [reload, setReload] = useState(0); useEffect(() => { let alive = true; setError(''); api .notificationServices() .then(({ services: loaded }) => alive && setServices(loaded)) .catch((reason) => { if (!alive) return; setServices([]); setError(messageFor(reason, 'Notification services could not be loaded.')); }); return () => { alive = false; }; }, [reload]); async function run(id: string, action: () => Promise, success: string) { setBusyId(id); setError(''); setNotice(''); try { await action(); setNotice(success); setReload((value) => value + 1); } catch (reason) { setError(messageFor(reason, 'The notification service could not be updated.')); } finally { setBusyId(null); } } return (

Notifications

Send outage, recovery, and optional reminder messages to your team.

{!editor && ( )}
{error && (

{error}

)} {notice && (

{notice}

)} {editor ? ( setEditor(null)} onSaved={() => { setEditor(null); setNotice('Notification service saved.'); setReload((value) => value + 1); }} /> ) : services === null ? (

Loading notification services…

) : services.length === 0 && !error ? (

No notification services yet.

) : (
{services.map((service) => (
{service.provider}

{service.name}

{service.provider === 'telegram' && service.config.chatId ? `Chat ${service.config.chatId}` : service.provider === 'discord' ? 'Discord webhook configured' : 'Configuration saved'}

))}
)}
); } function NotificationServiceEditor({ value: initialValue, onCancel, onSaved, }: { value: EditorState; onCancel: () => void; onSaved: () => void; }) { const [value, setValue] = useState(initialValue); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const editing = Boolean(value.id); const set = (key: K, next: EditorState[K]) => setValue((current) => ({ ...current, [key]: next })); async function submit(event: FormEvent) { event.preventDefault(); setBusy(true); setError(''); try { const config: Record = buildNotificationConfig(value); if (value.id) { const input: NotificationServiceUpdate = { name: value.name, enabled: value.enabled, config: config as NotificationServiceUpdate['config'], }; await api.updateNotificationService(value.id, input); } else { const input = { name: value.name, enabled: value.enabled, provider: value.provider, config, } as NotificationServiceCreate; await api.createNotificationService(input); } onSaved(); } catch (reason) { setError(messageFor(reason, 'The notification service could not be saved.')); } finally { setBusy(false); } } return (

{editing ? 'Edit service' : 'Add notification service'}

Credentials are stored for sending and are never returned to the browser.

{value.provider === 'telegram' ? ( <>
) : value.provider === 'discord' ? ( <> ) : ( )} {error && (

{error}

)}
); } function messageFor(reason: unknown, fallback: string) { return reason instanceof RequestError || reason instanceof Error ? reason.message : fallback; } export function buildNotificationConfig(value: EditorState) { const config: Record = {}; const put = (key: string, item: unknown, secret = false) => { if (!secret || (typeof item === 'string' && item.length > 0)) config[key] = item; }; const putOptional = (key: string, item: string) => { if (item.trim() || value.id) config[key] = item.trim(); }; switch (value.provider) { case 'telegram': put('botToken', value.botToken, true); put('chatId', value.chatId); break; case 'discord': put('webhookUrl', value.webhookUrl, true); break; case 'resend': put('apiKey', value.apiKey, true); put('from', value.from); put('to', recipients(value.to)); putOptional('subject', value.subject); break; case 'gotify': put('serverUrl', value.serverUrl); put('applicationToken', value.applicationToken, true); put('priority', Number(value.priority || 8)); break; case 'webhook': put('webhookUrl', value.webhookUrl, true); put('bearerToken', value.bearerToken, true); break; case 'smtp': put('host', value.host); put('port', Number(value.port || 587)); put('security', value.security); putOptional('username', value.username); put('password', value.password, true); put('from', value.from); put('to', recipients(value.to)); putOptional('subject', value.subject); break; case 'home-assistant': put('serverUrl', value.serverUrl); put('accessToken', value.accessToken, true); put('service', value.service || 'notify'); break; } return config; } function ProviderFields({ value, set, editing, }: { value: EditorState; set: (key: K, next: EditorState[K]) => void; editing: boolean; }) { const secret = (label: string, key: keyof EditorState, placeholder: string, required = true) => ( ); const text = (label: string, key: keyof EditorState, required = true) => ( ); const recipientsField = (label: string) => (