"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; type Heartbeat = { ts: string; status: string; message?: string | null; ip?: string | null; fwVersion?: string | null; }; type Kpi = { ts: string; oee?: number | null; availability?: number | null; performance?: number | null; quality?: number | null; workOrderId?: string | null; sku?: string | null; good?: number | null; scrap?: number | null; target?: number | null; cycleTime?: number | null; }; type EventRow = { id: string; ts: string; topic: string; eventType: string; severity: string; title: string; description?: string | null; requiresAck: boolean; }; type MachineDetail = { id: string; name: string; code?: string | null; location?: string | null; latestHeartbeat: Heartbeat | null; latestKpi: Kpi | null; }; export default function MachineDetailClient() { const params = useParams<{ machineId: string }>(); const machineId = params?.machineId; const [loading, setLoading] = useState(true); const [machine, setMachine] = useState(null); const [events, setEvents] = useState([]); const [error, setError] = useState(null); useEffect(() => { if (!machineId) return; // <-- IMPORTANT guard let alive = true; async function load() { try { const res = await fetch(`/api/machines/${machineId}`, { cache: "no-store", credentials: "include", }); const json = await res.json(); if (!alive) return; if (!res.ok || json?.ok === false) { setError(json?.error ?? "Failed to load machine"); setLoading(false); return; } setMachine(json.machine ?? null); setEvents(json.events ?? []); setError(null); setLoading(false); } catch { if (!alive) return; setError("Network error"); setLoading(false); } } load(); const t = setInterval(load, 5000); return () => { alive = false; clearInterval(t); }; }, [machineId]); function fmtPct(v?: number | null) { if (v === null || v === undefined || Number.isNaN(v)) return "—"; return `${v.toFixed(1)}%`; } function fmtNum(v?: number | null) { if (v === null || v === undefined || Number.isNaN(v)) return "—"; return `${v}`; } function timeAgo(ts?: string) { if (!ts) return "never"; const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); if (diff < 60) return `${diff}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; return `${Math.floor(diff / 3600)}h ago`; } function isOffline(ts?: string) { if (!ts) return true; return Date.now() - new Date(ts).getTime() > 15000; } function statusBadgeClass(status?: string, offline?: boolean) { if (offline) return "bg-white/10 text-zinc-300"; const s = (status ?? "").toUpperCase(); if (s === "RUN") return "bg-emerald-500/15 text-emerald-300"; if (s === "IDLE") return "bg-yellow-500/15 text-yellow-300"; if (s === "STOP" || s === "DOWN") return "bg-red-500/15 text-red-300"; return "bg-white/10 text-white"; } function severityBadgeClass(sev?: string) { const s = (sev ?? "").toLowerCase(); if (s === "critical") return "bg-red-500/15 text-red-300"; if (s === "warning") return "bg-yellow-500/15 text-yellow-300"; return "bg-white/10 text-zinc-200"; } const hb = machine?.latestHeartbeat ?? null; const kpi = machine?.latestKpi ?? null; const offline = useMemo(() => isOffline(hb?.ts), [hb?.ts]); const statusLabel = offline ? "OFFLINE" : (hb?.status ?? "UNKNOWN"); return (

{machine?.name ?? "Machine"}

{statusLabel}
{machine?.code ? machine.code : "—"} • {machine?.location ? machine.location : "—"} • Last seen{" "} {hb?.ts ? timeAgo(hb.ts) : "never"}
Back
{loading &&
Loading…
} {error && !loading && (
{error}
)} {!loading && !error && ( <> {/* KPI cards */}
OEE
{fmtPct(kpi?.oee)}
Updated {kpi?.ts ? timeAgo(kpi.ts) : "never"}
Availability
{fmtPct(kpi?.availability)}
Performance
{fmtPct(kpi?.performance)}
Quality
{fmtPct(kpi?.quality)}
{/* Work order + recent events */}
Current Work Order
{kpi?.workOrderId ?? "—"}
SKU
{kpi?.sku ?? "—"}
Target
{fmtNum(kpi?.target)}
Good
{fmtNum(kpi?.good)}
Scrap
{fmtNum(kpi?.scrap)}
Cycle target: {kpi?.cycleTime ? `${kpi.cycleTime}s` : "—"}
Recent Events
{events.length} shown
{events.length === 0 ? (
No events yet.
) : (
{events.map((e) => (
{e.severity.toUpperCase()} {e.eventType} {e.requiresAck && ( ACK )}
{e.title}
{e.description && (
{e.description}
)}
{timeAgo(e.ts)}
))}
)}
)}
); }