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:
parent
daa223c49d
commit
1e45b1609b
3 changed files with 86 additions and 22 deletions
|
|
@ -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 """
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue