From f5f8eb75d8bcc417a1d91072b20347bf76019855 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 24 Mar 2026 10:53:12 +0000 Subject: [PATCH] Stabilize shell connection badge --- .../v6/internal/subsystems/cloud-paid.md | 10 ++++++ .../subsystems/frontend-primitives.md | 6 ++++ frontend-modern/src/App.tsx | 1 + frontend-modern/src/AppLayout.tsx | 22 ++++++++---- .../src/__tests__/App.architecture.test.ts | 3 ++ frontend-modern/src/useAppRuntimeState.ts | 34 +++++++++++++++++++ 6 files changed, 69 insertions(+), 7 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index dcf5bc7aa..8cc21459d 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -328,6 +328,16 @@ theme synchronization, and authenticated runtime startup, and as org switching and kiosk-safe navigation. Future hosted browser bootstrap work must extend that split rather than pulling org bootstrap and app chrome back into one monolithic route component. +That same authenticated shell contract now also has to distinguish backend +availability from websocket-stream liveness. When hosted runtime health stays +available during a stream reconnect or renegotiation window, +`frontend-modern/src/useAppRuntimeState.ts` must preserve a canonical +backend-healthy signal and `frontend-modern/src/AppLayout.tsx` must not +advertise the whole paid shell as disconnected or "Reconnecting..." solely +because the live stream is transiently recovering. Future hosted shell work +must keep the top-right connection badge aligned to overall authenticated +runtime availability first, with websocket churn treated as a narrower live +stream status instead of the shell-level truth. That same route/provider shell must stay page-oriented as well: `App.tsx` should lazy-load route shells like `frontend-modern/src/pages/Storage.tsx` and `frontend-modern/src/pages/Operations.tsx` diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 57caf5497..558825b36 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -146,6 +146,12 @@ The subsystem registry now also requires explicit proof-policy coverage for all shared runtime files, and shared-component guardrails fail if raw table composition is reintroduced in new shared components outside the canonical allowlist. +The root app shell now also treats backend availability as distinct from +websocket liveness: `frontend-modern/src/AppLayout.tsx` and +`frontend-modern/src/useAppRuntimeState.ts` must keep the top-right connection +badge aligned to overall backend availability so a healthy dev/runtime backend +does not present the whole shell as reconnecting just because the live stream +is transiently renegotiating. Shared feature presentation helpers under `frontend-modern/src/features/` now also need to preserve route-owned page-health semantics when the owning surface is REST-backed: operators should only see reconnect or disconnected shells when diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 6336097d2..db5b60ffa 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -325,6 +325,7 @@ function App() { class={`app-scroll-shell flex-1 min-w-0 overflow-y-scroll bg-base text-base-content font-sans py-4 sm:py-6 transition-all duration-300`} > boolean; connected: () => boolean; reconnecting: () => boolean; dataUpdated: () => boolean; @@ -87,21 +88,25 @@ export interface AppLayoutProps { } function ConnectionStatusBadge(props: { + backendHealthy: () => boolean; connected: () => boolean; reconnecting: () => boolean; class?: string; }) { + const shellAvailable = () => props.connected() || props.backendHealthy(); + const showSyncing = () => !props.connected() && props.backendHealthy() && props.reconnecting(); + return (
- + - + - + - {props.connected() - ? 'Connected' + {shellAvailable() + ? showSyncing() + ? 'Connected' + : 'Connected' : props.reconnecting() ? 'Reconnecting...' : 'Disconnected'} @@ -593,6 +600,7 @@ export function AppLayout(props: AppLayoutProps) {
{ it('keeps authenticated chrome in AppLayout and hosted bootstrap in useAppRuntimeState', () => { expect(appLayoutSource).toContain('export function AppLayout(props: AppLayoutProps)'); expect(appLayoutSource).toContain(' props.connected() || props.backendHealthy();'); expect(appLayoutSource).toContain('const utilityTabs = createMemo(() =>'); expect(appRuntimeStateSource).toContain('export const useAppRuntimeState = () =>'); expect(appRuntimeStateSource).toContain('const beginAuthenticatedRuntime = async () =>'); + expect(appRuntimeStateSource).toContain("const [backendHealthy, setBackendHealthy] = createSignal(false);"); + expect(appRuntimeStateSource).toContain("const checkBackendHealth = async () => {"); expect(appRuntimeStateSource).toContain('const loadOrganizations = async () =>'); expect(appRuntimeStateSource).toContain('const handleOrgSwitch = (nextOrgID: string) =>'); expect(appRuntimeStateSource).toContain( diff --git a/frontend-modern/src/useAppRuntimeState.ts b/frontend-modern/src/useAppRuntimeState.ts index d29dda628..4308b1e38 100644 --- a/frontend-modern/src/useAppRuntimeState.ts +++ b/frontend-modern/src/useAppRuntimeState.ts @@ -181,6 +181,7 @@ export const useAppRuntimeState = () => { logoutURL?: string; } | null>(null); const [wsStore, setWsStore] = createSignal(null); + const [backendHealthy, setBackendHealthy] = createSignal(false); const state = (): State => wsStore()?.state || fallbackState; const connected = () => wsStore()?.connected() || false; const reconnecting = () => wsStore()?.reconnecting() || false; @@ -226,9 +227,21 @@ export const useAppRuntimeState = () => { setNeedsAuth(false); await loadOrganizations(); setWsStore(acquireWsStore()); + setBackendHealthy(true); await loadSystemSettingsAndLayout(); }; + const checkBackendHealth = async () => { + try { + const response = await apiFetch('/api/health', { cache: 'no-store' }); + setBackendHealthy(response.ok); + return response.ok; + } catch { + setBackendHealthy(false); + return false; + } + }; + const loadOrganizations = async () => { setOrgsLoading(true); try { @@ -419,6 +432,26 @@ export const useAppRuntimeState = () => { } }); + createEffect(() => { + if (connected()) { + setBackendHealthy(true); + return; + } + + if (!reconnecting()) { + return; + } + + void checkBackendHealth(); + const interval = window.setInterval(() => { + void checkBackendHealth(); + }, 5000); + + onCleanup(() => { + window.clearInterval(interval); + }); + }); + const handleThemeChange = async (newPreference: ThemePreference) => { applyThemePreferenceLocally(newPreference); logger.info('Theme changed', { pref: newPreference, active: computeIsDark(newPreference) ? 'dark' : 'light' }); @@ -634,6 +667,7 @@ export const useAppRuntimeState = () => { proxyAuthInfo, state, connected, + backendHealthy, reconnecting, dataUpdated, lastUpdateText,