job_board 0.3.0 → 0.4.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/CHANGELOG.md +10 -0
- data/app/controllers/job_board/application_controller.rb +11 -10
- data/app/controllers/job_board/assets_controller.rb +3 -1
- data/app/controllers/job_board/jobs_controller.rb +28 -27
- data/app/controllers/job_board/metrics_controller.rb +15 -0
- data/app/models/job_board/jobs_query.rb +30 -29
- data/app/models/job_board/throughput.rb +22 -0
- data/app/views/job_board/queues/index.html.erb +18 -0
- data/app/views/layouts/job_board/application.html.erb +2 -0
- data/config/routes.rb +2 -0
- data/lib/job_board/assets/throughput_chart.css +64 -0
- data/lib/job_board/assets/throughput_chart.js +89 -0
- data/lib/job_board/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfca59214924a123a70c43a88852ea184782d00ca60107c07808b79cf6c3c3ac
|
|
4
|
+
data.tar.gz: 0e64e1a67b82fb40227e2f9d87729fb647fe2b7d00bfc6bb9cab0ecf331ba3c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da74ce333cc798a40a50f3ee1cafae15d7e882713eb7a8e2afa6b80773d1f0a31bcd47ad88a88e43fb321053a0ca8a8242642a3a2bc90282fee16c447d2e8224
|
|
7
|
+
data.tar.gz: 7e979f3d406ef8038fddc553e47cb72a0af08320a3da0a936d5319e929f8177fa067ec2323093273fa0fee9adb4d24d28886e11b0cace363f6591636039f6360
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Queues (home) page: a real-time **Throughput** line chart at the top, plotting jobs
|
|
6
|
+
that became ready to run against jobs that finished, one point per poll interval.
|
|
7
|
+
It tracks only what happens after the page loads (no history), keeps a rolling
|
|
8
|
+
10-minute window scrolling right-to-left, and is fed by a small JSON `metrics`
|
|
9
|
+
endpoint so the SVG state survives the auto-refresh swap.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
3
13
|
## 0.3.0
|
|
4
14
|
|
|
5
15
|
- Recurring tasks page: **Run now** button to trigger a task outside its schedule
|
|
@@ -7,18 +7,19 @@ module JobBoard
|
|
|
7
7
|
layout "job_board/application"
|
|
8
8
|
|
|
9
9
|
private
|
|
10
|
-
def authenticate
|
|
11
|
-
credentials = JobBoard.config.http_basic_auth
|
|
12
|
-
return unless credentials
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
end
|
|
18
|
-
end
|
|
11
|
+
def authenticate
|
|
12
|
+
credentials = JobBoard.config.http_basic_auth
|
|
13
|
+
return unless credentials
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
authenticate_or_request_with_http_basic("JobBoard") do |name, password|
|
|
16
|
+
ActiveSupport::SecurityUtils.secure_compare(name, credentials[:name].to_s) &
|
|
17
|
+
ActiveSupport::SecurityUtils.secure_compare(password, credentials[:password].to_s)
|
|
22
18
|
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stale_threshold
|
|
22
|
+
JobBoard.config.stale_process_threshold || SolidQueue.process_alive_threshold
|
|
23
|
+
end
|
|
23
24
|
end
|
|
24
25
|
end
|
|
@@ -53,36 +53,37 @@ module JobBoard
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
private
|
|
56
|
-
def set_query
|
|
57
|
-
status = params[:status].presence || "ready"
|
|
58
|
-
head :not_found and return unless JobsQuery::STATUSES.include?(status)
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class_name: params[:class_name].presence
|
|
64
|
-
)
|
|
65
|
-
end
|
|
57
|
+
def set_query
|
|
58
|
+
status = params[:status].presence || "ready"
|
|
59
|
+
head :not_found and return unless JobsQuery::STATUSES.include?(status)
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
@query = JobsQuery.new(
|
|
62
|
+
status: status,
|
|
63
|
+
queue_name: params[:queue_name].presence,
|
|
64
|
+
class_name: params[:class_name].presence
|
|
65
|
+
)
|
|
66
|
+
end
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
query.failed_jobs_relation.find_in_batches(batch_size: 500, &block)
|
|
81
|
-
end
|
|
68
|
+
def find_job
|
|
69
|
+
SolidQueue::Job
|
|
70
|
+
.includes(:ready_execution, :scheduled_execution, :claimed_execution,
|
|
71
|
+
:blocked_execution, :failed_execution)
|
|
72
|
+
.find(params[:id])
|
|
73
|
+
end
|
|
82
74
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
def failed_jobs_in_batches(&block)
|
|
76
|
+
query = JobsQuery.new(
|
|
77
|
+
status: "failed",
|
|
78
|
+
queue_name: params[:queue_name].presence,
|
|
79
|
+
class_name: params[:class_name].presence
|
|
80
|
+
)
|
|
81
|
+
query.failed_jobs_relation.find_in_batches(batch_size: 500, &block)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def failed_jobs_path
|
|
85
|
+
jobs_path(status: "failed", queue_name: params[:queue_name].presence,
|
|
86
|
+
class_name: params[:class_name].presence)
|
|
87
|
+
end
|
|
87
88
|
end
|
|
88
89
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module JobBoard
|
|
2
|
+
class MetricsController < ApplicationController
|
|
3
|
+
def show
|
|
4
|
+
render json: Throughput.snapshot(since: parse_time(params[:since]))
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def parse_time(value)
|
|
10
|
+
Time.iso8601(value.to_s)
|
|
11
|
+
rescue ArgumentError
|
|
12
|
+
nil # blank or malformed cursor → treat as a fresh baseline
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -49,38 +49,39 @@ module JobBoard
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
private
|
|
52
|
-
def relation
|
|
53
|
-
case status
|
|
54
|
-
when "ready"
|
|
55
|
-
filtered(SolidQueue::ReadyExecution.includes(:job), own_queue_column: true)
|
|
56
|
-
when "scheduled"
|
|
57
|
-
filtered(SolidQueue::ScheduledExecution.includes(:job), own_queue_column: true)
|
|
58
|
-
when "in_progress"
|
|
59
|
-
filtered(SolidQueue::ClaimedExecution.includes(:process, :job), own_queue_column: false)
|
|
60
|
-
when "blocked"
|
|
61
|
-
filtered(SolidQueue::BlockedExecution.includes(:job), own_queue_column: true)
|
|
62
|
-
when "failed"
|
|
63
|
-
filtered(SolidQueue::FailedExecution.includes(:job), own_queue_column: false)
|
|
64
|
-
when "finished"
|
|
65
|
-
filtered_jobs(SolidQueue::Job.finished)
|
|
66
|
-
else
|
|
67
|
-
raise ArgumentError, "unknown status #{status.inspect}"
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
52
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
53
|
+
def relation
|
|
54
|
+
case status
|
|
55
|
+
when "ready"
|
|
56
|
+
filtered(SolidQueue::ReadyExecution.includes(:job), own_queue_column: true)
|
|
57
|
+
when "scheduled"
|
|
58
|
+
filtered(SolidQueue::ScheduledExecution.includes(:job), own_queue_column: true)
|
|
59
|
+
when "in_progress"
|
|
60
|
+
filtered(SolidQueue::ClaimedExecution.includes(:process, :job), own_queue_column: false)
|
|
61
|
+
when "blocked"
|
|
62
|
+
filtered(SolidQueue::BlockedExecution.includes(:job), own_queue_column: true)
|
|
63
|
+
when "failed"
|
|
64
|
+
filtered(SolidQueue::FailedExecution.includes(:job), own_queue_column: false)
|
|
65
|
+
when "finished"
|
|
66
|
+
filtered_jobs(SolidQueue::Job.finished)
|
|
67
|
+
else
|
|
68
|
+
raise ArgumentError, "unknown status #{status.inspect}"
|
|
78
69
|
end
|
|
70
|
+
end
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
scope = scope.where(
|
|
83
|
-
|
|
72
|
+
def filtered(scope, own_queue_column:)
|
|
73
|
+
if queue_name
|
|
74
|
+
scope = own_queue_column ? scope.where(queue_name: queue_name)
|
|
75
|
+
: scope.joins(:job).where(solid_queue_jobs: { queue_name: queue_name })
|
|
84
76
|
end
|
|
77
|
+
scope = scope.joins(:job).where(solid_queue_jobs: { class_name: class_name }) if class_name
|
|
78
|
+
scope
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def filtered_jobs(scope)
|
|
82
|
+
scope = scope.where(queue_name: queue_name) if queue_name
|
|
83
|
+
scope = scope.where(class_name: class_name) if class_name
|
|
84
|
+
scope
|
|
85
|
+
end
|
|
85
86
|
end
|
|
86
87
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module JobBoard
|
|
2
|
+
class Throughput
|
|
3
|
+
READY_AT = "COALESCE(scheduled_at, created_at)".freeze
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def snapshot(since:)
|
|
7
|
+
now = Time.current
|
|
8
|
+
{
|
|
9
|
+
now: now.iso8601(3),
|
|
10
|
+
enqueued: since ? count_between(READY_AT, since, now) : 0,
|
|
11
|
+
completed: since ? count_between("finished_at", since, now) : 0
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def count_between(expression, since, now)
|
|
18
|
+
SolidQueue::Job.where("#{expression} > ? AND #{expression} <= ?", since, now).count
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
<h1>Queues</h1>
|
|
2
2
|
|
|
3
|
+
<section id="throughput-chart" class="throughput-chart"
|
|
4
|
+
data-metrics-url="<%= metrics_path %>"
|
|
5
|
+
data-poll-interval="<%= JobBoard.config.poll_interval.to_i %>"
|
|
6
|
+
data-max-points="120">
|
|
7
|
+
<div class="throughput-chart__header">
|
|
8
|
+
<h2>Throughput</h2>
|
|
9
|
+
<div class="throughput-chart__legend">
|
|
10
|
+
<span class="tp-key tp-key--enqueued">Enqueued <b data-tp-latest="enqueued">0</b></span>
|
|
11
|
+
<span class="tp-key tp-key--completed">Completed <b data-tp-latest="completed">0</b></span>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<svg class="throughput-chart__svg" viewBox="0 0 600 160" preserveAspectRatio="none"
|
|
15
|
+
role="img" aria-label="Enqueued and completed jobs per interval">
|
|
16
|
+
<polyline class="tp-line tp-line--enqueued" fill="none" points=""></polyline>
|
|
17
|
+
<polyline class="tp-line tp-line--completed" fill="none" points=""></polyline>
|
|
18
|
+
</svg>
|
|
19
|
+
</section>
|
|
20
|
+
|
|
3
21
|
<div data-poll-region="queues">
|
|
4
22
|
<% if @queue_list.active.empty? && @queue_list.inactive.empty? %>
|
|
5
23
|
<p class="empty-state">No queues yet — queues appear once jobs are enqueued.</p>
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
7
|
<link rel="stylesheet" href="<%= static_asset_path("application.css", v: JobBoard::VERSION) %>">
|
|
8
|
+
<link rel="stylesheet" href="<%= static_asset_path("throughput_chart.css", v: JobBoard::VERSION) %>">
|
|
8
9
|
<script src="<%= static_asset_path("application.js", v: JobBoard::VERSION) %>" defer></script>
|
|
10
|
+
<script src="<%= static_asset_path("throughput_chart.js", v: JobBoard::VERSION) %>" defer></script>
|
|
9
11
|
</head>
|
|
10
12
|
<body data-poll-interval="<%= JobBoard.config.poll_interval.to_i %>">
|
|
11
13
|
<%= render "job_board/shared/nav" %>
|
data/config/routes.rb
CHANGED
|
@@ -24,5 +24,7 @@ JobBoard::Engine.routes.draw do
|
|
|
24
24
|
post "run", to: "recurring_tasks/runs#create", as: :recurring_task_run
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
get "metrics", to: "metrics#show", as: :metrics
|
|
28
|
+
|
|
27
29
|
get "assets/:name", to: "assets#show", as: :static_asset, constraints: { name: /[a-z_]+\.(css|js)/ }
|
|
28
30
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* Home-page throughput chart. Relies on the :root custom properties defined in
|
|
2
|
+
application.css, which the layout always loads alongside this file. */
|
|
3
|
+
.throughput-chart {
|
|
4
|
+
background: var(--surface);
|
|
5
|
+
border: 1px solid var(--border);
|
|
6
|
+
border-radius: 10px;
|
|
7
|
+
padding: 0.75rem 1rem 1rem;
|
|
8
|
+
margin-bottom: 1.5rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.throughput-chart__header {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: baseline;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
gap: 1rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.throughput-chart__header h2 {
|
|
19
|
+
font-size: 0.95rem;
|
|
20
|
+
margin: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.throughput-chart__legend {
|
|
24
|
+
display: flex;
|
|
25
|
+
gap: 1rem;
|
|
26
|
+
font-size: 0.82rem;
|
|
27
|
+
color: var(--text-muted);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tp-key {
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 0.4rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.tp-key b {
|
|
37
|
+
color: var(--text);
|
|
38
|
+
font-variant-numeric: tabular-nums;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.tp-key::before {
|
|
42
|
+
content: "";
|
|
43
|
+
width: 0.75rem;
|
|
44
|
+
height: 0.2rem;
|
|
45
|
+
border-radius: 2px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.tp-key--enqueued::before { background: var(--accent); }
|
|
49
|
+
.tp-key--completed::before { background: var(--success); }
|
|
50
|
+
|
|
51
|
+
.throughput-chart__svg {
|
|
52
|
+
display: block;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 160px;
|
|
55
|
+
margin-top: 0.5rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.tp-line {
|
|
59
|
+
stroke-width: 2;
|
|
60
|
+
vector-effect: non-scaling-stroke;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.tp-line--enqueued { stroke: var(--accent); }
|
|
64
|
+
.tp-line--completed { stroke: var(--success); }
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var chart = document.getElementById("throughput-chart");
|
|
5
|
+
if (!chart) return;
|
|
6
|
+
|
|
7
|
+
var metricsUrl = chart.dataset.metricsUrl;
|
|
8
|
+
var pollIntervalMs = parseInt(chart.dataset.pollInterval, 10) * 1000;
|
|
9
|
+
var maxVisiblePoints = parseInt(chart.dataset.maxPoints, 10) || 120;
|
|
10
|
+
if (!metricsUrl || !pollIntervalMs || pollIntervalMs <= 0) return;
|
|
11
|
+
|
|
12
|
+
var viewBox = chart.querySelector(".throughput-chart__svg").viewBox.baseVal;
|
|
13
|
+
var enqueuedLine = chart.querySelector(".tp-line--enqueued");
|
|
14
|
+
var completedLine = chart.querySelector(".tp-line--completed");
|
|
15
|
+
var latestLabels = {
|
|
16
|
+
enqueued: chart.querySelector('[data-tp-latest="enqueued"]'),
|
|
17
|
+
completed: chart.querySelector('[data-tp-latest="completed"]')
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
var samplesOldestToNewest = [];
|
|
21
|
+
var previousServerTime = null;
|
|
22
|
+
|
|
23
|
+
function ignoreTransientError() {}
|
|
24
|
+
|
|
25
|
+
function fetchMetrics(query) {
|
|
26
|
+
return fetch(metricsUrl + query, {
|
|
27
|
+
headers: { "Accept": "application/json" },
|
|
28
|
+
credentials: "same-origin"
|
|
29
|
+
}).then(function (response) {
|
|
30
|
+
if (!response.ok) throw new Error("metrics fetch failed");
|
|
31
|
+
return response.json();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resetBaselineWithoutPlotting() {
|
|
36
|
+
return fetchMetrics("").then(function (data) { previousServerTime = data.now; });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function highestValue() {
|
|
40
|
+
var peak = 1;
|
|
41
|
+
samplesOldestToNewest.forEach(function (sample) {
|
|
42
|
+
if (sample.enqueued > peak) peak = sample.enqueued;
|
|
43
|
+
if (sample.completed > peak) peak = sample.completed;
|
|
44
|
+
});
|
|
45
|
+
return peak;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pointsFor(series) {
|
|
49
|
+
if (samplesOldestToNewest.length === 0) return "";
|
|
50
|
+
var peak = highestValue();
|
|
51
|
+
var horizontalStep = samplesOldestToNewest.length > 1 ? viewBox.width / (samplesOldestToNewest.length - 1) : 0;
|
|
52
|
+
return samplesOldestToNewest.map(function (sample, index) {
|
|
53
|
+
var stepsFromRightEdge = samplesOldestToNewest.length - 1 - index;
|
|
54
|
+
var x = viewBox.width - stepsFromRightEdge * horizontalStep;
|
|
55
|
+
var y = viewBox.height - (sample[series] / peak) * viewBox.height;
|
|
56
|
+
return x.toFixed(1) + "," + y.toFixed(1);
|
|
57
|
+
}).join(" ");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function redraw() {
|
|
61
|
+
enqueuedLine.setAttribute("points", pointsFor("enqueued"));
|
|
62
|
+
completedLine.setAttribute("points", pointsFor("completed"));
|
|
63
|
+
var newestSample = samplesOldestToNewest[samplesOldestToNewest.length - 1];
|
|
64
|
+
if (newestSample) {
|
|
65
|
+
latestLabels.enqueued.textContent = newestSample.enqueued;
|
|
66
|
+
latestLabels.completed.textContent = newestSample.completed;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectSample() {
|
|
71
|
+
var baselineReady = previousServerTime !== null;
|
|
72
|
+
if (document.hidden || !baselineReady) return;
|
|
73
|
+
fetchMetrics("?since=" + encodeURIComponent(previousServerTime)).then(function (data) {
|
|
74
|
+
samplesOldestToNewest.push({ enqueued: data.enqueued, completed: data.completed });
|
|
75
|
+
if (samplesOldestToNewest.length > maxVisiblePoints) samplesOldestToNewest.shift();
|
|
76
|
+
previousServerTime = data.now;
|
|
77
|
+
redraw();
|
|
78
|
+
}).catch(ignoreTransientError);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function reBaselineWhenTabBecomesVisible() {
|
|
82
|
+
if (!document.hidden) resetBaselineWithoutPlotting();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
document.addEventListener("visibilitychange", reBaselineWhenTabBecomesVisible);
|
|
86
|
+
|
|
87
|
+
resetBaselineWithoutPlotting().catch(ignoreTransientError);
|
|
88
|
+
setInterval(collectSample, pollIntervalMs);
|
|
89
|
+
})();
|
data/lib/job_board/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: job_board
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- app/controllers/job_board/application_controller.rb
|
|
52
52
|
- app/controllers/job_board/assets_controller.rb
|
|
53
53
|
- app/controllers/job_board/jobs_controller.rb
|
|
54
|
+
- app/controllers/job_board/metrics_controller.rb
|
|
54
55
|
- app/controllers/job_board/processes_controller.rb
|
|
55
56
|
- app/controllers/job_board/queues/pauses_controller.rb
|
|
56
57
|
- app/controllers/job_board/queues_controller.rb
|
|
@@ -65,6 +66,7 @@ files:
|
|
|
65
66
|
- app/models/job_board/page.rb
|
|
66
67
|
- app/models/job_board/process_tree.rb
|
|
67
68
|
- app/models/job_board/queue_list.rb
|
|
69
|
+
- app/models/job_board/throughput.rb
|
|
68
70
|
- app/views/job_board/jobs/_filters.html.erb
|
|
69
71
|
- app/views/job_board/jobs/_job_row.html.erb
|
|
70
72
|
- app/views/job_board/jobs/index.html.erb
|
|
@@ -81,6 +83,8 @@ files:
|
|
|
81
83
|
- lib/job_board.rb
|
|
82
84
|
- lib/job_board/assets/application.css
|
|
83
85
|
- lib/job_board/assets/application.js
|
|
86
|
+
- lib/job_board/assets/throughput_chart.css
|
|
87
|
+
- lib/job_board/assets/throughput_chart.js
|
|
84
88
|
- lib/job_board/configuration.rb
|
|
85
89
|
- lib/job_board/engine.rb
|
|
86
90
|
- lib/job_board/version.rb
|