From 914c38ff5d3f9cbba8319e05fdf05e186a0ad048 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 2 Jul 2026 23:42:14 -0700 Subject: [PATCH] fix(dashboard): poll health so a stale "engine not running" banner clears MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useDashboardHealth fetched /api/health once on mount and never again. Right after a project is created the engine is still starting, so that single fetch reports engine.available=false and the "AI engine is not running" banner shows — and because health was never refreshed, the banner stayed up even after the engine came online (the user reported the banner while the engine was in fact running). Poll every 15s and preserve the last value on transient errors so the banner clears on its own once the engine reports available. Co-Authored-By: Claude Opus 4.8 --- .../dashboard/app/hooks/useDashboardHealth.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/dashboard/app/hooks/useDashboardHealth.ts b/packages/dashboard/app/hooks/useDashboardHealth.ts index 5cbf75b73d..79d5a582c2 100644 --- a/packages/dashboard/app/hooks/useDashboardHealth.ts +++ b/packages/dashboard/app/hooks/useDashboardHealth.ts @@ -35,21 +35,36 @@ export function useDashboardHealth(): UseDashboardHealthResult { useEffect(() => { let cancelled = false; + let settledOnce = false; - fetchDashboardHealth() - .then((next) => { - if (!cancelled) { + /* + * FNXC:DashboardHealth 2026-07-03-08:40: + * Poll health periodically instead of fetching once on mount. Engine availability is transient: + * right after a project is created the engine is still starting, so the first fetch reports + * engine.available=false and the "AI engine is not running" banner shows. Without polling, health + * was never refreshed, so the banner stayed up permanently even after the engine came online. + * Re-fetching every 15s lets the banner clear on its own. Preserve the previous value on transient + * poll errors (only clear to null if we never had a value) so banners don't flicker. + */ + const load = () => { + fetchDashboardHealth() + .then((next) => { + if (cancelled) return; + settledOnce = true; setHealth(next); - } - }) - .catch(() => { - if (!cancelled) { - setHealth(null); - } - }); + }) + .catch(() => { + if (cancelled) return; + setHealth((prev) => (settledOnce ? prev : null)); + }); + }; + + load(); + const interval = setInterval(load, 15_000); return () => { cancelled = true; + clearInterval(interval); }; }, []);