diff --git a/DESIGN.md b/DESIGN.md
index 5476fb0..ecb137f 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -349,6 +349,15 @@ below is the working backlog; expand as we go.
expandable-row patterns (useful for `/veeam-analysis`-style
drill-downs).
+### Command palette (2026-05-03)
+- [x] **Cmd+K / Ctrl+K** opens a global launcher
+ (`components/navigation/command-palette.tsx`). Three sections:
+ Navigation (every primary route, role-gated), Recent activity
+ (last 5 audits + 5 observations from `/api/dashboard/overview`),
+ Companies (filtered client-side from `/api/companies`). The `/`
+ key also opens it when no input is focused. Top-bar shows a small
+ "Search · ⌘K" button (md+) for discoverability.
+
### Status & dashboard split (2026-05-03)
- [x] Move integration health + sync health off `/dashboard` onto a
dedicated `/status` route. Top-bar `` links there.
diff --git a/app/layout.tsx b/app/layout.tsx
index eaf1cd8..9f6c937 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,6 +3,7 @@ import { IBM_Plex_Sans, IBM_Plex_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { AppNavigation } from "@/components/navigation/app-navigation";
+import { CommandPalette } from "@/components/navigation/command-palette";
import { TaglineFooter } from "@/components/branding/tagline-footer";
import { Toaster } from "sonner";
import { AuthProvider } from "@/components/auth/auth-provider";
@@ -57,6 +58,7 @@ export default function RootLayout({
{children}
+
diff --git a/components/navigation/app-navigation.tsx b/components/navigation/app-navigation.tsx
index a93b898..a4a6b59 100644
--- a/components/navigation/app-navigation.tsx
+++ b/components/navigation/app-navigation.tsx
@@ -315,6 +315,16 @@ export function AppNavigation() {
{/* Right Side Actions */}
+
diff --git a/components/navigation/command-palette.tsx b/components/navigation/command-palette.tsx
new file mode 100644
index 0000000..7dc2080
--- /dev/null
+++ b/components/navigation/command-palette.tsx
@@ -0,0 +1,298 @@
+/* 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.
+
+
+
+
+ );
+}
diff --git a/components/ui/command.tsx b/components/ui/command.tsx
new file mode 100644
index 0000000..8fe3ccb
--- /dev/null
+++ b/components/ui/command.tsx
@@ -0,0 +1,184 @@
+"use client"
+
+import * as React from "react"
+import { Command as CommandPrimitive } from "cmdk"
+import { SearchIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+
+function Command({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandDialog({
+ title = "Command Palette",
+ description = "Search for a command to run...",
+ children,
+ className,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ title?: string
+ description?: string
+ className?: string
+ showCloseButton?: boolean
+}) {
+ return (
+
+ )
+}
+
+function CommandInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function CommandList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandEmpty({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CommandShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/package-lock.json b/package-lock.json
index e133403..1560de8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,7 @@
"better-auth": "^1.4.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"ioredis": "^5.9.0",
@@ -8433,6 +8434,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
"node_modules/code-block-writer": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
diff --git a/package.json b/package.json
index 71d3fcb..2ca5052 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"better-auth": "^1.4.10",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"ioredis": "^5.9.0",