@gravito/zenith 1.1.3 → 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 (70) hide show
  1. package/README.md +28 -10
  2. package/dist/bin.js +43235 -76691
  3. package/dist/client/index.html +13 -0
  4. package/dist/server/index.js +43235 -76691
  5. package/package.json +16 -7
  6. package/CHANGELOG.md +0 -62
  7. package/Dockerfile +0 -46
  8. package/Dockerfile.demo-worker +0 -29
  9. package/bin/flux-console.ts +0 -2
  10. package/doc/ECOSYSTEM_EXPANSION_RFC.md +0 -130
  11. package/docker-compose.yml +0 -40
  12. package/docs/ALERTING_GUIDE.md +0 -71
  13. package/docs/DEPLOYMENT.md +0 -157
  14. package/docs/DOCS_INTERNAL.md +0 -73
  15. package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
  16. package/docs/QUASAR_MASTER_PLAN.md +0 -140
  17. package/docs/QUICK_TEST_GUIDE.md +0 -72
  18. package/docs/ROADMAP.md +0 -85
  19. package/docs/integrations/LARAVEL.md +0 -207
  20. package/postcss.config.js +0 -6
  21. package/scripts/debug_redis_keys.ts +0 -24
  22. package/scripts/flood-logs.ts +0 -21
  23. package/scripts/seed.ts +0 -213
  24. package/scripts/verify-throttle.ts +0 -49
  25. package/scripts/worker.ts +0 -124
  26. package/specs/PULSE_SPEC.md +0 -86
  27. package/src/bin.ts +0 -6
  28. package/src/client/App.tsx +0 -72
  29. package/src/client/Layout.tsx +0 -669
  30. package/src/client/Sidebar.tsx +0 -112
  31. package/src/client/ThroughputChart.tsx +0 -158
  32. package/src/client/WorkerStatus.tsx +0 -202
  33. package/src/client/components/BrandIcons.tsx +0 -168
  34. package/src/client/components/ConfirmDialog.tsx +0 -134
  35. package/src/client/components/JobInspector.tsx +0 -487
  36. package/src/client/components/LogArchiveModal.tsx +0 -432
  37. package/src/client/components/NotificationBell.tsx +0 -212
  38. package/src/client/components/PageHeader.tsx +0 -47
  39. package/src/client/components/Toaster.tsx +0 -90
  40. package/src/client/components/UserProfileDropdown.tsx +0 -186
  41. package/src/client/contexts/AuthContext.tsx +0 -105
  42. package/src/client/contexts/NotificationContext.tsx +0 -128
  43. package/src/client/index.css +0 -172
  44. package/src/client/main.tsx +0 -15
  45. package/src/client/pages/LoginPage.tsx +0 -164
  46. package/src/client/pages/MetricsPage.tsx +0 -445
  47. package/src/client/pages/OverviewPage.tsx +0 -519
  48. package/src/client/pages/PulsePage.tsx +0 -409
  49. package/src/client/pages/QueuesPage.tsx +0 -378
  50. package/src/client/pages/SchedulesPage.tsx +0 -535
  51. package/src/client/pages/SettingsPage.tsx +0 -1001
  52. package/src/client/pages/WorkersPage.tsx +0 -380
  53. package/src/client/pages/index.ts +0 -8
  54. package/src/client/utils.ts +0 -15
  55. package/src/server/config/ServerConfigManager.ts +0 -90
  56. package/src/server/index.ts +0 -860
  57. package/src/server/middleware/auth.ts +0 -127
  58. package/src/server/services/AlertService.ts +0 -321
  59. package/src/server/services/CommandService.ts +0 -136
  60. package/src/server/services/LogStreamProcessor.ts +0 -93
  61. package/src/server/services/MaintenanceScheduler.ts +0 -78
  62. package/src/server/services/PulseService.ts +0 -148
  63. package/src/server/services/QueueMetricsCollector.ts +0 -138
  64. package/src/server/services/QueueService.ts +0 -924
  65. package/src/shared/types.ts +0 -223
  66. package/tailwind.config.js +0 -80
  67. package/tests/placeholder.test.ts +0 -7
  68. package/tsconfig.json +0 -29
  69. package/tsconfig.node.json +0 -10
  70. package/vite.config.ts +0 -27
@@ -1,519 +0,0 @@
1
- import { useQuery } from '@tanstack/react-query'
2
- import { AnimatePresence, animate, motion } from 'framer-motion'
3
- import {
4
- Activity,
5
- AlertCircle,
6
- ArrowRight,
7
- ChevronRight,
8
- Clock,
9
- Cpu,
10
- Hourglass,
11
- ListTree,
12
- Search,
13
- Terminal,
14
- } from 'lucide-react'
15
- import React from 'react'
16
- import { JobInspector } from '../components/JobInspector'
17
- import { LogArchiveModal } from '../components/LogArchiveModal'
18
- import { ThroughputChart } from '../ThroughputChart'
19
- import { cn } from '../utils'
20
- import { WorkerStatus } from '../WorkerStatus'
21
-
22
- interface QueueStats {
23
- name: string
24
- waiting: number
25
- delayed: number
26
- active: number
27
- failed: number
28
- }
29
-
30
- interface SystemLog {
31
- timestamp: string
32
- level: 'error' | 'warn' | 'success' | 'info'
33
- workerId: string
34
- queue?: string
35
- message: string
36
- }
37
-
38
- interface FluxStats {
39
- queues: QueueStats[]
40
- workers: any[]
41
- }
42
-
43
- const DEFAULT_STATS: FluxStats = {
44
- queues: [],
45
- workers: [],
46
- }
47
-
48
- function LiveLogs({
49
- logs,
50
- onSearchArchive,
51
- onWorkerHover,
52
- }: {
53
- logs: SystemLog[]
54
- onSearchArchive: () => void
55
- onWorkerHover?: (id: string | null) => void
56
- }) {
57
- const scrollRef = React.useRef<HTMLUListElement>(null)
58
-
59
- React.useEffect(() => {
60
- // Access logs to satisfy dependency check (and trigger on update)
61
- if (scrollRef.current && logs) {
62
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight
63
- }
64
- }, [logs])
65
-
66
- return (
67
- <div className="absolute inset-0 card-premium flex flex-col overflow-hidden group">
68
- <div className="p-4 border-b bg-muted/5 flex justify-between items-center">
69
- <div className="flex items-center gap-2">
70
- <Terminal size={14} className="text-primary" />
71
- <h2 className="text-xs font-black uppercase tracking-widest opacity-70">
72
- Operational Logs
73
- </h2>
74
- </div>
75
- <div className="flex items-center gap-3">
76
- <button
77
- type="button"
78
- onClick={onSearchArchive}
79
- aria-label="Search Logs Archive"
80
- className="flex items-center gap-1.5 px-2 py-1 hover:bg-muted rounded-md text-[10px] font-black uppercase tracking-tighter text-muted-foreground transition-all"
81
- >
82
- <Search size={12} />
83
- Search Archive
84
- </button>
85
- <div className="flex gap-1">
86
- <div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></div>
87
- <div className="w-1.5 h-1.5 rounded-full bg-green-500/40"></div>
88
- </div>
89
- </div>
90
- </div>
91
- <ul
92
- ref={scrollRef}
93
- className="flex-1 min-h-0 overflow-y-auto p-4 font-mono text-[10px] space-y-2 scrollbar-thin scroll-smooth bg-black/20"
94
- >
95
- {logs.length === 0 ? (
96
- <div className="h-full flex flex-col items-center justify-center text-muted-foreground/20 gap-2">
97
- <Activity size={24} className="animate-pulse opacity-50" />
98
- <p className="font-bold uppercase tracking-[0.3em] text-[8px]">Scanning spectrum...</p>
99
- </div>
100
- ) : (
101
- logs.map((log, i) => (
102
- <li
103
- key={i}
104
- onMouseEnter={() => onWorkerHover?.(log.workerId)}
105
- onMouseLeave={() => onWorkerHover?.(null)}
106
- className="group flex gap-3 hover:bg-white/[0.03] -mx-2 px-3 py-1 rounded transition-all animate-in fade-in slide-in-from-left-1 duration-200 cursor-default border-l-2 border-transparent hover:border-primary/40"
107
- >
108
- <span className="text-muted-foreground/60 shrink-0 tabular-nums select-none opacity-60 group-hover:opacity-100 transition-opacity">
109
- {new Date(log.timestamp).toLocaleTimeString([], {
110
- hour12: false,
111
- hour: '2-digit',
112
- minute: '2-digit',
113
- second: '2-digit',
114
- })}
115
- </span>
116
- <div className="flex-1">
117
- <div className="flex items-center gap-2 mb-0.5">
118
- <span
119
- className={cn(
120
- 'text-[8px] font-black uppercase tracking-widest px-1 rounded',
121
- log.level === 'error'
122
- ? 'bg-red-500/10 text-red-500'
123
- : log.level === 'warn'
124
- ? 'bg-amber-500/10 text-amber-500'
125
- : log.level === 'success'
126
- ? 'bg-green-500/10 text-green-500'
127
- : 'bg-primary/10 text-primary'
128
- )}
129
- >
130
- {log.level}
131
- </span>
132
- <span className="text-[8px] font-bold text-muted-foreground/40 uppercase tracking-tighter opacity-0 group-hover:opacity-100 transition-all">
133
- ID:{log.workerId}
134
- </span>
135
- </div>
136
- <p className="text-foreground/70 group-hover:text-foreground leading-relaxed whitespace-pre-wrap break-all">
137
- {log.message}
138
- </p>
139
- </div>
140
- </li>
141
- ))
142
- )}
143
- </ul>
144
- </div>
145
- )
146
- }
147
-
148
- function QueueHeatmap({ queues }: { queues: any[] }) {
149
- return (
150
- <div className="card-premium p-6 mb-8 overflow-hidden relative group">
151
- <div className="absolute inset-0 opacity-10 group-hover:opacity-20 transition-opacity scanline pointer-events-none"></div>
152
- <div className="flex items-center justify-between mb-4">
153
- <div className="flex items-center gap-2">
154
- <Activity size={14} className="text-primary animate-pulse" />
155
- <h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60">
156
- Pipeline Load Distribution
157
- </h3>
158
- </div>
159
- <div className="flex gap-1">
160
- {[0.2, 0.4, 0.6, 0.8, 1].map((o) => (
161
- <div key={o} className="w-2 h-2 rounded-sm bg-primary" style={{ opacity: o }} />
162
- ))}
163
- </div>
164
- </div>
165
- <div className="grid grid-cols-10 sm:grid-cols-20 gap-1.5">
166
- {queues.map((q, i) => {
167
- const load = Math.min(1, q.waiting / 200)
168
- return (
169
- <div
170
- key={i}
171
- className="aspect-square rounded-sm transition-all duration-500 hover:scale-125 cursor-help group/tile relative"
172
- style={{
173
- backgroundColor: `hsl(var(--primary) / ${0.1 + load * 0.9})`,
174
- boxShadow: load > 0.7 ? `0 0 10px hsl(var(--primary) / ${load})` : 'none',
175
- }}
176
- >
177
- <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 p-2 bg-popover text-popover-foreground rounded-lg text-[10px] whitespace-nowrap opacity-0 group-hover/tile:opacity-100 transition-opacity pointer-events-none border border-border z-50">
178
- <span className="font-black">{q.name}</span>: {q.waiting} items
179
- </div>
180
- </div>
181
- )
182
- })}
183
- {Array.from({ length: Math.max(0, 40 - queues.length) }).map((_, i) => (
184
- <div
185
- key={`empty-${i}`}
186
- className="aspect-square rounded-sm bg-muted/20 border border-border/5 group-hover:border-border/10 transition-colors"
187
- />
188
- ))}
189
- </div>
190
- </div>
191
- )
192
- }
193
-
194
- function AnimatedNumber({ value }: { value: number }) {
195
- const [displayValue, setDisplayValue] = React.useState(value)
196
-
197
- React.useEffect(() => {
198
- const controls = animate(displayValue, value, {
199
- duration: 1.5,
200
- ease: 'easeOut',
201
- onUpdate: (latest: number) => setDisplayValue(Math.round(latest)),
202
- })
203
- return () => controls.stop()
204
- }, [value, displayValue])
205
-
206
- return <span>{displayValue.toLocaleString()}</span>
207
- }
208
-
209
- interface MetricCardProps {
210
- title: string
211
- value: number
212
- icon: React.ReactNode
213
- color: string
214
- trend?: string
215
- data?: number[]
216
- }
217
-
218
- function MetricCard({ title, value, icon, color, trend, data }: MetricCardProps) {
219
- const displayData = data && data.length > 0 ? data : [20, 30, 25, 40, 35, 50, 45, 60, 55, 70]
220
- const max = Math.max(...displayData, 10)
221
-
222
- return (
223
- <div className="card-premium p-6 hover:shadow-2xl transform hover:-translate-y-1 group relative overflow-hidden flex flex-col justify-between">
224
- <div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none scanline z-0"></div>
225
-
226
- <div className="flex justify-between items-start mb-4 z-10 relative">
227
- <div
228
- className={cn(
229
- 'p-3 rounded-xl bg-muted/20 transition-all group-hover:scale-110 duration-500 border border-transparent group-hover:border-primary/20 shadow-inner',
230
- color
231
- )}
232
- >
233
- {icon}
234
- </div>
235
- {trend && (
236
- <div className="flex flex-col items-end">
237
- <span className={cn('text-[9px] font-black uppercase tracking-widest', color)}>
238
- {trend}
239
- </span>
240
- <div className="w-10 h-1 bg-muted/30 rounded-full mt-1 overflow-hidden">
241
- <motion.div
242
- initial={{ width: 0 }}
243
- animate={{ width: '100%' }}
244
- transition={{ duration: 1.5, repeat: Infinity, repeatType: 'reverse' }}
245
- className={cn('h-full', color.replace('text-', 'bg-'))}
246
- />
247
- </div>
248
- </div>
249
- )}
250
- </div>
251
-
252
- <div className="z-10 relative">
253
- <p className="text-[10px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] mb-1 font-heading">
254
- {title}
255
- </p>
256
- <div className="text-3xl font-black tracking-tight flex items-baseline gap-1 font-mono">
257
- <AnimatedNumber value={value} />
258
- {title === 'Waiting Jobs' && value > 100 && (
259
- <span className="text-red-500 animate-pulse text-sm ml-1">!</span>
260
- )}
261
- </div>
262
- </div>
263
-
264
- <div className="mt-6 flex items-end gap-1 h-12 opacity-10 group-hover:opacity-30 transition-all duration-700 absolute bottom-0 left-0 right-0 pointer-events-none">
265
- {displayData.map((v, i) => (
266
- <div
267
- key={i}
268
- className={cn(
269
- 'flex-1 rounded-t-sm transition-all duration-1000',
270
- color.replace('text-', 'bg-')
271
- )}
272
- style={{
273
- height: `${(v / max) * 100}%`,
274
- opacity: 0.2 + (i / displayData.length) * 0.8,
275
- transitionDelay: `${i * 20}ms`,
276
- }}
277
- ></div>
278
- ))}
279
- </div>
280
- </div>
281
- )
282
- }
283
-
284
- function QueueList({
285
- queues,
286
- setSelectedQueue,
287
- }: {
288
- queues: QueueStats[]
289
- setSelectedQueue: (name: string | null) => void
290
- }) {
291
- return (
292
- <div className="card-premium h-full flex flex-col overflow-hidden">
293
- <div className="p-4 border-b bg-muted/5 flex justify-between items-center">
294
- <div className="flex items-center gap-2">
295
- <ListTree size={14} className="text-primary" />
296
- <h2 className="text-xs font-black uppercase tracking-widest opacity-70">
297
- Processing Pipelines
298
- </h2>
299
- </div>
300
- <button
301
- type="button"
302
- className="text-[10px] font-black text-primary hover:underline flex items-center gap-2 uppercase tracking-widest transition-opacity"
303
- >
304
- Stats <ChevronRight size={12} />
305
- </button>
306
- </div>
307
- <div className="flex-1 overflow-auto scrollbar-thin">
308
- <table className="w-full text-left">
309
- <thead className="bg-muted/10 text-muted-foreground uppercase text-[9px] font-black tracking-widest sticky top-0">
310
- <tr>
311
- <th className="px-4 py-3">Queue</th>
312
- <th className="px-4 py-3">Waiting</th>
313
- <th className="px-4 py-3 text-right">Ops</th>
314
- </tr>
315
- </thead>
316
- <tbody className="divide-y divide-border/30 text-xs">
317
- {queues.map((queue) => (
318
- <tr key={queue.name} className="hover:bg-muted/5 transition-colors group">
319
- <td className="px-4 py-4">
320
- <div className="flex flex-col">
321
- <span className="font-black text-foreground">{queue.name}</span>
322
- {queue.failed > 0 && (
323
- <span className="text-[9px] text-red-500 font-bold uppercase">
324
- {queue.failed} FAILED
325
- </span>
326
- )}
327
- </div>
328
- </td>
329
- <td className="px-4 py-4 font-mono font-black">{queue.waiting.toLocaleString()}</td>
330
- <td className="px-4 py-4 text-right">
331
- <div className="flex justify-end gap-2">
332
- <button
333
- type="button"
334
- onClick={() => setSelectedQueue(queue.name)}
335
- className="p-1.5 bg-muted hover:bg-primary/20 hover:text-primary rounded text-muted-foreground transition-all active:scale-90"
336
- title="Inspect"
337
- aria-label={`Inspect ${queue.name} queue`}
338
- >
339
- <ArrowRight size={14} />
340
- </button>
341
- </div>
342
- </td>
343
- </tr>
344
- ))}
345
- </tbody>
346
- </table>
347
- </div>
348
- </div>
349
- )
350
- }
351
-
352
- /**
353
- * System Overview Dashboard Page.
354
- *
355
- * Provides a high-level, real-time view of all processing pipelines,
356
- * including job counts, worker status, and live operational logs.
357
- *
358
- * @public
359
- * @since 3.0.0
360
- */
361
- export function OverviewPage() {
362
- const [selectedQueue, setSelectedQueue] = React.useState<string | null>(null)
363
- const [hoveredWorkerId, setHoveredWorkerId] = React.useState<string | null>(null)
364
-
365
- const [logs, setLogs] = React.useState<SystemLog[]>([])
366
- const [stats, setStats] = React.useState<FluxStats>(DEFAULT_STATS)
367
- const [isLogArchiveOpen, setIsLogArchiveOpen] = React.useState(false)
368
-
369
- React.useEffect(() => {
370
- const handler = (e: any) => setSelectedQueue(e.detail)
371
- window.addEventListener('select-queue', handler)
372
- return () => window.removeEventListener('select-queue', handler)
373
- }, [])
374
-
375
- // Initial fetch
376
- React.useEffect(() => {
377
- fetch('/api/queues')
378
- .then((res) => res.json())
379
- .then((data) => {
380
- if (data.queues) {
381
- setStats((prev) => ({ ...prev, queues: data.queues }))
382
- }
383
- })
384
- fetch('/api/workers')
385
- .then((res) => res.json())
386
- .then((data) => {
387
- if (data.workers) {
388
- setStats((prev) => ({ ...prev, workers: data.workers }))
389
- }
390
- })
391
- }, [])
392
-
393
- // Stats update listener
394
- React.useEffect(() => {
395
- const handler = (e: any) => {
396
- const newStats = e.detail
397
- if (newStats) {
398
- setStats((prev) => ({
399
- queues: newStats.queues || prev.queues,
400
- workers: newStats.workers || prev.workers,
401
- }))
402
- }
403
- }
404
- window.addEventListener('flux-stats-update', handler)
405
- return () => window.removeEventListener('flux-stats-update', handler)
406
- }, [])
407
-
408
- // Live log listener
409
- React.useEffect(() => {
410
- const handler = (e: CustomEvent) => {
411
- const data = e.detail
412
- setLogs((prev) => [...prev.slice(-99), data])
413
- }
414
- window.addEventListener('flux-log-update', handler as EventListener)
415
- return () => window.removeEventListener('flux-log-update', handler as EventListener)
416
- }, [])
417
-
418
- // Clear logs listener
419
- React.useEffect(() => {
420
- const handler = () => setLogs([])
421
- window.addEventListener('clear-logs', handler)
422
- return () => window.removeEventListener('clear-logs', handler)
423
- }, [])
424
-
425
- const { data: historyData } = useQuery<{ history: Record<string, number[]> }>({
426
- queryKey: ['metrics-history'],
427
- queryFn: () => fetch('/api/metrics/history').then((res) => res.json()),
428
- refetchInterval: 30000,
429
- })
430
-
431
- const history = historyData?.history || {}
432
- const { queues, workers } = stats
433
-
434
- const totalWaiting = queues.reduce((acc, q) => acc + q.waiting, 0)
435
- const totalDelayed = queues.reduce((acc, q) => acc + q.delayed, 0)
436
- const totalFailed = queues.reduce((acc, q) => acc + q.failed, 0)
437
- const activeWorkers = workers.length
438
-
439
- return (
440
- <div className="space-y-12">
441
- <LogArchiveModal isOpen={isLogArchiveOpen} onClose={() => setIsLogArchiveOpen(false)} />
442
- <AnimatePresence>
443
- {selectedQueue && (
444
- <JobInspector queueName={selectedQueue} onClose={() => setSelectedQueue(null)} />
445
- )}
446
- </AnimatePresence>
447
-
448
- <div className="flex justify-between items-end">
449
- <div>
450
- <h1 className="text-4xl font-black tracking-tighter">System Overview</h1>
451
- <p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
452
- Real-time status of your processing pipelines.
453
- </p>
454
- </div>
455
- <div className="flex items-center gap-2 text-[10px] font-black text-green-500 bg-green-500/10 px-4 py-2 rounded-full border border-green-500/20 uppercase tracking-[0.2em] animate-pulse">
456
- <span className="w-2 h-2 bg-green-500 rounded-full shadow-[0_0_8px_rgba(34,197,94,0.6)]"></span>
457
- Live Syncing
458
- </div>
459
- </div>
460
-
461
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
462
- <MetricCard
463
- title="Waiting Jobs"
464
- value={totalWaiting}
465
- icon={<Hourglass size={20} />}
466
- color="text-amber-500"
467
- trend="+12% / hr"
468
- data={history.waiting}
469
- />
470
- <MetricCard
471
- title="Delayed Jobs"
472
- value={totalDelayed}
473
- icon={<Clock size={20} />}
474
- color="text-blue-500"
475
- trend="Stable"
476
- data={history.delayed}
477
- />
478
- <MetricCard
479
- title="Failed Jobs"
480
- value={totalFailed}
481
- icon={<AlertCircle size={20} />}
482
- color="text-red-500"
483
- trend={totalFailed > 0 ? 'CRITICAL' : 'CLEAN'}
484
- data={history.failed}
485
- />
486
- <MetricCard
487
- title="Active Workers"
488
- value={activeWorkers}
489
- icon={<Cpu size={20} />}
490
- color="text-indigo-500"
491
- trend={activeWorkers > 0 ? 'ONLINE' : 'IDLE'}
492
- data={history.workers}
493
- />
494
- </div>
495
-
496
- <ThroughputChart />
497
-
498
- <QueueHeatmap queues={queues} />
499
-
500
- <div className="flex flex-col lg:grid lg:grid-cols-3 gap-8 lg:h-[600px]">
501
- <div className="lg:col-span-1 h-[400px] lg:h-full">
502
- <WorkerStatus highlightedWorkerId={hoveredWorkerId} workers={workers} />
503
- </div>
504
- <div className="lg:col-span-2 flex flex-col lg:grid lg:grid-rows-2 gap-6 lg:h-full">
505
- <div className="relative h-[300px] lg:h-full min-h-0 overflow-hidden bg-card rounded-xl border border-border/50">
506
- <LiveLogs
507
- logs={logs}
508
- onSearchArchive={() => setIsLogArchiveOpen(true)}
509
- onWorkerHover={setHoveredWorkerId}
510
- />
511
- </div>
512
- <div className="h-[300px] lg:h-full min-h-0 overflow-hidden bg-card rounded-xl border border-border/50">
513
- <QueueList queues={queues} setSelectedQueue={setSelectedQueue} />
514
- </div>
515
- </div>
516
- </div>
517
- </div>
518
- )
519
- }