Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Policy. The layout now renders `csp_meta_tag`, so the bundled Turbo can nonce
the `<style>`/`<script>` it injects at runtime. No effect on hosts without a
CSP configured.
- The dashboard no longer relies on inline `style` attributes, so column widths
and the queue depth bars survive a strict Content Security Policy.

## [1.1.0] - 2026-08-12

Expand Down

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions app/assets/flightdeck/manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"flightdeck.css": {
"file": "flightdeck-ad6cd9e6c4b8.css",
"digest": "ad6cd9e6c4b8",
"sha256": "ad6cd9e6c4b8c1df7488915a2051938a78e267b03c1e79973d9e64958d321ec7",
"file": "flightdeck-38ba36531cee.css",
"digest": "38ba36531cee",
"sha256": "38ba36531cee83452b312ce04f858f83eeb262b498b6614c775e1947ba2303da",
"content_type": "text/css; charset=utf-8",
"size": 31546
"size": 31759
},
"flightdeck.js": {
"file": "flightdeck-8c7c4d4896b4.js",
Expand Down
2 changes: 1 addition & 1 deletion app/views/flightdeck/jobs/_failed_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</td>
<%# Inside a group the header already names the exception class, so the row
shows only the message. %>
<td class="args" style="max-width:260px" title="<%= row.error_summary.to_s %>">
<td class="args fd-error-cell" title="<%= row.error_summary.to_s %>">
<%= fd_error_cell(row.error_summary, grouped: group_key.present?) %>
</td>
<td><%= fd_queue_badge(row.queue_name) %></td>
Expand Down
6 changes: 3 additions & 3 deletions app/views/flightdeck/jobs/_list.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<table class="fd-data">
<thead>
<tr>
<th style="width:34px">
<th class="fd-col-select">
<input type="checkbox" data-selection-target="all" data-action="selection#toggleAll"
aria-label="Select all failed jobs on this page">
</th>
Expand All @@ -34,7 +34,7 @@
<th>Queue</th>
<th class="num">Attempts</th>
<th class="num">Failed</th>
<th style="width:150px"></th>
<th class="fd-col-actions-lg"></th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -77,7 +77,7 @@
<th class="num">Priority</th>
<th class="num">Enqueued</th>
<th class="num">Progress</th>
<th style="width:90px"></th>
<th class="fd-col-actions"></th>
</tr>
</thead>
<tbody>
Expand Down
9 changes: 5 additions & 4 deletions app/views/flightdeck/overview/_queues.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<th class="num">Depth</th>
<th class="num">Latency</th>
<th>Last <%= Flightdeck::Metrics::Series::SPARKLINE_HOURS %>h</th>
<th style="width:90px"></th>
<th class="fd-col-actions"></th>
</tr>
</thead>
<tbody>
Expand All @@ -28,9 +28,10 @@
<td class="num dim"><%= queue.latency ? fd_duration(queue.latency) : "—" %></td>
<td><%= render "flightdeck/metrics/sparkline", sparkline: overview.sparkline_for(queue.name) %></td>
<td>
<div class="fd-depth-bar">
<i style="width: <%= fd_depth_share(queue.depth, overview.max_queue_depth) %>%"></i>
</div>
<svg class="fd-depth-bar" viewBox="0 0 100 4" preserveAspectRatio="none" aria-hidden="true">
<rect class="fd-depth-track" width="100" height="4"/>
<rect class="fd-depth-fill" width="<%= fd_depth_share(queue.depth, overview.max_queue_depth) %>" height="4"/>
</svg>
</td>
</tr>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/flightdeck/processes/_fleet.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<th>Configuration</th>
<th class="num">Claimed</th>
<th>Heartbeat</th>
<th style="width:90px"></th>
<th class="fd-col-actions"></th>
</tr>
</thead>
<tbody>
Expand Down
2 changes: 1 addition & 1 deletion app/views/flightdeck/processes/_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<td class="cls<%= " fd-indent" if child %>">
<%= "↳ " if child %><%= node.kind %>
</td>
<td class="dim fd-mono" style="font-size:12px"><%= node.hostname.presence || "—" %></td>
<td class="dim fd-mono"><%= node.hostname.presence || "—" %></td>
<td class="num"><%= node.pid %></td>
<td class="dim"><%= node.config_summary.presence || "—" %></td>
<td class="num">
Expand Down
2 changes: 1 addition & 1 deletion app/views/flightdeck/recurring_tasks/_table.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<th class="num">Last run</th>
<th class="num">Next run</th>
<th>Status</th>
<th style="width:110px"></th>
<th class="fd-col-actions-md"></th>
</tr>
</thead>
<tbody>
Expand Down
10 changes: 8 additions & 2 deletions assets-src/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,11 @@ table.fd-data .args {
max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
table.fd-data .dim { color: var(--ink-3); font-size: 12px; }
table.fd-data .args.fd-error-cell { max-width: 260px; }
table.fd-data th.fd-col-select { width: 34px; }
table.fd-data th.fd-col-actions { width: 90px; }
table.fd-data th.fd-col-actions-md { width: 110px; }
table.fd-data th.fd-col-actions-lg { width: 150px; }
.fd-scroll-x { overflow-x: auto; }
.fd-empty { padding: 28px 14px; text-align: center; color: var(--ink-3); font-size: 13px; }

Expand Down Expand Up @@ -995,8 +1000,9 @@ table.fd-data td.fd-indent { padding-left: 28px; }
.fd-spark-line { fill: none; stroke: var(--s1); stroke-width: 1.5; }
.fd-spark-area { fill: var(--s1); opacity: .14; stroke: none; }

.fd-depth-bar { height: 4px; border-radius: 2px; background: var(--panel-3); overflow: hidden; min-width: 70px; }
.fd-depth-bar i { display: block; height: 100%; border-radius: 2px; background: var(--accent); }
.fd-depth-bar { display: block; width: 100%; height: 4px; min-width: 70px; border-radius: 2px; overflow: hidden; }
.fd-depth-track { fill: var(--panel-3); }
.fd-depth-fill { fill: var(--accent); }

/* --------------------------------------------------------------------------
Fleet strip
Expand Down
Binary file modified docs/screenshots/job-detail-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/jobs-failed-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/overview-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/overview-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/processes-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/queues-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions test/integration/csp_inline_styles_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require "test_helper"

# A nonce cannot rescue an inline style *attribute*: those are governed by
# style-src-attr, which falls back to style-src, so a strict `style-src 'self'`
# blocks every one of them — column widths collapse and the queue depth bars
# vanish. The dashboard therefore renders no style attributes at all; widths and
# fills belong in the stylesheet or in SVG geometry attributes.
class Flightdeck::CspInlineStylesTest < FlightdeckIntegrationTest
test "no dashboard page renders an inline style attribute" do
seed_everything

dashboard_paths.each { |path| assert_no_inline_styles(path) }
end

# Guards against the assertion above passing vacuously: the depth bar is the
# one width that varies per request, so it has to be on the page and non-zero.
test "the overview renders a non-zero queue depth bar without inline styles" do
seed_everything

get_fd "/flightdeck"
fills = Nokogiri::HTML(response.body).css(".fd-depth-bar .fd-depth-fill")

assert_predicate fills, :any?, "expected the overview to render queue depth bars"
assert fills.any? { |fill| fill["width"].to_i.positive? },
"expected at least one queue with a non-zero depth, or this test proves nothing"
end

private
def dashboard_paths
paths = %w[/flightdeck /flightdeck/queues /flightdeck/processes /flightdeck/recurring_tasks]
paths += Flightdeck::JobsQuery::STATES.map { |state| "/flightdeck/jobs?state=#{state}" }
paths + @job_paths
end

def assert_no_inline_styles(path)
get_fd path
assert_response :success, "#{path} did not render"

styled = Nokogiri::HTML(response.body).css("[style]")
assert_empty styled.map { |element| element.to_html.truncate(120) },
"#{path} renders inline style attributes; move them into assets-src/input.css " \
"(or an SVG geometry attribute) so a strict CSP cannot break the layout"
end

def seed_everything
scenario = create_full_scenario
create_fleet
task = create_recurring_task(key: "digest")
record_recurring_run(task, run_at: 1.hour.ago)
create_finished_job(queue_name: "critical", finished_at: 30.minutes.ago)

# Both detail code paths: the failed one renders the error panel, the
# ready one does not.
@job_paths = [
"/flightdeck/jobs/#{scenario[:failed].id}?state=failed",
"/flightdeck/jobs/#{scenario[:ready].id}"
]
end
end