'use client' import { useAuth } from '@/lib/auth/context' import { useEffect, useState } from 'react' import { useParams } from 'next/navigation' import { useSite, useUptimeStatus } from '@/lib/swr/dashboard' import { updateSite, type Site } from '@/lib/api/sites' import { getMonitorChecks, type UptimeStatusResponse, type MonitorStatus, type UptimeCheck, type UptimeDailyStat, } from '@/lib/api/uptime' import { toast } from '@ciphera-net/ui' import { Button } from '@ciphera-net/ui' import { UptimeSkeleton, ChecksSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons' import { formatDateFull, formatTime, formatDateTimeShort } from '@/lib/utils/formatDate' import { AreaChart, Area, XAxis, YAxis, CartesianGrid, } from 'recharts' import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/charts' const responseTimeChartConfig = { ms: { label: 'Response Time', color: 'var(--chart-1)', }, } satisfies ChartConfig // * 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' } } // * Overall status text for the top card function getOverallStatusText(status: string): string { switch (status) { case 'up': case 'operational': return 'All Systems Operational' case 'degraded': return 'Partial Outage' case 'down': return 'Major Outage' default: return 'Unknown Status' } } function getOverallStatusTextColor(status: string): string { switch (status) { case 'up': case 'operational': return 'text-emerald-600 dark:text-emerald-400' case 'degraded': return 'text-amber-600 dark:text-amber-400' case 'down': return 'text-red-600 dark:text-red-400' default: return 'text-neutral-400' } } function getDayBarColor(stat: UptimeDailyStat | undefined): string { if (!stat || stat.total_checks === 0) return 'bg-neutral-300 dark:bg-neutral-600' 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: Styled tooltip for status bar function StatusBarTooltip({ stat, date, visible, position, }: { stat: UptimeDailyStat | undefined date: string visible: boolean position: { x: number; y: number } }) { if (!visible) return null const formattedDate = formatDateFull(new Date(date + 'T00:00:00')) return (
{formattedDate}
{stat && stat.total_checks > 0 ? (
Uptime {formatUptime(stat.uptime_percentage)}
Checks {stat.total_checks}
Avg Response {formatMs(Math.round(stat.avg_response_time_ms))}
{stat.failed_checks > 0 && (
Failed {stat.failed_checks}
)}
) : (
No data
)} {/* Tooltip arrow */}
) } // * 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) } } const [hoveredDay, setHoveredDay] = useState<{ date: string; stat: UptimeDailyStat | undefined } | null>(null) const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 }) const handleMouseEnter = (e: React.MouseEvent, date: string, stat: UptimeDailyStat | undefined) => { const rect = (e.target as HTMLElement).getBoundingClientRect() setTooltipPos({ x: rect.left + rect.width / 2, y: rect.top }) setHoveredDay({ date, stat }) } return (
setHoveredDay(null)} >
{dateRange.map((date) => { const stat = statsMap.get(date) const barColor = getDayBarColor(stat) return (
handleMouseEnter(e, date, stat)} onMouseLeave={() => setHoveredDay(null)} /> ) })}
) } // * Component: Response time chart (Recharts area chart) function ResponseTimeChart({ checks }: { checks: UptimeCheck[] }) { // * Prepare data in chronological order (oldest first) const data = [...checks] .reverse() .filter((c) => c.response_time_ms !== null) .map((c) => ({ time: formatTime(new Date(c.checked_at)), ms: c.response_time_ms as number, status: c.status, })) if (data.length < 2) return null return (

Response Time

`${v}ms`} /> {value}ms} /> } />
) } // * Main uptime page export default function UptimePage() { const { user } = useAuth() const canEdit = user?.role === 'owner' || user?.role === 'admin' const params = useParams() const siteId = params.id as string const { data: site, mutate: mutateSite } = useSite(siteId) const { data: uptimeData, isLoading, mutate: mutateUptime } = useUptimeStatus(siteId) const [toggling, setToggling] = useState(false) const [checks, setChecks] = useState([]) const [loadingChecks, setLoadingChecks] = useState(false) // * Single monitor from the auto-managed uptime system const monitor = uptimeData?.monitors?.[0] ?? null const overallUptime = uptimeData?.overall_uptime ?? 100 const overallStatus = uptimeData?.status ?? 'operational' // * Fetch recent checks when we have a monitor useEffect(() => { if (!monitor) { setChecks([]) return } const fetchChecks = async () => { setLoadingChecks(true) try { const data = await getMonitorChecks(siteId, monitor.monitor.id, 20) setChecks(data) } catch { // * Silent fail for check details } finally { setLoadingChecks(false) } } fetchChecks() }, [siteId, monitor?.monitor.id]) const handleToggleUptime = async (enabled: boolean) => { if (!site) return setToggling(true) try { await updateSite(site.id, { name: site.name, timezone: site.timezone, is_public: site.is_public, excluded_paths: site.excluded_paths, uptime_enabled: enabled, }) mutateSite() mutateUptime() toast.success(enabled ? 'Uptime monitoring enabled' : 'Uptime monitoring disabled') } catch { toast.error('Failed to update uptime monitoring') } finally { setToggling(false) } } useEffect(() => { if (site?.domain) document.title = `Uptime · ${site.domain} | Pulse` }, [site?.domain]) const showSkeleton = useMinimumLoading(isLoading && !uptimeData) const fadeClass = useSkeletonFade(showSkeleton) if (showSkeleton) return if (!site) return
Site not found
const uptimeEnabled = site.uptime_enabled // * Disabled state — show empty state with enable toggle if (!uptimeEnabled) { return (
{/* Header */}

Uptime

Monitor your site's availability and response time

{/* Empty state */}

Uptime monitoring is disabled

Enable uptime monitoring to track your site's availability and response time around the clock.

{canEdit && ( )}
) } // * Enabled state — show uptime dashboard return (
{/* Header */}

Uptime

Monitor your site's availability and response time

{canEdit && ( )}
{/* Overall status card */}
{site.name} {getOverallStatusText(overallStatus)}
{formatUptime(overallUptime)} uptime {monitor && (
Last checked {formatTimeAgo(monitor.monitor.last_checked_at)}
)}
{/* 90-day uptime bar */} {monitor && (

90-Day Availability

90 days ago Today
)} {/* Response time chart + Recent checks */} {monitor && (
{/* Monitor details grid */}
Status
{getStatusLabel(monitor.monitor.last_status)}
Response Time
{formatMs(monitor.monitor.last_response_time_ms)}
Check Interval
{monitor.monitor.check_interval_seconds >= 60 ? `${Math.floor(monitor.monitor.check_interval_seconds / 60)}m` : `${monitor.monitor.check_interval_seconds}s`}
Overall Uptime
{formatUptime(monitor.overall_uptime)}
{/* Response time chart */} {loadingChecks ? ( ) : checks.length > 0 ? ( <> {/* Recent checks */}

Recent Checks

{checks.slice(0, 20).map((check) => (
{formatDateTimeShort(new Date(check.checked_at))}
{check.status_code && ( {check.status_code} )} {formatMs(check.response_time_ms)}
))}
) : null}
)}
) }