@skyhook-io/radar-app 1.14.5 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +2 -9
  2. package/package.json +6 -6
  3. package/src/App.tsx +142 -334
  4. package/src/RadarApp.tsx +3 -8
  5. package/src/api/client.admission.test.ts +34 -0
  6. package/src/api/client.jobset.test.ts +18 -0
  7. package/src/api/client.resource-identity.test.ts +44 -0
  8. package/src/api/client.rightsizing.test.ts +7 -3
  9. package/src/api/client.ts +267 -78
  10. package/src/api/preferences.test.ts +54 -0
  11. package/src/api/preferences.ts +38 -0
  12. package/src/api/usage-data.fixtures.ts +33 -0
  13. package/src/api/usage-data.ts +165 -0
  14. package/src/components/CloudFunnelButton.test.tsx +155 -0
  15. package/src/components/CloudFunnelButton.tsx +113 -63
  16. package/src/components/ContextSwitcher.tsx +13 -132
  17. package/src/components/SelfManagedStart.tsx +96 -0
  18. package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
  19. package/src/components/applications/ApplicationsView.tsx +5 -3
  20. package/src/components/audit/AuditSettingsDialog.tsx +15 -11
  21. package/src/components/cloudConnectHandoff.test.ts +1 -1
  22. package/src/components/cloudConnectHandoff.ts +1 -1
  23. package/src/components/diagnose/ActivityTurn.tsx +3 -2
  24. package/src/components/diagnose/AgentControls.tsx +26 -3
  25. package/src/components/diagnose/ApplyDialog.tsx +4 -2
  26. package/src/components/diagnose/DiagnoseContext.tsx +1 -0
  27. package/src/components/diagnose/agentCatalog.ts +6 -0
  28. package/src/components/diagnose/launch.test.ts +23 -0
  29. package/src/components/diagnose/launch.ts +4 -0
  30. package/src/components/diagnose/parts.test.tsx +18 -0
  31. package/src/components/dialog-names.test.ts +69 -0
  32. package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
  33. package/src/components/dock/NodeTerminalTab.tsx +5 -3
  34. package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
  35. package/src/components/dock/WorkloadLogsTab.tsx +2 -1
  36. package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
  37. package/src/components/execution/BatchExecutionView.test.ts +52 -1
  38. package/src/components/execution/BatchExecutionView.tsx +324 -65
  39. package/src/components/execution/JobSetAdmission.test.tsx +165 -0
  40. package/src/components/execution/JobSetAdmission.tsx +26 -0
  41. package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
  42. package/src/components/execution/JobSetMemberComparison.tsx +88 -0
  43. package/src/components/execution/batch-run-actions.test.ts +1 -0
  44. package/src/components/execution/batch-timeline.test.ts +3 -0
  45. package/src/components/execution/execution-definition.test.ts +15 -1
  46. package/src/components/execution/execution-definition.ts +4 -0
  47. package/src/components/execution/member-collection.render.test.tsx +142 -0
  48. package/src/components/gitops/GitOpsView.tsx +33 -2
  49. package/src/components/gitops/destination-toast.test.ts +33 -0
  50. package/src/components/gitops/destination-toast.ts +46 -0
  51. package/src/components/gitops/useDestinationCluster.ts +30 -0
  52. package/src/components/helm/TrackChartSourceDialog.tsx +25 -14
  53. package/src/components/home/HomeView.tsx +6 -2
  54. package/src/components/home/MCPSetupDialog.tsx +10 -0
  55. package/src/components/home/RadarVersionLine.test.tsx +14 -0
  56. package/src/components/home/RadarVersionLine.tsx +28 -3
  57. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
  58. package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
  59. package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
  60. package/src/components/nav/PrimaryNavRail.tsx +27 -8
  61. package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
  62. package/src/components/resource/PVCUsageBar.tsx +62 -16
  63. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
  64. package/src/components/resources/ResourcesView.tsx +3 -3
  65. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
  66. package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
  67. package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
  68. package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
  69. package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
  70. package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
  71. package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
  72. package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
  73. package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
  74. package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
  75. package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
  76. package/src/components/rightsizing/RightsizingScanView.tsx +34 -22
  77. package/src/components/rightsizing/copy.test.ts +7 -0
  78. package/src/components/rightsizing/model.test.ts +2 -1
  79. package/src/components/rightsizing/notices.test.tsx +4 -3
  80. package/src/components/settings/OperatorManagedNotice.tsx +17 -0
  81. package/src/components/settings/PrivacySection.test.tsx +84 -0
  82. package/src/components/settings/PrivacySection.tsx +140 -0
  83. package/src/components/settings/SettingsDialog.tsx +168 -92
  84. package/src/components/settings/controls.tsx +55 -0
  85. package/src/components/settings/settings-state.test.ts +18 -0
  86. package/src/components/settings/settings-state.ts +19 -0
  87. package/src/components/timeline/TimelineView.tsx +9 -3
  88. package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
  89. package/src/components/ui/ErrorBoundary.tsx +5 -0
  90. package/src/components/ui/Omnibar.tsx +16 -0
  91. package/src/components/ui/UpdateNotification.tsx +13 -9
  92. package/src/components/ui/command-items.ts +14 -3
  93. package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
  94. package/src/components/usage-data/UsageDataAsk.tsx +106 -0
  95. package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
  96. package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
  97. package/src/components/useContextSwitchFlow.tsx +147 -0
  98. package/src/components/whats-new/WhatsNew.test.ts +110 -0
  99. package/src/components/whats-new/WhatsNew.tsx +423 -0
  100. package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
  101. package/src/components/whats-new/check-whats-new.test.ts +65 -0
  102. package/src/components/whats-new/releaseNotes.ts +121 -0
  103. package/src/components/workload/WorkloadView.test.ts +17 -1
  104. package/src/components/workload/WorkloadView.tsx +47 -19
  105. package/src/context/ContextSwitchContext.tsx +18 -2
  106. package/src/context/NavCustomization.tsx +4 -58
  107. package/src/context/ThemeContext.tsx +3 -11
  108. package/src/hooks/useClusterLoadState.ts +2 -2
  109. package/src/hooks/useFavorites.ts +3 -5
  110. package/src/k8s-ui-exports.test.ts +73 -0
  111. package/src/utils/auditBadges.ts +1 -1
  112. package/src/utils/navigation.test.ts +34 -1
  113. package/src/utils/navigation.ts +28 -5
  114. package/src/utils/topology-selection.test.ts +56 -0
  115. package/src/utils/topology-selection.ts +9 -9
  116. package/src/utils/version.ts +11 -0
  117. package/src/components/ui/CommandPalette.tsx +0 -261
package/src/App.tsx CHANGED
@@ -9,8 +9,9 @@ import { DebugOverlay } from './components/DebugOverlay'
9
9
  import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
10
10
  import { investigationWorkspaceSearch, isInvestigationWorkspacePath, useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
11
11
  import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
12
- import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader, assetUrl } from '@skyhook-io/k8s-ui'
12
+ import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
13
13
  import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
14
+ import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
14
15
  import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
15
16
  import { TimelineView } from './components/timeline/TimelineView'
16
17
  import { ResourcesView } from './components/resources/ResourcesView'
@@ -48,8 +49,10 @@ import { CapabilitiesProvider, useCapabilitiesContext } from './contexts/Capabil
48
49
  import { UserMenu } from './components/UserMenu'
49
50
  import { ErrorBoundary } from './components/ui/ErrorBoundary'
50
51
  import { UpdateNotification } from './components/ui/UpdateNotification'
52
+ import { openWhatsNew, useWhatsNewStatus, WhatsNew } from './components/whats-new/WhatsNew'
53
+ import { useUsageData, useUsageRecording } from './api/usage-data'
54
+ import { UsageDataPrompt } from './components/usage-data/UsageDataPrompt'
51
55
  import { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay'
52
- import { CommandPalette } from './components/ui/CommandPalette'
53
56
  import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
54
57
  import { useEventSource } from './hooks/useEventSource'
55
58
  import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
@@ -61,20 +64,18 @@ import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
61
64
  import { useDocumentTitle } from './hooks/useDocumentTitle'
62
65
  import type { ClusterLoadState } from './types/clusterLoadState'
63
66
  import { useClusterLoadState } from './hooks/useClusterLoadState'
64
- import { Network, List, Clock, Package, Sun, Moon, Activity, Home, Star, Search, Bug, SquareTerminal, ShieldCheck, GitBranch, Gauge, HelpCircle, Loader2, RefreshCw } from 'lucide-react'
67
+ import { Network, Sun, Moon, Star, Bug, SquareTerminal, HelpCircle, Loader2, RefreshCw } from 'lucide-react'
65
68
  import { useTheme } from './context/ThemeContext'
66
69
  import { Tooltip } from './components/ui/Tooltip'
67
70
  import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
68
71
  import { SettingsDialog, type SettingsSectionId } from './components/settings/SettingsDialog'
69
72
  import type { APIResource, TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
70
- import { kindToPluralWithGroup, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource } from './utils/navigation'
73
+ import { kindToPluralWithGroup, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource, withCrossViewParams } from './utils/navigation'
71
74
  import { findSelectedTopologyNode } from './utils/topology-selection'
72
75
  import { type OmnibarHandle } from './components/ui/Omnibar'
73
76
  import { RadarOmnibar } from './components/ui/RadarOmnibar'
74
77
  import type { ContextSwitcherHandle } from './components/ContextSwitcher'
75
78
 
76
- const radarLogoUrl = assetUrl('/images/radar/radar-icon.svg')
77
-
78
79
  // All possible node kinds (core + GitOps)
79
80
  const ALL_NODE_KINDS: NodeKind[] = [
80
81
  'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
@@ -146,6 +147,28 @@ function getViewFromPath(pathname: string): ExtendedMainView {
146
147
  return 'home'
147
148
  }
148
149
 
150
+ // The usage-data name for the current screen: a resource list names its
151
+ // kind's plural ("resources:deployments"); the server keeps built-in kinds
152
+ // only, so a custom resource's name never leaves.
153
+ function usageView(pathname: string, view: ExtendedMainView, upgrade: boolean): string {
154
+ if (upgrade) return 'upgrade'
155
+ if (view === 'resources') {
156
+ const plural = pathname.match(/^\/resources\/([^/]+)/)?.[1]
157
+ if (plural) return `resources:${plural.toLowerCase()}`
158
+ }
159
+ return view
160
+ }
161
+
162
+ // The screen a crash is counted under. Fixed names, because release builds
163
+ // shorten component names and the component stack can't say which screen.
164
+ const CRASH_LABELS: Record<ExtendedMainView, string> = {
165
+ home: 'Home', topology: 'Topology', resources: 'Resources', timeline: 'Timeline',
166
+ issues: 'Issues', helm: 'Helm', helmCompare: 'HelmCompare', traffic: 'Traffic',
167
+ cost: 'Cost', capacity: 'Capacity', checks: 'Checks', gitops: 'GitOps',
168
+ applications: 'Applications', workload: 'Workload', compare: 'Compare',
169
+ investigations: 'Investigations',
170
+ }
171
+
149
172
  // The namespace scope filter is meaningful only on namespaced surfaces. On
150
173
  // cluster-scoped views it does nothing, so we disable it with an explanation
151
174
  // rather than leaving a dead control that silently ignores the pick:
@@ -343,30 +366,23 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
343
366
  [navCustomization],
344
367
  )
345
368
  // Resolve every host-takeover URL ONCE (memoized on navCustomization) so the
346
- // setMainView intercept, redirect effect, nav-pill filtering, inline-view
347
- // gating, and the cert click handler all consume the SAME value — host
369
+ // setMainView intercept, redirect effect, inline-view gating, and the cert
370
+ // click handler all consume the SAME value — host
348
371
  // callbacks aren't guaranteed idempotent (scope / flags / signed URLs can
349
372
  // shift between calls). undefined = not taken over → Radar renders the view
350
- // itself. `clusterChecksHref` is the deprecated pre-1.7 hook, folded into the
351
- // 'checks' target for back-compat.
373
+ // itself.
352
374
  const takeover: Record<FleetTakeoverTarget, string | undefined> = useMemo(
353
375
  () => ({
354
376
  issues: navCustomization.fleetTakeoverHref?.('issues'),
355
377
  gitops: navCustomization.fleetTakeoverHref?.('gitops'),
356
- checks: navCustomization.fleetTakeoverHref?.('checks') ?? navCustomization.clusterChecksHref?.(),
378
+ checks: navCustomization.fleetTakeoverHref?.('checks'),
357
379
  certs: navCustomization.fleetTakeoverHref?.('certs'),
358
380
  }),
359
381
  [navCustomization],
360
382
  )
361
383
  const { pinned: navRailPinned, togglePinned: toggleNavRailPinned } = useNavRailPinned()
362
- // Standalone Radar gets the left nav rail; embedded hosts (Radar Hub) own
363
- // the left chrome via their own fleet rail and keep Radar's top-bar pills.
364
- const showNavRail = !navCustomization.embedded
365
- // Chromeless embed: the host (Radar Hub) owns ALL chrome and drives view
366
- // navigation + scope from its own UI, so Radar renders just the active view's
367
- // content — no top bar, no view-switcher. Used for per-cluster views surfaced
368
- // as native cloud destinations behind a cluster picker.
369
- const chromeless = navCustomization.embedded === true && navCustomization.chrome === 'none'
384
+ const embedded = navCustomization.embedded === true
385
+ const showNavRail = !embedded
370
386
  // Force the slim rail on narrow windows: a pinned 176px rail needs viewport
371
387
  // ≥976 to keep content above its ~800px floor (collapsed needs only ≥856).
372
388
  // Below 976 we render collapsed regardless of the pin preference — a
@@ -426,6 +442,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
426
442
  const mainView = getViewFromPath(location.pathname)
427
443
  const upgradeReadinessRoute = location.pathname.startsWith('/checks/upgrade')
428
444
 
445
+ // Opt-in usage data. Embedded hosts own their own consent, so Radar never asks there.
446
+ const usageData = useUsageData(!navCustomization.embedded)
447
+ // A status cached by another screen must not start recording inside a host.
448
+ useUsageRecording(usageView(location.pathname, mainView, upgradeReadinessRoute), navCustomization.embedded ? undefined : usageData.data)
449
+
429
450
  // Initialize the kind→plural discovery map app-wide (not just on ResourcesView
430
451
  // mount) so the omnibar can open a CRD hit with an irregular plural from any
431
452
  // view — kindToPlural would otherwise English-guess the route before a
@@ -501,6 +522,12 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
501
522
  navigate({ pathname: path, search: newParams.toString() })
502
523
  }, [location.search, navigate, takeover, goHost])
503
524
 
525
+ const whatsNewStatus = useWhatsNewStatus()
526
+
527
+ const navigateToPath = useCallback((path: string) => {
528
+ navigate(withCrossViewParams(path, location.search))
529
+ }, [location.search, navigate])
530
+
504
531
  // The standalone rail expresses intent to leave the full-width investigation
505
532
  // workspace. Close it before routing so the destination is immediately visible;
506
533
  // docked investigations stay open across views as a persistent side panel.
@@ -513,15 +540,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
513
540
  }, [diagnoseOpen, diagnoseMaximized, dismissDiagnoseForNavigation, setMainView])
514
541
 
515
542
  // Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
516
- // own fleet pages scoped to this cluster — owned by the host's left rail — so
517
- // Radar drops the matching pills (see the nav below). In-app nav hands off in
543
+ // own fleet pages scoped to this cluster. In-app navigation hands off in
518
544
  // setMainView (above); direct /<view> URL entry funnels through the redirect
519
545
  // effect below. Both consume the memoized `takeover` resolved above. Standalone
520
546
  // OSS (no fleetTakeoverHref) is unaffected and renders the in-app view.
521
547
  //
522
548
  // Has the host claimed this view? View-shaped targets only ('certs' has no
523
- // Radar view — only its Home card consults `takeover`). Used to drop the nav
524
- // pill and gate the inline view render in favor of the "Opening…" splash.
549
+ // Radar view — only its Home card consults `takeover`). Used to gate the
550
+ // inline view render in favor of the "Opening…" splash.
525
551
  const isViewTakenOver = (view: ExtendedMainView): boolean =>
526
552
  (view === 'issues' || view === 'gitops' || view === 'checks') && !!takeover[view]
527
553
  // The host's URL for the CURRENT view, if taken over. Drives the redirect
@@ -565,9 +591,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
565
591
  // Help overlay state
566
592
  const [showHelp, setShowHelp] = useState(false)
567
593
 
568
- // Command palette state
569
- const [showCommandPalette, setShowCommandPalette] = useState(false)
570
-
571
594
  // Settings dialog state
572
595
  const [showSettings, setShowSettings] = useState(false)
573
596
  const [settingsSection, setSettingsSection] = useState<SettingsSectionId>('overview')
@@ -683,7 +706,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
683
706
  // Dialog-kind overlays wait their exit duration (shorter than the entrance);
684
707
  // drawers keep the symmetric slide.
685
708
  const helpOverlay = useAnimatedUnmount(showHelp, overlayExitMs('dialog'))
686
- const commandPaletteAnim = useAnimatedUnmount(showCommandPalette, overlayExitMs('dialog'))
687
709
  const diagnosticsOverlay = useAnimatedUnmount(showDiagnostics, overlayExitMs('dialog'))
688
710
 
689
711
  // Hold last valid values so drawers can animate out before data disappears
@@ -791,13 +813,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
791
813
  metadata: { namespace: resource.namespace ?? '', name: resource.name },
792
814
  })
793
815
  if (gitOpsPath) {
794
- const destination = new URL(gitOpsPath, window.location.origin)
795
- if (investigationRunID) destination.searchParams.set('ai-run', investigationRunID)
816
+ const destination = new URL(withCrossViewParams(gitOpsPath, searchParams.toString()), window.location.origin)
817
+ if (investigationRunID === null) destination.searchParams.delete('ai-run')
818
+ else if (investigationRunID) destination.searchParams.set('ai-run', investigationRunID)
796
819
  navigate(`${destination.pathname}${destination.search}${destination.hash}`)
797
820
  return
798
821
  }
799
822
  navigateToResourceList(resource, investigationRunID)
800
- }, [navigate, navigateToHelmRelease, navigateToResourceList])
823
+ }, [navigate, navigateToHelmRelease, navigateToResourceList, searchParams])
801
824
 
802
825
  // Collapse the over-list fullscreen back to the drawer = drop ?full=1 (and the
803
826
  // resource-scoped ?tab) in place. The button means "collapse THIS to a drawer"
@@ -830,7 +853,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
830
853
  // Theme toggle for keyboard shortcut
831
854
  const { toggleTheme } = useTheme()
832
855
 
833
- // Context switching for command palette
834
856
  const switchContext = useSwitchContext()
835
857
 
836
858
  // Refs for dropdown components to trigger them via shortcuts
@@ -909,10 +931,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
909
931
  category: 'General' as const,
910
932
  scope: 'global' as const,
911
933
  allowInInputs: true,
912
- // Standalone focuses the top-center omnibar; embedded opens the modal. In
913
- // a chromeless embed the HOST owns ⌘K (its own omnibar), so do nothing —
914
- // otherwise both the host omnibar and Radar's palette fire on one ⌘K.
915
- handler: () => { if (showNavRail) omnibarRef.current?.focus(); else if (!chromeless) setShowCommandPalette(true) },
934
+ // Radar Hub owns its own omnibar.
935
+ handler: () => { if (!embedded) omnibarRef.current?.focus() },
916
936
  },
917
937
  {
918
938
  id: 'diagnostics',
@@ -981,7 +1001,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
981
1001
  const { data: namespaceScope } = useNamespaceScope()
982
1002
 
983
1003
  // Context switch state
984
- const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch } = useContextSwitch()
1004
+ const { isSwitching, targetContext, progressMessage, updateProgress, endSwitch, takeOpenAfterSwitch } = useContextSwitch()
985
1005
 
986
1006
  // Connection state (for graceful startup)
987
1007
  const { connection, retry: retryConnection, isRetrying, updateFromSSE: updateConnectionFromSSE } = useConnection()
@@ -1008,7 +1028,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1008
1028
  const { clusterLoadState, showHomeClusterLoadFallback, clusterLoadInitial } = useClusterLoadState({
1009
1029
  namespaces,
1010
1030
  mainView,
1011
- chromeless,
1031
+ chromeless: embedded,
1012
1032
  contentReady,
1013
1033
  onClusterLoadStateChange,
1014
1034
  })
@@ -1158,7 +1178,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1158
1178
  const { topology, connected: eventStreamConnected, connecting: eventStreamConnecting, reconnect: reconnectEventStream } = useEventSource(namespaces, sseMode as 'resources' | 'traffic', {
1159
1179
  onContextSwitchComplete: endSwitch,
1160
1180
  onContextSwitchProgress: updateProgress,
1161
- onContextChanged: () => {
1181
+ onContextChanged: (context) => {
1162
1182
  // Clear all React Query caches when cluster context changes
1163
1183
  // This ensures helm releases, resources, etc. are refetched from the new cluster
1164
1184
  // removeQueries clears cached data, invalidateQueries triggers refetch
@@ -1181,7 +1201,16 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1181
1201
  // Reset resource-specific params while retaining the durable investigation
1182
1202
  // focus. Diagnose resolves runs by id and owns whether the focused run is
1183
1203
  // still readable after the context switch.
1184
- if (isInvestigationWorkspacePath(location.pathname)) {
1204
+ const openAfter = takeOpenAfterSwitch(context)
1205
+ if (openAfter) {
1206
+ // The switch was made to open this resource on the new cluster; the
1207
+ // page it was requested from belongs to the old one.
1208
+ setSelectedResource(openAfter)
1209
+ const params = new URLSearchParams()
1210
+ params.set('resource', openAfter.namespace ? `${openAfter.namespace}/${openAfter.name}` : openAfter.name)
1211
+ if (openAfter.group) params.set('apiGroup', openAfter.group)
1212
+ navigate({ pathname: `/resources/${openAfter.kind}`, search: params.toString() }, { replace: true })
1213
+ } else if (isInvestigationWorkspacePath(location.pathname)) {
1185
1214
  navigate(
1186
1215
  {
1187
1216
  pathname: location.pathname,
@@ -1321,6 +1350,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1321
1350
  if (node.kind === 'Internet') return
1322
1351
 
1323
1352
  const nodeGroup = apiVersionToGroup(node.data.apiVersion as string | undefined)
1353
+ const resourceKind = topologyNodeResourceKind(node)
1324
1354
  // Radar's topology PodGroup is a virtual pod aggregate. A Kubernetes
1325
1355
  // scheduling.k8s.io PodGroup carries apiVersion and is a real resource.
1326
1356
  if (node.kind === 'PodGroup' && !nodeGroup) return
@@ -1330,14 +1360,14 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1330
1360
  // detail page with tree + insights + ops that the drawer can't reproduce.
1331
1361
  // Route there from the main topology when the node is one of those kinds;
1332
1362
  // everything else falls back to the drawer.
1333
- const gitOpsPath = gitOpsRouteForKind(node.kind, namespace, node.name)
1363
+ const gitOpsPath = gitOpsRouteForKind(resourceKind, namespace, node.name, nodeGroup, true)
1334
1364
  if (gitOpsPath) {
1335
1365
  navigate(gitOpsPath)
1336
1366
  return
1337
1367
  }
1338
1368
 
1339
1369
  navigateToResource({
1340
- kind: kindToPluralWithGroup(node.kind, nodeGroup),
1370
+ kind: kindToPluralWithGroup(resourceKind, nodeGroup),
1341
1371
  namespace,
1342
1372
  name: node.name,
1343
1373
  group: nodeGroup,
@@ -1739,6 +1769,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1739
1769
  showPinToggle={!railForcedSlim}
1740
1770
  onOpenSettings={() => openSettings()}
1741
1771
  accountSlot={<UserMenu variant="rail" pinned={navRailEffectivePinned} />}
1772
+ whatsNew={whatsNewStatus.available ? { unread: whatsNewStatus.unread, onOpen: openWhatsNew } : undefined}
1742
1773
  />
1743
1774
  )}
1744
1775
  {/* `relative` makes this column the containing block for the absolute
@@ -1752,46 +1783,20 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1752
1783
  − nav rail), not the viewport, so it collapses gracefully on narrow windows.
1753
1784
  The AI panel docks BELOW the navbar and pushes only the content region, so the
1754
1785
  navbar is never squeezed by it — these thresholds react to real window width. */}
1755
- {!chromeless && (
1786
+ {!embedded && (
1756
1787
  <header className="@container relative z-50 flex items-center justify-between px-4 py-2 bg-theme-base/90 backdrop-blur-sm border-b border-theme-border/50">
1757
- {/* Left: Logo + Cluster info. In the standalone (nav-rail) layout this
1758
- is a FIXED-WIDTH column so the omnibar after it is force-pinned: the
1759
- scope pill + status dot can change width (cluster/namespace value)
1760
- without ever shifting the search box. The pill's own name/value caps
1761
- keep it inside this width; the embedded/pill layout keeps auto width
1762
- (its center bar is absolutely positioned). */}
1763
- <div className={`flex items-center gap-4 shrink-0 ${showNavRail ? 'w-[492px]' : ''}`}>
1764
- {/* Standalone rail owns the brand; only the embedded/pill layout
1765
- shows it in the header (host may override via brandSlot). */}
1766
- {navCustomization.brandSlot ?? (showNavRail ? null : <Logo />)}
1767
-
1768
- <div className={`flex items-center gap-2 min-w-0 ${showNavRail ? 'flex-1' : ''}`}>
1769
- {navCustomization.contextSlot ? (
1770
- // Embedded host supplies its own cluster switcher — keep the two
1771
- // controls separate (the host owns the cluster chip's styling).
1772
- <>
1773
- {navCustomization.contextSlot}
1774
- <NamespaceSwitcher
1775
- ref={namespaceSwitcherRef}
1776
- disabled={namespaceFilter.disabled}
1777
- disabledTooltip={namespaceFilter.tooltip}
1778
- />
1779
- </>
1780
- ) : (
1781
- // Standalone: cluster + namespace as one "scope" pill — two
1782
- // borderless segments split by a divider, reading as a single
1783
- // "what am I looking at" unit. The shared ScopePill shell is the
1784
- // same one Radar Hub's cluster top bar uses, so the two match.
1785
- <ScopePill>
1786
- <ContextSwitcher ref={contextSwitcherRef} variant="segment" />
1787
- <NamespaceSwitcher
1788
- ref={namespaceSwitcherRef}
1789
- variant="segment"
1790
- disabled={namespaceFilter.disabled}
1791
- disabledTooltip={namespaceFilter.tooltip}
1792
- />
1793
- </ScopePill>
1794
- )}
1788
+ {/* Fixed width keeps the omnibar steady as cluster and namespace names change. */}
1789
+ <div className="flex items-center gap-4 shrink-0 w-[492px]">
1790
+ <div className="flex items-center gap-2 min-w-0 flex-1">
1791
+ <ScopePill>
1792
+ <ContextSwitcher ref={contextSwitcherRef} variant="segment" />
1793
+ <NamespaceSwitcher
1794
+ ref={namespaceSwitcherRef}
1795
+ variant="segment"
1796
+ disabled={namespaceFilter.disabled}
1797
+ disabledTooltip={namespaceFilter.tooltip}
1798
+ />
1799
+ </ScopePill>
1795
1800
  {/* Connection status — a fixed-size dot (state in the tooltip), an
1796
1801
  optional reconnect button, and when the header is wide enough
1797
1802
  (xl+), a label. The label is nowrap and unbounded: it overflows
@@ -1817,7 +1822,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1817
1822
  }`}
1818
1823
  />
1819
1824
  </Tooltip>
1820
- {showNavRail && headerConnectionDisplayLabel && (
1825
+ {headerConnectionDisplayLabel && (
1821
1826
  <span className="hidden xl:flex items-center gap-1.5 whitespace-nowrap text-[11px] text-theme-text-tertiary">
1822
1827
  {showClusterWarmupLabel && <Loader2 className="w-3 h-3 animate-spin" />}
1823
1828
  {headerConnectionDisplayLabel}
@@ -1842,65 +1847,6 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1842
1847
  </div>
1843
1848
  </div>
1844
1849
 
1845
- {/* Center: View tabs — embedded/pill layout only. Standalone Radar
1846
- navigates via the left rail (showNavRail), so the pill bar is
1847
- suppressed there to avoid a duplicate primary nav. */}
1848
- {!showNavRail && (
1849
- <div className="@min-[920px]:absolute @min-[920px]:left-1/2 @min-[920px]:-translate-x-1/2 flex items-center gap-0.5 bg-theme-elevated/50 rounded-full p-1 ml-2 @min-[920px]:ml-0">
1850
- {([
1851
- { view: 'home' as const, icon: Home, label: 'Home' },
1852
- { view: 'topology' as const, icon: Network, label: 'Topology' },
1853
- { view: 'resources' as const, icon: List, label: 'Resources' },
1854
- { view: 'capacity' as const, icon: Gauge, label: 'Capacity' },
1855
- { view: 'timeline' as const, icon: Clock, label: 'Timeline' },
1856
- { view: 'helm' as const, icon: Package, label: 'Helm' },
1857
- { view: 'gitops' as const, icon: GitBranch, label: 'GitOps' },
1858
- // Applications is intentionally hidden from the pill bar for now —
1859
- // the bar is full, and the view's primary home is Cloud's fleet
1860
- // rail. The view still exists and is reachable via /applications
1861
- // and the view-switching shortcuts. Same treatment as Cost below.
1862
- { view: 'traffic' as const, icon: Activity, label: 'Live Traffic' },
1863
- // Cost is intentionally hidden from the pill bar for now — the view still
1864
- // exists and is reachable via /cost, the Home dashboard card, and the
1865
- // command palette (⌘K). Remove this comment to restore it.
1866
- { view: 'checks' as const, icon: ShieldCheck, label: 'Checks' },
1867
- ] as const)
1868
- // In Cloud, fleet-shaped views (Checks, Issues, GitOps) are owned by
1869
- // the host's left rail; the per-cluster view is just that fleet page
1870
- // filtered to this cluster, so duplicating it as a peer pill here
1871
- // would be a second copy that teleports out of the cluster shell.
1872
- // Drop any pill the host took over — cluster-scoped access stays
1873
- // available via the Home cards (redirected by the takeover effect
1874
- // above), ⌘K, and bookmarks. Standalone OSS keeps every pill.
1875
- .filter(({ view }) => !isViewTakenOver(view))
1876
- .map(({ view, icon: Icon, label }) => (
1877
- <Tooltip key={view} content={label} delay={100} position="bottom">
1878
- <button
1879
- onClick={() => setMainView(view)}
1880
- className={`flex items-center gap-1 px-2 py-1 text-[13px] rounded-full transition-colors ${
1881
- mainView === view || (mainView === 'helmCompare' && view === 'helm')
1882
- ? 'bg-skyhook-600 dark:bg-skyhook-500 text-white shadow-glow-brand-sm'
1883
- : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover'
1884
- }`}
1885
- >
1886
- <Icon className="w-4 h-4" />
1887
- {/* Labels appear only when the absolute-centered nav has
1888
- enough horizontal room past the left section. Right-side
1889
- chrome that adds further pressure (Connected text, star
1890
- count) is intentionally pushed to the next tier (xl) so
1891
- label rendering and right-side expansion stay decoupled.
1892
- Per-button Tooltip discloses labels on hover when the
1893
- icon-only viewport is in effect. The 1440 anchor is an
1894
- off-system breakpoint chosen by measurement at the time
1895
- of this PR — recompute if the cluster switcher cap or
1896
- other left-section chrome changes appreciably. */}
1897
- <span className="hidden @min-[1264px]:inline">{label}</span>
1898
- </button>
1899
- </Tooltip>
1900
- ))}
1901
- </div>
1902
- )}
1903
-
1904
1850
  {/* Center: omnibar — standalone search + command surface (the ⌘K entry).
1905
1851
  Its container is one of three EQUAL flex-1 columns (see the left/right
1906
1852
  groups): equal side columns keep this middle column — and the search
@@ -1908,60 +1854,38 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1908
1854
  chrome gets (cluster name, namespace label, "Discovering…" /
1909
1855
  "Disconnected" text). Overflowing side content truncates instead of
1910
1856
  dragging the box. Same pattern as Radar Hub's ClusterTopBar. */}
1911
- {showNavRail && (
1912
- <div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
1913
- <RadarOmnibar
1914
- ref={omnibarRef}
1915
- onNavigateView={(view) => setMainView(view)}
1916
- onNavigateKind={(kind, group) => {
1917
- const params = new URLSearchParams(searchParams)
1918
- params.delete('kind')
1919
- if (group) params.set('apiGroup', group); else params.delete('apiGroup')
1920
- params.delete('resource')
1921
- params.delete('full')
1922
- params.delete('tab')
1923
- navigate({ pathname: `/resources/${kind}`, search: params.toString() })
1924
- }}
1925
- onSwitchContext={(name) => switchContext.mutate({ name }, { onSettled: () => setNamespaces([]) })}
1926
- onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }}
1927
- onToggleTheme={toggleTheme}
1928
- onShowDiagnostics={() => setShowDiagnostics(true)}
1929
- onOpenResource={(hit) => navigateToResourceList(searchHitToSelectedResource(hit))}
1930
- />
1931
- </div>
1932
- )}
1857
+ <div className="hidden @min-[720px]:flex flex-1 justify-center min-w-0 px-3">
1858
+ <RadarOmnibar
1859
+ ref={omnibarRef}
1860
+ onNavigateView={(view) => setMainView(view)}
1861
+ onNavigateKind={(kind, group) => {
1862
+ const params = new URLSearchParams(searchParams)
1863
+ params.delete('kind')
1864
+ if (group) params.set('apiGroup', group); else params.delete('apiGroup')
1865
+ params.delete('resource')
1866
+ params.delete('full')
1867
+ params.delete('tab')
1868
+ navigate({ pathname: `/resources/${kind}`, search: params.toString() })
1869
+ }}
1870
+ onSwitchContext={(name) => switchContext.mutate({ name }, { onSettled: () => setNamespaces([]) })}
1871
+ onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }}
1872
+ onToggleTheme={toggleTheme}
1873
+ onShowDiagnostics={() => setShowDiagnostics(true)}
1874
+ onShowWhatsNew={whatsNewStatus.available ? openWhatsNew : undefined}
1875
+ onOpenResource={(hit) => navigateToResourceList(searchHitToSelectedResource(hit))}
1876
+ />
1877
+ </div>
1933
1878
 
1934
1879
  {/* Right: Controls */}
1935
1880
  <div className="flex items-center gap-3 shrink-0">
1936
- {/* Command palette trigger — embedded only; standalone has the
1937
- top-center omnibar (which is the ⌘K surface). */}
1938
- {!showNavRail && (
1939
- <button
1940
- onClick={() => setShowCommandPalette(true)}
1941
- aria-label="Open command palette"
1942
- className="hidden lg:flex items-center gap-2 h-7 px-2.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1943
- >
1944
- <Search className="w-3.5 h-3.5" />
1945
- <kbd className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded border border-theme-border-light">
1946
- {typeof navigator !== 'undefined' && navigator.platform.includes('Mac') ? '⌘' : 'Ctrl+'}K
1947
- </kbd>
1948
- </button>
1949
- )}
1950
-
1951
- {/* GitHub star — hidden in embedded mode (not OSS-distribution chrome). */}
1952
- {!navCustomization.embedded && (
1953
- <div className="hidden @min-[1100px]:block">
1954
- <GitHubStarButton />
1955
- </div>
1956
- )}
1881
+ <div className="hidden @min-[1100px]:block">
1882
+ <GitHubStarButton />
1883
+ </div>
1957
1884
 
1958
1885
  {/* AI investigations (self-hides when no agent CLI is present) */}
1959
1886
  <GlobalDiagnoseButton />
1960
1887
 
1961
- {/* Radar Cloud funnel — OSS-only chrome, same gate as the star.
1962
- Cloud embeds render chromeless anyway; the explicit gate is
1963
- belt-and-braces for future chrome-bearing embedders. */}
1964
- {!navCustomization.embedded && <CloudFunnelButton />}
1888
+ <CloudFunnelButton />
1965
1889
 
1966
1890
  {/* Local terminal */}
1967
1891
  {capabilities.localTerminal && (
@@ -1976,50 +1900,28 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1976
1900
  </Tooltip>
1977
1901
  )}
1978
1902
 
1979
- {/* Theme toggle — hidden in embedded mode. Host apps (e.g. Radar
1980
- Cloud) own the user-theme preference and mount their own picker
1981
- in the account menu; a second toggle in Radar's topbar would
1982
- fight them (one writes to Radar's localStorage key, the other
1983
- to the host's cookie/backend) and the user would see the theme
1984
- bounce on every navigation between host routes and /c/:id. */}
1985
- {!navCustomization.embedded && (
1986
- <div className="hidden @min-[920px]:flex items-center">
1987
- <ThemeToggle />
1988
- </div>
1989
- )}
1990
-
1991
- {/* Help + Report-a-bug — standalone only (the left rail owns chrome;
1992
- embedded hosts provide their own help/support). These replace the
1993
- old floating bottom-right pair. Settings moved to the rail bottom. */}
1994
- {showNavRail && (
1995
- <>
1996
- <Tooltip content="Keyboard shortcuts (?)">
1997
- <button
1998
- onClick={() => setShowHelp(true)}
1999
- aria-label="Show keyboard shortcuts"
2000
- className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
2001
- >
2002
- <HelpCircle className="w-4 h-4" />
2003
- </button>
2004
- </Tooltip>
2005
- <Tooltip content="Report a bug / Diagnostics">
2006
- <button
2007
- onClick={() => setShowDiagnostics(true)}
2008
- aria-label="Open diagnostics"
2009
- className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
2010
- >
2011
- <Bug className="w-4 h-4" />
2012
- </button>
2013
- </Tooltip>
2014
- </>
2015
- )}
2016
-
2017
- {/* Account moved to the rail bottom (standalone). Embedded never showed
2018
- Radar's UserMenu — the host provides its own via rightExtras. */}
1903
+ <div className="hidden @min-[920px]:flex items-center">
1904
+ <ThemeToggle />
1905
+ </div>
2019
1906
 
2020
- {/* Consumer-provided extras (e.g. Radar Hub's Install button +
2021
- avatar menu) appended to the right of the action bar. */}
2022
- {navCustomization.rightExtras}
1907
+ <Tooltip content="Keyboard shortcuts (?)">
1908
+ <button
1909
+ onClick={() => setShowHelp(true)}
1910
+ aria-label="Show keyboard shortcuts"
1911
+ className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1912
+ >
1913
+ <HelpCircle className="w-4 h-4" />
1914
+ </button>
1915
+ </Tooltip>
1916
+ <Tooltip content="Report a bug / Diagnostics">
1917
+ <button
1918
+ onClick={() => setShowDiagnostics(true)}
1919
+ aria-label="Open diagnostics"
1920
+ className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1921
+ >
1922
+ <Bug className="w-4 h-4" />
1923
+ </button>
1924
+ </Tooltip>
2023
1925
  </div>
2024
1926
  </header>
2025
1927
  )}
@@ -2114,7 +2016,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2114
2016
  {/* Search included, not just the path: selection inside a view rides in
2115
2017
  the query (?resource=, ?release=), so a path-only key would still
2116
2018
  strand a crash on the view that produced it. */}
2117
- <ErrorBoundary resetKey={location.pathname + location.search}>
2019
+ <ErrorBoundary
2020
+ resetKey={location.pathname + location.search}
2021
+ usageLabel={upgradeReadinessRoute ? 'Upgrade' : CRASH_LABELS[mainView]}
2022
+ >
2118
2023
  {/* Initial sync in progress: views that need the full cluster dataset
2119
2024
  show per-kind progress instead; resource views work as kinds sync. */}
2120
2025
  {viewsSyncGated && connection.syncStatus && (
@@ -2131,7 +2036,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2131
2036
  fallbackClusterLoadState={showHomeClusterLoadFallback ? clusterLoadState : undefined}
2132
2037
  onNavigateToView={setMainView}
2133
2038
  onNavigateToHelmRelease={navCustomization.embedded ? undefined : navigateToHelmRelease}
2134
- onNavigateToManagerPath={navCustomization.embedded || takeover.gitops ? undefined : (path) => navigate(path)}
2039
+ onNavigateToManagerPath={navCustomization.embedded || takeover.gitops ? undefined : navigateToPath}
2040
+ onShowWhatsNew={!navCustomization.embedded && whatsNewStatus.available ? openWhatsNew : undefined}
2135
2041
  // Upgrade impact lives under /checks, which a Cloud host takes
2136
2042
  // over wholesale — its fleet pages have no upgrade sub-route, so
2137
2043
  // the version line stays plain text there.
@@ -2454,7 +2360,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2454
2360
  initialTab={drawerInitialTab}
2455
2361
  // No Radar header in chromeless embeds (Radar Hub) — anchor the drawer
2456
2362
  // to the top of the content area instead of leaving a 49px gap.
2457
- headerHeight={chromeless ? 0 : undefined}
2363
+ headerHeight={embedded ? 0 : undefined}
2458
2364
  rightInset={contentGutter}
2459
2365
  isOpen={resourceDrawer.isOpen}
2460
2366
  expanded={drawerExpandedProp}
@@ -2523,7 +2429,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2523
2429
  maximized = fills the frame. */}
2524
2430
  {diagnoseOpen && (
2525
2431
  <DiagnoseSurface
2526
- topInset={chromeless ? 0 : APP_HEADER_HEIGHT}
2432
+ topInset={embedded ? 0 : APP_HEADER_HEIGHT}
2527
2433
  onBrowseIssues={() => setMainView('issues')}
2528
2434
  onOpenResource={(ref, investigationRunID) => {
2529
2435
  const resource: SelectedResource = {
@@ -2572,6 +2478,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2572
2478
 
2573
2479
  {/* Update notification — hidden in embedded mode (OSS download nudge). */}
2574
2480
  {!navCustomization.embedded && <UpdateNotification />}
2481
+ {!navCustomization.embedded && <WhatsNew onNavigate={navigateToPath} usageData={usageData.data} />}
2482
+ {!navCustomization.embedded && <UsageDataPrompt status={usageData.data} />}
2575
2483
 
2576
2484
  {/* Bottom Dock for Terminal/Logs */}
2577
2485
  <BottomDock />
@@ -2579,55 +2487,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2579
2487
  {/* Spacer for dock */}
2580
2488
  <DockSpacer />
2581
2489
 
2582
- {/* Floating action buttons — embedded only, and not in chromeless (the
2583
- host owns help/diagnostics chrome). Standalone moved help + bug to
2584
- visible top-bar icons (the rail owns chrome). */}
2585
- {!showNavRail && !chromeless && (
2586
- <FloatingButtons showHelp={showHelp} showCommandPalette={showCommandPalette} showDiagnostics={showDiagnostics} onHelp={() => setShowHelp(true)} onBugReport={() => setShowDiagnostics(true)} />
2587
- )}
2588
-
2589
2490
  {/* Keyboard shortcut help overlay */}
2590
2491
  {helpOverlay.shouldRender && <ShortcutHelpOverlay isOpen={helpOverlay.isOpen} onClose={() => setShowHelp(false)} currentView={mainView} />}
2591
2492
 
2592
- {/* Command palette */}
2593
- {commandPaletteAnim.shouldRender && (
2594
- <CommandPalette
2595
- isOpen={commandPaletteAnim.isOpen}
2596
- onClose={() => setShowCommandPalette(false)}
2597
- onNavigateView={(view) => setMainView(view)}
2598
- onNavigateKind={(kind, group) => {
2599
- const params = new URLSearchParams(searchParams)
2600
- params.delete('kind')
2601
- if (group) params.set('apiGroup', group)
2602
- else params.delete('apiGroup')
2603
- params.delete('resource')
2604
- params.delete('full')
2605
- params.delete('tab')
2606
- navigate({ pathname: `/resources/${kind}`, search: params.toString() })
2607
- // Focus the table search after navigation — the user came from ⌘K
2608
- // (keyboard flow) and expects to type a resource name immediately.
2609
- setTimeout(() => {
2610
- (document.querySelector('input[placeholder="Search... (press /)"]') as HTMLInputElement)?.focus()
2611
- }, 100)
2612
- }}
2613
- onSwitchContext={(name) => switchContext.mutate(
2614
- { name },
2615
- // Namespace filter from the previous context may not exist in the
2616
- // new one — clear it so resource lists don't silently go empty.
2617
- // The server clears all per-user picks on context switch already;
2618
- // local state mirrors that via the namespace-scope effect.
2619
- { onSettled: () => setNamespaces([]) },
2620
- )}
2621
- onSetNamespaces={(ns) => {
2622
- if (namespaceScope?.cacheScoped && ns.length !== 1) return
2623
- setNamespaces(ns)
2624
- setActiveNamespace.mutate({ namespaces: ns })
2625
- }}
2626
- onToggleTheme={toggleTheme}
2627
- onShowDiagnostics={() => setShowDiagnostics(true)}
2628
- />
2629
- )}
2630
-
2631
2493
  {/* Diagnostics overlay */}
2632
2494
  {diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
2633
2495
 
@@ -2666,29 +2528,6 @@ function DockSpacer() {
2666
2528
  )
2667
2529
  }
2668
2530
 
2669
- // Floating action buttons that position themselves above the dock
2670
- function FloatingButtons({ showHelp, showCommandPalette, showDiagnostics, onHelp, onBugReport }: { showHelp: boolean; showCommandPalette: boolean; showDiagnostics: boolean; onHelp: () => void; onBugReport: () => void }) {
2671
- const { tabs } = useDock()
2672
- if (showHelp || showCommandPalette || showDiagnostics) return null
2673
- // When dock tab bar is visible (36px), shift the buttons up above it
2674
- const bottom = tabs.length > 0 ? 'bottom-10' : 'bottom-2'
2675
- const btnClass = 'w-7 h-7 flex items-center justify-center rounded-full bg-theme-elevated/80 hover:bg-theme-hover border border-theme-border-light text-theme-text-tertiary hover:text-theme-text-secondary text-xs font-medium shadow-sm backdrop-blur-sm transition-all'
2676
- return (
2677
- <div className={`fixed ${bottom} right-4 z-40 flex items-center gap-1.5`}>
2678
- <Tooltip content="Report bug / Diagnostics" position="top">
2679
- <button onClick={onBugReport} aria-label="Open diagnostics" className={btnClass}>
2680
- <Bug className="w-3.5 h-3.5" />
2681
- </button>
2682
- </Tooltip>
2683
- <Tooltip content="Keyboard shortcuts (?)" position="top">
2684
- <button onClick={onHelp} aria-label="Show keyboard shortcuts" className={btnClass}>
2685
- ?
2686
- </button>
2687
- </Tooltip>
2688
- </div>
2689
- )
2690
- }
2691
-
2692
2531
  // Main App component wrapped with providers
2693
2532
  function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
2694
2533
  return (
@@ -2710,37 +2549,6 @@ function App({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadSt
2710
2549
  )
2711
2550
  }
2712
2551
 
2713
- // Header brand: emerald-square radar icon + stacked "Radar" / "by Skyhook"
2714
- // wordmark. Shares its visual shape with the radar-hub-web shell so the
2715
- // standalone OSS app and the embedded Cloud experience read as the same
2716
- // product, and is narrow enough to leave room for the cluster switcher
2717
- // and nav block on standard laptop viewports.
2718
- function Logo() {
2719
- return (
2720
- <div className="flex items-center gap-2.5">
2721
- <div className="relative w-7 h-7 rounded-lg overflow-hidden flex-shrink-0 bg-emerald-500/10 border border-emerald-500/20">
2722
- <img
2723
- src={radarLogoUrl}
2724
- alt=""
2725
- aria-hidden
2726
- className="w-full h-full p-0.5"
2727
- // Fail loud on a missing/blocked asset rather than rendering an
2728
- // empty emerald square next to the wordmark — the latter reads
2729
- // as broken chrome with no diagnostics. Most likely cause is a
2730
- // build/deploy path mismatch.
2731
- onError={(e) =>
2732
- console.error('Radar logo asset failed to load:', (e.currentTarget as HTMLImageElement).src)
2733
- }
2734
- />
2735
- </div>
2736
- <div className="flex flex-col leading-none">
2737
- <span className="font-semibold text-[15px] tracking-tight text-theme-text-primary">Radar</span>
2738
- <span className="text-[9px] mt-0.5 tracking-wide uppercase text-theme-text-tertiary">by Skyhook</span>
2739
- </div>
2740
- </div>
2741
- )
2742
- }
2743
-
2744
2552
  // GitHub star button with live star count + programmatic starring via gh CLI
2745
2553
  // Shows a callout popover when the backend says shouldPrompt is true (synced with CLI state)
2746
2554
  function GitHubStarButton() {