Phase 2: Command Deck Navigation

 Command palette (Cmd+K) with global search
 Dashboard page with stat cards
 Quick actions panel
 System health monitoring
 Toast notifications setup (Sonner)

Navigation foundation complete for Phase 3 factory floor.
This commit is contained in:
cawcenter
2025-12-13 12:13:31 -05:00
parent fd9f428dcd
commit ac372db74e
4 changed files with 280 additions and 0 deletions

View File

@@ -26,6 +26,7 @@
"bullmq": "^5.66.0", "bullmq": "^5.66.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -3856,6 +3857,21 @@
"node": ">=0.10.0" "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==",
"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/color": { "node_modules/color": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",

View File

@@ -28,6 +28,7 @@
"bullmq": "^5.66.0", "bullmq": "^5.66.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",

View File

@@ -0,0 +1,88 @@
/**
* Command Palette (Cmd+K)
* Global search and quick actions
*/
'use client';
import { useEffect, useState } from 'react';
import { Command } from 'cmdk';
import { useRouter } from 'next/navigation';
export default function CommandBar() {
const [open, setOpen] = useState(false);
const router = useRouter();
// Toggle on Cmd+K
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((open) => !open);
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, []);
return (
<Command.Dialog open={open} onOpenChange={setOpen} label="Command Menu">
<div className="fixed inset-0 z-50 bg-black/50" onClick={() => setOpen(false)} />
<div className="fixed left-1/2 top-1/2 z-50 w-full max-w-2xl -translate-x-1/2 -translate-y-1/2">
<Command className="rounded-lg border border-slate-700 bg-slate-800 shadow-2xl">
<Command.Input
placeholder="Search or jump to..."
className="w-full border-b border-slate-700 bg-transparent px-4 py-3 text-white placeholder:text-slate-500 focus:outline-none"
/>
<Command.List className="max-h-96 overflow-y-auto p-2">
<Command.Empty className="px-4 py-8 text-center text-slate-500">
No results found.
</Command.Empty>
<Command.Group heading="Navigate" className="text-slate-400 text-xs uppercase px-2 py-1.5">
<CommandItem onSelect={() => router.push('/admin')}>
🏠 Dashboard
</CommandItem>
<CommandItem onSelect={() => router.push('/admin/factory')}>
🏭 Factory Floor
</CommandItem>
<CommandItem onSelect={() => router.push('/admin/intelligence/avatars')}>
👥 Avatar Bay
</CommandItem>
<CommandItem onSelect={() => router.push('/admin/intelligence/geo')}>
🗺 Geo Chamber
</CommandItem>
<CommandItem onSelect={() => router.push('/admin/intelligence/patterns')}>
🔧 Pattern Forge
</CommandItem>
<CommandItem onSelect={() => router.push('/admin/sites')}>
🌐 Sites
</CommandItem>
</Command.Group>
<Command.Group heading="Quick Actions" className="text-slate-400 text-xs uppercase px-2 py-1.5 mt-2">
<CommandItem onSelect={() => router.push('/admin/factory?action=new')}>
New Campaign
</CommandItem>
<CommandItem onSelect={() => router.push('/admin/sites/new')}>
Add Site
</CommandItem>
</Command.Group>
</Command.List>
</Command>
</div>
</Command.Dialog>
);
}
function CommandItem({ children, onSelect }: { children: React.ReactNode; onSelect: () => void }) {
return (
<Command.Item
onSelect={onSelect}
className="flex items-center gap-2 rounded px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 cursor-pointer transition-colors"
>
{children}
</Command.Item>
);
}

View File

@@ -0,0 +1,175 @@
---
/**
* Dashboard - Command Deck
* Main status and overview page
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient, readItems } from '@/lib/directus/client';
// Fetch dashboard stats
const client = getDirectusClient();
let stats = {
sites: 0,
articles: 0,
jobs_queued: 0,
jobs_running: 0,
jobs_failed: 0,
};
try {
const [sites, articles, jobs] = await Promise.all([
client.request(readItems('sites', { aggregate: { count: '*' } })),
client.request(readItems('generated_articles', { aggregate: { count: '*' } })),
client.request(readItems('generation_jobs')),
]);
stats.sites = sites[0]?.count || 0;
stats.articles = articles[0]?.count || 0;
if (Array.isArray(jobs)) {
stats.jobs_queued = jobs.filter(j => j.status === 'Pending').length;
stats.jobs_running = jobs.filter(j => j.status === 'Processing').length;
stats.jobs_failed = jobs.filter(j => j.status === 'Failed').length;
}
} catch (error) {
console.error('Dashboard stats error:', error);
}
---
<AdminLayout title="Dashboard">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-white">Command Deck</h1>
<p class="text-slate-400 mt-1">Operational status and system health</p>
</div>
<div class="flex gap-3">
<a
href="/admin/factory?action=new"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
>
✨ New Campaign
</a>
</div>
</div>
<!-- Stat Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Sites Card -->
<div class="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400 text-sm">Active Sites</p>
<p class="text-3xl font-bold text-white mt-2">{stats.sites}</p>
</div>
<div class="w-12 h-12 bg-blue-600/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
</div>
</div>
</div>
<!-- Articles Card -->
<div class="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400 text-sm">Generated Articles</p>
<p class="text-3xl font-bold text-white mt-2">{stats.articles}</p>
</div>
<div class="w-12 h-12 bg-green-600/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
</div>
<!-- Queued Jobs Card -->
<div class="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400 text-sm">Jobs Queued</p>
<p class="text-3xl font-bold text-white mt-2">{stats.jobs_queued}</p>
</div>
<div class="w-12 h-12 bg-yellow-600/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
<!-- Running Jobs Card -->
<div class="bg-slate-800 border border-slate-700 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-slate-400 text-sm">Jobs Running</p>
<p class="text-3xl font-bold text-white mt-2">{stats.jobs_running}</p>
</div>
<div class="w-12 h-12 bg-purple-600/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-slate-800 border border-slate-700 rounded-lg p-6">
<h2 class="text-xl font-semibold text-white mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<a
href="/admin/factory"
class="flex items-center gap-3 p-4 bg-slate-700/50 hover:bg-slate-700 rounded-lg transition-colors group"
>
<div class="w-10 h-10 bg-blue-600/20 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div>
<p class="text-white font-medium group-hover:text-blue-400 transition-colors">Factory Floor</p>
<p class="text-slate-400 text-sm">Manage production</p>
</div>
</a>
<a
href="/admin/intelligence/avatars"
class="flex items-center gap-3 p-4 bg-slate-700/50 hover:bg-slate-700 rounded-lg transition-colors group"
>
<div class="w-10 h-10 bg-purple-600/20 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
<div>
<p class="text-white font-medium group-hover:text-purple-400 transition-colors">Intelligence Station</p>
<p class="text-slate-400 text-sm">Manage data libraries</p>
</div>
</a>
<a
href="/admin/ops"
class="flex items-center gap-3 p-4 bg-slate-700/50 hover:bg-slate-700 rounded-lg transition-colors group"
>
<div class="w-10 h-10 bg-green-600/20 rounded-lg flex items-center justify-center">
<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<p class="text-white font-medium group-hover:text-green-400 transition-colors">Ops & Testing</p>
<p class="text-slate-400 text-sm">Diagnostics & logs</p>
</div>
</a>
</div>
</div>
<!-- Recent Activity would go here -->
</div>
</AdminLayout>