'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}
)}
)
}