"use client"; import { useState, useEffect } from "react"; import { Activity, Plus, RefreshCw, Trash2, ExternalLink } from "lucide-react"; interface App { id: string; name: string; description: string; url: string; port: number; path: string; command: string; category: string; color: string; enabled: boolean; } interface StatusEntry { appId: string; timestamp: string; status: "up" | "down"; responseTime?: number; } export default function HeartbeatMonitor() { const [apps, setApps] = useState([]); const [status, setStatus] = useState([]); const [loading, setLoading] = useState(true); const [checking, setChecking] = useState(null); const [showAddApp, setShowAddApp] = useState(false); const [newApp, setNewApp] = useState>({ name: "", description: "", url: "http://localhost:", port: 3000, path: "", command: "npm run dev", category: "Other", color: "#22C55E", enabled: true, }); useEffect(() => { fetchData(); const interval = setInterval(fetchData, 30000); return () => clearInterval(interval); }, []); async function fetchData() { try { const res = await fetch("/api/monitor"); const data = await res.json(); setApps(data.apps || []); setStatus(data.status || []); } catch (err) { console.error("Failed to fetch data:", err); } finally { setLoading(false); } } async function checkApp(app: App) { setChecking(app.id); try { const start = Date.now(); await fetch(app.url, { method: "HEAD", mode: "no-cors" }); const responseTime = Date.now() - start; const entry: StatusEntry = { appId: app.id, timestamp: new Date().toISOString(), status: "up", responseTime, }; await fetch("/api/monitor", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "recordStatus", entry }), }); fetchData(); } catch { const entry: StatusEntry = { appId: app.id, timestamp: new Date().toISOString(), status: "down", }; await fetch("/api/monitor", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "recordStatus", entry }), }); fetchData(); } finally { setChecking(null); } } async function addApp(e: React.FormEvent) { e.preventDefault(); if (!newApp.name || !newApp.url) return; await fetch("/api/monitor", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "addApp", app: newApp }), }); setNewApp({ name: "", description: "", url: "http://localhost:", port: 3000, path: "", command: "npm run dev", category: "Other", color: "#22C55E", enabled: true, }); setShowAddApp(false); fetchData(); } async function deleteApp(id: string) { if (!confirm("Delete this app from monitoring?")) return; await fetch("/api/monitor", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "deleteApp", id }), }); fetchData(); } function getAppStatus(appId: string) { const appStatus = status.filter((s) => s.appId === appId); const latest = appStatus[appStatus.length - 1]; const isUp = latest?.status === "up"; const uptime = appStatus.length > 0 ? Math.round((appStatus.filter(s => s.status === "up").length / appStatus.length) * 100) : 100; return { latest, isUp, uptime }; } const totalApps = apps.length; const onlineApps = apps.filter((app) => getAppStatus(app.id).isUp).length; if (loading) { return (
Loading...
); } return (
{/* Header */}

Heartbeat Monitor

{onlineApps} of {totalApps} services online

{/* Table */}
{apps.map((app) => { const { isUp, uptime, latest } = getAppStatus(app.id); return ( ); })}
Status Name URL Port Uptime Response Actions
{isUp ? "ONLINE" : "OFFLINE"}

{app.name}

{app.description}

{app.url} {app.port} = 90 ? "text-emerald-400" : "text-yellow-400"}`}> {uptime}% {latest?.responseTime ? `${latest.responseTime}ms` : "--"}
{/* Add App Modal */} {showAddApp && (

Add New App

setNewApp({ ...newApp, name: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" required />
setNewApp({ ...newApp, description: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" />
setNewApp({ ...newApp, url: e.target.value })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" required />
setNewApp({ ...newApp, port: parseInt(e.target.value) })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white" required />
)}
); }