Files
wireguard-admin/app/templates/peers.html
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 1b227c2470 Multi-interface management, topology view, performance tuning, precompiled CSS
- Interface model with per-interface subnet/port/keys; import/adopt existing wg-quick configs (key-less imported peers, optional key rotation), cascade delete
- Split wireguard.py into a package (keys via cryptography X25519, status, addressing, conf parse/render, sync, host tuning)
- ECharts horizontal topology view (interface -> peers -> site subnets)
- Advanced options: MTU, MSS clamping, FwMark/Table, custom PostUp/PostDown, per-peer keepalive override
- Runtime settings (sample interval/retention, online threshold, UI refresh) with traffic sample pruning; host tuning (UDP buffers, backlog, GRO forwarding)
- Precompiled Tailwind CSS replacing Play CDN runtime (fixes FOUC); stable table layout and diffed polling renders
- Host network mode in compose; NAT/isolation iptables moved into app sync

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-05 14:01:44 +08:00

149 lines
7.8 KiB
HTML

{% extends "base.html" %}
{% block title %}Peers - WireGuard Admin{% endblock %}
{% block breadcrumbs %}<span>/</span> <span>Peers</span>{% endblock %}
{% block content %}
<h1 class="mb-6 text-2xl font-bold">Peers</h1>
{% 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 gap-1 border-b border-slate-200">
{% for iface in interfaces %}
<a href="/peers?interface={{ iface.id }}"
class="rounded-t-md px-4 py-2 text-sm font-medium {% if current and iface.id == current.id %}border border-b-0 border-slate-200 bg-white text-blue-600{% else %}text-slate-500 hover:text-slate-700{% endif %}">
{{ iface.name }}
<span class="ml-1 rounded-full bg-slate-100 px-1.5 text-xs text-slate-500">{{ iface.peers | length }}</span>
</a>
{% endfor %}
<a href="/interfaces" class="ml-auto px-4 py-2 text-sm text-slate-400 hover:text-slate-600">Manage interfaces</a>
</div>
{% if not current %}
<p class="rounded-xl bg-white p-6 text-center text-slate-400 shadow-sm">
No interfaces configured. <a href="/interfaces" class="text-blue-600 hover:underline">Create one first.</a>
</p>
{% else %}
<div class="mb-8 rounded-xl bg-white p-5 shadow-sm">
<h2 class="mb-4 text-xs font-semibold uppercase tracking-wide text-slate-400">Add peer to {{ current.name }}</h2>
<form method="post" action="/peers" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<input type="hidden" name="interface_id" value="{{ current.id }}">
<label class="block text-sm font-medium text-slate-600">Name
<input name="name" required placeholder="laptop"
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">Note
<input name="note" placeholder="optional"
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">IP address
<input name="address" placeholder="auto"
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" placeholder="{{ current.dns }}"
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"
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">Count
<input name="count" type="number" min="1" max="50" value="1"
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">Expires
<input name="expires_at" type="datetime-local"
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">Site subnets
<input name="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">
</label>
<label class="block text-sm font-medium text-slate-600">Client routes
<input name="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">
</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">
Add peer(s)
</button>
</div>
</form>
<p class="mt-3 text-xs text-slate-400">
Count &gt; 1 creates a batch named name-1, name-2, ... (custom IP ignored). Empty quota = unlimited.
</p>
</div>
<div class="overflow-hidden rounded-xl bg-white shadow-sm">
<table class="w-full text-sm">
<thead>
<tr class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-400">
<th class="px-4 py-3 font-semibold">Name</th>
<th class="px-4 py-3 font-semibold">Address</th>
<th class="px-4 py-3 font-semibold">Enabled</th>
<th class="px-4 py-3 font-semibold">Status</th>
<th class="px-4 py-3 font-semibold">Usage</th>
<th class="px-4 py-3 font-semibold">Note</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for peer in peers %}
{% set ps = status.peers.get(peer.public_key) %}
<tr class="hover:bg-slate-50">
<td class="px-4 py-3">
<a href="/peers/{{ peer.id }}" class="font-medium text-blue-600 hover:underline">{{ peer.name }}</a>
{% if not peer.has_private_key %}
<span class="ml-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700">imported</span>
{% endif %}
</td>
<td class="px-4 py-3 font-mono text-xs">{{ peer.address }}</td>
<td class="px-4 py-3">
{% if peer.enabled %}
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">enabled</span>
{% elif 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>
{% else %}
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">disabled</span>
{% endif %}
</td>
<td class="px-4 py-3">
{% if ps and ps.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 %}
</td>
<td class="px-4 py-3 text-slate-500">
{{ peer.cum_total | fmt_bytes }}{% if peer.quota_bytes %} / {{ peer.quota_bytes | fmt_bytes }}{% endif %}
</td>
<td class="px-4 py-3 text-slate-500">{{ peer.note }}</td>
<td class="px-4 py-3 text-right">
<div class="flex justify-end gap-2">
<form method="post" action="/peers/{{ peer.id }}/toggle">
<button type="submit"
class="rounded-md border border-slate-300 px-3 py-1 text-xs font-medium text-slate-600 hover:bg-slate-100">
{{ "Disable" if peer.enabled else "Enable" }}
</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 px-3 py-1 text-xs font-medium text-red-600 hover:bg-red-50">
Delete
</button>
</form>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="7" class="px-4 py-6 text-center text-slate-400">No peers yet. Add one above.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}