/* CommandPalette — global ⌘K / Ctrl+K launcher. * * Three sections: * • Navigation — every primary route from the top-bar nav, * plus admin shortcuts. * • Recent activity — last 5 analyses + last 5 device observations, * pulled lazily from /api/dashboard/overview. * • Companies — fuzzy search against /api/companies (kicks in * once the user types a query). * * Mounted in the root layout so it's available on every page. */ 'use client'; import { useEffect, useState, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useSession } from '@/lib/auth-client'; import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from '@/components/ui/command'; import { LayoutDashboard, Server, Activity, HardDrive, Sparkles, Brain, Search, AlertTriangle, Database, GitCompare, Plug, Power, Users, Workflow, Settings, ShieldCheck, TrendingUp, Building2, Clock, } from 'lucide-react'; // ── Static navigation entries ──────────────────────────────────────── interface CommandRoute { href: string; label: string; hint?: string; icon: React.ElementType; /** Comma-separated extra keywords to match. */ keywords?: string; superAdminOnly?: boolean; } const ROUTES: CommandRoute[] = [ // Primary { href: '/', label: 'Dashboard', icon: LayoutDashboard, keywords: 'home ops operations today kpi' }, { href: '/status', label: 'System status', icon: Activity, keywords: 'health integrations workers sync uptime' }, { href: '/configuration-items', label: 'Configuration items', icon: Server, keywords: 'ci devices assets' }, { href: '/backup-status', label: 'Backup status', icon: HardDrive, keywords: 'veeam rpo' }, { href: '/veeam-comparison', label: 'RPO comparison', icon: GitCompare, keywords: 'pulse veeam datto rmm' }, { href: '/veeam-analysis', label: 'Backup ticket analysis', icon: Brain, keywords: 'veeam category resolution' }, // Engagement (super-admin) { href: '/engagement', label: 'Engagement overview', icon: TrendingUp, superAdminOnly: true, keywords: 'hours teams email' }, { href: '/engagement/profile', label: 'Engagement · employee profile', icon: TrendingUp, superAdminOnly: true, keywords: 'employee resource' }, // Analyzer { href: '/analyzer/tickets', label: 'Analyzer · browse tickets', icon: Search, keywords: 'tickets analyzer browse' }, { href: '/analyzer/reports', label: 'Analyzer · aggregate reports', icon: Sparkles, keywords: 'aggregate report' }, { href: '/analyzer/queue', label: 'Analyzer · needs review', icon: Sparkles, keywords: 'human review queue' }, { href: '/analyzer/itglue/applications', label: 'IT Glue · applications', icon: Database, keywords: 'flexible asset' }, { href: '/analyzer/itglue/configurations', label: 'IT Glue · configurations', icon: Database, keywords: 'configuration' }, // Admin (super-admin) { href: '/admin', label: 'Admin home', icon: Activity, superAdminOnly: true, keywords: 'admin dashboard' }, { href: '/admin/integrations', label: 'Admin · integrations', icon: Power, superAdminOnly: true, keywords: 'toggle disable enable s1 sentinelone datto itglue' }, { href: '/admin/sync', label: 'Admin · sync schedules', icon: Clock, superAdminOnly: true, keywords: 'cron schedules autotask veeam' }, { href: '/admin/workflow', label: 'Admin · workflow rules', icon: Workflow, superAdminOnly: true, keywords: 'classifier ai prompt' }, { href: '/admin/rmm-overshell', label: 'Admin · RMM Overshell', icon: Activity, superAdminOnly: true, keywords: 'datto execute script' }, { href: '/admin/itglue-writes', label: 'Admin · IT Glue write log', icon: Database, superAdminOnly: true, keywords: 'audit revert' }, { href: '/admin/device-link-conflicts', label: 'Admin · device-link conflicts', icon: AlertTriangle, superAdminOnly: true, keywords: 'mapping conflicts xref' }, { href: '/admin/users', label: 'Admin · users & roles', icon: Users, superAdminOnly: true, keywords: 'invite role permission' }, // Account { href: '/settings', label: 'Settings', icon: Settings, keywords: 'profile account' }, { href: '/settings/security', label: 'Security · 2FA & sessions', icon: ShieldCheck, keywords: 'two factor totp logout' }, ]; interface OverviewResponse { observations: Array<{ id: string; kind: string; hostname: string | null; companyName: string | null }>; audits: Array<{ id: string; hostname: string | null; companyName: string | null }>; } interface CompanyHit { id: number; name: string; } interface CompaniesApiRow { id: number; companyName: string; } // ── Component ──────────────────────────────────────────────────────── export function CommandPalette() { const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [recents, setRecents] = useState(null); const [allCompanies, setAllCompanies] = useState(null); const [companiesLoading, setCompaniesLoading] = useState(false); const router = useRouter(); const { data: session } = useSession(); const isSuperAdmin = (session?.user as { role?: string } | undefined)?.role === 'super-admin'; // Bind ⌘K / Ctrl+K (and / when nothing else is focused). useEffect(() => { function onKey(e: KeyboardEvent) { const isCmdK = (e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey); const isSlash = e.key === '/' && !(document.activeElement instanceof HTMLInputElement) && !(document.activeElement instanceof HTMLTextAreaElement) && !(document.activeElement as HTMLElement | null)?.isContentEditable; if (isCmdK || isSlash) { e.preventDefault(); setOpen((v) => !v); } } document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, []); // Lazy-load recent activity once on first open. useEffect(() => { if (!open || recents) return; void fetch('/api/dashboard/overview', { cache: 'no-store' }) .then((r) => (r.ok ? r.json() : null)) .then((j: OverviewResponse | null) => { if (j) setRecents(j); }) .catch(() => { /* ignore */ }); }, [open, recents]); // Companies are loaded once on first open (the endpoint returns the // full active customer list — small enough to filter client-side). useEffect(() => { if (!open || allCompanies !== null) return; setCompaniesLoading(true); void fetch('/api/companies', { cache: 'no-store' }) .then((r) => (r.ok ? r.json() : { companies: [] })) .then((j: { companies?: CompaniesApiRow[] }) => { const items: CompanyHit[] = (j.companies ?? []).map((c) => ({ id: c.id, name: c.companyName, })); setAllCompanies(items); }) .catch(() => setAllCompanies([])) .finally(() => setCompaniesLoading(false)); }, [open, allCompanies]); const companyMatches = (() => { const q = query.trim().toLowerCase(); if (q.length < 2 || !allCompanies) return []; return allCompanies .filter((c) => c.name.toLowerCase().includes(q)) .slice(0, 8); })(); const go = useCallback( (href: string) => { setOpen(false); router.push(href); }, [router], ); const visibleRoutes = ROUTES.filter((r) => !r.superAdminOnly || isSuperAdmin); return ( No results. {visibleRoutes.map((r) => ( go(r.href)} > {r.label} {r.hint && {r.hint}} ))} {recents && recents.audits.length > 0 && ( <> {recents.audits.slice(0, 5).map((a) => ( go(`/analyzer/itglue/configurations`)} > {a.hostname ?? '(unanchored)'} {a.companyName && ( · {a.companyName} )} ))} )} {recents && recents.observations.length > 0 && ( <> {recents.observations.slice(0, 5).map((o) => ( go('/configuration-items')} > {o.hostname ?? '(unanchored)'} {o.companyName && ( · {o.companyName} )} ))} )} {companyMatches.length > 0 && ( <> {companyMatches.map((c) => ( go( `/configuration-items?company=${encodeURIComponent(String(c.id))}`, ) } > {c.name} ))} )} Press ⌘ K or / to open this panel anywhere. ); }