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,233 @@
1
+ // Offline Manager - Handles offline detection and external dependency loading
2
+ class OfflineManager {
3
+ constructor() {
4
+ this.isOnline = navigator.onLine;
5
+ this.stimulusLoaded = false;
6
+ this.chartLoaded = false;
7
+ this.initialized = false;
8
+
9
+ this.setupEventListeners();
10
+ }
11
+
12
+ setupEventListeners() {
13
+ // Set up online/offline event listeners
14
+ window.addEventListener('online', () => {
15
+ this.isOnline = true;
16
+ this.hideOfflineBanner();
17
+ this.hideOfflineContent();
18
+ });
19
+
20
+ window.addEventListener('offline', () => {
21
+ this.isOnline = false;
22
+ this.showOfflineBanner();
23
+ this.showOfflineContent();
24
+ });
25
+ }
26
+
27
+ // Function to show offline banner
28
+ showOfflineBanner() {
29
+ const banner = document.getElementById('offline-banner');
30
+ if (banner) {
31
+ banner.style.display = 'block';
32
+ }
33
+ }
34
+
35
+ // Function to hide offline banner
36
+ hideOfflineBanner() {
37
+ const banner = document.getElementById('offline-banner');
38
+ if (banner) {
39
+ banner.style.display = 'none';
40
+ }
41
+ }
42
+
43
+ // Function to show offline content
44
+ showOfflineContent() {
45
+ const content = document.getElementById('offline-content');
46
+ const mainContent = document.querySelector('.stats-grid');
47
+ if (content && mainContent) {
48
+ content.style.display = 'flex';
49
+ mainContent.style.display = 'none';
50
+ // Hide other main sections
51
+ document.querySelectorAll('.chart-grid, .processes-grid, .queues-grid').forEach(section => {
52
+ section.style.display = 'none';
53
+ });
54
+ }
55
+ }
56
+
57
+ // Function to hide offline content
58
+ hideOfflineContent() {
59
+ const content = document.getElementById('offline-content');
60
+ const mainContent = document.querySelector('.stats-grid');
61
+ if (content && mainContent) {
62
+ content.style.display = 'none';
63
+ mainContent.style.display = 'grid';
64
+ // Show other main sections
65
+ document.querySelectorAll('.chart-grid, .processes-grid, .queues-grid').forEach(section => {
66
+ section.style.display = 'grid';
67
+ });
68
+ }
69
+ }
70
+
71
+ // Function to load external scripts with fallback
72
+ loadExternalScript(src, fallbackCode, onLoad) {
73
+ const script = document.createElement('script');
74
+ script.src = src;
75
+ script.onload = function() {
76
+ onLoad(true);
77
+ };
78
+ script.onerror = function() {
79
+ console.warn('Failed to load external script:', src);
80
+ if (fallbackCode) {
81
+ // Execute fallback code
82
+ eval(fallbackCode);
83
+ onLoad(false);
84
+ } else {
85
+ onLoad(false);
86
+ }
87
+ };
88
+ document.head.appendChild(script);
89
+ }
90
+
91
+ // Load Stimulus with fallback
92
+ loadStimulus() {
93
+ return new Promise((resolve) => {
94
+ this.loadExternalScript(
95
+ 'https://unpkg.com/@hotwired/stimulus@3.2.2/dist/stimulus.umd.js',
96
+ `
97
+ // Minimal Stimulus fallback
98
+ window.Stimulus = {
99
+ Application: class {
100
+ constructor() { this.controllers = new Map(); }
101
+ register(name, controller) { this.controllers.set(name, controller); }
102
+ start() {
103
+ console.warn('Using offline Stimulus fallback');
104
+ // Basic controller initialization
105
+ document.querySelectorAll('[data-controller]').forEach(element => {
106
+ const controllers = element.dataset.controller.split(' ');
107
+ controllers.forEach(controllerName => {
108
+ const ControllerClass = this.controllers.get(controllerName);
109
+ if (ControllerClass) {
110
+ new ControllerClass(element);
111
+ }
112
+ });
113
+ });
114
+ }
115
+ },
116
+ Controller: class {
117
+ constructor(element) { this.element = element; }
118
+ get targets() {
119
+ return {
120
+ find: (name) => this.element.querySelector('[data-' + this.constructor.name.toLowerCase().replace('controller', '') + '-target="' + name + '"]'),
121
+ findAll: (name) => this.element.querySelectorAll('[data-' + this.constructor.name.toLowerCase().replace('controller', '') + '-target="' + name + '"]')
122
+ };
123
+ }
124
+ }
125
+ };
126
+ `,
127
+ (loaded) => {
128
+ this.stimulusLoaded = loaded;
129
+ if (!loaded) this.showOfflineBanner();
130
+ resolve(loaded);
131
+ }
132
+ );
133
+ });
134
+ }
135
+
136
+ // Load Chart.js with fallback
137
+ loadChartJs() {
138
+ return new Promise((resolve) => {
139
+ this.loadExternalScript(
140
+ 'https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js',
141
+ `
142
+ // Chart.js fallback - simple canvas drawing
143
+ window.Chart = class {
144
+ constructor(ctx, config) {
145
+ this.ctx = ctx;
146
+ this.config = config;
147
+ this.drawFallbackChart();
148
+ }
149
+
150
+ drawFallbackChart() {
151
+ const canvas = this.ctx.canvas;
152
+ const width = canvas.width;
153
+ const height = canvas.height;
154
+
155
+ // Clear canvas
156
+ this.ctx.clearRect(0, 0, width, height);
157
+
158
+ // Draw fallback message
159
+ this.ctx.fillStyle = '#666';
160
+ this.ctx.font = '14px system-ui';
161
+ this.ctx.textAlign = 'center';
162
+ this.ctx.fillText('Charts unavailable offline', width/2, height/2);
163
+ }
164
+
165
+ update() { this.drawFallbackChart(); }
166
+ destroy() {}
167
+ };
168
+ `,
169
+ (loaded) => {
170
+ this.chartLoaded = loaded;
171
+ if (!loaded) this.showOfflineBanner();
172
+ resolve(loaded);
173
+ }
174
+ );
175
+ });
176
+ }
177
+
178
+ // Load local scripts serially. Controllers reference `StimulusApp`
179
+ // defined by app.js, so they must not execute before app.js.
180
+ loadLocalScripts() {
181
+ const scripts = [
182
+ 'assets/js/app.js',
183
+ 'assets/js/controllers/theme_controller.js',
184
+ 'assets/js/controllers/connection_controller.js',
185
+ 'assets/js/controllers/dashboard_controller.js',
186
+ 'assets/js/controllers/queue_controller.js'
187
+ ];
188
+
189
+ const loadOne = (src) => new Promise((resolve) => {
190
+ const script = document.createElement('script');
191
+ script.src = src;
192
+ script.onload = () => resolve();
193
+ script.onerror = () => {
194
+ console.warn('Failed to load local script:', src);
195
+ resolve();
196
+ };
197
+ document.head.appendChild(script);
198
+ });
199
+
200
+ return scripts.reduce((chain, src) => chain.then(() => loadOne(src)), Promise.resolve());
201
+ }
202
+
203
+ // Initialize the application
204
+ async initialize() {
205
+ if (this.initialized) return;
206
+ this.initialized = true;
207
+
208
+ // Set up dismiss button
209
+ const dismissBtn = document.getElementById('dismiss-offline');
210
+ if (dismissBtn) {
211
+ dismissBtn.addEventListener('click', () => this.hideOfflineBanner());
212
+ }
213
+
214
+ // Check initial online status
215
+ if (!this.isOnline) {
216
+ this.showOfflineBanner();
217
+ // Show offline content if we're offline and external dependencies failed to load
218
+ setTimeout(() => {
219
+ if (!this.stimulusLoaded || !this.chartLoaded) {
220
+ this.showOfflineContent();
221
+ }
222
+ }, 2000); // Wait 2 seconds to see if dependencies load
223
+ }
224
+
225
+ // Load dependencies in sequence
226
+ await this.loadStimulus();
227
+ await this.loadChartJs();
228
+ await this.loadLocalScripts();
229
+ }
230
+ }
231
+
232
+ // Export for use in other modules
233
+ window.OfflineManager = OfflineManager;
@@ -0,0 +1,65 @@
1
+ // Service Worker Manager - Handles service worker registration and updates
2
+ class ServiceWorkerManager {
3
+ constructor() {
4
+ this.registration = null;
5
+ }
6
+
7
+ // Register service worker. The SW is purely an offline-cache enhancement,
8
+ // so we never await `.ready` — if the SW install fails (e.g. one cached
9
+ // asset 401s behind auth) we would otherwise hang the whole dashboard
10
+ // bootstrap waiting for a worker that will never activate.
11
+ async register() {
12
+ if (!('serviceWorker' in navigator)) {
13
+ console.warn('Service Worker not supported');
14
+ return false;
15
+ }
16
+
17
+ try {
18
+ this.registration = await navigator.serviceWorker.register('sw.js');
19
+ console.log('Service Worker registered:', this.registration.scope);
20
+
21
+ this.registration.addEventListener('updatefound', () => {
22
+ const newWorker = this.registration.installing;
23
+ newWorker.addEventListener('statechange', () => {
24
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
25
+ console.log('New service worker available. Reload to update.');
26
+ this.showUpdateNotification();
27
+ }
28
+ });
29
+ });
30
+
31
+ return true;
32
+ } catch (error) {
33
+ console.warn('Service Worker registration failed:', error);
34
+ return false;
35
+ }
36
+ }
37
+
38
+ // Show update notification (optional enhancement)
39
+ showUpdateNotification() {
40
+ // You could implement a notification here to prompt users to reload
41
+ // For now, we'll just log it
42
+ console.log('Update available - consider implementing user notification');
43
+ }
44
+
45
+ // Unregister service worker (for development/testing)
46
+ async unregister() {
47
+ if (this.registration) {
48
+ await this.registration.unregister();
49
+ console.log('Service Worker unregistered');
50
+ }
51
+ }
52
+
53
+ // Check if service worker is active
54
+ isActive() {
55
+ return navigator.serviceWorker.controller !== null;
56
+ }
57
+
58
+ // Get service worker registration
59
+ getRegistration() {
60
+ return this.registration;
61
+ }
62
+ }
63
+
64
+ // Export for use in other modules
65
+ window.ServiceWorkerManager = ServiceWorkerManager;
data/web/index.html ADDED
@@ -0,0 +1,159 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <base href="__BASE_PATH__" />
7
+ <title>Lepus Dashboard</title>
8
+ <link rel="stylesheet" href="assets/css/styles.css" />
9
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%236ea8fe'/%3E%3Ctext x='16' y='22' font-family='system-ui, -apple-system, sans-serif' font-size='18' font-weight='700' text-anchor='middle' fill='white'%3EL%3C/text%3E%3C/svg%3E" />
10
+ </head>
11
+ <body data-controller="theme connection dashboard">
12
+ <!-- Offline notification banner -->
13
+ <div id="offline-banner" class="offline-banner" style="display: none;">
14
+ <div class="offline-content">
15
+ <span class="offline-icon">⚠️</span>
16
+ <span class="offline-message">You're offline. Some features may not work properly.</span>
17
+ <button id="dismiss-offline" class="offline-dismiss">×</button>
18
+ </div>
19
+ </div>
20
+
21
+ <header class="navbar">
22
+ <div class="brand">
23
+ <span class="logo-badge">L</span>
24
+ <span class="title">Lepus</span>
25
+ </div>
26
+ <div class="nav-controls">
27
+ <div class="refresh-control">
28
+ <label for="refreshRange">Refresh:</label>
29
+ <input id="refreshRange" type="range" min="5" max="60" step="5" value="15" data-dashboard-target="refreshRange" data-action="input->dashboard#updateRefresh" />
30
+ <span class="refresh-value"><span data-dashboard-target="refreshLabel">15</span>s</span>
31
+ </div>
32
+ </div>
33
+ <div class="nav-actions">
34
+ <div class="connection-status" data-connection-target="indicator" title="Connection status">
35
+ <span class="status-dot"></span>
36
+ <span class="status-text" data-connection-target="text">Connected</span>
37
+ </div>
38
+ <button class="btn" data-action="theme#toggle" aria-label="Toggle theme" title="Toggle theme">
39
+ <span data-theme-target="icon">🌙</span>
40
+ </button>
41
+ </div>
42
+ </header>
43
+
44
+ <main class="container">
45
+ <!-- Offline content placeholder -->
46
+ <div id="offline-content" class="offline-content-placeholder" style="display: none;">
47
+ <div class="offline-card">
48
+ <div class="offline-icon-large">📡</div>
49
+ <h2>You're Offline</h2>
50
+ <p>The Lepus dashboard requires an internet connection to function properly.</p>
51
+ <p>Some features may be limited while offline:</p>
52
+ <ul class="offline-limitations">
53
+ <li>Real-time data updates won't work</li>
54
+ <li>External dependencies may not load</li>
55
+ <li>Charts may show fallback content</li>
56
+ </ul>
57
+ <div class="offline-actions">
58
+ <button data-offline-action="reload" class="btn btn-primary">Retry Connection</button>
59
+ <button data-offline-action="dismiss" class="btn btn-secondary">Continue Offline</button>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <section class="grid stats-grid">
65
+ <div class="card stat-card">
66
+ <div class="stat-label">Processes</div>
67
+ <div class="stat-value" data-dashboard-target="processCount">0</div>
68
+ <div class="stat-detail" data-dashboard-target="processDetail">Supervisors 0, Workers 0</div>
69
+ </div>
70
+ <div class="card stat-card">
71
+ <div class="stat-label">Active queues</div>
72
+ <div class="stat-value" data-dashboard-target="queueCount">0</div>
73
+ <div class="stat-detail" data-dashboard-target="queueDetail">0 running, 0 paused</div>
74
+ </div>
75
+ <div class="card stat-card">
76
+ <div class="stat-label">Messages</div>
77
+ <div class="stat-value" data-dashboard-target="totalMessages">0</div>
78
+ <div class="stat-detail" data-dashboard-target="messageDetail">0 ready, 0 unacked</div>
79
+ </div>
80
+ <div class="card stat-card">
81
+ <div class="stat-label">Memory usage</div>
82
+ <div class="stat-value" data-dashboard-target="memoryUsage">0 MB</div>
83
+ <div class="stat-detail" data-dashboard-target="memoryDetail">RSS 0 MB, Heap 0 MB</div>
84
+ </div>
85
+ <div class="card stat-card">
86
+ <div class="stat-label">Connections</div>
87
+ <div class="stat-value" data-dashboard-target="connectionCount">0</div>
88
+ <div class="stat-detail" data-dashboard-target="connectionDetail">0 active, 0 idle</div>
89
+ </div>
90
+ <div class="card stat-card">
91
+ <div class="stat-label">Message rates</div>
92
+ <div class="stat-value"><span data-dashboard-target="publishRate">0</span>/<span data-dashboard-target="consumeRate">0</span> msg/s</div>
93
+ <div class="stat-detail" data-dashboard-target="rateDetail">Peak: 0/0 msg/s</div>
94
+ </div>
95
+ </section>
96
+
97
+ <section class="grid chart-grid">
98
+ <div class="card">
99
+ <div class="card-header">
100
+ <h3>Message Rates</h3>
101
+ </div>
102
+ <div class="card-body">
103
+ <canvas id="rateChart" height="240"></canvas>
104
+ </div>
105
+ </div>
106
+ <div class="card">
107
+ <div class="card-header">
108
+ <h3>Queue Distribution</h3>
109
+ </div>
110
+ <div class="card-body">
111
+ <canvas id="queueChart" height="240"></canvas>
112
+ </div>
113
+ </div>
114
+ </section>
115
+
116
+ <section class="grid processes-grid" data-dashboard-target="processesRoot">
117
+ <!-- Process hierarchy cards rendered here -->
118
+ </section>
119
+
120
+ <section class="grid queues-grid">
121
+ <div class="card">
122
+ <div class="card-header">
123
+ <h3>Queues</h3>
124
+ </div>
125
+ <div class="card-body">
126
+ <div class="table-responsive">
127
+ <table class="table" data-controller="queue">
128
+ <thead>
129
+ <tr>
130
+ <th>Queue</th>
131
+ <th>Type</th>
132
+ <th>Ready</th>
133
+ <th>Unacked</th>
134
+ <th>Total</th>
135
+ <th>Consumers</th>
136
+ <th>Memory</th>
137
+ <th></th>
138
+ </tr>
139
+ </thead>
140
+ <tbody data-dashboard-target="queuesRoot">
141
+ <!-- Queue rows populated here -->
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </section>
148
+
149
+ </main>
150
+
151
+ <!-- Load offline support modules. Bootstrap is a separate file so it runs
152
+ under strict Content Security Policies that forbid inline scripts. -->
153
+ <script src="assets/js/offline-manager.js"></script>
154
+ <script src="assets/js/service-worker-manager.js"></script>
155
+ <script src="assets/js/bootstrap.js"></script>
156
+ </body>
157
+ </html>
158
+
159
+
data/web/sw.js ADDED
@@ -0,0 +1,144 @@
1
+ // Service Worker for Lepus Web Dashboard
2
+ // Provides offline caching for static assets
3
+
4
+ const CACHE_NAME = 'lepus-dashboard-v1';
5
+
6
+ // Paths are relative to the service worker script URL, so they resolve
7
+ // correctly whether the dashboard is mounted at `/` or a sub-path like `/lepus`.
8
+ const STATIC_ASSETS = [
9
+ './',
10
+ 'assets/css/styles.css',
11
+ 'assets/js/app.js',
12
+ 'assets/js/bootstrap.js',
13
+ 'assets/js/offline-manager.js',
14
+ 'assets/js/service-worker-manager.js',
15
+ 'assets/js/controllers/theme_controller.js',
16
+ 'assets/js/controllers/connection_controller.js',
17
+ 'assets/js/controllers/dashboard_controller.js',
18
+ 'assets/js/controllers/queue_controller.js'
19
+ ];
20
+
21
+ // Install event - cache static assets
22
+ self.addEventListener('install', (event) => {
23
+ console.log('Service Worker: Installing...');
24
+ event.waitUntil(
25
+ caches.open(CACHE_NAME)
26
+ .then((cache) => {
27
+ console.log('Service Worker: Caching static assets');
28
+ return cache.addAll(STATIC_ASSETS);
29
+ })
30
+ .then(() => {
31
+ console.log('Service Worker: Installation complete');
32
+ return self.skipWaiting();
33
+ })
34
+ .catch((error) => {
35
+ console.error('Service Worker: Installation failed', error);
36
+ })
37
+ );
38
+ });
39
+
40
+ // Activate event - clean up old caches
41
+ self.addEventListener('activate', (event) => {
42
+ console.log('Service Worker: Activating...');
43
+ event.waitUntil(
44
+ caches.keys().then((cacheNames) => {
45
+ return Promise.all(
46
+ cacheNames.map((cacheName) => {
47
+ if (cacheName !== CACHE_NAME) {
48
+ console.log('Service Worker: Deleting old cache', cacheName);
49
+ return caches.delete(cacheName);
50
+ }
51
+ })
52
+ );
53
+ }).then(() => {
54
+ console.log('Service Worker: Activation complete');
55
+ return self.clients.claim();
56
+ })
57
+ );
58
+ });
59
+
60
+ // Fetch event - serve from cache when offline
61
+ self.addEventListener('fetch', (event) => {
62
+ // Only handle GET requests
63
+ if (event.request.method !== 'GET') {
64
+ return;
65
+ }
66
+
67
+ // Skip cross-origin requests
68
+ if (!event.request.url.startsWith(self.location.origin)) {
69
+ return;
70
+ }
71
+
72
+ event.respondWith(
73
+ caches.match(event.request)
74
+ .then((cachedResponse) => {
75
+ // Return cached version if available
76
+ if (cachedResponse) {
77
+ console.log('Service Worker: Serving from cache', event.request.url);
78
+ return cachedResponse;
79
+ }
80
+
81
+ // Otherwise, fetch from network
82
+ console.log('Service Worker: Fetching from network', event.request.url);
83
+ return fetch(event.request)
84
+ .then((response) => {
85
+ // Don't cache non-successful responses
86
+ if (!response || response.status !== 200 || response.type !== 'basic') {
87
+ return response;
88
+ }
89
+
90
+ // Clone the response for caching
91
+ const responseToCache = response.clone();
92
+
93
+ caches.open(CACHE_NAME)
94
+ .then((cache) => {
95
+ // Only cache static assets under our scope, not API responses.
96
+ // `self.registration.scope` is the absolute URL the worker controls
97
+ // (e.g. "https://host/lepus/"), so it transparently covers any mount path.
98
+ const scope = new URL(self.registration.scope);
99
+ const url = new URL(event.request.url);
100
+ const scopePath = scope.pathname;
101
+ const path = url.pathname;
102
+ const isUnderScope = path.startsWith(scopePath);
103
+ const relative = isUnderScope ? path.slice(scopePath.length) : path;
104
+ if (isUnderScope && (relative === '' || relative === 'index.html' || relative.startsWith('assets/'))) {
105
+ console.log('Service Worker: Caching response', event.request.url);
106
+ cache.put(event.request, responseToCache);
107
+ }
108
+ });
109
+
110
+ return response;
111
+ })
112
+ .catch((error) => {
113
+ console.log('Service Worker: Network fetch failed', event.request.url, error);
114
+
115
+ // For navigation requests, return the cached index (scope root)
116
+ if (event.request.mode === 'navigate') {
117
+ return caches.match('./');
118
+ }
119
+
120
+ // For other requests, return a generic offline response
121
+ if (event.request.url.endsWith('.js') || event.request.url.endsWith('.css')) {
122
+ return new Response(
123
+ '/* Offline - Resource not available */',
124
+ {
125
+ status: 200,
126
+ headers: {
127
+ 'Content-Type': event.request.url.endsWith('.js') ? 'application/javascript' : 'text/css'
128
+ }
129
+ }
130
+ );
131
+ }
132
+
133
+ throw error;
134
+ });
135
+ })
136
+ );
137
+ });
138
+
139
+ // Handle messages from the main thread
140
+ self.addEventListener('message', (event) => {
141
+ if (event.data && event.data.type === 'SKIP_WAITING') {
142
+ self.skipWaiting();
143
+ }
144
+ });