solid_bro 0.1.2
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/MIT-LICENSE +20 -0
- data/README.md +133 -0
- data/Rakefile +15 -0
- data/app/assets/javascripts/solid_bro/application.js +25 -0
- data/app/assets/stylesheets/solid_bro/application.css +636 -0
- data/app/assets/stylesheets/solid_bro/core.css +0 -0
- data/app/controllers/solid_bro/application_controller.rb +29 -0
- data/app/controllers/solid_bro/jobs_controller.rb +99 -0
- data/app/controllers/solid_bro/processes_controller.rb +7 -0
- data/app/controllers/solid_bro/queues_controller.rb +25 -0
- data/app/controllers/solid_bro/recurring_tasks_controller.rb +9 -0
- data/app/helpers/solid_bro/application_helper.rb +38 -0
- data/app/jobs/solid_bro/application_job.rb +4 -0
- data/app/mailers/solid_bro/application_mailer.rb +6 -0
- data/app/models/solid_bro/application_record.rb +5 -0
- data/app/views/layouts/solid_bro/application.html.erb +56 -0
- data/app/views/solid_bro/jobs/index.html.erb +139 -0
- data/app/views/solid_bro/jobs/show.html.erb +73 -0
- data/app/views/solid_bro/processes/index.html.erb +37 -0
- data/app/views/solid_bro/queues/index.html.erb +44 -0
- data/app/views/solid_bro/recurring_tasks/index.html.erb +35 -0
- data/config/initializers/pagy.rb +15 -0
- data/config/routes.rb +21 -0
- data/lib/generators/solid_bro/install/install_generator.rb +56 -0
- data/lib/solid_bro/engine.rb +42 -0
- data/lib/solid_bro/version.rb +3 -0
- data/lib/solid_bro.rb +8 -0
- data/lib/tasks/solid_bro_tasks.rake +53 -0
- metadata +130 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidBro
|
|
4
|
+
class JobsController < ApplicationController
|
|
5
|
+
JOB_SCOPES = %w[all failed in_progress blocked scheduled finished].freeze
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@scope = JOB_SCOPES.include?(params[:scope]) ? params[:scope] : "all"
|
|
9
|
+
@counts = job_counts
|
|
10
|
+
base = scope_relation(@scope)
|
|
11
|
+
base = apply_filters(base)
|
|
12
|
+
base = base.preload(:ready_execution, :claimed_execution, :blocked_execution, :scheduled_execution, :failed_execution)
|
|
13
|
+
# Support both Pagy 9.x (items) and Pagy 8+/43.x (limit)
|
|
14
|
+
pagy_options = if defined?(Pagy::DEFAULT) && Pagy::DEFAULT.key?(:items)
|
|
15
|
+
{ items: 25 }
|
|
16
|
+
else
|
|
17
|
+
{ limit: 25 }
|
|
18
|
+
end
|
|
19
|
+
@pagy, @jobs = pagy(base.order(created_at: :desc), **pagy_options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show
|
|
23
|
+
@job = SolidQueue::Job.find(params[:id])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def retry
|
|
27
|
+
@job = SolidQueue::Job.find(params[:id])
|
|
28
|
+
@job.retry if @job.respond_to?(:retry) && @job.failed?
|
|
29
|
+
redirect_to jobs_path(scope: "failed"), notice: "Job retry requested."
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
redirect_to job_path(@job), alert: "Retry failed: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def discard
|
|
35
|
+
@job = SolidQueue::Job.find(params[:id])
|
|
36
|
+
@job.discard
|
|
37
|
+
redirect_to jobs_path(scope: params[:scope].presence || "all"), notice: "Job discarded."
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
redirect_to job_path(@job), alert: "Discard failed: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def scope_relation(scope)
|
|
45
|
+
case scope
|
|
46
|
+
when "failed" then SolidQueue::Job.failed
|
|
47
|
+
when "in_progress" then SolidQueue::Job.joins(:claimed_execution)
|
|
48
|
+
when "blocked" then SolidQueue::Job.joins(:blocked_execution)
|
|
49
|
+
when "scheduled" then SolidQueue::Job.joins(:scheduled_execution)
|
|
50
|
+
when "finished" then SolidQueue::Job.finished
|
|
51
|
+
else SolidQueue::Job
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def job_counts
|
|
56
|
+
{
|
|
57
|
+
"all" => SolidQueue::Job.count,
|
|
58
|
+
"failed" => SolidQueue::Job.failed.count,
|
|
59
|
+
"in_progress" => SolidQueue::Job.joins(:claimed_execution).count,
|
|
60
|
+
"blocked" => SolidQueue::Job.joins(:blocked_execution).count,
|
|
61
|
+
"scheduled" => SolidQueue::Job.joins(:scheduled_execution).count,
|
|
62
|
+
"finished" => SolidQueue::Job.finished.count
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_filters(relation)
|
|
67
|
+
table = relation.respond_to?(:table_name) ? "#{relation.table_name}." : "solid_queue_jobs."
|
|
68
|
+
|
|
69
|
+
if params[:class_name].present?
|
|
70
|
+
term = params[:class_name].to_s.downcase
|
|
71
|
+
term = SolidQueue::Job.sanitize_sql_like(term)
|
|
72
|
+
relation = relation.where("LOWER(class_name) LIKE ?", "%#{term}%")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if params[:queue_name].present?
|
|
76
|
+
term = params[:queue_name].to_s.downcase
|
|
77
|
+
term = SolidQueue::Job.sanitize_sql_like(term)
|
|
78
|
+
relation = relation.where("LOWER(queue_name) LIKE ?", "%#{term}%")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if @scope == "finished"
|
|
82
|
+
relation = relation.where("finished_at >= ?", parse_datetime(params[:finished_at_start])) if params[:finished_at_start].present?
|
|
83
|
+
relation = relation.where("finished_at <= ?", parse_datetime(params[:finished_at_end])) if params[:finished_at_end].present?
|
|
84
|
+
else
|
|
85
|
+
relation = relation.where("#{table}created_at >= ?", parse_datetime(params[:created_at_start])) if params[:created_at_start].present?
|
|
86
|
+
relation = relation.where("#{table}created_at <= ?", parse_datetime(params[:created_at_end])) if params[:created_at_end].present?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
relation
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_datetime(value)
|
|
93
|
+
return nil if value.blank?
|
|
94
|
+
Time.zone.parse(value.to_s)
|
|
95
|
+
rescue ArgumentError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module SolidBro
|
|
2
|
+
class QueuesController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
# Aggregate job counts by queue
|
|
5
|
+
@queues = SolidQueue::Job.group(:queue_name).count
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def pause
|
|
9
|
+
# Placeholder for pausing queue
|
|
10
|
+
redirect_to queues_path, notice: "Queue pause logic not implemented."
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resume
|
|
14
|
+
# Placeholder for resuming queue
|
|
15
|
+
redirect_to queues_path, notice: "Queue resume logic not implemented."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def destroy_jobs
|
|
19
|
+
queue_name = params[:id]
|
|
20
|
+
SolidQueue::Job.where(queue_name: queue_name).delete_all
|
|
21
|
+
|
|
22
|
+
redirect_to queues_path, notice: "All jobs in queue '#{queue_name}' deleted."
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module SolidBro
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
# Support both Pagy 9.x (Frontend) and Pagy 8+/43.x (no separate module needed)
|
|
4
|
+
if defined?(Pagy::Frontend)
|
|
5
|
+
include Pagy::Frontend
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Pretty-print job arguments for display
|
|
9
|
+
def format_job_arguments(arguments)
|
|
10
|
+
return "—" if arguments.blank?
|
|
11
|
+
JSON.pretty_generate(arguments)
|
|
12
|
+
rescue StandardError
|
|
13
|
+
# Fallback: break Ruby inspect for readability
|
|
14
|
+
arguments.inspect.gsub("=>", " => ").gsub(", ", ",\n ")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Filter params to preserve when changing job scope tabs
|
|
18
|
+
def filter_params_for_link
|
|
19
|
+
params.permit(:class_name, :queue_name, :created_at_start, :created_at_end, :finished_at_start, :finished_at_end).to_h.compact_blank
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def jobs_tab_class(scope)
|
|
23
|
+
(params[:scope].presence || "all") == scope ? "active" : ""
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def jobs_scope_title(scope)
|
|
27
|
+
case scope
|
|
28
|
+
when "all" then "All"
|
|
29
|
+
when "failed" then "Failed"
|
|
30
|
+
when "in_progress" then "In progress"
|
|
31
|
+
when "blocked" then "Blocked"
|
|
32
|
+
when "scheduled" then "Scheduled"
|
|
33
|
+
when "finished" then "Finished"
|
|
34
|
+
else "All"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
<title>SolidBro Dashboard</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<%= csp_meta_tag %>
|
|
9
|
+
<%= stylesheet_link_tag "solid_bro/application", media: "all" %>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
11
|
+
<%= javascript_include_tag "solid_bro/application", defer: true %>
|
|
12
|
+
</head>
|
|
13
|
+
|
|
14
|
+
<body>
|
|
15
|
+
<header class="top-menu">
|
|
16
|
+
<div class="logo">SolidBro</div>
|
|
17
|
+
<nav>
|
|
18
|
+
<ul>
|
|
19
|
+
<li><%= link_to "Jobs", jobs_path, class: (controller_name == 'jobs' ? 'active' : '') %></li>
|
|
20
|
+
<li><%= link_to "Queues", queues_path, class: (controller_name == 'queues' ? 'active' : '') %></li>
|
|
21
|
+
<li><%= link_to "Workers", processes_path, class: (controller_name == 'processes' ? 'active' : '') %></li>
|
|
22
|
+
<li><%= link_to "Recurring tasks", recurring_tasks_path, class: (controller_name == 'recurring_tasks' ? 'active' : '') %></li>
|
|
23
|
+
</ul>
|
|
24
|
+
</nav>
|
|
25
|
+
<div class="user-profile">
|
|
26
|
+
<span class="avatar">SB</span>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<main class="content">
|
|
31
|
+
<% if notice %>
|
|
32
|
+
<div style="background-color: var(--success-color); color: white; padding: 1rem; margin-bottom: 1rem; border-radius: 6px;">
|
|
33
|
+
<%= notice %>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% if alert %>
|
|
37
|
+
<div style="background-color: var(--danger-color); color: white; padding: 1rem; margin-bottom: 1rem; border-radius: 6px;">
|
|
38
|
+
<%= alert %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<%= yield %>
|
|
43
|
+
</main>
|
|
44
|
+
|
|
45
|
+
<footer class="footer">
|
|
46
|
+
<div class="footer-content">
|
|
47
|
+
<div class="copyright">
|
|
48
|
+
© <%= Time.current.year %> SolidBro. All rights reserved. v<%= SolidBro::VERSION %>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="footer-links">
|
|
51
|
+
<!-- Add footer links if needed -->
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</footer>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<nav class="jobs-tabs" aria-label="Job status">
|
|
2
|
+
<ul class="jobs-tabs-list">
|
|
3
|
+
<li><%= link_to "All", jobs_path(scope: "all", **filter_params_for_link), class: jobs_tab_class("all") %></li>
|
|
4
|
+
<li><%= link_to "Failed jobs (#{@counts['failed']})", jobs_path(scope: "failed", **filter_params_for_link), class: jobs_tab_class("failed") %></li>
|
|
5
|
+
<li><%= link_to "In Progress (#{@counts['in_progress']})", jobs_path(scope: "in_progress", **filter_params_for_link), class: jobs_tab_class("in_progress") %></li>
|
|
6
|
+
<li><%= link_to "Blocked (#{@counts['blocked']})", jobs_path(scope: "blocked", **filter_params_for_link), class: jobs_tab_class("blocked") %></li>
|
|
7
|
+
<li><%= link_to "Scheduled (#{@counts['scheduled']})", jobs_path(scope: "scheduled", **filter_params_for_link), class: jobs_tab_class("scheduled") %></li>
|
|
8
|
+
<li><%= link_to "Finished (#{@counts['finished']})", jobs_path(scope: "finished", **filter_params_for_link), class: jobs_tab_class("finished") %></li>
|
|
9
|
+
</ul>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<%= form_with url: jobs_path, method: :get, local: true, class: "jobs-filters" do |f| %>
|
|
13
|
+
<%= hidden_field_tag :scope, @scope %>
|
|
14
|
+
<div class="jobs-filters-grid">
|
|
15
|
+
<div class="jobs-filter-field">
|
|
16
|
+
<%= label_tag :class_name, "Job class name" %>
|
|
17
|
+
<%= text_field_tag :class_name, params[:class_name], placeholder: "Filter by job class...", class: "jobs-filter-input" %>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="jobs-filter-field">
|
|
20
|
+
<%= label_tag :queue_name, "Queue name" %>
|
|
21
|
+
<%= text_field_tag :queue_name, params[:queue_name], placeholder: "Filter by queue name...", class: "jobs-filter-input" %>
|
|
22
|
+
</div>
|
|
23
|
+
<% if @scope == "finished" %>
|
|
24
|
+
<div class="jobs-filter-field">
|
|
25
|
+
<%= label_tag :finished_at_start, "Finished at start" %>
|
|
26
|
+
<%= text_field_tag :finished_at_start, params[:finished_at_start], class: "jobs-filter-input", type: "datetime-local" %>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="jobs-filter-field">
|
|
29
|
+
<%= label_tag :finished_at_end, "Finished at end" %>
|
|
30
|
+
<%= text_field_tag :finished_at_end, params[:finished_at_end], class: "jobs-filter-input", type: "datetime-local" %>
|
|
31
|
+
</div>
|
|
32
|
+
<% else %>
|
|
33
|
+
<div class="jobs-filter-field">
|
|
34
|
+
<%= label_tag :created_at_start, "Created at start" %>
|
|
35
|
+
<%= text_field_tag :created_at_start, params[:created_at_start], class: "jobs-filter-input", type: "datetime-local" %>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="jobs-filter-field">
|
|
38
|
+
<%= label_tag :created_at_end, "Created at end" %>
|
|
39
|
+
<%= text_field_tag :created_at_end, params[:created_at_end], class: "jobs-filter-input", type: "datetime-local" %>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<div class="jobs-filter-actions">
|
|
43
|
+
<%= submit_tag "Filter", class: "btn-primary" %>
|
|
44
|
+
<%= link_to "Clear", jobs_path(scope: @scope), class: "btn-icon" %>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
|
|
49
|
+
<div class="table-container">
|
|
50
|
+
<div class="table-header">
|
|
51
|
+
<h2><%= jobs_scope_title(@scope) %> jobs</h2>
|
|
52
|
+
</div>
|
|
53
|
+
<table>
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
<th>ID</th>
|
|
57
|
+
<th>Class / Queue</th>
|
|
58
|
+
<th>Arguments</th>
|
|
59
|
+
<th>Status</th>
|
|
60
|
+
<th><%= @scope == "finished" ? "Finished" : "Created" %> At</th>
|
|
61
|
+
<th class="text-right">Actions</th>
|
|
62
|
+
</tr>
|
|
63
|
+
</thead>
|
|
64
|
+
<tbody>
|
|
65
|
+
<% if @jobs.any? %>
|
|
66
|
+
<% @jobs.each do |job| %>
|
|
67
|
+
<tr>
|
|
68
|
+
<td>
|
|
69
|
+
<%= link_to job.id, job_path(job), class: "link-accent" %>
|
|
70
|
+
</td>
|
|
71
|
+
<td>
|
|
72
|
+
<div class="font-medium"><%= job.class_name %></div>
|
|
73
|
+
<div class="text-muted text-xs"><%= job.queue_name %></div>
|
|
74
|
+
</td>
|
|
75
|
+
<td title="<%= job.arguments.inspect %>">
|
|
76
|
+
<div class="truncate max-w-xs"><%= truncate(job.arguments.inspect, length: 40) %></div>
|
|
77
|
+
</td>
|
|
78
|
+
<td>
|
|
79
|
+
<% status_class = case job.status.to_s
|
|
80
|
+
when "failed" then "status-failed"
|
|
81
|
+
when "finished" then "status-finished"
|
|
82
|
+
when "claimed" then "status-claimed"
|
|
83
|
+
when "ready" then "status-ready"
|
|
84
|
+
else "status-default"
|
|
85
|
+
end %>
|
|
86
|
+
<span class="status-badge <%= status_class %>"><%= job.status %></span>
|
|
87
|
+
</td>
|
|
88
|
+
<td>
|
|
89
|
+
<%= @scope == "finished" && job.finished_at ? "#{time_ago_in_words(job.finished_at)} ago" : "#{time_ago_in_words(job.created_at)} ago" %>
|
|
90
|
+
</td>
|
|
91
|
+
<td class="text-right">
|
|
92
|
+
<div class="job-action-buttons">
|
|
93
|
+
<% if job.status.to_s == "failed" %>
|
|
94
|
+
<%= button_to retry_job_path(job),
|
|
95
|
+
method: :put,
|
|
96
|
+
class: "btn-icon",
|
|
97
|
+
params: { scope: @scope },
|
|
98
|
+
title: "Retry job" do %>
|
|
99
|
+
<svg viewBox="0 0 20 20" aria-hidden="true" class="job-action-icon">
|
|
100
|
+
<path d="M4 4v4h4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
101
|
+
<path d="M4.75 10a5.25 5.25 0 0 0 9.16 3.71L15.5 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
102
|
+
<path d="M15.25 8A5.25 5.25 0 0 0 6.09 4.29L4.5 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
103
|
+
</svg>
|
|
104
|
+
<% end %>
|
|
105
|
+
<% end %>
|
|
106
|
+
|
|
107
|
+
<%= button_to discard_job_path(job),
|
|
108
|
+
method: :delete,
|
|
109
|
+
class: "btn-icon btn-danger",
|
|
110
|
+
params: { scope: @scope },
|
|
111
|
+
title: "Discard job" do %>
|
|
112
|
+
<svg viewBox="0 0 20 20" aria-hidden="true" class="job-action-icon">
|
|
113
|
+
<path d="M4 6h12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
114
|
+
<path d="M8 6V4h4v2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
115
|
+
<path d="M7 6v9a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
116
|
+
</svg>
|
|
117
|
+
<% end %>
|
|
118
|
+
</div>
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
<% end %>
|
|
122
|
+
<% else %>
|
|
123
|
+
<tr>
|
|
124
|
+
<td colspan="6" class="text-muted">No jobs found.</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<% end %>
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
<% if @pagy.pages > 1 %>
|
|
130
|
+
<div class="pagination-wrapper">
|
|
131
|
+
<%# Support both Pagy 9.x (pagy_nav) and Pagy 8+/43.x (series_nav) %>
|
|
132
|
+
<% if @pagy.respond_to?(:series_nav) %>
|
|
133
|
+
<%== @pagy.series_nav %>
|
|
134
|
+
<% else %>
|
|
135
|
+
<%== pagy_nav(@pagy) %>
|
|
136
|
+
<% end %>
|
|
137
|
+
</div>
|
|
138
|
+
<% end %>
|
|
139
|
+
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<div class="job-page-header">
|
|
2
|
+
<div class="job-page-title">
|
|
3
|
+
<h1>Job #<%= @job.id %></h1>
|
|
4
|
+
<p class="job-page-subtitle"><%= @job.class_name %> on <span class="job-queue-name"><%= @job.queue_name %></span></p>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="job-page-actions">
|
|
7
|
+
<% if @job.status.to_s == "failed" %>
|
|
8
|
+
<%= button_to "Retry", retry_job_path(@job), method: :put, class: "btn-icon" %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<%= button_to "Discard", discard_job_path(@job), method: :delete, class: "btn-icon btn-danger" %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="table-container">
|
|
15
|
+
<div class="table-header">
|
|
16
|
+
<h2>Details</h2>
|
|
17
|
+
</div>
|
|
18
|
+
<table class="job-details-table">
|
|
19
|
+
<tbody>
|
|
20
|
+
<tr>
|
|
21
|
+
<th scope="row">ID</th>
|
|
22
|
+
<td><%= @job.id %></td>
|
|
23
|
+
</tr>
|
|
24
|
+
<tr>
|
|
25
|
+
<th scope="row">Class</th>
|
|
26
|
+
<td><%= @job.class_name %></td>
|
|
27
|
+
</tr>
|
|
28
|
+
<tr>
|
|
29
|
+
<th scope="row">Queue</th>
|
|
30
|
+
<td><%= @job.queue_name %></td>
|
|
31
|
+
</tr>
|
|
32
|
+
<tr>
|
|
33
|
+
<th scope="row">Status</th>
|
|
34
|
+
<td>
|
|
35
|
+
<% status_class = case @job.status.to_s
|
|
36
|
+
when "failed" then "status-failed"
|
|
37
|
+
when "finished" then "status-finished"
|
|
38
|
+
when "claimed" then "status-claimed"
|
|
39
|
+
when "ready" then "status-ready"
|
|
40
|
+
else "status-default"
|
|
41
|
+
end %>
|
|
42
|
+
<span class="status-badge <%= status_class %>"><%= @job.status %></span>
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
<tr>
|
|
46
|
+
<th scope="row">Created at</th>
|
|
47
|
+
<td><%= @job.created_at ? l(@job.created_at, format: :long) : "—" %></td>
|
|
48
|
+
</tr>
|
|
49
|
+
<tr>
|
|
50
|
+
<th scope="row">Scheduled at</th>
|
|
51
|
+
<td><%= @job.scheduled_at ? l(@job.scheduled_at, format: :long) : "—" %></td>
|
|
52
|
+
</tr>
|
|
53
|
+
<tr>
|
|
54
|
+
<th scope="row">Arguments</th>
|
|
55
|
+
<td>
|
|
56
|
+
<pre class="job-arguments-block"><%= format_job_arguments(@job.arguments) %></pre>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<% if @job.respond_to?(:failed_execution) && @job.failed_execution %>
|
|
64
|
+
<div class="table-container table-container--error">
|
|
65
|
+
<div class="table-header">
|
|
66
|
+
<h2>Exception</h2>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="exception-content">
|
|
69
|
+
<p class="exception-message"><%= @job.failed_execution.error_class %>: <%= @job.failed_execution.error_message %></p>
|
|
70
|
+
<pre class="job-arguments-block job-arguments-block--backtrace"><%= @job.failed_execution.backtrace.join("\n") %></pre>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="table-container">
|
|
2
|
+
<div class="table-header">
|
|
3
|
+
<h2>Workers</h2>
|
|
4
|
+
</div>
|
|
5
|
+
<table>
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th>ID</th>
|
|
9
|
+
<th>Name</th>
|
|
10
|
+
<th>Hostname</th>
|
|
11
|
+
<th>Kind</th>
|
|
12
|
+
<th>Last Heartbeat</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
<% @processes.each do |process| %>
|
|
17
|
+
<tr>
|
|
18
|
+
<td>
|
|
19
|
+
<span class="font-medium text-white"><%= process.id %></span>
|
|
20
|
+
</td>
|
|
21
|
+
<td>
|
|
22
|
+
<span class="font-medium text-white"><%= process.name %></span>
|
|
23
|
+
</td>
|
|
24
|
+
<td>
|
|
25
|
+
<span class="text-gray-300"><%= process.hostname %></span>
|
|
26
|
+
</td>
|
|
27
|
+
<td>
|
|
28
|
+
<span class="badge badge-viewer"><%= process.kind %></span>
|
|
29
|
+
</td>
|
|
30
|
+
<td class="text-gray-300">
|
|
31
|
+
<%= process.last_heartbeat_at %>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
</tbody>
|
|
36
|
+
</table>
|
|
37
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<div class="table-container">
|
|
2
|
+
<div class="table-header">
|
|
3
|
+
<h2>Queues</h2>
|
|
4
|
+
</div>
|
|
5
|
+
<table>
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th>Queue Name</th>
|
|
9
|
+
<th>Jobs Count</th>
|
|
10
|
+
<th>Status</th>
|
|
11
|
+
<th class="text-right">Actions</th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% @queues.each do |queue_name, count| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td>
|
|
18
|
+
<span class="font-medium text-white"><%= queue_name %></span>
|
|
19
|
+
</td>
|
|
20
|
+
<td>
|
|
21
|
+
<span class="text-gray-300"><%= count %></span>
|
|
22
|
+
</td>
|
|
23
|
+
<td>
|
|
24
|
+
<span class="status-dot status-active"></span> Active
|
|
25
|
+
</td>
|
|
26
|
+
<td class="text-right">
|
|
27
|
+
<div class="queue-action-buttons">
|
|
28
|
+
<%= button_to "Pause",
|
|
29
|
+
pause_queue_path(queue_name),
|
|
30
|
+
method: :put,
|
|
31
|
+
class: "btn-icon",
|
|
32
|
+
style: "color: var(--warning-color);" %>
|
|
33
|
+
<%= button_to "Delete all",
|
|
34
|
+
destroy_jobs_queue_path(queue_name),
|
|
35
|
+
method: :delete,
|
|
36
|
+
data: { confirm: "Are you sure you want to delete all jobs in this queue?" },
|
|
37
|
+
class: "btn-icon btn-danger" %>
|
|
38
|
+
</div>
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
<% end %>
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<div class="table-container">
|
|
2
|
+
<div class="table-header">
|
|
3
|
+
<h2>Recurring tasks</h2>
|
|
4
|
+
</div>
|
|
5
|
+
<table>
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th>Key</th>
|
|
9
|
+
<th>Schedule</th>
|
|
10
|
+
<th>Class / Command</th>
|
|
11
|
+
<th>Queue</th>
|
|
12
|
+
<th>Description</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
<% if @recurring_tasks.any? %>
|
|
17
|
+
<% @recurring_tasks.each do |task| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td><span class="font-medium"><%= task.key %></span></td>
|
|
20
|
+
<td><code class="code-inline"><%= task.schedule %></code></td>
|
|
21
|
+
<td>
|
|
22
|
+
<%= task.class_name.presence || task.command %>
|
|
23
|
+
</td>
|
|
24
|
+
<td><%= task.queue_name.presence || "—" %></td>
|
|
25
|
+
<td class="text-secondary"><%= task.description.presence || "—" %></td>
|
|
26
|
+
</tr>
|
|
27
|
+
<% end %>
|
|
28
|
+
<% else %>
|
|
29
|
+
<tr>
|
|
30
|
+
<td colspan="5" class="text-secondary">No recurring tasks configured.</td>
|
|
31
|
+
</tr>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tbody>
|
|
34
|
+
</table>
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pagy default configuration for SolidBro
|
|
4
|
+
# Supports Pagy 9.x, 8.x, and 43.x
|
|
5
|
+
# See https://ddnexus.github.io/pagy/resources/initializer/
|
|
6
|
+
|
|
7
|
+
if defined?(Pagy)
|
|
8
|
+
if Pagy.respond_to?(:options)
|
|
9
|
+
# Pagy 8+/43.x uses Pagy.options with 'limit'
|
|
10
|
+
Pagy.options[:limit] = 25
|
|
11
|
+
elsif defined?(Pagy::DEFAULT)
|
|
12
|
+
# Pagy 9.x uses Pagy::DEFAULT with 'items'
|
|
13
|
+
Pagy::DEFAULT[:items] = 25
|
|
14
|
+
end
|
|
15
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
SolidBro::Engine.routes.draw do
|
|
2
|
+
root to: "jobs#index"
|
|
3
|
+
|
|
4
|
+
resources :jobs, only: [:index, :show], path: "jobs" do
|
|
5
|
+
member do
|
|
6
|
+
put :retry
|
|
7
|
+
delete :discard
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
resources :queues, only: [:index] do
|
|
12
|
+
member do
|
|
13
|
+
put :pause
|
|
14
|
+
put :resume
|
|
15
|
+
delete :destroy_jobs
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
resources :processes, only: [:index], path: "workers"
|
|
20
|
+
resources :recurring_tasks, only: [:index], path: "recurring-tasks"
|
|
21
|
+
end
|