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