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,912 @@
1
+ <template>
2
+ <div class="analytics-view">
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="fetchAnalytics"
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="fetchAnalytics"
58
+ />
59
+ </div>
60
+ <div class="filter-group">
61
+ <Button
62
+ label="Apply Filters"
63
+ icon="pi pi-filter"
64
+ @click="fetchAnalytics"
65
+ :loading="loading"
66
+ />
67
+ </div>
68
+ </div>
69
+ <div class="analytics-grid">
70
+ <div class="chart-container">
71
+ <div class="chart-header">
72
+ <h3 class="chart-title">System Throughput (Last Hour)</h3>
73
+ <div class="chart-actions">
74
+ <div class="realtime-indicator" v-tooltip="'Real-time updates via WebSocket'">
75
+ <i class="pi pi-circle-fill realtime-dot"></i>
76
+ <span class="realtime-text">Live</span>
77
+ </div>
78
+ <Button
79
+ icon="pi pi-refresh"
80
+ class="p-button-text p-button-sm"
81
+ @click="fetchAnalytics"
82
+ :loading="loading"
83
+ v-tooltip="'Refresh'"
84
+ />
85
+ </div>
86
+ </div>
87
+ <ThroughputChart :data="throughputData" :loading="loading" />
88
+ </div>
89
+
90
+ <div class="card-v3">
91
+ <h3 class="card-title">Queue Performance</h3>
92
+ <DataTable :value="queueStats" :loading="loading" size="small" class="dark-table-v3">
93
+ <Column field="queue" header="Queue" />
94
+ <Column field="throughput" header="Throughput">
95
+ <template #body="{ data }">
96
+ <span class="metric-value">{{ data.throughput }} msg/min</span>
97
+ </template>
98
+ </Column>
99
+ <Column field="successRate" header="Success Rate">
100
+ <template #body="{ data }">
101
+ <Tag :value="`${data.successRate}%`" :severity="getSuccessRateSeverity(data.successRate)" />
102
+ </template>
103
+ </Column>
104
+ </DataTable>
105
+ </div>
106
+
107
+ <!-- Queue Lag Analysis -->
108
+ <div class="lag-chart-wrapper full-width">
109
+ <QueueLagChart />
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </template>
114
+
115
+ <script setup>
116
+ import { ref, onMounted, onUnmounted } from 'vue'
117
+ import { useToast } from 'primevue/usetoast'
118
+ import DataTable from 'primevue/datatable'
119
+ import Column from 'primevue/column'
120
+ import Button from 'primevue/button'
121
+ import Tag from 'primevue/tag'
122
+ import Select from 'primevue/select'
123
+ import Calendar from 'primevue/calendar'
124
+ import ThroughputChart from '../components/charts/ThroughputChart.vue'
125
+ import QueueLagChart from '../components/charts/QueueLagChart.vue'
126
+ import api from '../services/api.js'
127
+ import websocket from '../services/websocket.js'
128
+
129
+ const toast = useToast()
130
+ const loading = ref(false)
131
+
132
+ // Filter state
133
+ const selectedTimeRange = ref('1h')
134
+ const fromDateTime = ref(null)
135
+ const toDateTime = ref(null)
136
+ const selectedQueue = ref(null)
137
+ const selectedNamespace = ref(null)
138
+ const selectedTask = ref(null)
139
+
140
+ // Filter options
141
+ const timeRangeOptions = ref([
142
+ { label: 'Last Hour', value: '1h' },
143
+ { label: 'Last 6 Hours', value: '6h' },
144
+ { label: 'Last 24 Hours', value: '24h' },
145
+ { label: 'Last 7 Days', value: '7d' },
146
+ { label: 'Last 30 Days', value: '30d' },
147
+ { label: 'Custom Range', value: 'custom' }
148
+ ])
149
+
150
+ const queueOptions = ref([])
151
+ const namespaceOptions = ref([])
152
+
153
+ // Data
154
+ const throughputData = ref({
155
+ labels: [],
156
+ datasets: []
157
+ })
158
+ const queueStats = ref([])
159
+ const systemMetrics = ref({})
160
+ const topQueues = ref([])
161
+ const errorSummary = ref([])
162
+
163
+ // Real-time data tracking
164
+ const realtimeMetrics = ref({
165
+ currentMinuteIncoming: 0,
166
+ currentMinuteCompleted: 0,
167
+ currentMinuteFailed: 0,
168
+ currentMinuteDeadLetter: 0,
169
+ lastMinuteTimestamp: null
170
+ })
171
+
172
+ // Queue depth cache for smooth updates
173
+ const queueDepthCache = ref(new Map())
174
+
175
+ // Smooth update functions
176
+ const updateThroughputDataPoint = (timestamp, incoming = 0, completed = 0, failed = 0, deadLetter = 0) => {
177
+ const currentData = throughputData.value
178
+ if (!currentData.labels || currentData.labels.length === 0) return
179
+
180
+ const timeLabel = new Date(timestamp).toLocaleTimeString('en-US', {
181
+ hour: 'numeric',
182
+ minute: '2-digit'
183
+ })
184
+
185
+ // Add new data point and remove oldest if we have more than 60 points
186
+ currentData.labels.push(timeLabel)
187
+ currentData.datasets[0].data.push(incoming)
188
+ currentData.datasets[1].data.push(completed)
189
+ currentData.datasets[2].data.push(failed)
190
+ currentData.datasets[3].data.push(deadLetter)
191
+
192
+ // Keep only last 60 data points for smooth scrolling
193
+ if (currentData.labels.length > 60) {
194
+ currentData.labels.shift()
195
+ currentData.datasets.forEach(dataset => dataset.data.shift())
196
+ }
197
+
198
+ // Trigger reactivity
199
+ throughputData.value = { ...currentData }
200
+ }
201
+
202
+ const smoothUpdateQueueStats = (queueName, newStats) => {
203
+ const currentStats = queueStats.value
204
+ const existingIndex = currentStats.findIndex(q => q.queue === queueName)
205
+
206
+ if (existingIndex >= 0) {
207
+ // Smooth transition for existing queue
208
+ const existing = currentStats[existingIndex]
209
+ const updated = {
210
+ ...existing,
211
+ throughput: Math.round(newStats.completed / 60),
212
+ successRate: newStats.total > 0 ? ((newStats.completed / newStats.total) * 100).toFixed(1) : existing.successRate
213
+ }
214
+
215
+ // Only update if values actually changed to avoid unnecessary re-renders
216
+ if (updated.throughput !== existing.throughput || updated.successRate !== existing.successRate) {
217
+ currentStats[existingIndex] = updated
218
+ queueStats.value = [...currentStats]
219
+ }
220
+ } else if (newStats.total > 0) {
221
+ // Add new queue
222
+ currentStats.push({
223
+ queue: queueName,
224
+ throughput: Math.round(newStats.completed / 60),
225
+ successRate: newStats.total > 0 ? ((newStats.completed / newStats.total) * 100).toFixed(1) : 100
226
+ })
227
+ queueStats.value = currentStats.sort((a, b) => b.throughput - a.throughput).slice(0, 10)
228
+ }
229
+ }
230
+
231
+ const incrementRealtimeMetrics = (eventType) => {
232
+ const now = new Date()
233
+ const currentMinute = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes())
234
+
235
+ // Reset counters if we've moved to a new minute
236
+ if (!realtimeMetrics.value.lastMinuteTimestamp ||
237
+ realtimeMetrics.value.lastMinuteTimestamp.getTime() !== currentMinute.getTime()) {
238
+
239
+ // If we have data from the previous minute, add it to the chart
240
+ if (realtimeMetrics.value.lastMinuteTimestamp) {
241
+ updateThroughputDataPoint(
242
+ realtimeMetrics.value.lastMinuteTimestamp,
243
+ realtimeMetrics.value.currentMinuteIncoming,
244
+ realtimeMetrics.value.currentMinuteCompleted,
245
+ realtimeMetrics.value.currentMinuteFailed,
246
+ realtimeMetrics.value.currentMinuteDeadLetter
247
+ )
248
+ }
249
+
250
+ // Reset for new minute
251
+ realtimeMetrics.value.currentMinuteIncoming = 0
252
+ realtimeMetrics.value.currentMinuteCompleted = 0
253
+ realtimeMetrics.value.currentMinuteFailed = 0
254
+ realtimeMetrics.value.currentMinuteDeadLetter = 0
255
+ realtimeMetrics.value.lastMinuteTimestamp = currentMinute
256
+ }
257
+
258
+ // Increment appropriate counter
259
+ switch (eventType) {
260
+ case 'message.pushed':
261
+ realtimeMetrics.value.currentMinuteIncoming++
262
+ break
263
+ case 'message.completed':
264
+ realtimeMetrics.value.currentMinuteCompleted++
265
+ break
266
+ case 'message.failed':
267
+ realtimeMetrics.value.currentMinuteFailed++
268
+ break
269
+ case 'message.dead_letter':
270
+ realtimeMetrics.value.currentMinuteDeadLetter++
271
+ break
272
+ }
273
+ }
274
+
275
+ // WebSocket event handlers
276
+ const handleQueueDepthUpdate = (data) => {
277
+ // Update queue depth cache
278
+ queueDepthCache.value.set(data.queue, data)
279
+
280
+ // Calculate aggregated stats for this queue
281
+ const totalStats = {
282
+ pending: data.totalDepth || 0,
283
+ processing: data.totalProcessing || 0,
284
+ completed: 0,
285
+ failed: 0,
286
+ total: 0
287
+ }
288
+
289
+ // Add partition stats if available
290
+ if (data.partitions) {
291
+ Object.values(data.partitions).forEach(partition => {
292
+ totalStats.completed += partition.completed || 0
293
+ totalStats.failed += partition.failed || 0
294
+ totalStats.total += (partition.depth || 0) + (partition.processing || 0) + (partition.completed || 0) + (partition.failed || 0)
295
+ })
296
+ }
297
+
298
+ // Smooth update queue stats
299
+ smoothUpdateQueueStats(data.queue, totalStats)
300
+ }
301
+
302
+ const handleSystemStats = (data) => {
303
+ // Smooth update system metrics without full replacement
304
+ const current = systemMetrics.value
305
+ systemMetrics.value = {
306
+ ...current,
307
+ connections: data.connections || current.connections || 0,
308
+ // Only update if significantly different to avoid jitter
309
+ requestRate: Math.abs((data.recentCreated || 0) - (current.requestRate || 0)) > 5
310
+ ? data.recentCreated || 0
311
+ : current.requestRate || 0,
312
+ latency: current.latency || 0, // Keep existing latency
313
+ errorRate: current.errorRate || 0, // Keep existing error rate
314
+ memory: current.memory || 0, // Keep existing memory
315
+ uptime: current.uptime || 0 // Keep existing uptime
316
+ }
317
+ }
318
+
319
+ const handleWebSocketMessage = (wsMessage) => {
320
+ const { event, data } = wsMessage
321
+
322
+ switch (event) {
323
+ case 'queue.depth':
324
+ handleQueueDepthUpdate(data)
325
+ break
326
+ case 'system.stats':
327
+ handleSystemStats(data)
328
+ break
329
+ case 'message.pushed':
330
+ case 'message.completed':
331
+ case 'message.failed':
332
+ case 'message.dead_letter':
333
+ incrementRealtimeMetrics(event)
334
+ break
335
+ }
336
+ }
337
+
338
+ // Fetch analytics data
339
+ const fetchAnalytics = async (silent = false) => {
340
+ try {
341
+ // Only show loading indicator for initial load, not for periodic refreshes
342
+ if (!silent) {
343
+ loading.value = true
344
+ }
345
+
346
+ // Build filter parameters
347
+ const filters = {}
348
+
349
+ // Handle time range
350
+ if (selectedTimeRange.value === 'custom') {
351
+ if (fromDateTime.value) filters.fromDateTime = fromDateTime.value.toISOString()
352
+ if (toDateTime.value) filters.toDateTime = toDateTime.value.toISOString()
353
+ } else {
354
+ // Calculate from/to based on selected range
355
+ const now = new Date()
356
+ const from = new Date()
357
+
358
+ switch (selectedTimeRange.value) {
359
+ case '1h':
360
+ from.setHours(from.getHours() - 1)
361
+ break
362
+ case '6h':
363
+ from.setHours(from.getHours() - 6)
364
+ break
365
+ case '24h':
366
+ from.setDate(from.getDate() - 1)
367
+ break
368
+ case '7d':
369
+ from.setDate(from.getDate() - 7)
370
+ break
371
+ case '30d':
372
+ from.setDate(from.getDate() - 30)
373
+ break
374
+ }
375
+
376
+ filters.fromDateTime = from.toISOString()
377
+ filters.toDateTime = now.toISOString()
378
+ }
379
+
380
+ // Add other filters
381
+ if (selectedQueue.value) filters.queue = selectedQueue.value
382
+ if (selectedNamespace.value) filters.namespace = selectedNamespace.value
383
+ if (selectedTask.value) filters.task = selectedTask.value
384
+
385
+ // Fetch throughput data with filters
386
+ const throughput = await api.getThroughput(filters)
387
+ processThroughputData(throughput?.throughput || [])
388
+
389
+ // Fetch real queue stats with filters
390
+ try {
391
+ const queueStatsData = await api.getQueueStats(filters)
392
+ if (queueStatsData && Array.isArray(queueStatsData) && queueStatsData.length > 0) {
393
+ // Process the array of queue-partition data and aggregate by queue
394
+ const queueMap = new Map()
395
+
396
+ queueStatsData.forEach(item => {
397
+ const queueName = item.queue
398
+ const stats = item.stats
399
+
400
+ if (!queueMap.has(queueName)) {
401
+ queueMap.set(queueName, {
402
+ queue: queueName,
403
+ pending: 0,
404
+ processing: 0,
405
+ completed: 0,
406
+ failed: 0,
407
+ total: 0
408
+ })
409
+ }
410
+
411
+ const aggregated = queueMap.get(queueName)
412
+ aggregated.pending += stats.pending || 0
413
+ aggregated.processing += stats.processing || 0
414
+ aggregated.completed += stats.completed || 0
415
+ aggregated.failed += stats.failed || 0
416
+ aggregated.total += stats.total || 0
417
+ })
418
+
419
+ // Convert to array and calculate throughput and success rate
420
+ queueStats.value = Array.from(queueMap.values())
421
+ .filter(queue => queue.total > 0) // Only show queues with activity
422
+ .map(queue => ({
423
+ queue: queue.queue,
424
+ throughput: Math.round(queue.completed / 60), // Rough throughput estimate (completed per minute)
425
+ successRate: queue.total > 0 ? ((queue.completed / queue.total) * 100).toFixed(1) : 0
426
+ }))
427
+ .sort((a, b) => b.throughput - a.throughput) // Sort by throughput descending
428
+ .slice(0, 10) // Limit to top 10
429
+ } else {
430
+ // Fallback: try to get queue data and calculate stats
431
+ const queues = await api.getQueues()
432
+ if (queues?.queues && queues.queues.length > 0) {
433
+ queueStats.value = queues.queues
434
+ .filter(queue => queue.stats && (queue.stats.completed > 0 || queue.stats.pending > 0))
435
+ .map(queue => ({
436
+ queue: queue.name,
437
+ throughput: Math.round((queue.stats.completed || 0) / 60), // Convert to per minute
438
+ successRate: queue.stats.total > 0 ? ((queue.stats.completed / queue.stats.total) * 100).toFixed(1) : 100
439
+ }))
440
+ .slice(0, 10) // Limit to top 10
441
+ } else {
442
+ // Final fallback to mock data
443
+ queueStats.value = [
444
+ { queue: 'email-queue', throughput: 120, successRate: 98.5 },
445
+ { queue: 'notification-queue', throughput: 85, successRate: 99.2 },
446
+ { queue: 'analytics-queue', throughput: 200, successRate: 95.8 },
447
+ { queue: 'payment-queue', throughput: 45, successRate: 99.9 }
448
+ ]
449
+ }
450
+ }
451
+ } catch (error) {
452
+ console.error('Failed to fetch queue stats:', error)
453
+ // Show empty state on error
454
+ queueStats.value = []
455
+ }
456
+
457
+ // Fetch real system metrics from API
458
+ try {
459
+ const health = await api.getHealth()
460
+ systemMetrics.value = {
461
+ connections: health?.connections || 0,
462
+ memory: health?.memory || 0,
463
+ requestRate: health?.requestRate || 0,
464
+ latency: health?.latency || 0,
465
+ errorRate: health?.errorRate || 0,
466
+ uptime: parseInt(health?.uptime) || 0
467
+ }
468
+ } catch (error) {
469
+ // Use defaults if API fails
470
+ systemMetrics.value = {
471
+ connections: 0,
472
+ memory: 0,
473
+ requestRate: 0,
474
+ latency: 0,
475
+ errorRate: 0,
476
+ uptime: 0
477
+ }
478
+ }
479
+
480
+ // Initialize empty arrays - no mock data
481
+ topQueues.value = []
482
+ errorSummary.value = []
483
+
484
+ } catch (error) {
485
+ console.error('Failed to fetch analytics:', error)
486
+ toast.add({
487
+ severity: 'error',
488
+ summary: 'Error',
489
+ detail: 'Failed to load analytics data',
490
+ life: 3000
491
+ })
492
+ } finally {
493
+ loading.value = false
494
+ }
495
+ }
496
+
497
+ // Process throughput data for chart
498
+ const processThroughputData = (data) => {
499
+ if (!data || data.length === 0) {
500
+ // Only generate mock data if we don't have existing data
501
+ if (!throughputData.value.labels || throughputData.value.labels.length === 0) {
502
+ const now = new Date()
503
+ const mockData = []
504
+ for (let i = 23; i >= 0; i--) {
505
+ const time = new Date(now.getTime() - i * 60 * 60 * 1000)
506
+ mockData.push({
507
+ timestamp: time.toISOString(),
508
+ incoming: { messagesPerMinute: Math.floor(Math.random() * 200) + 100 },
509
+ completed: { messagesPerMinute: Math.floor(Math.random() * 180) + 80 },
510
+ failed: { messagesPerMinute: Math.floor(Math.random() * 20) },
511
+ deadLetter: { messagesPerMinute: Math.floor(Math.random() * 5) }
512
+ })
513
+ }
514
+ data = mockData
515
+ } else {
516
+ // Keep existing data if API returns empty
517
+ return
518
+ }
519
+ }
520
+
521
+ // Take last 60 data points for recent view and reverse to show chronological order (oldest to newest)
522
+ const recentData = data.reverse().slice(-60)
523
+
524
+ // Only update if we have significant changes to avoid unnecessary re-renders
525
+ const newLabels = recentData.map(item => {
526
+ const date = new Date(item.timestamp)
527
+ return date.toLocaleTimeString('en-US', {
528
+ hour: 'numeric',
529
+ minute: '2-digit'
530
+ })
531
+ })
532
+
533
+ // Check if data has actually changed
534
+ const currentLabels = throughputData.value.labels || []
535
+ const hasChanged = newLabels.length !== currentLabels.length ||
536
+ newLabels[newLabels.length - 1] !== currentLabels[currentLabels.length - 1]
537
+
538
+ if (hasChanged) {
539
+ throughputData.value = {
540
+ labels: newLabels,
541
+ datasets: [
542
+ {
543
+ label: 'Incoming Messages',
544
+ data: recentData.map(item => item.incoming?.messagesPerMinute || 0)
545
+ },
546
+ {
547
+ label: 'Completed Messages',
548
+ data: recentData.map(item => item.completed?.messagesPerMinute || 0)
549
+ },
550
+ {
551
+ label: 'Failed Messages',
552
+ data: recentData.map(item => item.failed?.messagesPerMinute || 0)
553
+ },
554
+ {
555
+ label: 'Dead Letter Messages',
556
+ data: recentData.map(item => item.deadLetter?.messagesPerMinute || 0)
557
+ }
558
+ ]
559
+ }
560
+ }
561
+ }
562
+
563
+ // Get severity for success rate
564
+ const getSuccessRateSeverity = (rate) => {
565
+ if (rate >= 99) return 'success'
566
+ if (rate >= 95) return 'warning'
567
+ return 'danger'
568
+ }
569
+
570
+ // Format bytes
571
+ const formatBytes = (bytes) => {
572
+ if (!bytes) return '0 B'
573
+ const k = 1024
574
+ const sizes = ['B', 'KB', 'MB', 'GB']
575
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
576
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
577
+ }
578
+
579
+ // Format uptime
580
+ const formatUptime = (seconds) => {
581
+ if (!seconds) return '-'
582
+ const days = Math.floor(seconds / 86400)
583
+ const hours = Math.floor((seconds % 86400) / 3600)
584
+ if (days > 0) {
585
+ return `${days}d ${hours}h`
586
+ }
587
+ return `${hours}h`
588
+ }
589
+
590
+ let refreshInterval = null
591
+
592
+ // Function to handle time range change
593
+ const onTimeRangeChange = () => {
594
+ if (selectedTimeRange.value !== 'custom') {
595
+ fromDateTime.value = null
596
+ toDateTime.value = null
597
+ fetchAnalytics()
598
+ }
599
+ }
600
+
601
+ // Function to load filter options
602
+ const loadFilterOptions = async () => {
603
+ try {
604
+ const [queues, namespaces] = await Promise.all([
605
+ api.getQueues(),
606
+ api.getNamespaces()
607
+ ])
608
+
609
+ // Format queue options
610
+ if (queues?.queues && Array.isArray(queues.queues)) {
611
+ queueOptions.value = queues.queues.map(q => ({
612
+ label: q.queue || 'Unknown',
613
+ value: q.queue || ''
614
+ }))
615
+ } else {
616
+ queueOptions.value = []
617
+ }
618
+
619
+ // Format namespace options - handle both array and object responses
620
+ if (namespaces) {
621
+ const namespaceList = Array.isArray(namespaces) ? namespaces : (namespaces.namespaces || [])
622
+ if (Array.isArray(namespaceList)) {
623
+ namespaceOptions.value = namespaceList.map(ns => ({
624
+ label: ns.namespace || 'Default',
625
+ value: ns.namespace || ''
626
+ }))
627
+ } else {
628
+ namespaceOptions.value = []
629
+ }
630
+ } else {
631
+ namespaceOptions.value = []
632
+ }
633
+ } catch (error) {
634
+ console.error('Failed to load filter options:', error)
635
+ // Ensure arrays are set even on error
636
+ queueOptions.value = []
637
+ namespaceOptions.value = []
638
+ }
639
+ }
640
+
641
+ onMounted(() => {
642
+ loadFilterOptions()
643
+ fetchAnalytics()
644
+
645
+ // Subscribe to WebSocket events for real-time updates
646
+ websocket.on('message', handleWebSocketMessage)
647
+
648
+ // Reduced polling frequency - now every 5 minutes instead of 30 seconds
649
+ // WebSocket handles real-time updates, polling is just for data consistency
650
+ refreshInterval = setInterval(() => {
651
+ fetchAnalytics(true) // Silent refresh to avoid loading indicators
652
+ }, 300000) // 5 minutes
653
+ })
654
+
655
+ onUnmounted(() => {
656
+ // Clean up WebSocket listeners
657
+ websocket.off('message', handleWebSocketMessage)
658
+
659
+ if (refreshInterval) {
660
+ clearInterval(refreshInterval)
661
+ }
662
+ })
663
+ </script>
664
+
665
+ <style scoped>
666
+ .analytics-view {
667
+ padding: 0;
668
+ }
669
+
670
+ /* Filter Bar */
671
+ .filter-bar {
672
+ display: flex;
673
+ gap: 1rem;
674
+ padding: 1rem;
675
+ background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
676
+ border-radius: 12px;
677
+ margin-bottom: 1.5rem;
678
+ flex-wrap: wrap;
679
+ align-items: flex-end;
680
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
681
+ }
682
+
683
+ .filter-group {
684
+ display: flex;
685
+ flex-direction: column;
686
+ gap: 0.5rem;
687
+ min-width: 150px;
688
+ }
689
+
690
+ .filter-group label {
691
+ font-size: 0.875rem;
692
+ color: #a0a0a0;
693
+ font-weight: 500;
694
+ }
695
+
696
+ .filter-group :deep(.p-select),
697
+ .filter-group :deep(.p-calendar) {
698
+ background: rgba(255, 255, 255, 0.05);
699
+ border: 1px solid rgba(255, 255, 255, 0.1);
700
+ border-radius: 8px;
701
+ }
702
+
703
+ .filter-group :deep(.p-select:hover),
704
+ .filter-group :deep(.p-calendar:hover) {
705
+ border-color: rgba(236, 72, 153, 0.5);
706
+ }
707
+
708
+ .filter-group :deep(.p-select-label),
709
+ .filter-group :deep(.p-inputtext) {
710
+ color: #ffffff;
711
+ }
712
+
713
+ .filter-group :deep(.p-button) {
714
+ background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
715
+ border: none;
716
+ border-radius: 8px;
717
+ padding: 0.75rem 1.5rem;
718
+ font-weight: 500;
719
+ transition: all 0.3s ease;
720
+ }
721
+
722
+ .filter-group :deep(.p-button:hover) {
723
+ transform: translateY(-2px);
724
+ box-shadow: 0 4px 12px rgba(236, 72, 153, 0.3);
725
+ }
726
+
727
+ .analytics-grid {
728
+ display: grid;
729
+ grid-template-columns: 1fr 1fr;
730
+ gap: 1.5rem;
731
+ }
732
+
733
+ .card-v3 {
734
+ background: transparent;
735
+ border: 1px solid rgba(255, 255, 255, 0.1);
736
+ border-radius: 16px;
737
+ padding: 1.5rem;
738
+ position: relative;
739
+ overflow: hidden;
740
+ }
741
+
742
+ .card-title {
743
+ font-size: 1.125rem;
744
+ font-weight: 600;
745
+ color: var(--surface-700);
746
+ margin: 0 0 1.5rem 0;
747
+ }
748
+
749
+ .chart-container {
750
+ background: transparent;
751
+ border: 1px solid rgba(255, 255, 255, 0.1);
752
+ border-radius: 16px;
753
+ padding: 1.5rem;
754
+ height: 100%;
755
+ display: flex;
756
+ flex-direction: column;
757
+ }
758
+
759
+ .chart-header {
760
+ display: flex;
761
+ justify-content: space-between;
762
+ align-items: center;
763
+ margin-bottom: 1.5rem;
764
+ }
765
+
766
+ .chart-title {
767
+ font-size: 1.125rem;
768
+ font-weight: 600;
769
+ color: var(--surface-700);
770
+ margin: 0;
771
+ }
772
+
773
+ .chart-actions {
774
+ display: flex;
775
+ align-items: center;
776
+ gap: 1rem;
777
+ }
778
+
779
+ .realtime-indicator {
780
+ display: flex;
781
+ align-items: center;
782
+ gap: 0.5rem;
783
+ padding: 0.25rem 0.75rem;
784
+ background: rgba(34, 197, 94, 0.1);
785
+ border: 1px solid rgba(34, 197, 94, 0.2);
786
+ border-radius: 12px;
787
+ font-size: 0.75rem;
788
+ font-weight: 500;
789
+ }
790
+
791
+ .realtime-dot {
792
+ color: #22c55e;
793
+ font-size: 0.5rem;
794
+ animation: pulse 2s infinite;
795
+ }
796
+
797
+ .realtime-text {
798
+ color: #22c55e;
799
+ text-transform: uppercase;
800
+ letter-spacing: 0.05em;
801
+ }
802
+
803
+ @keyframes pulse {
804
+ 0%, 100% {
805
+ opacity: 1;
806
+ }
807
+ 50% {
808
+ opacity: 0.5;
809
+ }
810
+ }
811
+
812
+ .system-metrics-card {
813
+ margin-top: 1.5rem;
814
+ }
815
+
816
+ .metrics-grid {
817
+ display: grid;
818
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
819
+ gap: 1.5rem;
820
+ }
821
+
822
+ .metric-item {
823
+ display: flex;
824
+ flex-direction: column;
825
+ gap: 0.5rem;
826
+ }
827
+
828
+ .metric-label {
829
+ font-size: 0.75rem;
830
+ color: var(--surface-400);
831
+ text-transform: uppercase;
832
+ letter-spacing: 0.05em;
833
+ }
834
+
835
+ .metric-value {
836
+ font-size: 1.25rem;
837
+ font-weight: 600;
838
+ color: var(--primary-500);
839
+ }
840
+
841
+ .queue-name-cell {
842
+ display: flex;
843
+ align-items: center;
844
+ gap: 0.5rem;
845
+ }
846
+
847
+ .queue-icon-small {
848
+ width: 24px;
849
+ height: 24px;
850
+ border-radius: 6px;
851
+ background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
852
+ display: flex;
853
+ align-items: center;
854
+ justify-content: center;
855
+ color: white;
856
+ font-weight: 600;
857
+ font-size: 0.75rem;
858
+ }
859
+
860
+ /* DataTable dark theme overrides */
861
+ :deep(.dark-table-v3) {
862
+ background: transparent !important;
863
+ border: none !important;
864
+ }
865
+
866
+ :deep(.dark-table-v3 .p-datatable-thead > tr > th) {
867
+ background: transparent !important;
868
+ color: var(--surface-400) !important;
869
+ border-color: rgba(255, 255, 255, 0.1) !important;
870
+ padding: 0.75rem;
871
+ }
872
+
873
+ :deep(.dark-table-v3 .p-datatable-tbody > tr) {
874
+ background: transparent !important;
875
+ }
876
+
877
+ :deep(.dark-table-v3 .p-datatable-tbody > tr:hover) {
878
+ background: rgba(236, 72, 153, 0.05) !important;
879
+ }
880
+
881
+ :deep(.dark-table-v3 .p-datatable-tbody > tr > td) {
882
+ color: var(--surface-600) !important;
883
+ border-color: rgba(255, 255, 255, 0.05) !important;
884
+ padding: 0.625rem 0.75rem;
885
+ }
886
+
887
+ .full-width {
888
+ grid-column: 1 / -1;
889
+ }
890
+
891
+ .lag-chart-wrapper {
892
+ background: #0a0a0a;
893
+ border: 1px solid rgba(255, 255, 255, 0.1);
894
+ border-radius: 16px;
895
+ overflow: hidden;
896
+ position: relative;
897
+ z-index: 1;
898
+ }
899
+
900
+ /* Responsive */
901
+ @media (max-width: 1024px) {
902
+ .analytics-grid {
903
+ grid-template-columns: 1fr;
904
+ }
905
+ }
906
+
907
+ @media (max-width: 768px) {
908
+ .metrics-grid {
909
+ grid-template-columns: repeat(2, 1fr);
910
+ }
911
+ }
912
+ </style>