'use client'; import { Activity, RefreshCcw, AlertCircle, X, Plus, Trash2, ExternalLink } from 'lucide-react'; import { useState, useEffect, useCallback, useRef } from 'react'; interface ServiceStatus { name: string; url: string; status: 'up' | 'down'; latency: number; uptime24h?: number; uptime7d?: number; uptimeLifetime?: number; avgLatency24h?: number; history?: Array<{ hour: string; up: boolean }>; } // Build exactly 24 bars from whatever history data exists function build24Bars(history: Array<{ hour: string; up: boolean }> | undefined) { const bars: Array<{ hour: string; status: 'up' | 'down' | 'unknown' }> = []; const now = new Date(); // Create a map of hour-string -> up/down from available data const hourMap = new Map(); if (history) { for (const h of history) { hourMap.set(h.hour, h.up); } } // Generate 24 hour slots ending at current hour for (let i = 23; i >= 0; i--) { const d = new Date(now); d.setMinutes(0, 0, 0); d.setHours(d.getHours() - i); // Format to match the API: "YYYY-MM-DD HH:00" const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + String(d.getHours()).padStart(2, '0') + ':00'; const found = hourMap.get(key); bars.push({ hour: key, status: found === undefined ? 'unknown' : found ? 'up' : 'down', }); } return bars; } function HistoryBars({ history, large }: { history?: Array<{ hour: string; up: boolean }>; large?: boolean }) { const bars = build24Bars(history); const h = large ? 'h-4' : 'h-1.5'; return (
{bars.map((b, i) => (
))}
); } function DetailModal({ service, onClose, onDelete, }: { service: ServiceStatus; onClose: () => void; onDelete: (name: string) => void; }) { const overlayRef = useRef(null); useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleKey); return () => document.removeEventListener('keydown', handleKey); }, [onClose]); return (
{ if (e.target === overlayRef.current) onClose(); }} >
{/* Header */}

{service.name}

{/* URL */}
{service.url}
{/* Status + Latency Row */}
Status
{service.status === 'up' ? 'Operational' : 'Down'}
Current Latency
{service.latency}ms
{/* Uptime Stats */}
24h
{service.uptime24h ?? 100}%
7d
{service.uptime7d ?? 100}%
Lifetime
{service.uptimeLifetime ?? 100}%
Avg Lat
{service.avgLatency24h ?? 0}ms
{/* 24h History Bar (large) */}
24-Hour History
-24h now
{/* Delete Button */}
); } function AddServiceForm({ onAdded }: { onAdded: () => void }) { const [open, setOpen] = useState(false); const [name, setName] = useState(''); const [url, setUrl] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setSubmitting(true); try { const res = await fetch('/api/services', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim(), url: url.trim() }), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || 'Failed to add'); } setName(''); setUrl(''); setOpen(false); onAdded(); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Failed to add service'); } finally { setSubmitting(false); } }; if (!open) { return ( ); } return (
setName(e.target.value)} required className="flex-1 bg-neutral-800 border border-neutral-700 rounded-lg px-2.5 py-1.5 text-xs text-white placeholder-neutral-500 focus:outline-none focus:border-neutral-500" /> setUrl(e.target.value)} required className="flex-[2] bg-neutral-800 border border-neutral-700 rounded-lg px-2.5 py-1.5 text-xs text-white placeholder-neutral-500 focus:outline-none focus:border-neutral-500" />
{error &&

{error}

}
); } export function UptimeCard() { const [services, setServices] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [selectedService, setSelectedService] = useState(null); const fetchStatus = useCallback(async () => { try { const res = await fetch('/api/status'); if (!res.ok) throw new Error('Failed to fetch'); const data = await res.json(); setServices(data); setError(false); } catch (e) { console.error(e); setError(true); } finally { setLoading(false); } }, []); useEffect(() => { fetchStatus(); const interval = setInterval(fetchStatus, 30000); return () => clearInterval(interval); }, [fetchStatus]); const handleDelete = async (serviceName: string) => { try { // Look up ID from the services API const listRes = await fetch('/api/services'); const list = await listRes.json(); const svc = list.find((s: { name: string }) => s.name === serviceName); if (!svc) return; await fetch('/api/services', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: svc.id }), }); fetchStatus(); } catch (e) { console.error('Delete failed:', e); } }; return ( <>
Uptime Monitor {loading && }
{error ? (
Failed to load status
) : (
{services.map((service) => (
setSelectedService(service)} >
{service.name}
{service.status === 'up' ? 'UP' : 'DOWN'} {service.latency}ms
24h: {service.uptime24h ?? 100}% lifetime: {service.uptimeLifetime ?? 100}% 7d: {service.uptime7d ?? 100}%
))}
)}
{selectedService && ( setSelectedService(null)} onDelete={handleDelete} /> )} ); }