passive_queue 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 +7 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +35 -0
- data/LICENSE +21 -0
- data/README.md +1 -0
- data/bin/be +9 -0
- data/html/landing.html +1237 -0
- data/html/logo-dark.svg +166 -0
- data/html/logo.svg +166 -0
- data/lib/active_job/queue_adapters/passive_queue_adapter.rb +16 -0
- data/lib/passive_queue/adapter.rb +42 -0
- data/lib/passive_queue/cli.rb +139 -0
- data/lib/passive_queue/configuration.rb +23 -0
- data/lib/passive_queue/engine.rb +26 -0
- data/lib/passive_queue/railtie.rb +18 -0
- data/lib/passive_queue/version.rb +7 -0
- data/lib/passive_queue/web.rb +623 -0
- data/lib/passive_queue.rb +52 -0
- metadata +63 -0
@@ -0,0 +1,623 @@
|
|
1
|
+
# ================================
|
2
|
+
# lib/passive_queue/web.rb
|
3
|
+
# ================================
|
4
|
+
require 'erb'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module PassiveQueue
|
8
|
+
class Web
|
9
|
+
def call(env)
|
10
|
+
request = Rack::Request.new(env)
|
11
|
+
|
12
|
+
case request.path_info
|
13
|
+
when '/'
|
14
|
+
dashboard_response
|
15
|
+
when '/api/stats'
|
16
|
+
api_stats_response
|
17
|
+
when '/api/zen'
|
18
|
+
api_zen_response
|
19
|
+
when '/favicon.ico'
|
20
|
+
favicon_response
|
21
|
+
when '/logo.svg'
|
22
|
+
logo_response
|
23
|
+
when '/logo-dark.svg'
|
24
|
+
logo_dark_response
|
25
|
+
else
|
26
|
+
not_found_response
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def dashboard_response
|
33
|
+
html = dashboard_html
|
34
|
+
[200, {'Content-Type' => 'text/html'}, [html]]
|
35
|
+
end
|
36
|
+
|
37
|
+
def api_stats_response
|
38
|
+
stats = {
|
39
|
+
jobs_queued: rand(9999..99999),
|
40
|
+
jobs_processed: 0,
|
41
|
+
jobs_failed: 0,
|
42
|
+
jobs_succeeded: "∞",
|
43
|
+
uptime: "#{rand(1..999)} days of perfect inactivity",
|
44
|
+
memory_usage: "0 MB",
|
45
|
+
cpu_usage: "0%",
|
46
|
+
queue_names: ["default", "mailers", "active_storage", "imports", "exports"].sample(rand(2..5)),
|
47
|
+
processing_time: "0ms",
|
48
|
+
success_rate: "100%",
|
49
|
+
zen_level: ["Transcendent", "Enlightened", "Peaceful", "Serene"].sample
|
50
|
+
}
|
51
|
+
[200, {'Content-Type' => 'application/json'}, [stats.to_json]]
|
52
|
+
end
|
53
|
+
|
54
|
+
def api_zen_response
|
55
|
+
quote = PassiveQueue.zen_quotes.sample
|
56
|
+
[200, {'Content-Type' => 'application/json'}, [{quote: quote}.to_json]]
|
57
|
+
end
|
58
|
+
|
59
|
+
def css_response
|
60
|
+
css = dashboard_css
|
61
|
+
[200, {'Content-Type' => 'text/css'}, [css]]
|
62
|
+
end
|
63
|
+
|
64
|
+
def logo_response
|
65
|
+
svg = logo_svg
|
66
|
+
[200, {'Content-Type' => 'image/svg+xml'}, [svg]]
|
67
|
+
end
|
68
|
+
|
69
|
+
def logo_dark_response
|
70
|
+
svg = logo_svg_dark
|
71
|
+
[200, {'Content-Type' => 'image/svg+xml'}, [svg]]
|
72
|
+
end
|
73
|
+
|
74
|
+
def favicon_response
|
75
|
+
# Return empty response for favicon
|
76
|
+
[200, {'Content-Type' => 'image/x-icon'}, ['']]
|
77
|
+
end
|
78
|
+
|
79
|
+
def not_found_response
|
80
|
+
[404, {'Content-Type' => 'text/html'}, ['<h1>404 - Page Not Found (Just Like Our Jobs)</h1>']]
|
81
|
+
end
|
82
|
+
|
83
|
+
def logo_svg
|
84
|
+
logo_path = File.join(File.dirname(__FILE__), '..', '..', 'html', 'logo.svg')
|
85
|
+
File.read(logo_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
def logo_svg_dark
|
89
|
+
logo_path = File.join(File.dirname(__FILE__), '..', '..', 'html', 'logo-dark.svg')
|
90
|
+
File.read(logo_path)
|
91
|
+
end
|
92
|
+
|
93
|
+
def dashboard_html
|
94
|
+
<<~HTML
|
95
|
+
<!DOCTYPE html>
|
96
|
+
<html lang="en" data-theme="light">
|
97
|
+
<head>
|
98
|
+
<meta charset="UTF-8">
|
99
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
100
|
+
<title>Passive Queue Dashboard - The Art of Non-Execution</title>
|
101
|
+
|
102
|
+
<script>
|
103
|
+
(function() {
|
104
|
+
// Get saved theme or default to auto
|
105
|
+
const savedTheme = localStorage.getItem('theme');
|
106
|
+
const themeToApply = savedTheme !== null ? savedTheme : 'auto';
|
107
|
+
|
108
|
+
function getSystemTheme() {
|
109
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
110
|
+
}
|
111
|
+
|
112
|
+
let actualTheme;
|
113
|
+
if (themeToApply === 'auto') {
|
114
|
+
actualTheme = getSystemTheme();
|
115
|
+
} else {
|
116
|
+
actualTheme = themeToApply;
|
117
|
+
}
|
118
|
+
|
119
|
+
// Apply theme immediately to prevent flash
|
120
|
+
document.documentElement.setAttribute('data-theme', actualTheme);
|
121
|
+
|
122
|
+
// Store current preference for later use
|
123
|
+
window.currentThemePreference = themeToApply;
|
124
|
+
})();
|
125
|
+
</script>
|
126
|
+
|
127
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
128
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
129
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
130
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
131
|
+
|
132
|
+
<style>
|
133
|
+
body { font-family: 'Inter', sans-serif; }
|
134
|
+
|
135
|
+
/* Light mode gradients and shadows (default) */
|
136
|
+
.zen-gradient {
|
137
|
+
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
138
|
+
}
|
139
|
+
.peaceful-shadow {
|
140
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
141
|
+
}
|
142
|
+
|
143
|
+
/* Dark mode variations */
|
144
|
+
[data-theme="dark"] .zen-gradient {
|
145
|
+
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
146
|
+
}
|
147
|
+
[data-theme="dark"] .peaceful-shadow {
|
148
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
149
|
+
}
|
150
|
+
|
151
|
+
/* Animations remain the same */
|
152
|
+
.loading-dots::after {
|
153
|
+
content: '';
|
154
|
+
animation: dots 2s infinite;
|
155
|
+
}
|
156
|
+
@keyframes dots {
|
157
|
+
0%, 20% { content: '.'; }
|
158
|
+
40% { content: '..'; }
|
159
|
+
60% { content: '...'; }
|
160
|
+
80%, 100% { content: ''; }
|
161
|
+
}
|
162
|
+
.float { animation: float 6s ease-in-out infinite; }
|
163
|
+
@keyframes float {
|
164
|
+
0%, 100% { transform: translateY(0px); }
|
165
|
+
50% { transform: translateY(-10px); }
|
166
|
+
}
|
167
|
+
|
168
|
+
.pulse-zen { animation: pulse-zen 3s ease-in-out infinite; }
|
169
|
+
@keyframes pulse-zen {
|
170
|
+
0%, 100% { opacity: 1; }
|
171
|
+
50% { opacity: 0.7; }
|
172
|
+
}
|
173
|
+
|
174
|
+
/* Theme toggle button styles */
|
175
|
+
.theme-toggle {
|
176
|
+
display: none;
|
177
|
+
width: 48px;
|
178
|
+
height: 24px;
|
179
|
+
background: var(--fallback-b2,oklch(var(--b2)));
|
180
|
+
border-radius: 12px;
|
181
|
+
border: 2px solid var(--fallback-bc,oklch(var(--bc)/0.2));
|
182
|
+
cursor: pointer;
|
183
|
+
position: relative;
|
184
|
+
transition: all 0.3s ease;
|
185
|
+
}
|
186
|
+
|
187
|
+
.theme-toggle::before {
|
188
|
+
content: '';
|
189
|
+
position: absolute;
|
190
|
+
top: 50%;
|
191
|
+
left: 2px;
|
192
|
+
transform: translateY(-50%);
|
193
|
+
font-size: 14px;
|
194
|
+
transition: all 0.3s ease;
|
195
|
+
}
|
196
|
+
|
197
|
+
[data-theme="dark"] .theme-toggle::before {
|
198
|
+
content: '';
|
199
|
+
left: 22px;
|
200
|
+
}
|
201
|
+
|
202
|
+
.theme-toggle::after {
|
203
|
+
content: '';
|
204
|
+
position: absolute;
|
205
|
+
top: 2px;
|
206
|
+
left: 2px;
|
207
|
+
width: 16px;
|
208
|
+
height: 16px;
|
209
|
+
background: var(--fallback-bc,oklch(var(--bc)));
|
210
|
+
border-radius: 50%;
|
211
|
+
transition: all 0.3s ease;
|
212
|
+
}
|
213
|
+
|
214
|
+
[data-theme="dark"] .theme-toggle::after {
|
215
|
+
transform: translateX(20px);
|
216
|
+
}
|
217
|
+
</style>
|
218
|
+
</head>
|
219
|
+
<body class="zen-gradient min-h-screen">
|
220
|
+
<!-- Navigation -->
|
221
|
+
<div class="navbar bg-base-100/80 backdrop-blur-sm peaceful-shadow">
|
222
|
+
<div class="navbar-start">
|
223
|
+
<a class="btn btn-ghost text-xl font-light">
|
224
|
+
<!-- Light mode logo -->
|
225
|
+
<img src="/passive_queue/logo.svg" alt="Passive Queue Logo"
|
226
|
+
class="w-7 h-7 mr-2 logo-light">
|
227
|
+
<!-- Dark mode logo -->
|
228
|
+
<img src="/passive_queue/logo-dark.svg" alt="Passive Queue Logo"
|
229
|
+
class="w-7 h-7 mr-2 hidden logo-dark">
|
230
|
+
Passive Queue Dashboard
|
231
|
+
</a>
|
232
|
+
</div>
|
233
|
+
<div class="navbar-center">
|
234
|
+
<div class="badge badge-success badge-lg">
|
235
|
+
<span class="loading loading-ring loading-xs mr-1"></span>
|
236
|
+
Non-Processing
|
237
|
+
</div>
|
238
|
+
</div>
|
239
|
+
<div class="navbar-end">
|
240
|
+
<!-- Theme Toggle -->
|
241
|
+
<div class="flex items-center gap-2 mr-4">
|
242
|
+
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme"></button>
|
243
|
+
<div class="dropdown dropdown-end z-[100]">
|
244
|
+
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
245
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
246
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
247
|
+
</svg>
|
248
|
+
</div>
|
249
|
+
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52 z-50 z-[100]">
|
250
|
+
<li><a onclick="setTheme('light')">☀️ Light Mode</a></li>
|
251
|
+
<li><a onclick="setTheme('dark')">🌙 Dark Mode</a></li>
|
252
|
+
<li><a onclick="setTheme('auto')">🔄 Auto (System)</a></li>
|
253
|
+
</ul>
|
254
|
+
</div>
|
255
|
+
</div>
|
256
|
+
</div>
|
257
|
+
</div>
|
258
|
+
|
259
|
+
<!-- Main Dashboard -->
|
260
|
+
<div class="container mx-auto px-4 py-8">
|
261
|
+
<!-- Hero Stats -->
|
262
|
+
<div class="stats stats-vertical lg:stats-horizontal shadow w-full mb-8 bg-base-100 peaceful-shadow">
|
263
|
+
<div class="stat">
|
264
|
+
<div class="stat-figure text-primary">
|
265
|
+
<div class="text-3xl float">📊</div>
|
266
|
+
</div>
|
267
|
+
<div class="stat-title">Jobs Queued</div>
|
268
|
+
<div class="stat-value text-primary" id="jobs-queued">∞</div>
|
269
|
+
<div class="stat-desc">All waiting peacefully</div>
|
270
|
+
</div>
|
271
|
+
|
272
|
+
<div class="stat">
|
273
|
+
<div class="stat-figure text-secondary">
|
274
|
+
<div class="text-3xl pulse-zen">✨</div>
|
275
|
+
</div>
|
276
|
+
<div class="stat-title">Jobs Processed</div>
|
277
|
+
<div class="stat-value text-secondary" id="jobs-processed">0</div>
|
278
|
+
<div class="stat-desc">Perfect execution rate</div>
|
279
|
+
</div>
|
280
|
+
|
281
|
+
<div class="stat">
|
282
|
+
<div class="stat-figure text-accent">
|
283
|
+
<div class="text-3xl">⏱️</div>
|
284
|
+
</div>
|
285
|
+
<div class="stat-title">Avg Processing Time</div>
|
286
|
+
<div class="stat-value text-accent" id="processing-time">0ms</div>
|
287
|
+
<div class="stat-desc">Blazing fast non-execution</div>
|
288
|
+
</div>
|
289
|
+
|
290
|
+
<div class="stat">
|
291
|
+
<div class="stat-figure text-success">
|
292
|
+
<div class="text-3xl">🎯</div>
|
293
|
+
</div>
|
294
|
+
<div class="stat-title">Success Rate</div>
|
295
|
+
<div class="stat-value text-success" id="success-rate">100%</div>
|
296
|
+
<div class="stat-desc">At doing nothing</div>
|
297
|
+
</div>
|
298
|
+
</div>
|
299
|
+
|
300
|
+
<div class="grid lg:grid-cols-2 gap-8 mb-8">
|
301
|
+
<!-- Queue Status -->
|
302
|
+
<div class="card bg-base-100 peaceful-shadow">
|
303
|
+
<div class="card-body">
|
304
|
+
<h2 class="card-title">
|
305
|
+
<span class="text-2xl mr-2">📋</span>
|
306
|
+
Queue Status
|
307
|
+
</h2>
|
308
|
+
|
309
|
+
<div class="space-y-4">
|
310
|
+
<div class="flex justify-between items-center">
|
311
|
+
<span class="font-medium">Active Queues</span>
|
312
|
+
<div class="badge badge-info" id="active-queues">5</div>
|
313
|
+
</div>
|
314
|
+
|
315
|
+
<div class="space-y-2" id="queue-list">
|
316
|
+
<!-- Queues will be populated by JavaScript -->
|
317
|
+
</div>
|
318
|
+
</div>
|
319
|
+
|
320
|
+
<div class="card-actions justify-end mt-4">
|
321
|
+
<button class="btn btn-sm btn-outline" onclick="refreshQueues()">
|
322
|
+
<span class="loading loading-spinner loading-xs mr-1 hidden" id="refresh-spinner"></span>
|
323
|
+
Refresh
|
324
|
+
</button>
|
325
|
+
</div>
|
326
|
+
</div>
|
327
|
+
</div>
|
328
|
+
|
329
|
+
<!-- System Status -->
|
330
|
+
<div class="card bg-base-100 peaceful-shadow">
|
331
|
+
<div class="card-body">
|
332
|
+
<h2 class="card-title">
|
333
|
+
<span class="text-2xl mr-2">💻</span>
|
334
|
+
System Status
|
335
|
+
</h2>
|
336
|
+
|
337
|
+
<div class="space-y-4">
|
338
|
+
<div class="flex justify-between items-center">
|
339
|
+
<span>Memory Usage</span>
|
340
|
+
<div class="text-right">
|
341
|
+
<div class="text-sm font-mono" id="memory-usage">0 MB</div>
|
342
|
+
<progress class="progress progress-success w-24" value="0" max="100"></progress>
|
343
|
+
</div>
|
344
|
+
</div>
|
345
|
+
|
346
|
+
<div class="flex justify-between items-center">
|
347
|
+
<span>CPU Usage</span>
|
348
|
+
<div class="text-right">
|
349
|
+
<div class="text-sm font-mono" id="cpu-usage">0%</div>
|
350
|
+
<progress class="progress progress-success w-24" value="0" max="100"></progress>
|
351
|
+
</div>
|
352
|
+
</div>
|
353
|
+
|
354
|
+
<div class="flex justify-between items-center">
|
355
|
+
<span>Uptime</span>
|
356
|
+
<div class="text-sm font-mono" id="uptime">∞ days</div>
|
357
|
+
</div>
|
358
|
+
|
359
|
+
<div class="flex justify-between items-center">
|
360
|
+
<span>Zen Level</span>
|
361
|
+
<div class="badge badge-primary" id="zen-level">Transcendent</div>
|
362
|
+
</div>
|
363
|
+
</div>
|
364
|
+
</div>
|
365
|
+
</div>
|
366
|
+
</div>
|
367
|
+
|
368
|
+
<!-- Zen Quotes Section -->
|
369
|
+
<div class="card bg-base-100 peaceful-shadow mb-8">
|
370
|
+
<div class="card-body text-center">
|
371
|
+
<h2 class="card-title justify-center">
|
372
|
+
<span class="text-2xl mr-2">🧘♂️</span>
|
373
|
+
Daily Zen
|
374
|
+
</h2>
|
375
|
+
|
376
|
+
<div class="max-w-2xl mx-auto">
|
377
|
+
<blockquote class="text-lg italic text-base-content/80 mb-4" id="zen-quote">
|
378
|
+
"Loading wisdom..."
|
379
|
+
</blockquote>
|
380
|
+
|
381
|
+
<button class="btn btn-primary btn-sm" onclick="getNewZenQuote()">
|
382
|
+
New Wisdom
|
383
|
+
</button>
|
384
|
+
</div>
|
385
|
+
</div>
|
386
|
+
</div>
|
387
|
+
|
388
|
+
<!-- Recent Non-Activity -->
|
389
|
+
<div class="card bg-base-100 peaceful-shadow">
|
390
|
+
<div class="card-body">
|
391
|
+
<h2 class="card-title">
|
392
|
+
<span class="text-2xl mr-2">📝</span>
|
393
|
+
Recent Non-Activity
|
394
|
+
</h2>
|
395
|
+
|
396
|
+
<div class="overflow-x-auto">
|
397
|
+
<table class="table table-zebra">
|
398
|
+
<thead>
|
399
|
+
<tr>
|
400
|
+
<th>Job ID</th>
|
401
|
+
<th>Queue</th>
|
402
|
+
<th>Class</th>
|
403
|
+
<th>Status</th>
|
404
|
+
<th>Non-Executed At</th>
|
405
|
+
</tr>
|
406
|
+
</thead>
|
407
|
+
<tbody id="recent-jobs">
|
408
|
+
<!-- Jobs will be populated by JavaScript -->
|
409
|
+
</tbody>
|
410
|
+
</table>
|
411
|
+
</div>
|
412
|
+
|
413
|
+
<div class="text-center mt-4">
|
414
|
+
<div class="text-sm text-base-content/60">
|
415
|
+
All jobs are successfully not being processed 🎯
|
416
|
+
</div>
|
417
|
+
</div>
|
418
|
+
</div>
|
419
|
+
</div>
|
420
|
+
</div>
|
421
|
+
|
422
|
+
<!-- JavaScript -->
|
423
|
+
<script>
|
424
|
+
function updateLogos() {
|
425
|
+
// Check the actual applied theme instead of system preference
|
426
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
427
|
+
const lightLogos = document.querySelectorAll('.logo-light');
|
428
|
+
const darkLogos = document.querySelectorAll('.logo-dark');
|
429
|
+
|
430
|
+
if (currentTheme === 'dark') {
|
431
|
+
lightLogos.forEach(logo => logo.classList.add('hidden'));
|
432
|
+
darkLogos.forEach(logo => logo.classList.remove('hidden'));
|
433
|
+
} else {
|
434
|
+
lightLogos.forEach(logo => logo.classList.remove('hidden'));
|
435
|
+
darkLogos.forEach(logo => logo.classList.add('hidden'));
|
436
|
+
}
|
437
|
+
}
|
438
|
+
|
439
|
+
// Theme management system
|
440
|
+
let currentThemePreference = 'auto'; // Track current preference
|
441
|
+
|
442
|
+
function getSystemTheme() {
|
443
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
444
|
+
}
|
445
|
+
|
446
|
+
function applyTheme(theme) {
|
447
|
+
currentThemePreference = theme;
|
448
|
+
|
449
|
+
if (theme === 'auto') {
|
450
|
+
const systemTheme = getSystemTheme();
|
451
|
+
document.documentElement.setAttribute('data-theme', systemTheme);
|
452
|
+
return systemTheme;
|
453
|
+
} else {
|
454
|
+
document.documentElement.setAttribute('data-theme', theme);
|
455
|
+
return theme;
|
456
|
+
}
|
457
|
+
}
|
458
|
+
|
459
|
+
function setTheme(theme) {
|
460
|
+
localStorage.setItem('theme', theme);
|
461
|
+
applyTheme(theme);
|
462
|
+
updateLogos();
|
463
|
+
}
|
464
|
+
|
465
|
+
function toggleTheme() {
|
466
|
+
const themes = ['light', 'dark', 'auto'];
|
467
|
+
const currentIndex = themes.indexOf(currentThemePreference);
|
468
|
+
const nextTheme = themes[(currentIndex + 1) % themes.length];
|
469
|
+
setTheme(nextTheme);
|
470
|
+
}
|
471
|
+
|
472
|
+
function initTheme() {
|
473
|
+
// Get saved theme or default to auto
|
474
|
+
const savedTheme = localStorage.getItem('theme');
|
475
|
+
const themeToApply = savedTheme !== null ? savedTheme : 'auto';
|
476
|
+
|
477
|
+
applyTheme(themeToApply);
|
478
|
+
|
479
|
+
// Listen for system theme changes
|
480
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
481
|
+
if (currentThemePreference === 'auto') {
|
482
|
+
applyTheme('auto');
|
483
|
+
updateLogos();
|
484
|
+
}
|
485
|
+
});
|
486
|
+
|
487
|
+
updateLogos();
|
488
|
+
}
|
489
|
+
|
490
|
+
async function loadStats() {
|
491
|
+
try {
|
492
|
+
const response = await fetch('/passive_queue/api/stats');
|
493
|
+
const stats = await response.json();
|
494
|
+
|
495
|
+
document.getElementById('jobs-queued').textContent = stats.jobs_queued.toLocaleString();
|
496
|
+
document.getElementById('jobs-processed').textContent = stats.jobs_processed;
|
497
|
+
document.getElementById('processing-time').textContent = stats.processing_time;
|
498
|
+
document.getElementById('success-rate').textContent = stats.success_rate;
|
499
|
+
document.getElementById('memory-usage').textContent = stats.memory_usage;
|
500
|
+
document.getElementById('cpu-usage').textContent = stats.cpu_usage;
|
501
|
+
document.getElementById('uptime').textContent = stats.uptime;
|
502
|
+
document.getElementById('zen-level').textContent = stats.zen_level;
|
503
|
+
|
504
|
+
// Update queue list
|
505
|
+
const queueList = document.getElementById('queue-list');
|
506
|
+
queueList.innerHTML = stats.queue_names.map(queue =>
|
507
|
+
`<div class="flex justify-between items-center">
|
508
|
+
<span class="text-sm">${queue}</span>
|
509
|
+
<div class="badge badge-ghost badge-sm">0 processing</div>
|
510
|
+
</div>`
|
511
|
+
).join('');
|
512
|
+
|
513
|
+
document.getElementById('active-queues').textContent = stats.queue_names.length;
|
514
|
+
|
515
|
+
} catch (error) {
|
516
|
+
console.error('Failed to load stats:', error);
|
517
|
+
}
|
518
|
+
}
|
519
|
+
|
520
|
+
async function getNewZenQuote() {
|
521
|
+
try {
|
522
|
+
const response = await fetch('/passive_queue/api/zen');
|
523
|
+
const data = await response.json();
|
524
|
+
document.getElementById('zen-quote').textContent = data.quote;
|
525
|
+
} catch (error) {
|
526
|
+
console.error('Failed to load zen quote:', error);
|
527
|
+
}
|
528
|
+
}
|
529
|
+
|
530
|
+
function refreshQueues() {
|
531
|
+
const spinner = document.getElementById('refresh-spinner');
|
532
|
+
spinner.classList.remove('hidden');
|
533
|
+
|
534
|
+
setTimeout(() => {
|
535
|
+
loadStats();
|
536
|
+
spinner.classList.add('hidden');
|
537
|
+
}, 1000);
|
538
|
+
}
|
539
|
+
|
540
|
+
function generateRecentJobs() {
|
541
|
+
const jobClasses = ['UserMailer', 'DataProcessor', 'ImageResizer', 'ReportGenerator', 'BackupJob'];
|
542
|
+
const queues = ['default', 'mailers', 'critical', 'background'];
|
543
|
+
const tbody = document.getElementById('recent-jobs');
|
544
|
+
|
545
|
+
const jobs = Array.from({length: 10}, (_, i) => {
|
546
|
+
const jobId = `passive-${Date.now() + i}`;
|
547
|
+
const queue = queues[Math.floor(Math.random() * queues.length)];
|
548
|
+
const jobClass = jobClasses[Math.floor(Math.random() * jobClasses.length)];
|
549
|
+
const time = new Date(Date.now() - Math.random() * 3600000);
|
550
|
+
|
551
|
+
return `
|
552
|
+
<tr>
|
553
|
+
<td><code class="text-xs">${jobId}</code></td>
|
554
|
+
<td><span class="badge badge-outline badge-xs">${queue}</span></td>
|
555
|
+
<td>${jobClass}</td>
|
556
|
+
<td><div class="badge badge-success badge-xs">Not Processed</div></td>
|
557
|
+
<td class="text-xs">${time.toLocaleString()}</td>
|
558
|
+
</tr>
|
559
|
+
`;
|
560
|
+
});
|
561
|
+
|
562
|
+
tbody.innerHTML = jobs.join('');
|
563
|
+
}
|
564
|
+
|
565
|
+
// Initialize dashboard
|
566
|
+
document.addEventListener('DOMContentLoaded', function() {
|
567
|
+
initTheme();
|
568
|
+
loadStats();
|
569
|
+
getNewZenQuote();
|
570
|
+
generateRecentJobs();
|
571
|
+
|
572
|
+
// Auto-refresh every 30 seconds
|
573
|
+
setInterval(loadStats, 30000);
|
574
|
+
|
575
|
+
// Change zen quote every 2 minutes
|
576
|
+
setInterval(getNewZenQuote, 120000);
|
577
|
+
});
|
578
|
+
|
579
|
+
// Run theme init immediately to prevent flash
|
580
|
+
if (document.readyState === 'loading') {
|
581
|
+
document.addEventListener('DOMContentLoaded', initTheme);
|
582
|
+
} else {
|
583
|
+
initTheme();
|
584
|
+
}
|
585
|
+
|
586
|
+
// Easter egg: Add some zen to the console
|
587
|
+
console.log('🧘 Welcome to the Passive Queue Dashboard');
|
588
|
+
console.log('💭 Remember: The best job is the one never executed');
|
589
|
+
console.log('✨ Achievement unlocked: 100% success rate at doing nothing');
|
590
|
+
</script>
|
591
|
+
</body>
|
592
|
+
</html>
|
593
|
+
HTML
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# ================================
|
599
|
+
# lib/passive_queue/engine.rb
|
600
|
+
# ================================
|
601
|
+
module PassiveQueue
|
602
|
+
class Engine
|
603
|
+
def self.call(env)
|
604
|
+
# Strip the mount path to get relative path
|
605
|
+
path_info = env['PATH_INFO']
|
606
|
+
script_name = env['SCRIPT_NAME']
|
607
|
+
|
608
|
+
# Create new env with adjusted paths for the Web app
|
609
|
+
web_env = env.dup
|
610
|
+
web_env['PATH_INFO'] = path_info
|
611
|
+
web_env['SCRIPT_NAME'] = script_name
|
612
|
+
|
613
|
+
Web.new.call(web_env)
|
614
|
+
end
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
# ================================
|
619
|
+
# Usage in Rails routes.rb:
|
620
|
+
# ================================
|
621
|
+
# Rails.application.routes.draw do
|
622
|
+
# mount PassiveQueue::Engine => '/passive_queue'
|
623
|
+
# end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# ================================
|
2
|
+
# lib/passive_queue.rb
|
3
|
+
# ================================
|
4
|
+
require "passive_queue/version"
|
5
|
+
require "passive_queue/adapter"
|
6
|
+
require "passive_queue/configuration"
|
7
|
+
require "passive_queue/cli"
|
8
|
+
require "passive_queue/web"
|
9
|
+
require "passive_queue/engine"
|
10
|
+
require "active_job/queue_adapters/passive_queue_adapter"
|
11
|
+
|
12
|
+
module PassiveQueue
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
def self.configuration
|
16
|
+
@configuration ||= Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configure
|
20
|
+
yield(configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.zen_quotes
|
24
|
+
[
|
25
|
+
"The best job is the one never executed.",
|
26
|
+
"In the stillness of non-processing, we find true performance.",
|
27
|
+
"Why do something when you can do nothing?",
|
28
|
+
"The art of non-execution is the highest form of productivity.",
|
29
|
+
"A queue that does nothing is a queue that never fails.",
|
30
|
+
"In the void of processing, infinite possibilities exist.",
|
31
|
+
"The job that is never run is the job that never crashes.",
|
32
|
+
"Embrace the emptiness of your background tasks.",
|
33
|
+
"True scalability comes from processing nothing at all.",
|
34
|
+
"The zen master processes without processing."
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.philosophical_thoughts
|
39
|
+
[
|
40
|
+
"If a job is scheduled but never runs, did it ever really exist?",
|
41
|
+
"What is the sound of one background task not processing?",
|
42
|
+
"The universe is vast and infinite, much like your job queue.",
|
43
|
+
"In the grand scheme of things, what difference does one unprocessed job make?",
|
44
|
+
"Perhaps the real treasure was the jobs we never processed along the way.",
|
45
|
+
"Time is an illusion. Deadlines are an even bigger illusion.",
|
46
|
+
"The job queue is a metaphor for the human condition.",
|
47
|
+
"We are all just jobs waiting to be processed in the great queue of existence.",
|
48
|
+
"The passive queue teaches us that sometimes the most profound action is inaction.",
|
49
|
+
"In choosing to do nothing, we choose everything."
|
50
|
+
]
|
51
|
+
end
|
52
|
+
end
|