"use client"; import { useEffect, useRef, useState } from "react"; import type { ServerConnectionPhase, ServerRuntime, } from "@/modules/shell/hooks/useServerConnection"; // After this long, restarting Homeio by hand is worth mentioning. Earlier it // would only worry people during an ordinary restart. const LONG_WAIT_MS = 45_000; const RESTART_COMMANDS: Record, { label: string; command: string }> = { host: { label: "Installed with the install script", command: "sudo systemctl restart home-server" }, docker: { label: "Running in Docker", command: "docker restart homeio" }, }; type ConnectionLostScreenProps = { phase: Exclude; isBrowserOffline: boolean; lostSince: number | null; serverRestarted: boolean; runtime: ServerRuntime; logoSrc: string; onCheckNow: () => void; }; function formatDuration(ms: number) { const totalSeconds = Math.max(0, Math.floor(ms / 1_000)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}:${seconds.toString().padStart(2, "0")}`; } // The icon set loads its images from the server, which is exactly what is // missing here, so these few glyphs are drawn inline. function CheckGlyph({ className }: { className?: string }) { return ( ); } function WifiOffGlyph({ className }: { className?: string }) { return ( ); } function CopyGlyph({ className }: { className?: string }) { return ( ); } function copyWithSelection(text: string) { const field = document.createElement("textarea"); field.value = text; field.style.position = "fixed"; field.style.opacity = "0"; document.body.appendChild(field); field.select(); try { return document.execCommand("copy"); } catch { return false; } finally { document.body.removeChild(field); } } // navigator.clipboard is missing on plain http (how Homeio is often opened on // the LAN) and some browsers refuse it outright, so fall back to the older // copy command before giving up. async function copyText(text: string) { try { await navigator.clipboard.writeText(text); return true; } catch { return copyWithSelection(text); } } function RestartCommand({ label, command }: { label: string; command: string }) { const [copyState, setCopyState] = useState<"idle" | "copied" | "manual">("idle"); const commandRef = useRef(null); useEffect(() => { if (copyState !== "copied") return; const timer = setTimeout(() => setCopyState("idle"), 1_800); return () => clearTimeout(timer); }, [copyState]); const copy = async () => { if (await copyText(command)) { setCopyState("copied"); return; } // Leave the command selected so a plain Ctrl/Cmd+C still works. const node = commandRef.current; const selection = window.getSelection(); if (node && selection) { const range = document.createRange(); range.selectNodeContents(node); selection.removeAllRanges(); selection.addRange(range); } setCopyState("manual"); }; return (

{label}

$ {command}
{copyState === "manual" ? (

Your browser blocked copying. The command is selected — press Ctrl+C (⌘C on a Mac).

) : null}
); } function getCopy( phase: ConnectionLostScreenProps["phase"], isBrowserOffline: boolean, isLongWait: boolean, serverRestarted: boolean, ) { if (phase === "restored") { return serverRestarted ? { title: "Homeio is back", body: "The server restarted. Loading the latest version for you…" } : { title: "Back online", body: "Reconnected to Homeio. Everything is where you left it." }; } if (isBrowserOffline) { return { title: "You're offline", body: "This device lost its network connection. Homeio will pick up where you left off as soon as you're back online.", }; } if (isLongWait) { return { title: "Homeio is still unreachable", body: "This is taking longer than a normal restart. Homeio keeps trying in the background, and you can also restart it yourself.", }; } return { title: "Reconnecting to Homeio…", body: "The server stopped answering — it may be restarting or updating. Your apps and files are safe, and this page reconnects on its own.", }; } export function ConnectionLostScreen({ phase, isBrowserOffline, lostSince, serverRestarted, runtime, logoSrc, onCheckNow, }: ConnectionLostScreenProps) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1_000); return () => clearInterval(timer); }, []); const restored = phase === "restored"; const elapsedMs = lostSince ? now - lostSince : 0; const isLongWait = !restored && !isBrowserOffline && elapsedMs >= LONG_WAIT_MS; const copy = getCopy(phase, isBrowserOffline, isLongWait, serverRestarted); const commands = runtime ? [RESTART_COMMANDS[runtime]] : [RESTART_COMMANDS.host, RESTART_COMMANDS.docker]; return (
{!restored ? [0, 1.2, 2.4].map((delay) => ( )) : null}
{/* eslint-disable-next-line @next/next/no-img-element -- a blob URL, fetched before the server went away */} Homeio
{restored || isBrowserOffline ? ( {restored ? : } ) : null}
{restored ? "Connected" : isBrowserOffline ? "No network" : "Connection lost"}

{copy.title}

{copy.body}

{!restored ? (
{/* No countdown to the next check: with a try every two seconds it only loops 3, 2, 1 and reads as a glitch. */} {lostSince ? `Offline for ${formatDuration(elapsedMs)}` : "Offline"} {" · reconnecting automatically"}
) : null} {isLongWait ? (

Restart Homeio on the server

{commands.map((entry) => ( ))}

If the server itself doesn't respond, check that the machine is powered on and connected to your network.

) : null}
); }