331 lines
14 KiB
Text
331 lines
14 KiB
Text
<Layouts.admin flash={@flash} timezone={@timezone}>
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{t("Job Monitoring Dashboard")}
|
|
</h1>
|
|
<p class="text-gray-600 dark:text-gray-400">
|
|
{t("Real-time monitoring of polling and discovery jobs")}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Health Metrics Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
|
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">Completed (1h)</h3>
|
|
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
|
|
{Map.get(@health_metrics, :completed_last_hour, 0)}
|
|
</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">jobs completed</p>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
|
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">Failed (1h)</h3>
|
|
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
|
|
{Map.get(@health_metrics, :failed_last_hour, 0)}
|
|
</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">failures</p>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
|
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">{t("Avg Duration")}</h3>
|
|
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
|
|
<%= if Map.get(@health_metrics, :avg_execution_time_seconds) do %>
|
|
{Map.get(@health_metrics, :avg_execution_time_seconds) |> Float.round(1)}s
|
|
<% else %>
|
|
{t("N/A")}
|
|
<% end %>
|
|
</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">last hour</p>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
|
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">{t("Active Jobs")}</h3>
|
|
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
|
|
{length(@active_jobs)}
|
|
</p>
|
|
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">executing now</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Operations -->
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
Active Operations ({length(@executing_jobs)})
|
|
</h2>
|
|
|
|
<%= if length(@executing_jobs) == 0 do %>
|
|
<p class="text-gray-600 dark:text-gray-400 text-sm">No jobs currently executing</p>
|
|
<% else %>
|
|
<div class="space-y-3">
|
|
<%= for job <- @executing_jobs do %>
|
|
<div class="border border-gray-200 dark:border-gray-700 rounded p-3">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<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-gray-500 dark:text-gray-500 mt-1">
|
|
Started {ToweropsWeb.TimeHelpers.format_time_ago(job.attempted_at)}
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<.icon name="hero-arrow-path" class="w-4 h-4 animate-spin text-blue-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<!-- Problems -->
|
|
<%= 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>
|
|
<% end %>
|
|
|
|
<!-- Health Metrics -->
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t("Health Metrics")}
|
|
</h2>
|
|
|
|
<!-- Current Activity -->
|
|
<div class="mb-6">
|
|
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
|
{t("Current Activity")}
|
|
</h3>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Executing</p>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{@metrics.executing_count}
|
|
</p>
|
|
</div>
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Queued</p>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{@metrics.queued_count}
|
|
</p>
|
|
</div>
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Completed (1h)</p>
|
|
<p class="text-2xl font-bold text-green-600 dark:text-green-400">
|
|
{@metrics.completed_last_hour}
|
|
</p>
|
|
</div>
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">Failed (1h)</p>
|
|
<p class="text-2xl font-bold text-red-600 dark:text-red-400">
|
|
{@metrics.failed_last_hour}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Rates -->
|
|
<div class="mb-6">
|
|
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
|
{t("Success Rates")}
|
|
</h3>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<div class="flex justify-between text-xs mb-1">
|
|
<span class="text-gray-600 dark:text-gray-400">Polling (1h)</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">
|
|
{Float.round(@metrics.polling_success_rate_1h * 100, 1)}%
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div
|
|
class="bg-green-600 h-2 rounded-full transition-all"
|
|
style={"width: #{@metrics.polling_success_rate_1h * 100}%"}
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="flex justify-between text-xs mb-1">
|
|
<span class="text-gray-600 dark:text-gray-400">Discovery (1h)</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">
|
|
{Float.round(@metrics.discovery_success_rate_1h * 100, 1)}%
|
|
</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div
|
|
class="bg-blue-600 h-2 rounded-full transition-all"
|
|
style={"width: #{@metrics.discovery_success_rate_1h * 100}%"}
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Performance -->
|
|
<div class="mb-6">
|
|
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
|
{t("Performance")}
|
|
</h3>
|
|
<dl class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-600 dark:text-gray-400">Avg Execution Time</dt>
|
|
<dd class="font-medium text-gray-900 dark:text-white">
|
|
<%= if @metrics.avg_execution_time_seconds do %>
|
|
{Float.round(@metrics.avg_execution_time_seconds, 1)}s
|
|
<% else %>
|
|
{t("N/A")}
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-600 dark:text-gray-400">Jobs/Hour</dt>
|
|
<dd class="font-medium text-gray-900 dark:text-white">
|
|
{@metrics.completed_last_hour}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<!-- Queue Depths -->
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
|
{t("Queue Depths")}
|
|
</h3>
|
|
<dl class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-600 dark:text-gray-400">Pollers</dt>
|
|
<dd class="font-medium text-gray-900 dark:text-white">
|
|
{@metrics.queue_depths.pollers}
|
|
</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-600 dark:text-gray-400">Discovery</dt>
|
|
<dd class="font-medium text-gray-900 dark:text-white">
|
|
{@metrics.queue_depths.discovery}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<p class="text-xs text-gray-500 dark:text-gray-500">
|
|
{t("Updates every 10 seconds")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
{t("Recent Activity")}
|
|
</h2>
|
|
|
|
<%= if length(@recent_events) == 0 do %>
|
|
<p class="text-gray-600 dark:text-gray-400 text-sm">No recent activity</p>
|
|
<% else %>
|
|
<div class="space-y-2 max-h-96 overflow-y-auto">
|
|
<%= for job <- Enum.take(@recent_events, 50) do %>
|
|
<div class={"text-sm border-l-2 pl-3 py-2 #{event_border_color(job.state)}"}>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<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-xs text-gray-600 dark:text-gray-400">
|
|
{worker_name(job.worker)} · {event_outcome(job)}
|
|
</p>
|
|
</div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-500 whitespace-nowrap ml-2">
|
|
{ToweropsWeb.TimeHelpers.format_time_ago(
|
|
job.completed_at || job.cancelled_at || job.discarded_at || job.inserted_at
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</Layouts.admin>
|