'use client' import { useState } from 'react' import { Button, toast, Spinner, Modal, Select } from '@ciphera-net/ui' import { Plus, Pencil, Trash, EnvelopeSimple, WebhooksLogo, PaperPlaneTilt } from '@phosphor-icons/react' import { SiDiscord } from '@icons-pack/react-simple-icons' import { useReportSchedules, useAlertSchedules } from '@/lib/swr/dashboard' import { useSite } from '@/lib/swr/dashboard' import { createReportSchedule, updateReportSchedule, deleteReportSchedule, testReportSchedule, type ReportSchedule, type CreateReportScheduleRequest, type EmailConfig, type WebhookConfig, } from '@/lib/api/report-schedules' import { getAuthErrorMessage } from '@ciphera-net/ui' import { formatDateTime } from '@/lib/utils/formatDate' // ── Helpers ────────────────────────────────────────────────────────────────── const TIMEZONES = [ 'UTC', 'America/New_York', 'America/Los_Angeles', 'America/Chicago', 'America/Toronto', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Amsterdam', 'Asia/Tokyo', 'Asia/Singapore', 'Asia/Dubai', 'Australia/Sydney', 'Pacific/Auckland', ] const WEEKDAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] const formatHour = (hour: number) => { if (hour === 0) return '12:00 AM' if (hour === 12) return '12:00 PM' return hour < 12 ? `${hour}:00 AM` : `${hour - 12}:00 PM` } const ordinalSuffix = (n: number) => { const s = ['th', 'st', 'nd', 'rd'] const v = n % 100 return n + (s[(v - 20) % 10] || s[v] || s[0]) } // ── Icons ──────────────────────────────────────────────────────────────────── function SlackIcon({ size = 16 }: { size?: number }) { return ( ) } const CHANNEL_ICONS: Record = { email: , slack: , discord: , webhook: , } function ChannelIcon({ channel }: { channel: string }) { return <>{CHANNEL_ICONS[channel] ?? } } // ── Schedule Row ───────────────────────────────────────────────────────────── function ScheduleRow({ schedule, siteId, onMutate, onEdit, }: { schedule: ReportSchedule siteId: string onMutate: () => void onEdit: (schedule: ReportSchedule) => void }) { const [testing, setTesting] = useState(false) const handleTest = async () => { setTesting(true) try { await testReportSchedule(siteId, schedule.id) toast.success('Test report sent') } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to send test') } finally { setTesting(false) } } const handleToggle = async () => { try { await updateReportSchedule(siteId, schedule.id, { channel: schedule.channel, channel_config: schedule.channel_config, frequency: schedule.frequency, report_type: schedule.report_type, enabled: !schedule.enabled, send_hour: schedule.send_hour, send_day: schedule.send_day ?? undefined, timezone: schedule.timezone, purpose: schedule.purpose, }) toast.success(schedule.enabled ? 'Report paused' : 'Report enabled') onMutate() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to update') } } const handleDelete = async () => { try { await deleteReportSchedule(siteId, schedule.id) toast.success('Report deleted') onMutate() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to delete') } } return (

{schedule.channel === 'email' && 'recipients' in schedule.channel_config ? (schedule.channel_config as EmailConfig).recipients?.[0] : schedule.channel} {!schedule.enabled && (paused)}

{schedule.frequency} · {schedule.report_type} report {schedule.last_sent_at && ( · sent {formatDateTime(new Date(schedule.last_sent_at))} )}

{schedule.last_error && (

{schedule.last_error}

)}
) } // ── Channel Grid Picker ────────────────────────────────────────────────────── const CHANNELS = ['email', 'slack', 'discord', 'webhook'] as const function ChannelPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) { return (
{CHANNELS.map((ch) => ( ))}
) } // ── Shared form label ──────────────────────────────────────────────────────── function FormLabel({ children, htmlFor }: { children: React.ReactNode; htmlFor?: string }) { return } function FormInput({ id, type = 'text', value, onChange, placeholder }: { id?: string; type?: string; value: string; onChange: (v: string) => void; placeholder?: string }) { return ( onChange(e.target.value)} placeholder={placeholder} className="w-full h-10 px-4 bg-transparent border border-neutral-800 rounded-lg text-sm text-white placeholder:text-neutral-600 focus:outline-none focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 transition-colors" /> ) } // ── Report Schedule Modal ──────────────────────────────────────────────────── function ReportScheduleModal({ isOpen, onClose, siteId, siteTimezone, editing, onSaved, }: { isOpen: boolean onClose: () => void siteId: string siteTimezone: string editing: ReportSchedule | null onSaved: () => void }) { const [saving, setSaving] = useState(false) const [form, setForm] = useState(() => formFromSchedule(editing, siteTimezone)) // Reset form when editing target changes function formFromSchedule(schedule: ReportSchedule | null, fallbackTz: string) { if (schedule) { return { channel: schedule.channel, recipients: schedule.channel === 'email' && 'recipients' in schedule.channel_config ? (schedule.channel_config as EmailConfig).recipients.join(', ') : '', webhookUrl: schedule.channel !== 'email' && 'url' in schedule.channel_config ? (schedule.channel_config as WebhookConfig).url : '', frequency: schedule.frequency, reportType: schedule.report_type, timezone: schedule.timezone || fallbackTz, sendHour: schedule.send_hour, sendDay: schedule.send_day ?? 1, } } return { channel: 'email', recipients: '', webhookUrl: '', frequency: 'weekly', reportType: 'summary', timezone: fallbackTz, sendHour: 9, sendDay: 1, } } // Re-init when modal opens with different editing target const [prevEditing, setPrevEditing] = useState(editing) if (editing !== prevEditing) { setPrevEditing(editing) setForm(formFromSchedule(editing, siteTimezone)) } const updateField = (key: K, value: (typeof form)[K]) => setForm((f) => ({ ...f, [key]: value })) const handleSubmit = async () => { // Validation if (form.channel === 'email') { const emails = form.recipients.split(',').map((r) => r.trim()).filter(Boolean) if (emails.length === 0) { toast.error('Enter at least one email address'); return } } else { if (!form.webhookUrl.trim()) { toast.error('Enter a webhook URL'); return } } setSaving(true) try { const channelConfig: EmailConfig | WebhookConfig = form.channel === 'email' ? { recipients: form.recipients.split(',').map((r) => r.trim()).filter(Boolean) } : { url: form.webhookUrl.trim() } const payload: CreateReportScheduleRequest = { channel: form.channel, channel_config: channelConfig, frequency: form.frequency, report_type: form.reportType, timezone: form.timezone, send_hour: form.sendHour, send_day: form.frequency === 'weekly' || form.frequency === 'monthly' ? form.sendDay : undefined, purpose: 'report', } if (editing) { await updateReportSchedule(siteId, editing.id, payload) toast.success('Report schedule updated') } else { await createReportSchedule(siteId, payload) toast.success('Report schedule created') } onSaved() onClose() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to save schedule') } finally { setSaving(false) } } const webhookPlaceholder = form.channel === 'slack' ? 'https://hooks.slack.com/services/...' : form.channel === 'discord' ? 'https://discord.com/api/webhooks/...' : 'https://example.com/webhook' return (
{/* Channel */}
Channel updateField('channel', v)} />
{/* Recipients / URL */} {form.channel === 'email' ? (
Recipients updateField('recipients', v)} placeholder="email@example.com, another@example.com" />

Comma-separated email addresses

) : (
Webhook URL updateField('webhookUrl', v)} placeholder={webhookPlaceholder} />
)} {/* Frequency */}
Frequency updateField('sendDay', Number(v))} variant="input" fullWidth options={WEEKDAY_NAMES.map((name, i) => ({ value: String(i + 1), label: name }))} />
)} {/* Day of month (monthly) */} {form.frequency === 'monthly' && (
Day of Month updateField('sendHour', Number(v))} variant="input" fullWidth options={Array.from({ length: 24 }, (_, i) => ({ value: String(i), label: formatHour(i), }))} />
{/* Timezone */}
Timezone updateField('reportType', v)} variant="input" fullWidth options={[ { value: 'summary', label: 'Summary' }, { value: 'pages', label: 'Pages' }, { value: 'sources', label: 'Sources' }, { value: 'goals', label: 'Goals' }, ]} />
{/* Actions */}
) } // ── Alert Channel Modal ────────────────────────────────────────────────────── function AlertChannelModal({ isOpen, onClose, siteId, siteTimezone, editing, onSaved, }: { isOpen: boolean onClose: () => void siteId: string siteTimezone: string editing: ReportSchedule | null onSaved: () => void }) { const [saving, setSaving] = useState(false) const [form, setForm] = useState(() => formFromAlert(editing)) function formFromAlert(schedule: ReportSchedule | null) { if (schedule) { return { channel: schedule.channel, recipients: schedule.channel === 'email' && 'recipients' in schedule.channel_config ? (schedule.channel_config as EmailConfig).recipients.join(', ') : '', webhookUrl: schedule.channel !== 'email' && 'url' in schedule.channel_config ? (schedule.channel_config as WebhookConfig).url : '', } } return { channel: 'email', recipients: '', webhookUrl: '' } } const [prevEditing, setPrevEditing] = useState(editing) if (editing !== prevEditing) { setPrevEditing(editing) setForm(formFromAlert(editing)) } const updateField = (key: K, value: (typeof form)[K]) => setForm((f) => ({ ...f, [key]: value })) const handleSubmit = async () => { if (form.channel === 'email') { const emails = form.recipients.split(',').map((r) => r.trim()).filter(Boolean) if (emails.length === 0) { toast.error('Enter at least one email address'); return } } else { if (!form.webhookUrl.trim()) { toast.error('Enter a webhook URL'); return } } setSaving(true) try { const channelConfig: EmailConfig | WebhookConfig = form.channel === 'email' ? { recipients: form.recipients.split(',').map((r) => r.trim()).filter(Boolean) } : { url: form.webhookUrl.trim() } const payload: CreateReportScheduleRequest = { channel: form.channel, channel_config: channelConfig, frequency: 'daily', // Alerts don't have a user-chosen frequency timezone: siteTimezone, purpose: 'alert', } if (editing) { await updateReportSchedule(siteId, editing.id, payload) toast.success('Alert channel updated') } else { await createReportSchedule(siteId, payload) toast.success('Alert channel created') } onSaved() onClose() } catch (err) { toast.error(getAuthErrorMessage(err as Error) || 'Failed to save alert channel') } finally { setSaving(false) } } const webhookPlaceholder = form.channel === 'slack' ? 'https://hooks.slack.com/services/...' : form.channel === 'discord' ? 'https://discord.com/api/webhooks/...' : 'https://example.com/webhook' return (
{/* Channel */}
Channel updateField('channel', v)} />
{/* Recipients / URL */} {form.channel === 'email' ? (
Recipients updateField('recipients', v)} placeholder="email@example.com, another@example.com" />

Comma-separated email addresses

) : (
Webhook URL updateField('webhookUrl', v)} placeholder={webhookPlaceholder} />
)} {/* Info box */}

Alerts are sent automatically when your site goes down or recovers.

{/* Actions */}
) } // ── Main Tab ───────────────────────────────────────────────────────────────── export default function SiteReportsTab({ siteId }: { siteId: string }) { const { data: site } = useSite(siteId) const { data: reports = [], isLoading: reportsLoading, mutate: mutateReports } = useReportSchedules(siteId) const { data: alerts = [], isLoading: alertsLoading, mutate: mutateAlerts } = useAlertSchedules(siteId) // Report modal state const [reportModalOpen, setReportModalOpen] = useState(false) const [editingSchedule, setEditingSchedule] = useState(null) // Alert modal state const [alertModalOpen, setAlertModalOpen] = useState(false) const [editingAlert, setEditingAlert] = useState(null) const siteTimezone = site?.timezone || 'UTC' const loading = reportsLoading || alertsLoading const openNewReport = () => { setEditingSchedule(null); setReportModalOpen(true) } const openEditReport = (schedule: ReportSchedule) => { setEditingSchedule(schedule); setReportModalOpen(true) } const openNewAlert = () => { setEditingAlert(null); setAlertModalOpen(true) } const openEditAlert = (schedule: ReportSchedule) => { setEditingAlert(schedule); setAlertModalOpen(true) } if (loading) return
return (
{/* Scheduled Reports */}

Scheduled Reports

Automated analytics summaries via email or webhook.

{reports.length === 0 ? (

No scheduled reports yet.

) : (
{reports.map((r) => ( mutateReports()} onEdit={openEditReport} /> ))}
)}
{/* Alert Channels */}

Alert Channels

Get notified when uptime monitors go down.

{alerts.length === 0 ? (

No alert channels configured.

) : (
{alerts.map((a) => ( mutateAlerts()} onEdit={openEditAlert} /> ))}
)}
{/* Report Schedule Modal */} {reportModalOpen && ( setReportModalOpen(false)} siteId={siteId} siteTimezone={siteTimezone} editing={editingSchedule} onSaved={() => mutateReports()} /> )} {/* Alert Channel Modal */} {alertModalOpen && ( setAlertModalOpen(false)} siteId={siteId} siteTimezone={siteTimezone} editing={editingAlert} onSaved={() => mutateAlerts()} /> )}
) }