A focused, single-branch sweep of the entire UI surface (templates/, static/js/, static/css/). 51 commits, each scoped to one fix, organized into four waves: cross-cutting refactors (R-1..R-6), per-page quick wins (QW-1..QW-15), per-page Tier A / Tier B work, and per-page section passes (2.x base, 3.x dashboard, 4.x settings, 5.x activity, 6.x login, 7.x help, 8.x cross-cutting).
No API breakage. No data migration. No new env vars. All changes are at the template / asset / client-side layer, plus three small modules/web/ additions to support ?next= and current_user rendering.
Cross-cutting refactors
- R-1 —
templates/settings.htmlAlpine root repair. A long-standing structural bug had Alpine partials sitting outside the rootx-dataelement due to an<!-- comment inside attribute -->that broke HTML parsing. Tabs and modals were silently un-reactive in some browsers. Re-parented partials inside the main card and removed the comment-in-attribute. - R-2 — Standardized modal macro. New
templates/partials/_modal.htmlwith a{% call modal(id, title, size) %}macro: dialog role,aria-modal,aria-labelledby, header with[data-modal-close], scrollable body, panel sizing. Paired withCertMate.modal.open/closein static/js/certmate.js: global Esc handler, backdrop click, focus trap,modal:closeCustomEvent, MutationObserver-based discovery so partials added at runtime are wired automatically. Settings modals (addAccountModal,editAccountModal) migrated as the first callsites. - R-3 — Component-class scaffold in static/css/input.css. New
@layer componentswith.btn,.btn-primary/secondary/danger/ghost,.btn-sm/lg,.card,.badge,.badge-success/warning/error/info,.form-input,.form-select,.form-label— all defined via@apply. tailwind.config.js safelist added so PurgeCSS doesn't drop classes until the migration of callsites lands in a follow-up sprint. No existing markup changed in this release. - R-5 — Dashboard mobile card meta block. On
md:hiddenwidths each row gets a secondary line with expiry, provider, and deployment status. Previously these only rendered on desktop; mobile users couldn't tell certs apart at a glance. - R-6 — Debug surface gating.
?debug=1opt-in writeslocalStorage.certmate_debug='1'and exposesCertMate.debugEnabled. All[data-debug-control]elements stay hidden until both the localStorage flag AND/api/auth/mereturnsrole === 'admin'. Two-layer defense-in-depth — URL opt-in plus role check.
templates/base.html (2.1–2.3)
- 2.1 / A1 — Theme toggle icon swap switched from
style.displaytoclassList.toggle('hidden')so Tailwind's dark-mode variant cascades correctly. - 2.2 / A4 — API Docs
/redoclink added to the desktop nav alongside/docs/. - 2.2 / B1 — Logout button now server-side rendered via
{% if current_user %}instead of a 500 ms client-sidefetch('/api/auth/me')probe. New Jinjacontext_processorin modules/web/routes.py injectscurrent_user. - 2.2 / B2 — Mobile-only search button (
sm:hidden, icon-only). - 2.3 / A2 —
aria-labelon every icon-only top-nav button (theme, keyboard shortcuts, notifications, logout, search). - 2.3 / A3 —
aria-current="page"on the active link in both desktop and mobile bottom nav. - 2.3 / B3 — Notification panel migrated to a proper Disclosure pattern:
aria-expanded,aria-controls, Esc handler, focus restoration via_closeNotifPanel(). No focus trap (it's a disclosure, not a dialog).
templates/index.html (3.1–3.3) — dashboard
- 3.1 / A1 — Debug button + console gated behind
?debug=1per R-6. - 3.1 / A2 — Loading modal split
hidden/flexclasses correctly so it shows/hides without an extra reflow tick. - 3.2 / A3 — Explanatory
title/aria-labelon the Check-All checkbox. - 3.2 / A4 — Emoji prefixes dropped from CA provider
<option>labels. - 3.2 / A5 — Quick Tips bullets replaced with a link to the Help page (single source of truth).
- 3.2 / B3 — Cert-detail panel renders a skeleton on open and clears content on close so stale data never flashes.
- 3.2 / B4 — Stats cards render a JS-driven skeleton via
STAT_METRICS_COUNT; empty container shipped in the template. - 3.3 / B1 —
aria-label="${title} ${domain}"on every per-row action button. - 3.3 / B2 — Sortable column headers: implicit
columnheaderrole on<th>, internal<button>for interaction,aria-sorttoggled viasetAttributeon each sort. - QW-4 — Dark-mode variants on the curl-snippet modal.
- QW-5 — Confirm guard on the delete action via
CertMate.confirm. - QW-11 —
autocomplete="off"on cert-create domain inputs (primary + SAN), plus a more permissive SAN parser (,;\n\tseparators, dedup). - QW-12 — Form lock during in-flight create requests:
isCreatingCertflag, disabled fields, spinner. - QW-15 —
normalizeHostname()strips scheme / port / path / trailing dot on submit, preserves*.wildcard prefix.
templates/settings.html (4.1–4.2)
- 4.1 / A1 — Debug helper renamed
toggleDebugConsole→toggleSettingsDebugConsoleso it no longer collides with the dashboard's helper of the same name. - 4.2 / A2 — Tabs go icon-only on mobile;
aria-selectedbound totab === t.id. - 4.2 / A3 — DNS-scope prefix added to status indicators. Orphan markup retained with an explanatory comment for the follow-up sprint that will migrate it to the new layout.
templates/activity.html (5.1–5.3)
- 5.1 — Differentiated error states via
renderErrorState()instead of a single generic banner. - 5.2 — Date-range filter (Today / 7d / 30d / All), full-text search across loaded entries, skeleton rows during load, clickable cert entries that deep-link into the dashboard detail panel via
?cert=<domain>, server-sidelimitparam + client "Load more" button (also fixed a backend bug —/api/activityreturned a bare array but the client readdata.entries, so the page was always empty), andapi_requestevent type hidden from the default view (still surfaced when filtered). - 5.3 — ARIA semantics:
<ul role="feed">,<li role="article">,aria-busyon the container during load. Errors userole="alert"witharia-live="assertive". - QW-8 — Tooltip with absolute timestamp on every relative time via
absoluteTime().
templates/login.html (6.1–6.3)
- QW-1 — FOUC fix: dark-mode script in
<head>mirrorsbase.htmlso a user who toggled dark inside the app no longer sees a light flash on every/loginredirect.meta theme-colorpaired for light + dark. - 6.2 / A1 —
/loginserver-side redirects to/(or?next=) on a valid session cookie. The previous client-side check is kept as a defensive fallback. - 6.2 / A2 —
autocomplete="username"andautocomplete="current-password"so password managers fill correctly. - 6.2 / A3 —
?next=redirect withsafeNextUrl()open-redirect guard: only same-origin relative paths (/-prefixed, no//). - 6.2 / A4 — Forgot-password hint pointing at the admin + the
scripts/reset_admin_password.pyin-container reset script (no email infra assumption). - 6.3 / A5 — Password visibility toggle:
aria-label,aria-pressed,aria-controlsswapped in lockstep with the icon glyph.
templates/help.html (7.1–7.2)
- 7.1 / A1 — "What's New in v2.0" renamed to "v2.x" with a link to
RELEASE_NOTES.md. - 7.1 / A2 — Clean six-item Quick Links grid (
grid-cols-3) including a Backup card. - 7.2 / A3 — Swagger UI vs ReDoc disambiguated with explicit labels; eight
rel="noopener"adds on outbound links. - 7.2 / B1 — In-page search filter for help sections via
data-help-sectionmarkers.
Cross-cutting (8.x)
- 8.2 / A2 — Graceful red
role="alert"banner ifwindow.Alpine === undefinedafterDOMContentLoaded, so a CDN failure doesn't leave the UI looking broken-but-silent. - 8.2 / A3 — Debug surface admin-role check (R-6 server-side leg).
- 8.3 / A1 — Skip-to-content link as the first body child on every page (keyboard a11y).
Server-side additions
Three small, backwards-compatible additions in modules/web/:
- modules/web/routes.py —
context_processorinjectingcurrent_userinto every Jinja template render. - modules/web/ui_routes.py —
/route setsrequest.current_user = user_infoaftervalidate_session; redirect to login passes?next=request.path. - modules/web/auth_routes.py —
login_page()checks the session cookie server-side and 302s to/(or to?next=if present) instead of always rendering the login form.
Tests
E2E suite run before push: 99 passed, 12 skipped, 2 xfailed in 55.5 s against a freshly built certmate:test image (Docker fixture in tests/conftest.py, port 18888). The 2 xfailed are pre-existing markers in test_ui.py; the 12 skipped are real-cert / opt-in UI tests gated on explicit env. Unit suite green.
Backward compatibility
- No API shape changes. No new required env vars.
- The
?next=parameter is opt-in; absence falls back to/. current_userin Jinja isNonefor unauthenticated requests — no template that uses it assumes it's set.- R-3 component classes are scaffolded only; no callsite migrated, no class renamed.
- Debug surface gating fails-closed: if
/api/auth/meerrors or returns a non-admin role, the surface stays hidden.
Acknowledgement
This is a single-contributor sweep — 51 commits on feature/v3-ui-fixes-2026-05-15, each commit one fix. The discipline came out of the v2.4.x cycle where mixing fixes in single commits made review and rollback harder than they needed to be. Every fix in this release can be reverted in isolation.