feat: build detailed problems section UI for stuck and failed jobs

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2026-02-06 18:33:21 -06:00 committed by Graham McIntire
parent daa223c49d
commit 1e45b1609b
No known key found for this signature in database
3 changed files with 86 additions and 22 deletions

View file

@ -53,6 +53,7 @@ defmodule Towerops.JobMonitoring do
order_by: [asc: j.attempted_at]
)
|> Repo.all()
|> preload_device_context()
end
@doc """
@ -69,6 +70,7 @@ defmodule Towerops.JobMonitoring do
limit: 100
)
|> Repo.all()
|> preload_device_context()
end
@doc """

View file

@ -26,7 +26,9 @@ defmodule ToweropsWeb.Admin.MonitoringLive do
|> assign(:page_title, "Job Monitoring")
|> assign(:active_jobs, [])
|> assign(:executing_jobs, [])
|> assign(:stuck_jobs, [])
|> assign(:stuck_jobs_count, 0)
|> assign(:failed_jobs, [])
|> assign(:failed_jobs_count, 0)
|> assign(:recent_jobs, [])
|> assign(:health_metrics, %{})
@ -68,7 +70,9 @@ defmodule ToweropsWeb.Admin.MonitoringLive do
socket
|> assign(:executing_jobs, executing_jobs)
|> assign(:stuck_jobs, stuck_jobs)
|> assign(:stuck_jobs_count, length(stuck_jobs))
|> assign(:failed_jobs, failed_jobs)
|> assign(:failed_jobs_count, length(failed_jobs))
|> assign(:recent_jobs, recent_jobs)
|> assign(:health_metrics, metrics)
@ -77,4 +81,15 @@ defmodule ToweropsWeb.Admin.MonitoringLive do
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
end

View file

@ -87,30 +87,77 @@
</div>
<!-- Problems -->
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Problems</h2>
<div class="flex gap-4">
<span class="text-sm text-orange-600 dark:text-orange-400">
{if @stuck_jobs_count > 0, do: "#{@stuck_jobs_count} stuck", else: ""}
</span>
<span class="text-sm text-red-600 dark:text-red-400">
{if @failed_jobs_count > 0, do: "#{@failed_jobs_count} failed", else: ""}
</span>
</div>
</div>
<div class="p-4">
<%= if @stuck_jobs_count == 0 and @failed_jobs_count == 0 do %>
<p class="text-gray-500 dark:text-gray-400 text-center py-8">
No problems detected
</p>
<% else %>
<p class="text-sm text-gray-600 dark:text-gray-400">
Problems section coming soon
</p>
<%= if length(@stuck_jobs) > 0 or length(@failed_jobs) > 0 do %>
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<h2 class="text-lg font-semibold text-red-900 dark:text-red-100 mb-4">
⚠️ Problems Detected
</h2>
<%= if length(@stuck_jobs) > 0 do %>
<div class="mb-4">
<h3 class="font-medium text-red-800 dark:text-red-200 mb-2">
Stuck Jobs ({length(@stuck_jobs)})
</h3>
<div class="space-y-2">
<%= for job <- @stuck_jobs do %>
<div class="bg-white dark:bg-gray-800 rounded p-3 border border-red-300 dark:border-red-700">
<div class="flex items-start justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">
<%= if job.device do %>
<%= job.device.name %>
<% else %>
Device #{get_in(job.args, ["device_id"])}
<% end %>
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
<%= worker_name(job.worker) %>
</p>
<p class="text-xs text-red-600 dark:text-red-400 mt-1">
Running for <%= duration_in_words(job.attempted_at) %>
</p>
</div>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<%= if length(@failed_jobs) > 0 do %>
<div>
<h3 class="font-medium text-red-800 dark:text-red-200 mb-2">
Failed Jobs ({length(@failed_jobs)})
</h3>
<div class="space-y-2">
<%= for job <- Enum.take(@failed_jobs, 5) do %>
<div class="bg-white dark:bg-gray-800 rounded p-3 border border-red-300 dark:border-red-700">
<div class="flex items-start justify-between">
<div>
<p class="font-medium text-gray-900 dark:text-white">
<%= if job.device do %>
<%= job.device.name %>
<% else %>
Device #{get_in(job.args, ["device_id"])}
<% end %>
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
<%= worker_name(job.worker) %> - Attempt <%= job.attempt %>/<%= job.max_attempts %>
</p>
<%= if job.errors && length(job.errors) > 0 do %>
<p class="text-xs text-red-600 dark:text-red-400 mt-1">
<%= hd(job.errors)["error"] || "Unknown error" %>
</p>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- Health Metrics -->
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">