Add project-scoped engine connectivity remediation to the dashboard. - Add engine status/start API helpers and server routes that can resume paused projects or start missing engines. - Render a project banner with dashboard-only guidance, polling, disabled starting state, localized copy, and focused tests. - Document the engine status banner behavior and add a changeset for the published CLI package. Files changed: .changeset/fn-6827-engine-disconnected-banner.md | 7 + docs/dashboard-guide.md | 8 + packages/dashboard/app/api/legacy.ts | 20 +++ .../app/components/EngineStatusBanner.css | 87 +++++++++++ .../app/components/EngineStatusBanner.tsx | 62 ++++++++ .../__tests__/EngineStatusBanner.test.tsx | 145 +++++++++++++++++ .../app/components/dashboard/DashboardBanners.tsx | 3 + .../app/hooks/__tests__/useEngineStatus.test.ts | 171 +++++++++++++++++++++ packages/dashboard/app/hooks/useEngineStatus.ts | 111 +++++++++++++ packages/dashboard/src/__tests__/server.test.ts | 129 ++++++++++++++++ packages/dashboard/src/server.ts | 89 +++++++++++ packages/i18n/locales/en/app.json | 10 ++ packages/i18n/locales/es/app.json | 10 ++ packages/i18n/locales/fr/app.json | 10 ++ packages/i18n/locales/ko/app.json | 10 ++ packages/i18n/locales/zh-CN/app.json | 10 ++ packages/i18n/locales/zh-TW/app.json | 10 ++ 17 files changed, 892 insertions(+) Fusion-Task-Id: FN-6827 Fusion-Task-Lineage: 56821ca3-04c8-4674-9400-3639f10e463d
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { fetchEngineStatus, startEngine, type EngineStatusResponse } from "../api";
|
|
|
|
const POLL_INTERVAL_MS = 10000;
|
|
|
|
const DISCONNECTED_UNREACHABLE_STATUS: EngineStatusResponse = {
|
|
connected: false,
|
|
starting: false,
|
|
canStart: false,
|
|
reason: "unreachable",
|
|
};
|
|
|
|
export interface UseEngineStatusResult {
|
|
status: EngineStatusResponse | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
canStart: boolean;
|
|
starting: boolean;
|
|
refetch: () => Promise<void>;
|
|
start: () => Promise<void>;
|
|
}
|
|
|
|
function getErrorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
/*
|
|
* FNXC:EngineStatusBanner 2026-06-22-00:00:
|
|
* The banner must never leave the board silently inert. Poll while the current project is disconnected, stop once connected to avoid steady-state traffic, and fold local Start engine clicks into `starting` so the button cannot be double-triggered before the server reports its transient starting state.
|
|
*/
|
|
export function useEngineStatus(projectId?: string): UseEngineStatusResult {
|
|
const [status, setStatus] = useState<EngineStatusResponse | null>(null);
|
|
const [loading, setLoading] = useState(Boolean(projectId));
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [startInFlight, setStartInFlight] = useState(false);
|
|
const requestIdRef = useRef(0);
|
|
|
|
const refetch = useCallback(async () => {
|
|
const requestId = requestIdRef.current + 1;
|
|
requestIdRef.current = requestId;
|
|
|
|
if (!projectId) {
|
|
setStatus(null);
|
|
setLoading(false);
|
|
setStartInFlight(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const nextStatus = await fetchEngineStatus(projectId);
|
|
if (requestIdRef.current !== requestId) return;
|
|
setStatus(nextStatus);
|
|
if (nextStatus.connected) {
|
|
setStartInFlight(false);
|
|
}
|
|
} catch {
|
|
if (requestIdRef.current !== requestId) return;
|
|
setStatus({ ...DISCONNECTED_UNREACHABLE_STATUS, projectId });
|
|
} finally {
|
|
if (requestIdRef.current === requestId) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}, [projectId]);
|
|
|
|
const start = useCallback(async () => {
|
|
if (!projectId) return;
|
|
|
|
setStartInFlight(true);
|
|
setError(null);
|
|
try {
|
|
const startedStatus = await startEngine(projectId);
|
|
setStatus(startedStatus);
|
|
await refetch();
|
|
} catch (err) {
|
|
setError(getErrorMessage(err));
|
|
} finally {
|
|
setStartInFlight(false);
|
|
}
|
|
}, [projectId, refetch]);
|
|
|
|
useEffect(() => {
|
|
setStatus(null);
|
|
setError(null);
|
|
setStartInFlight(false);
|
|
void refetch();
|
|
}, [refetch]);
|
|
|
|
useEffect(() => {
|
|
if (!projectId || status?.connected) return;
|
|
|
|
const interval = window.setInterval(() => {
|
|
void refetch();
|
|
}, POLL_INTERVAL_MS);
|
|
|
|
return () => {
|
|
window.clearInterval(interval);
|
|
};
|
|
}, [projectId, refetch, status?.connected]);
|
|
|
|
return useMemo(() => ({
|
|
status,
|
|
loading,
|
|
error,
|
|
canStart: Boolean(status?.canStart),
|
|
starting: Boolean(status?.starting || startInFlight),
|
|
refetch,
|
|
start,
|
|
}), [error, loading, refetch, start, startInFlight, status]);
|
|
}
|