mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-21 18:46:08 +00:00
Stabilize shell connection badge
This commit is contained in:
parent
999449a87a
commit
f5f8eb75d8
6 changed files with 69 additions and 7 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`}
|
||||
>
|
||||
<AppLayout
|
||||
backendHealthy={runtime.backendHealthy}
|
||||
connected={runtime.connected}
|
||||
reconnecting={runtime.reconnecting}
|
||||
dataUpdated={runtime.dataUpdated}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ type UtilityTab = {
|
|||
};
|
||||
|
||||
export interface AppLayoutProps {
|
||||
backendHealthy: () => 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 (
|
||||
<div
|
||||
class={`group status text-xs rounded-full flex items-center justify-center transition-all duration-500 ease-in-out px-1.5 ${
|
||||
props.connected()
|
||||
shellAvailable()
|
||||
? 'connected bg-green-200 dark:bg-green-700 text-green-700 dark:text-green-300 min-w-6 h-6 group-hover:px-3'
|
||||
: props.reconnecting()
|
||||
? 'reconnecting bg-yellow-200 dark:bg-yellow-700 text-yellow-700 dark:text-yellow-300 py-1'
|
||||
: 'disconnected bg-surface-hover text-base-content min-w-6 h-6 group-hover:px-3'
|
||||
} ${props.class ?? ''}`}
|
||||
>
|
||||
<Show when={props.reconnecting()}>
|
||||
<Show when={!shellAvailable() && props.reconnecting()}>
|
||||
<svg class="animate-spin h-3 w-3 flex-shrink-0" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="opacity-25"
|
||||
|
|
@ -118,21 +123,23 @@ function ConnectionStatusBadge(props: {
|
|||
/>
|
||||
</svg>
|
||||
</Show>
|
||||
<Show when={props.connected()}>
|
||||
<Show when={shellAvailable()}>
|
||||
<span class="h-2.5 w-2.5 rounded-full bg-green-600 dark:bg-green-400 flex-shrink-0" />
|
||||
</Show>
|
||||
<Show when={!props.connected() && !props.reconnecting()}>
|
||||
<Show when={!shellAvailable() && !props.reconnecting()}>
|
||||
<span class="h-2.5 w-2.5 rounded-full bg-slate-600 flex-shrink-0" />
|
||||
</Show>
|
||||
<span
|
||||
class={`whitespace-nowrap overflow-hidden transition-all duration-500 ${
|
||||
props.connected() || (!props.connected() && !props.reconnecting())
|
||||
shellAvailable() || (!shellAvailable() && !props.reconnecting())
|
||||
? 'max-w-0 group-hover:max-w-[100px] group-hover:ml-2 group-hover:mr-1 opacity-0 group-hover:opacity-100'
|
||||
: 'max-w-[100px] ml-1 opacity-100'
|
||||
}`}
|
||||
>
|
||||
{props.connected()
|
||||
? 'Connected'
|
||||
{shellAvailable()
|
||||
? showSyncing()
|
||||
? 'Connected'
|
||||
: 'Connected'
|
||||
: props.reconnecting()
|
||||
? 'Reconnecting...'
|
||||
: 'Disconnected'}
|
||||
|
|
@ -593,6 +600,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
</div>
|
||||
</Show>
|
||||
<ConnectionStatusBadge
|
||||
backendHealthy={props.backendHealthy}
|
||||
connected={props.connected}
|
||||
reconnecting={props.reconnecting}
|
||||
class="flex-shrink-0"
|
||||
|
|
|
|||
|
|
@ -23,9 +23,12 @@ describe('App architecture', () => {
|
|||
it('keeps authenticated chrome in AppLayout and hosted bootstrap in useAppRuntimeState', () => {
|
||||
expect(appLayoutSource).toContain('export function AppLayout(props: AppLayoutProps)');
|
||||
expect(appLayoutSource).toContain('<OrgSwitcher');
|
||||
expect(appLayoutSource).toContain('const shellAvailable = () => 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(
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ export const useAppRuntimeState = () => {
|
|||
logoutURL?: string;
|
||||
} | null>(null);
|
||||
const [wsStore, setWsStore] = createSignal<EnhancedStore | null>(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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue