solid_stack_web 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0adcafbdcb32ea6e96f982efc8d44502ea31e75e9f25138558ba58a8952719c8
4
- data.tar.gz: a0e5b5695d620e4d616535e5f7b394df10c756ee712584a7de7c7ef489ded759
3
+ metadata.gz: 777352d5919952ea022a545b13608ace8f35e0f91077431c45a679e5c30bec19
4
+ data.tar.gz: d36dbf96b4234d730249df1d259099883d0473c160c62ab9bee73fe88ff133c4
5
5
  SHA512:
6
- metadata.gz: 42c74a1cbc74acf3485ce0649e7f894bbbc1fdb4172037dbac4edb6e6805c8571c3731efb5acaa774763ae979ae8bb632e81e16f7f36e51faed447c8eb3bd81d
7
- data.tar.gz: e35fb81b13d2f4cfa2d43f2e9ee223ef32c9605a69c98cf38e94f193d037f284efae651922ace035e04ad0ac1129361825345d5a64b97d91809caad1201351b9
6
+ metadata.gz: eefbb43300e8e244b6354d1fb952edc7577585c76f5721920d6fa28aba41038840d4924cae9799903a3742e0782a2e8f95d8b142502194d1726e87fbb8c35e7a
7
+ data.tar.gz: 254f13440a71b0dcfbeb3330f2c1be4c21a5e038e09355493bba1fe5b3767b70d314ba37982d7adced753915a971d159d06bdc29741e5c37e223e520184f34e9
data/README.md CHANGED
@@ -10,8 +10,12 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol
10
10
  ## Features
11
11
 
12
12
  - **Overview dashboard** with live counts across all three Solid Stack components; cards are clickable and link directly to each section
13
- - **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; manage failed jobs (retry / discard / bulk retry / bulk discard), pause/resume queues, and inspect worker processes; **Bulk selection** checkbox-selects individual jobs for discard or retry; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs or failed jobs as a CSV file respecting active filters
13
+ - **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; manage failed jobs (retry / discard / bulk retry / bulk discard), pause/resume queues, and inspect worker processes; **Bulk selection** checkbox-selects individual jobs for discard or retry; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs or failed jobs as a CSV file respecting active filters; **Per-queue browser** — click any queue name or size to drill into its ready jobs with per-row and bulk discard
14
+ - **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; CSV export respects active filters
15
+ - **Scheduled job management** — "Run Now" and offset buttons (+1h / +24h / +7d) per row update the scheduled time inline via Turbo Stream; "Run All Now (N)" in the header back-dates all matching executions at once
16
+ - **Recurring task list** — enumerates all `SolidQueue::RecurringTask` records with cron schedule, job class or command, queue, next-run and last-run times, and a static/dynamic badge; each row has a "Run Now" button that immediately enqueues the task
14
17
  - **Job detail page** — drill into any job to see full arguments (pretty-printed JSON), queue, priority, enqueued time, Active Job ID, concurrency key, scheduled/blocked-until metadata, and a Discard button
18
+ - **Failed job detail page** — drill into any failed job to see the full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action
15
19
  - **Solid Cache** — entry count and total byte size at a glance
16
20
  - **Solid Cable** — active message count and distinct channel count
17
21
  - **Turbo Stream** job discard — removes the row inline without a full page reload
@@ -82,6 +86,7 @@ The `authenticate` block is evaluated in the context of each request's controlle
82
86
  - [solid_cache](https://github.com/rails/solid_cache) >= 1.0
83
87
  - [solid_cable](https://github.com/rails/solid_cable) >= 1.0
84
88
  - [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0
89
+ - [importmap-rails](https://github.com/rails/importmap-rails) >= 1.2
85
90
 
86
91
  ## Contributing
87
92
 
@@ -72,12 +72,20 @@
72
72
 
73
73
  .sqw-page-header { margin-bottom: 1.25rem; }
74
74
  .sqw-page-title { font-size: 20px; font-weight: 600; }
75
+ .sqw-page-title-row { display: flex; align-items: center; gap: 0.5rem; }
76
+
77
+ @keyframes sqw-flash-dismiss {
78
+ 0%, 86% { opacity: 1; max-height: 200px; margin-bottom: 1rem; }
79
+ 100% { opacity: 0; max-height: 0; margin-bottom: 0; padding: 0; }
80
+ }
75
81
 
76
82
  .sqw-flash {
77
83
  padding: 0.75rem 1rem;
78
84
  border-radius: var(--radius);
79
85
  margin-bottom: 1rem;
80
86
  font-size: 13px;
87
+ animation: sqw-flash-dismiss 7s ease forwards;
88
+ overflow: hidden;
81
89
  }
82
90
  .sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }
83
91
  .sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }
@@ -15,7 +15,7 @@ module SolidStackWeb
15
15
 
16
16
  def current_section
17
17
  case controller_name
18
- when "jobs", "failed_jobs", "queues", "processes" then :queue
18
+ when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs", "recurring_tasks" then :queue
19
19
  when "cache" then :cache
20
20
  when "cable" then :cable
21
21
  else :overview
@@ -1,22 +1,28 @@
1
1
  module SolidStackWeb
2
2
  module FailedJobs
3
3
  class SelectionsController < ApplicationController
4
+ before_action :set_ids
5
+
4
6
  def create
5
- ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
6
- SolidQueue::FailedExecution.where(id: ids).each(&:retry)
7
+ SolidQueue::FailedExecution.where(id: @ids).each(&:retry)
7
8
  redirect_to failed_jobs_path
8
9
  rescue => e
9
10
  redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}"
10
11
  end
11
12
 
12
13
  def destroy
13
- ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
14
- job_ids = SolidQueue::FailedExecution.where(id: ids).pluck(:job_id)
14
+ job_ids = SolidQueue::FailedExecution.where(id: @ids).pluck(:job_id)
15
15
  SolidQueue::Job.where(id: job_ids).destroy_all
16
16
  redirect_to failed_jobs_path
17
17
  rescue => e
18
18
  redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}"
19
19
  end
20
+
21
+ private
22
+
23
+ def set_ids
24
+ @ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?)
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -0,0 +1,42 @@
1
+ module SolidStackWeb
2
+ class HistoryController < ApplicationController
3
+ before_action :set_filters
4
+
5
+ def index
6
+ respond_to do |format|
7
+ format.html { @pagy, @jobs = pagy(filtered_scope) }
8
+ format.csv do
9
+ send_data history_csv(filtered_scope),
10
+ filename: "job-history-#{Date.today}.csv",
11
+ type: "text/csv", disposition: "attachment"
12
+ end
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def set_filters
19
+ @queue = params[:queue].presence
20
+ @search = params[:q].presence
21
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
22
+ end
23
+
24
+ def filtered_scope
25
+ scope = SolidQueue::Job.where.not(finished_at: nil).order(finished_at: :desc)
26
+ scope = scope.where(queue_name: @queue) if @queue.present?
27
+ scope = scope.where("class_name LIKE ?", "%#{@search}%") if @search.present?
28
+ scope = scope.where("finished_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
29
+ scope
30
+ end
31
+
32
+ def history_csv(scope)
33
+ CSV.generate(headers: true) do |csv|
34
+ csv << %w[id class_name queue_name duration_seconds finished_at]
35
+ scope.order(finished_at: :desc).each do |job|
36
+ duration = job.finished_at && job.created_at ? (job.finished_at - job.created_at).round : nil
37
+ csv << [job.id, job.class_name, job.queue_name, duration, job.finished_at.iso8601]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module SolidStackWeb
2
+ class Queues::PausesController < ApplicationController
3
+ def create
4
+ ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:queue_id])
5
+ redirect_back_or_to queues_path
6
+ end
7
+
8
+ def destroy
9
+ ::SolidQueue::Pause.find_by(queue_name: params[:queue_id])&.destroy
10
+ redirect_back_or_to queues_path
11
+ end
12
+ end
13
+ end
@@ -13,14 +13,15 @@ module SolidStackWeb
13
13
  end
14
14
  end
15
15
 
16
- def pause
17
- ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:id])
18
- redirect_to queues_path
19
- end
20
-
21
- def resume
22
- ::SolidQueue::Pause.find_by(queue_name: params[:id])&.destroy
23
- redirect_to queues_path
16
+ def show
17
+ @queue_name = params[:id]
18
+ @paused = ::SolidQueue::Pause.exists?(queue_name: @queue_name)
19
+ @pagy, @executions = pagy(
20
+ ::SolidQueue::ReadyExecution
21
+ .where(queue_name: @queue_name)
22
+ .includes(:job)
23
+ .order(created_at: :desc)
24
+ )
24
25
  end
25
26
  end
26
27
  end
@@ -0,0 +1,18 @@
1
+ module SolidStackWeb
2
+ class RecurringTasks::RunsController < ApplicationController
3
+ def create
4
+ task = SolidQueue::RecurringTask.find_by!(key: params[:recurring_task_key])
5
+ result = task.enqueue(at: Time.current)
6
+
7
+ if result
8
+ redirect_to recurring_tasks_path, notice: "\"#{task.key}\" queued for immediate execution."
9
+ else
10
+ redirect_to recurring_tasks_path, alert: "Could not enqueue \"#{task.key}\" — it may have just run."
11
+ end
12
+ rescue ActiveRecord::RecordNotFound
13
+ redirect_to recurring_tasks_path, alert: "Recurring task not found."
14
+ rescue => e
15
+ redirect_to recurring_tasks_path, alert: "Could not run task: #{e.message}"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module SolidStackWeb
2
+ class RecurringTasksController < ApplicationController
3
+ def index
4
+ @recurring_tasks = SolidQueue::RecurringTask.includes(:recurring_executions).order(:key)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,52 @@
1
+ module SolidStackWeb
2
+ class ScheduledJobsController < ApplicationController
3
+ def create
4
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
5
+ job_ids = scheduled_scope.pluck("solid_queue_jobs.id")
6
+ SolidQueue::ScheduledExecution.where(job_id: job_ids).update_all(scheduled_at: 1.second.ago)
7
+ SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
8
+ redirect_to jobs_path(status: "scheduled", period: @period),
9
+ notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately."
10
+ rescue => e
11
+ redirect_to jobs_path(status: "scheduled", period: @period),
12
+ alert: "Could not run jobs: #{e.message}"
13
+ end
14
+
15
+ def update
16
+ @execution = SolidQueue::ScheduledExecution.find(params[:id])
17
+ @period = params[:period].presence_in(PERIOD_DURATIONS.keys)
18
+ @run_now = params[:offset] == "now"
19
+ new_time = resolve_new_time(@execution, params[:offset])
20
+
21
+ @execution.update!(scheduled_at: new_time)
22
+ @execution.job.update!(scheduled_at: new_time)
23
+
24
+ respond_to do |format|
25
+ format.turbo_stream
26
+ format.html do
27
+ notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}."
28
+ redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
29
+ end
30
+ end
31
+ rescue ArgumentError => e
32
+ redirect_to jobs_path(status: "scheduled"), alert: e.message
33
+ rescue => e
34
+ redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}"
35
+ end
36
+
37
+ private
38
+
39
+ def scheduled_scope
40
+ scope = SolidQueue::ScheduledExecution.joins(:job)
41
+ scope = scope.where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present?
42
+ scope
43
+ end
44
+
45
+ def resolve_new_time(execution, offset)
46
+ return 1.second.ago if offset == "now"
47
+ raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset)
48
+
49
+ execution.scheduled_at + PERIOD_DURATIONS[offset]
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,13 @@
1
1
  module SolidStackWeb
2
2
  module ApplicationHelper
3
+ def format_duration(seconds)
4
+ s = seconds.to_i
5
+ return "#{s}s" if s < 60
6
+ return "#{s / 60}m #{s % 60}s" if s < 3600
7
+
8
+ "#{s / 3600}h #{(s % 3600) / 60}m"
9
+ end
10
+
3
11
  def inline_styles
4
12
  dir = SolidStackWeb::Engine.root.join("app/assets/stylesheets/solid_stack_web")
5
13
  css = dir.glob("_*.css").sort.map(&:read).join("\n")
@@ -33,6 +33,10 @@
33
33
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %>
34
34
  <%= link_to "Queues", queues_path,
35
35
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %>
36
+ <%= link_to "Recurring", recurring_tasks_path,
37
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "recurring_tasks"}" %>
38
+ <%= link_to "History", history_path,
39
+ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "history"}" %>
36
40
  <%= link_to "Processes", processes_path,
37
41
  class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "processes"}" %>
38
42
  </div>
@@ -0,0 +1,73 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <h1 class="sqw-page-title">Job History</h1>
3
+ <div class="sqw-header-actions">
4
+ <% if @jobs&.any? %>
5
+ <%= link_to "Export CSV", history_path(format: :csv, queue: @queue, q: @search, period: @period),
6
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
7
+ <% end %>
8
+ </div>
9
+ </div>
10
+
11
+ <form class="sqw-filters" action="<%= history_path %>" method="get">
12
+ <% if @queue.present? %>
13
+ <input type="hidden" name="queue" value="<%= @queue %>">
14
+ <% end %>
15
+ <input type="hidden" name="period" value="<%= @period %>">
16
+ <input class="sqw-search-input" type="search" name="q" value="<%= @search %>"
17
+ placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class">
18
+ <button type="submit" class="sqw-btn sqw-btn--muted sqw-btn--sm">Search</button>
19
+ <% if @search.present? %>
20
+ <%= link_to "Clear", history_path(queue: @queue, period: @period), class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
21
+ <% end %>
22
+ <div class="sqw-period-filter" role="group" aria-label="Time period">
23
+ <%= link_to "All", history_path(queue: @queue, q: @search),
24
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period.nil?}" %>
25
+ <%= link_to "1h", history_path(queue: @queue, q: @search, period: "1h"),
26
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "1h"}" %>
27
+ <%= link_to "24h", history_path(queue: @queue, q: @search, period: "24h"),
28
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "24h"}" %>
29
+ <%= link_to "7d", history_path(queue: @queue, q: @search, period: "7d"),
30
+ class: "sqw-period-btn #{"sqw-period-btn--active" if @period == "7d"}" %>
31
+ </div>
32
+ </form>
33
+
34
+ <% if @queue.present? %>
35
+ <p class="sqw-muted" style="font-size: 13px; margin-bottom: 0.75rem;">
36
+ Filtering by queue: <strong><%= @queue %></strong> &mdash;
37
+ <%= link_to "Clear filter", history_path(q: @search, period: @period) %>
38
+ </p>
39
+ <% end %>
40
+
41
+ <% if @jobs.any? %>
42
+ <div class="sqw-detail-card">
43
+ <table class="sqw-table">
44
+ <thead>
45
+ <tr>
46
+ <th>Job Class</th>
47
+ <th>Queue</th>
48
+ <th>Duration</th>
49
+ <th>Finished At</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>
53
+ <% @jobs.each do |job| %>
54
+ <tr>
55
+ <td class="sqw-monospace"><%= job.class_name %></td>
56
+ <td>
57
+ <%= link_to job.queue_name,
58
+ history_path(queue: job.queue_name, q: @search, period: @period),
59
+ class: "sqw-badge sqw-badge--queue" %>
60
+ </td>
61
+ <td class="sqw-monospace"><%= format_duration(job.finished_at - job.created_at) %></td>
62
+ <td class="sqw-muted"><%= job.finished_at.strftime("%b %d %H:%M:%S") %></td>
63
+ </tr>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
68
+ </div>
69
+ <% else %>
70
+ <div class="sqw-empty">
71
+ <p>No finished jobs found.</p>
72
+ </div>
73
+ <% end %>
@@ -3,6 +3,14 @@
3
3
  <div class="sqw-header-actions">
4
4
  <%= link_to "Export CSV", jobs_path(format: :csv, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
5
5
  class: "sqw-btn sqw-btn--muted sqw-btn--sm", data: { turbo: false } %>
6
+ <% if @status == "scheduled" && @executions&.any? %>
7
+ <%= button_to "Run All Now (#{@pagy.count})",
8
+ run_all_now_scheduled_jobs_path(period: @period),
9
+ method: :post,
10
+ class: "sqw-btn sqw-btn--sm",
11
+ data: { turbo_confirm: "Run all #{@pagy.count} scheduled jobs immediately?",
12
+ turbo_frame: "_top" } %>
13
+ <% end %>
6
14
  <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) && @executions&.any? %>
7
15
  <%= button_to "Discard All (#{@pagy.count})",
8
16
  discard_all_jobs_path(status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
@@ -111,9 +119,22 @@
111
119
  <td><%= execution.job.priority %></td>
112
120
  <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
113
121
  <% if @status == "scheduled" %>
114
- <td class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
122
+ <td id="scheduled_at_<%= execution.id %>" class="sqw-muted"><%= execution.scheduled_at&.strftime("%b %d %H:%M") %></td>
115
123
  <% end %>
116
124
  <td class="sqw-actions">
125
+ <% if @status == "scheduled" %>
126
+ <%= button_to "Run Now", scheduled_job_path(execution),
127
+ method: :patch,
128
+ params: { offset: "now", period: @period },
129
+ class: "sqw-btn sqw-btn--sm",
130
+ data: { turbo_confirm: "Run this job immediately?" } %>
131
+ <% %w[1h 24h 7d].each do |offset| %>
132
+ <%= button_to "+#{offset}", scheduled_job_path(execution),
133
+ method: :patch,
134
+ params: { offset: offset, period: @period },
135
+ class: "sqw-btn sqw-btn--muted sqw-btn--sm" %>
136
+ <% end %>
137
+ <% end %>
117
138
  <% if %w[ready scheduled blocked].include?(@status) %>
118
139
  <%= button_to "Discard", job_path(execution, status: @status, q: @search, queue: @queue, period: @period, priority: @priority),
119
140
  method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
@@ -15,8 +15,8 @@
15
15
  <tbody>
16
16
  <% @queues.each do |queue| %>
17
17
  <tr>
18
- <td class="sqw-monospace"><%= queue[:name] %></td>
19
- <td><%= queue[:size] %></td>
18
+ <td class="sqw-monospace"><%= link_to queue[:name], queue_path(queue[:name]) %></td>
19
+ <td><%= link_to queue[:size], queue_path(queue[:name]) %></td>
20
20
  <td>
21
21
  <% if queue[:paused] %>
22
22
  <span class="sqw-badge sqw-badge--paused">Paused</span>
@@ -26,10 +26,10 @@
26
26
  </td>
27
27
  <td class="sqw-actions">
28
28
  <% if queue[:paused] %>
29
- <%= button_to "Resume", resume_queue_path(queue[:name]),
29
+ <%= button_to "Resume", queue_pause_path(queue[:name]),
30
30
  method: :delete, class: "sqw-btn sqw-btn--sm" %>
31
31
  <% else %>
32
- <%= button_to "Pause", pause_queue_path(queue[:name]),
32
+ <%= button_to "Pause", queue_pause_path(queue[:name]),
33
33
  method: :post, class: "sqw-btn sqw-btn--sm" %>
34
34
  <% end %>
35
35
  </td>
@@ -0,0 +1,67 @@
1
+ <div class="sqw-page-header sqw-page-header--split">
2
+ <div>
3
+ <div class="sqw-breadcrumb">
4
+ <%= link_to "Queues", queues_path %> &rsaquo; <%= @queue_name %>
5
+ </div>
6
+ <div class="sqw-page-title-row">
7
+ <h1 class="sqw-page-title sqw-monospace"><%= @queue_name %></h1>
8
+ <% if @paused %>
9
+ <span class="sqw-badge sqw-badge--paused">Paused</span>
10
+ <% else %>
11
+ <span class="sqw-badge sqw-badge--ready">Running</span>
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+ <div class="sqw-header-actions">
16
+ <% if @paused %>
17
+ <%= button_to "Resume", queue_pause_path(@queue_name),
18
+ method: :delete, class: "sqw-btn sqw-btn--sm" %>
19
+ <% else %>
20
+ <%= button_to "Pause", queue_pause_path(@queue_name),
21
+ method: :post, class: "sqw-btn sqw-btn--sm" %>
22
+ <% end %>
23
+ <% if @executions.any? %>
24
+ <%= button_to "Discard All Ready (#{@pagy.count})",
25
+ discard_all_jobs_path(status: "ready", queue: @queue_name),
26
+ method: :post,
27
+ class: "sqw-btn sqw-btn--danger sqw-btn--sm",
28
+ data: { turbo_confirm: "Discard all #{@pagy.count} ready jobs in #{@queue_name}? This cannot be undone.",
29
+ turbo_frame: "_top" } %>
30
+ <% end %>
31
+ </div>
32
+ </div>
33
+
34
+ <% if @executions.any? %>
35
+ <table class="sqw-table">
36
+ <thead>
37
+ <tr>
38
+ <th>Job Class</th>
39
+ <th>Priority</th>
40
+ <th>Enqueued At</th>
41
+ <th></th>
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ <% @executions.each do |execution| %>
46
+ <tr>
47
+ <td class="sqw-monospace">
48
+ <%= link_to execution.job.class_name, job_path(execution.id, status: "ready"),
49
+ data: { turbo_frame: "_top" } %>
50
+ </td>
51
+ <td><%= execution.job.priority %></td>
52
+ <td class="sqw-muted"><%= execution.created_at.strftime("%b %d %H:%M") %></td>
53
+ <td class="sqw-actions">
54
+ <%= button_to "Discard", job_path(execution, status: "ready", queue: @queue_name),
55
+ method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm",
56
+ data: { turbo_confirm: "Discard this job?" } %>
57
+ </td>
58
+ </tr>
59
+ <% end %>
60
+ </tbody>
61
+ </table>
62
+ <%== pagy_nav(@pagy) if @pagy.pages > 1 %>
63
+ <% else %>
64
+ <div class="sqw-empty">
65
+ <p>No ready jobs in <strong><%= @queue_name %></strong>.</p>
66
+ </div>
67
+ <% end %>
@@ -0,0 +1,67 @@
1
+ <div class="sqw-page-header">
2
+ <h1 class="sqw-page-title">Recurring Tasks</h1>
3
+ </div>
4
+
5
+ <% if @recurring_tasks.any? %>
6
+ <table class="sqw-table">
7
+ <thead>
8
+ <tr>
9
+ <th>Key</th>
10
+ <th>Schedule</th>
11
+ <th>Job / Command</th>
12
+ <th>Queue</th>
13
+ <th>Next Run</th>
14
+ <th>Last Run</th>
15
+ <th>Type</th>
16
+ <th></th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @recurring_tasks.each do |task| %>
21
+ <tr>
22
+ <td class="sqw-monospace"><%= task.key %></td>
23
+ <td class="sqw-monospace"><%= task.schedule %></td>
24
+ <td>
25
+ <% if task.class_name.present? %>
26
+ <span class="sqw-monospace"><%= task.class_name %></span>
27
+ <% if task.arguments.present? %>
28
+ <div class="sqw-muted sqw-monospace" style="font-size: 11px;"><%= task.arguments.inspect %></div>
29
+ <% end %>
30
+ <% else %>
31
+ <span class="sqw-monospace sqw-muted"><%= task.command %></span>
32
+ <% end %>
33
+ <% if task.description.present? %>
34
+ <div class="sqw-muted" style="font-size: 12px; margin-top: 0.2rem;"><%= task.description %></div>
35
+ <% end %>
36
+ </td>
37
+ <td><span class="sqw-badge sqw-badge--queue"><%= task.queue_name.presence || "default" %></span></td>
38
+ <td class="sqw-muted sqw-monospace">
39
+ <% next_run = begin; task.next_time.strftime("%b %d %H:%M"); rescue; nil; end %>
40
+ <%= next_run || "—" %>
41
+ </td>
42
+ <td class="sqw-muted sqw-monospace">
43
+ <% last_run = task.last_enqueued_time %>
44
+ <%= last_run ? last_run.strftime("%b %d %H:%M") : "—" %>
45
+ </td>
46
+ <td>
47
+ <% if task.static? %>
48
+ <span class="sqw-badge sqw-badge--scheduled">Static</span>
49
+ <% else %>
50
+ <span class="sqw-badge sqw-badge--blocked">Dynamic</span>
51
+ <% end %>
52
+ </td>
53
+ <td class="sqw-actions">
54
+ <%= button_to "Run Now", recurring_task_run_path(task.key),
55
+ method: :post,
56
+ class: "sqw-btn sqw-btn--sm",
57
+ data: { turbo_confirm: "Run \"#{task.key}\" immediately?" } %>
58
+ </td>
59
+ </tr>
60
+ <% end %>
61
+ </tbody>
62
+ </table>
63
+ <% else %>
64
+ <div class="sqw-empty">
65
+ <p>No recurring tasks configured.</p>
66
+ </div>
67
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <% if @run_now %>
2
+ <%= turbo_stream.remove "execution_#{@execution.id}" %>
3
+ <% else %>
4
+ <%= turbo_stream.replace "scheduled_at_#{@execution.id}" do %>
5
+ <td id="scheduled_at_<%= @execution.id %>" class="sqw-muted">
6
+ <%= @execution.scheduled_at.strftime("%b %d %H:%M") %>
7
+ </td>
8
+ <% end %>
9
+ <% end %>
data/config/routes.rb CHANGED
@@ -4,6 +4,16 @@ SolidStackWeb::Engine.routes.draw do
4
4
  resource :job_selection, path: "jobs/selection", only: [:destroy], controller: "jobs/selections"
5
5
  resource :failed_job_selection, path: "failed_jobs/selection", only: [:create, :destroy], controller: "failed_jobs/selections"
6
6
 
7
+ resources :recurring_tasks, only: [:index], param: :key do
8
+ resource :run, only: [:create], controller: "recurring_tasks/runs"
9
+ end
10
+
11
+ resources :scheduled_jobs, only: [:update] do
12
+ collection do
13
+ post :run_all_now, action: :create
14
+ end
15
+ end
16
+
7
17
  resources :jobs, only: [:index, :show, :destroy] do
8
18
  collection do
9
19
  post :discard_all, action: :destroy
@@ -15,15 +25,13 @@ SolidStackWeb::Engine.routes.draw do
15
25
  resource :arguments, only: [:update], controller: "failed_jobs/arguments"
16
26
  end
17
27
 
18
- resources :queues, only: [:index] do
19
- member do
20
- post :pause
21
- delete :resume
22
- end
28
+ resources :queues, only: [:index, :show] do
29
+ resource :pause, only: [:create, :destroy], controller: "queues/pauses"
23
30
  end
24
31
 
25
32
  resources :processes, only: [:index]
26
33
 
34
+ get "history", to: "history#index", as: :history
27
35
  get "cache", to: "cache#index", as: :cache
28
36
  get "cable", to: "cable#index", as: :cable
29
37
  end
@@ -1,3 +1,3 @@
1
1
  module SolidStackWeb
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_stack_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -150,10 +150,15 @@ files:
150
150
  - app/controllers/solid_stack_web/failed_jobs/arguments_controller.rb
151
151
  - app/controllers/solid_stack_web/failed_jobs/selections_controller.rb
152
152
  - app/controllers/solid_stack_web/failed_jobs_controller.rb
153
+ - app/controllers/solid_stack_web/history_controller.rb
153
154
  - app/controllers/solid_stack_web/jobs/selections_controller.rb
154
155
  - app/controllers/solid_stack_web/jobs_controller.rb
155
156
  - app/controllers/solid_stack_web/processes_controller.rb
157
+ - app/controllers/solid_stack_web/queues/pauses_controller.rb
156
158
  - app/controllers/solid_stack_web/queues_controller.rb
159
+ - app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb
160
+ - app/controllers/solid_stack_web/recurring_tasks_controller.rb
161
+ - app/controllers/solid_stack_web/scheduled_jobs_controller.rb
157
162
  - app/helpers/solid_stack_web/application_helper.rb
158
163
  - app/javascript/solid_stack_web/application.js
159
164
  - app/javascript/solid_stack_web/selection_controller.js
@@ -165,12 +170,16 @@ files:
165
170
  - app/views/solid_stack_web/failed_jobs/destroy.turbo_stream.erb
166
171
  - app/views/solid_stack_web/failed_jobs/index.html.erb
167
172
  - app/views/solid_stack_web/failed_jobs/show.html.erb
173
+ - app/views/solid_stack_web/history/index.html.erb
168
174
  - app/views/solid_stack_web/jobs/_empty.html.erb
169
175
  - app/views/solid_stack_web/jobs/destroy.turbo_stream.erb
170
176
  - app/views/solid_stack_web/jobs/index.html.erb
171
177
  - app/views/solid_stack_web/jobs/show.html.erb
172
178
  - app/views/solid_stack_web/processes/index.html.erb
173
179
  - app/views/solid_stack_web/queues/index.html.erb
180
+ - app/views/solid_stack_web/queues/show.html.erb
181
+ - app/views/solid_stack_web/recurring_tasks/index.html.erb
182
+ - app/views/solid_stack_web/scheduled_jobs/update.turbo_stream.erb
174
183
  - config/importmap.rb
175
184
  - config/routes.rb
176
185
  - lib/solid_stack_web.rb