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.
- checksums.yaml +4 -4
- data/.github/workflows/linter.yml +21 -0
- data/.github/workflows/specs.yml +93 -13
- data/.gitignore +2 -0
- data/.rubocop.yml +10 -0
- data/.tool-versions +1 -1
- data/Gemfile +7 -0
- data/Gemfile.lock +36 -9
- data/Makefile +19 -0
- data/README.md +562 -7
- data/bin/setup +5 -2
- data/config.ru +14 -0
- data/docker-compose.yml +5 -3
- data/docs/README.md +80 -0
- data/docs/cli.md +108 -0
- data/docs/configuration.md +171 -0
- data/docs/consumers.md +168 -0
- data/docs/getting-started.md +136 -0
- data/docs/images/lepus-web.png +0 -0
- data/docs/middleware.md +240 -0
- data/docs/producers.md +173 -0
- data/docs/prometheus.md +112 -0
- data/docs/rails.md +161 -0
- data/docs/supervisor.md +112 -0
- data/docs/testing.md +141 -0
- data/docs/web.md +85 -0
- data/examples/grafana-dashboard.json +450 -0
- data/gemfiles/Gemfile.rails-5.2 +7 -0
- data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
- data/gemfiles/Gemfile.rails-6.1 +7 -0
- data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
- data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
- data/gemfiles/Gemfile.rails-7.2.lock +321 -0
- data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
- data/gemfiles/Gemfile.rails-8.0.lock +322 -0
- data/lepus.gemspec +7 -1
- data/lib/lepus/cli.rb +35 -4
- data/lib/lepus/configuration.rb +107 -0
- data/lib/lepus/connection_pool.rb +135 -0
- data/lib/lepus/consumer.rb +59 -41
- data/lib/lepus/consumers/config.rb +183 -0
- data/lib/lepus/consumers/handler.rb +56 -0
- data/lib/lepus/consumers/middleware_chain.rb +22 -0
- data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
- data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
- data/lib/lepus/consumers/middlewares/json.rb +37 -0
- data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
- data/lib/lepus/consumers/middlewares/unique.rb +65 -0
- data/lib/lepus/consumers/stats.rb +70 -0
- data/lib/lepus/consumers/stats_registry.rb +29 -0
- data/lib/lepus/consumers/worker.rb +141 -0
- data/lib/lepus/consumers/worker_factory.rb +124 -0
- data/lib/lepus/consumers.rb +6 -0
- data/lib/lepus/message/delivery_info.rb +72 -0
- data/lib/lepus/message/metadata.rb +99 -0
- data/lib/lepus/message.rb +88 -5
- data/lib/lepus/middleware_chain.rb +83 -0
- data/lib/lepus/primitive/hash.rb +29 -0
- data/lib/lepus/process.rb +24 -24
- data/lib/lepus/process_registry/backend.rb +49 -0
- data/lib/lepus/process_registry/file_backend.rb +108 -0
- data/lib/lepus/process_registry/message_builder.rb +72 -0
- data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
- data/lib/lepus/process_registry.rb +56 -23
- data/lib/lepus/processes/base.rb +0 -5
- data/lib/lepus/processes/callbacks.rb +3 -0
- data/lib/lepus/processes/interruptible.rb +4 -8
- data/lib/lepus/processes/procline.rb +1 -1
- data/lib/lepus/processes/registrable.rb +1 -1
- data/lib/lepus/processes/runnable.rb +1 -1
- data/lib/lepus/processes.rb +15 -0
- data/lib/lepus/producer.rb +141 -30
- data/lib/lepus/producers/config.rb +46 -0
- data/lib/lepus/producers/definition.rb +48 -0
- data/lib/lepus/producers/hooks.rb +170 -0
- data/lib/lepus/producers/middleware_chain.rb +22 -0
- data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
- data/lib/lepus/producers/middlewares/header.rb +47 -0
- data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
- data/lib/lepus/producers/middlewares/json.rb +47 -0
- data/lib/lepus/producers/middlewares/unique.rb +67 -0
- data/lib/lepus/producers.rb +7 -0
- data/lib/lepus/prometheus/collector.rb +149 -0
- data/lib/lepus/prometheus/instrumentation.rb +168 -0
- data/lib/lepus/prometheus.rb +48 -0
- data/lib/lepus/publisher.rb +67 -0
- data/lib/lepus/supervisor/children_pipes.rb +25 -0
- data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
- data/lib/lepus/supervisor/pidfiled.rb +1 -1
- data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
- data/lib/lepus/supervisor.rb +129 -25
- data/lib/lepus/testing/exchange.rb +95 -0
- data/lib/lepus/testing/message_builder.rb +177 -0
- data/lib/lepus/testing/rspec_matchers.rb +258 -0
- data/lib/lepus/testing.rb +210 -0
- data/lib/lepus/unique.rb +18 -0
- data/lib/lepus/version.rb +1 -1
- data/lib/lepus/web/aggregator.rb +154 -0
- data/lib/lepus/web/api.rb +132 -0
- data/lib/lepus/web/app.rb +37 -0
- data/lib/lepus/web/management_api.rb +192 -0
- data/lib/lepus/web/respond_with.rb +28 -0
- data/lib/lepus/web.rb +238 -0
- data/lib/lepus.rb +39 -28
- data/test_offline.html +189 -0
- data/web/assets/css/styles.css +635 -0
- data/web/assets/js/app.js +6 -0
- data/web/assets/js/bootstrap.js +20 -0
- data/web/assets/js/controllers/connection_controller.js +44 -0
- data/web/assets/js/controllers/dashboard_controller.js +499 -0
- data/web/assets/js/controllers/queue_controller.js +17 -0
- data/web/assets/js/controllers/theme_controller.js +31 -0
- data/web/assets/js/offline-manager.js +233 -0
- data/web/assets/js/service-worker-manager.js +65 -0
- data/web/index.html +159 -0
- data/web/sw.js +144 -0
- metadata +177 -18
- data/lib/lepus/consumer_config.rb +0 -149
- data/lib/lepus/consumer_wrapper.rb +0 -46
- data/lib/lepus/lifecycle_hooks.rb +0 -49
- data/lib/lepus/middlewares/honeybadger.rb +0 -23
- data/lib/lepus/middlewares/json.rb +0 -35
- data/lib/lepus/middlewares/max_retry.rb +0 -57
- data/lib/lepus/processes/consumer.rb +0 -113
- 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
|
+
});
|