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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -199
  3. metadata +12 -96
  4. data/MIT-LICENSE +0 -21
  5. data/app/assets/flightdeck/flightdeck-3967b9e6c506.css +0 -3
  6. data/app/assets/flightdeck/flightdeck-3aac23f80afe.js +0 -414
  7. data/app/assets/flightdeck/manifest.json +0 -16
  8. data/app/controllers/concerns/flightdeck/toasts.rb +0 -35
  9. data/app/controllers/flightdeck/application_controller.rb +0 -110
  10. data/app/controllers/flightdeck/assets_controller.rb +0 -39
  11. data/app/controllers/flightdeck/jobs/actions_controller.rb +0 -166
  12. data/app/controllers/flightdeck/jobs/discards_controller.rb +0 -43
  13. data/app/controllers/flightdeck/jobs/retries_controller.rb +0 -22
  14. data/app/controllers/flightdeck/jobs_controller.rb +0 -39
  15. data/app/controllers/flightdeck/overview_controller.rb +0 -11
  16. data/app/controllers/flightdeck/processes_controller.rb +0 -54
  17. data/app/controllers/flightdeck/queues_controller.rb +0 -56
  18. data/app/controllers/flightdeck/recurring_tasks_controller.rb +0 -51
  19. data/app/helpers/flightdeck/application_helper.rb +0 -85
  20. data/app/helpers/flightdeck/infrastructure_helper.rb +0 -24
  21. data/app/helpers/flightdeck/jobs_helper.rb +0 -117
  22. data/app/helpers/flightdeck/metrics_helper.rb +0 -49
  23. data/app/models/flightdeck/arguments_preview.rb +0 -95
  24. data/app/models/flightdeck/bulk_action.rb +0 -83
  25. data/app/models/flightdeck/cache.rb +0 -51
  26. data/app/models/flightdeck/cron_schedule.rb +0 -102
  27. data/app/models/flightdeck/duration.rb +0 -23
  28. data/app/models/flightdeck/error_summary.rb +0 -64
  29. data/app/models/flightdeck/grouped_failures.rb +0 -24
  30. data/app/models/flightdeck/job_detail.rb +0 -156
  31. data/app/models/flightdeck/job_row.rb +0 -103
  32. data/app/models/flightdeck/jobs_query.rb +0 -233
  33. data/app/models/flightdeck/metrics/bar_chart.rb +0 -49
  34. data/app/models/flightdeck/metrics/chart.rb +0 -111
  35. data/app/models/flightdeck/metrics/line_chart.rb +0 -63
  36. data/app/models/flightdeck/metrics/series.rb +0 -165
  37. data/app/models/flightdeck/metrics/sparkline.rb +0 -45
  38. data/app/models/flightdeck/metrics/time_bucket.rb +0 -95
  39. data/app/models/flightdeck/overview.rb +0 -210
  40. data/app/models/flightdeck/process_registry.rb +0 -102
  41. data/app/models/flightdeck/queue_stats.rb +0 -86
  42. data/app/models/flightdeck/recurring_catalog.rb +0 -114
  43. data/app/views/flightdeck/jobs/_failed_row.html.erb +0 -33
  44. data/app/views/flightdeck/jobs/_filter_bar.html.erb +0 -29
  45. data/app/views/flightdeck/jobs/_list.html.erb +0 -94
  46. data/app/views/flightdeck/jobs/_pager.html.erb +0 -22
  47. data/app/views/flightdeck/jobs/_row.html.erb +0 -23
  48. data/app/views/flightdeck/jobs/_state_tabs.html.erb +0 -10
  49. data/app/views/flightdeck/jobs/index.html.erb +0 -10
  50. data/app/views/flightdeck/jobs/show.html.erb +0 -100
  51. data/app/views/flightdeck/metrics/_bar_chart.html.erb +0 -34
  52. data/app/views/flightdeck/metrics/_line_chart.html.erb +0 -38
  53. data/app/views/flightdeck/metrics/_range_control.html.erb +0 -7
  54. data/app/views/flightdeck/metrics/_sparkline.html.erb +0 -9
  55. data/app/views/flightdeck/overview/_completion.html.erb +0 -19
  56. data/app/views/flightdeck/overview/_failures.html.erb +0 -28
  57. data/app/views/flightdeck/overview/_fleet.html.erb +0 -21
  58. data/app/views/flightdeck/overview/_queues.html.erb +0 -41
  59. data/app/views/flightdeck/overview/_throughput.html.erb +0 -23
  60. data/app/views/flightdeck/overview/_tiles.html.erb +0 -14
  61. data/app/views/flightdeck/overview/index.html.erb +0 -29
  62. data/app/views/flightdeck/processes/_fleet.html.erb +0 -54
  63. data/app/views/flightdeck/processes/_row.html.erb +0 -28
  64. data/app/views/flightdeck/processes/index.html.erb +0 -5
  65. data/app/views/flightdeck/queues/_cards.html.erb +0 -46
  66. data/app/views/flightdeck/queues/index.html.erb +0 -5
  67. data/app/views/flightdeck/recurring_tasks/_table.html.erb +0 -53
  68. data/app/views/flightdeck/recurring_tasks/index.html.erb +0 -5
  69. data/app/views/flightdeck/shared/_toast.html.erb +0 -7
  70. data/app/views/flightdeck/shared/refresh.turbo_stream.erb +0 -14
  71. data/app/views/layouts/flightdeck/application.html.erb +0 -110
  72. data/config/routes.rb +0 -40
  73. data/lib/flightdeck/assets.rb +0 -72
  74. data/lib/flightdeck/configuration.rb +0 -82
  75. data/lib/flightdeck/engine.rb +0 -31
  76. data/lib/flightdeck/version.rb +0 -5
  77. 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>&#10005;</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 %> &middot; <%= pluralize(group.count, "job") %> on this page
48
- <% if group.last_failed_at %>&middot; last <%= fd_ago(group.last_failed_at) %><% end %>
49
- &nbsp;&middot;&nbsp;
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>