'use client'; import { useState, useEffect, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { Send, RefreshCw, Trash2, Plus, CheckCircle2, XCircle, AlertTriangle, Clock, Loader2, ChevronDown, ChevronUp, ToggleLeft, ToggleRight, } from 'lucide-react'; import { PageHeader } from '@/components/navigation/page-header'; interface WebhookConfig { id: number; label: string; webhook_url: string; enabled: boolean; last_delivered_at: string | null; last_status: string | null; created_at: string; } interface SummaryConfig { weekend_suppression: boolean; monday_extended_window: boolean; severity_filter: number; outages_only: boolean; } interface SummaryRow { id: number; generated_at: string; open_count: number; resolved_count: number; mttr_minutes: number | null; is_weekend_window: boolean; delivery_status: Record; card_payload?: object; window_from?: string; window_to?: string; clients_affected?: string[]; } function fmtDate(d: string | null) { if (!d) return 'Never'; const date = new Date(d); const diff = Date.now() - date.getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'Just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.floor(hrs / 24)}d ago`; } function StatusBadge({ status }: { status: string | null }) { if (!status) return Never sent; if (status === 'success') return ( Success ); return ( Failed ); } export default function MorningSummaryPage() { const [webhooks, setWebhooks] = useState([]); const [config, setConfig] = useState(null); const [history, setHistory] = useState([]); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); const [testingId, setTestingId] = useState(null); const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null); const [cardExpanded, setCardExpanded] = useState(false); const [showAddWebhook, setShowAddWebhook] = useState(false); const [newLabel, setNewLabel] = useState(''); const [newUrl, setNewUrl] = useState(''); const [addingWebhook, setAddingWebhook] = useState(false); const showToast = (msg: string, ok: boolean) => { setToast({ msg, ok }); setTimeout(() => setToast(null), 4000); }; const fetchAll = useCallback(async () => { setLoading(true); try { const [wRes, cRes, hRes] = await Promise.all([ fetch('/api/notifications/morning-summary/webhooks'), fetch('/api/notifications/morning-summary/config'), fetch('/api/notifications/morning-summary/history'), ]); const [wData, cData, hData] = await Promise.all([wRes.json(), cRes.json(), hRes.json()]); setWebhooks(wData.webhooks ?? []); setConfig(cData.config ?? null); setHistory(hData.history ?? []); } finally { setLoading(false); } }, []); useEffect(() => { fetchAll(); }, [fetchAll]); const handleSendAll = async () => { setSending(true); try { const res = await fetch('/api/notifications/morning-summary/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); const ok = data.results?.filter((r: any) => r.success).length ?? 0; const fail = data.results?.filter((r: any) => !r.success).length ?? 0; showToast(`Sent — ${ok} succeeded, ${fail} failed`, fail === 0); fetchAll(); } catch (e) { showToast(String(e), false); } finally { setSending(false); } }; const handleTest = async (webhookId: number, label: string) => { setTestingId(webhookId); try { const res = await fetch('/api/notifications/morning-summary/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ webhookId }), }); const data = await res.json(); showToast(data.result?.success ? `✅ Test sent to ${label}` : `❌ Test failed: ${data.result?.error ?? data.error}`, data.result?.success); fetchAll(); } catch (e) { showToast(String(e), false); } finally { setTestingId(null); } }; const handleToggleWebhook = async (webhook: WebhookConfig) => { try { await fetch(`/api/notifications/morning-summary/webhooks/${webhook.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !webhook.enabled }), }); setWebhooks(prev => prev.map(w => w.id === webhook.id ? { ...w, enabled: !w.enabled } : w)); } catch (e) { showToast(String(e), false); } }; const handleDeleteWebhook = async (id: number) => { if (!confirm('Delete this webhook?')) return; try { await fetch(`/api/notifications/morning-summary/webhooks/${id}`, { method: 'DELETE' }); setWebhooks(prev => prev.filter(w => w.id !== id)); } catch (e) { showToast(String(e), false); } }; const handleAddWebhook = async () => { if (!newLabel.trim() || !newUrl.trim()) return; setAddingWebhook(true); try { const res = await fetch('/api/notifications/morning-summary/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label: newLabel.trim(), webhook_url: newUrl.trim() }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error); setWebhooks(prev => [...prev, data.webhook]); setNewLabel(''); setNewUrl(''); setShowAddWebhook(false); showToast('Webhook added', true); } catch (e) { showToast(String(e), false); } finally { setAddingWebhook(false); } }; const handleConfigToggle = async (field: keyof SummaryConfig) => { if (!config) return; const updated = { ...config, [field]: !config[field] }; setConfig(updated); try { await fetch('/api/notifications/morning-summary/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: updated[field] }), }); } catch (e) { showToast(String(e), false); setConfig(config); } }; const latestSummary = history[0] ?? null; if (loading) { return (
); } return ( <> } />
{/* Toast */} {toast && (
{toast.ok ? : } {toast.msg}
)} {/* Last Run Stats */} {latestSummary && (

Last Run

{fmtDate(latestSummary.generated_at)} {latestSummary.is_weekend_window && Weekend}
0 ? 'text-red-400' : 'text-muted-foreground'}`}>{latestSummary.open_count}
Open
0 ? 'text-green-400' : 'text-muted-foreground'}`}>{latestSummary.resolved_count}
Resolved
{latestSummary.mttr_minutes != null ? `${latestSummary.mttr_minutes}m` : '—'}
Avg MTTR
{/* Delivery results */} {Object.keys(latestSummary.delivery_status).length > 0 && (

Delivery

{Object.entries(latestSummary.delivery_status).map(([wid, r]) => { const webhook = webhooks.find(w => w.id === parseInt(wid)); return (
{webhook?.label ?? `Webhook #${wid}`} {r.success ? Delivered : {r.error ?? `HTTP ${r.httpStatus}`} }
); })}
)} {/* Card preview toggle */} {latestSummary.card_payload && (
{cardExpanded && (
                  {JSON.stringify(latestSummary.card_payload, null, 2)}
                
)}
)}
)} {/* Webhooks */}

Webhooks

{showAddWebhook && (
setNewLabel(e.target.value)} /> setNewUrl(e.target.value)} />
)} {webhooks.length === 0 && !showAddWebhook && (

No webhooks configured. Add one above.

)}
{webhooks.map(webhook => (
{webhook.label} {webhook.enabled ? Enabled : Disabled }
{webhook.last_delivered_at && ( {fmtDate(webhook.last_delivered_at)} )}
))}
{/* Schedule Config */} {config && (

Schedule Settings

Weekend Suppression

Skip Saturday & Sunday (cron already limits to Mon–Fri)

Monday Extended Window

On Mondays, extend window to cover the full weekend (Fri 6 PM → Mon 6:30 AM)

Outages Only

Only show "Unavailable" problems — filter out slow response and other non-outage alerts

)} {/* Run History */} {history.length > 0 && (

Recent Runs

{history.map(row => { const statusEntries = Object.values(row.delivery_status); const allOk = statusEntries.length > 0 && statusEntries.every((r: any) => r.success); const anyFail = statusEntries.some((r: any) => !r.success); return (
{allOk && } {anyFail && } {statusEntries.length === 0 && } {fmtDate(row.generated_at)} {row.is_weekend_window && Weekend}
0 ? 'text-red-400' : 'text-muted-foreground'}> {row.open_count} open 0 ? 'text-green-400' : 'text-muted-foreground'}> {row.resolved_count} resolved {row.mttr_minutes != null && ( {row.mttr_minutes}m MTTR )}
); })}
)}
); }