ragdoll-rails 0.1.9 → 0.1.11
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/app/assets/javascripts/ragdoll/application.js +129 -0
- data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
- data/app/assets/stylesheets/ragdoll/application.css +84 -0
- data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
- data/app/channels/application_cable/channel.rb +6 -0
- data/app/channels/application_cable/connection.rb +6 -0
- data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
- data/app/channels/ragdoll/file_processing_channel.rb +26 -0
- data/app/components/ragdoll/alert_component.html.erb +4 -0
- data/app/components/ragdoll/alert_component.rb +32 -0
- data/app/components/ragdoll/application_component.rb +6 -0
- data/app/components/ragdoll/card_component.html.erb +15 -0
- data/app/components/ragdoll/card_component.rb +21 -0
- data/app/components/ragdoll/document_list_component.html.erb +41 -0
- data/app/components/ragdoll/document_list_component.rb +13 -0
- data/app/components/ragdoll/document_table_component.html.erb +76 -0
- data/app/components/ragdoll/document_table_component.rb +13 -0
- data/app/components/ragdoll/empty_state_component.html.erb +12 -0
- data/app/components/ragdoll/empty_state_component.rb +17 -0
- data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
- data/app/components/ragdoll/flash_messages_component.rb +37 -0
- data/app/components/ragdoll/navbar_component.html.erb +24 -0
- data/app/components/ragdoll/navbar_component.rb +31 -0
- data/app/components/ragdoll/page_header_component.html.erb +13 -0
- data/app/components/ragdoll/page_header_component.rb +15 -0
- data/app/components/ragdoll/stats_card_component.html.erb +11 -0
- data/app/components/ragdoll/stats_card_component.rb +17 -0
- data/app/components/ragdoll/status_badge_component.html.erb +3 -0
- data/app/components/ragdoll/status_badge_component.rb +30 -0
- data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
- data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
- data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
- data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
- data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
- data/app/controllers/ragdoll/application_controller.rb +17 -0
- data/app/controllers/ragdoll/configuration_controller.rb +82 -0
- data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
- data/app/controllers/ragdoll/documents_controller.rb +460 -0
- data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
- data/app/controllers/ragdoll/jobs_controller.rb +116 -0
- data/app/controllers/ragdoll/search_controller.rb +368 -0
- data/app/jobs/application_job.rb +9 -0
- data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
- data/app/jobs/ragdoll/process_file_job.rb +166 -0
- data/app/services/ragdoll/worker_health_service.rb +111 -0
- data/app/views/layouts/ragdoll/application.html.erb +162 -0
- data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
- data/app/views/ragdoll/dashboard/index.html.erb +208 -0
- data/app/views/ragdoll/documents/edit.html.erb +91 -0
- data/app/views/ragdoll/documents/index.html.erb +302 -0
- data/app/views/ragdoll/documents/new.html.erb +1518 -0
- data/app/views/ragdoll/documents/show.html.erb +188 -0
- data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
- data/app/views/ragdoll/jobs/index.html.erb +669 -0
- data/app/views/ragdoll/jobs/show.html.erb +129 -0
- data/app/views/ragdoll/search/index.html.erb +324 -0
- data/config/cable.yml +12 -0
- data/config/routes.rb +56 -1
- data/lib/ragdoll/rails/engine.rb +32 -1
- data/lib/ragdoll/rails/version.rb +1 -1
- metadata +86 -1
@@ -0,0 +1,669 @@
|
|
1
|
+
<% content_for :title, "Job Dashboard - Ragdoll Engine" %>
|
2
|
+
|
3
|
+
<%= render Ragdoll::PageHeaderComponent.new(
|
4
|
+
title: "Job Dashboard",
|
5
|
+
icon: "fas fa-tasks",
|
6
|
+
subtitle: "Monitor background job processing and queue status"
|
7
|
+
) %>
|
8
|
+
|
9
|
+
<div class="row mb-4">
|
10
|
+
<div class="col-md-3">
|
11
|
+
<%= render Ragdoll::StatsCardComponent.new(
|
12
|
+
title: "Pending",
|
13
|
+
value: @stats[:pending],
|
14
|
+
icon: "fas fa-clock",
|
15
|
+
color: "warning",
|
16
|
+
description: "Jobs waiting to process"
|
17
|
+
) %>
|
18
|
+
</div>
|
19
|
+
<div class="col-md-3">
|
20
|
+
<%= render Ragdoll::StatsCardComponent.new(
|
21
|
+
title: "Completed",
|
22
|
+
value: @stats[:completed],
|
23
|
+
icon: "fas fa-check-circle",
|
24
|
+
color: "success",
|
25
|
+
description: "Successfully completed"
|
26
|
+
) %>
|
27
|
+
</div>
|
28
|
+
<div class="col-md-3">
|
29
|
+
<%= render Ragdoll::StatsCardComponent.new(
|
30
|
+
title: "Failed",
|
31
|
+
value: @stats[:failed],
|
32
|
+
icon: "fas fa-exclamation-circle",
|
33
|
+
color: "danger",
|
34
|
+
description: "Jobs that failed"
|
35
|
+
) %>
|
36
|
+
</div>
|
37
|
+
<div class="col-md-3">
|
38
|
+
<%= render Ragdoll::StatsCardComponent.new(
|
39
|
+
title: "Total",
|
40
|
+
value: @stats[:total],
|
41
|
+
icon: "fas fa-tasks",
|
42
|
+
color: "info",
|
43
|
+
description: "All jobs processed"
|
44
|
+
) %>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div class="row">
|
49
|
+
<div class="col-12">
|
50
|
+
<div class="card">
|
51
|
+
<div class="card-header">
|
52
|
+
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
53
|
+
<li class="nav-item">
|
54
|
+
<a class="nav-link active" id="pending-tab" data-bs-toggle="tab" href="#pending" role="tab">
|
55
|
+
<i class="fas fa-clock"></i> Pending Jobs (<%= @stats[:pending] %>)
|
56
|
+
</a>
|
57
|
+
</li>
|
58
|
+
<li class="nav-item">
|
59
|
+
<a class="nav-link" id="completed-tab" data-bs-toggle="tab" href="#completed" role="tab">
|
60
|
+
<i class="fas fa-check-circle"></i> Completed Jobs (<%= @stats[:completed] %>)
|
61
|
+
</a>
|
62
|
+
</li>
|
63
|
+
<li class="nav-item">
|
64
|
+
<a class="nav-link" id="failed-tab" data-bs-toggle="tab" href="#failed" role="tab">
|
65
|
+
<i class="fas fa-exclamation-circle"></i> Failed Jobs (<%= @stats[:failed] %>)
|
66
|
+
</a>
|
67
|
+
</li>
|
68
|
+
</ul>
|
69
|
+
</div>
|
70
|
+
<div class="card-body">
|
71
|
+
<div class="tab-content">
|
72
|
+
<!-- Pending Jobs Tab -->
|
73
|
+
<div class="tab-pane fade show active" id="pending" role="tabpanel">
|
74
|
+
<% if @pending_jobs.any? %>
|
75
|
+
<!-- Bulk Actions for Pending Jobs -->
|
76
|
+
<div class="mb-3 p-3 bg-light rounded">
|
77
|
+
<div class="d-flex align-items-center justify-content-between">
|
78
|
+
<div>
|
79
|
+
<input type="checkbox" id="select-all-pending" class="form-check-input me-2">
|
80
|
+
<label for="select-all-pending" class="form-check-label">Select All</label>
|
81
|
+
<span id="pending-selected-count" class="ms-2 text-muted">(0 selected)</span>
|
82
|
+
</div>
|
83
|
+
<div>
|
84
|
+
<button type="button" id="bulk-delete-pending" class="btn btn-danger btn-sm" disabled>
|
85
|
+
<i class="fas fa-trash"></i> Delete Selected
|
86
|
+
</button>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
|
91
|
+
<div class="table-responsive">
|
92
|
+
<table class="table table-striped">
|
93
|
+
<thead>
|
94
|
+
<tr>
|
95
|
+
<th width="40px">
|
96
|
+
<input type="checkbox" id="select-all-pending-header" class="form-check-input">
|
97
|
+
</th>
|
98
|
+
<th>Job Class</th>
|
99
|
+
<th>Arguments</th>
|
100
|
+
<th>Queue</th>
|
101
|
+
<th>Created</th>
|
102
|
+
<th>Actions</th>
|
103
|
+
</tr>
|
104
|
+
</thead>
|
105
|
+
<tbody>
|
106
|
+
<% @pending_jobs.each do |job| %>
|
107
|
+
<tr>
|
108
|
+
<td>
|
109
|
+
<input type="checkbox" class="form-check-input job-checkbox-pending" value="<%= job.id %>">
|
110
|
+
</td>
|
111
|
+
<td>
|
112
|
+
<strong><%= job.class_name %></strong>
|
113
|
+
</td>
|
114
|
+
<td>
|
115
|
+
<small class="text-muted">
|
116
|
+
<%= truncate(job.arguments.to_s, length: 100) %>
|
117
|
+
</small>
|
118
|
+
</td>
|
119
|
+
<td>
|
120
|
+
<span class="badge bg-primary"><%= job.queue_name %></span>
|
121
|
+
</td>
|
122
|
+
<td>
|
123
|
+
<small><%= time_ago_in_words(job.created_at) %> ago</small>
|
124
|
+
</td>
|
125
|
+
<td>
|
126
|
+
<%= link_to "Details", ragdoll.job_path(job), class: "btn btn-sm btn-outline-info" %>
|
127
|
+
<%= form_with url: ragdoll.job_path(job), method: :delete, local: true,
|
128
|
+
data: { confirm: "Are you sure?" },
|
129
|
+
style: "display: inline;" do |form| %>
|
130
|
+
<%= form.submit "Delete", class: "btn btn-sm btn-outline-danger" %>
|
131
|
+
<% end %>
|
132
|
+
</td>
|
133
|
+
</tr>
|
134
|
+
<% end %>
|
135
|
+
</tbody>
|
136
|
+
</table>
|
137
|
+
</div>
|
138
|
+
<% else %>
|
139
|
+
<div class="text-center py-4">
|
140
|
+
<i class="fas fa-check-circle text-success" style="font-size: 3rem;"></i>
|
141
|
+
<h5 class="mt-3">No Pending Jobs</h5>
|
142
|
+
<p class="text-muted">All jobs have been processed!</p>
|
143
|
+
</div>
|
144
|
+
<% end %>
|
145
|
+
</div>
|
146
|
+
|
147
|
+
<!-- Completed Jobs Tab -->
|
148
|
+
<div class="tab-pane fade" id="completed" role="tabpanel">
|
149
|
+
<% if @completed_jobs.any? %>
|
150
|
+
<div class="table-responsive">
|
151
|
+
<table class="table table-striped">
|
152
|
+
<thead>
|
153
|
+
<tr>
|
154
|
+
<th>Job Class</th>
|
155
|
+
<th>Arguments</th>
|
156
|
+
<th>Queue</th>
|
157
|
+
<th>Completed</th>
|
158
|
+
<th>Duration</th>
|
159
|
+
<th>Actions</th>
|
160
|
+
</tr>
|
161
|
+
</thead>
|
162
|
+
<tbody>
|
163
|
+
<% @completed_jobs.each do |job| %>
|
164
|
+
<tr>
|
165
|
+
<td>
|
166
|
+
<strong><%= job.class_name %></strong>
|
167
|
+
</td>
|
168
|
+
<td>
|
169
|
+
<small class="text-muted">
|
170
|
+
<%= truncate(job.arguments.to_s, length: 100) %>
|
171
|
+
</small>
|
172
|
+
</td>
|
173
|
+
<td>
|
174
|
+
<span class="badge bg-success"><%= job.queue_name %></span>
|
175
|
+
</td>
|
176
|
+
<td>
|
177
|
+
<small><%= time_ago_in_words(job.finished_at) %> ago</small>
|
178
|
+
</td>
|
179
|
+
<td>
|
180
|
+
<% if job.finished_at && job.created_at %>
|
181
|
+
<small><%= number_with_precision((job.finished_at - job.created_at), precision: 2) %>s</small>
|
182
|
+
<% else %>
|
183
|
+
<small>-</small>
|
184
|
+
<% end %>
|
185
|
+
</td>
|
186
|
+
<td>
|
187
|
+
<%= link_to "Details", ragdoll.job_path(job), class: "btn btn-sm btn-outline-info" %>
|
188
|
+
</td>
|
189
|
+
</tr>
|
190
|
+
<% end %>
|
191
|
+
</tbody>
|
192
|
+
</table>
|
193
|
+
</div>
|
194
|
+
<% else %>
|
195
|
+
<div class="text-center py-4">
|
196
|
+
<i class="fas fa-history text-muted" style="font-size: 3rem;"></i>
|
197
|
+
<h5 class="mt-3">No Completed Jobs</h5>
|
198
|
+
<p class="text-muted">No jobs have been completed yet.</p>
|
199
|
+
</div>
|
200
|
+
<% end %>
|
201
|
+
</div>
|
202
|
+
|
203
|
+
<!-- Failed Jobs Tab -->
|
204
|
+
<div class="tab-pane fade" id="failed" role="tabpanel">
|
205
|
+
<% if @failed_jobs.any? %>
|
206
|
+
<!-- Bulk Actions for Failed Jobs -->
|
207
|
+
<div class="mb-3 p-3 bg-light rounded">
|
208
|
+
<div class="d-flex align-items-center justify-content-between">
|
209
|
+
<div>
|
210
|
+
<input type="checkbox" id="select-all-failed" class="form-check-input me-2">
|
211
|
+
<label for="select-all-failed" class="form-check-label">Select All</label>
|
212
|
+
<span id="failed-selected-count" class="ms-2 text-muted">(0 selected)</span>
|
213
|
+
</div>
|
214
|
+
<div>
|
215
|
+
<button type="button" id="bulk-retry-failed" class="btn btn-warning btn-sm me-2" disabled>
|
216
|
+
<i class="fas fa-redo"></i> Retry Selected
|
217
|
+
</button>
|
218
|
+
<button type="button" id="bulk-delete-failed" class="btn btn-danger btn-sm" disabled>
|
219
|
+
<i class="fas fa-trash"></i> Delete Selected
|
220
|
+
</button>
|
221
|
+
</div>
|
222
|
+
</div>
|
223
|
+
</div>
|
224
|
+
|
225
|
+
<div class="table-responsive">
|
226
|
+
<table class="table table-striped">
|
227
|
+
<thead>
|
228
|
+
<tr>
|
229
|
+
<th width="40px">
|
230
|
+
<input type="checkbox" id="select-all-failed-header" class="form-check-input">
|
231
|
+
</th>
|
232
|
+
<th>Job Class</th>
|
233
|
+
<th>Error</th>
|
234
|
+
<th>Failed At</th>
|
235
|
+
<th>Actions</th>
|
236
|
+
</tr>
|
237
|
+
</thead>
|
238
|
+
<tbody>
|
239
|
+
<% @failed_jobs.each do |failed_job| %>
|
240
|
+
<tr>
|
241
|
+
<td>
|
242
|
+
<input type="checkbox" class="form-check-input job-checkbox-failed" value="<%= failed_job.id %>">
|
243
|
+
</td>
|
244
|
+
<td>
|
245
|
+
<strong><%= failed_job.job.class_name %></strong>
|
246
|
+
</td>
|
247
|
+
<td>
|
248
|
+
<small class="text-danger">
|
249
|
+
<%= truncate(failed_job.error.to_s, length: 150) %>
|
250
|
+
</small>
|
251
|
+
</td>
|
252
|
+
<td>
|
253
|
+
<small><%= time_ago_in_words(failed_job.created_at) %> ago</small>
|
254
|
+
</td>
|
255
|
+
<td>
|
256
|
+
<%= link_to "Retry", ragdoll.retry_job_path(failed_job), method: :post,
|
257
|
+
class: "btn btn-sm btn-warning" %>
|
258
|
+
<%= form_with url: ragdoll.job_path(failed_job, type: 'failed'), method: :delete, local: true,
|
259
|
+
data: { confirm: "Are you sure?" },
|
260
|
+
style: "display: inline;" do |form| %>
|
261
|
+
<%= form.submit "Delete", class: "btn btn-sm btn-outline-danger" %>
|
262
|
+
<% end %>
|
263
|
+
</td>
|
264
|
+
</tr>
|
265
|
+
<% end %>
|
266
|
+
</tbody>
|
267
|
+
</table>
|
268
|
+
</div>
|
269
|
+
<% else %>
|
270
|
+
<div class="text-center py-4">
|
271
|
+
<i class="fas fa-shield-alt text-success" style="font-size: 3rem;"></i>
|
272
|
+
<h5 class="mt-3">No Failed Jobs</h5>
|
273
|
+
<p class="text-muted">All jobs are running smoothly!</p>
|
274
|
+
</div>
|
275
|
+
<% end %>
|
276
|
+
</div>
|
277
|
+
</div>
|
278
|
+
</div>
|
279
|
+
</div>
|
280
|
+
</div>
|
281
|
+
</div>
|
282
|
+
|
283
|
+
<div class="row mt-4">
|
284
|
+
<div class="col-md-6">
|
285
|
+
<div class="card">
|
286
|
+
<div class="card-header">
|
287
|
+
<h5><i class="fas fa-info-circle"></i> Queue Information</h5>
|
288
|
+
</div>
|
289
|
+
<div class="card-body">
|
290
|
+
<p><strong>Queue Adapter:</strong> SolidQueue</p>
|
291
|
+
<p><strong>Default Queue:</strong> default</p>
|
292
|
+
<p><strong>Auto-refresh:</strong> This page auto-refreshes every 30 seconds</p>
|
293
|
+
<div class="mt-3">
|
294
|
+
<%= link_to "Refresh Now", ragdoll.jobs_path, class: "btn btn-primary" %>
|
295
|
+
<%= link_to "Back to Dashboard", ragdoll.dashboard_index_path, class: "btn btn-outline-secondary" %>
|
296
|
+
<% if @stats[:pending] > 0 %>
|
297
|
+
<%= form_with url: ragdoll.cancel_all_pending_jobs_path, method: :delete, local: true,
|
298
|
+
data: { confirm: "⚠️ WARNING: This will cancel ALL #{@stats[:pending]} pending jobs! This cannot be undone. Are you absolutely sure?" },
|
299
|
+
style: "display: inline;" do |form| %>
|
300
|
+
<%= form.submit "⚡ Cancel All Pending", class: "btn btn-danger ms-2" %>
|
301
|
+
<% end %>
|
302
|
+
<% end %>
|
303
|
+
</div>
|
304
|
+
</div>
|
305
|
+
</div>
|
306
|
+
</div>
|
307
|
+
|
308
|
+
<div class="col-md-6">
|
309
|
+
<div class="card">
|
310
|
+
<div class="card-header">
|
311
|
+
<h5><i class="fas fa-heartbeat"></i> Worker Health</h5>
|
312
|
+
</div>
|
313
|
+
<div class="card-body">
|
314
|
+
<div id="worker-health-status">
|
315
|
+
<div class="d-flex justify-content-center">
|
316
|
+
<div class="spinner-border text-primary" role="status">
|
317
|
+
<span class="visually-hidden">Checking...</span>
|
318
|
+
</div>
|
319
|
+
</div>
|
320
|
+
</div>
|
321
|
+
<div class="mt-3">
|
322
|
+
<%= link_to "Check Health", ragdoll.health_jobs_path,
|
323
|
+
class: "btn btn-info",
|
324
|
+
id: "check-health-btn" %>
|
325
|
+
<%= form_with url: ragdoll.restart_workers_jobs_path, method: :post, local: true,
|
326
|
+
data: { confirm: "Are you sure you want to restart the workers?" },
|
327
|
+
style: "display: inline;" do |form| %>
|
328
|
+
<%= form.submit "Restart Workers", class: "btn btn-warning", id: "restart-workers-btn" %>
|
329
|
+
<% end %>
|
330
|
+
</div>
|
331
|
+
</div>
|
332
|
+
</div>
|
333
|
+
</div>
|
334
|
+
</div>
|
335
|
+
|
336
|
+
<%= content_for :javascript do %>
|
337
|
+
<script>
|
338
|
+
// Worker health checking
|
339
|
+
function checkWorkerHealth() {
|
340
|
+
fetch('<%= ragdoll.health_jobs_path %>')
|
341
|
+
.then(response => response.json())
|
342
|
+
.then(data => {
|
343
|
+
updateHealthDisplay(data);
|
344
|
+
})
|
345
|
+
.catch(error => {
|
346
|
+
console.error('Health check failed:', error);
|
347
|
+
document.getElementById('worker-health-status').innerHTML =
|
348
|
+
'<div class="alert alert-danger">Health check failed</div>';
|
349
|
+
});
|
350
|
+
}
|
351
|
+
|
352
|
+
function updateHealthDisplay(health) {
|
353
|
+
const statusElement = document.getElementById('worker-health-status');
|
354
|
+
const restartBtn = document.getElementById('restart-workers-btn');
|
355
|
+
|
356
|
+
let statusHtml = '';
|
357
|
+
let alertClass = '';
|
358
|
+
|
359
|
+
switch(health.status) {
|
360
|
+
case 'healthy':
|
361
|
+
alertClass = 'alert-success';
|
362
|
+
statusHtml = `
|
363
|
+
<div class="alert ${alertClass}">
|
364
|
+
<i class="fas fa-check-circle"></i> <strong>Healthy</strong><br>
|
365
|
+
<small>${health.worker_count} workers running, ${health.stalled_jobs} pending jobs</small>
|
366
|
+
</div>
|
367
|
+
`;
|
368
|
+
restartBtn.disabled = false;
|
369
|
+
break;
|
370
|
+
|
371
|
+
case 'stalled':
|
372
|
+
alertClass = 'alert-warning';
|
373
|
+
const oldestAge = Math.round(health.oldest_pending_job / 60);
|
374
|
+
statusHtml = `
|
375
|
+
<div class="alert ${alertClass}">
|
376
|
+
<i class="fas fa-exclamation-triangle"></i> <strong>Stalled</strong><br>
|
377
|
+
<small>${health.stalled_jobs} jobs pending for ${oldestAge}+ minutes</small>
|
378
|
+
</div>
|
379
|
+
`;
|
380
|
+
restartBtn.disabled = false;
|
381
|
+
restartBtn.classList.add('btn-danger');
|
382
|
+
restartBtn.classList.remove('btn-warning');
|
383
|
+
break;
|
384
|
+
|
385
|
+
case 'no_workers':
|
386
|
+
alertClass = 'alert-danger';
|
387
|
+
statusHtml = `
|
388
|
+
<div class="alert ${alertClass}">
|
389
|
+
<i class="fas fa-times-circle"></i> <strong>No Workers</strong><br>
|
390
|
+
<small>No worker processes detected</small>
|
391
|
+
</div>
|
392
|
+
`;
|
393
|
+
restartBtn.disabled = false;
|
394
|
+
restartBtn.classList.add('btn-danger');
|
395
|
+
restartBtn.classList.remove('btn-warning');
|
396
|
+
break;
|
397
|
+
|
398
|
+
case 'processing':
|
399
|
+
alertClass = 'alert-info';
|
400
|
+
statusHtml = `
|
401
|
+
<div class="alert ${alertClass}">
|
402
|
+
<i class="fas fa-cog fa-spin"></i> <strong>Processing</strong><br>
|
403
|
+
<small>${health.worker_count} workers, ${health.stalled_jobs} jobs in queue</small>
|
404
|
+
</div>
|
405
|
+
`;
|
406
|
+
restartBtn.disabled = false;
|
407
|
+
break;
|
408
|
+
}
|
409
|
+
|
410
|
+
statusElement.innerHTML = statusHtml;
|
411
|
+
}
|
412
|
+
|
413
|
+
// Check health on page load
|
414
|
+
document.addEventListener('DOMContentLoaded', function() {
|
415
|
+
checkWorkerHealth();
|
416
|
+
|
417
|
+
// Check health every 15 seconds
|
418
|
+
setInterval(checkWorkerHealth, 15000);
|
419
|
+
|
420
|
+
// Manual health check button
|
421
|
+
document.getElementById('check-health-btn').addEventListener('click', function(e) {
|
422
|
+
e.preventDefault();
|
423
|
+
checkWorkerHealth();
|
424
|
+
});
|
425
|
+
});
|
426
|
+
|
427
|
+
// Auto-refresh page every 30 seconds (disabled when jobs are selected to prevent losing selection)
|
428
|
+
let autoRefreshTimeout;
|
429
|
+
function scheduleAutoRefresh() {
|
430
|
+
clearTimeout(autoRefreshTimeout);
|
431
|
+
autoRefreshTimeout = setTimeout(function() {
|
432
|
+
// Don't auto-refresh if jobs are selected
|
433
|
+
const pendingSelected = document.querySelectorAll('.job-checkbox-pending:checked').length;
|
434
|
+
const failedSelected = document.querySelectorAll('.job-checkbox-failed:checked').length;
|
435
|
+
|
436
|
+
if (pendingSelected === 0 && failedSelected === 0) {
|
437
|
+
window.location.reload();
|
438
|
+
} else {
|
439
|
+
// Reschedule for later
|
440
|
+
scheduleAutoRefresh();
|
441
|
+
}
|
442
|
+
}, 30000);
|
443
|
+
}
|
444
|
+
scheduleAutoRefresh();
|
445
|
+
|
446
|
+
// Bulk operations for pending jobs
|
447
|
+
document.addEventListener('DOMContentLoaded', function() {
|
448
|
+
// Pending jobs bulk operations
|
449
|
+
const selectAllPending = document.getElementById('select-all-pending');
|
450
|
+
const selectAllPendingHeader = document.getElementById('select-all-pending-header');
|
451
|
+
const pendingCheckboxes = document.querySelectorAll('.job-checkbox-pending');
|
452
|
+
const bulkDeletePending = document.getElementById('bulk-delete-pending');
|
453
|
+
const pendingSelectedCount = document.getElementById('pending-selected-count');
|
454
|
+
|
455
|
+
// Sync "Select All" checkboxes for pending jobs
|
456
|
+
if (selectAllPending && selectAllPendingHeader) {
|
457
|
+
[selectAllPending, selectAllPendingHeader].forEach(checkbox => {
|
458
|
+
checkbox.addEventListener('change', function() {
|
459
|
+
const checked = this.checked;
|
460
|
+
pendingCheckboxes.forEach(cb => cb.checked = checked);
|
461
|
+
if (selectAllPending !== this) selectAllPending.checked = checked;
|
462
|
+
if (selectAllPendingHeader !== this) selectAllPendingHeader.checked = checked;
|
463
|
+
updatePendingBulkButtons();
|
464
|
+
});
|
465
|
+
});
|
466
|
+
}
|
467
|
+
|
468
|
+
// Individual checkbox handlers for pending jobs
|
469
|
+
pendingCheckboxes.forEach(checkbox => {
|
470
|
+
checkbox.addEventListener('change', updatePendingBulkButtons);
|
471
|
+
});
|
472
|
+
|
473
|
+
function updatePendingBulkButtons() {
|
474
|
+
const checkedBoxes = document.querySelectorAll('.job-checkbox-pending:checked');
|
475
|
+
const count = checkedBoxes.length;
|
476
|
+
|
477
|
+
if (pendingSelectedCount) {
|
478
|
+
pendingSelectedCount.textContent = `(${count} selected)`;
|
479
|
+
}
|
480
|
+
|
481
|
+
if (bulkDeletePending) {
|
482
|
+
bulkDeletePending.disabled = count === 0;
|
483
|
+
}
|
484
|
+
|
485
|
+
// Update "Select All" checkboxes
|
486
|
+
const allChecked = count === pendingCheckboxes.length && count > 0;
|
487
|
+
if (selectAllPending) selectAllPending.checked = allChecked;
|
488
|
+
if (selectAllPendingHeader) selectAllPendingHeader.checked = allChecked;
|
489
|
+
}
|
490
|
+
|
491
|
+
// Bulk delete pending jobs
|
492
|
+
if (bulkDeletePending) {
|
493
|
+
bulkDeletePending.addEventListener('click', function() {
|
494
|
+
const selectedIds = Array.from(document.querySelectorAll('.job-checkbox-pending:checked')).map(cb => cb.value);
|
495
|
+
|
496
|
+
if (selectedIds.length === 0) return;
|
497
|
+
|
498
|
+
if (confirm(`Are you sure you want to delete ${selectedIds.length} pending job(s)? This cannot be undone.`)) {
|
499
|
+
const form = document.createElement('form');
|
500
|
+
form.method = 'POST';
|
501
|
+
form.action = '<%= ragdoll.bulk_delete_jobs_path %>';
|
502
|
+
|
503
|
+
// Add CSRF token
|
504
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
505
|
+
if (csrfToken) {
|
506
|
+
const csrfInput = document.createElement('input');
|
507
|
+
csrfInput.type = 'hidden';
|
508
|
+
csrfInput.name = 'authenticity_token';
|
509
|
+
csrfInput.value = csrfToken.content;
|
510
|
+
form.appendChild(csrfInput);
|
511
|
+
}
|
512
|
+
|
513
|
+
// Add job type
|
514
|
+
const typeInput = document.createElement('input');
|
515
|
+
typeInput.type = 'hidden';
|
516
|
+
typeInput.name = 'job_type';
|
517
|
+
typeInput.value = 'pending';
|
518
|
+
form.appendChild(typeInput);
|
519
|
+
|
520
|
+
// Add selected job IDs
|
521
|
+
selectedIds.forEach(id => {
|
522
|
+
const input = document.createElement('input');
|
523
|
+
input.type = 'hidden';
|
524
|
+
input.name = 'job_ids[]';
|
525
|
+
input.value = id;
|
526
|
+
form.appendChild(input);
|
527
|
+
});
|
528
|
+
|
529
|
+
document.body.appendChild(form);
|
530
|
+
form.submit();
|
531
|
+
}
|
532
|
+
});
|
533
|
+
}
|
534
|
+
|
535
|
+
// Failed jobs bulk operations
|
536
|
+
const selectAllFailed = document.getElementById('select-all-failed');
|
537
|
+
const selectAllFailedHeader = document.getElementById('select-all-failed-header');
|
538
|
+
const failedCheckboxes = document.querySelectorAll('.job-checkbox-failed');
|
539
|
+
const bulkRetryFailed = document.getElementById('bulk-retry-failed');
|
540
|
+
const bulkDeleteFailed = document.getElementById('bulk-delete-failed');
|
541
|
+
const failedSelectedCount = document.getElementById('failed-selected-count');
|
542
|
+
|
543
|
+
// Sync "Select All" checkboxes for failed jobs
|
544
|
+
if (selectAllFailed && selectAllFailedHeader) {
|
545
|
+
[selectAllFailed, selectAllFailedHeader].forEach(checkbox => {
|
546
|
+
checkbox.addEventListener('change', function() {
|
547
|
+
const checked = this.checked;
|
548
|
+
failedCheckboxes.forEach(cb => cb.checked = checked);
|
549
|
+
if (selectAllFailed !== this) selectAllFailed.checked = checked;
|
550
|
+
if (selectAllFailedHeader !== this) selectAllFailedHeader.checked = checked;
|
551
|
+
updateFailedBulkButtons();
|
552
|
+
});
|
553
|
+
});
|
554
|
+
}
|
555
|
+
|
556
|
+
// Individual checkbox handlers for failed jobs
|
557
|
+
failedCheckboxes.forEach(checkbox => {
|
558
|
+
checkbox.addEventListener('change', updateFailedBulkButtons);
|
559
|
+
});
|
560
|
+
|
561
|
+
function updateFailedBulkButtons() {
|
562
|
+
const checkedBoxes = document.querySelectorAll('.job-checkbox-failed:checked');
|
563
|
+
const count = checkedBoxes.length;
|
564
|
+
|
565
|
+
if (failedSelectedCount) {
|
566
|
+
failedSelectedCount.textContent = `(${count} selected)`;
|
567
|
+
}
|
568
|
+
|
569
|
+
if (bulkRetryFailed) {
|
570
|
+
bulkRetryFailed.disabled = count === 0;
|
571
|
+
}
|
572
|
+
|
573
|
+
if (bulkDeleteFailed) {
|
574
|
+
bulkDeleteFailed.disabled = count === 0;
|
575
|
+
}
|
576
|
+
|
577
|
+
// Update "Select All" checkboxes
|
578
|
+
const allChecked = count === failedCheckboxes.length && count > 0;
|
579
|
+
if (selectAllFailed) selectAllFailed.checked = allChecked;
|
580
|
+
if (selectAllFailedHeader) selectAllFailedHeader.checked = allChecked;
|
581
|
+
}
|
582
|
+
|
583
|
+
// Bulk retry failed jobs
|
584
|
+
if (bulkRetryFailed) {
|
585
|
+
bulkRetryFailed.addEventListener('click', function() {
|
586
|
+
const selectedIds = Array.from(document.querySelectorAll('.job-checkbox-failed:checked')).map(cb => cb.value);
|
587
|
+
|
588
|
+
if (selectedIds.length === 0) return;
|
589
|
+
|
590
|
+
if (confirm(`Are you sure you want to retry ${selectedIds.length} failed job(s)?`)) {
|
591
|
+
const form = document.createElement('form');
|
592
|
+
form.method = 'POST';
|
593
|
+
form.action = '<%= ragdoll.bulk_retry_jobs_path %>';
|
594
|
+
|
595
|
+
// Add CSRF token
|
596
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
597
|
+
if (csrfToken) {
|
598
|
+
const csrfInput = document.createElement('input');
|
599
|
+
csrfInput.type = 'hidden';
|
600
|
+
csrfInput.name = 'authenticity_token';
|
601
|
+
csrfInput.value = csrfToken.content;
|
602
|
+
form.appendChild(csrfInput);
|
603
|
+
}
|
604
|
+
|
605
|
+
// Add selected job IDs
|
606
|
+
selectedIds.forEach(id => {
|
607
|
+
const input = document.createElement('input');
|
608
|
+
input.type = 'hidden';
|
609
|
+
input.name = 'job_ids[]';
|
610
|
+
input.value = id;
|
611
|
+
form.appendChild(input);
|
612
|
+
});
|
613
|
+
|
614
|
+
document.body.appendChild(form);
|
615
|
+
form.submit();
|
616
|
+
}
|
617
|
+
});
|
618
|
+
}
|
619
|
+
|
620
|
+
// Bulk delete failed jobs
|
621
|
+
if (bulkDeleteFailed) {
|
622
|
+
bulkDeleteFailed.addEventListener('click', function() {
|
623
|
+
const selectedIds = Array.from(document.querySelectorAll('.job-checkbox-failed:checked')).map(cb => cb.value);
|
624
|
+
|
625
|
+
if (selectedIds.length === 0) return;
|
626
|
+
|
627
|
+
if (confirm(`Are you sure you want to delete ${selectedIds.length} failed job(s)? This cannot be undone.`)) {
|
628
|
+
const form = document.createElement('form');
|
629
|
+
form.method = 'POST';
|
630
|
+
form.action = '<%= ragdoll.bulk_delete_jobs_path %>';
|
631
|
+
|
632
|
+
// Add CSRF token
|
633
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
634
|
+
if (csrfToken) {
|
635
|
+
const csrfInput = document.createElement('input');
|
636
|
+
csrfInput.type = 'hidden';
|
637
|
+
csrfInput.name = 'authenticity_token';
|
638
|
+
csrfInput.value = csrfToken.content;
|
639
|
+
form.appendChild(csrfInput);
|
640
|
+
}
|
641
|
+
|
642
|
+
// Add job type
|
643
|
+
const typeInput = document.createElement('input');
|
644
|
+
typeInput.type = 'hidden';
|
645
|
+
typeInput.name = 'job_type';
|
646
|
+
typeInput.value = 'failed';
|
647
|
+
form.appendChild(typeInput);
|
648
|
+
|
649
|
+
// Add selected job IDs
|
650
|
+
selectedIds.forEach(id => {
|
651
|
+
const input = document.createElement('input');
|
652
|
+
input.type = 'hidden';
|
653
|
+
input.name = 'job_ids[]';
|
654
|
+
input.value = id;
|
655
|
+
form.appendChild(input);
|
656
|
+
});
|
657
|
+
|
658
|
+
document.body.appendChild(form);
|
659
|
+
form.submit();
|
660
|
+
}
|
661
|
+
});
|
662
|
+
}
|
663
|
+
|
664
|
+
// Initialize button states
|
665
|
+
updatePendingBulkButtons();
|
666
|
+
updateFailedBulkButtons();
|
667
|
+
});
|
668
|
+
</script>
|
669
|
+
<% end %>
|