diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 291b18cc..97d14079 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -843,6 +843,8 @@ export default function EditorLayout() { onFleetActiveTabChange={setFleetActiveTab} renderEditor={renderEditor} stackUpdates={stackUpdates} + urlHydratingStack={urlHydratingStack} + isFileLoading={isFileLoading} /> ); diff --git a/frontend/src/components/EditorLayout/ViewRouter.tsx b/frontend/src/components/EditorLayout/ViewRouter.tsx index 450e773c..d402c00d 100644 --- a/frontend/src/components/EditorLayout/ViewRouter.tsx +++ b/frontend/src/components/EditorLayout/ViewRouter.tsx @@ -16,6 +16,7 @@ import type { MuteRuleDraft } from '@/lib/muteRules'; import type { ActiveView } from './hooks/useViewNavigationState'; import type { StackUpdateInfo } from '@/types/imageUpdates'; import type { SecurityTab, FleetTab } from '@/lib/events'; +import { isStackEditorDeepLink } from '@/lib/router/readUrlRouteState'; // Paid-tier views are loaded on demand. Their internal PaidGate / // CapabilityGate wrappers render @@ -101,6 +102,8 @@ export interface ViewRouterProps { // not on every parent render that lands on a different view. renderEditor: () => ReactNode; stackUpdates: Record; + urlHydratingStack: string | null; + isFileLoading: boolean; } export function ViewRouter({ @@ -131,6 +134,8 @@ export function ViewRouter({ onFleetActiveTabChange, renderEditor, stackUpdates, + urlHydratingStack, + isFileLoading, }: ViewRouterProps): ReactNode { const { can } = useAuth(); if (activeView === 'settings') { @@ -175,10 +180,14 @@ export function ViewRouter({ ); } - // Fall-through: when activeView === 'editor' but selectedFile is - // null or the stack is still loading, drop through to the default - // HomeDashboard render below. This matches the pre-extraction - // behavior of the conditional ternary chain in EditorLayout.tsx. + // Stack editor deep link: keep a loading shell until selectedFile is set. + // Without this, cold load / refresh shows HomeDashboard while URL hydration runs. + if (activeView === 'editor' && !selectedFile) { + const awaitingStack = urlHydratingStack != null || isFileLoading || isStackEditorDeepLink(); + if (awaitingStack) { + return ; + } + } if (!isLoading && selectedFile && activeView === 'editor') { return renderEditor(); } diff --git a/frontend/src/lib/router/readUrlRouteState.ts b/frontend/src/lib/router/readUrlRouteState.ts index 9cc2cdaa..715178b6 100644 --- a/frontend/src/lib/router/readUrlRouteState.ts +++ b/frontend/src/lib/router/readUrlRouteState.ts @@ -19,6 +19,13 @@ const DEFAULT: UrlRouteState = { filterNodeId: null, }; +/** True when the current URL is a stack editor deep link (cold-load bootstrap). */ +export function isStackEditorDeepLink(): boolean { + if (typeof window === 'undefined') return false; + const parsed = parsePath(window.location.pathname, window.location.search); + return parsed.view === 'editor' && parsed.stackName != null; +} + /** Read shell navigation fields from the current browser URL (cold-load bootstrap). */ export function readUrlRouteState(): UrlRouteState { if (typeof window === 'undefined') return DEFAULT;