flightdeck 0.5.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/MIT-LICENSE +21 -0
- data/README.md +190 -0
- data/app/assets/flightdeck/flightdeck-00403457c5d9.css +3 -0
- data/app/assets/flightdeck/flightdeck-c02e38040ddb.js +414 -0
- data/app/assets/flightdeck/manifest.json +16 -0
- data/app/controllers/concerns/flightdeck/toasts.rb +25 -0
- data/app/controllers/flightdeck/application_controller.rb +96 -0
- data/app/controllers/flightdeck/assets_controller.rb +39 -0
- data/app/controllers/flightdeck/jobs/actions_controller.rb +167 -0
- data/app/controllers/flightdeck/jobs/discards_controller.rb +44 -0
- data/app/controllers/flightdeck/jobs/retries_controller.rb +23 -0
- data/app/controllers/flightdeck/jobs_controller.rb +39 -0
- data/app/controllers/flightdeck/overview_controller.rb +11 -0
- data/app/controllers/flightdeck/processes_controller.rb +49 -0
- data/app/controllers/flightdeck/queues_controller.rb +51 -0
- data/app/controllers/flightdeck/recurring_tasks_controller.rb +46 -0
- data/app/helpers/flightdeck/application_helper.rb +68 -0
- data/app/helpers/flightdeck/infrastructure_helper.rb +24 -0
- data/app/helpers/flightdeck/jobs_helper.rb +136 -0
- data/app/helpers/flightdeck/metrics_helper.rb +49 -0
- data/app/models/flightdeck/arguments_preview.rb +95 -0
- data/app/models/flightdeck/bulk_action.rb +83 -0
- data/app/models/flightdeck/cache.rb +51 -0
- data/app/models/flightdeck/cron_schedule.rb +102 -0
- data/app/models/flightdeck/error_summary.rb +64 -0
- data/app/models/flightdeck/grouped_failures.rb +24 -0
- data/app/models/flightdeck/job_detail.rb +163 -0
- data/app/models/flightdeck/job_row.rb +93 -0
- data/app/models/flightdeck/jobs_query.rb +241 -0
- data/app/models/flightdeck/metrics/bar_chart.rb +49 -0
- data/app/models/flightdeck/metrics/chart.rb +111 -0
- data/app/models/flightdeck/metrics/line_chart.rb +67 -0
- data/app/models/flightdeck/metrics/series.rb +165 -0
- data/app/models/flightdeck/metrics/sparkline.rb +45 -0
- data/app/models/flightdeck/metrics/time_bucket.rb +95 -0
- data/app/models/flightdeck/overview.rb +217 -0
- data/app/models/flightdeck/process_registry.rb +108 -0
- data/app/models/flightdeck/queue_stats.rb +88 -0
- data/app/models/flightdeck/recurring_catalog.rb +116 -0
- data/app/views/flightdeck/jobs/_failed_row.html.erb +33 -0
- data/app/views/flightdeck/jobs/_filter_bar.html.erb +29 -0
- data/app/views/flightdeck/jobs/_list.html.erb +89 -0
- data/app/views/flightdeck/jobs/_pager.html.erb +22 -0
- data/app/views/flightdeck/jobs/_row.html.erb +23 -0
- data/app/views/flightdeck/jobs/_state_tabs.html.erb +10 -0
- data/app/views/flightdeck/jobs/actions/create.turbo_stream.erb +18 -0
- data/app/views/flightdeck/jobs/index.html.erb +13 -0
- data/app/views/flightdeck/jobs/show.html.erb +100 -0
- data/app/views/flightdeck/metrics/_bar_chart.html.erb +34 -0
- data/app/views/flightdeck/metrics/_line_chart.html.erb +38 -0
- data/app/views/flightdeck/metrics/_range_control.html.erb +7 -0
- data/app/views/flightdeck/metrics/_sparkline.html.erb +9 -0
- data/app/views/flightdeck/overview/_completion.html.erb +19 -0
- data/app/views/flightdeck/overview/_failures.html.erb +28 -0
- data/app/views/flightdeck/overview/_fleet.html.erb +21 -0
- data/app/views/flightdeck/overview/_queues.html.erb +41 -0
- data/app/views/flightdeck/overview/_throughput.html.erb +23 -0
- data/app/views/flightdeck/overview/_tiles.html.erb +14 -0
- data/app/views/flightdeck/overview/index.html.erb +54 -0
- data/app/views/flightdeck/processes/_fleet.html.erb +54 -0
- data/app/views/flightdeck/processes/_row.html.erb +28 -0
- data/app/views/flightdeck/processes/index.html.erb +9 -0
- data/app/views/flightdeck/processes/prune.turbo_stream.erb +15 -0
- data/app/views/flightdeck/queues/_cards.html.erb +46 -0
- data/app/views/flightdeck/queues/index.html.erb +9 -0
- data/app/views/flightdeck/queues/update.turbo_stream.erb +15 -0
- data/app/views/flightdeck/recurring_tasks/_table.html.erb +53 -0
- data/app/views/flightdeck/recurring_tasks/index.html.erb +9 -0
- data/app/views/flightdeck/recurring_tasks/run.turbo_stream.erb +15 -0
- data/app/views/flightdeck/shared/_toast.html.erb +7 -0
- data/app/views/layouts/flightdeck/application.html.erb +110 -0
- data/config/routes.rb +40 -0
- data/lib/flightdeck/assets.rb +78 -0
- data/lib/flightdeck/configuration.rb +80 -0
- data/lib/flightdeck/engine.rb +31 -0
- data/lib/flightdeck/version.rb +5 -0
- data/lib/flightdeck.rb +31 -0
- metadata +150 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<div class="fd-card">
|
|
2
|
+
<div class="fd-card-hd">
|
|
3
|
+
<h3>Time to completion</h3>
|
|
4
|
+
<span class="sub">enqueue → finish, average <%= window.subtitle %></span>
|
|
5
|
+
<div class="right">
|
|
6
|
+
<div class="fd-legend"><span><i class="fd-swatch-s3"></i>Average</span></div>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="fd-chart-body">
|
|
10
|
+
<%= render "flightdeck/metrics/line_chart",
|
|
11
|
+
chart: Flightdeck::Metrics::LineChart.new(points: series.completion_time, window: window) %>
|
|
12
|
+
</div>
|
|
13
|
+
<%# Deliberately not "time to start": Solid Queue deletes the claim row when a
|
|
14
|
+
job finishes, so historical time-to-start cannot be recovered. %>
|
|
15
|
+
<div class="fd-chart-note">
|
|
16
|
+
Measured from enqueue to finish. Time spent waiting and time spent running are not
|
|
17
|
+
separable after the fact.
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<div class="fd-card">
|
|
2
|
+
<div class="fd-card-hd">
|
|
3
|
+
<h3>Recent failures</h3>
|
|
4
|
+
<div class="right">
|
|
5
|
+
<%= link_to "View all #{fd_count(overview.failed_count)}", jobs_path(state: :failed), class: "fd-btn sm" %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="fd-scroll-x">
|
|
9
|
+
<table class="fd-data">
|
|
10
|
+
<tbody>
|
|
11
|
+
<% if overview.recent_failures.empty? %>
|
|
12
|
+
<tr><td><div class="fd-empty">No failures. Nothing to see here.</div></td></tr>
|
|
13
|
+
<% else %>
|
|
14
|
+
<% overview.recent_failures.each do |row| %>
|
|
15
|
+
<tr>
|
|
16
|
+
<td class="cls">
|
|
17
|
+
<%= link_to row.class_name, job_path(row.id, state: :failed) %>
|
|
18
|
+
<div class="args"><%= row.error_summary.present? ? row.error_summary.to_s : "—" %></div>
|
|
19
|
+
</td>
|
|
20
|
+
<td><%= fd_queue_badge(row.queue_name) %></td>
|
|
21
|
+
<td class="dim num"><%= row.failed_at ? fd_ago(row.failed_at) : "—" %></td>
|
|
22
|
+
</tr>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</tbody>
|
|
26
|
+
</table>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="fd-card">
|
|
2
|
+
<div class="fd-card-hd">
|
|
3
|
+
<h3>Fleet</h3>
|
|
4
|
+
<span class="sub"><%= pluralize(registry.total, "process") %> · last heartbeats</span>
|
|
5
|
+
<div class="right"><%= link_to "Processes", processes_path, class: "fd-btn sm" %></div>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="fd-health-strip">
|
|
8
|
+
<% if registry.nodes.empty? %>
|
|
9
|
+
<div class="fd-empty">No Solid Queue processes are registered.</div>
|
|
10
|
+
<% else %>
|
|
11
|
+
<% registry.nodes.each do |node| %>
|
|
12
|
+
<span class="fd-proc-chip" title="<%= node.name %> · heartbeat <%= fd_ago(node.last_heartbeat_at) %>">
|
|
13
|
+
<span class="fd-led <%= node.freshness %>"></span>
|
|
14
|
+
<%= node.kind.downcase %> · <%= node.hostname.presence || "unknown host" %><%
|
|
15
|
+
%><% if node.claims_executions? && node.freshness == :fresh %> · <%= node.claimed_count %> claimed<%
|
|
16
|
+
%><% else %> · heartbeat <%= fd_ago(node.last_heartbeat_at) %><% end %>
|
|
17
|
+
</span>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<div class="fd-card">
|
|
2
|
+
<div class="fd-card-hd">
|
|
3
|
+
<h3>Queues</h3>
|
|
4
|
+
<div class="right"><%= link_to "View all", queues_path, class: "fd-btn sm" %></div>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="fd-scroll-x">
|
|
7
|
+
<table class="fd-data">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Queue</th>
|
|
11
|
+
<th class="num">Depth</th>
|
|
12
|
+
<th class="num">Latency</th>
|
|
13
|
+
<th>Last <%= Flightdeck::Metrics::Series::SPARKLINE_HOURS %>h</th>
|
|
14
|
+
<th style="width:90px"></th>
|
|
15
|
+
</tr>
|
|
16
|
+
</thead>
|
|
17
|
+
<tbody>
|
|
18
|
+
<% if overview.queues.empty? %>
|
|
19
|
+
<tr><td colspan="5"><div class="fd-empty">No queues yet.</div></td></tr>
|
|
20
|
+
<% else %>
|
|
21
|
+
<% overview.queues.each do |queue| %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td class="cls">
|
|
24
|
+
<%= link_to queue.name, queues_path %>
|
|
25
|
+
<% if queue.paused? %><span class="fd-pill paused">PAUSED</span><% end %>
|
|
26
|
+
</td>
|
|
27
|
+
<td class="num"><%= number_with_delimiter(queue.depth) %></td>
|
|
28
|
+
<td class="num dim"><%= queue.latency ? fd_duration(queue.latency) : "—" %></td>
|
|
29
|
+
<td><%= render "flightdeck/metrics/sparkline", sparkline: overview.sparkline_for(queue.name) %></td>
|
|
30
|
+
<td>
|
|
31
|
+
<div class="fd-depth-bar">
|
|
32
|
+
<i style="width: <%= fd_depth_share(queue.depth, overview.max_queue_depth) %>%"></i>
|
|
33
|
+
</div>
|
|
34
|
+
</td>
|
|
35
|
+
</tr>
|
|
36
|
+
<% end %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<div class="fd-card">
|
|
2
|
+
<div class="fd-card-hd">
|
|
3
|
+
<h3>Throughput</h3>
|
|
4
|
+
<span class="sub">jobs finished <%= window.subtitle %></span>
|
|
5
|
+
<div class="right">
|
|
6
|
+
<div class="fd-legend">
|
|
7
|
+
<span><i class="fd-swatch-s1"></i>Succeeded</span>
|
|
8
|
+
<span><i class="fd-swatch-s2"></i>Failed</span>
|
|
9
|
+
</div>
|
|
10
|
+
<%= render "flightdeck/metrics/range_control", window: window %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="fd-chart-body">
|
|
14
|
+
<%= render "flightdeck/metrics/bar_chart",
|
|
15
|
+
chart: Flightdeck::Metrics::BarChart.new(points: series.throughput, window: window) %>
|
|
16
|
+
</div>
|
|
17
|
+
<% if series.truncated_by_retention? %>
|
|
18
|
+
<div class="fd-chart-note">
|
|
19
|
+
Finished jobs are retained for <%= fd_duration(series.retention) %>, which is shorter than this
|
|
20
|
+
window — the oldest buckets undercount.
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="fd-tiles">
|
|
2
|
+
<% overview.tiles.each do |tile| %>
|
|
3
|
+
<div class="fd-card fd-tile">
|
|
4
|
+
<div class="lbl"><%= tile.label %></div>
|
|
5
|
+
<div class="val">
|
|
6
|
+
<%= tile.value.is_a?(Numeric) ? number_with_delimiter(tile.value) : tile.value %><%
|
|
7
|
+
%><% if tile.unit %><small><%= tile.unit %></small><% end %>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="delta <%= fd_delta_class(tile) %>">
|
|
10
|
+
<%= fd_trend_glyph(tile.trend) %> <%= tile.detail %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<% content_for :page_title, "Overview" %>
|
|
2
|
+
|
|
3
|
+
<turbo-frame id="fd-tiles"
|
|
4
|
+
target="_top"
|
|
5
|
+
data-controller="refresh"
|
|
6
|
+
data-refresh-interval-value="<%= fd_poll_ms %>"
|
|
7
|
+
data-refresh-url-value="<%= root_path(range: @window.key) %>">
|
|
8
|
+
<%= render "flightdeck/overview/tiles", overview: @overview %>
|
|
9
|
+
</turbo-frame>
|
|
10
|
+
|
|
11
|
+
<div class="fd-grid-2">
|
|
12
|
+
<turbo-frame id="fd-throughput"
|
|
13
|
+
target="_top"
|
|
14
|
+
data-controller="refresh"
|
|
15
|
+
data-refresh-interval-value="<%= fd_chart_poll_ms %>"
|
|
16
|
+
data-refresh-url-value="<%= root_path(range: @window.key) %>">
|
|
17
|
+
<%= render "flightdeck/overview/throughput", series: @series, window: @window %>
|
|
18
|
+
</turbo-frame>
|
|
19
|
+
|
|
20
|
+
<turbo-frame id="fd-completion"
|
|
21
|
+
target="_top"
|
|
22
|
+
data-controller="refresh"
|
|
23
|
+
data-refresh-interval-value="<%= fd_chart_poll_ms %>"
|
|
24
|
+
data-refresh-url-value="<%= root_path(range: @window.key) %>">
|
|
25
|
+
<%= render "flightdeck/overview/completion", series: @series, window: @window %>
|
|
26
|
+
</turbo-frame>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="fd-grid-2b">
|
|
30
|
+
<turbo-frame id="fd-overview-queues"
|
|
31
|
+
target="_top"
|
|
32
|
+
data-controller="refresh"
|
|
33
|
+
data-refresh-interval-value="<%= fd_poll_ms %>"
|
|
34
|
+
data-refresh-url-value="<%= root_path(range: @window.key) %>">
|
|
35
|
+
<%= render "flightdeck/overview/queues", overview: @overview %>
|
|
36
|
+
</turbo-frame>
|
|
37
|
+
|
|
38
|
+
<turbo-frame id="fd-overview-failures"
|
|
39
|
+
target="_top"
|
|
40
|
+
data-controller="refresh"
|
|
41
|
+
data-refresh-interval-value="<%= fd_poll_ms %>"
|
|
42
|
+
data-refresh-url-value="<%= root_path(range: @window.key) %>">
|
|
43
|
+
<%= render "flightdeck/overview/failures", overview: @overview %>
|
|
44
|
+
</turbo-frame>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<turbo-frame id="fd-fleet"
|
|
48
|
+
target="_top"
|
|
49
|
+
class="fd-block"
|
|
50
|
+
data-controller="refresh"
|
|
51
|
+
data-refresh-interval-value="<%= fd_poll_ms %>"
|
|
52
|
+
data-refresh-url-value="<%= root_path(range: @window.key) %>">
|
|
53
|
+
<%= render "flightdeck/overview/fleet", registry: @overview.registry %>
|
|
54
|
+
</turbo-frame>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<% if registry.any_dead? %>
|
|
2
|
+
<div class="fd-alert" role="alert">
|
|
3
|
+
<svg class="ic" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M8 1.5 15 14H1L8 1.5z" stroke-linejoin="round"/><path d="M8 6v4M8 12.2v.2"/></svg>
|
|
4
|
+
<div>
|
|
5
|
+
<b><%= pluralize(registry.dead.size, "process") %> missed heartbeats for longer than
|
|
6
|
+
<%= fd_duration(SolidQueue.process_alive_threshold) %>.</b>
|
|
7
|
+
<% if registry.dead_claimed_count.positive? %>
|
|
8
|
+
<%= pluralize(registry.dead_claimed_count, "claimed execution") %> will be released and
|
|
9
|
+
marked failed when they are pruned.
|
|
10
|
+
<% else %>
|
|
11
|
+
They hold no claimed executions.
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="right">
|
|
15
|
+
<% if registry.dead_claimed_count.positive? %>
|
|
16
|
+
<%= link_to "Inspect claimed", jobs_path(state: :in_progress), class: "fd-btn sm" %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<div class="fd-card">
|
|
23
|
+
<div class="fd-card-hd">
|
|
24
|
+
<h3>Processes</h3>
|
|
25
|
+
<span class="sub">grouped by supervisor · heartbeat threshold <%= fd_duration(SolidQueue.process_alive_threshold) %></span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="fd-scroll-x">
|
|
28
|
+
<table class="fd-data">
|
|
29
|
+
<thead>
|
|
30
|
+
<tr>
|
|
31
|
+
<th>Kind</th>
|
|
32
|
+
<th>Host</th>
|
|
33
|
+
<th class="num">PID</th>
|
|
34
|
+
<th>Configuration</th>
|
|
35
|
+
<th class="num">Claimed</th>
|
|
36
|
+
<th>Heartbeat</th>
|
|
37
|
+
<th style="width:90px"></th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
<% if registry.nodes.empty? %>
|
|
42
|
+
<tr><td colspan="7"><div class="fd-empty">No Solid Queue processes are registered.</div></td></tr>
|
|
43
|
+
<% else %>
|
|
44
|
+
<% registry.tree.each do |root| %>
|
|
45
|
+
<%= render "flightdeck/processes/row", node: root, child: false %>
|
|
46
|
+
<% root.children.each do |child| %>
|
|
47
|
+
<%= render "flightdeck/processes/row", node: child, child: true %>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<tr id="<%= dom_id_for_process(node.id) %>" class="<%= "fd-row-danger" if node.dead? %>">
|
|
2
|
+
<td class="cls<%= " fd-indent" if child %>">
|
|
3
|
+
<%= "↳ " if child %><%= node.kind %>
|
|
4
|
+
</td>
|
|
5
|
+
<td class="dim fd-mono" style="font-size:12px"><%= node.hostname.presence || "—" %></td>
|
|
6
|
+
<td class="num"><%= node.pid %></td>
|
|
7
|
+
<td class="dim"><%= node.config_summary.presence || "—" %></td>
|
|
8
|
+
<td class="num">
|
|
9
|
+
<% if node.claims_executions? %>
|
|
10
|
+
<%= link_to number_with_delimiter(node.claimed_count), jobs_path(state: :in_progress) %>
|
|
11
|
+
<% else %>
|
|
12
|
+
—
|
|
13
|
+
<% end %>
|
|
14
|
+
</td>
|
|
15
|
+
<td>
|
|
16
|
+
<span class="fd-hb">
|
|
17
|
+
<span class="fd-led <%= node.freshness %>"></span>
|
|
18
|
+
<%= fd_ago(node.last_heartbeat_at) %>
|
|
19
|
+
</span>
|
|
20
|
+
</td>
|
|
21
|
+
<td class="num">
|
|
22
|
+
<% if node.prunable? %>
|
|
23
|
+
<%= button_to "Prune", prune_process_path(node.id), class: "fd-btn sm danger",
|
|
24
|
+
form_class: "fd-inline-form",
|
|
25
|
+
data: { turbo_confirm: "Prune #{node.kind} #{node.name}? Its claimed jobs will be marked failed." } %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</td>
|
|
28
|
+
</tr>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% content_for :page_title, "Processes" %>
|
|
2
|
+
|
|
3
|
+
<turbo-frame id="fd-processes"
|
|
4
|
+
target="_top"
|
|
5
|
+
data-controller="refresh"
|
|
6
|
+
data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
|
|
7
|
+
data-refresh-url-value="<%= processes_path %>">
|
|
8
|
+
<%= render "flightdeck/processes/fleet", registry: @registry %>
|
|
9
|
+
</turbo-frame>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<turbo-stream action="append" target="fd-toasts">
|
|
2
|
+
<template><%= render partial: "flightdeck/shared/toast", formats: [ :html ], locals: { toast: @toast } %></template>
|
|
3
|
+
</turbo-stream>
|
|
4
|
+
|
|
5
|
+
<turbo-stream action="replace" target="fd-processes">
|
|
6
|
+
<template>
|
|
7
|
+
<turbo-frame id="fd-processes"
|
|
8
|
+
target="_top"
|
|
9
|
+
data-controller="refresh"
|
|
10
|
+
data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
|
|
11
|
+
data-refresh-url-value="<%= processes_path %>">
|
|
12
|
+
<%= render partial: "flightdeck/processes/fleet", formats: [ :html ], locals: { registry: @registry } %>
|
|
13
|
+
</turbo-frame>
|
|
14
|
+
</template>
|
|
15
|
+
</turbo-stream>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<% if stats.rows.empty? %>
|
|
2
|
+
<div class="fd-card"><div class="fd-empty">No queues yet. They appear as soon as jobs are enqueued.</div></div>
|
|
3
|
+
<% else %>
|
|
4
|
+
<div class="fd-queue-cards">
|
|
5
|
+
<% stats.rows.each do |queue| %>
|
|
6
|
+
<div class="fd-card fd-qcard<%= " paused-card" if queue.paused? %>" id="<%= dom_id_for_queue(queue.name) %>">
|
|
7
|
+
<div class="top">
|
|
8
|
+
<h3><%= queue.name %></h3>
|
|
9
|
+
<% if queue.paused? %>
|
|
10
|
+
<span class="fd-pill paused">PAUSED · <%= fd_duration(queue.paused_for) %></span>
|
|
11
|
+
<% else %>
|
|
12
|
+
<span class="fd-pill ready">ACTIVE</span>
|
|
13
|
+
<% end %>
|
|
14
|
+
<div class="right">
|
|
15
|
+
<% if queue.paused? %>
|
|
16
|
+
<%= button_to "▶ Resume", resume_queues_path(name: queue.name),
|
|
17
|
+
class: "fd-btn sm primary", form_class: "fd-inline-form" %>
|
|
18
|
+
<% else %>
|
|
19
|
+
<%= button_to "⏸ Pause", pause_queues_path(name: queue.name),
|
|
20
|
+
class: "fd-btn sm", form_class: "fd-inline-form",
|
|
21
|
+
data: { turbo_confirm: "Pause #{queue.name}? Workers will stop picking up its jobs until you resume it." } %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="stats">
|
|
27
|
+
<div>
|
|
28
|
+
<div class="k">Depth</div>
|
|
29
|
+
<div class="v"><%= number_with_delimiter(queue.depth) %></div>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<div class="k">Latency</div>
|
|
33
|
+
<div class="v"><%= queue.latency ? fd_duration(queue.latency) : "—" %></div>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<div class="k">Rate · 1h</div>
|
|
37
|
+
<div class="v"><%= number_with_delimiter(queue.rate_per_hour) %><small>/h</small></div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<%= render "flightdeck/metrics/sparkline",
|
|
42
|
+
sparkline: Flightdeck::Metrics::Sparkline.new(sparklines[queue.name] || []) %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% content_for :page_title, "Queues" %>
|
|
2
|
+
|
|
3
|
+
<turbo-frame id="fd-queues"
|
|
4
|
+
target="_top"
|
|
5
|
+
data-controller="refresh"
|
|
6
|
+
data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
|
|
7
|
+
data-refresh-url-value="<%= queues_path %>">
|
|
8
|
+
<%= render "flightdeck/queues/cards", stats: @stats, sparklines: @sparklines %>
|
|
9
|
+
</turbo-frame>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<turbo-stream action="append" target="fd-toasts">
|
|
2
|
+
<template><%= render partial: "flightdeck/shared/toast", formats: [ :html ], locals: { toast: @toast } %></template>
|
|
3
|
+
</turbo-stream>
|
|
4
|
+
|
|
5
|
+
<turbo-stream action="replace" target="fd-queues">
|
|
6
|
+
<template>
|
|
7
|
+
<turbo-frame id="fd-queues"
|
|
8
|
+
target="_top"
|
|
9
|
+
data-controller="refresh"
|
|
10
|
+
data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
|
|
11
|
+
data-refresh-url-value="<%= queues_path %>">
|
|
12
|
+
<%= render partial: "flightdeck/queues/cards", formats: [ :html ], locals: { stats: @stats, sparklines: @sparklines } %>
|
|
13
|
+
</turbo-frame>
|
|
14
|
+
</template>
|
|
15
|
+
</turbo-stream>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div class="fd-card">
|
|
2
|
+
<div class="fd-card-hd">
|
|
3
|
+
<h3>Recurring tasks</h3>
|
|
4
|
+
<span class="sub"><%= pluralize(catalog.rows.size, "task") %> registered</span>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="fd-scroll-x">
|
|
7
|
+
<table class="fd-data">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Key</th>
|
|
11
|
+
<th>Job</th>
|
|
12
|
+
<th>Schedule</th>
|
|
13
|
+
<th class="num">Last run</th>
|
|
14
|
+
<th class="num">Next run</th>
|
|
15
|
+
<th>Status</th>
|
|
16
|
+
<th style="width:110px"></th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% if catalog.rows.empty? %>
|
|
21
|
+
<tr><td colspan="7"><div class="fd-empty">No recurring tasks are configured.</div></td></tr>
|
|
22
|
+
<% else %>
|
|
23
|
+
<% catalog.rows.each do |row| %>
|
|
24
|
+
<tr id="<%= dom_id_for_recurring_task(row.id) %>">
|
|
25
|
+
<td class="cls"><%= row.key %></td>
|
|
26
|
+
<td class="dim"><%= row.job_class %></td>
|
|
27
|
+
<td class="fd-cron">
|
|
28
|
+
<%= row.schedule %>
|
|
29
|
+
<% if row.human_schedule %><small><%= row.human_schedule %></small><% end %>
|
|
30
|
+
</td>
|
|
31
|
+
<td class="num dim">
|
|
32
|
+
<%= row.ever_run? ? fd_ago(row.last_run_at) : "never" %>
|
|
33
|
+
</td>
|
|
34
|
+
<td class="num">
|
|
35
|
+
<% if row.next_run_at %>
|
|
36
|
+
<span title="<%= fd_full_time(row.next_run_at) %>">in <%= fd_duration(row.next_run_in) %></span>
|
|
37
|
+
<% else %>
|
|
38
|
+
—
|
|
39
|
+
<% end %>
|
|
40
|
+
</td>
|
|
41
|
+
<td><%= fd_recurring_status_pill(row.last_status) %></td>
|
|
42
|
+
<td class="num">
|
|
43
|
+
<%= button_to "▶ Run now", run_recurring_task_path(row.id),
|
|
44
|
+
class: "fd-btn sm", form_class: "fd-inline-form",
|
|
45
|
+
data: { turbo_confirm: "Enqueue #{row.key} now?" } %>
|
|
46
|
+
</td>
|
|
47
|
+
</tr>
|
|
48
|
+
<% end %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</tbody>
|
|
51
|
+
</table>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% content_for :page_title, "Recurring tasks" %>
|
|
2
|
+
|
|
3
|
+
<turbo-frame id="fd-recurring"
|
|
4
|
+
target="_top"
|
|
5
|
+
data-controller="refresh"
|
|
6
|
+
data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
|
|
7
|
+
data-refresh-url-value="<%= recurring_tasks_path %>">
|
|
8
|
+
<%= render "flightdeck/recurring_tasks/table", catalog: @catalog %>
|
|
9
|
+
</turbo-frame>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<turbo-stream action="append" target="fd-toasts">
|
|
2
|
+
<template><%= render partial: "flightdeck/shared/toast", formats: [ :html ], locals: { toast: @toast } %></template>
|
|
3
|
+
</turbo-stream>
|
|
4
|
+
|
|
5
|
+
<turbo-stream action="replace" target="fd-recurring">
|
|
6
|
+
<template>
|
|
7
|
+
<turbo-frame id="fd-recurring"
|
|
8
|
+
target="_top"
|
|
9
|
+
data-controller="refresh"
|
|
10
|
+
data-refresh-interval-value="<%= (Flightdeck.config.poll_interval.to_f * 1000).round %>"
|
|
11
|
+
data-refresh-url-value="<%= recurring_tasks_path %>">
|
|
12
|
+
<%= render partial: "flightdeck/recurring_tasks/table", formats: [ :html ], locals: { catalog: @catalog } %>
|
|
13
|
+
</turbo-frame>
|
|
14
|
+
</template>
|
|
15
|
+
</turbo-stream>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div class="<%= fd_toast_level_class(toast[:level]) %>"
|
|
2
|
+
role="status"
|
|
3
|
+
data-controller="toast"
|
|
4
|
+
data-toast-timeout-value="<%= toast[:continuable] ? 12_000 : 6_000 %>">
|
|
5
|
+
<span><%= toast[:message] %></span>
|
|
6
|
+
<button type="button" class="fd-toast-close" data-action="toast#dismiss" aria-label="Dismiss">✕</button>
|
|
7
|
+
</div>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%= content_for?(:title) ? yield(:title) : "Flightdeck" %></title>
|
|
7
|
+
<meta name="turbo-root" content="<%= flightdeck.root_path %>">
|
|
8
|
+
<%= csrf_meta_tags %>
|
|
9
|
+
<% if (css = flightdeck_asset_path("flightdeck.css")) %>
|
|
10
|
+
<link rel="stylesheet" href="<%= css %>">
|
|
11
|
+
<% end %>
|
|
12
|
+
<% if (js = flightdeck_asset_path("flightdeck.js")) %>
|
|
13
|
+
<script src="<%= js %>" defer></script>
|
|
14
|
+
<% end %>
|
|
15
|
+
</head>
|
|
16
|
+
<body data-controller="theme">
|
|
17
|
+
<div class="fd-app">
|
|
18
|
+
<aside class="fd-side">
|
|
19
|
+
<div class="fd-brand">
|
|
20
|
+
<%= flightdeck_brand_svg %>
|
|
21
|
+
<span class="fd-brand-word">FLIGHT<em>DECK</em></span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<nav class="fd-nav">
|
|
25
|
+
<%= link_to flightdeck.root_path, class: "fd-nav-link#{" on" if current_page?(flightdeck.root_path)}" do %>
|
|
26
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M1.5 13.5v-5h3v5m1.5 0v-9h3v9m1.5 0v-12h3v12" stroke-linejoin="round"/></svg>
|
|
27
|
+
Overview
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<span class="fd-nav-section">Jobs</span>
|
|
31
|
+
|
|
32
|
+
<%= link_to flightdeck.jobs_path, class: "fd-nav-link#{" on" if fd_nav_active?(:all)}" do %>
|
|
33
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="1.5" y="2.5" width="12" height="10" rx="1.5"/><path d="M4.5 6h6M4.5 9h4"/></svg>
|
|
34
|
+
All jobs <span class="fd-nav-count"><%= fd_nav_count(:all) %></span>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<%= link_to flightdeck.jobs_path(state: :failed), class: "fd-nav-link#{" on" if fd_nav_active?(:failed)}" do %>
|
|
38
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="7.5" cy="7.5" r="6"/><path d="M7.5 4.5v3.5M7.5 10.4v.2"/></svg>
|
|
39
|
+
Failed <span class="fd-nav-count"><%= fd_nav_count(:failed) %></span>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<span class="fd-nav-section">Infrastructure</span>
|
|
43
|
+
|
|
44
|
+
<%= link_to flightdeck.queues_path, class: "fd-nav-link#{" on" if controller_name == "queues"}" do %>
|
|
45
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M2 4h11M2 7.5h11M2 11h7"/></svg>
|
|
46
|
+
Queues <span class="fd-nav-count"><%= fd_nav_queue_count %></span>
|
|
47
|
+
<% end %>
|
|
48
|
+
|
|
49
|
+
<%= link_to flightdeck.processes_path, class: "fd-nav-link#{" on" if controller_name == "processes"}" do %>
|
|
50
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="2" y="2" width="11" height="7" rx="1.5"/><path d="M5 13h5M7.5 9v4"/></svg>
|
|
51
|
+
Processes <span class="fd-nav-count <%= "alert" if fd_nav_any_dead_processes? %>"><%= fd_nav_process_count %></span>
|
|
52
|
+
<% end %>
|
|
53
|
+
|
|
54
|
+
<%= link_to flightdeck.recurring_tasks_path, class: "fd-nav-link#{" on" if controller_name == "recurring_tasks"}" do %>
|
|
55
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="7.5" cy="7.5" r="6"/><path d="M7.5 4v3.5l2.5 1.5"/></svg>
|
|
56
|
+
Recurring <span class="fd-nav-count"><%= fd_nav_recurring_count %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
</nav>
|
|
59
|
+
|
|
60
|
+
<div class="fd-side-foot">
|
|
61
|
+
<b>flightdeck</b><br>
|
|
62
|
+
v<%= Flightdeck::VERSION %>
|
|
63
|
+
</div>
|
|
64
|
+
</aside>
|
|
65
|
+
|
|
66
|
+
<div class="fd-main">
|
|
67
|
+
<div class="fd-topbar">
|
|
68
|
+
<h2><%= content_for?(:page_title) ? yield(:page_title) : "Overview" %></h2>
|
|
69
|
+
<div class="fd-topbar-right">
|
|
70
|
+
<span class="fd-clock"
|
|
71
|
+
data-controller="clock"
|
|
72
|
+
data-clock-offset-value="<%= fd_zone_offset_seconds %>"
|
|
73
|
+
data-clock-label-value="<%= fd_zone_label %>"><%= fd_clock_now %></span>
|
|
74
|
+
|
|
75
|
+
<button class="fd-live" type="button" aria-pressed="true"
|
|
76
|
+
data-controller="live"
|
|
77
|
+
data-live-interval-value="<%= fd_poll_ms %>"
|
|
78
|
+
data-action="live#toggle">
|
|
79
|
+
<span class="fd-dot"></span>
|
|
80
|
+
<span data-live-target="label">LIVE · <%= fd_poll_seconds %>s</span>
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<%# Which icon shows is decided by theme tokens, so it is correct on
|
|
84
|
+
first paint without waiting for JavaScript. The controller
|
|
85
|
+
refines the label to name the action once it connects. %>
|
|
86
|
+
<button class="fd-theme-toggle" type="button"
|
|
87
|
+
data-action="theme#toggle" data-theme-target="button"
|
|
88
|
+
aria-label="Switch theme" title="Switch theme">
|
|
89
|
+
<svg class="fd-icon-sun" width="15" height="15" viewBox="0 0 16 16" fill="none"
|
|
90
|
+
stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true">
|
|
91
|
+
<circle cx="8" cy="8" r="3.1"/>
|
|
92
|
+
<path d="M8 1.2v1.6M8 13.2v1.6M14.8 8h-1.6M2.8 8H1.2M12.8 3.2l-1.1 1.1M4.3 11.7l-1.1 1.1M12.8 12.8l-1.1-1.1M4.3 4.3 3.2 3.2"/>
|
|
93
|
+
</svg>
|
|
94
|
+
<svg class="fd-icon-moon" width="15" height="15" viewBox="0 0 16 16" fill="none"
|
|
95
|
+
stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" aria-hidden="true">
|
|
96
|
+
<path d="M13.4 9.8A5.9 5.9 0 0 1 6.2 2.6a5.9 5.9 0 1 0 7.2 7.2z"/>
|
|
97
|
+
</svg>
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<main class="fd-content">
|
|
103
|
+
<%= yield %>
|
|
104
|
+
</main>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div id="fd-toasts" class="fd-toasts" aria-live="polite" aria-atomic="false"></div>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Flightdeck::Engine.routes.draw do
|
|
4
|
+
root to: "overview#index"
|
|
5
|
+
|
|
6
|
+
resources :jobs, only: %i[index show] do
|
|
7
|
+
member do
|
|
8
|
+
post :retry, to: "jobs/retries#create"
|
|
9
|
+
post :discard, to: "jobs/discards#create"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
collection do
|
|
13
|
+
post :retry, to: "jobs/retries#create", as: :bulk_retry
|
|
14
|
+
post :discard, to: "jobs/discards#create", as: :bulk_discard
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Queue names are arbitrary strings (dots, slashes, colons all legal), so the
|
|
19
|
+
# name travels as a parameter rather than as a path segment.
|
|
20
|
+
resources :queues, only: :index do
|
|
21
|
+
collection do
|
|
22
|
+
post :pause
|
|
23
|
+
post :resume
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
resources :processes, only: :index do
|
|
28
|
+
member { post :prune }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
resources :recurring_tasks, only: :index do
|
|
32
|
+
member { post :run }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
get "assets/:name",
|
|
36
|
+
to: "assets#show",
|
|
37
|
+
as: :asset_file,
|
|
38
|
+
format: false,
|
|
39
|
+
constraints: { name: /flightdeck-[0-9a-f]{12}\.(?:css|js)/ }
|
|
40
|
+
end
|