"use client"; import { useState, useRef, useEffect } from "react"; import { Github, MessageSquare, Download, AlertCircle, CheckCircle2, RefreshCw, ExternalLink, X } from "lucide-react"; import { API_BASE } from "@/lib/api"; import packageJson from "../../package.json"; type UpdateStatus = | "idle" | "checking" | "available" | "uptodate" | "error" | "confirming" | "updating" | "restarting" | "update_error"; export default function TopRightControls() { const [updateStatus, setUpdateStatus] = useState("idle"); const [latestVersion, setLatestVersion] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const pollRef = useRef | null>(null); const timeoutRef = useRef | null>(null); const currentVersion = packageJson.version; // Cleanup polling on unmount useEffect(() => { return () => { if (pollRef.current) clearInterval(pollRef.current); if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []); const checkForUpdates = async () => { setUpdateStatus("checking"); try { const res = await fetch("https://api.github.com/repos/BigBodyCobain/Shadowbroker/releases/latest"); if (!res.ok) throw new Error("Failed to fetch"); const data = await res.json(); const latest = data.tag_name?.replace("v", "") || data.name?.replace("v", ""); const current = currentVersion.replace("v", ""); if (latest && latest !== current) { setLatestVersion(latest); setUpdateStatus("available"); } else { setUpdateStatus("uptodate"); setTimeout(() => setUpdateStatus("idle"), 3000); } } catch (err) { console.error("Update check failed:", err); setUpdateStatus("error"); setTimeout(() => setUpdateStatus("idle"), 3000); } }; const startRestartPolling = () => { setUpdateStatus("restarting"); // Poll /api/health until backend comes back pollRef.current = setInterval(async () => { try { const h = await fetch(`${API_BASE}/api/health`); if (h.ok) { if (pollRef.current) clearInterval(pollRef.current); if (timeoutRef.current) clearTimeout(timeoutRef.current); window.location.reload(); } } catch { // Backend still down — keep polling } }, 3000); // Give up after 90 seconds timeoutRef.current = setTimeout(() => { if (pollRef.current) clearInterval(pollRef.current); setErrorMessage("Restart timed out — the app may need to be started manually."); setUpdateStatus("update_error"); }, 90000); }; const triggerUpdate = async () => { setUpdateStatus("updating"); setErrorMessage(""); try { const headers: Record = {}; const adminKey = typeof window !== "undefined" ? localStorage.getItem("sb_admin_key") : null; if (adminKey) headers["X-Admin-Key"] = adminKey; const res = await fetch(`${API_BASE}/api/system/update`, { method: "POST", headers }); const data = await res.json(); if (!res.ok) throw new Error(data.message || data.detail || "Update failed"); startRestartPolling(); } catch (err: any) { // The update extracts files over the project, which causes the Next.js // dev server to hot-reload and drop the proxy connection mid-request. // A network error during update likely means it SUCCEEDED and the // server is restarting — transition to polling instead of showing failure. const isNetworkDrop = err instanceof TypeError || err.message === "Failed to fetch"; if (isNetworkDrop) { startRestartPolling(); } else { setErrorMessage(err.message || "Unknown error"); setUpdateStatus("update_error"); } } }; // ── Confirmation Dialog ── const renderConfirmDialog = () => (
{/* Header */}
UPDATE v{currentVersion} → v{latestVersion}
{/* Actions */}
MANUAL DOWNLOAD
); // ── Error Dialog ── const renderErrorDialog = () => (
UPDATE FAILED

{errorMessage}

MANUAL DOWNLOAD
); return (
{/* Discussions link */} DISCUSSIONS {/* ── Update Available → opens confirmation ── */} {updateStatus === "available" && ( )} {/* ── Confirming → show dialog ── */} {updateStatus === "confirming" && ( <> {renderConfirmDialog()} )} {/* ── Updating → spinner ── */} {updateStatus === "updating" && (
DOWNLOADING UPDATE...
)} {/* ── Restarting → spinner + waiting ── */} {updateStatus === "restarting" && (
RESTARTING...
)} {/* ── Error → show error dialog ── */} {updateStatus === "update_error" && ( <> {renderErrorDialog()} )} {/* ── Default states: idle / checking / uptodate / check-error ── */} {!["available", "confirming", "updating", "restarting", "update_error"].includes(updateStatus) && ( )}
); }