queen-mq 0.1.1 → 0.1.2

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 (139) hide show
  1. package/API.md +1226 -0
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +276 -53
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-dashboard-api.js +200 -0
  17. package/package.json +4 -2
  18. package/server.log +1 -0
  19. package/src/benchmark/consumer.js +207 -0
  20. package/src/benchmark/consumer_multi.js +216 -0
  21. package/src/benchmark/producer.js +75 -0
  22. package/src/benchmark/producer_multi.js +115 -0
  23. package/src/client/client.js +192 -16
  24. package/src/client/queenClient.js +5 -0
  25. package/src/cluster-server.js +242 -0
  26. package/src/config.js +19 -5
  27. package/src/database/connection.js +42 -16
  28. package/src/database/poolManager.js +7 -0
  29. package/src/database/schema-v2.sql +194 -130
  30. package/src/managers/queueManagerOptimized.js +823 -933
  31. package/src/managers/systemEventManager.js +8 -3
  32. package/src/routes/messages.js +127 -57
  33. package/src/routes/pop.js +27 -43
  34. package/src/routes/resources.js +61 -27
  35. package/src/routes/status.js +1037 -0
  36. package/src/server.js +308 -272
  37. package/src/services/evictionService.js +57 -28
  38. package/src/services/retentionService.js +44 -11
  39. package/src/test/advanced-pattern-tests.js +5 -5
  40. package/src/test/bus-mode-tests.js +24 -11
  41. package/src/test/edge-case-tests.js +12 -5
  42. package/src/test/enterprise-tests.js +48 -15
  43. package/src/test/utils.js +1 -1
  44. package/src/utils/streaming.js +231 -0
  45. package/src/utils/uuid.js +2 -2
  46. package/src/websocket/wsServer.js +10 -3
  47. package/test-keepalive-v2.sh +22 -0
  48. package/webapp/COLOR_GUIDE.md +118 -0
  49. package/webapp/README.md +143 -0
  50. package/webapp/index.html +14 -0
  51. package/webapp/package-lock.json +3184 -0
  52. package/webapp/package.json +25 -0
  53. package/webapp/postcss.config.js +7 -0
  54. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  55. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  56. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  57. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  58. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  59. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  60. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  61. package/webapp/public/assets/queen-logo.svg +263 -0
  62. package/webapp/src/App.vue +19 -0
  63. package/webapp/src/api/analytics.js +10 -0
  64. package/webapp/src/api/client.js +29 -0
  65. package/webapp/src/api/consumers.js +52 -0
  66. package/webapp/src/api/health.js +7 -0
  67. package/webapp/src/api/messages.js +26 -0
  68. package/webapp/src/api/queues.js +14 -0
  69. package/webapp/src/api/resources.js +8 -0
  70. package/webapp/src/assets/styles/main.css +357 -0
  71. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  72. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  73. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  74. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  75. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  76. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  77. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  78. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  79. package/webapp/src/components/common/MetricCard.vue +43 -0
  80. package/webapp/src/components/common/StatusBadge.vue +45 -0
  81. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  82. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  83. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  84. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  85. package/webapp/src/components/layout/AppLayout.vue +110 -0
  86. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  87. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  88. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  89. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  90. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  91. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  92. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  93. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  94. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  95. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  96. package/webapp/src/composables/useApi.js +34 -0
  97. package/webapp/src/composables/useTheme.js +36 -0
  98. package/webapp/src/main.js +11 -0
  99. package/webapp/src/router/index.js +42 -0
  100. package/webapp/src/utils/colors.js +96 -0
  101. package/webapp/src/utils/formatters.js +49 -0
  102. package/webapp/src/views/Analytics.vue +377 -0
  103. package/webapp/src/views/ConsumerGroups.vue +433 -0
  104. package/webapp/src/views/Dashboard.vue +418 -0
  105. package/webapp/src/views/Messages.vue +361 -0
  106. package/webapp/src/views/QueueDetail.vue +582 -0
  107. package/webapp/src/views/Queues.vue +496 -0
  108. package/webapp/tailwind.config.js +25 -0
  109. package/webapp/vite.config.js +10 -0
  110. package/dashboard/.vscode/extensions.json +0 -3
  111. package/dashboard/README.md +0 -5
  112. package/dashboard/index.html +0 -14
  113. package/dashboard/package-lock.json +0 -1458
  114. package/dashboard/package.json +0 -25
  115. package/dashboard/public/vite.svg +0 -1
  116. package/dashboard/src/App.vue +0 -29
  117. package/dashboard/src/assets/styles/main.css +0 -908
  118. package/dashboard/src/assets/vue.svg +0 -1
  119. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  120. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  121. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  122. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  123. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  124. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  125. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  126. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  127. package/dashboard/src/main.js +0 -44
  128. package/dashboard/src/router.js +0 -54
  129. package/dashboard/src/services/api.js +0 -187
  130. package/dashboard/src/services/websocket.js +0 -167
  131. package/dashboard/src/utils/constants.js +0 -56
  132. package/dashboard/src/utils/helpers.js +0 -118
  133. package/dashboard/src/views/Analytics.vue +0 -912
  134. package/dashboard/src/views/Dashboard.vue +0 -906
  135. package/dashboard/src/views/Messages.vue +0 -437
  136. package/dashboard/src/views/QueueDetail.vue +0 -501
  137. package/dashboard/src/views/Queues.vue +0 -333
  138. package/dashboard/vite.config.js +0 -30
  139. package/src/routes/analytics.js +0 -812
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
@@ -1,298 +0,0 @@
1
- <template>
2
- <div class="metric-card-v3" :class="`metric-${color}`">
3
- <div class="metric-icon-wrapper">
4
- <i :class="icon"></i>
5
- </div>
6
-
7
- <div class="metric-content">
8
- <p class="metric-label">{{ title }}</p>
9
-
10
- <h3 class="metric-value" v-if="!loading">
11
- {{ formattedValue }}{{ suffix }}
12
- </h3>
13
- <Skeleton v-else height="2rem" width="80%" />
14
-
15
- <p class="metric-change" :class="changeClass" v-if="change !== null && !loading">
16
- <i :class="changeIcon"></i>
17
- <span>{{ Math.abs(change) }}%</span>
18
- <span class="metric-period">{{ period }}</span>
19
- </p>
20
- </div>
21
-
22
- <div class="metric-sparkline" v-if="sparklineData && sparklineData.length > 0">
23
- <svg viewBox="0 0 80 30" preserveAspectRatio="none">
24
- <polyline
25
- :points="sparklinePoints"
26
- fill="none"
27
- :stroke="sparklineColor"
28
- stroke-width="2"
29
- stroke-linecap="round"
30
- stroke-linejoin="round"
31
- />
32
- </svg>
33
- </div>
34
- </div>
35
- </template>
36
-
37
- <script setup>
38
- import { computed } from 'vue'
39
- import Skeleton from 'primevue/skeleton'
40
-
41
- const props = defineProps({
42
- title: {
43
- type: String,
44
- required: true
45
- },
46
- value: {
47
- type: [String, Number],
48
- required: true
49
- },
50
- icon: {
51
- type: String,
52
- default: 'pi pi-chart-line'
53
- },
54
- color: {
55
- type: String,
56
- default: 'primary',
57
- validator: (value) => ['primary', 'success', 'warning', 'danger', 'info'].includes(value)
58
- },
59
- change: {
60
- type: Number,
61
- default: null
62
- },
63
- period: {
64
- type: String,
65
- default: 'vs last hour'
66
- },
67
- suffix: {
68
- type: String,
69
- default: ''
70
- },
71
- loading: {
72
- type: Boolean,
73
- default: false
74
- },
75
- sparklineData: {
76
- type: Array,
77
- default: () => []
78
- }
79
- })
80
-
81
- // Format large numbers
82
- const formattedValue = computed(() => {
83
- const val = typeof props.value === 'string' ? parseFloat(props.value) : props.value
84
- if (isNaN(val)) return props.value
85
-
86
- if (val >= 1000000) {
87
- return (val / 1000000).toFixed(1) + 'M'
88
- } else if (val >= 1000) {
89
- return (val / 1000).toFixed(1) + 'K'
90
- }
91
- return val.toLocaleString()
92
- })
93
-
94
- // Change indicator
95
- const changeClass = computed(() => ({
96
- positive: props.change > 0,
97
- negative: props.change < 0,
98
- neutral: props.change === 0
99
- }))
100
-
101
- const changeIcon = computed(() => {
102
- if (props.change > 0) return 'pi pi-arrow-up'
103
- if (props.change < 0) return 'pi pi-arrow-down'
104
- return 'pi pi-minus'
105
- })
106
-
107
- // Sparkline calculation
108
- const sparklinePoints = computed(() => {
109
- if (!props.sparklineData || props.sparklineData.length === 0) return ''
110
-
111
- const data = props.sparklineData
112
- const max = Math.max(...data)
113
- const min = Math.min(...data)
114
- const range = max - min || 1
115
-
116
- return data.map((value, index) => {
117
- const x = (index / (data.length - 1)) * 80
118
- const y = 30 - ((value - min) / range) * 25
119
- return `${x},${y}`
120
- }).join(' ')
121
- })
122
-
123
- const sparklineColor = computed(() => {
124
- const colors = {
125
- primary: '#ec4899',
126
- success: '#10b981',
127
- warning: '#f59e0b',
128
- danger: '#ef4444',
129
- info: '#06b6d4'
130
- }
131
- return colors[props.color] || colors.primary
132
- })
133
- </script>
134
-
135
- <style scoped>
136
- .metric-card-v3 {
137
- background: transparent;
138
- border: 1px solid rgba(255, 255, 255, 0.08);
139
- border-radius: 12px;
140
- padding: 1.25rem;
141
- display: flex;
142
- align-items: flex-start;
143
- gap: 1rem;
144
- position: relative;
145
- overflow: hidden;
146
- transition: all var(--transition-base);
147
- min-height: auto;
148
- }
149
-
150
- /* Removed top gradient line */
151
-
152
- .metric-card-v3:hover {
153
- border-color: rgba(236, 72, 153, 0.15);
154
- }
155
-
156
- .metric-icon-wrapper {
157
- width: 56px;
158
- height: 56px;
159
- border-radius: 14px;
160
- background: var(--gradient-primary);
161
- display: flex;
162
- align-items: center;
163
- justify-content: center;
164
- flex-shrink: 0;
165
- }
166
-
167
- .metric-success .metric-icon-wrapper {
168
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
169
- }
170
-
171
- .metric-warning .metric-icon-wrapper {
172
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
173
- }
174
-
175
- .metric-danger .metric-icon-wrapper {
176
- background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
177
- }
178
-
179
- .metric-info .metric-icon-wrapper {
180
- background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
181
- }
182
-
183
- .metric-icon-wrapper i {
184
- font-size: 1.375rem;
185
- color: white;
186
- }
187
-
188
- .metric-content {
189
- flex: 1;
190
- min-width: 0;
191
- }
192
-
193
- .metric-label {
194
- font-size: 0.625rem;
195
- color: #78716c;
196
- text-transform: uppercase;
197
- letter-spacing: 0.1em;
198
- margin: 0 0 0.5rem 0;
199
- font-weight: 500;
200
- opacity: 0.8;
201
- }
202
-
203
- .metric-value {
204
- font-size: 2rem;
205
- font-weight: 700;
206
- color: #fafaf9;
207
- line-height: 1;
208
- margin: 0;
209
- }
210
-
211
- .metric-change {
212
- display: inline-flex;
213
- align-items: center;
214
- gap: 0.25rem;
215
- font-size: 0.875rem;
216
- font-weight: 500;
217
- margin: 0;
218
- }
219
-
220
- .metric-change.positive {
221
- color: var(--success-color);
222
- }
223
-
224
- .metric-change.negative {
225
- color: var(--danger-color);
226
- }
227
-
228
- .metric-change.neutral {
229
- color: var(--surface-400);
230
- }
231
-
232
- .metric-change i {
233
- font-size: 0.75rem;
234
- }
235
-
236
- .metric-period {
237
- color: var(--surface-400);
238
- font-weight: 400;
239
- margin-left: 0.25rem;
240
- }
241
-
242
- .metric-sparkline {
243
- position: absolute;
244
- right: 1.25rem;
245
- bottom: 1.25rem;
246
- width: 100px;
247
- height: 40px;
248
- opacity: 0.4;
249
- }
250
-
251
- .metric-card-v3:hover .metric-sparkline {
252
- opacity: 0.6;
253
- }
254
-
255
- /* Responsive */
256
- @media (max-width: 768px) {
257
- .metric-card-v3 {
258
- padding: 1rem;
259
- min-height: 100px;
260
- }
261
-
262
- .metric-icon-wrapper {
263
- width: 40px;
264
- height: 40px;
265
- }
266
-
267
- .metric-icon-wrapper i {
268
- font-size: 1rem;
269
- }
270
-
271
- .metric-value {
272
- font-size: 1.5rem;
273
- }
274
-
275
- .metric-label {
276
- font-size: 0.7rem;
277
- }
278
-
279
- .metric-change {
280
- font-size: 0.75rem;
281
- }
282
-
283
- .metric-sparkline {
284
- width: 60px;
285
- height: 20px;
286
- }
287
- }
288
-
289
- @media (max-width: 480px) {
290
- .metric-card-v3 {
291
- padding: 0.875rem;
292
- }
293
-
294
- .metric-value {
295
- font-size: 1.25rem;
296
- }
297
- }
298
- </style>
@@ -1,276 +0,0 @@
1
- <template>
2
- <div class="chart-container">
3
- <div class="chart-header">
4
- <h3 class="chart-title">{{ title || 'Queue Depth' }}</h3>
5
- <div class="chart-actions">
6
- <Button
7
- icon="pi pi-refresh"
8
- class="p-button-text p-button-sm"
9
- @click="$emit('refresh')"
10
- v-tooltip="'Refresh'"
11
- />
12
- </div>
13
- </div>
14
-
15
- <div class="queue-depth-chart">
16
- <Chart
17
- v-if="!loading && chartDataWithTheme && chartDataWithTheme.labels && chartDataWithTheme.labels.length > 0"
18
- :key="`queue-depth-${chartDataWithTheme.labels.length}`"
19
- type="bar"
20
- :data="chartDataWithTheme"
21
- :options="chartOptionsWithTheme"
22
- class="chart"
23
- />
24
- <div v-else-if="loading" class="chart-loading">
25
- <ProgressSpinner />
26
- <span>Loading chart data...</span>
27
- </div>
28
- <div v-else class="chart-empty">
29
- <i class="pi pi-chart-bar empty-icon"></i>
30
- <span>No queues with messages</span>
31
- </div>
32
- </div>
33
- </div>
34
- </template>
35
-
36
- <script setup>
37
- import { computed } from 'vue'
38
- import Chart from 'primevue/chart'
39
- import Button from 'primevue/button'
40
- import ProgressSpinner from 'primevue/progressspinner'
41
-
42
- // Import and register Chart.js components
43
- import {
44
- Chart as ChartJS,
45
- CategoryScale,
46
- LinearScale,
47
- BarElement,
48
- Title,
49
- Tooltip,
50
- Legend
51
- } from 'chart.js'
52
-
53
- ChartJS.register(
54
- CategoryScale,
55
- LinearScale,
56
- BarElement,
57
- Title,
58
- Tooltip,
59
- Legend
60
- )
61
-
62
- const props = defineProps({
63
- title: {
64
- type: String,
65
- default: null
66
- },
67
- data: {
68
- type: Object,
69
- default: null
70
- },
71
- loading: {
72
- type: Boolean,
73
- default: false
74
- }
75
- })
76
-
77
- const emit = defineEmits(['refresh'])
78
-
79
- // Apply dark theme colors to chart data
80
- const chartDataWithTheme = computed(() => {
81
- if (!props.data) return null
82
-
83
- // Create gradient colors for each dataset
84
- const colors = {
85
- pending: '#f59e0b',
86
- processing: '#06b6d4',
87
- completed: '#10b981',
88
- failed: '#ef4444',
89
- dead_letter: '#64748b'
90
- }
91
-
92
- return {
93
- labels: props.data.labels,
94
- datasets: props.data.datasets?.map(dataset => ({
95
- ...dataset,
96
- backgroundColor: colors[dataset.label?.toLowerCase().replace(' ', '_')] || '#ec4899',
97
- borderColor: 'transparent',
98
- borderWidth: 0,
99
- borderRadius: 4
100
- })) || []
101
- }
102
- })
103
-
104
- // Dark theme chart options
105
- const chartOptionsWithTheme = computed(() => ({
106
- responsive: true,
107
- maintainAspectRatio: false,
108
- plugins: {
109
- legend: {
110
- position: 'bottom',
111
- labels: {
112
- padding: 15,
113
- usePointStyle: true,
114
- font: {
115
- size: 11,
116
- family: 'Inter'
117
- },
118
- color: '#cbd5e1'
119
- }
120
- },
121
- tooltip: {
122
- backgroundColor: 'rgba(30, 41, 59, 0.95)',
123
- titleColor: '#f1f5f9',
124
- bodyColor: '#cbd5e1',
125
- borderColor: 'rgba(236, 72, 153, 0.3)',
126
- borderWidth: 1,
127
- cornerRadius: 8,
128
- padding: 12,
129
- displayColors: true,
130
- callbacks: {
131
- label: (context) => {
132
- return `${context.dataset.label}: ${context.parsed.y.toLocaleString()} messages`
133
- }
134
- }
135
- }
136
- },
137
- scales: {
138
- x: {
139
- stacked: true,
140
- grid: {
141
- display: false
142
- },
143
- ticks: {
144
- color: '#94a3b8',
145
- maxRotation: 45,
146
- minRotation: 0,
147
- font: {
148
- size: 11
149
- },
150
- callback: function(value, index) {
151
- const label = this.getLabelForValue(value)
152
- return label && label.length > 15 ? label.substr(0, 15) + '...' : (label || '')
153
- }
154
- },
155
- border: {
156
- display: false
157
- }
158
- },
159
- y: {
160
- stacked: true,
161
- beginAtZero: true,
162
- grid: {
163
- color: 'rgba(255, 255, 255, 0.03)',
164
- drawBorder: false
165
- },
166
- ticks: {
167
- color: '#94a3b8',
168
- callback: (value) => {
169
- if (value >= 1000000) {
170
- return (value / 1000000).toFixed(1) + 'M'
171
- } else if (value >= 1000) {
172
- return (value / 1000).toFixed(1) + 'K'
173
- }
174
- return value
175
- },
176
- font: {
177
- size: 11
178
- }
179
- },
180
- border: {
181
- display: false
182
- }
183
- }
184
- },
185
- animation: {
186
- duration: 750,
187
- easing: 'easeInOutQuart'
188
- }
189
- }))
190
- </script>
191
-
192
- <style scoped>
193
- .chart-container {
194
- background: transparent;
195
- border: 1px solid rgba(255, 255, 255, 0.1);
196
- border-radius: 16px;
197
- padding: 1.5rem;
198
- height: 100%;
199
- display: flex;
200
- flex-direction: column;
201
- }
202
-
203
- .chart-header {
204
- display: flex;
205
- justify-content: space-between;
206
- align-items: center;
207
- margin-bottom: 1.5rem;
208
- }
209
-
210
- .chart-title {
211
- font-size: 1.125rem;
212
- font-weight: 600;
213
- color: var(--surface-700);
214
- margin: 0;
215
- }
216
-
217
- .chart-actions {
218
- display: flex;
219
- gap: 0.5rem;
220
- }
221
-
222
- .queue-depth-chart {
223
- flex: 1;
224
- min-height: 300px;
225
- position: relative;
226
- }
227
-
228
- .chart {
229
- height: 100%;
230
- }
231
-
232
- .chart-loading,
233
- .chart-empty {
234
- height: 100%;
235
- display: flex;
236
- flex-direction: column;
237
- align-items: center;
238
- justify-content: center;
239
- gap: 1rem;
240
- color: var(--surface-400);
241
- }
242
-
243
- .empty-icon {
244
- font-size: 3rem;
245
- color: var(--surface-300);
246
- }
247
-
248
- :deep(.p-button-sm) {
249
- padding: 0.5rem;
250
- font-size: 0.875rem;
251
- }
252
-
253
- :deep(.p-button-text) {
254
- color: var(--surface-500);
255
- }
256
-
257
- :deep(.p-button-text:hover) {
258
- background: rgba(236, 72, 153, 0.1);
259
- color: var(--primary-500);
260
- }
261
-
262
- /* Responsive */
263
- @media (max-width: 768px) {
264
- .chart-container {
265
- padding: 1rem;
266
- }
267
-
268
- .chart-title {
269
- font-size: 1rem;
270
- }
271
-
272
- .queue-depth-chart {
273
- min-height: 250px;
274
- }
275
- }
276
- </style>