mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
import debugFactory from 'debug';
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import {
|
|
fetchHarnessInitStatus,
|
|
type HarnessInitSnapshot,
|
|
runHarnessInit,
|
|
} from '../../services/harnessInitService';
|
|
import InitProgressScreen from './InitProgressScreen';
|
|
|
|
const log = debugFactory('harness-init');
|
|
|
|
const POLL_MS = 2000;
|
|
|
|
// Persist the "Run in background" dismissal for the *current* provisioning run
|
|
// so a remount or reload does not reopen the overlay (GH-5047). A run is keyed
|
|
// by its `startedAt` timestamp — a genuinely new provisioning run gets a fresh
|
|
// timestamp and is allowed to surface again. `sessionStorage` survives a
|
|
// renderer reload within the same window; a module-level mirror covers plain
|
|
// React remounts even if storage is unavailable.
|
|
const DISMISS_KEY = 'harness-init-dismissed-run';
|
|
// Runs before `startedAt` is stamped (or when it is absent) still need a stable
|
|
// key so an early dismissal sticks.
|
|
const UNKEYED_RUN = 'pending';
|
|
|
|
let dismissedRunMirror: string | null = null;
|
|
|
|
function runKey(snapshot: HarnessInitSnapshot | null): string {
|
|
return snapshot?.startedAt ?? UNKEYED_RUN;
|
|
}
|
|
|
|
function readDismissedRun(): string | null {
|
|
if (dismissedRunMirror !== null) {
|
|
return dismissedRunMirror;
|
|
}
|
|
try {
|
|
return window.sessionStorage.getItem(DISMISS_KEY);
|
|
} catch {
|
|
log('sessionStorage read failed; treating run as not dismissed');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeDismissedRun(key: string): void {
|
|
dismissedRunMirror = key;
|
|
try {
|
|
window.sessionStorage.setItem(DISMISS_KEY, key);
|
|
log('dismissed run persisted to sessionStorage: %s', key);
|
|
} catch {
|
|
// Non-fatal: the module-level mirror still guards remounts this session.
|
|
log('sessionStorage unavailable; dismissed run %s held in module mirror only', key);
|
|
}
|
|
}
|
|
|
|
function isRunDismissed(snapshot: HarnessInitSnapshot | null): boolean {
|
|
return readDismissedRun() === runKey(snapshot);
|
|
}
|
|
|
|
/**
|
|
* Blocking first-run initialization gate.
|
|
*
|
|
* Polls `openhuman.harness_init_status` and, while the run is in progress,
|
|
* covers the app with a full-screen overlay showing per-step progress. The
|
|
* overlay offers a "Run in background" action so the user can dismiss it and
|
|
* keep working while setup continues — the core runs init as a background task
|
|
* regardless of whether the overlay is shown. On a warm host every step is
|
|
* already provisioned, so the snapshot reports `done` on the first poll and
|
|
* this renders nothing. On a terminal `failed` it offers Retry / Continue —
|
|
* failures are non-fatal (the core degrades to a fallback).
|
|
*
|
|
* Polling-based (not socket) to sidestep the cold-start race where the socket
|
|
* is not yet connected when init begins.
|
|
*/
|
|
export default function HarnessInitOverlay() {
|
|
const [snapshot, setSnapshot] = useState<HarnessInitSnapshot | null>(null);
|
|
const [dismissed, setDismissed] = useState(false);
|
|
const [retrying, setRetrying] = useState(false);
|
|
const cancelledRef = useRef(false);
|
|
// Mirrors `dismissed` so the poll loop can stop without re-running the effect.
|
|
const dismissedRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
cancelledRef.current = false;
|
|
let timeoutId: number | null = null;
|
|
|
|
const poll = async () => {
|
|
try {
|
|
const next = await fetchHarnessInitStatus();
|
|
if (cancelledRef.current || dismissedRef.current) {
|
|
return;
|
|
}
|
|
if (next) {
|
|
setSnapshot(next);
|
|
// If this run was already dismissed to the background (possibly in a
|
|
// prior mount / before a reload), stay hidden and stop polling —
|
|
// don't let a remount reopen the overlay (GH-5047).
|
|
if (isRunDismissed(next)) {
|
|
log(
|
|
'warm poll: run %s already dismissed — staying hidden, stopping poll',
|
|
runKey(next)
|
|
);
|
|
dismissedRef.current = true;
|
|
setDismissed(true);
|
|
return;
|
|
}
|
|
// Stop polling once the run is terminal; a `failed` snapshot stays
|
|
// on screen (with Retry) but does not need further polling.
|
|
if (next.overall === 'done' || next.overall === 'failed') {
|
|
return;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Status can fail while the core is still coming up — keep polling.
|
|
log('status poll failed: %O', err);
|
|
}
|
|
if (!cancelledRef.current && !dismissedRef.current) {
|
|
timeoutId = window.setTimeout(() => void poll(), POLL_MS);
|
|
}
|
|
};
|
|
|
|
void poll();
|
|
|
|
return () => {
|
|
cancelledRef.current = true;
|
|
if (timeoutId !== null) {
|
|
window.clearTimeout(timeoutId);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handleRetry = useCallback(async () => {
|
|
setRetrying(true);
|
|
try {
|
|
const next = await runHarnessInit(false);
|
|
if (next) {
|
|
setSnapshot(next);
|
|
}
|
|
} catch (err) {
|
|
log('retry failed: %O', err);
|
|
} finally {
|
|
setRetrying(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleContinue = useCallback(() => {
|
|
// Hide the overlay and stop polling; the core keeps running init as a
|
|
// background task regardless. Persist the dismissal for this run so a
|
|
// remount/reload does not reopen it (GH-5047).
|
|
log('user dismissed overlay to background for run %s', runKey(snapshot));
|
|
writeDismissedRun(runKey(snapshot));
|
|
dismissedRef.current = true;
|
|
setDismissed(true);
|
|
}, [snapshot]);
|
|
|
|
if (dismissed || !snapshot) {
|
|
return null;
|
|
}
|
|
|
|
// A run dismissed to the background stays hidden across remounts.
|
|
if (isRunDismissed(snapshot)) {
|
|
return null;
|
|
}
|
|
|
|
// Block only while a run is actively in progress, or hold a failed run on
|
|
// screen until the user explicitly continues. `idle` (no run started yet)
|
|
// and `done` never block.
|
|
const shouldShow = snapshot.overall === 'running' || snapshot.overall === 'failed';
|
|
if (!shouldShow) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<InitProgressScreen
|
|
snapshot={snapshot}
|
|
onRetry={handleRetry}
|
|
onContinue={handleContinue}
|
|
retrying={retrying}
|
|
/>
|
|
);
|
|
}
|