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:
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
88
frontend/src/components/layout/CommandBar.tsx
Normal file
88
frontend/src/components/layout/CommandBar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
175
frontend/src/pages/dashboard.astro
Normal file
175
frontend/src/pages/dashboard.astro
Normal 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>
|
||||||
Reference in New Issue
Block a user