From a8d1a9d4619a00ed1c731f90f8dcefbc7212c8f4 Mon Sep 17 00:00:00 2001 From: Anso Date: Sat, 2 May 2026 02:13:02 -0400 Subject: [PATCH] feat(frontend): code-split non-settings paid views and security overlay (#872) Extends the settings code-splitting from PR #870 to the full-screen views in EditorLayout. Six paid views (HostConsole, FleetView, AuditLogView, ScheduledOperationsView, AutoUpdateReadinessView, GlobalObservabilityView) and the SecurityHistoryView overlay used to ship statically into the main bundle, so every Community user downloaded ~300 kB raw / ~80 kB gzip of paid feature code on first page load even if they never clicked those tabs. Convert each to a lazy() declaration and wrap the call site in Suspense with a small ViewSkeleton fallback. SecurityHistoryView is an always-mounted overlay, so it is also conditionally mounted on its open state to keep the lazy import from firing on EditorLayout's first render. GlobalObservabilityView is a free-tier feature with no internal gate; it is split here purely for the bundle-size win, not for IP protection. The other paid views still have their existing PaidGate / AdmiralGate / CapabilityGate wrappers, which render a blurred preview with upsell card rather than short-circuiting. When a tier-locked or capability-missing operator opens one of those tabs, the chunk fetches to render the blurred preview. The gate-short-circuit refactor is a separate follow-up. Build evidence: 7 new chunks total ~308 kB raw / ~83 kB gzip; main bundle shrunk from 1,468 kB / 407 kB gzip to 1,165 kB / 332 kB gzip. Combined with PR #870, Community users save ~390 kB raw / ~107 kB gzip on initial load. --- frontend/src/components/EditorLayout.tsx | 122 +++++++++++++++++------ 1 file changed, 89 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 1f77e226..89fedf83 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useMemo, useCallback, Suspense } from 'react'; +import { useState, useEffect, useRef, useMemo, useCallback, lazy, Suspense } from 'react'; type Theme = 'light' | 'dark' | 'auto'; import { Editor } from '@/lib/monacoLoader'; @@ -8,7 +8,7 @@ import ErrorBoundary from './ErrorBoundary'; import HomeDashboard from './HomeDashboard'; import type { NotificationItem } from './dashboard/types'; import BashExecModal from './BashExecModal'; -import HostConsole from './HostConsole'; +import { Skeleton } from '@/components/ui/skeleton'; import { AdmiralGate } from './AdmiralGate'; import { CapabilityGate } from './CapabilityGate'; import ResourcesView from './ResourcesView'; @@ -46,12 +46,48 @@ import { LogViewer } from './LogViewer'; import StructuredLogViewer from './StructuredLogViewer'; import StackAnatomyPanel from './StackAnatomyPanel'; import { Sparkline } from './ui/sparkline'; -import { GlobalObservabilityView } from './GlobalObservabilityView'; -import { FleetView } from './FleetView'; -import { AuditLogView } from './AuditLogView'; -import ScheduledOperationsView, { type ScheduleTaskPrefill } from './ScheduledOperationsView'; -import AutoUpdateReadinessView from './AutoUpdateReadinessView'; -import { SecurityHistoryView } from './SecurityHistoryView'; +import type { ScheduleTaskPrefill } from './ScheduledOperationsView'; + +// Paid-tier views and the security-history overlay are loaded on demand. +// Their internal PaidGate / AdmiralGate / CapabilityGate wrappers render +// the upsell or capability-missing card with blurred children rather than +// short-circuiting, so a tier-locked or capability-missing operator +// opening one of these tabs still triggers the chunk fetch to render the +// blurred preview. The gate-short-circuit fix lives in a follow-up +// branch. What this lazy split closes is the much larger initial-bundle +// leak: every Community user used to download the full FleetView, +// AuditLogView, etc. on first page load even if they never clicked those +// tabs. After this change, the chunks fetch only on tab open. +// +// GlobalObservabilityView is a free-tier feature with no internal gate; +// it is split here purely for the bundle-size win, not for IP protection. +const HostConsole = lazy(() => import('./HostConsole')); +const GlobalObservabilityView = lazy(() => + import('./GlobalObservabilityView').then(m => ({ default: m.GlobalObservabilityView })), +); +const FleetView = lazy(() => + import('./FleetView').then(m => ({ default: m.FleetView })), +); +const AuditLogView = lazy(() => + import('./AuditLogView').then(m => ({ default: m.AuditLogView })), +); +const SecurityHistoryView = lazy(() => + import('./SecurityHistoryView').then(m => ({ default: m.SecurityHistoryView })), +); +const ScheduledOperationsView = lazy(() => import('./ScheduledOperationsView')); +const AutoUpdateReadinessView = lazy(() => import('./AutoUpdateReadinessView')); + +// Sized for the main workspace area (flex-1 with p-6 padding). Visible +// only during the brief window between an unlocked view's chunk request +// and its first render. +function ViewSkeleton() { + return ( +
+ + +
+ ); +} import { SENCHO_NAVIGATE_EVENT } from './NodeManager'; import type { SenchoNavigateDetail } from './NodeManager'; import { NodeSwitcher } from './NodeSwitcher'; @@ -2487,7 +2523,9 @@ export default function EditorLayout() { ) : activeView === 'host-console' ? ( - setActiveView(selectedFile ? 'editor' : 'dashboard')} /> + }> + setActiveView(selectedFile ? 'editor' : 'dashboard')} /> + ) : !isLoading && selectedFile && activeView === 'editor' ? ( @@ -3026,37 +3064,47 @@ export default function EditorLayout() { ) : activeView === 'global-observability' ? ( - + }> + + ) : activeView === 'fleet' ? ( - { - const node = nodes.find(n => n.id === nodeId); - if (node) { - if (activeNode?.id === nodeId) { - loadFile(stackName); - } else { - pendingStackLoadRef.current = stackName; - setActiveNode(node); + }> + { + const node = nodes.find(n => n.id === nodeId); + if (node) { + if (activeNode?.id === nodeId) { + loadFile(stackName); + } else { + pendingStackLoadRef.current = stackName; + setActiveNode(node); + } } - } - }} /> + }} /> + ) : activeView === 'audit-log' ? ( - + }> + + ) : activeView === 'auto-updates' ? ( - + }> + + ) : activeView === 'scheduled-ops' ? ( - setFilterNodeId(null)} - prefill={schedulePrefill} - onPrefillConsumed={handlePrefillConsumed} - /> + }> + setFilterNodeId(null)} + prefill={schedulePrefill} + onPrefillConsumed={handlePrefillConsumed} + /> + ) : ( - {/* Scan history overlay */} - setSecurityHistoryOpen(false)} - /> + {/* Scan history overlay. Conditionally mounted so the lazy chunk + only fetches when the user opens the overlay; an always-mounted + lazy component would fetch on EditorLayout's first render and + defeat the split. The overlay has no internal state that needs + to persist across opens. */} + {securityHistoryOpen ? ( + + setSecurityHistoryOpen(false)} + /> + + ) : null} );