"use client"; import { reloadBrowserWindow } from "@/lib/desktop/browser-reload"; import { clearPersistedPowerActionState, readPersistedPowerActionState, writePersistedPowerActionCompletion, writePersistedPowerActionState, } from "@/lib/desktop/reboot-state"; import { StatusScreen } from "@/modules/shell/components/status-screen"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; const HEALTH_POLL_MS = 2_000; const STARTING_PHASE_MS = 4_000; export type UpdateRecoveryPhase = "starting" | "waiting" | "reconnecting" | "failed"; type UpdateRecoveryScreenProps = { manageRecovery?: boolean; phaseOverride?: Exclude; }; function getUpdateRecoveryCopy(phase: UpdateRecoveryPhase, fatalError: string | null) { if (phase === "failed") { return { title: "Homeio update could not start", body: fatalError ?? "The update request failed before the service restart began. Return to Homeio and try again.", }; } if (phase === "reconnecting") { return { title: "Reconnecting to Homeio...", body: "The update completed and Homeio is coming back. This page will refresh automatically when the new build is ready.", }; } if (phase === "waiting") { return { title: "Applying Homeio update...", body: "The updater is rebuilding and restarting Homeio now. This page will reconnect automatically when the updated service is available again.", }; } return { title: "Starting Homeio update...", body: "Preparing the updater now. This page will stay in recovery mode until Homeio finishes restarting.", }; } export function UpdateRecoveryScreen({ manageRecovery = true, phaseOverride, }: UpdateRecoveryScreenProps) { const router = useRouter(); const [phase, setPhase] = useState("starting"); const [fatalError, setFatalError] = useState(null); const phaseCopy = useMemo(() => { return getUpdateRecoveryCopy(phaseOverride ?? phase, fatalError); }, [fatalError, phase, phaseOverride]); useEffect(() => { if (manageRecovery) return; setFatalError(null); setPhase(phaseOverride ?? "starting"); }, [manageRecovery, phaseOverride]); useEffect(() => { if (!manageRecovery) return; if (typeof window === "undefined") return; const actionState = readPersistedPowerActionState(window.localStorage); if (actionState?.action !== "update") { router.replace("/"); return; } let cancelled = false; let timer: ReturnType | null = null; let hasObservedUnavailable = false; const scheduleNextCheck = () => { if (cancelled) return; timer = setTimeout(() => { void pollRecovery(); }, HEALTH_POLL_MS); }; const pollRecovery = async () => { if (cancelled) return; const latestState = readPersistedPowerActionState(window.localStorage); if (!latestState || latestState.action !== "update") { router.replace("/"); return; } if (!latestState.requestDispatchedAt) { writePersistedPowerActionState(window.localStorage, { ...latestState, requestDispatchedAt: latestState.startedAt, }); } const startedAtMs = Date.parse(latestState.startedAt); const elapsedMs = Number.isFinite(startedAtMs) ? Date.now() - startedAtMs : STARTING_PHASE_MS; setPhase(elapsedMs < STARTING_PHASE_MS ? "starting" : "waiting"); try { const healthResponse = await fetch("/api/health", { cache: "no-store", }); if (!healthResponse.ok) { hasObservedUnavailable = true; scheduleNextCheck(); return; } if (!hasObservedUnavailable && elapsedMs < STARTING_PHASE_MS) { scheduleNextCheck(); return; } setPhase("reconnecting"); const authResponse = await fetch("/api/auth/me", { cache: "no-store", }); if (authResponse.status === 401) { clearPersistedPowerActionState(window.localStorage); router.replace("/login"); return; } if (!authResponse.ok) { scheduleNextCheck(); return; } writePersistedPowerActionCompletion(window.localStorage, { action: "update", completedAt: new Date().toISOString(), }); clearPersistedPowerActionState(window.localStorage); reloadBrowserWindow(); } catch { hasObservedUnavailable = true; scheduleNextCheck(); } }; void pollRecovery(); return () => { cancelled = true; if (timer) { clearTimeout(timer); } }; }, [manageRecovery, router]); return ( router.replace("/")} className="mt-6 inline-flex items-center rounded-[var(--radius)] bg-primary px-5 py-2.5 text-sm font-medium text-primary-foreground transition hover:brightness-110" > Return to Homeio ) : null } /> ); }