'use client' import { useAuth } from '@/lib/auth/context' import { useEffect, useState, useCallback } from 'react' import { useParams, useRouter } from 'next/navigation' import { motion, AnimatePresence } from 'framer-motion' import { getSite, type Site } from '@/lib/api/sites' import { getUptimeStatus, createUptimeMonitor, updateUptimeMonitor, deleteUptimeMonitor, getMonitorChecks, type UptimeStatusResponse, type MonitorStatus, type UptimeCheck, type UptimeDailyStat, type CreateMonitorRequest, } from '@/lib/api/uptime' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@/lib/utils/authErrors' import { LoadingOverlay, Button, Modal } from '@ciphera-net/ui' // * Status color mapping function getStatusColor(status: string): string { switch (status) { case 'up': case 'operational': return 'bg-emerald-500' case 'degraded': return 'bg-amber-500' case 'down': return 'bg-red-500' default: return 'bg-neutral-300 dark:bg-neutral-600' } } function getStatusDotColor(status: string): string { switch (status) { case 'up': case 'operational': return 'bg-emerald-500' case 'degraded': return 'bg-amber-500' case 'down': return 'bg-red-500' default: return 'bg-neutral-400' } } function getStatusLabel(status: string): string { switch (status) { case 'up': case 'operational': return 'Operational' case 'degraded': return 'Degraded' case 'down': return 'Down' default: return 'Unknown' } } function getDayStatus(stat: UptimeDailyStat | undefined): string { if (!stat) return 'no_data' if (stat.failed_checks > 0) return 'down' if (stat.degraded_checks > 0) return 'degraded' return 'up' } function getDayBarColor(stat: UptimeDailyStat | undefined): string { if (!stat) return 'bg-neutral-200 dark:bg-neutral-700' if (stat.failed_checks > 0) return 'bg-red-500' if (stat.degraded_checks > 0) return 'bg-amber-500' return 'bg-emerald-500' } function formatUptime(pct: number): string { return pct.toFixed(2) + '%' } function formatMs(ms: number | null): string { if (ms === null) return '-' if (ms < 1000) return `${ms}ms` return `${(ms / 1000).toFixed(1)}s` } function formatTimeAgo(dateString: string | null): string { if (!dateString) return 'Never' const date = new Date(dateString) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffSec = Math.floor(diffMs / 1000) if (diffSec < 60) return 'just now' if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago` if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago` return `${Math.floor(diffSec / 86400)}d ago` } // * Generate array of dates for the last N days function generateDateRange(days: number): string[] { const dates: string[] = [] const now = new Date() for (let i = days - 1; i >= 0; i--) { const d = new Date(now) d.setDate(d.getDate() - i) dates.push(d.toISOString().split('T')[0]) } return dates } // * Component: Uptime status bar (the colored bars visualization) function UptimeStatusBar({ dailyStats, days = 90, }: { dailyStats: UptimeDailyStat[] | null days?: number }) { const dateRange = generateDateRange(days) const statsMap = new Map() if (dailyStats) { for (const s of dailyStats) { statsMap.set(s.date, s) } } return (
{dateRange.map((date) => { const stat = statsMap.get(date) const barColor = getDayBarColor(stat) const dayStatus = getDayStatus(stat) const tooltipText = stat ? `${new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}: ${formatUptime(stat.uptime_percentage)} uptime (${stat.total_checks} checks)` : `${new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}: No data` return (
) })}
) } // * Component: Monitor card (matches the reference image design) function MonitorCard({ monitorStatus, expanded, onToggle, onEdit, onDelete, canEdit, siteId, }: { monitorStatus: MonitorStatus expanded: boolean onToggle: () => void onEdit: () => void onDelete: () => void canEdit: boolean siteId: string }) { const { monitor, daily_stats, overall_uptime } = monitorStatus const [checks, setChecks] = useState([]) const [loadingChecks, setLoadingChecks] = useState(false) useEffect(() => { if (expanded && checks.length === 0) { const fetchChecks = async () => { setLoadingChecks(true) try { const data = await getMonitorChecks(siteId, monitor.id, 20) setChecks(data) } catch { // * Silent fail for check details } finally { setLoadingChecks(false) } } fetchChecks() } }, [expanded, siteId, monitor.id, checks.length]) return (
{/* Header */} {/* Status bar */}
90 days ago Today
{/* Expanded details */} {expanded && (
{/* Monitor details grid */}
Status
{getStatusLabel(monitor.last_status)}
Response Time
{formatMs(monitor.last_response_time_ms)}
Check Interval
{monitor.check_interval_seconds >= 60 ? `${Math.floor(monitor.check_interval_seconds / 60)}m` : `${monitor.check_interval_seconds}s`}
Last Checked
{formatTimeAgo(monitor.last_checked_at)}
{/* Recent checks */} {loadingChecks ? (
Loading recent checks...
) : checks.length > 0 ? (

Recent Checks

{checks.map((check) => (
{new Date(check.checked_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', })}
{check.status_code && ( {check.status_code} )} {formatMs(check.response_time_ms)}
))}
) : null} {/* Actions */} {canEdit && (
)}
)}
) } // * Main uptime page export default function UptimePage() { const { user } = useAuth() const canEdit = user?.role === 'owner' || user?.role === 'admin' const params = useParams() const router = useRouter() const siteId = params.id as string const [site, setSite] = useState(null) const [loading, setLoading] = useState(true) const [uptimeData, setUptimeData] = useState(null) const [expandedMonitor, setExpandedMonitor] = useState(null) const [showAddModal, setShowAddModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [editingMonitor, setEditingMonitor] = useState(null) const [formData, setFormData] = useState({ name: '', url: '', check_interval_seconds: 300, expected_status_code: 200, timeout_seconds: 30, }) const [saving, setSaving] = useState(false) const loadData = useCallback(async () => { try { const [siteData, statusData] = await Promise.all([ getSite(siteId), getUptimeStatus(siteId), ]) setSite(siteData) setUptimeData(statusData) } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to load uptime data') } finally { setLoading(false) } }, [siteId]) useEffect(() => { loadData() }, [loadData]) // * Auto-refresh every 30 seconds useEffect(() => { const interval = setInterval(async () => { try { const statusData = await getUptimeStatus(siteId) setUptimeData(statusData) } catch { // * Silent refresh failure } }, 30000) return () => clearInterval(interval) }, [siteId]) const handleAddMonitor = async () => { if (!formData.name || !formData.url) { toast.error('Name and URL are required') return } setSaving(true) try { await createUptimeMonitor(siteId, formData) toast.success('Monitor created successfully') setShowAddModal(false) setFormData({ name: '', url: '', check_interval_seconds: 300, expected_status_code: 200, timeout_seconds: 30 }) await loadData() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to create monitor') } finally { setSaving(false) } } const handleEditMonitor = async () => { if (!editingMonitor || !formData.name || !formData.url) return setSaving(true) try { await updateUptimeMonitor(siteId, editingMonitor.monitor.id, { name: formData.name, url: formData.url, check_interval_seconds: formData.check_interval_seconds, expected_status_code: formData.expected_status_code, timeout_seconds: formData.timeout_seconds, enabled: editingMonitor.monitor.enabled, }) toast.success('Monitor updated successfully') setShowEditModal(false) setEditingMonitor(null) await loadData() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to update monitor') } finally { setSaving(false) } } const handleDeleteMonitor = async (monitorId: string) => { if (!confirm('Are you sure you want to delete this monitor? All historical data will be lost.')) return try { await deleteUptimeMonitor(siteId, monitorId) toast.success('Monitor deleted') await loadData() } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to delete monitor') } } const openEditModal = (ms: MonitorStatus) => { setEditingMonitor(ms) setFormData({ name: ms.monitor.name, url: ms.monitor.url, check_interval_seconds: ms.monitor.check_interval_seconds, expected_status_code: ms.monitor.expected_status_code, timeout_seconds: ms.monitor.timeout_seconds, }) setShowEditModal(true) } if (loading) return if (!site) return
Site not found
const monitors = uptimeData?.monitors ?? [] const overallUptime = uptimeData?.overall_uptime ?? 100 const overallStatus = uptimeData?.status ?? 'operational' return ( {/* Header */}
/

Uptime

Monitor your endpoints and track availability over time

{canEdit && ( )}
{/* Overall status card */} {monitors.length > 0 && (
{site.name} {monitors.length} {monitors.length === 1 ? 'component' : 'components'}
{formatUptime(overallUptime)} uptime
)} {/* Monitor list */} {monitors.length > 0 ? (
{monitors.map((ms) => ( setExpandedMonitor( expandedMonitor === ms.monitor.id ? null : ms.monitor.id )} onEdit={() => openEditModal(ms)} onDelete={() => handleDeleteMonitor(ms.monitor.id)} canEdit={canEdit} siteId={siteId} /> ))}
) : ( /* Empty state */

No monitors yet

Add a monitor to start tracking the uptime and response time of your endpoints. You can monitor APIs, websites, and any HTTP endpoint.

{canEdit && ( )}
)} {/* Add Monitor Modal */} setShowAddModal(false)} title="Add Monitor"> setShowAddModal(false)} saving={saving} submitLabel="Create Monitor" siteDomain={site.domain} /> {/* Edit Monitor Modal */} setShowEditModal(false)} title="Edit Monitor"> setShowEditModal(false)} saving={saving} submitLabel="Save Changes" siteDomain={site.domain} /> ) } // * Monitor creation/edit form function MonitorForm({ formData, setFormData, onSubmit, onCancel, saving, submitLabel, siteDomain, }: { formData: CreateMonitorRequest setFormData: (data: CreateMonitorRequest) => void onSubmit: () => void onCancel: () => void saving: boolean submitLabel: string siteDomain: string }) { return (
{/* Name */}
setFormData({ ...formData, name: e.target.value })} placeholder="e.g. API, Website, CDN" className="w-full px-3 py-2 rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:border-transparent text-sm" />
{/* URL */}
setFormData({ ...formData, url: e.target.value })} placeholder={`https://${siteDomain}`} className="w-full px-3 py-2 rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:border-transparent text-sm" />

Must be on {siteDomain} or a subdomain (e.g. api.{siteDomain})

{/* Check interval */}
{/* Expected status code */}
setFormData({ ...formData, expected_status_code: parseInt(e.target.value) || 200 })} min={100} max={599} className="w-full px-3 py-2 rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-orange focus:border-transparent text-sm" />
{/* Timeout */}
setFormData({ ...formData, timeout_seconds: parseInt(e.target.value) || 30 })} min={5} max={60} className="w-full px-3 py-2 rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand-orange focus:border-transparent text-sm" />
{/* Actions */}
) }