Uh oh!
There was an error while loading. Please reload this page.
Maintenance mode: layout override + scheduled windows + PubSub - #491
Conversation
Replaces the old @show_maintenance assign approach with a dynamic layout swap via socket.private[:live_layout] — the underlying LiveView keeps running so form state and scroll position are preserved when maintenance toggles on or off. URL never changes. Core changes: - Layout override in on_mount hook instead of redirect. When maintenance turns on, put_in socket.private[:live_layout] swaps the layout live; when it ends, PubSub triggers restoration of the original layout - New PhoenixKitWeb.Layouts :maintenance template with countdown timer - HTTP plug renders inline 503 HTML (with Retry-After header) for controller routes, with proper Phoenix.HTML escaping to prevent XSS - Scheduled maintenance windows with start/end UTC datetimes, 1-year upper bound, and 60-second tolerance for datetime-local minute precision - cleanup_expired_schedule auto-disables stale state on every page access - Process.send_after timer unblocks users when scheduled end arrives (clamped to Erlang's 32-bit timeout limit) - PubSub broadcasts on every state change so all connected LiveViews react instantly; admin tabs stay put, user tabs swap layouts - Manual toggle clears expired schedule on enable to avoid stale locks - Activity logging for all admin actions (toggle, content, schedule) - All user-facing strings wrapped in gettext Schedule validation rejects: empty, past start/end, end before start, dates >1 year in the future. Datetime inputs use the system time_zone setting for display and convert to UTC for storage. Extracts timezone helpers (offset_to_seconds, shift_to_offset, parse_datetime_local, format_datetime_local) to PhoenixKit.Utils.Date so they can be tested in isolation. Adds 93 new tests: unit tests for validate_schedule and PubSub, integration tests for Maintenance context and the plug (including XSS regression test), and doctested timezone helpers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move phx-change from individual inputs to the form element so the live preview updates on every keystroke (input-level phx-change on text inputs only fires on blur, making the preview appear broken) - Remove the "Preview" link that navigated to /maintenance. The path went through locale-prefixed routes and got caught by the publishing module's /:language/:group catch-all. The settings page already has an inline Live Preview card rendering the same content, so the link was redundant Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ddon
commented
Apr 14, 2026
Hi @mdon — nice work on this one. Architecture is solid (layout-swap + PubSub live restore is a real UX win), the Full review at BUG — HIGH (blocking)
BUG — MEDIUM
IMPROVEMENT — MEDIUM
Let me know when you're ready for another pass. The HIGH items (missing JS hook + wrong CSS path + |
HIGH (merge blockers): - Register MaintenanceCountdown hook in priv/static/assets/phoenix_kit.js alongside the other phoenix_kit hooks. Parent apps already include this file, so the hook is now reliably available (was previously injected by a plug script that wasn't guaranteed to run before LiveSocket init). Remove the fragile inline injection from the Integration plug. - Plug's 503 HTML now uses inline CSS instead of linking to /assets/css/app.css (which wasn't actually served — the real digested path is /assets/app.css). The page is now self-contained with light + dark mode support via prefers-color-scheme, so it works on any route regardless of the parent app's asset pipeline. - Replace String.contains?/2 with String.starts_with?/2 in the plug's auth_route?/1 and static_asset?/1. A parent-app path like /blog/users/log-in-to-us would have bypassed maintenance mode. Add a regression test covering parent-app look-alike paths. MEDIUM: - disable_system/0 now clears maintenance_scheduled_end in addition to maintenance_scheduled_start so a stale end time doesn't surprise-disable the next re-enable. Update the test to assert both fields are cleared. - Track the Process.send_after timer ref in socket assigns and cancel it on reschedule (via new reschedule_maintenance_end_timer/1) so schedule changes don't leave a stale "auto-off" signal in flight. - Settings LiveView's PubSub handler now re-reads header and subtext so multi-admin editing stays in sync. The save handler broadcasts status change to trigger this sync. - schedule_error_message/1 catch-all now logs a warning with the unknown atom so future validation additions surface instead of being silently swallowed. - Document check_maintenance_mode/1's required call sites (all 6 on_mount hooks listed in the @doc) so new live_sessions don't forget it. - Add a HACK comment near put_in socket.private[:live_layout] noting it relies on Phoenix LiveView internals (same pattern as maybe_apply_plugin_layout) and should be revisited on major LV upgrades. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses the final MEDIUM improvement from PR review: instead of each of the 6 on_mount hooks explicitly calling check_maintenance_mode/1, fold it into mount_phoenix_kit_current_scope/3 which all 6 already use. New live_sessions that use a scope-mounting on_mount hook now inherit maintenance mode enforcement automatically — no way to forget it. Removes 6 redundant call sites and updates the @doc comment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ddon
commented
Apr 15, 2026
Thanks @mdon — re-reviewed the two follow-up commits (
LGTM from my side. 🚀 |
ddon
left a comment
There was a problem hiding this comment.
Approved — follow-up commits address all HIGH/MEDIUM items from the review.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Refactors maintenance mode from a redirect-based approach to a layout override — when maintenance turns on, a user's LiveView keeps running and their form state is preserved,
but the rendered layout is dynamically swapped to the maintenance page. URL never changes. When it ends, PubSub pushes the update and the layout swaps back, revealing the user's
page exactly as they left it.
Adds scheduled maintenance windows (UTC start/end with full validation), a scheduled auto-shutoff timer so users sitting on a blocked page get unblocked at the scheduled
end, and a live admin settings UI with real-time preview.
Highlights
Layout override approach (
lib/phoenix_kit_web/users/auth.ex)check_maintenance_mode/1setssocket.private[:live_layout]to{PhoenixKitWeb.Layouts, :maintenance}for non-admins via the existing plugin-layout pattern already usedelsewhere in phoenix_kit
attach_hookfires on every status change — non-admin tabs swap layouts instantly, admin tabs stay put with no reloadsave_original_layout+ defensive restore (deletes the key when original was nil) prevents renderer crashesProcess.send_aftertimer on connected mount unblocks users when a scheduled end arrives (clamped to Erlang's 32-bit timeout max)Scheduled maintenance (
lib/modules/maintenance/maintenance.ex)validate_schedule/2rejects: empty, past start/end (60s tolerance for minute-precision inputs), end ≤ start, and >1 year in futurecleanup_expired_schedule/0auto-cleans stale state on every page accessactive?/0wrapped in rescue so a DB outage fails open with a logged errorPlug for non-LiveView routes (
lib/modules/maintenance/web/plugs/maintenance_mode.ex)Phoenix.HTML.html_escape(XSS-safe) andRetry-AfterheaderAdmin settings UI (
lib/modules/maintenance/settings.ex)gettextphx-changeso preview updates on every keystroke (not just blur)Timezone helpers extracted to
PhoenixKit.Utils.Date(with doctests):offset_to_seconds/1,shift_to_offset/2,parse_datetime_local/2,format_datetime_local/2