lepus 0.2.2 → 0.3.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. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile.lock +3 -3
  4. data/README.md +104 -9
  5. data/bin/web-demo +592 -0
  6. data/docs/brand/BRAND.md +67 -0
  7. data/docs/brand/apple-touch-icon.png +0 -0
  8. data/docs/brand/favicon-16.png +0 -0
  9. data/docs/brand/favicon-32.png +0 -0
  10. data/docs/brand/favicon.ico +0 -0
  11. data/docs/brand/favicon.svg +1 -0
  12. data/docs/brand/icon-512.png +0 -0
  13. data/docs/brand/lockup-lepus-mono.svg +1 -0
  14. data/docs/brand/lockup-lepus-on-dark.svg +1 -0
  15. data/docs/brand/lockup-lepus-on-light.svg +1 -0
  16. data/docs/brand/lockup-lepus-saga-mono.svg +1 -0
  17. data/docs/brand/lockup-lepus-saga-on-dark.svg +1 -0
  18. data/docs/brand/lockup-lepus-saga-on-light.svg +1 -0
  19. data/docs/brand/lockup-lepus-saga.svg +1 -0
  20. data/docs/brand/lockup-lepus.svg +1 -0
  21. data/docs/brand/mark-mono.svg +1 -0
  22. data/docs/brand/mark-on-dark.svg +1 -0
  23. data/docs/brand/mark-on-light.svg +1 -0
  24. data/docs/brand/mark-small-mono.svg +1 -0
  25. data/docs/brand/mark-small-on-dark.svg +1 -0
  26. data/docs/brand/mark-small-on-light.svg +1 -0
  27. data/docs/brand/mark-small.svg +1 -0
  28. data/docs/brand/mark.svg +1 -0
  29. data/docs/configuration.md +1 -0
  30. data/docs/consumers.md +39 -5
  31. data/docs/getting-started.md +1 -1
  32. data/docs/images/lepus-web.png +0 -0
  33. data/docs/middleware.md +1 -1
  34. data/docs/producers.md +41 -0
  35. data/docs/testing.md +67 -48
  36. data/docs/web.md +39 -13
  37. data/gemfiles/Gemfile.rails-5.2.lock +2 -2
  38. data/gemfiles/Gemfile.rails-6.1.lock +4 -4
  39. data/gemfiles/Gemfile.rails-7.2.lock +1 -1
  40. data/gemfiles/Gemfile.rails-8.0.lock +1 -1
  41. data/lib/lepus/configuration.rb +6 -0
  42. data/lib/lepus/consumer.rb +34 -0
  43. data/lib/lepus/consumers/config.rb +8 -7
  44. data/lib/lepus/consumers/stats.rb +15 -2
  45. data/lib/lepus/delay.rb +180 -0
  46. data/lib/lepus/primitive/string.rb +2 -12
  47. data/lib/lepus/producer.rb +6 -1
  48. data/lib/lepus/producers/definition.rb +3 -1
  49. data/lib/lepus/publisher.rb +29 -3
  50. data/lib/lepus/testing/rspec_matchers.rb +20 -1
  51. data/lib/lepus/testing.rb +2 -0
  52. data/lib/lepus/version.rb +1 -1
  53. data/lib/lepus/web/api.rb +45 -0
  54. data/lib/lepus/web/app.rb +19 -16
  55. data/lib/lepus/web/management_api.rb +125 -2
  56. data/lib/lepus/web/topology.rb +341 -0
  57. data/lib/lepus/web.rb +17 -18
  58. data/lib/lepus.rb +1 -0
  59. data/web/assets/app.css +862 -0
  60. data/web/assets/apple-touch-icon.png +0 -0
  61. data/web/assets/favicon-32.png +0 -0
  62. data/web/assets/favicon.svg +1 -0
  63. data/web/assets/fonts/overpass/OFL.txt +93 -0
  64. data/web/assets/fonts/overpass/overpass-latin-wght.woff2 +0 -0
  65. data/web/assets/js/api.js +27 -0
  66. data/web/assets/js/dom.js +95 -0
  67. data/web/assets/js/format.js +63 -0
  68. data/web/assets/js/live.js +74 -0
  69. data/web/assets/js/main.js +156 -0
  70. data/web/assets/js/map.js +606 -0
  71. data/web/assets/js/model.js +262 -0
  72. data/web/assets/js/rates.js +161 -0
  73. data/web/assets/js/theme.js +8 -0
  74. data/web/assets/js/views/connections.js +61 -0
  75. data/web/assets/js/views/overview.js +229 -0
  76. data/web/assets/js/views/processes.js +101 -0
  77. data/web/assets/js/views/queues.js +456 -0
  78. data/web/index.html +58 -153
  79. data/web/sw.js +14 -143
  80. metadata +47 -12
  81. data/test_offline.html +0 -189
  82. data/web/assets/css/styles.css +0 -635
  83. data/web/assets/js/app.js +0 -6
  84. data/web/assets/js/bootstrap.js +0 -20
  85. data/web/assets/js/controllers/connection_controller.js +0 -44
  86. data/web/assets/js/controllers/dashboard_controller.js +0 -499
  87. data/web/assets/js/controllers/queue_controller.js +0 -17
  88. data/web/assets/js/controllers/theme_controller.js +0 -31
  89. data/web/assets/js/offline-manager.js +0 -233
  90. data/web/assets/js/service-worker-manager.js +0 -65
@@ -1,635 +0,0 @@
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
-
data/web/assets/js/app.js DELETED
@@ -1,6 +0,0 @@
1
- (function() {
2
- const application = Stimulus.Application.start();
3
- window.StimulusApp = application;
4
- })();
5
-
6
-
@@ -1,20 +0,0 @@
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
- })();
@@ -1,44 +0,0 @@
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
-