solid_queue_lite 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 (31) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +10 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +142 -0
  5. data/Rakefile +3 -0
  6. data/app/assets/stylesheets/soliq_queue_lite/application.css +15 -0
  7. data/app/controllers/concerns/solid_queue_lite/approximate_countable.rb +10 -0
  8. data/app/controllers/solid_queue_lite/application_controller.rb +4 -0
  9. data/app/controllers/solid_queue_lite/dashboards_controller.rb +61 -0
  10. data/app/controllers/solid_queue_lite/jobs_controller.rb +129 -0
  11. data/app/controllers/solid_queue_lite/processes_controller.rb +39 -0
  12. data/app/controllers/solid_queue_lite/queues_controller.rb +31 -0
  13. data/app/helpers/solid_queue_lite/application_helper.rb +27 -0
  14. data/app/jobs/solid_queue_lite/application_job.rb +4 -0
  15. data/app/jobs/solid_queue_lite/telemetry_sampler_job.rb +11 -0
  16. data/app/models/solid_queue_lite/application_record.rb +5 -0
  17. data/app/models/solid_queue_lite/stat.rb +7 -0
  18. data/app/views/layouts/solid_queue_lite/application.html.erb +383 -0
  19. data/app/views/solid_queue_lite/dashboards/show.html.erb +573 -0
  20. data/config/routes.rb +30 -0
  21. data/db/migrate/20260406000000_create_solid_queue_lite_stats.rb +16 -0
  22. data/lib/solid_queue_lite/approximate_counter.rb +87 -0
  23. data/lib/solid_queue_lite/engine.rb +20 -0
  24. data/lib/solid_queue_lite/install.rb +107 -0
  25. data/lib/solid_queue_lite/jobs.rb +236 -0
  26. data/lib/solid_queue_lite/processes.rb +156 -0
  27. data/lib/solid_queue_lite/telemetry.rb +201 -0
  28. data/lib/solid_queue_lite/version.rb +3 -0
  29. data/lib/solid_queue_lite.rb +46 -0
  30. data/lib/tasks/solid_queue_lite_tasks.rake +14 -0
  31. metadata +116 -0
@@ -0,0 +1,383 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Solid Queue Lite</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <link rel="preconnect" href="https://cdn.jsdelivr.net">
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
11
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
12
+
13
+ <style>
14
+ :root {
15
+ --sq-primary: #4f46e5;
16
+ --sq-success: #10b981;
17
+ --sq-danger: #ef4444;
18
+ --sq-warning: #f59e0b;
19
+ --sq-ink: #111827;
20
+ --sq-muted: #6b7280;
21
+ --sq-panel: #ffffff;
22
+
23
+ --pico-font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
24
+ --pico-primary: var(--sq-primary);
25
+ --pico-primary-background: var(--sq-primary);
26
+ --pico-primary-border: var(--sq-primary);
27
+ --pico-primary-hover: #4338ca;
28
+ --pico-primary-hover-background: #4338ca;
29
+ --pico-primary-hover-border: #4338ca;
30
+ --pico-primary-focus: rgba(79, 70, 229, 0.25);
31
+ --pico-border-radius: 0;
32
+ }
33
+
34
+ [x-cloak] {
35
+ display: none !important;
36
+ }
37
+
38
+ body {
39
+ min-height: 100vh;
40
+ color: var(--sq-ink);
41
+ }
42
+
43
+ header.container {
44
+ padding-top: 2rem;
45
+ padding-bottom: 1rem;
46
+ }
47
+
48
+ main.container {
49
+ padding-bottom: 3rem;
50
+ }
51
+
52
+ article {
53
+ box-shadow: none !important;
54
+ border: 1px solid var(--pico-muted-border-color);
55
+ border-radius: 0 !important;
56
+ background: var(--sq-panel);
57
+ }
58
+
59
+ .panel {
60
+ border: 1px solid var(--pico-muted-border-color);
61
+ background: var(--sq-panel);
62
+ }
63
+
64
+ .status-pill {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 0.35rem;
68
+ padding: 0.3rem 0.6rem;
69
+ font-size: 0.8rem;
70
+ font-weight: 700;
71
+ background: rgba(79, 70, 229, 0.08);
72
+ color: var(--sq-primary);
73
+ border: 1px solid rgba(79, 70, 229, 0.2);
74
+ }
75
+
76
+ .status-dot {
77
+ display: inline-block;
78
+ width: 10px;
79
+ height: 10px;
80
+ border-radius: 50%;
81
+ margin-right: 0.4rem;
82
+ }
83
+
84
+ .status-active {
85
+ background-color: var(--sq-success);
86
+ }
87
+
88
+ .status-stale,
89
+ .status-warning {
90
+ background-color: var(--sq-warning);
91
+ }
92
+
93
+ .status-dead {
94
+ background-color: var(--sq-danger);
95
+ }
96
+
97
+ .filter-nav {
98
+ margin-top: 1rem;
99
+ margin-bottom: 2rem;
100
+ border-bottom: 1px solid var(--pico-muted-border-color);
101
+ }
102
+
103
+ .filter-nav a {
104
+ cursor: pointer;
105
+ padding: 0.5rem 1rem;
106
+ display: inline-block;
107
+ border-bottom: 2px solid transparent;
108
+ color: var(--sq-muted);
109
+ text-decoration: none;
110
+ }
111
+
112
+ .filter-nav a.active {
113
+ border-bottom-color: var(--sq-primary);
114
+ color: var(--sq-ink);
115
+ font-weight: 700;
116
+ }
117
+
118
+ .toolbar {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ align-items: center;
122
+ margin-bottom: 1rem;
123
+ }
124
+
125
+ .metric-grid {
126
+ display: grid;
127
+ gap: 1rem;
128
+ grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
129
+ }
130
+
131
+ .metric-label,
132
+ .process-meta,
133
+ .queue-meta,
134
+ .muted {
135
+ color: var(--sq-muted);
136
+ font-size: 0.92rem;
137
+ }
138
+
139
+ .cluster {
140
+ display: flex;
141
+ flex-wrap: wrap;
142
+ align-items: center;
143
+ gap: 0.5rem;
144
+ }
145
+
146
+ .stack {
147
+ display: grid;
148
+ gap: 1rem;
149
+ }
150
+
151
+ .ct-series-a .ct-line,
152
+ .ct-series-a .ct-point {
153
+ stroke: var(--sq-primary);
154
+ }
155
+
156
+ .ct-series-a .ct-area {
157
+ fill: rgba(79, 70, 229, 0.12);
158
+ }
159
+
160
+ .ct-series-b .ct-line,
161
+ .ct-series-b .ct-point {
162
+ stroke: var(--sq-danger);
163
+ }
164
+
165
+ .ct-chart {
166
+ height: 220px;
167
+ margin-top: 1rem;
168
+ }
169
+
170
+ .job-row {
171
+ cursor: pointer;
172
+ transition: background-color 0.1s ease;
173
+ }
174
+
175
+ .job-row:hover {
176
+ background-color: rgba(128, 128, 128, 0.05);
177
+ }
178
+
179
+ .chevron {
180
+ transition: transform 0.2s ease;
181
+ display: inline-block;
182
+ width: 16px;
183
+ height: 16px;
184
+ vertical-align: text-bottom;
185
+ margin-right: 4px;
186
+ }
187
+
188
+ .chevron.expanded {
189
+ transform: rotate(90deg);
190
+ }
191
+
192
+ .job-detail-row td {
193
+ padding: 0;
194
+ border-bottom: none;
195
+ }
196
+
197
+ .job-detail-content {
198
+ padding: 1.5rem;
199
+ background: rgba(128, 128, 128, 0.02);
200
+ border-left: 3px solid var(--sq-primary);
201
+ margin: 0 0 1rem;
202
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
203
+ }
204
+
205
+ .job-detail-content pre,
206
+ .process-card pre,
207
+ .code-block {
208
+ margin: 0;
209
+ padding: 1rem;
210
+ background: var(--pico-code-background-color);
211
+ border: 1px solid var(--pico-muted-border-color);
212
+ font-size: 0.85rem;
213
+ overflow-x: auto;
214
+ }
215
+
216
+ .actions-col {
217
+ display: flex;
218
+ gap: 0.5rem;
219
+ }
220
+
221
+ .actions-col button {
222
+ padding: 0.25rem 0.5rem;
223
+ font-size: 0.8rem;
224
+ margin: 0;
225
+ width: auto;
226
+ }
227
+
228
+ .process-grid {
229
+ display: grid;
230
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
231
+ gap: 1.5rem;
232
+ }
233
+
234
+ .process-card {
235
+ margin-bottom: 0;
236
+ display: flex;
237
+ flex-direction: column;
238
+ }
239
+
240
+ .process-card header {
241
+ padding: 0.75rem 1rem;
242
+ border-bottom: 1px solid var(--pico-muted-border-color);
243
+ background: rgba(128, 128, 128, 0.02);
244
+ display: flex;
245
+ justify-content: space-between;
246
+ align-items: center;
247
+ margin-bottom: 0;
248
+ }
249
+
250
+ .process-card ul {
251
+ margin: 0;
252
+ padding: 1rem;
253
+ list-style: none;
254
+ font-size: 0.9rem;
255
+ flex-grow: 1;
256
+ }
257
+
258
+ .process-card ul li {
259
+ margin-bottom: 0.5rem;
260
+ display: flex;
261
+ justify-content: space-between;
262
+ }
263
+
264
+ .process-card footer {
265
+ padding: 0.75rem 1rem;
266
+ border-top: 1px solid var(--pico-muted-border-color);
267
+ background: rgba(128, 128, 128, 0.02);
268
+ margin-top: 0;
269
+ }
270
+
271
+ .queue-badge {
272
+ font-size: 0.75rem;
273
+ padding: 0.15rem 0.5rem;
274
+ border-radius: 4px;
275
+ background: var(--pico-secondary-background);
276
+ color: var(--pico-secondary-inverse);
277
+ text-decoration: none;
278
+ display: inline-block;
279
+ margin: 0.1rem 0.2rem 0.1rem 0;
280
+ border: 1px solid var(--pico-secondary-border);
281
+ transition: filter 0.1s ease;
282
+ }
283
+
284
+ .queue-badge:hover {
285
+ filter: brightness(1.1);
286
+ color: var(--pico-secondary-inverse);
287
+ }
288
+
289
+ .empty-state {
290
+ padding: 2rem 1rem;
291
+ text-align: center;
292
+ color: var(--sq-muted);
293
+ background: var(--sq-panel);
294
+ border: 1px solid var(--pico-muted-border-color);
295
+ }
296
+
297
+ @media (max-width: 768px) {
298
+ .toolbar {
299
+ flex-direction: column;
300
+ align-items: stretch;
301
+ }
302
+
303
+ .filter-nav a {
304
+ padding-left: 0.5rem;
305
+ padding-right: 0.5rem;
306
+ }
307
+ }
308
+ </style>
309
+
310
+ <script>
311
+ document.addEventListener("alpine:init", () => {
312
+ Alpine.store("dashboard", {
313
+ autoRefresh: 0,
314
+ interval: null,
315
+ paused: false,
316
+ init() {
317
+ const storedValue = Number(window.sessionStorage.getItem("solid-queue-dashboard-lite:auto-refresh") || 0);
318
+ this.setAutoRefresh(storedValue);
319
+ },
320
+ setAutoRefresh(seconds) {
321
+ this.autoRefresh = Number(seconds || 0);
322
+ window.sessionStorage.setItem("solid-queue-dashboard-lite:auto-refresh", this.autoRefresh);
323
+
324
+ if (this.interval) {
325
+ window.clearInterval(this.interval);
326
+ this.interval = null;
327
+ }
328
+
329
+ if (this.autoRefresh > 0) {
330
+ this.interval = window.setInterval(() => {
331
+ if (!this.paused && !document.hidden) {
332
+ window.location.reload();
333
+ }
334
+ }, this.autoRefresh * 1000);
335
+ }
336
+ },
337
+ setPaused(value) {
338
+ this.paused = Boolean(value);
339
+ }
340
+ });
341
+ });
342
+ </script>
343
+
344
+ <%= yield :head %>
345
+ </head>
346
+ <body x-data="{ autoRefresh: 0 }"
347
+ x-init="autoRefresh = $store.dashboard.autoRefresh; $store.dashboard.init(); autoRefresh = $store.dashboard.autoRefresh"
348
+ x-on:dashboard-pause.window="$store.dashboard.setPaused($event.detail.paused)">
349
+ <header class="container">
350
+ <nav>
351
+ <ul>
352
+ <li>
353
+ <hgroup>
354
+ <h1>Solid Queue</h1>
355
+ <p>Production Telemetry &amp; Operations</p>
356
+ </hgroup>
357
+ </li>
358
+ </ul>
359
+ <ul>
360
+ <li>
361
+ <select x-model="autoRefresh" @change="$store.dashboard.setAutoRefresh(Number(autoRefresh))" aria-label="Auto-refresh" style="width: auto; padding-right: 2rem;">
362
+ <option value="0">Auto-Refresh: Off</option>
363
+ <option value="5">Refresh: 5s</option>
364
+ <option value="10">Refresh: 10s</option>
365
+ <option value="30">Refresh: 30s</option>
366
+ </select>
367
+ </li>
368
+ </ul>
369
+ </nav>
370
+ </header>
371
+
372
+ <main class="container">
373
+ <% if notice.present? %>
374
+ <p><mark><%= notice %></mark></p>
375
+ <% end %>
376
+ <% if alert.present? %>
377
+ <p><mark><%= alert %></mark></p>
378
+ <% end %>
379
+
380
+ <%= yield %>
381
+ </main>
382
+ </body>
383
+ </html>