flightdeck 0.5.3 → 0.6.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 +4 -4
- data/README.md +8 -199
- metadata +12 -96
- data/MIT-LICENSE +0 -21
- data/app/assets/flightdeck/flightdeck-3967b9e6c506.css +0 -3
- data/app/assets/flightdeck/flightdeck-3aac23f80afe.js +0 -414
- data/app/assets/flightdeck/manifest.json +0 -16
- data/app/controllers/concerns/flightdeck/toasts.rb +0 -35
- data/app/controllers/flightdeck/application_controller.rb +0 -110
- data/app/controllers/flightdeck/assets_controller.rb +0 -39
- data/app/controllers/flightdeck/jobs/actions_controller.rb +0 -166
- data/app/controllers/flightdeck/jobs/discards_controller.rb +0 -43
- data/app/controllers/flightdeck/jobs/retries_controller.rb +0 -22
- data/app/controllers/flightdeck/jobs_controller.rb +0 -39
- data/app/controllers/flightdeck/overview_controller.rb +0 -11
- data/app/controllers/flightdeck/processes_controller.rb +0 -54
- data/app/controllers/flightdeck/queues_controller.rb +0 -56
- data/app/controllers/flightdeck/recurring_tasks_controller.rb +0 -51
- data/app/helpers/flightdeck/application_helper.rb +0 -85
- data/app/helpers/flightdeck/infrastructure_helper.rb +0 -24
- data/app/helpers/flightdeck/jobs_helper.rb +0 -117
- data/app/helpers/flightdeck/metrics_helper.rb +0 -49
- data/app/models/flightdeck/arguments_preview.rb +0 -95
- data/app/models/flightdeck/bulk_action.rb +0 -83
- data/app/models/flightdeck/cache.rb +0 -51
- data/app/models/flightdeck/cron_schedule.rb +0 -102
- data/app/models/flightdeck/duration.rb +0 -23
- data/app/models/flightdeck/error_summary.rb +0 -64
- data/app/models/flightdeck/grouped_failures.rb +0 -24
- data/app/models/flightdeck/job_detail.rb +0 -156
- data/app/models/flightdeck/job_row.rb +0 -103
- data/app/models/flightdeck/jobs_query.rb +0 -233
- data/app/models/flightdeck/metrics/bar_chart.rb +0 -49
- data/app/models/flightdeck/metrics/chart.rb +0 -111
- data/app/models/flightdeck/metrics/line_chart.rb +0 -63
- data/app/models/flightdeck/metrics/series.rb +0 -165
- data/app/models/flightdeck/metrics/sparkline.rb +0 -45
- data/app/models/flightdeck/metrics/time_bucket.rb +0 -95
- data/app/models/flightdeck/overview.rb +0 -210
- data/app/models/flightdeck/process_registry.rb +0 -102
- data/app/models/flightdeck/queue_stats.rb +0 -86
- data/app/models/flightdeck/recurring_catalog.rb +0 -114
- data/app/views/flightdeck/jobs/_failed_row.html.erb +0 -33
- data/app/views/flightdeck/jobs/_filter_bar.html.erb +0 -29
- data/app/views/flightdeck/jobs/_list.html.erb +0 -94
- data/app/views/flightdeck/jobs/_pager.html.erb +0 -22
- data/app/views/flightdeck/jobs/_row.html.erb +0 -23
- data/app/views/flightdeck/jobs/_state_tabs.html.erb +0 -10
- data/app/views/flightdeck/jobs/index.html.erb +0 -10
- data/app/views/flightdeck/jobs/show.html.erb +0 -100
- data/app/views/flightdeck/metrics/_bar_chart.html.erb +0 -34
- data/app/views/flightdeck/metrics/_line_chart.html.erb +0 -38
- data/app/views/flightdeck/metrics/_range_control.html.erb +0 -7
- data/app/views/flightdeck/metrics/_sparkline.html.erb +0 -9
- data/app/views/flightdeck/overview/_completion.html.erb +0 -19
- data/app/views/flightdeck/overview/_failures.html.erb +0 -28
- data/app/views/flightdeck/overview/_fleet.html.erb +0 -21
- data/app/views/flightdeck/overview/_queues.html.erb +0 -41
- data/app/views/flightdeck/overview/_throughput.html.erb +0 -23
- data/app/views/flightdeck/overview/_tiles.html.erb +0 -14
- data/app/views/flightdeck/overview/index.html.erb +0 -29
- data/app/views/flightdeck/processes/_fleet.html.erb +0 -54
- data/app/views/flightdeck/processes/_row.html.erb +0 -28
- data/app/views/flightdeck/processes/index.html.erb +0 -5
- data/app/views/flightdeck/queues/_cards.html.erb +0 -46
- data/app/views/flightdeck/queues/index.html.erb +0 -5
- data/app/views/flightdeck/recurring_tasks/_table.html.erb +0 -53
- data/app/views/flightdeck/recurring_tasks/index.html.erb +0 -5
- data/app/views/flightdeck/shared/_toast.html.erb +0 -7
- data/app/views/flightdeck/shared/refresh.turbo_stream.erb +0 -14
- data/app/views/layouts/flightdeck/application.html.erb +0 -110
- data/config/routes.rb +0 -40
- data/lib/flightdeck/assets.rb +0 -72
- data/lib/flightdeck/configuration.rb +0 -82
- data/lib/flightdeck/engine.rb +0 -31
- data/lib/flightdeck/version.rb +0 -5
- data/lib/flightdeck.rb +0 -31
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Flightdeck
|
|
4
|
-
# The Solid Queue fleet: every registered process, grouped supervisor →
|
|
5
|
-
# children, with heartbeat freshness and claimed counts.
|
|
6
|
-
#
|
|
7
|
-
# Two queries total, regardless of fleet size.
|
|
8
|
-
class ProcessRegistry
|
|
9
|
-
# Freshness is measured against Solid Queue's own liveness threshold rather
|
|
10
|
-
# than a number of our own, so what Flightdeck calls dead is exactly what
|
|
11
|
-
# the pruner will collect.
|
|
12
|
-
FRESH_FRACTION = 0.2
|
|
13
|
-
|
|
14
|
-
Node = Struct.new(:process, :claimed_count, :children, keyword_init: true) do
|
|
15
|
-
def id = process.id
|
|
16
|
-
def kind = process.kind
|
|
17
|
-
def name = process.name
|
|
18
|
-
def hostname = process.hostname
|
|
19
|
-
def pid = process.pid
|
|
20
|
-
def last_heartbeat_at = process.last_heartbeat_at
|
|
21
|
-
def supervisor? = kind == "Supervisor"
|
|
22
|
-
def claims_executions? = kind == "Worker"
|
|
23
|
-
|
|
24
|
-
def heartbeat_age
|
|
25
|
-
Time.current - last_heartbeat_at
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def freshness
|
|
29
|
-
threshold = SolidQueue.process_alive_threshold
|
|
30
|
-
age = heartbeat_age
|
|
31
|
-
|
|
32
|
-
if age >= threshold then :dead
|
|
33
|
-
elsif age >= threshold * FRESH_FRACTION then :stale
|
|
34
|
-
else :fresh
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def dead? = freshness == :dead
|
|
39
|
-
def prunable? = dead?
|
|
40
|
-
|
|
41
|
-
def metadata
|
|
42
|
-
process.metadata.presence || {}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Human summary of what this process was configured to do. Keys differ per
|
|
46
|
-
# process kind, so known ones are formatted and anything else is shown
|
|
47
|
-
# verbatim rather than dropped.
|
|
48
|
-
def config_summary
|
|
49
|
-
parts = []
|
|
50
|
-
data = metadata.symbolize_keys
|
|
51
|
-
|
|
52
|
-
parts << "queues: #{data[:queues]}" if data[:queues].present?
|
|
53
|
-
parts << "#{data[:thread_pool_size]} threads" if data[:thread_pool_size].present?
|
|
54
|
-
parts << "batch #{data[:batch_size]}" if data[:batch_size].present?
|
|
55
|
-
parts << "every #{data[:polling_interval]}s" if data[:polling_interval].present?
|
|
56
|
-
parts << "#{Array(data[:recurring_schedule]).size} recurring tasks" if data[:recurring_schedule].present?
|
|
57
|
-
|
|
58
|
-
known = %i[queues thread_pool_size batch_size polling_interval recurring_schedule
|
|
59
|
-
concurrency_maintenance_interval]
|
|
60
|
-
data.except(*known).each { |key, value| parts << "#{key}: #{value}" }
|
|
61
|
-
|
|
62
|
-
parts.join(" · ")
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def nodes
|
|
67
|
-
@nodes ||= build_nodes
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Supervisors with their children nested; anything unsupervised (or whose
|
|
71
|
-
# supervisor row has gone) is listed flat so it can never be hidden.
|
|
72
|
-
def tree
|
|
73
|
-
@tree ||= begin
|
|
74
|
-
by_supervisor = nodes.group_by { |node| node.process.supervisor_id }
|
|
75
|
-
known_ids = nodes.map(&:id).to_set
|
|
76
|
-
|
|
77
|
-
roots = nodes.select { |node| node.process.supervisor_id.nil? || !known_ids.include?(node.process.supervisor_id) }
|
|
78
|
-
roots.each { |root| root.children = by_supervisor.fetch(root.id, []) }
|
|
79
|
-
roots
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def dead = nodes.select(&:dead?)
|
|
84
|
-
def any_dead? = dead.any?
|
|
85
|
-
def dead_claimed_count = dead.sum(&:claimed_count)
|
|
86
|
-
def total = nodes.size
|
|
87
|
-
|
|
88
|
-
def find(id)
|
|
89
|
-
nodes.find { |node| node.id == id.to_i }
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
def build_nodes
|
|
94
|
-
processes = SolidQueue::Process.order(:kind, :id).to_a
|
|
95
|
-
claimed = SolidQueue::ClaimedExecution.group(:process_id).count
|
|
96
|
-
|
|
97
|
-
processes.map do |process|
|
|
98
|
-
Node.new(process: process, claimed_count: claimed.fetch(process.id, 0), children: [])
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Flightdeck
|
|
4
|
-
# One row per queue, assembled from four grouped queries rather than the
|
|
5
|
-
# per-queue lookups SolidQueue::Queue#size / #latency would do — those are an
|
|
6
|
-
# N+1 as soon as an application has more than a handful of queues.
|
|
7
|
-
#
|
|
8
|
-
# Pausing and resuming still go through SolidQueue::Queue, which owns the
|
|
9
|
-
# semantics; Flightdeck never writes to solid_queue_pauses itself.
|
|
10
|
-
class QueueStats
|
|
11
|
-
RATE_WINDOW = 1.hour
|
|
12
|
-
|
|
13
|
-
Row = Struct.new(:name, :depth, :oldest_ready_at, :paused_at, :completed_in_window,
|
|
14
|
-
keyword_init: true) do
|
|
15
|
-
def paused? = paused_at.present?
|
|
16
|
-
|
|
17
|
-
# Latency only means something when something is actually waiting: an
|
|
18
|
-
# empty queue has no oldest job, and reporting "0s" would imply we
|
|
19
|
-
# measured rather than that there was nothing to measure.
|
|
20
|
-
def latency
|
|
21
|
-
return nil if depth.zero? || oldest_ready_at.nil?
|
|
22
|
-
|
|
23
|
-
Time.current - oldest_ready_at
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def paused_for
|
|
27
|
-
paused_at && Time.current - paused_at
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def rate_per_hour = completed_in_window
|
|
31
|
-
def idle? = depth.zero? && completed_in_window.zero?
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def rows
|
|
35
|
-
@rows ||= names.sort.map do |name|
|
|
36
|
-
Row.new(
|
|
37
|
-
name: name,
|
|
38
|
-
depth: depths.fetch(name, 0),
|
|
39
|
-
oldest_ready_at: oldest_ready.fetch(name, nil),
|
|
40
|
-
paused_at: pauses.fetch(name, nil),
|
|
41
|
-
completed_in_window: completions.fetch(name, 0)
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def find(name)
|
|
47
|
-
rows.find { |row| row.name == name }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def any_paused? = rows.any?(&:paused?)
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
# A queue is worth showing if Solid Queue knows about it, if something is
|
|
54
|
-
# waiting on it, or if it is paused — a paused queue with nothing in it is
|
|
55
|
-
# exactly the one an operator needs to be reminded of.
|
|
56
|
-
def names
|
|
57
|
-
(queue_names + depths.keys + pauses.keys + completions.keys).compact.uniq
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def queue_names
|
|
61
|
-
SolidQueue::Queue.all.map(&:name)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def depths
|
|
65
|
-
@depths ||= SolidQueue::ReadyExecution.group(:queue_name).count
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def oldest_ready
|
|
69
|
-
@oldest_ready ||= SolidQueue::ReadyExecution.group(:queue_name).minimum(:created_at)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def pauses
|
|
73
|
-
@pauses ||= SolidQueue::Pause.pluck(:queue_name, :created_at).to_h
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Bounded by the window rather than by a row cap: a grouped count cannot
|
|
77
|
-
# be capped per group the way a single count can, and an hour of finished
|
|
78
|
-
# jobs is already a bounded, index-backed range scan.
|
|
79
|
-
def completions
|
|
80
|
-
@completions ||= SolidQueue::Job
|
|
81
|
-
.where(finished_at: RATE_WINDOW.ago..)
|
|
82
|
-
.group(:queue_name)
|
|
83
|
-
.count
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Flightdeck
|
|
4
|
-
# The recurring schedule: every task, when it last ran, how that run went, and
|
|
5
|
-
# when it is due next.
|
|
6
|
-
#
|
|
7
|
-
# "Next run" comes from the task's own Fugit-backed API rather than from a
|
|
8
|
-
# calculation of ours, so Flightdeck and the scheduler can never disagree
|
|
9
|
-
# about when something is due.
|
|
10
|
-
class RecurringCatalog
|
|
11
|
-
Row = Struct.new(:task, :last_run_at, :last_status, :last_job_id, keyword_init: true) do
|
|
12
|
-
def id = task.id
|
|
13
|
-
def key = task.key
|
|
14
|
-
def schedule = task.schedule
|
|
15
|
-
def queue_name = task.queue_name
|
|
16
|
-
def priority = task.priority
|
|
17
|
-
def description = task.description
|
|
18
|
-
def static? = task.static?
|
|
19
|
-
|
|
20
|
-
def job_class
|
|
21
|
-
task.class_name.presence || task.command.presence || "(command)"
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def human_schedule
|
|
25
|
-
CronSchedule.humanize(schedule)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Fugit raises on schedules it cannot parse. A task that is on screen but
|
|
29
|
-
# unparseable should show "—" rather than take the page down.
|
|
30
|
-
def next_run_at
|
|
31
|
-
@next_run_at ||= task.next_time
|
|
32
|
-
rescue StandardError
|
|
33
|
-
nil
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def next_run_in
|
|
37
|
-
next_run_at && next_run_at - Time.current
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def failed? = last_status == :failed
|
|
41
|
-
def ever_run? = last_run_at.present?
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def rows
|
|
45
|
-
@rows ||= tasks.map do |task|
|
|
46
|
-
run = last_runs[task.key]
|
|
47
|
-
|
|
48
|
-
Row.new(
|
|
49
|
-
task: task,
|
|
50
|
-
last_run_at: run&.first,
|
|
51
|
-
last_job_id: run&.last,
|
|
52
|
-
last_status: status_for(run&.last)
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def find(id)
|
|
58
|
-
rows.find { |row| row.id == id.to_i }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def any? = rows.any?
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
def tasks
|
|
65
|
-
@tasks ||= SolidQueue::RecurringTask.order(:key).to_a
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def keys = tasks.map(&:key)
|
|
69
|
-
|
|
70
|
-
# [run_at, job_id] of the most recent execution of each task, in two
|
|
71
|
-
# queries: the maximum run_at per key, then the rows that match them.
|
|
72
|
-
def last_runs
|
|
73
|
-
@last_runs ||= compute_last_runs
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def compute_last_runs
|
|
77
|
-
return {} if keys.empty?
|
|
78
|
-
|
|
79
|
-
maxima = SolidQueue::RecurringExecution.where(task_key: keys).group(:task_key).maximum(:run_at)
|
|
80
|
-
return {} if maxima.empty?
|
|
81
|
-
|
|
82
|
-
SolidQueue::RecurringExecution
|
|
83
|
-
.where(task_key: maxima.keys, run_at: maxima.values)
|
|
84
|
-
.pluck(:task_key, :run_at, :job_id)
|
|
85
|
-
.each_with_object({}) do |(task_key, run_at, job_id), found|
|
|
86
|
-
next unless maxima[task_key] == run_at
|
|
87
|
-
|
|
88
|
-
found[task_key] = [ run_at, job_id ]
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def status_for(job_id)
|
|
93
|
-
return nil if job_id.nil?
|
|
94
|
-
# The job has been purged by clear_finished_jobs_after: we know it ran,
|
|
95
|
-
# but not how it went. Say so rather than claim success.
|
|
96
|
-
return :unknown unless surviving_job_ids.include?(job_id)
|
|
97
|
-
|
|
98
|
-
failed_job_ids.include?(job_id) ? :failed : :ok
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def last_job_ids
|
|
102
|
-
@last_job_ids ||= last_runs.values.filter_map(&:last)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def surviving_job_ids
|
|
106
|
-
@surviving_job_ids ||= last_job_ids.empty? ? Set.new : SolidQueue::Job.where(id: last_job_ids).pluck(:id).to_set
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def failed_job_ids
|
|
110
|
-
@failed_job_ids ||=
|
|
111
|
-
last_job_ids.empty? ? Set.new : SolidQueue::FailedExecution.where(job_id: last_job_ids).pluck(:job_id).to_set
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
<tr id="<%= dom_id_for_job(row.id) %>">
|
|
2
|
-
<td>
|
|
3
|
-
<input type="checkbox" name="job_ids[]" value="<%= row.id %>"
|
|
4
|
-
data-selection-target="row" data-action="selection#refresh"
|
|
5
|
-
data-group="<%= group_key %>"
|
|
6
|
-
aria-label="Select <%= row.class_name %> #<%= row.id %>">
|
|
7
|
-
</td>
|
|
8
|
-
<td class="cls">
|
|
9
|
-
<%= link_to row.class_name, job_path(row.id, state: :failed),
|
|
10
|
-
data: { turbo_frame: "_top" } %>
|
|
11
|
-
<div class="args"><%= fd_args_preview(row.args_preview) %></div>
|
|
12
|
-
</td>
|
|
13
|
-
<%# Inside a group the header already names the exception class, so the row
|
|
14
|
-
shows only the message. %>
|
|
15
|
-
<td class="args" style="max-width:260px" title="<%= row.error_summary.to_s %>">
|
|
16
|
-
<%= fd_error_cell(row.error_summary, grouped: group_key.present?) %>
|
|
17
|
-
</td>
|
|
18
|
-
<td><%= fd_queue_badge(row.queue_name) %></td>
|
|
19
|
-
<td class="num"><%= row.attempts || "—" %></td>
|
|
20
|
-
<td class="num dim"><%= row.failed_at ? fd_ago(row.failed_at) : "—" %></td>
|
|
21
|
-
<td class="num">
|
|
22
|
-
<%= link_to "Retry", retry_job_path(row.id, list_filters.merge(state: :failed)),
|
|
23
|
-
class: "fd-btn sm", data: {
|
|
24
|
-
turbo_method: :post,
|
|
25
|
-
turbo_confirm: "Retry #{row.class_name} ##{row.id}?"
|
|
26
|
-
} %>
|
|
27
|
-
<%= link_to "Discard", discard_job_path(row.id, list_filters.merge(state: :failed)),
|
|
28
|
-
class: "fd-btn sm danger", data: {
|
|
29
|
-
turbo_method: :post,
|
|
30
|
-
turbo_confirm: "Permanently discard #{row.class_name} ##{row.id}? This cannot be undone."
|
|
31
|
-
} %>
|
|
32
|
-
</td>
|
|
33
|
-
</tr>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<%= form_with url: jobs_path, method: :get, class: "fd-filterbar", data: { turbo_action: "advance" } do |form| %>
|
|
2
|
-
<%= form.hidden_field :state, value: params[:state], id: "fd-filter-state" %>
|
|
3
|
-
<%= form.search_field :q, value: params[:q],
|
|
4
|
-
placeholder: "Search job class…",
|
|
5
|
-
"aria-label": "Search jobs by class name" %>
|
|
6
|
-
<%= form.text_field :queue_name, value: params[:queue_name],
|
|
7
|
-
placeholder: "Queue", "aria-label": "Filter by queue" %>
|
|
8
|
-
<%= form.submit "Filter", class: "fd-btn" %>
|
|
9
|
-
|
|
10
|
-
<% if query.filters? %>
|
|
11
|
-
<%= link_to jobs_path(state: params[:state].presence), class: "fd-chip" do %>
|
|
12
|
-
clear filters <u>✕</u>
|
|
13
|
-
<% end %>
|
|
14
|
-
<% end %>
|
|
15
|
-
|
|
16
|
-
<% if query.state == :failed %>
|
|
17
|
-
<span class="spacer"></span>
|
|
18
|
-
<%# Grouped so the pair wraps as a unit rather than the second button
|
|
19
|
-
dropping onto a line of its own. %>
|
|
20
|
-
<div class="fd-filterbar-actions">
|
|
21
|
-
<%= button_to "Retry all matching", bulk_retry_jobs_path(list_filters.merge(scope: "all", state: :failed)),
|
|
22
|
-
class: "fd-btn", form_class: "fd-inline-form",
|
|
23
|
-
data: { turbo_confirm: "Retry up to #{number_with_delimiter(Flightdeck.config.bulk_action_limit)} failed jobs matching this filter?" } %>
|
|
24
|
-
<%= button_to "Discard all matching", bulk_discard_jobs_path(list_filters.merge(scope: "all", state: :failed)),
|
|
25
|
-
class: "fd-btn danger", form_class: "fd-inline-form",
|
|
26
|
-
data: { turbo_confirm: "Permanently discard up to #{number_with_delimiter(Flightdeck.config.bulk_action_limit)} failed jobs matching this filter? This cannot be undone." } %>
|
|
27
|
-
</div>
|
|
28
|
-
<% end %>
|
|
29
|
-
<% end %>
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
<% if query.state == :failed %>
|
|
2
|
-
<%= form_with url: bulk_retry_jobs_path(list_filters.merge(state: :failed)), method: :post,
|
|
3
|
-
data: { controller: "selection" } do |form| %>
|
|
4
|
-
<div class="fd-bulkbar" data-selection-target="bar" hidden>
|
|
5
|
-
<span><b data-selection-target="count">0</b> selected</span>
|
|
6
|
-
<button type="submit" class="fd-btn sm primary"
|
|
7
|
-
data-turbo-confirm="Retry the selected jobs?">Retry selected</button>
|
|
8
|
-
<button type="submit" class="fd-btn sm danger"
|
|
9
|
-
formaction="<%= bulk_discard_jobs_path(list_filters.merge(state: :failed)) %>"
|
|
10
|
-
data-turbo-confirm="Permanently discard the selected jobs? This cannot be undone.">Discard selected</button>
|
|
11
|
-
<span class="spacer"></span>
|
|
12
|
-
<%# Each "all matching" button names its own action: a single neutral
|
|
13
|
-
"apply to all" button inherited the form's action and silently
|
|
14
|
-
retried when the user meant to discard. %>
|
|
15
|
-
<span class="fd-bulkbar-all">or all <%= fd_count(query.count, capped: query.count_capped?) %> matching:</span>
|
|
16
|
-
<button type="submit" name="scope" value="all" class="fd-btn sm primary"
|
|
17
|
-
data-turbo-confirm="Retry all failed jobs matching the current filter?">Retry all</button>
|
|
18
|
-
<button type="submit" name="scope" value="all" class="fd-btn sm danger"
|
|
19
|
-
formaction="<%= bulk_discard_jobs_path(list_filters.merge(state: :failed)) %>"
|
|
20
|
-
data-turbo-confirm="Permanently discard all failed jobs matching the current filter? This cannot be undone.">Discard all</button>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="fd-card">
|
|
24
|
-
<div class="fd-scroll-x">
|
|
25
|
-
<table class="fd-data">
|
|
26
|
-
<thead>
|
|
27
|
-
<tr>
|
|
28
|
-
<th style="width:34px">
|
|
29
|
-
<input type="checkbox" data-selection-target="all" data-action="selection#toggleAll"
|
|
30
|
-
aria-label="Select all failed jobs on this page">
|
|
31
|
-
</th>
|
|
32
|
-
<th>Job</th>
|
|
33
|
-
<th>Error</th>
|
|
34
|
-
<th>Queue</th>
|
|
35
|
-
<th class="num">Attempts</th>
|
|
36
|
-
<th class="num">Failed</th>
|
|
37
|
-
<th style="width:150px"></th>
|
|
38
|
-
</tr>
|
|
39
|
-
</thead>
|
|
40
|
-
<tbody>
|
|
41
|
-
<% if rows.empty? %>
|
|
42
|
-
<tr><td colspan="7"><div class="fd-empty">No failed jobs<%= " match this filter" if query.filters? %>.</div></td></tr>
|
|
43
|
-
<% elsif groups.present? %>
|
|
44
|
-
<% groups.each do |group| %>
|
|
45
|
-
<tr class="fd-group-row">
|
|
46
|
-
<td colspan="7">
|
|
47
|
-
<%= group.key.upcase %> · <%= pluralize(group.count, "job") %> on this page
|
|
48
|
-
<% if group.last_failed_at %>· last <%= fd_ago(group.last_failed_at) %><% end %>
|
|
49
|
-
·
|
|
50
|
-
<button type="button" class="fd-linkish"
|
|
51
|
-
data-action="selection#selectGroup"
|
|
52
|
-
data-selection-group-param="<%= group.key %>">select group</button>
|
|
53
|
-
</td>
|
|
54
|
-
</tr>
|
|
55
|
-
<%= render partial: "flightdeck/jobs/failed_row", collection: group.rows, as: :row,
|
|
56
|
-
locals: { group_key: group.key } %>
|
|
57
|
-
<% end %>
|
|
58
|
-
<% else %>
|
|
59
|
-
<%= render partial: "flightdeck/jobs/failed_row", collection: rows, as: :row,
|
|
60
|
-
locals: { group_key: nil } %>
|
|
61
|
-
<% end %>
|
|
62
|
-
</tbody>
|
|
63
|
-
</table>
|
|
64
|
-
</div>
|
|
65
|
-
<%= render "flightdeck/jobs/pager", query: query, noun: "failed job" %>
|
|
66
|
-
</div>
|
|
67
|
-
<% end %>
|
|
68
|
-
<% else %>
|
|
69
|
-
<div class="fd-card">
|
|
70
|
-
<div class="fd-scroll-x">
|
|
71
|
-
<table class="fd-data">
|
|
72
|
-
<thead>
|
|
73
|
-
<tr>
|
|
74
|
-
<th>Job</th>
|
|
75
|
-
<th>Queue</th>
|
|
76
|
-
<th>State</th>
|
|
77
|
-
<th class="num">Priority</th>
|
|
78
|
-
<th class="num">Enqueued</th>
|
|
79
|
-
<th class="num">Progress</th>
|
|
80
|
-
<th style="width:90px"></th>
|
|
81
|
-
</tr>
|
|
82
|
-
</thead>
|
|
83
|
-
<tbody>
|
|
84
|
-
<% if rows.empty? %>
|
|
85
|
-
<tr><td colspan="7"><div class="fd-empty">No jobs<%= " match this filter" if query.filters? %>.</div></td></tr>
|
|
86
|
-
<% else %>
|
|
87
|
-
<%= render partial: "flightdeck/jobs/row", collection: rows, as: :row %>
|
|
88
|
-
<% end %>
|
|
89
|
-
</tbody>
|
|
90
|
-
</table>
|
|
91
|
-
</div>
|
|
92
|
-
<%= render "flightdeck/jobs/pager", query: query, noun: "job" %>
|
|
93
|
-
</div>
|
|
94
|
-
<% end %>
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
<div class="fd-pager">
|
|
2
|
-
<span>
|
|
3
|
-
<%= fd_count(query.count, capped: query.count_capped?) %> <%= noun.pluralize %><%
|
|
4
|
-
%><% if query.count_capped? %> · counts capped at <%= number_with_delimiter(query.count_cap) %><% end %>
|
|
5
|
-
</span>
|
|
6
|
-
<div class="right">
|
|
7
|
-
<% if params[:before_id].present? %>
|
|
8
|
-
<%= link_to "‹ Newest", jobs_path(list_filters.merge(state: params[:state].presence).compact),
|
|
9
|
-
class: "fd-btn sm" %>
|
|
10
|
-
<% else %>
|
|
11
|
-
<span class="fd-btn sm disabled" aria-disabled="true">‹ Newest</span>
|
|
12
|
-
<% end %>
|
|
13
|
-
|
|
14
|
-
<% if query.next_page? %>
|
|
15
|
-
<%= link_to "Older ›",
|
|
16
|
-
jobs_path(list_filters.merge(state: params[:state].presence, before_id: query.next_cursor).compact),
|
|
17
|
-
class: "fd-btn sm" %>
|
|
18
|
-
<% else %>
|
|
19
|
-
<span class="fd-btn sm disabled" aria-disabled="true">Older ›</span>
|
|
20
|
-
<% end %>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<tr id="<%= dom_id_for_job(row.id) %>">
|
|
2
|
-
<td class="cls">
|
|
3
|
-
<%= link_to row.class_name, job_path(row.id, state: params[:state].presence),
|
|
4
|
-
data: { turbo_frame: "_top" } %>
|
|
5
|
-
<div class="args"><%= fd_args_preview(row.args_preview) %></div>
|
|
6
|
-
</td>
|
|
7
|
-
<td><%= fd_queue_badge(row.queue_name) %></td>
|
|
8
|
-
<td><%= fd_state_pill(row.state) %></td>
|
|
9
|
-
<td class="num"><%= row.priority %></td>
|
|
10
|
-
<td class="num dim"><%= fd_time(row.enqueued_at) %></td>
|
|
11
|
-
<td class="num dim"><%= fd_progress_label(row) %></td>
|
|
12
|
-
<td class="num">
|
|
13
|
-
<% if row.failed? %>
|
|
14
|
-
<%= button_to "Retry", retry_job_path(row.id, list_filters.merge(state: :failed)),
|
|
15
|
-
class: "fd-btn sm", form_class: "fd-inline-form",
|
|
16
|
-
data: { turbo_confirm: "Retry #{row.class_name} ##{row.id}?" } %>
|
|
17
|
-
<% elsif row.discardable? %>
|
|
18
|
-
<%= button_to "Discard", discard_job_path(row.id, list_filters.merge(state: row.state)),
|
|
19
|
-
class: "fd-btn sm danger", form_class: "fd-inline-form",
|
|
20
|
-
data: { turbo_confirm: "Permanently discard #{row.class_name} ##{row.id}? This cannot be undone." } %>
|
|
21
|
-
<% end %>
|
|
22
|
-
</td>
|
|
23
|
-
</tr>
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<div class="fd-statetabs">
|
|
2
|
-
<% Flightdeck::JobsQuery::STATES.each do |state| %>
|
|
3
|
-
<% counts = state_counts[state] %>
|
|
4
|
-
<%= link_to jobs_path(list_filters.merge(state: state == :all ? nil : state).compact),
|
|
5
|
-
class: ("on" if @query.state == state) do %>
|
|
6
|
-
<%= fd_state_label(state) %>
|
|
7
|
-
<span class="ct"><%= fd_count(counts[:count], capped: counts[:capped]) %></span>
|
|
8
|
-
<% end %>
|
|
9
|
-
<% end %>
|
|
10
|
-
</div>
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<% content_for :page_title, "Jobs" %>
|
|
2
|
-
|
|
3
|
-
<%= render "flightdeck/jobs/state_tabs", state_counts: @state_counts %>
|
|
4
|
-
<%= render "flightdeck/jobs/filter_bar", query: @query %>
|
|
5
|
-
|
|
6
|
-
<%# The frame re-requests the URL it is showing, so state, filters and cursor
|
|
7
|
-
all survive a poll. %>
|
|
8
|
-
<%= fd_refresh_frame "fd-jobs", url: fd_current_list_url, target: nil do %>
|
|
9
|
-
<%= render "flightdeck/jobs/list", query: @query, rows: @rows, groups: @groups %>
|
|
10
|
-
<% end %>
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
<% content_for :page_title, "Job ##{@job.id}" %>
|
|
2
|
-
|
|
3
|
-
<div class="fd-jd-head">
|
|
4
|
-
<div class="t">
|
|
5
|
-
<h2><%= @job.class_name %></h2>
|
|
6
|
-
<div class="sub">
|
|
7
|
-
job #<%= number_with_delimiter(@job.id) %>
|
|
8
|
-
<% if @job.active_job_id.present? %>· active_job <%= @job.active_job_id %><% end %>
|
|
9
|
-
</div>
|
|
10
|
-
</div>
|
|
11
|
-
<%= fd_state_pill(@job.state) %>
|
|
12
|
-
<div class="actions">
|
|
13
|
-
<% if @job.retryable? %>
|
|
14
|
-
<%= button_to "↻ Retry now", retry_job_path(@job.id), class: "fd-btn primary", form_class: "fd-inline-form",
|
|
15
|
-
data: { turbo_confirm: "Retry #{@job.class_name} ##{@job.id}?" } %>
|
|
16
|
-
<% end %>
|
|
17
|
-
<% if @job.discardable? %>
|
|
18
|
-
<%= button_to "Discard", discard_job_path(@job.id, state: @job.state), class: "fd-btn danger",
|
|
19
|
-
form_class: "fd-inline-form",
|
|
20
|
-
data: { turbo_confirm: "Permanently discard #{@job.class_name} ##{@job.id}? This cannot be undone." } %>
|
|
21
|
-
<% end %>
|
|
22
|
-
<%= link_to "Back to jobs", jobs_path(state: params[:state].presence), class: "fd-btn" %>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<div class="fd-grid-jd">
|
|
27
|
-
<div class="fd-stack">
|
|
28
|
-
<% if @job.failed? %>
|
|
29
|
-
<div class="fd-card fd-error-box">
|
|
30
|
-
<div class="fd-card-hd">
|
|
31
|
-
<h3>Error</h3>
|
|
32
|
-
<span class="sub">failed <%= fd_full_time(@job.execution&.created_at) %></span>
|
|
33
|
-
</div>
|
|
34
|
-
<div class="msg">
|
|
35
|
-
<b><%= @job.error_class.presence || "Unknown error" %>:</b>
|
|
36
|
-
<%= @job.error_message.presence || "(no message recorded)" %>
|
|
37
|
-
</div>
|
|
38
|
-
<% if @job.backtrace.any? %>
|
|
39
|
-
<details class="fd-bt" open>
|
|
40
|
-
<summary>
|
|
41
|
-
Backtrace · <%= pluralize(@job.backtrace_total, "frame") %><%
|
|
42
|
-
%><% if @job.backtrace_truncated? %> (showing <%= @job.backtrace.size %>)<% end %> · app frames highlighted
|
|
43
|
-
</summary>
|
|
44
|
-
<pre class="fd-code"><% @job.backtrace_frames.each do |frame| %><%
|
|
45
|
-
%><% if frame[:app] %><span class="app-line"><%= frame[:line] %></span><% else %><%= frame[:line] %><% end %>
|
|
46
|
-
<% end %></pre>
|
|
47
|
-
</details>
|
|
48
|
-
<% end %>
|
|
49
|
-
</div>
|
|
50
|
-
<% end %>
|
|
51
|
-
|
|
52
|
-
<div class="fd-card">
|
|
53
|
-
<div class="fd-card-hd">
|
|
54
|
-
<h3>Arguments</h3>
|
|
55
|
-
<div class="right sub">
|
|
56
|
-
<%= number_to_human_size(@job.arguments_bytes) %>
|
|
57
|
-
<% if @job.arguments_truncated? %>· truncated for display<% end %>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
<pre class="fd-code"><%= @job.arguments_preview %></pre>
|
|
61
|
-
<% if @job.arguments_truncated? %>
|
|
62
|
-
<div class="fd-card-bd">
|
|
63
|
-
<p class="fd-mono">Showing the first <%= number_to_human_size(Flightdeck::JobDetail::ARGUMENTS_DISPLAY_LIMIT) %>
|
|
64
|
-
of <%= number_to_human_size(@job.arguments_bytes) %>. Arguments are shown exactly as stored and are never deserialized.</p>
|
|
65
|
-
</div>
|
|
66
|
-
<% end %>
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
<div class="fd-card">
|
|
70
|
-
<div class="fd-card-hd"><h3>Metadata</h3></div>
|
|
71
|
-
<div class="fd-meta-grid">
|
|
72
|
-
<div><div class="k">Queue</div><div class="v"><%= fd_queue_badge(@job.queue_name) %></div></div>
|
|
73
|
-
<div><div class="k">Priority</div><div class="v"><%= @job.priority %></div></div>
|
|
74
|
-
<div><div class="k">Enqueued</div><div class="v"><%= fd_full_time(@job.enqueued_at) %></div></div>
|
|
75
|
-
<div><div class="k">Scheduled</div><div class="v"><%= @job.scheduled_at ? fd_full_time(@job.scheduled_at) : "—" %></div></div>
|
|
76
|
-
<div><div class="k">Finished</div><div class="v"><%= @job.finished_at ? fd_full_time(@job.finished_at) : "—" %></div></div>
|
|
77
|
-
<div><div class="k">Active job ID</div><div class="v mono"><%= @job.active_job_id.presence || "—" %></div></div>
|
|
78
|
-
<div><div class="k">Concurrency key</div><div class="v mono"><%= @job.concurrency_key.presence || "—" %></div></div>
|
|
79
|
-
<div><div class="k">Worker</div><div class="v mono"><%= @job.process_label || "—" %></div></div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
<div class="fd-card">
|
|
85
|
-
<div class="fd-card-hd"><h3>Timeline</h3></div>
|
|
86
|
-
<div class="fd-timeline">
|
|
87
|
-
<% @job.timeline.each do |event| %>
|
|
88
|
-
<div class="fd-tl-item <%= event.status %>">
|
|
89
|
-
<div class="fd-tl-dot"></div>
|
|
90
|
-
<div class="fd-tl-body">
|
|
91
|
-
<div class="t"><%= event.label %></div>
|
|
92
|
-
<div class="d">
|
|
93
|
-
<%= event.at ? fd_full_time(event.at) : "—" %><% if event.detail.present? %> · <%= event.detail %><% end %>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
<% end %>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<svg class="fd-chart" viewBox="0 0 <%= chart.width %> <%= chart.height %>"
|
|
2
|
-
preserveAspectRatio="xMidYMid meet" role="img"
|
|
3
|
-
aria-label="Jobs finished <%= chart.window.subtitle %>">
|
|
4
|
-
<% chart.gridlines.each do |line| %>
|
|
5
|
-
<line x1="<%= chart.plot_left %>" x2="<%= chart.plot_right %>"
|
|
6
|
-
y1="<%= line[:y] %>" y2="<%= line[:y] %>" class="fd-chart-grid"/>
|
|
7
|
-
<text x="<%= chart.plot_left - 6 %>" y="<%= line[:y] + 3 %>" class="fd-chart-label" text-anchor="end"><%= line[:label] %></text>
|
|
8
|
-
<% end %>
|
|
9
|
-
|
|
10
|
-
<% chart.bars.each do |bar| %>
|
|
11
|
-
<g>
|
|
12
|
-
<title><%= bar[:title] %></title>
|
|
13
|
-
<%# A transparent full-height rect gives every bucket — including empty
|
|
14
|
-
ones — a hover target for its native tooltip. %>
|
|
15
|
-
<rect x="<%= bar[:x] %>" y="<%= chart.plot_top %>" width="<%= bar[:width] %>"
|
|
16
|
-
height="<%= chart.plot_height %>" fill="transparent"/>
|
|
17
|
-
<% if bar[:succeeded][:height].positive? %>
|
|
18
|
-
<rect x="<%= bar[:x] %>" y="<%= bar[:succeeded][:y] %>" width="<%= bar[:width] %>"
|
|
19
|
-
height="<%= bar[:succeeded][:height] %>" rx="<%= bar[:corner] %>" class="fd-chart-bar-succeeded"/>
|
|
20
|
-
<% end %>
|
|
21
|
-
<% if bar[:failed][:height].positive? %>
|
|
22
|
-
<rect x="<%= bar[:x] %>" y="<%= bar[:failed][:y] %>" width="<%= bar[:width] %>"
|
|
23
|
-
height="<%= bar[:failed][:height] %>" rx="<%= bar[:corner] %>" class="fd-chart-bar-failed"/>
|
|
24
|
-
<% end %>
|
|
25
|
-
</g>
|
|
26
|
-
<% end %>
|
|
27
|
-
|
|
28
|
-
<line x1="<%= chart.plot_left %>" x2="<%= chart.plot_right %>"
|
|
29
|
-
y1="<%= chart.plot_bottom %>" y2="<%= chart.plot_bottom %>" class="fd-chart-axis"/>
|
|
30
|
-
|
|
31
|
-
<% chart.x_ticks.each do |tick| %>
|
|
32
|
-
<text x="<%= tick[:x] %>" y="<%= chart.height - 6 %>" class="fd-chart-label" text-anchor="middle"><%= tick[:label] %></text>
|
|
33
|
-
<% end %>
|
|
34
|
-
</svg>
|