Packages

phoenix_kit

1.7.156
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules maintenance maintenance.ex
Raw

lib/modules/maintenance/maintenance.ex

defmodule PhoenixKit.Modules.Maintenance do
@moduledoc """
Maintenance Mode module for PhoenixKit.
This module provides a system-wide maintenance mode that shows a
maintenance page to all non-admin users while allowing
admins and owners to access the site normally.
Maintenance can be activated in two ways:
- **Manual toggle**: Immediately enables/disables maintenance mode
- **Scheduled window**: Set a start and end time for automatic activation
`active?/0` returns true when either the manual toggle is on OR the current
time falls within a scheduled window.
## Settings
The module uses the following settings stored in the database:
- `maintenance_module_enabled` - Boolean to enable/disable the module settings page (default: false)
- `maintenance_enabled` - Boolean to enable/disable maintenance mode manually (default: false)
- `maintenance_header` - Main heading text (default: "Maintenance Mode")
- `maintenance_subtext` - Descriptive subtext (default: "We'll be back soon")
- `maintenance_scheduled_start` - ISO 8601 UTC datetime for scheduled start (default: nil)
- `maintenance_scheduled_end` - ISO 8601 UTC datetime for scheduled end (default: nil)
## Usage
# Check if maintenance mode is currently active (manual OR scheduled)
if PhoenixKit.Modules.Maintenance.active?() do
# Show maintenance page to non-admin users
end
# Enable/disable manually
PhoenixKit.Modules.Maintenance.enable_system()
PhoenixKit.Modules.Maintenance.disable_system()
# Schedule a maintenance window
PhoenixKit.Modules.Maintenance.update_schedule(
~U[2026-04-14 17:00:00Z],
~U[2026-04-14 18:00:00Z]
)
# Get module configuration
config = PhoenixKit.Modules.Maintenance.get_config()
"""
use PhoenixKit.Module
alias PhoenixKit.Dashboard.Tab
alias PhoenixKit.PubSub.Manager, as: PubSubManager
alias PhoenixKit.Settings
@default_header "Maintenance Mode"
@default_subtext "We'll be back soon. Our team is working hard to bring you something amazing!"
@pubsub_topic "phoenix_kit:maintenance"
# ============================================================================
# Module Status
# ============================================================================
@doc """
Checks if the Maintenance module is enabled (settings page accessible).
"""
def module_enabled? do
Settings.get_boolean_setting("maintenance_module_enabled", false)
end
@doc """
Enables the Maintenance module (makes settings page accessible).
"""
def enable_module do
Settings.update_boolean_setting("maintenance_module_enabled", true)
end
@doc """
Disables the Maintenance module (hides settings page).
Also automatically disables maintenance mode and clears any schedule
to prevent users from being locked out.
"""
def disable_module do
disable_system()
clear_schedule()
Settings.update_boolean_setting("maintenance_module_enabled", false)
end
@impl PhoenixKit.Module
@doc """
Checks if the Maintenance module is enabled (PhoenixKit.Module callback).
"""
def enabled? do
Settings.get_boolean_setting("maintenance_module_enabled", false)
end
# ============================================================================
# Manual Toggle
# ============================================================================
@impl PhoenixKit.Module
@doc """
Enables maintenance mode manually.
When enabled, all non-admin users will see the maintenance page.
Broadcasts a PubSub event so LiveViews can react in real time.
"""
def enable_system do
# Clear any expired schedule so it doesn't suppress the toggle
if past_scheduled_end?() do
Settings.update_setting("maintenance_scheduled_start", "")
Settings.update_setting("maintenance_scheduled_end", "")
end
result = Settings.update_boolean_setting("maintenance_enabled", true)
case result do
{:ok, _} -> broadcast_status_change()
_ -> :ok
end
result
end
@impl PhoenixKit.Module
@doc """
Disables maintenance mode manually.
When disabled, all users can access the site normally.
Also clears both scheduled start and end times so stale schedule values
don't re-activate maintenance or leave surprise auto-off signals for
later re-enables.
Broadcasts a PubSub event so the maintenance layout is removed.
"""
def disable_system do
# Clear the whole schedule so it doesn't re-activate or surprise-deactivate later
Settings.update_setting("maintenance_scheduled_start", "")
Settings.update_setting("maintenance_scheduled_end", "")
result = Settings.update_boolean_setting("maintenance_enabled", false)
case result do
{:ok, _} -> broadcast_status_change()
_ -> :ok
end
result
end
@doc """
Returns whether the manual maintenance toggle is on.
"""
def manually_enabled? do
Settings.get_boolean_setting("maintenance_enabled", false)
end
# ============================================================================
# Scheduled Maintenance
# ============================================================================
@doc """
Returns the scheduled start time as a DateTime, or nil.
"""
def get_scheduled_start do
case Settings.get_setting_cached("maintenance_scheduled_start", nil) do
nil -> nil
"" -> nil
iso_string -> parse_datetime(iso_string)
end
end
@doc """
Returns the scheduled end time as a DateTime, or nil.
"""
def get_scheduled_end do
case Settings.get_setting_cached("maintenance_scheduled_end", nil) do
nil -> nil
"" -> nil
iso_string -> parse_datetime(iso_string)
end
end
@doc """
Validates a proposed maintenance schedule.
Rules:
- At least one of start or end must be provided
- Start (if set) must be in the future
- End (if set) must be in the future
- If both are set, end must be strictly after start
A small tolerance (60 seconds) is applied to "in the future" checks
to handle datetime-local inputs which only have minute precision and
minor clock drift between client and server.
Returns `:ok` or `{:error, atom}` where atom is one of:
- `:empty` — neither start nor end provided
- `:start_in_past`
- `:end_in_past`
- `:end_before_start` — end is before or equal to start
- `:too_far_future` — date is more than one year in the future
"""
def validate_schedule(start_dt, end_dt) do
with :ok <- validate_presence(start_dt, end_dt),
:ok <- validate_not_past(start_dt, :start_in_past),
:ok <- validate_not_past(end_dt, :end_in_past),
:ok <- validate_order(start_dt, end_dt),
:ok <- validate_not_too_far(start_dt) do
validate_not_too_far(end_dt)
end
end
defp validate_presence(nil, nil), do: {:error, :empty}
defp validate_presence(_, _), do: :ok
# 60-second tolerance for minute-precision inputs and clock drift
defp validate_not_past(nil, _), do: :ok
defp validate_not_past(%DateTime{} = dt, error) do
if DateTime.diff(dt, DateTime.utc_now()) < -60, do: {:error, error}, else: :ok
end
defp validate_order(%DateTime{} = start_dt, %DateTime{} = end_dt) do
if DateTime.compare(end_dt, start_dt) == :gt, do: :ok, else: {:error, :end_before_start}
end
defp validate_order(_, _), do: :ok
# Reject dates more than one year in the future
defp validate_not_too_far(nil), do: :ok
defp validate_not_too_far(%DateTime{} = dt) do
max_future_seconds = 365 * 24 * 60 * 60
if DateTime.diff(dt, DateTime.utc_now()) > max_future_seconds,
do: {:error, :too_far_future},
else: :ok
end
@doc """
Sets a scheduled maintenance window.
Either or both times can be provided:
- **Start only**: maintenance activates at start, stays on until manually disabled
- **End only**: maintenance auto-disables at end time
- **Both**: maintenance is active between start and end
Validates the schedule via `validate_schedule/2` before writing.
Times are stored as ISO 8601 UTC strings. Pass `nil` to clear a field.
Broadcasts a PubSub event on success.
Returns `:ok` on success or `{:error, atom}` on validation/DB failure.
"""
def update_schedule(start_dt, end_dt) do
with :ok <- validate_schedule(start_dt, end_dt),
start_val = if(start_dt, do: DateTime.to_iso8601(start_dt), else: ""),
end_val = if(end_dt, do: DateTime.to_iso8601(end_dt), else: ""),
{:ok, _} <- Settings.update_setting("maintenance_scheduled_start", start_val),
{:ok, _} <- Settings.update_setting("maintenance_scheduled_end", end_val) do
broadcast_status_change()
:ok
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Clears the scheduled maintenance window.
Broadcasts a PubSub event.
"""
def clear_schedule do
Settings.update_setting("maintenance_scheduled_start", "")
Settings.update_setting("maintenance_scheduled_end", "")
broadcast_status_change()
:ok
end
@doc """
Returns true if the current time is past the scheduled start time.
Used for start-only schedules (no end time) or as the "on" condition
in a start+end window.
"""
def past_scheduled_start? do
case get_scheduled_start() do
%DateTime{} = dt -> DateTime.compare(DateTime.utc_now(), dt) in [:gt, :eq]
_ -> false
end
end
@doc """
Returns true if the current time is past the scheduled end time.
When true, maintenance is forced off regardless of other settings.
Used as an auto-turn-off mechanism.
"""
def past_scheduled_end? do
case get_scheduled_end() do
%DateTime{} = dt -> DateTime.compare(DateTime.utc_now(), dt) in [:gt, :eq]
_ -> false
end
end
@doc """
Returns true if a scheduled maintenance window is currently active.
Handles three schedule configurations:
- **Start + End**: active between start and end times
- **Start only**: active once past start, stays on indefinitely
- **End only**: returns false (end-only acts as auto-off for the manual toggle)
"""
def within_scheduled_window? do
start_dt = get_scheduled_start()
end_dt = get_scheduled_end()
case {start_dt, end_dt} do
{nil, _} -> false
{_, nil} -> past_scheduled_start?()
{_, _} -> past_scheduled_start?() and not past_scheduled_end?()
end
end
# ============================================================================
# Active Check (the main entry point)
# ============================================================================
@doc """
Returns true if maintenance mode is currently active.
The main function used to check maintenance status. Logic:
1. If a scheduled end time is set and has passed → **off** (auto-turn-off)
2. If the manual toggle is on → **on**
3. If a scheduled start time is set and has passed → **on** (auto-turn-on)
4. Otherwise → **off**
Schedule configurations:
- **Start only**: activates at start time, stays on until manually disabled
- **End only**: manual toggle works, but auto-disables at end time
- **Start + End**: active during the window
- **Neither**: just the manual toggle
## Examples
iex> PhoenixKit.Modules.Maintenance.active?()
false
"""
def active? do
cond do
# End time has passed — maintenance is off regardless of toggle or start
past_scheduled_end?() -> false
manually_enabled?() -> true
past_scheduled_start?() -> true
true -> false
end
rescue
error ->
# If settings DB is unavailable, fail open (don't block the site)
# but log so the issue is visible in production.
require Logger
Logger.error("Maintenance.active? failed: #{Exception.message(error)}")
false
end
@doc """
Cleans up stale state when the scheduled end time has passed.
Disables the manual toggle and clears the schedule, then broadcasts
so any connected users get their layout restored.
Returns `true` if cleanup was performed, `false` if nothing needed cleaning.
Safe to call repeatedly — it's a no-op when there's nothing stale.
"""
def cleanup_expired_schedule do
if past_scheduled_end?() and
(manually_enabled?() or get_scheduled_start() != nil or get_scheduled_end() != nil) do
Settings.update_boolean_setting("maintenance_enabled", false)
Settings.update_setting("maintenance_scheduled_start", "")
Settings.update_setting("maintenance_scheduled_end", "")
broadcast_status_change()
true
else
false
end
rescue
error ->
require Logger
Logger.error("Maintenance.cleanup_expired_schedule failed: #{Exception.message(error)}")
false
end
@doc """
Returns the number of seconds until maintenance ends, or nil if unknown.
Used for the Retry-After HTTP header and countdown timer.
Returns nil if maintenance is manually enabled without a scheduled end,
or if maintenance is not active.
"""
def seconds_until_end do
cond do
# Scheduled window is active — use the end time
within_scheduled_window?() ->
case get_scheduled_end() do
%DateTime{} = end_dt -> DateTime.diff(end_dt, DateTime.utc_now())
_ -> nil
end
# Manual mode with a scheduled end in the future — use it as estimate
manually_enabled?() ->
case get_scheduled_end() do
%DateTime{} = end_dt ->
diff = DateTime.diff(end_dt, DateTime.utc_now())
if diff > 0, do: diff, else: nil
nil ->
nil
end
true ->
nil
end
end
# ============================================================================
# Content Settings
# ============================================================================
@doc """
Gets the header text for the maintenance page.
"""
def get_header do
Settings.get_setting_cached("maintenance_header", @default_header)
end
@doc """
Updates the header text for the maintenance page.
"""
def update_header(header) when is_binary(header) do
Settings.update_setting("maintenance_header", header)
end
@doc """
Gets the subtext for the maintenance page.
"""
def get_subtext do
Settings.get_setting_cached("maintenance_subtext", @default_subtext)
end
@doc """
Updates the subtext for the maintenance page.
"""
def update_subtext(subtext) when is_binary(subtext) do
Settings.update_setting("maintenance_subtext", subtext)
end
@impl PhoenixKit.Module
@doc """
Gets the full configuration for the Maintenance module.
"""
def get_config do
%{
module_enabled: module_enabled?(),
enabled: manually_enabled?(),
active: active?(),
header: get_header(),
subtext: get_subtext(),
scheduled_start: get_scheduled_start(),
scheduled_end: get_scheduled_end(),
scheduled_active: within_scheduled_window?()
}
end
# ============================================================================
# PubSub
# ============================================================================
@doc """
Returns the PubSub topic for maintenance status changes.
"""
def pubsub_topic, do: @pubsub_topic
@doc """
Subscribes the calling process to maintenance status change events.
"""
def subscribe do
PubSubManager.subscribe(@pubsub_topic)
end
@doc """
Broadcasts the current maintenance status.
Sends `{:maintenance_status_changed, %{active: boolean}}` to all subscribers.
"""
def broadcast_status_change do
PubSubManager.broadcast(@pubsub_topic, {:maintenance_status_changed, %{active: active?()}})
end
# ============================================================================
# Module Behaviour Callbacks
# ============================================================================
@impl PhoenixKit.Module
def module_key, do: "maintenance"
@impl PhoenixKit.Module
def module_name, do: "Maintenance"
@impl PhoenixKit.Module
def permission_metadata do
%{
key: "maintenance",
label: "Maintenance",
icon: "hero-wrench-screwdriver",
description: "Maintenance mode and under-construction pages"
}
end
@impl PhoenixKit.Module
def settings_tabs do
[
Tab.new!(
id: :admin_settings_maintenance,
label: "Maintenance",
icon: "hero-wrench-screwdriver",
path: "maintenance",
priority: 932,
level: :admin,
parent: :admin_settings,
permission: "maintenance",
gettext_backend: PhoenixKitWeb.Gettext
)
]
end
# ============================================================================
# Private
# ============================================================================
defp parse_datetime(iso_string) when is_binary(iso_string) do
case DateTime.from_iso8601(iso_string) do
{:ok, dt, _offset} -> dt
_ -> nil
end
end
end