@gravito/zenith 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +88 -0
- package/BATCH_OPERATIONS_IMPLEMENTATION.md +159 -0
- package/DEMO.md +156 -0
- package/DEPLOYMENT.md +157 -0
- package/DOCS_INTERNAL.md +73 -0
- package/Dockerfile +46 -0
- package/Dockerfile.demo-worker +29 -0
- package/EVOLUTION_BLUEPRINT.md +112 -0
- package/JOBINSPECTOR_SCROLL_FIX.md +152 -0
- package/PULSE_IMPLEMENTATION_PLAN.md +111 -0
- package/QUICK_TEST_GUIDE.md +72 -0
- package/README.md +33 -0
- package/ROADMAP.md +85 -0
- package/TESTING_BATCH_OPERATIONS.md +252 -0
- package/bin/flux-console.ts +2 -0
- package/dist/bin.js +108196 -0
- package/dist/client/assets/index-DGYEwTDL.css +1 -0
- package/dist/client/assets/index-oyTdySX0.js +421 -0
- package/dist/client/index.html +13 -0
- package/dist/server/index.js +108191 -0
- package/docker-compose.yml +40 -0
- package/docs/integrations/LARAVEL.md +207 -0
- package/package.json +50 -0
- package/postcss.config.js +6 -0
- package/scripts/flood-logs.ts +21 -0
- package/scripts/seed.ts +213 -0
- package/scripts/verify-throttle.ts +45 -0
- package/scripts/worker.ts +123 -0
- package/src/bin.ts +6 -0
- package/src/client/App.tsx +70 -0
- package/src/client/Layout.tsx +644 -0
- package/src/client/Sidebar.tsx +102 -0
- package/src/client/ThroughputChart.tsx +135 -0
- package/src/client/WorkerStatus.tsx +170 -0
- package/src/client/components/ConfirmDialog.tsx +103 -0
- package/src/client/components/JobInspector.tsx +524 -0
- package/src/client/components/LogArchiveModal.tsx +383 -0
- package/src/client/components/NotificationBell.tsx +203 -0
- package/src/client/components/Toaster.tsx +80 -0
- package/src/client/components/UserProfileDropdown.tsx +177 -0
- package/src/client/contexts/AuthContext.tsx +93 -0
- package/src/client/contexts/NotificationContext.tsx +103 -0
- package/src/client/index.css +174 -0
- package/src/client/index.html +12 -0
- package/src/client/main.tsx +15 -0
- package/src/client/pages/LoginPage.tsx +153 -0
- package/src/client/pages/MetricsPage.tsx +408 -0
- package/src/client/pages/OverviewPage.tsx +511 -0
- package/src/client/pages/QueuesPage.tsx +372 -0
- package/src/client/pages/SchedulesPage.tsx +531 -0
- package/src/client/pages/SettingsPage.tsx +449 -0
- package/src/client/pages/WorkersPage.tsx +316 -0
- package/src/client/pages/index.ts +7 -0
- package/src/client/utils.ts +6 -0
- package/src/server/index.ts +556 -0
- package/src/server/middleware/auth.ts +127 -0
- package/src/server/services/AlertService.ts +160 -0
- package/src/server/services/QueueService.ts +828 -0
- package/tailwind.config.js +73 -0
- package/tests/placeholder.test.ts +7 -0
- package/tsconfig.json +38 -0
- package/tsconfig.node.json +12 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
// ... imports ...
|
|
2
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { AnimatePresence, motion } from 'framer-motion'
|
|
4
|
+
import {
|
|
5
|
+
Activity,
|
|
6
|
+
BarChart3,
|
|
7
|
+
Briefcase,
|
|
8
|
+
Command,
|
|
9
|
+
HardDrive,
|
|
10
|
+
LayoutDashboard,
|
|
11
|
+
ListTree,
|
|
12
|
+
LogOut,
|
|
13
|
+
Moon,
|
|
14
|
+
RefreshCcw,
|
|
15
|
+
Search,
|
|
16
|
+
Settings,
|
|
17
|
+
ShieldCheck,
|
|
18
|
+
Sun,
|
|
19
|
+
Trash2,
|
|
20
|
+
Zap,
|
|
21
|
+
} from 'lucide-react'
|
|
22
|
+
import type * as React from 'react'
|
|
23
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
24
|
+
import { useNavigate } from 'react-router-dom'
|
|
25
|
+
import { NotificationBell } from './components/NotificationBell'
|
|
26
|
+
import { Toaster } from './components/Toaster'
|
|
27
|
+
import { UserProfileDropdown } from './components/UserProfileDropdown'
|
|
28
|
+
import { useAuth } from './contexts/AuthContext'
|
|
29
|
+
import { Sidebar } from './Sidebar'
|
|
30
|
+
import { cn } from './utils'
|
|
31
|
+
|
|
32
|
+
interface LayoutProps {
|
|
33
|
+
children: React.ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CommandItem {
|
|
37
|
+
id: string
|
|
38
|
+
title: string
|
|
39
|
+
description: string
|
|
40
|
+
icon: React.ReactNode
|
|
41
|
+
action: () => void
|
|
42
|
+
category: 'Navigation' | 'System' | 'Action'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function Layout({ children }: LayoutProps) {
|
|
46
|
+
const navigate = useNavigate()
|
|
47
|
+
const queryClient = useQueryClient()
|
|
48
|
+
const { isAuthEnabled, logout } = useAuth()
|
|
49
|
+
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
|
|
50
|
+
if (typeof window !== 'undefined') {
|
|
51
|
+
return (localStorage.getItem('theme') as 'light' | 'dark') || 'light'
|
|
52
|
+
}
|
|
53
|
+
return 'light'
|
|
54
|
+
})
|
|
55
|
+
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false)
|
|
56
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
57
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
58
|
+
const [health, setHealth] = useState(99.9)
|
|
59
|
+
const [systemStatus, setSystemStatus] = useState<Record<string, any>>({})
|
|
60
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(true)
|
|
61
|
+
|
|
62
|
+
// Initial System Status Fetch
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
fetch('/api/system/status')
|
|
65
|
+
.then((res) => res.json())
|
|
66
|
+
.then(setSystemStatus)
|
|
67
|
+
.catch(() => {})
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
// Global SSE Stream Manager
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
console.log('[Zenith] Establishing Global Event Stream...')
|
|
73
|
+
const ev = new EventSource('/api/logs/stream')
|
|
74
|
+
|
|
75
|
+
ev.addEventListener('log', (e) => {
|
|
76
|
+
try {
|
|
77
|
+
const data = JSON.parse(e.data)
|
|
78
|
+
window.dispatchEvent(new CustomEvent('flux-log-update', { detail: data }))
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('SSE Log Error', err)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
ev.addEventListener('stats', (e) => {
|
|
85
|
+
try {
|
|
86
|
+
const data = JSON.parse(e.data)
|
|
87
|
+
window.dispatchEvent(new CustomEvent('flux-stats-update', { detail: data }))
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error('SSE Stats Error', err)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
ev.onerror = (err) => {
|
|
94
|
+
console.error('[Zenith] SSE Connection Error', err)
|
|
95
|
+
ev.close()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
console.log('[Zenith] Closing Global Event Stream')
|
|
100
|
+
ev.close()
|
|
101
|
+
}
|
|
102
|
+
}, [])
|
|
103
|
+
|
|
104
|
+
// Fetch Queues for search (once)
|
|
105
|
+
const [queueData, setQueueData] = useState<{ queues: any[] }>({ queues: [] })
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
fetch('/api/queues')
|
|
108
|
+
.then((res) => res.json())
|
|
109
|
+
.then(setQueueData)
|
|
110
|
+
.catch(() => {})
|
|
111
|
+
|
|
112
|
+
// Optional: Listen to global stats if available (from OverviewPage) to keep queue stats fresh in command palette
|
|
113
|
+
const handler = (e: Event) => {
|
|
114
|
+
const customEvent = e as CustomEvent
|
|
115
|
+
if (customEvent.detail?.queues) {
|
|
116
|
+
setQueueData({ queues: customEvent.detail.queues })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
window.addEventListener('flux-stats-update', handler)
|
|
120
|
+
return () => window.removeEventListener('flux-stats-update', handler)
|
|
121
|
+
}, [])
|
|
122
|
+
|
|
123
|
+
// Debounced job search
|
|
124
|
+
const [debouncedQuery, setDebouncedQuery] = useState('')
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
if (searchQuery.length >= 2) {
|
|
129
|
+
setDebouncedQuery(searchQuery)
|
|
130
|
+
} else {
|
|
131
|
+
setDebouncedQuery('')
|
|
132
|
+
}
|
|
133
|
+
}, 300)
|
|
134
|
+
return () => clearTimeout(timer)
|
|
135
|
+
}, [searchQuery])
|
|
136
|
+
|
|
137
|
+
// Search jobs (Real-time and Archive)
|
|
138
|
+
const { data: searchResults } = useQuery<{ results: any[]; archiveResults?: any[] }>({
|
|
139
|
+
queryKey: ['job-search', debouncedQuery],
|
|
140
|
+
queryFn: async () => {
|
|
141
|
+
const [realtime, archive] = await Promise.all([
|
|
142
|
+
fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}&limit=10`).then((res) =>
|
|
143
|
+
res.json()
|
|
144
|
+
),
|
|
145
|
+
fetch(`/api/archive/search?q=${encodeURIComponent(debouncedQuery)}&limit=10`).then((res) =>
|
|
146
|
+
res.json()
|
|
147
|
+
),
|
|
148
|
+
])
|
|
149
|
+
return {
|
|
150
|
+
results: realtime.results || [],
|
|
151
|
+
archiveResults: archive.results || [],
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
enabled: debouncedQuery.length >= 2,
|
|
155
|
+
staleTime: 5000,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const root = window.document.documentElement
|
|
160
|
+
if (theme === 'dark') {
|
|
161
|
+
root.classList.add('dark')
|
|
162
|
+
} else {
|
|
163
|
+
root.classList.remove('dark')
|
|
164
|
+
}
|
|
165
|
+
localStorage.setItem('theme', theme)
|
|
166
|
+
}, [theme])
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const interval = setInterval(() => {
|
|
170
|
+
setHealth((prev) => {
|
|
171
|
+
const jitter = (Math.random() - 0.5) * 0.1
|
|
172
|
+
return Math.min(100, Math.max(98.5, prev + jitter))
|
|
173
|
+
})
|
|
174
|
+
}, 3000)
|
|
175
|
+
return () => clearInterval(interval)
|
|
176
|
+
}, [])
|
|
177
|
+
|
|
178
|
+
const toggleTheme = () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
|
|
179
|
+
|
|
180
|
+
const retryAllFailed = async () => {
|
|
181
|
+
const queues = queueData?.queues || []
|
|
182
|
+
for (const q of queues) {
|
|
183
|
+
if (q.failed > 0) {
|
|
184
|
+
await fetch(`/api/queues/${q.name}/retry-all-failed`, { method: 'POST' })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const baseCommands: CommandItem[] = [
|
|
191
|
+
{
|
|
192
|
+
id: 'nav-overview',
|
|
193
|
+
title: 'Go to Overview',
|
|
194
|
+
description: 'Navigate to system dashboard',
|
|
195
|
+
icon: <LayoutDashboard size={18} />,
|
|
196
|
+
category: 'Navigation',
|
|
197
|
+
action: () => navigate('/'),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'nav-queues',
|
|
201
|
+
title: 'Go to Queues',
|
|
202
|
+
description: 'Manage processing queues',
|
|
203
|
+
icon: <ListTree size={18} />,
|
|
204
|
+
category: 'Navigation',
|
|
205
|
+
action: () => navigate('/queues'),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'nav-workers',
|
|
209
|
+
title: 'Go to Workers',
|
|
210
|
+
description: 'Monitor worker nodes',
|
|
211
|
+
icon: <HardDrive size={18} />,
|
|
212
|
+
category: 'Navigation',
|
|
213
|
+
action: () => navigate('/workers'),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'nav-metrics',
|
|
217
|
+
title: 'Go to Metrics',
|
|
218
|
+
description: 'View system analytics',
|
|
219
|
+
icon: <BarChart3 size={18} />,
|
|
220
|
+
category: 'Navigation',
|
|
221
|
+
action: () => navigate('/metrics'),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'nav-settings',
|
|
225
|
+
title: 'Go to Settings',
|
|
226
|
+
description: 'Configure console preferences',
|
|
227
|
+
icon: <Settings size={18} />,
|
|
228
|
+
category: 'Navigation',
|
|
229
|
+
action: () => navigate('/settings'),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'act-retry-all',
|
|
233
|
+
title: 'Retry All Failed Jobs',
|
|
234
|
+
description: 'Recover all critical failures across all queues',
|
|
235
|
+
icon: <RefreshCcw size={18} />,
|
|
236
|
+
category: 'Action',
|
|
237
|
+
action: retryAllFailed,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'sys-theme',
|
|
241
|
+
title: `Switch to ${theme === 'dark' ? 'Light' : 'Dark'} Mode`,
|
|
242
|
+
description: 'Toggle system visual appearance',
|
|
243
|
+
icon: theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />,
|
|
244
|
+
category: 'System',
|
|
245
|
+
action: toggleTheme,
|
|
246
|
+
},
|
|
247
|
+
...(isAuthEnabled
|
|
248
|
+
? [
|
|
249
|
+
{
|
|
250
|
+
id: 'sys-logout',
|
|
251
|
+
title: 'Logout',
|
|
252
|
+
description: 'Sign out from the console',
|
|
253
|
+
icon: <LogOut size={18} />,
|
|
254
|
+
category: 'System' as const,
|
|
255
|
+
action: logout,
|
|
256
|
+
},
|
|
257
|
+
]
|
|
258
|
+
: []),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
const queueCommands: CommandItem[] = (queueData?.queues || []).map((q: any) => ({
|
|
262
|
+
id: `queue-${q.name}`,
|
|
263
|
+
title: `Queue: ${q.name}`,
|
|
264
|
+
description: `${q.waiting} waiting, ${q.failed} failed`,
|
|
265
|
+
icon: <ListTree size={18} />,
|
|
266
|
+
category: 'Navigation',
|
|
267
|
+
action: () => {
|
|
268
|
+
navigate('/queues')
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
window.dispatchEvent(new CustomEvent('select-queue', { detail: q.name }))
|
|
271
|
+
}, 100)
|
|
272
|
+
},
|
|
273
|
+
}))
|
|
274
|
+
|
|
275
|
+
const actionCommands: CommandItem[] = [
|
|
276
|
+
{
|
|
277
|
+
id: 'act-clear-logs',
|
|
278
|
+
title: 'Clear All Logs',
|
|
279
|
+
description: 'Flush temporary log buffer in UI',
|
|
280
|
+
icon: <Trash2 size={18} />,
|
|
281
|
+
category: 'Action',
|
|
282
|
+
action: () => {
|
|
283
|
+
window.dispatchEvent(new CustomEvent('clear-logs'))
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
// Dynamic job search results
|
|
289
|
+
const jobCommands: CommandItem[] = useMemo(() => {
|
|
290
|
+
const combined = [
|
|
291
|
+
...(searchResults?.results || []),
|
|
292
|
+
...(searchResults?.archiveResults || []).map((j: any) => ({ ...j, _archived: true })),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
if (!combined.length) {
|
|
296
|
+
return []
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return combined.slice(0, 15).map((job: any) => ({
|
|
300
|
+
id: `job-${job._queue}-${job.id}-${job._archived ? 'arch' : 'live'}`,
|
|
301
|
+
title: `Job: ${job.id || 'Unknown'}`,
|
|
302
|
+
description: `${job._queue} • ${job.status || job._type}${job._archived ? ' • ARCHIVED' : ''} • ${job.name || 'No name'}`,
|
|
303
|
+
icon: job._archived ? <HardDrive size={18} /> : <Briefcase size={18} />,
|
|
304
|
+
category: 'Action' as const,
|
|
305
|
+
action: () => {
|
|
306
|
+
navigate('/queues')
|
|
307
|
+
setTimeout(() => {
|
|
308
|
+
window.dispatchEvent(new CustomEvent('select-queue', { detail: job._queue }))
|
|
309
|
+
if (job._archived) {
|
|
310
|
+
// Future-proof: trigger archive view opening
|
|
311
|
+
window.dispatchEvent(
|
|
312
|
+
new CustomEvent('inspect-job', { detail: { queue: job._queue, job } })
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}, 100)
|
|
316
|
+
},
|
|
317
|
+
}))
|
|
318
|
+
}, [searchResults, navigate])
|
|
319
|
+
|
|
320
|
+
const commands = [...baseCommands, ...actionCommands, ...queueCommands, ...jobCommands]
|
|
321
|
+
|
|
322
|
+
const filteredCommands = commands.filter(
|
|
323
|
+
(cmd) =>
|
|
324
|
+
cmd.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
325
|
+
cmd.description.toLowerCase().includes(searchQuery.toLowerCase())
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
330
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
331
|
+
e.preventDefault()
|
|
332
|
+
setIsCommandPaletteOpen((prev) => !prev)
|
|
333
|
+
}
|
|
334
|
+
if (e.key === 'Escape') {
|
|
335
|
+
setIsCommandPaletteOpen(false)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
339
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
340
|
+
}, [])
|
|
341
|
+
|
|
342
|
+
const handleSelect = (cmd: CommandItem) => {
|
|
343
|
+
cmd.action()
|
|
344
|
+
setIsCommandPaletteOpen(false)
|
|
345
|
+
setSearchQuery('')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div className="flex h-screen bg-background text-foreground overflow-hidden transition-colors duration-300">
|
|
350
|
+
<motion.aside
|
|
351
|
+
initial={false}
|
|
352
|
+
animate={{ width: isSidebarOpen ? 260 : 80 }}
|
|
353
|
+
className="border-r border-border/40 bg-card/50 backdrop-blur-xl flex flex-col z-50 transition-all duration-300 ease-[0.22, 1, 0.36, 1]"
|
|
354
|
+
>
|
|
355
|
+
<div className="h-16 flex items-center px-6 border-b border-border/40 bg-card/80">
|
|
356
|
+
<div className="flex items-center gap-3 overflow-hidden">
|
|
357
|
+
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center shrink-0 shadow-lg shadow-primary/20">
|
|
358
|
+
<Zap className="text-primary-foreground fill-current" size={18} />
|
|
359
|
+
</div>
|
|
360
|
+
<motion.div
|
|
361
|
+
animate={{ opacity: isSidebarOpen ? 1 : 0 }}
|
|
362
|
+
className="flex flex-col min-w-[140px]"
|
|
363
|
+
>
|
|
364
|
+
<span className="font-extrabold text-lg tracking-tight leading-none bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/60">
|
|
365
|
+
Zenith
|
|
366
|
+
</span>
|
|
367
|
+
<span className="text-[9px] font-bold text-muted-foreground uppercase tracking-[0.2em]">
|
|
368
|
+
Control Plane
|
|
369
|
+
</span>
|
|
370
|
+
</motion.div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
<Sidebar
|
|
374
|
+
collapsed={!isSidebarOpen}
|
|
375
|
+
toggleCollapse={() => setIsSidebarOpen(!isSidebarOpen)}
|
|
376
|
+
/>
|
|
377
|
+
</motion.aside>
|
|
378
|
+
|
|
379
|
+
<main className="flex-1 flex flex-col relative overflow-hidden scanline">
|
|
380
|
+
{/* Top Header */}
|
|
381
|
+
<header className="h-16 border-b bg-card/50 backdrop-blur-md flex items-center justify-between px-8 sticky top-0 z-10 transition-colors">
|
|
382
|
+
<div className="flex items-center gap-6 flex-1">
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
className="relative max-w-md w-full group cursor-pointer outline-none block text-left"
|
|
386
|
+
onClick={() => setIsCommandPaletteOpen(true)}
|
|
387
|
+
>
|
|
388
|
+
<Search
|
|
389
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground group-hover:text-primary transition-colors"
|
|
390
|
+
size={18}
|
|
391
|
+
/>
|
|
392
|
+
<div className="w-full bg-muted/40 border border-border/50 rounded-xl py-2 pl-10 pr-4 text-sm text-muted-foreground/60 font-medium flex justify-between items-center transition-all hover:bg-muted/60 hover:border-primary/20">
|
|
393
|
+
<span>Search or command...</span>
|
|
394
|
+
<div className="flex gap-1 group-hover:scale-105 transition-transform">
|
|
395
|
+
<kbd className="bg-muted px-1.5 py-0.5 rounded border text-[10px] font-black opacity-60">
|
|
396
|
+
⌘
|
|
397
|
+
</kbd>
|
|
398
|
+
<kbd className="bg-muted px-1.5 py-0.5 rounded border text-[10px] font-black opacity-60">
|
|
399
|
+
K
|
|
400
|
+
</kbd>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</button>
|
|
404
|
+
|
|
405
|
+
{/* System Integrity Indicator */}
|
|
406
|
+
<div className="hidden lg:flex items-center gap-3 px-4 py-1.5 rounded-full bg-primary/5 border border-primary/10 transition-all hover:bg-primary/10 hover:border-primary/20 cursor-default group">
|
|
407
|
+
<div className="relative flex items-center justify-center">
|
|
408
|
+
<ShieldCheck size={14} className="text-primary z-10" />
|
|
409
|
+
<div className="absolute w-3 h-3 bg-primary rounded-full glow-pulse"></div>
|
|
410
|
+
</div>
|
|
411
|
+
<div className="flex flex-col">
|
|
412
|
+
<span className="text-[9px] font-black uppercase tracking-[0.1em] text-primary/60 leading-none">
|
|
413
|
+
System Integrity
|
|
414
|
+
</span>
|
|
415
|
+
<span className="text-[11px] font-black tracking-tight leading-none">
|
|
416
|
+
{health.toFixed(1)}% Nominal
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div className="flex items-center gap-6">
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
onClick={toggleTheme}
|
|
426
|
+
className="p-2.5 hover:bg-muted rounded-xl text-muted-foreground hover:text-primary transition-all duration-300 active:scale-95 group relative"
|
|
427
|
+
title={theme === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode'}
|
|
428
|
+
>
|
|
429
|
+
{theme === 'light' ? (
|
|
430
|
+
<Moon size={20} className="group-hover:rotate-[15deg] transition-transform" />
|
|
431
|
+
) : (
|
|
432
|
+
<Sun
|
|
433
|
+
size={20}
|
|
434
|
+
className="group-hover:rotate-90 transition-transform text-yellow-500"
|
|
435
|
+
/>
|
|
436
|
+
)}
|
|
437
|
+
</button>
|
|
438
|
+
|
|
439
|
+
<NotificationBell />
|
|
440
|
+
|
|
441
|
+
<div className="h-8 w-[1px] bg-border/50"></div>
|
|
442
|
+
|
|
443
|
+
<UserProfileDropdown />
|
|
444
|
+
</div>
|
|
445
|
+
</header>
|
|
446
|
+
|
|
447
|
+
{/* Content Area */}
|
|
448
|
+
<div className="flex-1 overflow-y-auto p-8 scrollbar-thin">
|
|
449
|
+
<motion.div
|
|
450
|
+
initial={{ opacity: 0, scale: 0.98, filter: 'blur(10px)' }}
|
|
451
|
+
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
|
|
452
|
+
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
|
453
|
+
>
|
|
454
|
+
{children}
|
|
455
|
+
</motion.div>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
{/* Dynamic Status Bar (Ambient) */}
|
|
459
|
+
<footer className="h-7 border-t bg-card/80 backdrop-blur-md flex items-center justify-between px-6 z-10 transition-colors">
|
|
460
|
+
<div className="flex items-center gap-6 overflow-hidden">
|
|
461
|
+
<div className="flex items-center gap-2 border-r border-border/50 pr-4">
|
|
462
|
+
<span className="w-1.5 h-1.5 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.4)]"></span>
|
|
463
|
+
<span className="text-[10px] font-black text-muted-foreground/60 uppercase tracking-widest whitespace-nowrap">
|
|
464
|
+
Node: {systemStatus?.env || 'production-east-1'}
|
|
465
|
+
</span>
|
|
466
|
+
</div>
|
|
467
|
+
<div className="flex items-center gap-4 text-[9px] font-black text-muted-foreground/40 uppercase tracking-[0.2em] animate-in fade-in slide-in-from-left duration-1000">
|
|
468
|
+
<span className="flex items-center gap-1.5">
|
|
469
|
+
<Activity size={10} className="text-primary/40" /> Latency: 4ms
|
|
470
|
+
</span>
|
|
471
|
+
<span className="hidden sm:inline border-l border-border/30 pl-4 text-primary">
|
|
472
|
+
RAM: {systemStatus?.memory?.rss || '...'} /{' '}
|
|
473
|
+
{systemStatus?.memory?.total || '4.00 GB'}
|
|
474
|
+
</span>
|
|
475
|
+
<span className="hidden md:inline border-l border-border/30 pl-4 uppercase">
|
|
476
|
+
Engine: {systemStatus?.engine || 'v2.4.1-beta'}
|
|
477
|
+
</span>
|
|
478
|
+
<span className="hidden lg:inline border-l border-border/30 pl-4 lowercase">
|
|
479
|
+
v: {systemStatus?.node || '...'}
|
|
480
|
+
</span>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="flex items-center gap-4 pl-4 bg-gradient-to-l from-card via-card to-transparent text-right">
|
|
484
|
+
<div className="flex items-center gap-2">
|
|
485
|
+
<div className="w-8 h-1 bg-muted rounded-full overflow-hidden">
|
|
486
|
+
<motion.div
|
|
487
|
+
animate={{ x: [-20, 20] }}
|
|
488
|
+
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
|
|
489
|
+
className="w-4 h-full bg-primary/40"
|
|
490
|
+
/>
|
|
491
|
+
</div>
|
|
492
|
+
<span className="text-[9px] font-black text-primary/60 uppercase tracking-widest">
|
|
493
|
+
Bus Traffic
|
|
494
|
+
</span>
|
|
495
|
+
</div>
|
|
496
|
+
<span className="font-mono text-[10px] text-muted-foreground/60 tabular-nums lowercase">
|
|
497
|
+
{new Date().toISOString().split('T')[1]?.split('.')[0] || ''} utc
|
|
498
|
+
</span>
|
|
499
|
+
</div>
|
|
500
|
+
</footer>
|
|
501
|
+
</main>
|
|
502
|
+
|
|
503
|
+
{/* Command Palette Modal */}
|
|
504
|
+
<AnimatePresence>
|
|
505
|
+
{isCommandPaletteOpen && (
|
|
506
|
+
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-24 px-4">
|
|
507
|
+
<motion.div
|
|
508
|
+
initial={{ opacity: 0 }}
|
|
509
|
+
animate={{ opacity: 1 }}
|
|
510
|
+
exit={{ opacity: 0 }}
|
|
511
|
+
className="absolute inset-0 bg-black/60 backdrop-blur-md"
|
|
512
|
+
onClick={() => setIsCommandPaletteOpen(false)}
|
|
513
|
+
/>
|
|
514
|
+
<motion.div
|
|
515
|
+
initial={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
516
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
517
|
+
exit={{ opacity: 0, scale: 0.95, y: -20 }}
|
|
518
|
+
className="relative w-full max-w-2xl bg-card border-border/50 border rounded-3xl shadow-2xl overflow-hidden scanline"
|
|
519
|
+
>
|
|
520
|
+
<div className="p-6 border-b flex items-center gap-4 bg-muted/5">
|
|
521
|
+
<Command className="text-primary animate-pulse" size={24} />
|
|
522
|
+
<input
|
|
523
|
+
type="text"
|
|
524
|
+
placeholder="Execute command or navigate..."
|
|
525
|
+
className="flex-1 bg-transparent border-none outline-none text-lg font-bold placeholder:text-muted-foreground/30"
|
|
526
|
+
value={searchQuery}
|
|
527
|
+
onChange={(e) => {
|
|
528
|
+
setSearchQuery(e.target.value)
|
|
529
|
+
setSelectedIndex(0)
|
|
530
|
+
}}
|
|
531
|
+
onKeyDown={(e) => {
|
|
532
|
+
if (e.key === 'ArrowDown') {
|
|
533
|
+
e.preventDefault()
|
|
534
|
+
setSelectedIndex((prev) => (prev + 1) % filteredCommands.length)
|
|
535
|
+
} else if (e.key === 'ArrowUp') {
|
|
536
|
+
e.preventDefault()
|
|
537
|
+
setSelectedIndex(
|
|
538
|
+
(prev) => (prev - 1 + filteredCommands.length) % filteredCommands.length
|
|
539
|
+
)
|
|
540
|
+
} else if (e.key === 'Enter' && filteredCommands[selectedIndex]) {
|
|
541
|
+
handleSelect(filteredCommands[selectedIndex]!)
|
|
542
|
+
}
|
|
543
|
+
}}
|
|
544
|
+
/>
|
|
545
|
+
<div className="flex items-center gap-2 px-2 py-1 rounded-lg bg-muted border text-[9px] font-black text-muted-foreground/60 uppercase">
|
|
546
|
+
ESC to close
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
<div className="max-h-[400px] overflow-y-auto p-2 custom-scrollbar">
|
|
550
|
+
{filteredCommands.length === 0 ? (
|
|
551
|
+
<div className="py-12 text-center text-muted-foreground/40 space-y-2">
|
|
552
|
+
<Activity size={32} className="mx-auto opacity-20" />
|
|
553
|
+
<p className="text-xs font-black uppercase tracking-widest">
|
|
554
|
+
No matching commands found
|
|
555
|
+
</p>
|
|
556
|
+
</div>
|
|
557
|
+
) : (
|
|
558
|
+
<div className="space-y-1">
|
|
559
|
+
{filteredCommands.map((cmd, i) => (
|
|
560
|
+
<button
|
|
561
|
+
type="button"
|
|
562
|
+
key={cmd.id}
|
|
563
|
+
className={cn(
|
|
564
|
+
'w-full flex items-center justify-between p-4 rounded-2xl transition-all cursor-pointer group/cmd outline-none',
|
|
565
|
+
i === selectedIndex
|
|
566
|
+
? 'bg-primary shadow-lg shadow-primary/20 -translate-x-1'
|
|
567
|
+
: 'hover:bg-muted'
|
|
568
|
+
)}
|
|
569
|
+
onClick={() => handleSelect(cmd)}
|
|
570
|
+
onMouseEnter={() => setSelectedIndex(i)}
|
|
571
|
+
>
|
|
572
|
+
<div className="flex items-center gap-4">
|
|
573
|
+
<div
|
|
574
|
+
className={cn(
|
|
575
|
+
'w-10 h-10 rounded-xl flex items-center justify-center transition-colors',
|
|
576
|
+
i === selectedIndex
|
|
577
|
+
? 'bg-white/20 text-white'
|
|
578
|
+
: 'bg-muted text-primary'
|
|
579
|
+
)}
|
|
580
|
+
>
|
|
581
|
+
{cmd.icon}
|
|
582
|
+
</div>
|
|
583
|
+
<div>
|
|
584
|
+
<p
|
|
585
|
+
className={cn(
|
|
586
|
+
'text-sm font-black tracking-tight',
|
|
587
|
+
i === selectedIndex ? 'text-white' : 'text-foreground'
|
|
588
|
+
)}
|
|
589
|
+
>
|
|
590
|
+
{cmd.title}
|
|
591
|
+
</p>
|
|
592
|
+
<p
|
|
593
|
+
className={cn(
|
|
594
|
+
'text-[10px] font-bold uppercase tracking-widest opacity-60',
|
|
595
|
+
i === selectedIndex ? 'text-white/80' : 'text-muted-foreground'
|
|
596
|
+
)}
|
|
597
|
+
>
|
|
598
|
+
{cmd.description}
|
|
599
|
+
</p>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
<div className="flex items-center gap-2">
|
|
603
|
+
<span
|
|
604
|
+
className={cn(
|
|
605
|
+
'text-[9px] font-black uppercase tracking-widest px-2 py-1 rounded-md',
|
|
606
|
+
i === selectedIndex
|
|
607
|
+
? 'bg-white/20 text-white'
|
|
608
|
+
: 'bg-muted text-muted-foreground'
|
|
609
|
+
)}
|
|
610
|
+
>
|
|
611
|
+
{cmd.category}
|
|
612
|
+
</span>
|
|
613
|
+
{i === selectedIndex && (
|
|
614
|
+
<kbd className="bg-white/20 px-1.5 py-0.5 rounded text-[10px] text-white">
|
|
615
|
+
↵
|
|
616
|
+
</kbd>
|
|
617
|
+
)}
|
|
618
|
+
</div>
|
|
619
|
+
</button>
|
|
620
|
+
))}
|
|
621
|
+
</div>
|
|
622
|
+
)}
|
|
623
|
+
</div>
|
|
624
|
+
<div className="p-4 border-t bg-muted/5 flex justify-between items-center px-6">
|
|
625
|
+
<div className="flex gap-4">
|
|
626
|
+
<div className="flex items-center gap-1.5 text-[9px] font-black text-muted-foreground/40 uppercase">
|
|
627
|
+
<kbd className="bg-muted px-1.5 py-0.5 rounded border">↑↓</kbd> to navigate
|
|
628
|
+
</div>
|
|
629
|
+
<div className="flex items-center gap-1.5 text-[9px] font-black text-muted-foreground/40 uppercase">
|
|
630
|
+
<kbd className="bg-muted px-1.5 py-0.5 rounded border">↵</kbd> to select
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
<span className="text-[9px] font-black text-primary/40 uppercase tracking-[0.2em]">
|
|
634
|
+
Gravito Zenith v1.0
|
|
635
|
+
</span>
|
|
636
|
+
</div>
|
|
637
|
+
</motion.div>
|
|
638
|
+
</div>
|
|
639
|
+
)}
|
|
640
|
+
</AnimatePresence>
|
|
641
|
+
<Toaster />
|
|
642
|
+
</div>
|
|
643
|
+
)
|
|
644
|
+
}
|