import { useState, useEffect, useMemo, useCallback } from 'react'; import { Terminal, CloudDownload, Home, HardDrive, ScrollText, Activity, Radar, RefreshCw, Clock, } from 'lucide-react'; import type { LucideIcon } from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; import { useLicense } from '@/context/LicenseContext'; import { useNodes } from '@/context/NodeContext'; import { SENCHO_NAVIGATE_EVENT } from '@/components/NodeManager'; import type { SenchoNavigateDetail } from '@/components/NodeManager'; import type { SectionId } from '@/components/settings/types'; import type { ScheduleTaskPrefill } from '@/components/ScheduledOperationsView'; export type ActiveView = | 'dashboard' | 'editor' | 'host-console' | 'resources' | 'templates' | 'global-observability' | 'fleet' | 'audit-log' | 'scheduled-ops' | 'auto-updates' | 'settings'; // Views that operate on hub-owned state (node registry, fleet schedules, // centralized audit, fleet-wide log aggregation, fleet-wide update preview). // Hidden from the nav strip and force-redirect to dashboard when the active // node is remote, since proxying them would surface that remote's own // disconnected state instead of the hub's. Settings sub-sections use the // parallel `hiddenOnRemote` registry (see settings/registry.ts). export const HUB_ONLY_VIEWS: ReadonlySet = new Set([ 'fleet', 'scheduled-ops', 'audit-log', 'global-observability', 'auto-updates', ]); export interface NavItem { value: ActiveView; label: string; icon: LucideIcon; } interface UseViewNavigationStateOptions { onNavigateToDashboard?: () => void; } export function useViewNavigationState(options?: UseViewNavigationStateOptions) { const { onNavigateToDashboard } = options ?? {}; const { isAdmin, can } = useAuth(); const { isPaid, license } = useLicense(); const { activeNode } = useNodes(); const isRemote = activeNode?.type === 'remote'; const [activeView, setActiveView] = useState('dashboard'); const [settingsSection, setSettingsSection] = useState('appearance'); const [securityHistoryOpen, setSecurityHistoryOpen] = useState(false); const [filterNodeId, setFilterNodeId] = useState(null); const [schedulePrefill, setSchedulePrefill] = useState(null); const [mobileNavOpen, setMobileNavOpen] = useState(false); const handleOpenSettings = useCallback((section?: SectionId) => { if (section) setSettingsSection(section); setActiveView('settings'); setFilterNodeId(null); }, []); const handlePrefillConsumed = useCallback(() => setSchedulePrefill(null), []); const handleNavigate = useCallback((value: string) => { if (value === activeView) return; if (value === 'dashboard') { onNavigateToDashboard?.(); setActiveView('dashboard'); } else { setActiveView(value as ActiveView); setFilterNodeId(null); } }, [activeView, onNavigateToDashboard]); useEffect(() => { const handler = (e: Event) => { const detail = (e as CustomEvent).detail; if (!detail?.view) return; if (detail.view === 'security-history') { setSecurityHistoryOpen(true); setFilterNodeId(detail.nodeId ?? null); return; } setActiveView(detail.view as ActiveView); setFilterNodeId(detail.nodeId ?? null); }; window.addEventListener(SENCHO_NAVIGATE_EVENT, handler); return () => window.removeEventListener(SENCHO_NAVIGATE_EVENT, handler); }, []); const navItems = useMemo((): NavItem[] => { const items: NavItem[] = [ { value: 'dashboard', label: 'Home', icon: Home }, { value: 'fleet', label: 'Fleet', icon: Radar }, { value: 'resources', label: 'Resources', icon: HardDrive }, { value: 'templates', label: 'App Store', icon: CloudDownload }, ]; // The aggregated Logs feed crosses every managed stack, so it is an // admin-only operator view (the backend gates the same routes on admin). if (isAdmin) items.push({ value: 'global-observability', label: 'Logs', icon: Activity }); if (isPaid && isAdmin) { items.push({ value: 'auto-updates', label: 'Auto-Update', icon: RefreshCw }); items.push({ value: 'scheduled-ops', label: 'Schedules', icon: Clock }); } if (isPaid && license?.variant === 'admiral') { if (isAdmin) items.push({ value: 'host-console', label: 'Console', icon: Terminal }); if (can('system:audit')) items.push({ value: 'audit-log', label: 'Audit', icon: ScrollText }); } return isRemote ? items.filter(i => !HUB_ONLY_VIEWS.has(i.value)) : items; }, [isAdmin, isPaid, license?.variant, can, isRemote]); useEffect(() => { // Redirect off a view the active context can't reach: a hub-only view while // a remote node is active, or the admin-only Logs view as a non-admin (e.g. // arrived via a deep-link event rather than the now-hidden nav item). const blockedByRemote = isRemote && HUB_ONLY_VIEWS.has(activeView); const blockedByRole = !isAdmin && activeView === 'global-observability'; if (blockedByRemote || blockedByRole) { onNavigateToDashboard?.(); setActiveView('dashboard'); setFilterNodeId(null); } }, [isRemote, isAdmin, activeView, onNavigateToDashboard]); return { activeView, setActiveView, settingsSection, setSettingsSection, securityHistoryOpen, setSecurityHistoryOpen, filterNodeId, setFilterNodeId, schedulePrefill, setSchedulePrefill, mobileNavOpen, setMobileNavOpen, handleOpenSettings, handlePrefillConsumed, handleNavigate, navItems, } as const; }