active_job_dash 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/MIT-LICENSE +21 -0
- data/README.md +81 -0
- data/app/controllers/active_job_dash/application_controller.rb +15 -0
- data/app/controllers/active_job_dash/dashboard_controller.rb +54 -0
- data/app/jobs/active_job_dash/record_execution_job.rb +9 -0
- data/app/models/active_job_dash/job_execution.rb +51 -0
- data/app/models/active_job_dash/record.rb +5 -0
- data/app/views/active_job_dash/dashboard/index.html.erb +126 -0
- data/app/views/active_job_dash/dashboard/jobs.html.erb +54 -0
- data/app/views/active_job_dash/dashboard/show.html.erb +77 -0
- data/app/views/layouts/active_job_dash/application.html.erb +207 -0
- data/config/routes.rb +10 -0
- data/lib/active_job_dash/engine.rb +44 -0
- data/lib/active_job_dash/generators/active_job_dash/install_generator.rb +49 -0
- data/lib/active_job_dash/generators/active_job_dash/templates/create_active_job_dash_executions.rb +29 -0
- data/lib/active_job_dash/generators/active_job_dash/templates/initializer.rb.erb +6 -0
- data/lib/active_job_dash/instrumentation.rb +214 -0
- data/lib/active_job_dash/log_subscriber.rb +24 -0
- data/lib/active_job_dash/stats.rb +103 -0
- data/lib/active_job_dash/tasks.rb +31 -0
- data/lib/active_job_dash/version.rb +3 -0
- data/lib/active_job_dash.rb +46 -0
- metadata +170 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1d5c1ebe48cc6f8dfa5d7c04abb8dd8339593785697e354095675f07e12ff1b8
|
|
4
|
+
data.tar.gz: 7d6a81b1d9d4e90a79a2df8b97d5c3ea9d319e8fbf68d37bf601cf90e0e01222
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5e43b17afd3711f2f777c846056e18e488c2b37321e0bcda570bce71fe00f58996cafba28807c61c9d206c98c182451adb63bd01043a87e15dd0ee5bd3b3ca96
|
|
7
|
+
data.tar.gz: 7a2fd2f938e1287199d0cf046c65039246fc0a72530088d1885b57d9990bf3f276cf8f4bd3673d9f460d316c5c655e31e3e1933ef6cfa077752088505893b0ff
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-01-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release
|
|
13
|
+
- Job execution tracking for ActiveJob (perform, enqueue, retry, discard events)
|
|
14
|
+
- Dashboard UI at `/admin/jobs` with job list, stats, and detail views
|
|
15
|
+
- Success/failure rate metrics and duration tracking
|
|
16
|
+
- Retry failed jobs from the dashboard
|
|
17
|
+
- Filter by job class, queue, and status
|
|
18
|
+
- Multi-region support with `REGION` environment variable
|
|
19
|
+
- Configurable retention period for job history
|
|
20
|
+
- Option to exclude specific jobs from tracking
|
|
21
|
+
- Dashboard authentication hook
|
|
22
|
+
- Programmatic access to stats via `ActiveJobDash.stats`
|
|
23
|
+
- Rails generator for installation (`rails generate active_job_dash:install`)
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BoringCache
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# ActiveJobDash
|
|
2
|
+
|
|
3
|
+
A dashboard and observability gem for ActiveJob. Monitor, track, and retry job executions with a built-in UI.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby >= 3.2
|
|
8
|
+
- Rails >= 7.2
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Track all job executions (perform, enqueue, retry, discard)
|
|
13
|
+
- View success/failure rates, duration metrics, and trends
|
|
14
|
+
- Retry failed jobs from the UI
|
|
15
|
+
- Filter by job class, queue, status
|
|
16
|
+
- Multi-region support
|
|
17
|
+
- Zero configuration required for basic usage
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Add to your Gemfile:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'active_job_dash'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then install:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bundle install
|
|
31
|
+
rails generate active_job_dash:install
|
|
32
|
+
rails db:migrate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Visit `/admin/jobs` to see the dashboard.
|
|
38
|
+
|
|
39
|
+
### Configuration
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# config/initializers/active_job_dash.rb
|
|
43
|
+
ActiveJobDash.configure do |config|
|
|
44
|
+
config.enabled = true
|
|
45
|
+
config.store_arguments = true
|
|
46
|
+
config.retention_days = 30
|
|
47
|
+
config.region = ENV['REGION']
|
|
48
|
+
|
|
49
|
+
# Exclude jobs from tracking
|
|
50
|
+
config.excluded_jobs = ['SomeNoisyJob']
|
|
51
|
+
|
|
52
|
+
# Dashboard authentication
|
|
53
|
+
config.dashboard_auth = -> {
|
|
54
|
+
authenticate_user!
|
|
55
|
+
redirect_to root_path unless current_user.admin?
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Programmatic Access
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# Get stats for the last hour
|
|
64
|
+
stats = ActiveJobDash.stats(period: 1.hour)
|
|
65
|
+
stats.summary
|
|
66
|
+
# => { total: 1523, success: 1500, failed: 23, avg_duration_ms: 45.2, failure_rate: 1.51 }
|
|
67
|
+
|
|
68
|
+
# Stats by job class
|
|
69
|
+
stats.by_job_class
|
|
70
|
+
|
|
71
|
+
# Recent failures
|
|
72
|
+
stats.recent_failures(limit: 10)
|
|
73
|
+
|
|
74
|
+
# Retry a specific job
|
|
75
|
+
execution = ActiveJobDash::JobExecution.find(id)
|
|
76
|
+
execution.retry!
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module ActiveJobDash
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
protect_from_forgery with: :exception
|
|
4
|
+
|
|
5
|
+
before_action :authenticate!
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def authenticate!
|
|
10
|
+
return unless ActiveJobDash.configuration.dashboard_auth
|
|
11
|
+
|
|
12
|
+
instance_exec(&ActiveJobDash.configuration.dashboard_auth)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module ActiveJobDash
|
|
2
|
+
class DashboardController < ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
@period = params[:period]&.to_i&.hours || 1.hour
|
|
5
|
+
@stats = ActiveJobDash.stats(period: @period)
|
|
6
|
+
@summary = @stats.summary
|
|
7
|
+
@by_job_class = @stats.by_job_class
|
|
8
|
+
@by_queue = @stats.by_queue
|
|
9
|
+
@recent_failures = @stats.recent_failures(limit: 10)
|
|
10
|
+
@timeline = @stats.timeline
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def jobs
|
|
14
|
+
@job_class = params[:job_class]
|
|
15
|
+
@status = params[:status]
|
|
16
|
+
@period = params[:period]&.to_i&.hours || 24.hours
|
|
17
|
+
|
|
18
|
+
@executions = JobExecution.where(created_at: @period.ago..)
|
|
19
|
+
@executions = @executions.where(job_class: @job_class) if @job_class.present?
|
|
20
|
+
@executions = @executions.where(status: @status) if @status.present?
|
|
21
|
+
@executions = @executions.order(created_at: :desc).limit(100)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def show
|
|
25
|
+
@execution = JobExecution.find(params[:id])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def retry
|
|
29
|
+
@execution = JobExecution.find(params[:id])
|
|
30
|
+
@execution.retry!
|
|
31
|
+
|
|
32
|
+
redirect_to dashboard_path, notice: "Job #{@execution.job_class} re-enqueued"
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
redirect_to dashboard_path, alert: "Failed to retry: #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def retry_all
|
|
38
|
+
count = 0
|
|
39
|
+
JobExecution.pending_retry.where(created_at: 24.hours.ago..).find_each do |execution|
|
|
40
|
+
execution.retry!
|
|
41
|
+
count += 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
redirect_to dashboard_path, notice: "Re-enqueued #{count} jobs"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def purge
|
|
48
|
+
days = params[:days]&.to_i || ActiveJobDash.configuration.retention_days
|
|
49
|
+
deleted = JobExecution.where(created_at: ...days.days.ago).delete_all
|
|
50
|
+
|
|
51
|
+
redirect_to dashboard_path, notice: "Purged #{deleted} records older than #{days} days"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module ActiveJobDash
|
|
2
|
+
class JobExecution < Record
|
|
3
|
+
self.table_name = "active_job_dash_executions"
|
|
4
|
+
|
|
5
|
+
validates :job_class, presence: true
|
|
6
|
+
validates :job_id, presence: true
|
|
7
|
+
validates :event_type, presence: true
|
|
8
|
+
|
|
9
|
+
scope :successful, -> { where(status: "success") }
|
|
10
|
+
scope :failed, -> { where(status: "failed") }
|
|
11
|
+
scope :pending_retry, -> { where(status: %w[failed exhausted]).where.not(arguments: nil) }
|
|
12
|
+
scope :performs, -> { where(event_type: "perform") }
|
|
13
|
+
scope :enqueues, -> { where(event_type: "enqueue") }
|
|
14
|
+
|
|
15
|
+
def retry!
|
|
16
|
+
return unless arguments.present?
|
|
17
|
+
return unless %w[failed exhausted discarded].include?(status)
|
|
18
|
+
|
|
19
|
+
job_class.constantize.perform_later(*parsed_arguments)
|
|
20
|
+
update!(status: "retried", retried_at: Time.current)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parsed_arguments
|
|
24
|
+
return [] if arguments.blank?
|
|
25
|
+
|
|
26
|
+
JSON.parse(arguments)
|
|
27
|
+
rescue JSON::ParserError
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failed?
|
|
32
|
+
status == "failed"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def successful?
|
|
36
|
+
status == "success"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def duration_human
|
|
40
|
+
return nil unless duration_ms
|
|
41
|
+
|
|
42
|
+
if duration_ms < 1000
|
|
43
|
+
"#{duration_ms}ms"
|
|
44
|
+
elsif duration_ms < 60_000
|
|
45
|
+
"#{(duration_ms / 1000.0).round(2)}s"
|
|
46
|
+
else
|
|
47
|
+
"#{(duration_ms / 60_000.0).round(2)}m"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<div class="stats-grid">
|
|
2
|
+
<div class="stat-card">
|
|
3
|
+
<div class="stat-value"><%= number_with_delimiter(@summary[:total]) %></div>
|
|
4
|
+
<div class="stat-label">Total Jobs</div>
|
|
5
|
+
</div>
|
|
6
|
+
<% if (@summary[:queued] || 0) > 0 || (@summary[:running] || 0) > 0 %>
|
|
7
|
+
<div class="stat-card">
|
|
8
|
+
<div class="stat-value"><%= @summary[:queued] || 0 %> / <%= @summary[:running] || 0 %></div>
|
|
9
|
+
<div class="stat-label">Queued / Running</div>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
<div class="stat-card success">
|
|
13
|
+
<div class="stat-value"><%= number_with_delimiter(@summary[:success]) %></div>
|
|
14
|
+
<div class="stat-label">Successful</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="stat-card error">
|
|
17
|
+
<div class="stat-value"><%= number_with_delimiter(@summary[:failed]) %></div>
|
|
18
|
+
<div class="stat-label">Failed</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="stat-card">
|
|
21
|
+
<div class="stat-value"><%= (@summary[:avg_duration_ms] || 0).round %>ms</div>
|
|
22
|
+
<div class="stat-label">Avg Duration</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="stat-card <%= (@summary[:failure_rate] || 0) > 5 ? 'error' : '' %>">
|
|
25
|
+
<div class="stat-value"><%= number_to_percentage(@summary[:failure_rate] || 0, precision: 1) %></div>
|
|
26
|
+
<div class="stat-label">Failure Rate</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="grid-2">
|
|
31
|
+
<div class="card">
|
|
32
|
+
<h2>Jobs by Class</h2>
|
|
33
|
+
<table>
|
|
34
|
+
<thead>
|
|
35
|
+
<tr>
|
|
36
|
+
<th>Job</th>
|
|
37
|
+
<th>Total</th>
|
|
38
|
+
<th>Success</th>
|
|
39
|
+
<th>Failed</th>
|
|
40
|
+
<th>Avg</th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
<% @by_job_class.each do |row| %>
|
|
45
|
+
<tr>
|
|
46
|
+
<td class="mono"><%= link_to row.job_class.gsub(/Job$/, ''), jobs_path(job_class: row.job_class) %></td>
|
|
47
|
+
<td><%= row.total %></td>
|
|
48
|
+
<td class="text-success"><%= row.success_count %></td>
|
|
49
|
+
<td class="text-error"><%= row.failed_count %></td>
|
|
50
|
+
<td class="text-muted"><%= row.avg_duration_ms&.round(0) %>ms</td>
|
|
51
|
+
</tr>
|
|
52
|
+
<% end %>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="card">
|
|
58
|
+
<h2>Jobs by Queue</h2>
|
|
59
|
+
<table>
|
|
60
|
+
<thead>
|
|
61
|
+
<tr>
|
|
62
|
+
<th>Queue</th>
|
|
63
|
+
<th>Total</th>
|
|
64
|
+
<th>Success</th>
|
|
65
|
+
<th>Failed</th>
|
|
66
|
+
</tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
<% @by_queue.each do |row| %>
|
|
70
|
+
<tr>
|
|
71
|
+
<td class="mono"><%= row.queue_name || 'default' %></td>
|
|
72
|
+
<td><%= row.total %></td>
|
|
73
|
+
<td class="text-success"><%= row.success_count %></td>
|
|
74
|
+
<td class="text-error"><%= row.failed_count %></td>
|
|
75
|
+
</tr>
|
|
76
|
+
<% end %>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="card">
|
|
83
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
84
|
+
<h2 style="margin: 0;">Recent Failures</h2>
|
|
85
|
+
<% if @recent_failures.any? %>
|
|
86
|
+
<%= button_to "Retry All Failed", retry_all_path, method: :post, class: "btn btn-primary", form: { data: { turbo_confirm: "Retry all failed jobs from the last 24 hours?" } } %>
|
|
87
|
+
<% end %>
|
|
88
|
+
</div>
|
|
89
|
+
<table>
|
|
90
|
+
<thead>
|
|
91
|
+
<tr>
|
|
92
|
+
<th>Time</th>
|
|
93
|
+
<th>Job</th>
|
|
94
|
+
<th>Queue</th>
|
|
95
|
+
<th>Error</th>
|
|
96
|
+
<th>Duration</th>
|
|
97
|
+
<th></th>
|
|
98
|
+
</tr>
|
|
99
|
+
</thead>
|
|
100
|
+
<tbody>
|
|
101
|
+
<% @recent_failures.each do |exec| %>
|
|
102
|
+
<tr>
|
|
103
|
+
<td class="text-muted text-sm"><%= exec.created_at.strftime("%H:%M:%S") %></td>
|
|
104
|
+
<td class="mono"><%= exec.job_class.gsub(/Job$/, '') %></td>
|
|
105
|
+
<td class="text-muted"><%= exec.queue_name %></td>
|
|
106
|
+
<td>
|
|
107
|
+
<span class="badge failed"><%= exec.error_class&.split('::')&.last || 'Error' %></span>
|
|
108
|
+
<span class="text-muted text-sm"><%= truncate(exec.error_message, length: 60) %></span>
|
|
109
|
+
</td>
|
|
110
|
+
<td class="text-muted"><%= exec.duration_human %></td>
|
|
111
|
+
<td style="white-space: nowrap;">
|
|
112
|
+
<%= link_to "View", execution_path(exec), class: "btn" %>
|
|
113
|
+
<% if exec.arguments.present? %>
|
|
114
|
+
<%= button_to "Retry", retry_path(exec), method: :post, class: "btn btn-primary", form_class: "inline" %>
|
|
115
|
+
<% end %>
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
<% end %>
|
|
119
|
+
<% if @recent_failures.empty? %>
|
|
120
|
+
<tr>
|
|
121
|
+
<td colspan="6" class="text-muted" style="text-align: center; padding: 24px;">No failures in this period</td>
|
|
122
|
+
</tr>
|
|
123
|
+
<% end %>
|
|
124
|
+
</tbody>
|
|
125
|
+
</table>
|
|
126
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<div style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center;">
|
|
2
|
+
<div>
|
|
3
|
+
<%= link_to "← Dashboard".html_safe, dashboard_path, class: "btn" %>
|
|
4
|
+
<% if @job_class.present? %>
|
|
5
|
+
<span class="text-muted" style="margin-left: 12px;">Filtered: <%= @job_class %></span>
|
|
6
|
+
<% end %>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="period-select">
|
|
9
|
+
<% [1, 6, 24, 168].each do |hours| %>
|
|
10
|
+
<%= link_to hours == 1 ? "1h" : hours == 6 ? "6h" : hours == 24 ? "24h" : "7d",
|
|
11
|
+
jobs_path(job_class: @job_class, status: @status, period: hours),
|
|
12
|
+
class: (@period == hours.hours ? "active" : "") %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="card">
|
|
18
|
+
<h2>Job Executions</h2>
|
|
19
|
+
<table>
|
|
20
|
+
<thead>
|
|
21
|
+
<tr>
|
|
22
|
+
<th>Time</th>
|
|
23
|
+
<th>Job</th>
|
|
24
|
+
<th>Event</th>
|
|
25
|
+
<th>Queue</th>
|
|
26
|
+
<th>Status</th>
|
|
27
|
+
<th>Duration</th>
|
|
28
|
+
<th>Region</th>
|
|
29
|
+
<th></th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<% @executions.each do |exec| %>
|
|
34
|
+
<tr>
|
|
35
|
+
<td class="text-muted text-sm"><%= exec.created_at.strftime("%m/%d %H:%M:%S") %></td>
|
|
36
|
+
<td class="mono"><%= exec.job_class.gsub(/Job$/, '') %></td>
|
|
37
|
+
<td class="text-sm"><%= exec.event_type %></td>
|
|
38
|
+
<td class="text-muted"><%= exec.queue_name %></td>
|
|
39
|
+
<td><span class="badge <%= exec.status %>"><%= exec.status %></span></td>
|
|
40
|
+
<td class="text-muted"><%= exec.duration_human %></td>
|
|
41
|
+
<td class="text-muted"><%= exec.region %></td>
|
|
42
|
+
<td>
|
|
43
|
+
<%= link_to "View", execution_path(exec), class: "btn" %>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if @executions.empty? %>
|
|
48
|
+
<tr>
|
|
49
|
+
<td colspan="8" class="text-muted" style="text-align: center; padding: 24px;">No job executions found</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<% end %>
|
|
52
|
+
</tbody>
|
|
53
|
+
</table>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<div style="margin-bottom: 16px;">
|
|
2
|
+
<%= link_to "← Back to Dashboard".html_safe, dashboard_path, class: "btn" %>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="card">
|
|
6
|
+
<h2>Job Execution Details</h2>
|
|
7
|
+
|
|
8
|
+
<table>
|
|
9
|
+
<tr>
|
|
10
|
+
<th style="width: 150px;">Job ID</th>
|
|
11
|
+
<td class="mono"><%= @execution.job_id %></td>
|
|
12
|
+
</tr>
|
|
13
|
+
<tr>
|
|
14
|
+
<th>Job Class</th>
|
|
15
|
+
<td class="mono"><%= @execution.job_class %></td>
|
|
16
|
+
</tr>
|
|
17
|
+
<tr>
|
|
18
|
+
<th>Queue</th>
|
|
19
|
+
<td><%= @execution.queue_name %></td>
|
|
20
|
+
</tr>
|
|
21
|
+
<tr>
|
|
22
|
+
<th>Event Type</th>
|
|
23
|
+
<td><%= @execution.event_type %></td>
|
|
24
|
+
</tr>
|
|
25
|
+
<tr>
|
|
26
|
+
<th>Status</th>
|
|
27
|
+
<td><span class="badge <%= @execution.status %>"><%= @execution.status %></span></td>
|
|
28
|
+
</tr>
|
|
29
|
+
<tr>
|
|
30
|
+
<th>Duration</th>
|
|
31
|
+
<td><%= @execution.duration_human || 'N/A' %></td>
|
|
32
|
+
</tr>
|
|
33
|
+
<tr>
|
|
34
|
+
<th>Executions</th>
|
|
35
|
+
<td><%= @execution.executions || 1 %></td>
|
|
36
|
+
</tr>
|
|
37
|
+
<tr>
|
|
38
|
+
<th>Region</th>
|
|
39
|
+
<td><%= @execution.region || 'N/A' %></td>
|
|
40
|
+
</tr>
|
|
41
|
+
<tr>
|
|
42
|
+
<th>Started At</th>
|
|
43
|
+
<td><%= @execution.started_at&.strftime("%Y-%m-%d %H:%M:%S.%L") %></td>
|
|
44
|
+
</tr>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>Finished At</th>
|
|
47
|
+
<td><%= @execution.finished_at&.strftime("%Y-%m-%d %H:%M:%S.%L") %></td>
|
|
48
|
+
</tr>
|
|
49
|
+
<tr>
|
|
50
|
+
<th>Created At</th>
|
|
51
|
+
<td><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
|
52
|
+
</tr>
|
|
53
|
+
</table>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<% if @execution.arguments.present? %>
|
|
57
|
+
<div class="card">
|
|
58
|
+
<h2>Arguments</h2>
|
|
59
|
+
<pre><%= JSON.pretty_generate(JSON.parse(@execution.arguments)) rescue @execution.arguments %></pre>
|
|
60
|
+
</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
<% if @execution.error_class.present? %>
|
|
64
|
+
<div class="card">
|
|
65
|
+
<h2>Error</h2>
|
|
66
|
+
<div class="mb-2">
|
|
67
|
+
<span class="badge failed"><%= @execution.error_class %></span>
|
|
68
|
+
</div>
|
|
69
|
+
<pre style="white-space: pre-wrap;"><%= @execution.error_message %></pre>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
|
|
73
|
+
<% if @execution.arguments.present? && @execution.failed? %>
|
|
74
|
+
<div style="margin-top: 16px;">
|
|
75
|
+
<%= button_to "Retry This Job", retry_path(@execution), method: :post, class: "btn btn-primary" %>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|