towerops/lib/towerops_web/live/agent_live/show.html.heex
Graham McIntire 4297556700
Implement all quick win improvements for agent UI and functionality
1. Fixed Docker Compose example to include socket mount
   - Added /var/run/docker.sock mount for auto-updates in agent modal

2. Show total equipment count with inheritance
   - Display both direct and inherited equipment counts in agents table
   - Shows breakdown: 'X total' with 'Y direct · Z inherited' details

3. Add visual status indicators with better CSS
   - Added animated pulsing dots for online agents
   - Color-coded status dots (green=online, yellow=warning, red=offline)
   - Improved status badge styling

4. Create agent detail page with metrics
   - New /agents/:id page showing comprehensive agent health
   - 4 metric cards: Status, Equipment count, Last seen, Agent info
   - Displays agent metadata (hostname, version, uptime)
   - Lists all polling targets with assignment source badges
   - Shows direct vs inherited equipment assignments
   - Clickable agent names in table link to detail page

5. Add health endpoint to Rust agent
   - Added /health HTTP endpoint on port 8080
   - Returns JSON with agent status, version, uptime, metrics pending
   - Uses lightweight tiny_http server
   - Non-blocking background thread
   - Ready for monitoring/health checks
2026-01-14 18:46:03 -06:00

194 lines
8 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="agents"
>
<.header>
{@page_title}
<:subtitle>Agent Details and Health</:subtitle>
<:actions>
<.link
navigate={~p"/orgs/#{@current_organization.slug}/agents"}
class="text-sm font-semibold text-zinc-700 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-zinc-100"
>
<.icon name="hero-arrow-left" class="h-5 w-5" /> Back to Agents
</.link>
</:actions>
</.header>
<!-- Agent Status Overview -->
<div class="mt-6 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<!-- Status Card -->
<div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Status</p>
<% {status, label} = agent_status(@agent_token) %>
<div class="mt-2">
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
{label}
</span>
</div>
</div>
<.icon name="hero-signal" class="h-8 w-8 text-zinc-400 dark:text-zinc-600" />
</div>
</div>
<!-- Equipment Count Card -->
<div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Equipment</p>
<p class="mt-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-100">
{length(@polling_targets)}
</p>
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
{@direct_assignments} direct
</p>
</div>
<.icon name="hero-server" class="h-8 w-8 text-zinc-400 dark:text-zinc-600" />
</div>
</div>
<!-- Last Seen Card -->
<div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Last Seen</p>
<p class="mt-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{format_last_seen(@agent_token.last_seen_at)}
</p>
<%= if @agent_token.last_ip do %>
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400 font-mono">
{@agent_token.last_ip}
</p>
<% end %>
</div>
<.icon name="hero-clock" class="h-8 w-8 text-zinc-400 dark:text-zinc-600" />
</div>
</div>
<!-- Version/Uptime Card -->
<div class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Agent Info</p>
<%= if @agent_token.metadata["version"] do %>
<p class="mt-2 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
v{@agent_token.metadata["version"]}
</p>
<% else %>
<p class="mt-2 text-lg font-semibold text-zinc-500 dark:text-zinc-400">
Unknown
</p>
<% end %>
<%= if @agent_token.metadata["uptime_seconds"] do %>
<p class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
Uptime: {format_uptime(@agent_token.metadata["uptime_seconds"])}
</p>
<% end %>
</div>
<.icon name="hero-information-circle" class="h-8 w-8 text-zinc-400 dark:text-zinc-600" />
</div>
</div>
</div>
<!-- Agent Metadata -->
<%= if @agent_token.metadata && map_size(@agent_token.metadata) > 0 do %>
<div class="mt-6 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-6">
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-4">
Agent Metadata
</h3>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<%= if @agent_token.metadata["hostname"] do %>
<div>
<dt class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Hostname</dt>
<dd class="mt-1 text-sm text-zinc-900 dark:text-zinc-100 font-mono">
{@agent_token.metadata["hostname"]}
</dd>
</div>
<% end %>
<%= if @agent_token.metadata["version"] do %>
<div>
<dt class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Version</dt>
<dd class="mt-1 text-sm text-zinc-900 dark:text-zinc-100">
{@agent_token.metadata["version"]}
</dd>
</div>
<% end %>
<%= if @agent_token.metadata["uptime_seconds"] do %>
<div>
<dt class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Uptime</dt>
<dd class="mt-1 text-sm text-zinc-900 dark:text-zinc-100">
{format_uptime(@agent_token.metadata["uptime_seconds"])}
</dd>
</div>
<% end %>
</dl>
</div>
<% end %>
<!-- Equipment List -->
<div class="mt-6 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg">
<div class="p-6 border-b border-zinc-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
Polling Targets
</h3>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Equipment that this agent is responsible for polling
</p>
</div>
<%= if @polling_targets == [] do %>
<div class="p-12 text-center">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
<h3 class="mt-4 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
No equipment assigned
</h3>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
This agent is not currently polling any equipment.
</p>
</div>
<% else %>
<div class="divide-y divide-zinc-200 dark:divide-zinc-800">
<%= for equipment <- @polling_targets do %>
<.link
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{equipment.id}"}
class="block hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
>
<div class="p-6">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100 truncate">
{equipment.name}
</p>
<% {source, source_label} = assignment_source(equipment, @agent_token.id) %>
<span class={"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium #{source_badge_class(source)}"}>
{source_label}
</span>
</div>
<div class="mt-1 flex items-center gap-4 text-sm text-zinc-600 dark:text-zinc-400">
<span class="flex items-center gap-1">
<.icon name="hero-globe-alt" class="h-4 w-4" />
{equipment.ip_address}
</span>
<%= if equipment.site do %>
<span class="flex items-center gap-1">
<.icon name="hero-building-office" class="h-4 w-4" />
{equipment.site.name}
</span>
<% end %>
</div>
</div>
<.icon name="hero-chevron-right" class="h-5 w-5 text-zinc-400 dark:text-zinc-600" />
</div>
</div>
</.link>
<% end %>
</div>
<% end %>
</div>
</Layouts.authenticated>