towerops/lib/towerops_web/live/agent_live/index.html.heex
Graham McIntire 538f582fec
Update agent setup instructions with Watchtower auto-updates
- Change default agent image to gmcintire/towerops-agent:main
- Add Watchtower service to docker-compose instructions
- Configure 15-minute update check interval for development
- Add DOCKER_API_VERSION=1.44 to prevent API compatibility errors
- Remove deprecated docker-compose version field
2026-01-16 17:38:30 -06:00

312 lines
12 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
current_organization={@current_organization}
active_page="agents"
>
<.header>
{@page_title}
<:subtitle>Manage remote agents for local SNMP polling</:subtitle>
<:actions>
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
<.icon name="hero-plus" class="h-5 w-5" /> New Agent
</.button>
</:actions>
</.header>
<!-- New Agent Form -->
<div id="new-agent-form" class="hidden mt-6">
<div class="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">
Create New Agent
</h3>
<.form for={@agent_form} phx-submit="create_agent">
<div class="space-y-4">
<.input
field={@agent_form[:name]}
type="text"
label="Agent Name"
placeholder="e.g., Datacenter A"
required
/>
<div class="flex gap-3">
<.button type="submit" variant="primary">
Create Agent
</.button>
<button
type="button"
phx-click={JS.hide(to: "#new-agent-form")}
class="px-3 py-2 text-sm font-semibold text-zinc-700 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-zinc-100"
>
Cancel
</button>
</div>
</div>
</.form>
</div>
</div>
<%= if @agent_tokens == [] do %>
<div class="text-center py-16">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-600" />
<h3 class="mt-4 text-lg font-semibold text-zinc-900 dark:text-zinc-100">No agents</h3>
<p class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
Get started by creating your first remote agent.
</p>
<div class="mt-6">
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
<.icon name="hero-plus" class="h-5 w-5" /> New Agent
</.button>
</div>
</div>
<% else %>
<div class="mt-6">
<.table id="agents-table" rows={@agent_tokens}>
<:col :let={agent} label="Name">
<.link
navigate={~p"/orgs/#{@current_organization.slug}/agents/#{agent.id}"}
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{agent.name}
</.link>
<%= if agent.metadata["version"] do %>
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
v{agent.metadata["version"]}
</div>
<% end %>
</:col>
<:col :let={agent} label="Status">
<% {status, label} = agent_status(agent) %>
<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>
</:col>
<:col :let={agent} label="Equipment">
<% counts = Map.get(@equipment_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-zinc-900 dark:text-zinc-100">
{counts.total} total
</div>
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
{counts.direct} direct
<%= if counts.total > counts.direct do %>
· {counts.total - counts.direct} inherited
<% end %>
</div>
</:col>
<:col :let={agent} label="Last Seen">
<div class="text-sm text-zinc-900 dark:text-zinc-100">
{format_last_seen(agent.last_seen_at)}
</div>
<%= if agent.last_seen_at do %>
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
{format_datetime(agent.last_seen_at)}
</div>
<% end %>
<%= if agent.last_ip do %>
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5 font-mono">
{agent.last_ip}
</div>
<% end %>
</:col>
<:col :let={agent} label="Metadata">
<%= if agent.metadata["hostname"] do %>
<div class="text-sm text-zinc-600 dark:text-zinc-400">
<.icon name="hero-computer-desktop" class="h-4 w-4 inline" />
{agent.metadata["hostname"]}
</div>
<% end %>
</:col>
<:col :let={agent} label="Created">
<div class="text-sm text-zinc-600 dark:text-zinc-400">
{Calendar.strftime(agent.inserted_at, "%b %d, %Y")}
</div>
</:col>
<:action :let={agent}>
<%= if agent.enabled do %>
<div class="flex items-center gap-3">
<button
type="button"
phx-click="show_setup"
phx-value-id={agent.id}
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
View Setup
</button>
<button
type="button"
phx-click="revoke_agent"
phx-value-id={agent.id}
data-confirm="Are you sure you want to revoke this agent? It will no longer be able to authenticate."
class="text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
Revoke
</button>
</div>
<% else %>
<span class="text-xs text-zinc-500 dark:text-zinc-400">Revoked</span>
<% end %>
</:action>
</.table>
</div>
<% end %>
<!-- Token Display Modal -->
<%= if @show_token_modal && @new_token do %>
<div
id="token-modal"
class="fixed inset-0 z-50 overflow-y-auto"
phx-mounted={JS.show()}
>
<div class="flex min-h-screen items-center justify-center p-4">
<div
class="fixed inset-0 bg-zinc-500/75 dark:bg-zinc-950/75 transition-opacity"
phx-click="close_token_modal"
>
</div>
<div class="relative bg-white dark:bg-zinc-900 rounded-lg shadow-xl max-w-2xl w-full">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
Agent Setup Instructions
</h3>
<p class="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Use the token and Docker Compose configuration below to deploy this agent.
</p>
</div>
<button
type="button"
phx-click="close_token_modal"
class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
>
<.icon name="hero-x-mark" class="h-6 w-6" />
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Agent Name
</label>
<div class="text-base font-mono text-zinc-900 dark:text-zinc-100">
{@new_token.agent_token.name}
</div>
</div>
<div>
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-2">
Authentication Token
</label>
<div class="relative">
<input
type="text"
readonly
value={@new_token.token}
class="w-full font-mono text-sm bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded px-3 py-2 pr-24 text-zinc-900 dark:text-zinc-100"
id="agent-token-input"
/>
<button
type="button"
phx-click={
JS.dispatch("phx:copy", to: "#agent-token-input")
|> JS.push("token_copied")
}
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded hover:bg-zinc-200 dark:hover:bg-zinc-700"
>
<.icon name="hero-clipboard" class="h-4 w-4" />
</button>
</div>
</div>
<div class="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg p-4">
<h4 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-2">
Docker Compose Setup with Auto-Updates
</h4>
<p class="text-xs text-zinc-600 dark:text-zinc-400 mb-3">
This configuration includes Watchtower for automatic agent updates. The agent will automatically update to the latest version every 15 minutes during development.
</p>
<pre class="text-xs font-mono text-zinc-600 dark:text-zinc-400 overflow-x-auto"><code>services:
towerops-agent:
image: {@agent_image}
container_name: towerops-agent
restart: unless-stopped
environment:
- TOWEROPS_API_URL={url(@socket, ~p"/")}
- TOWEROPS_AGENT_TOKEN={@new_token.token}
volumes:
- ./data:/data
labels:
- "com.centurylinklabs.watchtower.enable=true"
- "com.centurylinklabs.watchtower.scope=towerops"
watchtower:
image: containrrr/watchtower:latest
container_name: towerops-watchtower
restart: unless-stopped
environment:
# Check for updates every 15 minutes (900 seconds)
- WATCHTOWER_POLL_INTERVAL=900
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_LOG_LEVEL=info
# Use latest Docker API version
- DOCKER_API_VERSION=1.44
volumes:
- /var/run/docker.sock:/var/run/docker.sock</code></pre>
<button
type="button"
phx-click={JS.dispatch("phx:copy", to: "#docker-compose-example")}
class="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
Copy to clipboard
</button>
<textarea id="docker-compose-example" class="sr-only">services:
towerops-agent:
image: {@agent_image}
container_name: towerops-agent
restart: unless-stopped
environment:
- TOWEROPS_API_URL={url(@socket, ~p"/")}
- TOWEROPS_AGENT_TOKEN={@new_token.token}
volumes:
- ./data:/data
labels:
- "com.centurylinklabs.watchtower.enable=true"
- "com.centurylinklabs.watchtower.scope=towerops"
watchtower:
image: containrrr/watchtower:latest
container_name: towerops-watchtower
restart: unless-stopped
environment:
# Check for updates every 15 minutes (900 seconds)
- WATCHTOWER_POLL_INTERVAL=900
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_LOG_LEVEL=info
# Use latest Docker API version
- DOCKER_API_VERSION=1.44
volumes:
- /var/run/docker.sock:/var/run/docker.sock</textarea>
</div>
</div>
<div class="mt-6 flex justify-end">
<.button phx-click="close_token_modal" variant="primary">
I've Saved the Token
</.button>
</div>
</div>
</div>
</div>
</div>
<% end %>
</Layouts.authenticated>