towerops/lib/towerops_web/live/admin/monitoring_live.ex
Graham McIntire 5a7cdc7138 refactor: apply custom color palette across entire codebase
- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat
- Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast
- ~11,600 color replacements across ~99 files
- Update tests for new color class names
2026-06-23 10:58:48 -05:00

111 lines
3.3 KiB
Elixir

defmodule ToweropsWeb.Admin.MonitoringLive do
@moduledoc """
Real-time job monitoring dashboard for Oban workers.
Displays:
- Active operations (currently executing jobs)
- Problems (stuck/failed jobs)
- Health metrics (throughput, errors, latency)
- Recent activity timeline
"""
use ToweropsWeb, :live_view
alias Phoenix.LiveView.Socket
alias Towerops.JobMonitoring
alias Towerops.JobMonitoring.Metrics
on_mount {ToweropsWeb.UserAuth, :require_superuser}
@topic "job:lifecycle"
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
_ =
if connected?(socket) do
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, @topic)
end
socket =
socket
|> assign(:page_title, t("Job Monitoring"))
|> assign(:active_jobs, [])
|> assign(:executing_jobs, [])
|> assign(:stuck_jobs, [])
|> assign(:failed_jobs, [])
|> assign(:recent_events, [])
|> assign(:metrics, %{})
|> assign(:health_metrics, %{})
|> load_monitoring_data()
{:ok, socket}
end
@impl Phoenix.LiveView
def handle_info(%{event: :started} = event, socket) do
# Job started - add to active jobs list
active_jobs = [event | socket.assigns.active_jobs]
{:noreply, assign(socket, :active_jobs, active_jobs)}
end
def handle_info(%{event: event} = job_event, socket) when event in [:completed, :failed] do
# Job finished - remove from active jobs, reload data
active_jobs = Enum.reject(socket.assigns.active_jobs, fn job -> job.job_id == job_event.job_id end)
socket =
socket
|> assign(:active_jobs, active_jobs)
|> load_monitoring_data()
{:noreply, socket}
end
def handle_info(_event, socket) do
{:noreply, socket}
end
@spec load_monitoring_data(Socket.t()) :: Socket.t()
defp load_monitoring_data(socket) do
executing_jobs = JobMonitoring.list_active_jobs()
stuck_jobs = JobMonitoring.list_stuck_jobs()
failed_jobs = JobMonitoring.list_failed_jobs()
recent_jobs = JobMonitoring.list_recent_completions(20)
metrics = Metrics.calculate_all()
socket
|> assign(:executing_jobs, executing_jobs)
|> assign(:stuck_jobs, stuck_jobs)
|> assign(:failed_jobs, failed_jobs)
|> assign(:recent_events, recent_jobs)
|> assign(:metrics, metrics)
|> assign(:health_metrics, metrics)
end
defp worker_name("Towerops.Workers.DevicePollerWorker"), do: "Device Poll"
defp worker_name("Towerops.Workers.DiscoveryWorker"), do: "SNMP Discovery"
defp worker_name(worker), do: worker
defp duration_in_words(started_at) do
seconds = DateTime.diff(DateTime.utc_now(), started_at)
minutes = div(seconds, 60)
cond do
minutes < 1 -> "#{seconds}s"
minutes < 60 -> "#{minutes}m"
true -> "#{div(minutes, 60)}h #{rem(minutes, 60)}m"
end
end
defp event_border_color("completed"), do: "border-green-500"
defp event_border_color("cancelled"), do: "border-sweet-salmon-500"
defp event_border_color("discarded"), do: "border-cool-steel-400"
defp event_border_color(_), do: "border-cool-steel-300 dark:border-cool-steel-600"
defp event_outcome(job) do
case job.state do
"completed" -> "Completed"
"cancelled" -> "Failed"
"discarded" -> "Discarded"
_ -> String.capitalize(job.state)
end
end
end