queen-mq 0.1.0

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 (90) hide show
  1. package/API.md +1116 -0
  2. package/CACHE.md +519 -0
  3. package/DASHBOARD-V3.md +478 -0
  4. package/DASHBOARD.md +382 -0
  5. package/MOD_QUEUE.md +453 -0
  6. package/PARTITION_LOCKING_DESIGN.md +989 -0
  7. package/PLAN.md +707 -0
  8. package/QUERY_ANALSYS.md +72 -0
  9. package/QUEUE_BUS.md +334 -0
  10. package/README.md +1495 -0
  11. package/V2-PLAN.md +236 -0
  12. package/assets/dashboard.png +0 -0
  13. package/dashboard/.vscode/extensions.json +3 -0
  14. package/dashboard/README.md +5 -0
  15. package/dashboard/index.html +14 -0
  16. package/dashboard/package-lock.json +1458 -0
  17. package/dashboard/package.json +25 -0
  18. package/dashboard/public/vite.svg +1 -0
  19. package/dashboard/src/App.vue +29 -0
  20. package/dashboard/src/assets/styles/main.css +908 -0
  21. package/dashboard/src/assets/vue.svg +1 -0
  22. package/dashboard/src/components/cards/MetricCard.vue +298 -0
  23. package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
  24. package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
  25. package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
  26. package/dashboard/src/components/common/ActivityFeed.vue +251 -0
  27. package/dashboard/src/components/layout/AppHeader.vue +208 -0
  28. package/dashboard/src/components/layout/AppLayout.vue +88 -0
  29. package/dashboard/src/components/layout/AppSidebar.vue +261 -0
  30. package/dashboard/src/main.js +44 -0
  31. package/dashboard/src/router.js +54 -0
  32. package/dashboard/src/services/api.js +187 -0
  33. package/dashboard/src/services/websocket.js +167 -0
  34. package/dashboard/src/utils/constants.js +56 -0
  35. package/dashboard/src/utils/helpers.js +118 -0
  36. package/dashboard/src/views/Analytics.vue +912 -0
  37. package/dashboard/src/views/Dashboard.vue +906 -0
  38. package/dashboard/src/views/Messages.vue +437 -0
  39. package/dashboard/src/views/QueueDetail.vue +501 -0
  40. package/dashboard/src/views/Queues.vue +333 -0
  41. package/dashboard/vite.config.js +30 -0
  42. package/debug-namespace.js +110 -0
  43. package/docs/long-polling.md +159 -0
  44. package/docs/multi-server-cache-solutions.md +185 -0
  45. package/docs/performance-tuning.md +222 -0
  46. package/examples/bus-mode.js +239 -0
  47. package/examples/continuous-consumer-optimized.js +215 -0
  48. package/examples/continuous-consumer.js +159 -0
  49. package/examples/continuous-producer.js +343 -0
  50. package/examples/mixed-mode.js +277 -0
  51. package/examples/multi-server-test.js +305 -0
  52. package/examples/single.js +64 -0
  53. package/examples/smartchat-dealyed.js +42 -0
  54. package/examples/smartchat.js +52 -0
  55. package/examples/test-cache-invalidation.js +119 -0
  56. package/examples/test-cache-multi-server.js +245 -0
  57. package/examples/test-minimal-client.js +112 -0
  58. package/examples/test-queue-creation-policy.js +137 -0
  59. package/init-db.js +20 -0
  60. package/package.json +36 -0
  61. package/src/client/client.js +291 -0
  62. package/src/client/index.js +6 -0
  63. package/src/client/queenClient.js +513 -0
  64. package/src/client/utils/http.js +172 -0
  65. package/src/client/utils/loadBalancer.js +152 -0
  66. package/src/client/utils/retry.js +35 -0
  67. package/src/config.js +215 -0
  68. package/src/database/connection.js +103 -0
  69. package/src/database/poolManager.js +192 -0
  70. package/src/database/schema-v2.sql +214 -0
  71. package/src/managers/eventManager.js +59 -0
  72. package/src/managers/queueManagerOptimized.js +1512 -0
  73. package/src/managers/resourceCache.js +96 -0
  74. package/src/managers/systemEventManager.js +127 -0
  75. package/src/routes/ack.js +26 -0
  76. package/src/routes/analytics.js +812 -0
  77. package/src/routes/configure.js +46 -0
  78. package/src/routes/messages.js +298 -0
  79. package/src/routes/pop.js +85 -0
  80. package/src/routes/push.js +28 -0
  81. package/src/routes/resources.js +296 -0
  82. package/src/server.js +1286 -0
  83. package/src/services/encryptionService.js +82 -0
  84. package/src/services/evictionService.js +131 -0
  85. package/src/services/retentionService.js +129 -0
  86. package/src/services/startupSync.js +35 -0
  87. package/src/test/test.js +4521 -0
  88. package/src/utils/logger.js +44 -0
  89. package/src/utils/uuid.js +5 -0
  90. package/src/websocket/wsServer.js +221 -0
@@ -0,0 +1,906 @@
1
+ <template>
2
+ <div class="dashboard">
3
+ <!-- Date/Time Filter Bar -->
4
+ <div class="filter-bar">
5
+ <div class="filter-group">
6
+ <label>Time Range:</label>
7
+ <Select
8
+ v-model="selectedTimeRange"
9
+ :options="timeRangeOptions"
10
+ optionLabel="label"
11
+ optionValue="value"
12
+ placeholder="Select time range"
13
+ @change="onTimeRangeChange"
14
+ />
15
+ </div>
16
+ <div class="filter-group" v-if="selectedTimeRange === 'custom'">
17
+ <label>From:</label>
18
+ <Calendar
19
+ v-model="fromDateTime"
20
+ showTime
21
+ :showIcon="true"
22
+ dateFormat="yy-mm-dd"
23
+ placeholder="Start date/time"
24
+ />
25
+ </div>
26
+ <div class="filter-group" v-if="selectedTimeRange === 'custom'">
27
+ <label>To:</label>
28
+ <Calendar
29
+ v-model="toDateTime"
30
+ showTime
31
+ :showIcon="true"
32
+ dateFormat="yy-mm-dd"
33
+ placeholder="End date/time"
34
+ />
35
+ </div>
36
+ <div class="filter-group">
37
+ <label>Queue:</label>
38
+ <Select
39
+ v-model="selectedQueue"
40
+ :options="queueOptions || []"
41
+ optionLabel="label"
42
+ optionValue="value"
43
+ placeholder="All queues"
44
+ :showClear="true"
45
+ @change="refreshData"
46
+ />
47
+ </div>
48
+ <div class="filter-group">
49
+ <label>Namespace:</label>
50
+ <Select
51
+ v-model="selectedNamespace"
52
+ :options="namespaceOptions || []"
53
+ optionLabel="label"
54
+ optionValue="value"
55
+ placeholder="All namespaces"
56
+ :showClear="true"
57
+ @change="refreshData"
58
+ />
59
+ </div>
60
+ <div class="filter-group">
61
+ <Button
62
+ label="Apply Filters"
63
+ icon="pi pi-filter"
64
+ @click="refreshData"
65
+ :loading="loading"
66
+ />
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Metric Cards -->
71
+ <div class="metrics-grid">
72
+ <MetricCard
73
+ title="Total Messages"
74
+ :value="metrics.totalMessages"
75
+ icon="pi pi-envelope"
76
+ color="primary"
77
+ :loading="loading"
78
+ :sparklineData="sparklineData.messages"
79
+ />
80
+ <MetricCard
81
+ title="Pending"
82
+ :value="metrics.pending"
83
+ icon="pi pi-clock"
84
+ color="warning"
85
+ :loading="loading"
86
+ :sparklineData="sparklineData.pending"
87
+ />
88
+ <MetricCard
89
+ title="Processing"
90
+ :value="metrics.processing"
91
+ icon="pi pi-spin pi-spinner"
92
+ color="info"
93
+ :loading="loading"
94
+ :sparklineData="sparklineData.processing"
95
+ />
96
+ <MetricCard
97
+ title="Completed Today"
98
+ :value="metrics.completedToday"
99
+ icon="pi pi-check-circle"
100
+ color="success"
101
+ :loading="loading"
102
+ :sparklineData="sparklineData.completed"
103
+ />
104
+ <MetricCard
105
+ title="Failed Today"
106
+ :value="metrics.failedToday"
107
+ icon="pi pi-times-circle"
108
+ color="danger"
109
+ :loading="loading"
110
+ :sparklineData="sparklineData.failed"
111
+ />
112
+ <MetricCard
113
+ title="Dead Letter"
114
+ :value="metrics.deadLetter"
115
+ icon="pi pi-exclamation-triangle"
116
+ color="danger"
117
+ :loading="loading"
118
+ :sparklineData="sparklineData.deadLetter"
119
+ />
120
+ </div>
121
+
122
+ <!-- Charts Row -->
123
+ <div class="charts-row">
124
+ <ThroughputChart
125
+ title="Message Throughput (Last Hour)"
126
+ :data="throughputData"
127
+ :loading="loading"
128
+ @refresh="refreshData"
129
+ />
130
+
131
+ <QueueDepthChart
132
+ title="Queue Depths"
133
+ :data="queueDepthData"
134
+ :loading="loading"
135
+ @refresh="refreshData"
136
+ />
137
+ </div>
138
+
139
+ <!-- Queue List and Activity Feed -->
140
+ <div class="bottom-row">
141
+ <div class="card-v3">
142
+ <div class="card-header">
143
+ <h3>Top Queues by Activity</h3>
144
+ <Button
145
+ label="View All"
146
+ class="btn-secondary"
147
+ @click="$router.push('/queues')"
148
+ />
149
+ </div>
150
+
151
+ <DataTable
152
+ :value="topQueues"
153
+ :loading="loading"
154
+ responsiveLayout="scroll"
155
+ :rows="5"
156
+ class="dark-table-v3"
157
+ >
158
+ <Column field="name" header="Queue">
159
+ <template #body="{ data }">
160
+ <div class="queue-name-cell">
161
+ <div class="queue-icon">Q</div>
162
+ <span>{{ data.name }}</span>
163
+ </div>
164
+ </template>
165
+ </Column>
166
+ <Column field="namespace" header="Namespace">
167
+ <template #body="{ data }">
168
+ <Tag v-if="data.namespace" :value="data.namespace" class="namespace-tag" />
169
+ <span v-else class="text-muted">-</span>
170
+ </template>
171
+ </Column>
172
+ <Column field="pending" header="Pending">
173
+ <template #body="{ data }">
174
+ <span class="status-pending">{{ data.pending }}</span>
175
+ </template>
176
+ </Column>
177
+ <Column field="processing" header="Processing">
178
+ <template #body="{ data }">
179
+ <span class="status-processing">{{ data.processing }}</span>
180
+ </template>
181
+ </Column>
182
+ <Column field="throughput" header="Rate">
183
+ <template #body="{ data }">
184
+ <span class="rate-value">{{ data.throughput }} msg/min</span>
185
+ </template>
186
+ </Column>
187
+ <Column>
188
+ <template #body="{ data }">
189
+ <Button
190
+ icon="pi pi-eye"
191
+ class="p-button-text p-button-sm action-btn"
192
+ @click="$router.push(`/queues/${data.name}`)"
193
+ v-tooltip="'View Details'"
194
+ />
195
+ </template>
196
+ </Column>
197
+ </DataTable>
198
+ </div>
199
+
200
+ <div class="activity-feed">
201
+ <div class="activity-header">
202
+ <h3>Live Activity</h3>
203
+ <div class="live-indicator">
204
+ <span class="pulse-dot"></span>
205
+ <span>Live</span>
206
+ </div>
207
+ </div>
208
+
209
+ <ActivityFeed :events="activityEvents" />
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </template>
214
+
215
+ <script setup>
216
+ import { ref, onMounted, onUnmounted, computed } from 'vue'
217
+ import { useToast } from 'primevue/usetoast'
218
+ import Button from 'primevue/button'
219
+ import DataTable from 'primevue/datatable'
220
+ import Column from 'primevue/column'
221
+ import Tag from 'primevue/tag'
222
+ import Select from 'primevue/select'
223
+ import Calendar from 'primevue/calendar'
224
+
225
+ import MetricCard from '../components/cards/MetricCard.vue'
226
+ import ThroughputChart from '../components/charts/ThroughputChart.vue'
227
+ import QueueDepthChart from '../components/charts/QueueDepthChart.vue'
228
+ import ActivityFeed from '../components/common/ActivityFeed.vue'
229
+
230
+ import api from '../services/api.js'
231
+ import websocket from '../services/websocket.js'
232
+ import { formatNumber } from '../utils/helpers.js'
233
+
234
+ // Simple UUID generator for unique event IDs
235
+ const generateEventId = () => {
236
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 9)
237
+ }
238
+
239
+ const toast = useToast()
240
+ const loading = ref(false)
241
+
242
+ // Filter state
243
+ const selectedTimeRange = ref('1h')
244
+ const fromDateTime = ref(null)
245
+ const toDateTime = ref(null)
246
+ const selectedQueue = ref(null)
247
+ const selectedNamespace = ref(null)
248
+ const selectedTask = ref(null)
249
+
250
+ // Filter options
251
+ const timeRangeOptions = ref([
252
+ { label: 'Last Hour', value: '1h' },
253
+ { label: 'Last 6 Hours', value: '6h' },
254
+ { label: 'Last 24 Hours', value: '24h' },
255
+ { label: 'Last 7 Days', value: '7d' },
256
+ { label: 'Last 30 Days', value: '30d' },
257
+ { label: 'Custom Range', value: 'custom' }
258
+ ])
259
+
260
+ const queueOptions = ref([])
261
+ const namespaceOptions = ref([])
262
+
263
+ // Metrics data
264
+ const metrics = ref({
265
+ totalMessages: 0,
266
+ pending: 0,
267
+ processing: 0,
268
+ completedToday: 0,
269
+ failedToday: 0,
270
+ deadLetter: 0
271
+ })
272
+
273
+ // Sparkline data for metric cards
274
+ const sparklineData = ref({
275
+ messages: [],
276
+ pending: [],
277
+ processing: [],
278
+ completed: [],
279
+ failed: [],
280
+ deadLetter: []
281
+ })
282
+
283
+ // Chart data - Initialize with empty structure
284
+ const throughputData = ref({
285
+ labels: [],
286
+ datasets: [
287
+ { label: 'Incoming', data: [] },
288
+ { label: 'Completed', data: [] },
289
+ { label: 'Failed', data: [] },
290
+ { label: 'Dead Letter', data: [] }
291
+ ]
292
+ })
293
+
294
+ const queueDepthData = ref({
295
+ labels: [],
296
+ datasets: [
297
+ { label: 'Pending', data: [] },
298
+ { label: 'Processing', data: [] }
299
+ ]
300
+ })
301
+
302
+ // Queue distribution data
303
+ const queueDistribution = ref([])
304
+
305
+ // Table data
306
+ const topQueues = ref([])
307
+
308
+ // Activity feed
309
+ const activityEvents = ref([])
310
+ const maxActivityEvents = 20
311
+
312
+ // Generate random sparkline data (for demo)
313
+ const generateSparklineData = () => {
314
+ const generateArray = (base, variance) => {
315
+ return Array.from({ length: 10 }, () =>
316
+ base + Math.floor(Math.random() * variance - variance / 2)
317
+ )
318
+ }
319
+
320
+ sparklineData.value = {
321
+ messages: generateArray(1000, 200),
322
+ pending: generateArray(50, 20),
323
+ processing: generateArray(30, 10),
324
+ completed: generateArray(800, 150),
325
+ failed: generateArray(10, 5),
326
+ deadLetter: generateArray(5, 3)
327
+ }
328
+ }
329
+
330
+ // Calculate queue distribution
331
+ const calculateQueueDistribution = (queues) => {
332
+ // Use mock data if no real queues
333
+ if (!queues || queues.length === 0) {
334
+ queues = [
335
+ { name: 'email-queue', pending: 120, processing: 30 },
336
+ { name: 'notification-queue', pending: 85, processing: 20 },
337
+ { name: 'analytics-queue', pending: 200, processing: 45 },
338
+ { name: 'payment-queue', pending: 45, processing: 10 },
339
+ { name: 'report-queue', pending: 30, processing: 5 }
340
+ ]
341
+ }
342
+
343
+ const colors = ['#ec4899', '#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ef4444']
344
+ const total = queues.reduce((sum, q) => sum + (q.pending || 0) + (q.processing || 0), 0)
345
+
346
+ queueDistribution.value = queues
347
+ .slice(0, 6)
348
+ .map((queue, index) => {
349
+ const count = (queue.pending || 0) + (queue.processing || 0)
350
+ return {
351
+ name: queue.name,
352
+ count,
353
+ percentage: total > 0 ? Math.round((count / total) * 100) : 0,
354
+ color: colors[index % colors.length]
355
+ }
356
+ })
357
+ .filter(item => item.percentage > 0)
358
+ }
359
+
360
+ // Fetch dashboard data
361
+ const fetchData = async () => {
362
+ try {
363
+ loading.value = true
364
+
365
+ // Build filter parameters
366
+ const filters = {}
367
+
368
+ // Handle time range
369
+ if (selectedTimeRange.value === 'custom') {
370
+ if (fromDateTime.value) filters.fromDateTime = fromDateTime.value.toISOString()
371
+ if (toDateTime.value) filters.toDateTime = toDateTime.value.toISOString()
372
+ } else {
373
+ // Calculate from/to based on selected range
374
+ const now = new Date()
375
+ const from = new Date()
376
+
377
+ switch (selectedTimeRange.value) {
378
+ case '1h':
379
+ from.setHours(from.getHours() - 1)
380
+ break
381
+ case '6h':
382
+ from.setHours(from.getHours() - 6)
383
+ break
384
+ case '24h':
385
+ from.setDate(from.getDate() - 1)
386
+ break
387
+ case '7d':
388
+ from.setDate(from.getDate() - 7)
389
+ break
390
+ case '30d':
391
+ from.setDate(from.getDate() - 30)
392
+ break
393
+ }
394
+
395
+ filters.fromDateTime = from.toISOString()
396
+ filters.toDateTime = now.toISOString()
397
+ }
398
+
399
+ // Add other filters
400
+ if (selectedQueue.value) filters.queue = selectedQueue.value
401
+ if (selectedNamespace.value) filters.namespace = selectedNamespace.value
402
+ if (selectedTask.value) filters.task = selectedTask.value
403
+
404
+ // Fetch all data in parallel with filters
405
+ const [overview, throughput, depths, queues] = await Promise.all([
406
+ api.getSystemOverview(),
407
+ api.getThroughput(filters),
408
+ api.getQueueDepths(filters),
409
+ api.getQueues(filters)
410
+ ])
411
+
412
+ // Update metrics
413
+ if (overview) {
414
+ metrics.value = {
415
+ totalMessages: overview.messages?.total || 0,
416
+ pending: overview.messages?.pending || 0,
417
+ processing: overview.messages?.processing || 0,
418
+ completedToday: overview.messages?.completed || 0,
419
+ failedToday: overview.messages?.failed || 0,
420
+ deadLetter: overview.messages?.deadLetter || 0
421
+ }
422
+ }
423
+
424
+ // Process throughput data for chart - always process to show mock data if empty
425
+ processThroughputData(throughput?.throughput || [])
426
+
427
+ // Process queue depths for chart - always process to show mock data if empty
428
+ processQueueDepthData(depths?.depths || [])
429
+
430
+ // Process top queues and distribution - always process to show mock data if empty
431
+ processTopQueues(queues?.queues || [])
432
+ calculateQueueDistribution(queues?.queues || [])
433
+
434
+ // Generate sparkline data
435
+ generateSparklineData()
436
+
437
+ } catch (error) {
438
+ console.error('Failed to fetch dashboard data:', error)
439
+ toast.add({
440
+ severity: 'error',
441
+ summary: 'Error',
442
+ detail: 'Failed to load dashboard data',
443
+ life: 3000
444
+ })
445
+ } finally {
446
+ loading.value = false
447
+ }
448
+ }
449
+
450
+ // Process throughput data for chart
451
+ const processThroughputData = (data) => {
452
+ // Use empty array if no data available
453
+ if (!data || data.length === 0) {
454
+ data = []
455
+ }
456
+
457
+ // Take last 12 data points for hourly view and reverse to show chronological order (oldest to newest)
458
+ const recentData = data.reverse().slice(-100)
459
+
460
+ throughputData.value = {
461
+ labels: recentData.map(item => {
462
+ const date = new Date(item.timestamp)
463
+ return date.toLocaleTimeString('en-US', {
464
+ hour: 'numeric',
465
+ minute: '2-digit'
466
+ })
467
+ }),
468
+ datasets: [
469
+ {
470
+ label: 'Incoming Messages',
471
+ data: recentData.map(item => item.incoming?.messagesPerMinute || 0)
472
+ },
473
+ {
474
+ label: 'Completed Messages',
475
+ data: recentData.map(item => item.completed?.messagesPerMinute || 0)
476
+ },
477
+ {
478
+ label: 'Failed Messages',
479
+ data: recentData.map(item => item.failed?.messagesPerMinute || 0)
480
+ },
481
+ {
482
+ label: 'Dead Letter Messages',
483
+ data: recentData.map(item => item.deadLetter?.messagesPerMinute || 0)
484
+ }
485
+ ]
486
+ }
487
+ }
488
+
489
+ // Process queue depth data for chart
490
+ const processQueueDepthData = (data) => {
491
+ // Use empty array if no data available
492
+ if (!data || data.length === 0) {
493
+ data = []
494
+ }
495
+
496
+ // Filter out queues with no messages and sort by total messages, take top 8
497
+ const filtered = data.filter(item => (item.depth || 0) + (item.processing || 0) > 0)
498
+ const sorted = [...filtered]
499
+ .sort((a, b) => (b.depth + b.processing) - (a.depth + a.processing))
500
+ .slice(0, 8)
501
+
502
+ queueDepthData.value = {
503
+ labels: sorted.map(item => item.queue),
504
+ datasets: [
505
+ {
506
+ label: 'Pending',
507
+ data: sorted.map(item => item.depth || 0)
508
+ },
509
+ {
510
+ label: 'Processing',
511
+ data: sorted.map(item => item.processing || 0)
512
+ }
513
+ ]
514
+ }
515
+ }
516
+
517
+ // Process top queues for table
518
+ const processTopQueues = (queues) => {
519
+ // Use empty array if no real queues
520
+ if (!queues || queues.length === 0) {
521
+ queues = []
522
+ }
523
+
524
+ topQueues.value = queues
525
+ .map(queue => ({
526
+ name: queue.name,
527
+ namespace: queue.namespace,
528
+ pending: queue.stats?.pending || 0,
529
+ processing: queue.stats?.processing || 0,
530
+ throughput: 0 // No throughput data available
531
+ }))
532
+ .sort((a, b) => (b.pending + b.processing) - (a.pending + a.processing))
533
+ .slice(0, 5)
534
+ }
535
+
536
+ // Handle WebSocket events
537
+ const handleWebSocketEvent = (wsMessage) => {
538
+ // WebSocket message format: { event: "message.pushed", data: {...}, timestamp: "..." }
539
+ // ActivityFeed expects: { event: "message.pushed", data: {...}, timestamp: Date }
540
+
541
+ // Add to activity feed in the format ActivityFeed expects
542
+ activityEvents.value.unshift({
543
+ id: generateEventId(), // Use unique ID generator to avoid duplicate keys
544
+ event: wsMessage.event || 'unknown',
545
+ data: wsMessage.data || {},
546
+ timestamp: wsMessage.timestamp ? new Date(wsMessage.timestamp) : new Date()
547
+ })
548
+
549
+ // Limit activity events
550
+ if (activityEvents.value.length > maxActivityEvents) {
551
+ activityEvents.value = activityEvents.value.slice(0, maxActivityEvents)
552
+ }
553
+
554
+ // Update metrics based on event type
555
+ const eventType = wsMessage.event
556
+ if (eventType === 'message.pushed') {
557
+ metrics.value.pending++
558
+ metrics.value.totalMessages++
559
+ } else if (eventType === 'message.popped') {
560
+ metrics.value.pending = Math.max(0, metrics.value.pending - 1)
561
+ metrics.value.processing++
562
+ } else if (eventType === 'message.completed' || eventType === 'message.acknowledged') {
563
+ metrics.value.processing = Math.max(0, metrics.value.processing - 1)
564
+ metrics.value.completedToday++
565
+ } else if (eventType === 'message.failed') {
566
+ metrics.value.processing = Math.max(0, metrics.value.processing - 1)
567
+ metrics.value.failedToday++
568
+ } else if (eventType === 'message.dead_letter') {
569
+ metrics.value.deadLetter++
570
+ }
571
+ }
572
+
573
+ // Refresh data
574
+ const refreshData = () => {
575
+ fetchData()
576
+ }
577
+
578
+ // Lifecycle
579
+ // Function to handle time range change
580
+ const onTimeRangeChange = () => {
581
+ if (selectedTimeRange.value !== 'custom') {
582
+ fromDateTime.value = null
583
+ toDateTime.value = null
584
+ refreshData()
585
+ }
586
+ }
587
+
588
+ // Function to load filter options
589
+ const loadFilterOptions = async () => {
590
+ try {
591
+ const [queues, namespaces] = await Promise.all([
592
+ api.getQueues(),
593
+ api.getNamespaces()
594
+ ])
595
+
596
+ // Format queue options
597
+ if (queues?.queues && Array.isArray(queues.queues)) {
598
+ queueOptions.value = queues.queues.map(q => ({
599
+ label: q.queue || 'Unknown',
600
+ value: q.queue || ''
601
+ }))
602
+ } else {
603
+ queueOptions.value = []
604
+ }
605
+
606
+ // Format namespace options - handle both array and object responses
607
+ if (namespaces) {
608
+ const namespaceList = Array.isArray(namespaces) ? namespaces : (namespaces.namespaces || [])
609
+ if (Array.isArray(namespaceList)) {
610
+ namespaceOptions.value = namespaceList.map(ns => ({
611
+ label: ns.namespace || 'Default',
612
+ value: ns.namespace || ''
613
+ }))
614
+ } else {
615
+ namespaceOptions.value = []
616
+ }
617
+ } else {
618
+ namespaceOptions.value = []
619
+ }
620
+ } catch (error) {
621
+ console.error('Failed to load filter options:', error)
622
+ // Ensure arrays are set even on error
623
+ queueOptions.value = []
624
+ namespaceOptions.value = []
625
+ }
626
+ }
627
+
628
+ onMounted(() => {
629
+ loadFilterOptions()
630
+ fetchData()
631
+
632
+ // Subscribe to WebSocket events
633
+ websocket.on('message', handleWebSocketEvent)
634
+
635
+ // Auto-refresh every 30 seconds
636
+ const refreshInterval = setInterval(() => {
637
+ fetchData()
638
+ }, 30000)
639
+
640
+ // Store interval ID for cleanup
641
+ window.dashboardRefreshInterval = refreshInterval
642
+ })
643
+
644
+ onUnmounted(() => {
645
+ // Clean up WebSocket listener
646
+ websocket.off('message', handleWebSocketEvent)
647
+
648
+ // Clear refresh interval
649
+ if (window.dashboardRefreshInterval) {
650
+ clearInterval(window.dashboardRefreshInterval)
651
+ }
652
+ })
653
+ </script>
654
+
655
+ <style scoped>
656
+ .dashboard {
657
+ padding: 0;
658
+ width: 100%;
659
+ max-width: 1600px;
660
+ margin: 0 auto;
661
+ }
662
+
663
+ /* Filter Bar */
664
+ .filter-bar {
665
+ display: flex;
666
+ gap: 1rem;
667
+ padding: 1rem;
668
+ background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
669
+ border-radius: 12px;
670
+ margin-bottom: 1.5rem;
671
+ flex-wrap: wrap;
672
+ align-items: flex-end;
673
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
674
+ }
675
+
676
+ .filter-group {
677
+ display: flex;
678
+ flex-direction: column;
679
+ gap: 0.5rem;
680
+ min-width: 150px;
681
+ }
682
+
683
+ .filter-group label {
684
+ font-size: 0.875rem;
685
+ color: #a0a0a0;
686
+ font-weight: 500;
687
+ }
688
+
689
+ .filter-group :deep(.p-select),
690
+ .filter-group :deep(.p-calendar) {
691
+ background: rgba(255, 255, 255, 0.05);
692
+ border: 1px solid rgba(255, 255, 255, 0.1);
693
+ border-radius: 8px;
694
+ }
695
+
696
+ .filter-group :deep(.p-select:hover),
697
+ .filter-group :deep(.p-calendar:hover) {
698
+ border-color: rgba(236, 72, 153, 0.5);
699
+ }
700
+
701
+ .filter-group :deep(.p-select-label),
702
+ .filter-group :deep(.p-inputtext) {
703
+ color: #ffffff;
704
+ }
705
+
706
+ .filter-group :deep(.p-button) {
707
+ background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
708
+ border: none;
709
+ border-radius: 8px;
710
+ padding: 0.75rem 1.5rem;
711
+ font-weight: 500;
712
+ transition: all 0.3s ease;
713
+ }
714
+
715
+ .filter-group :deep(.p-button:hover) {
716
+ transform: translateY(-2px);
717
+ box-shadow: 0 4px 12px rgba(236, 72, 153, 0.3);
718
+ }
719
+
720
+ /* Header removed - no longer needed */
721
+
722
+ .metrics-grid {
723
+ display: grid;
724
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
725
+ gap: 1rem;
726
+ margin-bottom: 1.5rem;
727
+ }
728
+
729
+ .charts-row {
730
+ display: grid;
731
+ grid-template-columns: 1fr 1fr;
732
+ gap: 1.5rem;
733
+ margin-bottom: 1.5rem;
734
+ }
735
+
736
+ .bottom-row {
737
+ display: grid;
738
+ grid-template-columns: 1.5fr 1fr;
739
+ gap: 1.5rem;
740
+ }
741
+
742
+ .card-v3 {
743
+ background: transparent;
744
+ border: 1px solid rgba(255, 255, 255, 0.1);
745
+ border-radius: 16px;
746
+ padding: 1.5rem;
747
+ position: relative;
748
+ overflow: hidden;
749
+ }
750
+
751
+ .card-header {
752
+ display: flex;
753
+ justify-content: space-between;
754
+ align-items: center;
755
+ margin-bottom: 1.5rem;
756
+ }
757
+
758
+ .card-header h3 {
759
+ font-size: 1.125rem;
760
+ font-weight: 600;
761
+ color: var(--surface-700);
762
+ margin: 0;
763
+ }
764
+
765
+ /* Queue table styling */
766
+ .queue-name-cell {
767
+ display: flex;
768
+ align-items: center;
769
+ gap: 0.75rem;
770
+ }
771
+
772
+ .queue-icon {
773
+ width: 32px;
774
+ height: 32px;
775
+ border-radius: 8px;
776
+ background: var(--gradient-primary);
777
+ display: flex;
778
+ align-items: center;
779
+ justify-content: center;
780
+ color: white;
781
+ font-weight: 600;
782
+ font-size: 0.875rem;
783
+ }
784
+
785
+ .namespace-tag {
786
+ background: rgba(236, 72, 153, 0.15);
787
+ color: var(--primary-500);
788
+ border: 1px solid rgba(236, 72, 153, 0.3);
789
+ }
790
+
791
+ .rate-value {
792
+ color: var(--primary-500);
793
+ font-weight: 600;
794
+ font-size: 0.875rem;
795
+ }
796
+
797
+ .action-btn {
798
+ color: var(--surface-500) !important;
799
+ }
800
+
801
+ .action-btn:hover {
802
+ background: rgba(236, 72, 153, 0.1) !important;
803
+ color: var(--primary-500) !important;
804
+ }
805
+
806
+ /* Activity feed styling */
807
+ .activity-feed {
808
+ background: transparent;
809
+ border: 1px solid rgba(255, 255, 255, 0.1);
810
+ border-radius: 16px;
811
+ padding: 1.5rem;
812
+ max-height: 500px;
813
+ overflow: hidden;
814
+ display: flex;
815
+ flex-direction: column;
816
+ }
817
+
818
+ .activity-header {
819
+ display: flex;
820
+ justify-content: space-between;
821
+ align-items: center;
822
+ margin-bottom: 1.5rem;
823
+ }
824
+
825
+ .activity-header h3 {
826
+ font-size: 1.125rem;
827
+ font-weight: 600;
828
+ color: var(--surface-700);
829
+ margin: 0;
830
+ }
831
+
832
+ .live-indicator {
833
+ display: flex;
834
+ align-items: center;
835
+ gap: 0.5rem;
836
+ color: var(--success-color);
837
+ font-size: 0.875rem;
838
+ font-weight: 500;
839
+ }
840
+
841
+ .pulse-dot {
842
+ width: 8px;
843
+ height: 8px;
844
+ border-radius: 50%;
845
+ background: var(--success-color);
846
+ animation: pulse 2s infinite;
847
+ }
848
+
849
+ /* Distribution card is already styled in main.css */
850
+
851
+ /* Override DataTable styles for dark theme */
852
+ :deep(.dark-table-v3) {
853
+ background: transparent !important;
854
+ border: none !important;
855
+ }
856
+
857
+ :deep(.dark-table-v3 .p-datatable-thead > tr > th) {
858
+ background: var(--surface-0) !important;
859
+ color: var(--surface-400) !important;
860
+ border-color: rgba(255, 255, 255, 0.05) !important;
861
+ }
862
+
863
+ :deep(.dark-table-v3 .p-datatable-tbody > tr) {
864
+ background: transparent !important;
865
+ }
866
+
867
+ :deep(.dark-table-v3 .p-datatable-tbody > tr:hover) {
868
+ background: rgba(236, 72, 153, 0.05) !important;
869
+ }
870
+
871
+ :deep(.dark-table-v3 .p-datatable-tbody > tr > td) {
872
+ color: var(--surface-600) !important;
873
+ border-color: rgba(255, 255, 255, 0.03) !important;
874
+ }
875
+
876
+ /* Responsive */
877
+ @media (max-width: 1024px) {
878
+ .charts-row {
879
+ grid-template-columns: 1fr;
880
+ }
881
+
882
+ .bottom-row {
883
+ grid-template-columns: 1fr;
884
+ }
885
+
886
+ .metrics-grid {
887
+ grid-template-columns: repeat(2, 1fr);
888
+ }
889
+ }
890
+
891
+ @media (max-width: 768px) {
892
+ .dashboard-header {
893
+ flex-direction: column;
894
+ align-items: flex-start;
895
+ gap: 1rem;
896
+ }
897
+
898
+ .metrics-grid {
899
+ grid-template-columns: 1fr;
900
+ }
901
+
902
+ .activity-feed {
903
+ max-height: 400px;
904
+ }
905
+ }
906
+ </style>