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
126 lines
3.9 KiB
Elixir
126 lines
3.9 KiB
Elixir
defmodule ToweropsWeb.AgentLive.Show do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Agents
|
|
|
|
@impl true
|
|
def mount(%{"id" => id}, _session, socket) do
|
|
organization = socket.assigns.current_organization
|
|
agent_token = Agents.get_agent_token!(id)
|
|
|
|
# Verify the agent belongs to this organization
|
|
if agent_token.organization_id != organization.id do
|
|
raise Ecto.NoResultsError
|
|
end
|
|
|
|
# Get polling targets (all equipment this agent is responsible for)
|
|
polling_targets = Agents.list_agent_polling_targets(agent_token.id)
|
|
|
|
# Get direct assignments
|
|
direct_assignments = Agents.count_assigned_equipment(agent_token.id)
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, agent_token.name)
|
|
|> assign(:agent_token, agent_token)
|
|
|> assign(:polling_targets, polling_targets)
|
|
|> assign(:direct_assignments, direct_assignments)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(_params, _url, socket) do
|
|
{:noreply, socket}
|
|
end
|
|
|
|
defp agent_status(agent_token) do
|
|
case agent_token.last_seen_at do
|
|
nil ->
|
|
{:never, "Never connected"}
|
|
|
|
last_seen ->
|
|
seconds_ago = DateTime.diff(DateTime.utc_now(), last_seen, :second)
|
|
|
|
cond do
|
|
seconds_ago < 120 -> {:online, "Online"}
|
|
seconds_ago < 300 -> {:warning, "Warning"}
|
|
true -> {:offline, "Offline"}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp status_badge_class(status) do
|
|
case status do
|
|
:online -> "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
|
:warning -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
|
|
:offline -> "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
|
:never -> "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-300"
|
|
end
|
|
end
|
|
|
|
defp status_dot_class(status) do
|
|
case status do
|
|
:online -> "bg-green-600 dark:bg-green-400 animate-pulse"
|
|
:warning -> "bg-yellow-600 dark:bg-yellow-400"
|
|
:offline -> "bg-red-600 dark:bg-red-400"
|
|
:never -> "bg-zinc-400 dark:bg-zinc-600"
|
|
end
|
|
end
|
|
|
|
defp format_last_seen(nil), do: "Never"
|
|
|
|
defp format_last_seen(datetime) do
|
|
seconds_ago = DateTime.diff(DateTime.utc_now(), datetime, :second)
|
|
|
|
cond do
|
|
seconds_ago < 60 -> "#{seconds_ago}s ago"
|
|
seconds_ago < 3600 -> "#{div(seconds_ago, 60)}m ago"
|
|
seconds_ago < 86_400 -> "#{div(seconds_ago, 3600)}h ago"
|
|
true -> "#{div(seconds_ago, 86_400)}d ago"
|
|
end
|
|
end
|
|
|
|
defp format_uptime(nil), do: "Unknown"
|
|
|
|
defp format_uptime(seconds) when is_integer(seconds) do
|
|
cond do
|
|
seconds < 60 -> "#{seconds}s"
|
|
seconds < 3600 -> "#{div(seconds, 60)}m"
|
|
seconds < 86_400 -> "#{div(seconds, 3600)}h #{rem(div(seconds, 60), 60)}m"
|
|
true -> "#{div(seconds, 86_400)}d #{rem(div(seconds, 3600), 24)}h"
|
|
end
|
|
end
|
|
|
|
defp format_uptime(_), do: "Unknown"
|
|
|
|
defp assignment_source(equipment, agent_token_id) do
|
|
cond do
|
|
# Direct assignment
|
|
Enum.any?(equipment.agent_assignments || [], fn a ->
|
|
a.agent_token_id == agent_token_id and a.enabled
|
|
end) ->
|
|
{:direct, "Directly assigned"}
|
|
|
|
# Site assignment
|
|
equipment.site && equipment.site.agent_token_id == agent_token_id ->
|
|
{:site, "From site: #{equipment.site.name}"}
|
|
|
|
# Organization assignment
|
|
equipment.organization && equipment.organization.default_agent_token_id == agent_token_id ->
|
|
{:organization, "From organization"}
|
|
|
|
# Shouldn't happen but handle gracefully
|
|
true ->
|
|
{:unknown, "Unknown"}
|
|
end
|
|
end
|
|
|
|
defp source_badge_class(source) do
|
|
case source do
|
|
:direct -> "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"
|
|
:site -> "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300"
|
|
:organization -> "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300"
|
|
:unknown -> "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-300"
|
|
end
|
|
end
|
|
end
|