towerops/lib/towerops_web/live/status_page_live.ex
Graham McIntire 3a408a8dc1 Security hardening + performance fixes across codebase
CRITICAL:
- Membership: remove :role/:org_id/:user_id from mass-assignment cast; use explicit create_changeset/4 and role_update_changeset/2
- GraphQL member resolver: add authorize_invite/3 checking admin/owner role and role hierarchy
- REST invitations controller: add auth check for invite creation

HIGH:
- ApiToken: remove :organization_id/:user_id from cast; use explicit create_changeset/4

MEDIUM:
- Move 8 LiveView Ecto queries into context modules (Admin, Alerts, Coverages, OnCall, Snmp)
- Replace Process.put/Process.get with socket assigns for unresolved_alert_count (user_auth + layouts + 50 templates)
- Add batch get_utilization_for_interfaces/1 to eliminate N+1 capacity queries in device show
- Replace Process.sleep with Process.monitor/assert_receive or Process.send_after in 5 test files

LOW:
- Add handle_params/3 to UserResetPasswordLive, UserRegistrationLive, StatusPageLive
- Remove redundant Repo.preload calls; add preloads to list_site_devices/1
- Fix @impl annotations and credo nesting warnings
2026-06-21 17:40:50 -05:00

155 lines
5.2 KiB
Elixir

defmodule ToweropsWeb.StatusPageLive do
@moduledoc """
Public unauthenticated status page.
Renders at /status/:slug with a standalone layout (no sidebar).
Mobile-responsive for customers checking during outages.
"""
use ToweropsWeb, :live_view
alias Towerops.StatusPages
@impl true
def mount(%{"slug" => slug}, _session, socket) do
case StatusPages.get_config_by_slug(slug) do
nil ->
{:ok,
socket
|> put_flash(:error, "Status page not found")
|> assign(:not_found, true)
|> assign(:page_title, "Not Found")}
config ->
_ =
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "status_page:#{config.id}")
end
components = StatusPages.list_components(config.id)
active_incidents = StatusPages.list_active_incidents(config.id)
recent_incidents = StatusPages.list_incidents(config.id, limit: 10)
overall = StatusPages.overall_status(components)
{:ok,
socket
|> assign(:config, config)
|> assign(:components, components)
|> assign(:active_incidents, active_incidents)
|> assign(:recent_incidents, recent_incidents)
|> assign(:overall_status, overall)
|> assign(:not_found, false)
|> assign(:page_title, "#{config.company_name || "Status"} — System Status"), layout: false}
end
end
@impl true
def handle_params(params, _url, socket) do
slug = Map.get(params, "slug", "")
case slug do
"" ->
{:noreply,
socket
|> put_flash(:error, "Status page not found")
|> assign(:not_found, true)
|> assign(:page_title, "Not Found")}
slug ->
case StatusPages.get_config_by_slug(slug) do
nil ->
{:noreply,
socket
|> put_flash(:error, "Status page not found")
|> assign(:not_found, true)
|> assign(:page_title, "Not Found")}
config ->
maybe_subscribe_to_status_page(socket, config)
components = StatusPages.list_components(config.id)
active_incidents = StatusPages.list_active_incidents(config.id)
recent_incidents = StatusPages.list_incidents(config.id, limit: 10)
overall = StatusPages.overall_status(components)
{:noreply,
socket
|> assign(:config, config)
|> assign(:components, components)
|> assign(:active_incidents, active_incidents)
|> assign(:recent_incidents, recent_incidents)
|> assign(:overall_status, overall)
|> assign(:not_found, false)
|> assign(:page_title, "#{config.company_name || "Status"} — System Status")}
end
end
end
@impl true
def handle_info({:status_updated, _}, socket) do
config = socket.assigns.config
components = StatusPages.list_components(config.id)
active_incidents = StatusPages.list_active_incidents(config.id)
overall = StatusPages.overall_status(components)
{:noreply,
assign(socket,
components: components,
active_incidents: active_incidents,
overall_status: overall
)}
end
def handle_info(_, socket), do: {:noreply, socket}
# -- Helpers --
@doc false
def overall_color(:operational), do: "bg-green-500"
def overall_color(:degraded), do: "bg-yellow-500"
def overall_color(:partial_outage), do: "bg-orange-500"
def overall_color(:major_outage), do: "bg-red-500"
def overall_color(:maintenance), do: "bg-blue-500"
def overall_color(_), do: "bg-gray-500"
@doc false
def overall_text(:operational), do: "All Systems Operational"
def overall_text(:degraded), do: "Degraded Performance"
def overall_text(:partial_outage), do: "Partial System Outage"
def overall_text(:major_outage), do: "Major System Outage"
def overall_text(:maintenance), do: "Scheduled Maintenance"
def overall_text(_), do: "Unknown"
@doc false
def component_color("operational"), do: "bg-green-500"
def component_color("degraded"), do: "bg-yellow-500"
def component_color("partial_outage"), do: "bg-orange-500"
def component_color("major_outage"), do: "bg-red-500"
def component_color("maintenance"), do: "bg-blue-500"
def component_color(_), do: "bg-gray-400"
@doc false
def component_label("operational"), do: "Operational"
def component_label("degraded"), do: "Degraded"
def component_label("partial_outage"), do: "Partial Outage"
def component_label("major_outage"), do: "Major Outage"
def component_label("maintenance"), do: "Maintenance"
def component_label(s), do: s
@doc false
def severity_color("critical"), do: "text-red-600"
def severity_color("major"), do: "text-orange-600"
def severity_color(_), do: "text-yellow-600"
@doc false
def incident_status_label("investigating"), do: "Investigating"
def incident_status_label("identified"), do: "Identified"
def incident_status_label("monitoring"), do: "Monitoring"
def incident_status_label("resolved"), do: "Resolved"
def incident_status_label(s), do: s
defp maybe_subscribe_to_status_page(socket, config) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "status_page:#{config.id}")
end
end
end