2026-02-08 02:32:45 -05:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { Activity, RefreshCcw, AlertCircle } from 'lucide-react';
|
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
|
|
|
|
interface ServiceStatus {
|
|
|
|
|
name: string;
|
|
|
|
|
url: string;
|
|
|
|
|
status: 'up' | 'down';
|
|
|
|
|
latency: number;
|
2026-02-08 03:03:53 -05:00
|
|
|
uptime24h?: number;
|
|
|
|
|
uptime7d?: number;
|
2026-02-09 00:50:40 -05:00
|
|
|
history?: Array<{ hour: string; up: boolean }>;
|
2026-02-08 02:32:45 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function UptimeCard() {
|
|
|
|
|
const [services, setServices] = useState<ServiceStatus[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState(false);
|
|
|
|
|
|
|
|
|
|
const fetchStatus = 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);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="col-span-1 md:col-span-2 lg:col-span-2 row-span-1 bg-neutral-900 border border-neutral-800 rounded-xl p-6 flex flex-col justify-between hover:border-neutral-700 transition-colors">
|
|
|
|
|
<div className="flex justify-between items-start">
|
|
|
|
|
<div className="flex items-center gap-2 text-neutral-400">
|
|
|
|
|
<Activity size={18} />
|
|
|
|
|
<span className="text-sm font-medium">Uptime Monitor</span>
|
|
|
|
|
{loading && <RefreshCcw size={12} className="animate-spin ml-2 opacity-50" />}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{error ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-full text-red-400 gap-2">
|
|
|
|
|
<AlertCircle size={24} />
|
|
|
|
|
<span className="text-xs">Failed to load status</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-02-08 03:03:53 -05:00
|
|
|
<div className="space-y-4 mt-4">
|
|
|
|
|
{services.map((service) => (
|
|
|
|
|
<div key={service.name} className="flex flex-col gap-1">
|
|
|
|
|
<div className="flex items-center justify-between group">
|
2026-02-08 02:32:45 -05:00
|
|
|
<span className="text-sm text-neutral-300 font-medium group-hover:text-white transition-colors">{service.name}</span>
|
|
|
|
|
<div className="flex items-center gap-2">
|
2026-02-08 03:03:53 -05:00
|
|
|
<span className={`text-xs px-1.5 py-0.5 rounded ${service.status === 'up' ? 'bg-emerald-500/10 text-emerald-500' : 'bg-red-500/10 text-red-500'}`}>
|
|
|
|
|
{service.status === 'up' ? 'UP' : 'DOWN'}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-xs text-neutral-500 font-mono w-10 text-right">{service.latency}ms</span>
|
2026-02-08 02:32:45 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-08 03:03:53 -05:00
|
|
|
<div className="flex gap-[2px] h-1.5 opacity-50">
|
2026-02-09 00:50:40 -05:00
|
|
|
{service.history && service.history.length > 0 ? (
|
|
|
|
|
service.history.slice(-24).map((h, i) => (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className={`flex-1 rounded-full ${h.up ? 'bg-emerald-500' : 'bg-red-500'}`}
|
|
|
|
|
title={h.hour}
|
|
|
|
|
/>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
[...Array(24)].map((_, i) => (
|
|
|
|
|
<div key={i} className="flex-1 rounded-full bg-neutral-700" />
|
|
|
|
|
))
|
|
|
|
|
)}
|
2026-02-08 03:03:53 -05:00
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-between text-[10px] text-neutral-600 font-mono">
|
|
|
|
|
<span>24h: {service.uptime24h ?? 100}%</span>
|
|
|
|
|
<span>7d: {service.uptime7d ?? 100}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-02-08 02:32:45 -05:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|