- tests/test_breadcrumbs.py: the Prometheus target list is its section's
own top page, like SNMP's, so it carries no trail.
- The integration test's exposition has label braces, which str.format read
as fields: the fake scrape raised, the target was recorded down and no
series landed. It substitutes the counter with replace() now.
- docs/plugins/prometheus.md: adding a target, filtering, how series are
named and stored, alerting, and the Fabled Archives recipe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step #5429 "Reset by email: request, link and new-password page" of
milestone 497 "Passwords: change your own, and reset it by email".
- Sign-in page: "Forgot your password?" while a mail server is set.
- /forgot takes a username or email and answers the same either way; only
an active local account with a real address gets mail. Requests count
per address on their own throttle, so they never lock out sign-in.
- password_resets (migration 0039): a one-time key, stored as its sha256,
working for an hour; asking again replaces it.
- /reset/<key>: the new password, then signed straight in; every other
browser is signed out (sessions_since). Bad keys count as invite keys do.
- Email "[Steward] Reset your password", in the account's own zone.
- Audited: user.password_reset_requested, user.password_reset_by_email.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The operator's ask: an invite link sent by mail, the key in the URL, and
the new user choosing their own username, password and email.
- user_invites (migration 0037): the key's sha256 only, the address, the
role and days the admin chose, who sent it, used/revoked.
- core/invites: create (revokes an older open invite to the address),
renew (new key, new clock), find_pending, the four states, signup checks.
- /invite/<key>, outside sign-in and throttled like login: the invitee's
username, email (prefilled, theirs to change) and password; the account
takes the admin's role, the key is used up in the same transaction and
they are signed in. A bad key gets one plain message.
- Settings › Users: Invite by email (address, role, 1/7/30 days), sent from
the admin in their zone; without a mail server, or if sending fails, the
link is shown once to copy. Invites not yet used are listed with Resend
and Withdraw.
- Audit: user.invited, user.invite_resent, user.invite_revoked, user.joined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The last page of #5214's walk. Settings still described Steward as it was
before monitors were unified and SNMP was rebuilt, drew its tabs below the
breadcrumb, and showed the report schedule in UTC.
- Tabs (#5377): settings/_tabs.html is a settings_tabs() macro called in
each page's section_tabs block, above the trail, as Ansible's are. The
breadcrumb guard also fails any page that {% include %}s a tab strip.
- Report time (#5378): reports.schedule_tz keeps the zone of whoever saved
the schedule; check_and_send compares in it (report_due), and the form shows
the day and hour on the reader's clock (schedule_in) under their zone's
name. "Last sent" uses |when.
- Wording (#5379): the report contents list what the report holds (monitors,
not ping-enabled hosts); "Ping latency" is "Response time" and says what it
grades; plugin detail says enabling loads now and disabling needs a
restart; the monitor interval says it applies on the next check (the
scheduler reads it every pass); the dead list-config branch is gone.
- Traefik (#5380): the 5xx and 4xx cutoffs are settings
(traefik.threshold.error_5xx_pct 0.1, error_4xx_pct 1.0) in a "Traefik
routers" group on Thresholds while the plugin runs, read by router_state
through error_cutoffs(). The Traefik page has a Thresholds button for
admins, to /settings/thresholds/#traefik.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- users.time_zone (0034). An account without one takes the browser's zone
on its first page (POST /account/time-zone/adopt, never over a choice made
by hand); the session follows the account on every request.
- /account: who you are and the zone every page writes its times in,
reached from your name in the sidebar.
- formatting.when: 'Today 06:58', 'Yesterday 23:41', 'Tue 6 Oct 14:12',
'6 Oct 2025 14:12', 24-hour, in the reader's zone; |ago's hover is the
exact local moment instead of UTC; tz_label() for column headers.
- Audit log: When (EDT) with exact times, one detail fact a line, on classes.
- tzdata in the image, and the airgapped smoke checks a zone resolves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tabs were drawn in each page's content block, which base.html renders
after the breadcrumb, so on every nested Ansible page the strip moved down a
line. base.html now has a section_tabs block above the breadcrumb, and every
Ansible page fills it. A guard in test_breadcrumbs.py fails any page that calls
a tabs macro outside that block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Status repeated the Monitors page's counts, check strip and uptime. Monitors
now has one header row and every column Status had: the last 30 checks,
latest response with its trend, uptime over 24h / 7d / 30d (the range picker
goes) and TLS days on every row. /status and /status/rows and the nav link
are removed outright; the dashboard Overview (/status/widget) stays and its
Details link goes to Monitors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5095 "Share view on the shared stylesheet; breadcrumbs, focus-visible and
wide tables".
- dashboard/share_view.html loads theme.css and steward.css; its own copy of
the old purple palette is gone. Only the read-only frame and the x/y/w/h
grid stay inline, written in tokens. HEX_CEILING 75 -> 58.
- Breadcrumbs on settings/general, settings/thresholds, the dashboard share
page and share-link-created. tests/test_breadcrumbs.py fails on any page
extending base.html without a crumbs() trail, unless it is on the named
list of section top pages, logged-out and error pages (rule 134).
- :focus-visible draws --fs-focus-ring on links, buttons, summaries, inputs
and anything with tabindex.
- Any card holding a .table scrolls sideways (card:has(table.table)), so a
wide table no longer widens the page at phone width; .card-flush scrolls
too, main gets min-width:0, and .table-wrap covers a table outside a card.
- tests/test_share_view.py renders the share view: shared sheets, no hex
colour, escaped name, widgets loading through the share token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>