proxMon/server/lib/server_web/live/admin_hosts_live.ex
Carsten 50676a7cb8 refactor(ui): minimalistic utilitarian redesign across all views
New design language:
  - dark background, system sans for UI, monospace for data
  - single green accent, amber/red for warn/critical
  - square-bordered panels + tables, no rounded cards or shadows
  - status conveyed via left-border on overview cards + badges

Changes:
  - new app.css defines CSS vars + component classes (.panel, .tbl,
    .card, .btn, .input, .badge with [data-status=*])
  - new ServerWeb.DashboardNav function component for a shared top nav
    with active-link highlighting; replaces per-view navigation clutter
  - strip the Phoenix welcome scaffold (logo, version badge, twitter/GH
    links) from layouts/app.html.heex; leaves only flash + content
  - root.html.heex title suffix switched to 'Proxmox Monitor', body
    loses the Tailwind-white background
  - rewrite render/1 in all four LiveViews + login template to use the
    new classes; admin form now uses <.form for={@form}> and properly
    clears on success
  - login page redesigned to a single tight panel matching the rest

All 58 tests still pass; 'mix compile --warnings-as-errors' is clean.
2026-04-22 10:18:46 +02:00

142 lines
4.4 KiB
Elixir

defmodule ServerWeb.AdminHostsLive do
use ServerWeb, :live_view
alias Server.{Hosts, Repo, Schema.Host}
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(
hosts: Hosts.list_all(),
form: to_form(%{"name" => ""}, as: :host),
new_token: nil,
error: nil,
page_title: "Admin"
)}
end
@impl true
def handle_event("create", %{"host" => %{"name" => name}}, socket) do
case Hosts.create_host(name) do
{:ok, {host, token}} ->
{:noreply,
socket
|> assign(
hosts: Hosts.list_all(),
form: to_form(%{"name" => ""}, as: :host),
new_token: %{name: host.name, token: token},
error: nil
)}
{:error, cs} ->
{:noreply, assign(socket, :error, changeset_message(cs))}
end
end
def handle_event("rotate", %{"id" => id}, socket) do
%Host{} = host = Repo.get!(Host, id)
{:ok, {_, token}} = Hosts.rotate_token(host)
{:noreply,
socket
|> assign(hosts: Hosts.list_all(), new_token: %{name: host.name, token: token})}
end
def handle_event("delete", %{"id" => id}, socket) do
%Host{} = host = Repo.get!(Host, id)
{:ok, _} = Hosts.delete_host(host)
{:noreply, assign(socket, :hosts, Hosts.list_all())}
end
defp changeset_message(cs) do
cs.errors |> Enum.map_join(", ", fn {k, {msg, _}} -> "#{k}: #{msg}" end)
end
@impl true
def render(assigns) do
~H"""
<ServerWeb.DashboardNav.nav active={:admin} />
<div class="page">
<div class="pagehead">
<h1>Admin · Hosts</h1>
<span class="sub">{length(@hosts)} registered</span>
</div>
<div class="panel">
<header><span>Register a host</span></header>
<div class="body">
<.form for={@form} phx-submit="create" class="form-row">
<input
name="host[name]"
value=""
placeholder="pve-hostname"
required
autocomplete="off"
class="input"
/>
<button type="submit" class="btn primary">Add</button>
</.form>
<p :if={@error} class="callout err" style="margin-top: 0.7rem;">{@error}</p>
<div :if={@new_token} class="callout ok" style="margin-top: 0.7rem;">
Token for <strong class="mono">{@new_token.name}</strong> — shown once:<br/>
<code class="mono" style="color: var(--fg-bright); user-select: all;">{@new_token.token}</code>
</div>
</div>
</div>
<div class="panel">
<header><span>All hosts</span></header>
<div class="body tight">
<table class="tbl" :if={@hosts != []}>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Agent</th>
<th>Last seen</th>
<th class="right">Actions</th>
</tr>
</thead>
<tbody>
<tr :for={h <- @hosts}>
<td class="mono">
<.link navigate={~p"/hosts/#{h.name}"}>{h.name}</.link>
</td>
<td><span class="badge" data-status={h.status}>{h.status}</span></td>
<td class="mono">{h.agent_version || "—"}</td>
<td class="mono">{format_seen(h.last_seen_at)}</td>
<td class="right">
<button
type="button"
phx-click="rotate"
phx-value-id={h.id}
class="btn sm ghost"
data-confirm={"Rotate token for #{h.name}? Old token stops working immediately."}
>
rotate
</button>
<button
type="button"
phx-click="delete"
phx-value-id={h.id}
class="btn sm danger"
data-confirm={"Delete #{h.name} and all its metrics?"}
>
delete
</button>
</td>
</tr>
</tbody>
</table>
<div :if={@hosts == []} class="empty">No hosts yet.</div>
</div>
</div>
</div>
"""
end
defp format_seen(nil), do: "never"
defp format_seen(%DateTime{} = dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M UTC")
end