Monitor websites uptime using Cloudflare Workers
Something went wrong. Try again.
TSX
at optimization
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633import 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<NotificationProviderKind, string> = { 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<NotificationService[] | null>(null); const [editor, setEditor] = useState<EditorState | null>(null); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const [busyId, setBusyId] = useState<string | null>(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<unknown>, 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 ( <section className="notifications-page"> <div className="page-head"> <div> <h1>Notifications</h1> <p>Send outage, recovery, and optional reminder messages to your team.</p> </div> {!editor && ( <button className="button button--primary" onClick={() => setEditor(emptyEditor)}> Add service </button> )} </div>
{error && ( <p className="field-error notification-feedback" role="alert"> {error} </p> )} {notice && ( <p className="notification-feedback" role="status"> {notice} </p> )}
{editor ? ( <NotificationServiceEditor value={editor} onCancel={() => setEditor(null)} onSaved={() => { setEditor(null); setNotice('Notification service saved.'); setReload((value) => value + 1); }} /> ) : services === null ? ( <div className="state state--loading" role="status"> <p>Loading notification services…</p> </div> ) : services.length === 0 && !error ? ( <div className="state state--empty" role="status"> <p>No notification services yet.</p> <button className="button button--quiet" onClick={() => setEditor(emptyEditor)}> Add service </button> </div> ) : ( <div className="notification-list"> {services.map((service) => ( <article className="notification-card" key={service.id}> <div> <span className="mono-label">{service.provider}</span> <h2>{service.name}</h2> <p> {service.provider === 'telegram' && service.config.chatId ? `Chat ${service.config.chatId}` : service.provider === 'discord' ? 'Discord webhook configured' : 'Configuration saved'} </p> </div> <div className="notification-card__actions"> <label className="notification-enabled"> <input type="checkbox" checked={service.enabled} disabled={busyId === service.id} onChange={(event) => void run( service.id, () => api.updateNotificationService(service.id, { enabled: event.target.checked, }), event.target.checked ? 'Notification service enabled.' : 'Notification service disabled.', ) } /> Enabled </label> <button className="button button--quiet" disabled={busyId === service.id} onClick={() => void run( service.id, () => api.testNotificationService(service.id), 'Test notification sent.', ) } > Test </button> <button className="button button--quiet" disabled={busyId === service.id} onClick={() => setEditor(editorFromService(service))} > Edit </button> <button className="button button--danger" disabled={busyId === service.id} onClick={() => { if (!window.confirm(`Delete “${service.name}”? Monitors will stop using it.`)) return; void run( service.id, () => api.deleteNotificationService(service.id), 'Notification service deleted.', ); }} > Delete </button> </div> </article> ))} </div> )} </section> );}
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 = <K extends keyof EditorState>(key: K, next: EditorState[K]) => setValue((current) => ({ ...current, [key]: next }));
async function submit(event: FormEvent) { event.preventDefault(); setBusy(true); setError(''); try { const config: Record<string, unknown> = 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 ( <form className="monitor-form notification-editor" onSubmit={submit}> <div className="notification-editor__heading"> <div> <h2>{editing ? 'Edit service' : 'Add notification service'}</h2> <p>Credentials are stored for sending and are never returned to the browser.</p> </div> </div> <div className="form-grid"> <label className="field"> <span>Name</span> <input value={value.name} maxLength={120} required onChange={(event) => set('name', event.target.value)} /> </label> <label className="field"> <span>Provider</span> <select value={value.provider} disabled={editing} onChange={(event) => set('provider', event.target.value as NotificationProviderKind)} > {Object.entries(providerLabels).map(([kind, label]) => ( <option key={kind} value={kind}> {label} </option> ))} </select> {editing && <small>The provider cannot be changed after creation.</small>} </label> </div>
{value.provider === 'telegram' ? ( <> <aside className="notification-instructions"> <strong>Telegram setup</strong> <p> Create a bot with BotFather and copy its token. Start the bot or add it to a group, send a message, then use Telegram’s getUpdates endpoint to find the chat ID. </p> </aside> <div className="form-grid"> <label className="field"> <span>Bot token</span> <input type="password" autoComplete="new-password" value={value.botToken} required={!editing} placeholder={editing ? 'Leave blank to keep the saved token' : '123456:ABC…'} onChange={(event) => set('botToken', event.target.value)} /> {editing && <small>Leave blank to keep the saved token.</small>} </label> <label className="field"> <span>Chat ID</span> <input value={value.chatId} required placeholder="-1001234567890" onChange={(event) => set('chatId', event.target.value)} /> </label> </div> </> ) : value.provider === 'discord' ? ( <> <aside className="notification-instructions"> <strong>Discord setup</strong> <p> Open Server Settings, Integrations, Webhooks. Create or select a webhook and copy its webhook URL. </p> </aside> <label className="field"> <span>Webhook URL</span> <input type="password" inputMode="url" autoComplete="new-password" value={value.webhookUrl} required={!editing} placeholder={ editing ? 'Leave blank to keep the saved URL' : 'https://discord.com/api/webhooks/…' } onChange={(event) => set('webhookUrl', event.target.value)} /> {editing && <small>Leave blank to keep the saved webhook URL.</small>} </label> </> ) : ( <ProviderFields value={value} set={set} editing={editing} /> )}
<label className="notification-enabled"> <input type="checkbox" checked={value.enabled} onChange={(event) => set('enabled', event.target.checked)} /> Enable this service </label> {error && ( <p className="field-error" role="alert"> {error} </p> )} <div className="form-actions"> <button type="button" className="button button--quiet" onClick={onCancel}> Cancel </button> <button className="button button--primary" disabled={busy}> {busy ? 'Saving…' : 'Save service'} </button> </div> </form> );}
function messageFor(reason: unknown, fallback: string) { return reason instanceof RequestError || reason instanceof Error ? reason.message : fallback;}
export function buildNotificationConfig(value: EditorState) { const config: Record<string, unknown> = {}; 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: <K extends keyof EditorState>(key: K, next: EditorState[K]) => void; editing: boolean;}) { const secret = (label: string, key: keyof EditorState, placeholder: string, required = true) => ( <label className="field"> <span>{label}</span> <input type="password" autoComplete="new-password" value={value[key] as string} required={required && !editing} placeholder={editing ? 'Leave blank to keep the saved value' : placeholder} onChange={(event) => set(key, event.target.value)} /> {editing && <small>Leave blank to keep the saved value.</small>} </label> ); const text = (label: string, key: keyof EditorState, required = true) => ( <label className="field"> <span>{label}</span> <input value={value[key] as string} required={required} onChange={(event) => set(key, event.target.value)} /> </label> ); const recipientsField = (label: string) => ( <label className="field"> <span>{label}</span> <textarea value={value.to} required onChange={(event) => set('to', event.target.value)} rows={3} /> </label> ); if (value.provider === 'resend') return ( <> <aside className="notification-instructions"> <strong>Resend setup</strong> <p>Create an API key with permission to send mail and verify the From domain.</p> </aside> <div className="form-grid"> {secret('API key', 'apiKey', 're_…')} {text('From email', 'from')} {recipientsField('To email addresses')} {text('Subject', 'subject', false)} </div> </> ); if (value.provider === 'gotify') return ( <div className="form-grid"> {text('Server URL', 'serverUrl')} {secret('Application token', 'applicationToken', 'Application token')} {text('Priority (0–10)', 'priority')} </div> ); if (value.provider === 'webhook') return ( <div className="form-grid"> {secret('Webhook URL', 'webhookUrl', 'https://example.com/hook')} {secret('Bearer token', 'bearerToken', 'Optional bearer token', false)} </div> ); if (value.provider === 'home-assistant') return ( <div className="form-grid"> {text('Server URL', 'serverUrl')} {secret('Access token', 'accessToken', 'Long-lived access token')} {text('Service', 'service')} </div> ); return ( <> <div className="form-grid"> {text('SMTP host', 'host')} {text('Port', 'port')} <label className="field"> <span>Security</span> <select value={value.security} onChange={(event) => set('security', event.target.value as EditorState['security'])} > <option value="starttls">STARTTLS</option> <option value="tls">TLS</option> <option value="none">None</option> </select> </label> {text('Username', 'username', false)} {secret('Password', 'password', 'Optional password', false)} {text('From email', 'from')} {recipientsField('To email addresses')} {text('Subject', 'subject', false)} </div> <small>Recipients accept comma or newline separated email addresses.</small> {value.security === 'none' && ( <p className="field-error"> SMTP security is disabled. Credentials and notification content will be sent in plaintext. </p> )} </> );}