37039351be
- wireguard: POST /peers with JSON encoding (was /add-peer) - rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render - nat: port forward delete uses URL path params to match blueprint - nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch) - app.js renderers updated to use URL path deletes for rules and forwards - remove TODO.md
124 lines
5.5 KiB
HTML
124 lines
5.5 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}WireGuard - Vacuum Wall{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>WireGuard</h1>
|
|
<div class="subtitle">VPN tunnel management</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="btn {{ 'btn-outline' if (wg_status is defined and wg_status.get('state') == 'up') else 'btn-primary' }}"
|
|
hx-post="/api/wireguard/down"
|
|
hx-swap="none"
|
|
hx-on::after-request="if(evt.detail.successful){ showSuccessToast('Tunnel stopped'); }">
|
|
Stop Tunnel
|
|
</button>
|
|
<button class="btn {{ 'btn-outline' if (wg_status is not defined or wg_status.get('state') != 'up') else 'btn-primary' }}"
|
|
hx-post="/api/wireguard/apply"
|
|
hx-swap="none"
|
|
hx-on::after-request="if(evt.detail.successful){ showSuccessToast('Tunnel started'); }">
|
|
Start Tunnel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tunnel Status -->
|
|
<div class="card mb-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3>
|
|
<span class="status-dot {{ 'status-up' if (wg_status is defined and wg_status.get('state') == 'up') else 'status-down' }}"></span>
|
|
Tunnel State: <strong>{{ 'UP' if (wg_status is defined and wg_status.get('state') == 'up') else 'DOWN' }}</strong>
|
|
</h3>
|
|
</div>
|
|
<div class="text-sm text-muted">
|
|
{% if config %}
|
|
Listen Port: <strong>{{ config.get('listen_port', 'N/A') }}</strong> |
|
|
Public Key: <strong>{{ config.get('public_key', 'N/A')[:12] if config.get('public_key') else 'N/A' }}...</strong> |
|
|
Address: <strong>{{ config.get('address', 'N/A') }}</strong>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Peer Form -->
|
|
<div class="section-title">Peers</div>
|
|
|
|
<div class="card mb-4">
|
|
<h3>Add Peer</h3>
|
|
<form hx-post="/api/wireguard/peers" hx-encoding="json" hx-swap="none" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/wireguard/peers', document.getElementById('peer-rows'), renderPeers); showSuccessToast('Peer added'); }">
|
|
<div class="inline-form">
|
|
<div class="form-group">
|
|
<label for="peer-name">Name</label>
|
|
<input type="text" id="peer-name" name="name" placeholder="client-1" required style="width:140px;">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="peer-pubkey">Public Key</label>
|
|
<input type="text" id="peer-pubkey" name="public_key" placeholder="Base64 public key (48 chars)" required style="width:260px;">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="peer-allowed">Allowed IPs</label>
|
|
<input type="text" id="peer-allowed" name="allowed_ips" placeholder="10.8.0.2/32" value="10.8.0.{% set next = (peers or []|length + 2) %}{{ next }}/32" required style="width:160px;">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Add Peer</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Peers Table -->
|
|
<div class="card">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Public Key</th>
|
|
<th>Allowed IPs</th>
|
|
<th>Endpoint</th>
|
|
<th>Latest Handshake</th>
|
|
<th>Transfer</th>
|
|
<th style="width:160px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="peer-rows">
|
|
{% for peer in (peers or []) %}
|
|
<tr>
|
|
<td>
|
|
<span class="status-dot {{ 'status-up' if peer.get('latest_handshake') else 'status-down' }}"></span>
|
|
<strong>{{ peer.get('name', 'unnamed') }}</strong>
|
|
</td>
|
|
<td style="font-family:monospace;font-size:11px;">{{ peer.get('public_key', 'N/A')[:20] }}...</td>
|
|
<td class="text-sm">{{ peer.get('allowed_ips', '-') }}</td>
|
|
<td class="text-sm">{{ peer.get('endpoint', '-') }}</td>
|
|
<td class="text-sm">{{ peer.get('latest_handshake', 'Never') or 'Never' }}</td>
|
|
<td class="text-sm">
|
|
<div>Recv: {{ peer.get('transfer_recv', '0') or '0' }}</div>
|
|
<div>Sent: {{ peer.get('transfer_sent', '0') or '0' }}</div>
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-2">
|
|
<button class="btn btn-sm btn-outline" onclick="downloadPeerConfig('{{ peer.get('name', '') }}')">Config</button>
|
|
<form hx-delete="/api/wireguard/peers/{{ peer.get('name', '') }}" hx-swap="none" hx-confirm="Remove peer {{ peer.get('name', '') }}?" hx-on::after-request="if(evt.detail.successful){ refreshTable('/api/wireguard/peers', document.getElementById('peer-rows'), renderPeers); showSuccessToast('Peer removed'); }">
|
|
<button type="submit" class="btn btn-sm btn-danger">Remove</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if not (peers or []) %}
|
|
<tr>
|
|
<td colspan="7" class="text-muted text-sm">No peers configured. Add a peer above.</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
function downloadPeerConfig(peerName) {
|
|
var url = '/api/wireguard/peers/' + encodeURIComponent(peerName) + '/config';
|
|
window.open(url, '_blank');
|
|
}
|
|
</script>
|
|
{% endblock %}
|