Files
wireguard-admin/app/templates/peer_detail.html
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 5c7bdff028 Fix form alignment on interface edit and peer detail pages
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-07 00:23:46 +08:00

260 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ peer.name }} - WireGuard Admin{% endblock %}
{% block breadcrumbs %}<span>/</span> <a href="/peers" class="text-blue-600 hover:underline">Peers</a> <span>/</span> <span>{{ peer.name }}</span>{% endblock %}
{% block content %}
{% if error %}
<p class="mb-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700">{{ error }}</p>
{% endif %}
<div class="mb-6 flex flex-wrap items-center justify-between gap-3">
<h1 class="flex items-center gap-2 text-2xl font-bold">
{{ peer.name }}
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500">{{ iface.name }}</span>
{% if not peer.has_private_key %}
<span class="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700">imported</span>
{% endif %}
</h1>
<div class="flex flex-wrap gap-2">
<form method="post" action="/peers/{{ peer.id }}/toggle">
<button type="submit"
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-100">
{{ "Disable" if peer.enabled else "Enable" }}
</button>
</form>
<form method="post" action="/peers/{{ peer.id }}/rotate"
onsubmit="return confirm('Rotate keys? The old client config stops working immediately.')">
<button type="submit"
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-100">
Rotate keys
</button>
</form>
<form method="post" action="/peers/{{ peer.id }}/reset-usage"
onsubmit="return confirm('Reset usage counters to zero?')">
<button type="submit"
class="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-100">
Reset usage
</button>
</form>
<form method="post" action="/peers/{{ peer.id }}/delete"
onsubmit="return confirm('Delete peer {{ peer.name }}?')">
<button type="submit"
class="rounded-md border border-red-200 bg-white px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50">
Delete
</button>
</form>
</div>
</div>
<div class="grid gap-4 lg:grid-cols-3">
<div class="rounded-xl bg-white p-5 shadow-sm">
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Details</h3>
<dl class="space-y-2 text-sm">
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Address</dt>
<dd class="font-mono text-xs">{{ peer.address }}</dd>
</div>
<div>
<dt class="text-slate-500">Public key</dt>
<dd class="mt-1 break-all font-mono text-xs text-slate-600">{{ peer.public_key }}</dd>
</div>
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Enabled</dt>
<dd>
{% if peer.enabled %}
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">yes</span>
{% else %}
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">no</span>
{% endif %}
</dd>
</div>
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Created</dt>
<dd>{{ peer.created_at.strftime("%Y-%m-%d %H:%M") }}</dd>
</div>
{% if peer.expires_at %}
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Expires</dt>
<dd>{{ peer.expires_at.strftime("%Y-%m-%d %H:%M") }}</dd>
</div>
{% endif %}
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Usage</dt>
<dd>
{{ peer.cum_total | fmt_bytes }}
{% if peer.quota_bytes %} / {{ peer.quota_bytes | fmt_bytes }}
{% if peer.over_quota %}
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">over quota</span>
{% endif %}
{% endif %}
</dd>
</div>
</dl>
</div>
<div class="rounded-xl bg-white p-5 shadow-sm">
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Live status</h3>
{% if peer_status %}
<dl class="space-y-2 text-sm">
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Status</dt>
<dd>
{% if peer_status.online %}
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">online</span>
{% else %}
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500">offline</span>
{% endif %}
</dd>
</div>
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Endpoint</dt>
<dd class="font-mono text-xs">{{ peer_status.endpoint or "-" }}</dd>
</div>
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Handshake</dt>
<dd>{{ peer_status.latest_handshake.strftime("%Y-%m-%d %H:%M:%S") if peer_status.latest_handshake else "never" }}</dd>
</div>
<div class="flex items-baseline justify-between gap-2">
<dt class="text-slate-500">Session traffic</dt>
<dd>RX {{ peer_status.rx_bytes | fmt_bytes }} / TX {{ peer_status.tx_bytes | fmt_bytes }}</dd>
</div>
</dl>
{% else %}
<p class="text-sm text-slate-400">Not present on interface (disabled or interface down).</p>
{% endif %}
</div>
<div class="rounded-xl bg-white p-5 shadow-sm">
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Client config</h3>
{% if peer.has_private_key %}
<img src="/peers/{{ peer.id }}/qr" alt="QR code" class="mx-auto w-48 rounded-lg border border-slate-200">
<div class="mt-4 flex justify-center gap-2">
<a href="/peers/{{ peer.id }}/config"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
Download .conf
</a>
<button type="button" onclick="copyConfig(this)"
class="rounded-md border border-slate-300 px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100">
Copy
</button>
</div>
{% else %}
<p class="text-sm text-slate-500">
This peer was imported from an existing config, so the server only knows its public key.
The client keeps using its current config and everything works, but the panel cannot
generate a .conf or QR code.
</p>
<p class="mt-3 text-sm text-slate-500">
To manage the client config from here, rotate its keys and install the new config on the device.
</p>
<form method="post" action="/peers/{{ peer.id }}/rotate" class="mt-4 text-center"
onsubmit="return confirm('Rotate keys? The device stays offline until the new config is installed.')">
<button type="submit"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
Rotate keys and generate config
</button>
</form>
{% endif %}
</div>
</div>
<div class="mt-4 rounded-xl bg-white p-5 shadow-sm">
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Edit</h3>
<form method="post" action="/peers/{{ peer.id }}/update" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<label class="block text-sm font-medium text-slate-600">Note
<input name="note" value="{{ peer.note }}"
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</label>
<label class="block text-sm font-medium text-slate-600">DNS
<input name="dns" value="{{ peer.dns }}" placeholder="server default"
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</label>
<label class="block text-sm font-medium text-slate-600">Quota (GiB)
<input name="quota_gib" type="number" step="0.1" min="0" placeholder="unlimited"
value="{{ '%.1f' % (peer.quota_bytes / 1073741824) if peer.quota_bytes else '' }}"
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
</label>
<label class="block text-sm font-medium text-slate-600">Keepalive (s)
<input name="persistent_keepalive" type="number" min="0" max="3600"
value="{{ peer.persistent_keepalive if peer.persistent_keepalive is not none else '' }}"
placeholder="interface default ({{ iface.persistent_keepalive }})"
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
<span class="mt-1 block text-xs text-slate-400">0 disables keepalive (fixed-IP site-to-site peers). Empty = interface default.</span>
</label>
<label class="block text-sm font-medium text-slate-600">Site subnets behind this peer
<input name="extra_allowed_ips" value="{{ peer.extra_allowed_ips }}" placeholder="e.g. 192.168.5.0/24"
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
<span class="mt-1 block text-xs text-slate-400">Server-side AllowedIPs: routes these networks to this peer (site-to-site). Peer must forward traffic.</span>
</label>
<label class="block text-sm font-medium text-slate-600">Client routes (AllowedIPs)
<input name="client_allowed_ips" value="{{ peer.client_allowed_ips }}" placeholder="server default"
class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
<span class="mt-1 block text-xs text-slate-400">What this client routes through the tunnel. Empty = server default. Full tunnel: 0.0.0.0/0, ::/0</span>
</label>
<div class="col-span-full">
<button type="submit"
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
Save
</button>
</div>
</form>
</div>
{% if client_config %}
<div class="mt-4 rounded-xl bg-white p-5 shadow-sm">
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">{{ peer.name }}.conf</h3>
<pre id="client-config" class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-relaxed text-slate-200">{{ client_config }}</pre>
</div>
{% endif %}
{% if client_config %}
<div class="mt-4 rounded-xl bg-white p-5 shadow-sm">
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-400">Client setup guide</h3>
<div class="space-y-5 text-sm">
<div>
<h4 class="mb-2 font-semibold text-slate-700">Linux (wg-quick)</h4>
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-relaxed text-slate-200"># Install wireguard tools
sudo apt install wireguard # Debian/Ubuntu
sudo dnf install wireguard-tools # Fedora/RHEL
# Save the config (downloaded or copied from above)
sudo mv {{ peer.name }}.conf /etc/wireguard/{{ peer.name }}.conf
sudo chmod 600 /etc/wireguard/{{ peer.name }}.conf
# Bring the tunnel up / down
sudo wg-quick up {{ peer.name }}
sudo wg-quick down {{ peer.name }}
# Start automatically at boot
sudo systemctl enable --now wg-quick@{{ peer.name }}
# Verify: check handshake and ping the server
sudo wg show
ping {{ server_tunnel_ip }}</pre>
</div>
<div>
<h4 class="mb-2 font-semibold text-slate-700">Linux (NetworkManager)</h4>
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs leading-relaxed text-slate-200">nmcli connection import type wireguard file {{ peer.name }}.conf
nmcli connection up {{ peer.name }}</pre>
</div>
<div>
<h4 class="mb-2 font-semibold text-slate-700">macOS / Windows</h4>
<p class="text-slate-500">Install the official WireGuard app, then Import Tunnel from File and select the downloaded <span class="font-mono text-xs">{{ peer.name }}.conf</span>, or Add Empty Tunnel and paste the config above.</p>
</div>
<div>
<h4 class="mb-2 font-semibold text-slate-700">iOS / Android</h4>
<p class="text-slate-500">Install the WireGuard app, tap Add Tunnel, then Create from QR code and scan the QR code above.</p>
</div>
</div>
</div>
{% endif %}
<script>
function copyConfig(btn) {
navigator.clipboard.writeText(document.getElementById("client-config").textContent)
.then(() => {
btn.textContent = "Copied";
setTimeout(() => { btn.textContent = "Copy"; }, 1500);
});
}
</script>
{% endblock %}