lepus 0.0.1.beta2 → 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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/linter.yml +21 -0
  3. data/.github/workflows/specs.yml +93 -13
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +10 -0
  6. data/.tool-versions +1 -1
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +36 -9
  9. data/Makefile +19 -0
  10. data/README.md +562 -7
  11. data/bin/setup +5 -2
  12. data/config.ru +14 -0
  13. data/docker-compose.yml +5 -3
  14. data/docs/README.md +80 -0
  15. data/docs/cli.md +108 -0
  16. data/docs/configuration.md +171 -0
  17. data/docs/consumers.md +168 -0
  18. data/docs/getting-started.md +136 -0
  19. data/docs/images/lepus-web.png +0 -0
  20. data/docs/middleware.md +240 -0
  21. data/docs/producers.md +173 -0
  22. data/docs/prometheus.md +112 -0
  23. data/docs/rails.md +161 -0
  24. data/docs/supervisor.md +112 -0
  25. data/docs/testing.md +141 -0
  26. data/docs/web.md +85 -0
  27. data/examples/grafana-dashboard.json +450 -0
  28. data/gemfiles/Gemfile.rails-5.2 +7 -0
  29. data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
  30. data/gemfiles/Gemfile.rails-6.1 +7 -0
  31. data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
  32. data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
  33. data/gemfiles/Gemfile.rails-7.2.lock +321 -0
  34. data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
  35. data/gemfiles/Gemfile.rails-8.0.lock +322 -0
  36. data/lepus.gemspec +7 -1
  37. data/lib/lepus/cli.rb +35 -4
  38. data/lib/lepus/configuration.rb +107 -0
  39. data/lib/lepus/connection_pool.rb +135 -0
  40. data/lib/lepus/consumer.rb +59 -41
  41. data/lib/lepus/consumers/config.rb +183 -0
  42. data/lib/lepus/consumers/handler.rb +56 -0
  43. data/lib/lepus/consumers/middleware_chain.rb +22 -0
  44. data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
  45. data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
  46. data/lib/lepus/consumers/middlewares/json.rb +37 -0
  47. data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
  48. data/lib/lepus/consumers/middlewares/unique.rb +65 -0
  49. data/lib/lepus/consumers/stats.rb +70 -0
  50. data/lib/lepus/consumers/stats_registry.rb +29 -0
  51. data/lib/lepus/consumers/worker.rb +141 -0
  52. data/lib/lepus/consumers/worker_factory.rb +124 -0
  53. data/lib/lepus/consumers.rb +6 -0
  54. data/lib/lepus/message/delivery_info.rb +72 -0
  55. data/lib/lepus/message/metadata.rb +99 -0
  56. data/lib/lepus/message.rb +88 -5
  57. data/lib/lepus/middleware_chain.rb +83 -0
  58. data/lib/lepus/primitive/hash.rb +29 -0
  59. data/lib/lepus/process.rb +24 -24
  60. data/lib/lepus/process_registry/backend.rb +49 -0
  61. data/lib/lepus/process_registry/file_backend.rb +108 -0
  62. data/lib/lepus/process_registry/message_builder.rb +72 -0
  63. data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
  64. data/lib/lepus/process_registry.rb +56 -23
  65. data/lib/lepus/processes/base.rb +0 -5
  66. data/lib/lepus/processes/callbacks.rb +3 -0
  67. data/lib/lepus/processes/interruptible.rb +4 -8
  68. data/lib/lepus/processes/procline.rb +1 -1
  69. data/lib/lepus/processes/registrable.rb +1 -1
  70. data/lib/lepus/processes/runnable.rb +1 -1
  71. data/lib/lepus/processes.rb +15 -0
  72. data/lib/lepus/producer.rb +141 -30
  73. data/lib/lepus/producers/config.rb +46 -0
  74. data/lib/lepus/producers/definition.rb +48 -0
  75. data/lib/lepus/producers/hooks.rb +170 -0
  76. data/lib/lepus/producers/middleware_chain.rb +22 -0
  77. data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
  78. data/lib/lepus/producers/middlewares/header.rb +47 -0
  79. data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
  80. data/lib/lepus/producers/middlewares/json.rb +47 -0
  81. data/lib/lepus/producers/middlewares/unique.rb +67 -0
  82. data/lib/lepus/producers.rb +7 -0
  83. data/lib/lepus/prometheus/collector.rb +149 -0
  84. data/lib/lepus/prometheus/instrumentation.rb +168 -0
  85. data/lib/lepus/prometheus.rb +48 -0
  86. data/lib/lepus/publisher.rb +67 -0
  87. data/lib/lepus/supervisor/children_pipes.rb +25 -0
  88. data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
  89. data/lib/lepus/supervisor/pidfiled.rb +1 -1
  90. data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
  91. data/lib/lepus/supervisor.rb +129 -25
  92. data/lib/lepus/testing/exchange.rb +95 -0
  93. data/lib/lepus/testing/message_builder.rb +177 -0
  94. data/lib/lepus/testing/rspec_matchers.rb +258 -0
  95. data/lib/lepus/testing.rb +210 -0
  96. data/lib/lepus/unique.rb +18 -0
  97. data/lib/lepus/version.rb +1 -1
  98. data/lib/lepus/web/aggregator.rb +154 -0
  99. data/lib/lepus/web/api.rb +132 -0
  100. data/lib/lepus/web/app.rb +37 -0
  101. data/lib/lepus/web/management_api.rb +192 -0
  102. data/lib/lepus/web/respond_with.rb +28 -0
  103. data/lib/lepus/web.rb +238 -0
  104. data/lib/lepus.rb +39 -28
  105. data/test_offline.html +189 -0
  106. data/web/assets/css/styles.css +635 -0
  107. data/web/assets/js/app.js +6 -0
  108. data/web/assets/js/bootstrap.js +20 -0
  109. data/web/assets/js/controllers/connection_controller.js +44 -0
  110. data/web/assets/js/controllers/dashboard_controller.js +499 -0
  111. data/web/assets/js/controllers/queue_controller.js +17 -0
  112. data/web/assets/js/controllers/theme_controller.js +31 -0
  113. data/web/assets/js/offline-manager.js +233 -0
  114. data/web/assets/js/service-worker-manager.js +65 -0
  115. data/web/index.html +159 -0
  116. data/web/sw.js +144 -0
  117. metadata +177 -18
  118. data/lib/lepus/consumer_config.rb +0 -149
  119. data/lib/lepus/consumer_wrapper.rb +0 -46
  120. data/lib/lepus/lifecycle_hooks.rb +0 -49
  121. data/lib/lepus/middlewares/honeybadger.rb +0 -23
  122. data/lib/lepus/middlewares/json.rb +0 -35
  123. data/lib/lepus/middlewares/max_retry.rb +0 -57
  124. data/lib/lepus/processes/consumer.rb +0 -113
  125. data/lib/lepus/supervisor/config.rb +0 -45
@@ -0,0 +1,635 @@
1
+ :root {
2
+ --bg: #0f1419;
3
+ --panel: #1c2432;
4
+ --text: #e8ecf1;
5
+ --muted: #9aa8b4;
6
+ --primary: #6ea8fe;
7
+ --accent: #7ee787;
8
+ --danger: #ff6b6b;
9
+ --warning: #f7c948;
10
+ --border: #4b5563;
11
+ --shadow: rgba(0, 0, 0, 0.25);
12
+ }
13
+
14
+ [data-theme="light"] {
15
+ --bg: #f7f9fb;
16
+ --panel: #ffffff;
17
+ --text: #0a0f14;
18
+ --muted: #445668;
19
+ --primary: #0d6efd;
20
+ --accent: #198754;
21
+ --danger: #dc3545;
22
+ --warning: #c98a00;
23
+ --border: #e6eaef;
24
+ --shadow: rgba(0, 0, 0, 0.08);
25
+ }
26
+
27
+ * { box-sizing: border-box; }
28
+ html, body { height: 100%; }
29
+ body {
30
+ margin: 0;
31
+ font: 14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
32
+ color: var(--text);
33
+ background: var(--bg);
34
+ }
35
+
36
+ /* Offline banner styles */
37
+ .offline-banner {
38
+ position: fixed;
39
+ top: 0;
40
+ left: 0;
41
+ right: 0;
42
+ z-index: 1000;
43
+ background: linear-gradient(135deg, var(--warning), #e6a700);
44
+ color: #000;
45
+ padding: 8px 16px;
46
+ font-size: 14px;
47
+ font-weight: 500;
48
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
49
+ animation: slideDown 0.3s ease-out;
50
+ }
51
+
52
+ .offline-content {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ gap: 8px;
57
+ position: relative;
58
+ width: 100%;
59
+ }
60
+
61
+ /* On large screens, make dismiss button and icon full width positioned */
62
+ @media (min-width: 768px) {
63
+ .offline-content {
64
+ justify-content: space-between;
65
+ padding: 0 20px;
66
+ }
67
+
68
+ .offline-icon {
69
+ position: absolute;
70
+ left: 20px;
71
+ }
72
+
73
+ .offline-dismiss {
74
+ position: absolute;
75
+ right: 20px;
76
+ }
77
+ }
78
+
79
+ .offline-icon {
80
+ font-size: 16px;
81
+ flex-shrink: 0;
82
+ }
83
+
84
+ .offline-message {
85
+ flex: 1;
86
+ text-align: center;
87
+ }
88
+
89
+ .offline-dismiss {
90
+ background: none;
91
+ border: none;
92
+ font-size: 18px;
93
+ font-weight: bold;
94
+ color: #000;
95
+ cursor: pointer;
96
+ padding: 0;
97
+ width: 24px;
98
+ height: 24px;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ border-radius: 50%;
103
+ transition: background-color 0.2s ease;
104
+ flex-shrink: 0;
105
+ }
106
+
107
+ .offline-dismiss:hover {
108
+ background-color: rgba(0, 0, 0, 0.1);
109
+ }
110
+
111
+ @keyframes slideDown {
112
+ from {
113
+ transform: translateY(-100%);
114
+ opacity: 0;
115
+ }
116
+ to {
117
+ transform: translateY(0);
118
+ opacity: 1;
119
+ }
120
+ }
121
+
122
+ /* Adjust navbar when offline banner is visible */
123
+ body:has(.offline-banner[style*="block"]) .navbar {
124
+ top: 40px; /* Height of offline banner */
125
+ }
126
+
127
+ /* Offline content placeholder */
128
+ .offline-content-placeholder {
129
+ display: flex;
130
+ justify-content: center;
131
+ align-items: center;
132
+ min-height: 60vh;
133
+ padding: 2rem;
134
+ }
135
+
136
+ .offline-card {
137
+ background: var(--panel);
138
+ border: 1px solid var(--border);
139
+ border-radius: 12px;
140
+ padding: 2rem;
141
+ text-align: center;
142
+ max-width: 500px;
143
+ box-shadow: 0 4px 12px var(--shadow);
144
+ }
145
+
146
+ .offline-icon-large {
147
+ font-size: 4rem;
148
+ margin-bottom: 1rem;
149
+ opacity: 0.7;
150
+ }
151
+
152
+ .offline-card h2 {
153
+ margin: 0 0 1rem 0;
154
+ color: var(--text);
155
+ font-size: 1.5rem;
156
+ }
157
+
158
+ .offline-card p {
159
+ margin: 0 0 1rem 0;
160
+ color: var(--muted);
161
+ line-height: 1.5;
162
+ }
163
+
164
+ .offline-limitations {
165
+ text-align: left;
166
+ margin: 1rem 0;
167
+ padding-left: 1.5rem;
168
+ color: var(--muted);
169
+ }
170
+
171
+ .offline-limitations li {
172
+ margin: 0.5rem 0;
173
+ }
174
+
175
+ .offline-actions {
176
+ display: flex;
177
+ gap: 1rem;
178
+ justify-content: center;
179
+ margin-top: 2rem;
180
+ }
181
+
182
+ .offline-actions .btn {
183
+ padding: 0.75rem 1.5rem;
184
+ border: none;
185
+ border-radius: 6px;
186
+ font-weight: 500;
187
+ cursor: pointer;
188
+ transition: all 0.2s ease;
189
+ }
190
+
191
+ .offline-actions .btn-primary {
192
+ background: var(--primary);
193
+ color: white;
194
+ }
195
+
196
+ .offline-actions .btn-primary:hover {
197
+ background: var(--primary);
198
+ opacity: 0.9;
199
+ }
200
+
201
+ .offline-actions .btn-secondary {
202
+ background: transparent;
203
+ color: var(--text);
204
+ border: 1px solid var(--border);
205
+ }
206
+
207
+ .offline-actions .btn-secondary:hover {
208
+ background: var(--panel);
209
+ border-color: var(--primary);
210
+ }
211
+
212
+ .navbar {
213
+ position: sticky; top: 0; z-index: 10;
214
+ display: flex; align-items: center; justify-content: space-between;
215
+ padding: 12px 16px; border-bottom: 1px solid var(--border);
216
+ background: rgba(28, 36, 50, 0.85);
217
+ backdrop-filter: blur(8px);
218
+ gap: 16px;
219
+ }
220
+ [data-theme="light"] .navbar { background: rgba(255, 255, 255, 0.85); }
221
+ .brand { display: flex; gap: 10px; align-items: center; font-weight: 600; }
222
+ .logo-badge {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ width: 28px;
227
+ height: 28px;
228
+ background: var(--primary);
229
+ color: white;
230
+ border-radius: 6px;
231
+ font-size: 16px;
232
+ font-weight: 700;
233
+ box-shadow: 0 2px 6px var(--shadow);
234
+ }
235
+ .title {
236
+ letter-spacing: 0.3px;
237
+ font-size: 18px;
238
+ }
239
+ .nav-controls { display: flex; align-items: center; }
240
+ .refresh-control {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 8px;
244
+ padding: 4px 8px;
245
+ background: rgba(255,255,255,0.05);
246
+ border-radius: 6px;
247
+ border: 1px solid rgba(255,255,255,0.1);
248
+ }
249
+ .refresh-control label {
250
+ font-size: 12px;
251
+ color: var(--muted);
252
+ white-space: nowrap;
253
+ }
254
+ .refresh-control input[type="range"] {
255
+ width: 60px;
256
+ height: 4px;
257
+ background: var(--border);
258
+ border-radius: 2px;
259
+ outline: none;
260
+ }
261
+ .refresh-control input[type="range"]::-webkit-slider-thumb {
262
+ appearance: none;
263
+ width: 12px;
264
+ height: 12px;
265
+ background: var(--primary);
266
+ border-radius: 50%;
267
+ cursor: pointer;
268
+ }
269
+ .refresh-control input[type="range"]::-moz-range-thumb {
270
+ width: 12px;
271
+ height: 12px;
272
+ background: var(--primary);
273
+ border-radius: 50%;
274
+ border: none;
275
+ cursor: pointer;
276
+ }
277
+ .refresh-value {
278
+ font-size: 11px;
279
+ color: var(--muted);
280
+ min-width: 20px;
281
+ text-align: center;
282
+ }
283
+ .nav-actions { display: flex; gap: 8px; align-items: center; }
284
+ .connection-status {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 6px;
288
+ padding: 4px 8px;
289
+ border-radius: 12px;
290
+ background: rgba(25,135,84,0.12);
291
+ border: 1px solid rgba(25,135,84,0.3);
292
+ color: var(--accent);
293
+ font-size: 11px;
294
+ font-weight: 500;
295
+ transition: all 0.2s ease;
296
+ }
297
+ .connection-status.disconnected {
298
+ background: rgba(220,53,69,0.12);
299
+ border: 1px solid rgba(220,53,69,0.3);
300
+ color: var(--danger);
301
+ }
302
+ .status-dot {
303
+ width: 6px;
304
+ height: 6px;
305
+ border-radius: 50%;
306
+ background: var(--accent);
307
+ transition: background-color 0.2s ease;
308
+ }
309
+ .connection-status.disconnected .status-dot {
310
+ background: var(--danger);
311
+ }
312
+ .status-text {
313
+ white-space: nowrap;
314
+ }
315
+ .btn { border: 1px solid var(--border); background: var(--panel); color: var(--text); padding: 6px 10px; border-radius: 8px; cursor: pointer; transition: 120ms ease; }
316
+ .btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--shadow); }
317
+
318
+ .container { width: 100%; max-width: none; margin: 16px 0; padding: 0 12px 20px 12px; }
319
+ .panel, .card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 10px 30px var(--shadow); overflow: hidden; }
320
+ .panel-header, .card-header { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
321
+ .panel-body, .card-body { padding: 12px 14px; }
322
+
323
+ .grid { display: grid; gap: 12px; margin-top: 12px; }
324
+ .stats-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
325
+ .chart-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
326
+ .queues-grid { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
327
+ .processes-grid { grid-template-columns: 1fr; }
328
+
329
+ .stat-card { padding: 12px 14px; }
330
+ .stat-label { color: var(--muted); font-size: 14px; margin-bottom: 6px; }
331
+ .stat-value { font-size: 24px; font-weight: 600; letter-spacing: 0.3px; margin-bottom: 4px; }
332
+ .stat-detail { color: var(--muted); font-size: 13px; opacity: 0.8; }
333
+
334
+ .control-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
335
+ .control label { display: block; margin-bottom: 6px; color: var(--muted); }
336
+
337
+ .metric {
338
+ background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.02));
339
+ border: 1px dashed var(--border);
340
+ border-radius: 8px;
341
+ padding: 12px;
342
+ margin-bottom: 8px;
343
+ transition: all 0.2s ease;
344
+ }
345
+ .metric:hover {
346
+ background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(0,0,0,0.04));
347
+ border-color: var(--primary);
348
+ }
349
+ .metric-label {
350
+ color: var(--muted);
351
+ font-size: 12px;
352
+ font-weight: 600;
353
+ text-transform: uppercase;
354
+ letter-spacing: 0.5px;
355
+ margin-bottom: 8px;
356
+ display: block;
357
+ }
358
+ .metric-value { font-size: 18px; font-weight: 600; }
359
+
360
+ .table-responsive { width: 100%; overflow-x: auto; }
361
+ .table { width: 100%; border-collapse: collapse; }
362
+ .table th, .table td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; }
363
+ .table tbody tr:hover { background: rgba(255,255,255,0.02); }
364
+
365
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; border: 1px solid var(--border); }
366
+ .badge.ok { background: rgba(25,135,84,0.12); color: var(--accent); border-color: rgba(25,135,84,0.3); }
367
+ .badge.warn { background: rgba(247,201,72,0.12); color: var(--warning); border-color: rgba(247,201,72,0.3); }
368
+ .badge.err { background: rgba(220,53,69,0.12); color: var(--danger); border-color: rgba(220,53,69,0.3); }
369
+
370
+ .queue-row { cursor: pointer; }
371
+ .sub-row { background: rgba(255,255,255,0.03); }
372
+
373
+ .health { border-radius: 10px; border: 1px solid var(--border); padding: 10px; }
374
+ .health.ok { box-shadow: 0 0 0 2px rgba(25,135,84,0.15) inset; }
375
+ .health.bad { box-shadow: 0 0 0 2px rgba(220,53,69,0.15) inset; }
376
+
377
+ /* Process Hierarchy Styles */
378
+ .application-card {
379
+ border-radius: 12px;
380
+ }
381
+ .application-card .card-header h2 { margin: 0; font-size: 18px; }
382
+ .level-label {
383
+ display: inline-block;
384
+ padding: 2px 6px;
385
+ background: rgba(255,255,255,0.1);
386
+ color: var(--muted);
387
+ font-size: 10px;
388
+ font-weight: 500;
389
+ text-transform: uppercase;
390
+ letter-spacing: 0.5px;
391
+ border-radius: 3px;
392
+ margin-right: 8px;
393
+ }
394
+
395
+ .supervisor-card {
396
+ margin: 12px 0;
397
+ border-left: 3px solid var(--primary);
398
+ background: rgba(110,168,254,0.05);
399
+ border-radius: 8px;
400
+ }
401
+ .supervisor-card .card-header {
402
+ display: flex;
403
+ justify-content: space-between;
404
+ align-items: center;
405
+ padding: 10px 14px;
406
+ }
407
+ .supervisor-info h3 { margin: 0; font-size: 16px; }
408
+ .supervisor-meta {
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 8px;
412
+ color: var(--muted);
413
+ font-size: 12px;
414
+ text-align: right;
415
+ }
416
+ .supervisor-meta .meta-text {
417
+ color: var(--muted);
418
+ font-size: 12px;
419
+ }
420
+
421
+ .worker-card {
422
+ margin: 8px 0;
423
+ border-left: 3px solid var(--accent);
424
+ background: rgba(126,231,135,0.05);
425
+ border-radius: 8px;
426
+ }
427
+ .worker-card .card-header {
428
+ display: flex;
429
+ justify-content: space-between;
430
+ align-items: center;
431
+ padding: 8px 12px;
432
+ }
433
+ .worker-info h4 { margin: 0; font-size: 14px; }
434
+ .worker-meta {
435
+ display: flex;
436
+ align-items: center;
437
+ gap: 8px;
438
+ color: var(--muted);
439
+ font-size: 11px;
440
+ text-align: right;
441
+ }
442
+ .worker-meta .meta-text {
443
+ color: var(--muted);
444
+ font-size: 11px;
445
+ }
446
+
447
+ .consumers-container {
448
+ display: grid;
449
+ gap: 16px;
450
+ grid-template-columns: 1fr;
451
+ }
452
+
453
+ .consumer-card {
454
+ margin: 0;
455
+ border: 1px solid rgba(110,168,254,0.3);
456
+ background: rgba(110,168,254,0.05);
457
+ transition: all 0.2s ease;
458
+ display: flex;
459
+ flex-direction: column;
460
+ height: 100%;
461
+ border-radius: 8px;
462
+ }
463
+ .consumer-card.expanded {
464
+ background: rgba(110,168,254,0.08);
465
+ }
466
+ .consumer-card .card-header {
467
+ display: flex;
468
+ justify-content: space-between;
469
+ align-items: center;
470
+ padding: 6px 10px;
471
+ cursor: pointer;
472
+ transition: background-color 0.2s ease;
473
+ }
474
+ .consumer-card .card-header:hover {
475
+ background: rgba(110,168,254,0.1);
476
+ }
477
+ .consumer-info h5 { margin: 0; font-size: 13px; }
478
+ .consumer-meta {
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 8px;
482
+ color: var(--muted);
483
+ font-size: 10px;
484
+ }
485
+ .consumer-stats-preview {
486
+ display: flex;
487
+ gap: 4px;
488
+ }
489
+ .expand-icon {
490
+ font-size: 10px;
491
+ transition: transform 0.2s ease;
492
+ color: var(--muted);
493
+ }
494
+ .consumer-card.expanded .expand-icon {
495
+ transform: rotate(180deg);
496
+ }
497
+
498
+ .badge.concurrent {
499
+ background: rgba(139,92,246,0.12);
500
+ color: #8b5cf6;
501
+ border-color: rgba(139,92,246,0.3);
502
+ font-size: 10px;
503
+ }
504
+
505
+ .consumer-table {
506
+ width: 100%;
507
+ border-collapse: collapse;
508
+ font-size: 11px;
509
+ }
510
+ .consumer-table td {
511
+ padding: 2px 4px;
512
+ border: none;
513
+ }
514
+ .consumer-table .label {
515
+ color: var(--muted);
516
+ font-weight: 500;
517
+ width: 80px;
518
+ }
519
+
520
+ .stat {
521
+ display: inline-block;
522
+ margin-right: 8px;
523
+ padding: 1px 4px;
524
+ border-radius: 3px;
525
+ font-size: 10px;
526
+ }
527
+ .stat.processed { background: rgba(25,135,84,0.12); color: var(--accent); }
528
+ .stat.rejected { background: rgba(247,201,72,0.12); color: var(--warning); }
529
+ .stat.errored { background: rgba(220,53,69,0.12); color: var(--danger); }
530
+
531
+ .stat-mini {
532
+ display: inline-block;
533
+ padding: 1px 3px;
534
+ border-radius: 6px;
535
+ font-size: 9px;
536
+ font-weight: 500;
537
+ min-width: 30px;
538
+ text-align: center;
539
+ }
540
+ .stat-mini.processed { background: rgba(25,135,84,0.15); color: var(--accent); }
541
+ .stat-mini.rejected { background: rgba(247,201,72,0.15); color: var(--warning); }
542
+ .stat-mini.errored { background: rgba(220,53,69,0.15); color: var(--danger); }
543
+
544
+ /* Queue Detail Styles */
545
+ .queue-detail-content {
546
+ padding: 8px 0;
547
+ }
548
+
549
+ .queue-name {
550
+ font-weight: 600;
551
+ font-size: 14px;
552
+ color: var(--primary);
553
+ margin-bottom: 8px;
554
+ padding: 4px 0;
555
+ border-bottom: 1px solid rgba(110,168,254,0.2);
556
+ }
557
+
558
+ .queue-stats {
559
+ display: grid;
560
+ grid-template-columns: repeat(3, 1fr);
561
+ gap: 8px;
562
+ }
563
+
564
+ .stat-item {
565
+ display: flex;
566
+ flex-direction: column;
567
+ align-items: center;
568
+ padding: 6px 4px;
569
+ background: rgba(255,255,255,0.02);
570
+ border-radius: 6px;
571
+ border: 1px solid rgba(255,255,255,0.05);
572
+ }
573
+
574
+ .stat-item .stat-label {
575
+ color: var(--muted);
576
+ font-size: 11px;
577
+ font-weight: 500;
578
+ margin-bottom: 4px;
579
+ text-transform: uppercase;
580
+ letter-spacing: 0.3px;
581
+ }
582
+
583
+ .stat-item .stat-value {
584
+ color: var(--text);
585
+ font-size: 14px;
586
+ font-weight: 600;
587
+ text-align: center;
588
+ }
589
+
590
+
591
+ @media (max-width: 720px) {
592
+ .navbar {
593
+ padding: 10px 12px;
594
+ flex-wrap: wrap;
595
+ gap: 8px;
596
+ }
597
+ .nav-controls {
598
+ order: 3;
599
+ width: 100%;
600
+ justify-content: center;
601
+ }
602
+ .refresh-control {
603
+ padding: 3px 6px;
604
+ gap: 6px;
605
+ }
606
+ .refresh-control input[type="range"] {
607
+ width: 50px;
608
+ }
609
+ .container { margin: 12px auto; }
610
+ }
611
+
612
+ @media (min-width: 768px) {
613
+ .consumers-container {
614
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
615
+ gap: 20px;
616
+ }
617
+ }
618
+
619
+ @media (min-width: 1024px) {
620
+ .grid { gap: 24px; margin-top: 24px; }
621
+ .control-grid { gap: 24px; }
622
+ .container { margin: 24px auto; }
623
+ .consumers-container {
624
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
625
+ gap: 24px;
626
+ }
627
+ }
628
+
629
+ @media (min-width: 1280px) {
630
+ .consumers-container {
631
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
632
+ gap: 24px;
633
+ }
634
+ }
635
+
@@ -0,0 +1,6 @@
1
+ (function() {
2
+ const application = Stimulus.Application.start();
3
+ window.StimulusApp = application;
4
+ })();
5
+
6
+
@@ -0,0 +1,20 @@
1
+ // Bootstrap the Lepus dashboard. Kept in a separate file (rather than an
2
+ // inline <script>) so it runs under a strict Content Security Policy that
3
+ // forbids `unsafe-inline`, which is the default for many Rails apps.
4
+ (async function () {
5
+ const offlineManager = new OfflineManager();
6
+ const serviceWorkerManager = new ServiceWorkerManager();
7
+
8
+ document.querySelectorAll("[data-offline-action='reload']").forEach((el) => {
9
+ el.addEventListener("click", () => window.location.reload());
10
+ });
11
+ document.querySelectorAll("[data-offline-action='dismiss']").forEach((el) => {
12
+ el.addEventListener("click", () => {
13
+ const c = document.getElementById("offline-content");
14
+ if (c) c.style.display = "none";
15
+ });
16
+ });
17
+
18
+ await serviceWorkerManager.register();
19
+ await offlineManager.initialize();
20
+ })();
@@ -0,0 +1,44 @@
1
+ (function() {
2
+ StimulusApp.register("connection", class extends Stimulus.Controller {
3
+ static targets = ["indicator", "text"]
4
+
5
+ connect() {
6
+ this.check();
7
+ this.timer = setInterval(() => this.check(), 10000);
8
+ }
9
+
10
+ disconnect() {
11
+ if (this.timer) clearInterval(this.timer);
12
+ }
13
+
14
+ async check() {
15
+ // Check if we're offline first
16
+ if (!navigator.onLine) {
17
+ this.indicatorTarget.classList.add('disconnected');
18
+ this.textTarget.textContent = 'Offline';
19
+ return;
20
+ }
21
+
22
+ try {
23
+ const res = await fetch('api/health');
24
+ if (res.ok) {
25
+ this.indicatorTarget.classList.remove('disconnected');
26
+ this.textTarget.textContent = 'Connected';
27
+ } else {
28
+ this.indicatorTarget.classList.add('disconnected');
29
+ this.textTarget.textContent = 'Disconnected';
30
+ }
31
+ } catch (error) {
32
+ this.indicatorTarget.classList.add('disconnected');
33
+ // More specific error handling
34
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
35
+ this.textTarget.textContent = 'Offline';
36
+ } else {
37
+ this.textTarget.textContent = 'Disconnected';
38
+ }
39
+ }
40
+ }
41
+ });
42
+ })();
43
+
44
+