"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState, type KeyboardEvent } from "react"; import { useI18n } from "@/lib/i18n/useI18n"; type MachineRow = { id: string; name: string; code?: string | null; location?: string | null; latestHeartbeat: null | { ts: string; tsServer?: string | null; status: string; message?: string | null; ip?: string | null; fwVersion?: string | null; }; }; function secondsAgo(ts: string | undefined, locale: string, fallback: string) { if (!ts) return fallback; const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); if (diff < 60) return rtf.format(-diff, "second"); return rtf.format(-Math.floor(diff / 60), "minute"); } function isOffline(ts?: string) { if (!ts) return true; return Date.now() - new Date(ts).getTime() > 30000; // 30s threshold } function normalizeStatus(status?: string) { const s = (status ?? "").toUpperCase(); if (s === "ONLINE") return "RUN"; return s; } function badgeClass(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"; } export default function MachinesClient({ initialMachines = [] }: { initialMachines?: MachineRow[] }) { const { t, locale } = useI18n(); const router = useRouter(); const [machines, setMachines] = useState(() => initialMachines); const [loading, setLoading] = useState(false); const [showCreate, setShowCreate] = useState(false); const [createName, setCreateName] = useState(""); const [createCode, setCreateCode] = useState(""); const [createLocation, setCreateLocation] = useState(""); const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(null); const [createdMachine, setCreatedMachine] = useState<{ id: string; name: string; pairingCode: string; pairingExpiresAt: string; } | null>(null); const [copyStatus, setCopyStatus] = useState(null); useEffect(() => { let alive = true; async function load() { try { const res = await fetch("/api/machines", { cache: "no-store" }); const json = await res.json(); if (alive) { setMachines(json.machines ?? []); setLoading(false); } } catch { if (alive) setLoading(false); } } load(); const t = setInterval(load, 15000); return () => { alive = false; clearInterval(t); }; }, []); async function createMachine() { if (!createName.trim()) { setCreateError(t("machines.create.error.nameRequired")); return; } setCreating(true); setCreateError(null); try { const res = await fetch("/api/machines", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: createName, code: createCode, location: createLocation, }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.ok) { throw new Error(data.error || t("machines.create.error.failed")); } const nextMachine = { ...data.machine, latestHeartbeat: null, }; setMachines((prev) => [nextMachine, ...prev]); setCreatedMachine({ id: data.machine.id, name: data.machine.name, pairingCode: data.machine.pairingCode, pairingExpiresAt: data.machine.pairingCodeExpiresAt, }); setCreateName(""); setCreateCode(""); setCreateLocation(""); setShowCreate(false); } catch (err: unknown) { const message = err instanceof Error ? err.message : null; setCreateError(message || t("machines.create.error.failed")); } finally { setCreating(false); } } async function copyText(text: string) { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); setCopyStatus(t("machines.pairing.copied")); } else { setCopyStatus(t("machines.pairing.copyUnsupported")); } } catch { setCopyStatus(t("machines.pairing.copyFailed")); } setTimeout(() => setCopyStatus(null), 2000); } function handleCardKeyDown(event: KeyboardEvent, machineId: string) { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); router.push(`/machines/${machineId}`); } } const showCreateCard = showCreate || (!loading && machines.length === 0); return (

{t("machines.title")}

{t("machines.subtitle")}

{t("machines.backOverview")}
{showCreateCard && (
{t("machines.addCardTitle")}
{t("machines.addCardSubtitle")}
{createError &&
{createError}
}
)} {createdMachine && (
{t("machines.pairing.title")}
{t("machines.pairing.machine")} {createdMachine.name}
{t("machines.pairing.codeLabel")}
{createdMachine.pairingCode}
{t("machines.pairing.expires")}{" "} {createdMachine.pairingExpiresAt ? new Date(createdMachine.pairingExpiresAt).toLocaleString(locale) : t("machines.pairing.soon")}
{t("machines.pairing.instructions")}
{copyStatus &&
{copyStatus}
}
)} {loading &&
{t("machines.loading")}
} {!loading && machines.length === 0 && (
{t("machines.empty")}
)}
{(!loading ? machines : []).map((m) => { const hb = m.latestHeartbeat; const hbTs = hb?.tsServer ?? hb?.ts; const offline = isOffline(hbTs); const normalizedStatus = normalizeStatus(hb?.status); const statusLabel = offline ? t("machines.status.offline") : (normalizedStatus || t("machines.status.unknown")); const lastSeen = secondsAgo(hbTs, locale, t("common.never")); return (
router.push(`/machines/${m.id}`)} onKeyDown={(event) => handleCardKeyDown(event, m.id)} className="cursor-pointer rounded-2xl border border-white/10 bg-white/5 p-5 hover:bg-white/10" >
{m.name}
{m.code ? m.code : t("common.na")} - {t("machines.lastSeen", { time: lastSeen })}
{statusLabel}
{t("machines.status")}
{offline ? ( <>
); })}
); }