@gravito/zenith 1.1.2 → 1.1.6

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