IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

update-recovery-screen.tsx

Buka raw
"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<UpdateRecoveryPhase, "failed">;
};

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<UpdateRecoveryPhase>("starting");
  const [fatalError, setFatalError] = useState<string | null>(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<typeof setTimeout> | 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 (
    <StatusScreen
      title={phaseCopy.title}
      body={phaseCopy.body}
      failed={phase === "failed"}
      action={
        phase === "failed" ? (
          <button
            type="button"
            onClick={() => 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
          </button>
        ) : null
      }
    />
  );
}
byte
5.394
baris
189
Jenis
tsx
Commit terakhir
16 Oktober 2026