An audit of 1,980 text/surface pairs (every steward.css rule and every inline
template style, tints composited over page, card and hovered row) found:
- Red, yellow and orange status text on its own 20% tint over a hovered row
at 3.97-4.06:1 (badges, count badge, alert-error, btn-warn). --red and
--yellow are lifted 15% toward the primary text, --orange 18%, as --blue
already was: worst case now 4.54:1.
- Inputs, selects, ghost and range buttons outlined in --border-mid at
1.4-1.9:1, on surfaces 1.1:1 apart, so the outline is what marks them.
New --border-control (45% toward secondary text): 3.13:1 worst. The
dashboard switcher and widget-drawer fields drop their inline styles for
the shared .input class.
- The breadcrumb separator painted in a border colour (1.4-1.9:1): now
--text-dim (4.64:1) and aria-hidden, since it is decoration.
tests/test_contrast.py resolves the tokens (color-mix and tints included)
and holds every drawn pair to 4.5:1, status text on its tint to 4.5:1 and
control outlines to 3:1, with a test that the guard catches the old pairs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5091 "Local assets". base.html, the dashboard and the share view loaded
htmx and Chart.js from unpkg/jsdelivr, Gridstack from jsdelivr and their
fonts from Google. On an instance with no outbound network (rule 164) every
page lost htmx, and so every button and poll; and each page load told those
hosts about the visitor (rule 155).
- steward/static/vendor: htmx 2.0.4, Chart.js 4.4.4, Gridstack 12.6.0 (the
versions the pages already used), with their licences (0BSD, MIT, MIT).
- steward/static/fonts: Inter, Fraunces and JetBrains Mono, the design
system's faces (SIL OFL, licences beside them), every unicode-range
subset, with fonts.css. The serif face is now Fraunces in place of
EB Garamond (base) and Libertinus Serif (share view); applying Inter and
the type scale is the tokens step.
- Templates use url_for('static'); /static already passes every gate.
- Guard (tests/test_no_remote_assets.py): no template or stylesheet loads a
script, stylesheet, font or image from another origin (comments stripped,
example URLs in help text allowed), with a self-test that it can fail;
every url_for('static') file and every font the sheet names exists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5031 "Forged-request protection: SameSite cookie, per-session CSRF token
on every non-GET, HTMX header". A page on another site could make a
logged-in browser POST to Steward with the session cookie attached.
- steward/auth/csrf.py: a per-session token, checked in one before_request
for every POST/PUT/PATCH/DELETE (header X-CSRF-Token or form field
csrf_token, constant-time compare). The bearer-authenticated host-agent
ingest is exempt; /mcp never reaches Quart.
- Every POST form renders {{ csrf_field() }}; base.html carries the token
in a meta tag and adds it to every HTMX request; the dashboard's fetch()
calls send it.
- The session cookie is SameSite=Lax; login issues a fresh token.
- PUBLIC_BASE_URL is now loaded from Settings at startup (it was only set
when Settings → General was saved).
- tests/test_csrf.py: refusal, acceptance, rotation, the ingest exemption,
and a guard that every POST form in the templates carries the field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator chose a single canonical summary. Repurpose the status_overview
widget into 'Overview' (host count + monitor up/down/pending + Alerts link;
key kept so existing dashboards upgrade in place) and remove the redundant
fixed top strip from the dashboard view. Drops _get_summary_stats and its
now-unused PingResult/DnsResult imports (rule 22).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Milestone 72 phase D — the dashboard view IS the edit surface (operator ask):
- /d/<id> renders the grid via Gridstack in STATIC mode — positioned exactly like
before, live HTMX widget bodies keep polling. An "Edit" button flips the same
grid interactive (grid.setStatic(false)) in place, so you drag/resize over real
data. "Done" flips it back. Layout autosaves on change.
- The widget picker is now a bottom drawer (position:fixed overlay) revealed by
body.dash-editing — so the dashboard width is identical entering/leaving edit.
- Add: POST returns a single grid item; JS inserts it + grid.makeWidget +
htmx.process so it loads live data. Remove: POST 204 + grid.removeWidget.
Per-panel drag handle + remove ✕ are in the DOM for editors, shown only while
editing.
- Gridstack loads for everyone (viewers get the static grid); edit wiring is
gated on can_edit. Mobile collapses to one column.
- Removed the separate /d/<id>/edit route + edit.html + _edit_panels.html
(rule 22). Dashboard-list Edit and new-dashboard create now deep-link
/d/<id>?edit=1 which opens edit mode on load.
Browser-only behaviour — CI can't exercise it; needs an operator visual check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Milestone 72 phase B — Grafana-style drag-resize grid for dashboard widgets:
- DashboardWidget: replace `position` with a 12-col grid placement
(grid_x/grid_y/grid_w/grid_h). Migration 0021 backfills the old position
order into a 3-up grid (4 cols x 4 cells each) and drops position.
- View + share render a static CSS grid from x/y/w/h: fixed cell height
(h * 70px) with the body scrolling, so the arranged layout is what's shown;
collapses to a single column under 820px.
- Edit view: Gridstack.js 12.6.0 (vanilla, CDN, pinned) — drag the title bar
to move, drag a corner/edge to resize; every change autosaves to a new
/d/<id>/edit/layout endpoint. Replaces the SortableJS position reorder.
- add_widget appends at the bottom-left; remove no longer renumbers.
_get_widgets now orders by grid position (drives DOM + mobile fallback order).
Note: Gridstack drag-resize is browser-only, so CI can't exercise it — needs an
operator visual check of the edit experience.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the Python package directory, CLI command, env var prefix,
docker-compose service/container/image, Postgres role/db, and all
visible branding. Marketing form is "Fabled Steward".
Clean break from the previous rebrand: drops the fabledscryer→roundtable
import shim in __init__.py and the FABLEDSCRYER_* env var fallback in
config.py and migrations/env.py. Env vars are now STEWARD_* only.
Heads-up for existing deployments:
- Postgres user/db renamed fabledscryer → steward in docker-compose.yml.
Existing volumes need the role/db renamed inside Postgres, or override
POSTGRES_USER/POSTGRES_DB to keep the old names.
- Host-agent systemd unit is now steward-agent.service. Existing agents
keep running under the old name; reinstall to switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>