Restyle UI with Tailwind CSS: consistent cards, forms, tables and badges across all pages
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
628246c5c5
commit
08b32d2aa9
+11
-9
@@ -40,21 +40,23 @@ async function refresh() {
|
||||
|
||||
const tbody = document.querySelector("#peer-table tbody");
|
||||
if (!tbody) return;
|
||||
const badgeBase = "rounded-full px-2 py-0.5 text-xs font-medium";
|
||||
tbody.innerHTML = "";
|
||||
for (const p of data.peers) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "hover:bg-slate-50";
|
||||
const badge = p.online
|
||||
? '<span class="badge ok">online</span>'
|
||||
? `<span class="${badgeBase} bg-emerald-100 text-emerald-700">online</span>`
|
||||
: p.enabled
|
||||
? '<span class="badge muted">offline</span>'
|
||||
: '<span class="badge err">disabled</span>';
|
||||
? `<span class="${badgeBase} bg-slate-100 text-slate-500">offline</span>`
|
||||
: `<span class="${badgeBase} bg-red-100 text-red-700">disabled</span>`;
|
||||
tr.innerHTML =
|
||||
`<td><a href="/peers/${p.id}"></a></td>` +
|
||||
`<td>${p.address}</td>` +
|
||||
`<td>${badge}</td>` +
|
||||
`<td>${fmtHandshake(p.latest_handshake)}</td>` +
|
||||
`<td>${fmtBytes(p.rx_bytes)}</td>` +
|
||||
`<td>${fmtBytes(p.tx_bytes)}</td>`;
|
||||
`<td class="px-4 py-3"><a href="/peers/${p.id}" class="font-medium text-blue-600 hover:underline"></a></td>` +
|
||||
`<td class="px-4 py-3 font-mono text-xs">${p.address}</td>` +
|
||||
`<td class="px-4 py-3">${badge}</td>` +
|
||||
`<td class="px-4 py-3 text-slate-500">${fmtHandshake(p.latest_handshake)}</td>` +
|
||||
`<td class="px-4 py-3 text-slate-500">${fmtBytes(p.rx_bytes)}</td>` +
|
||||
`<td class="px-4 py-3 text-slate-500">${fmtBytes(p.tx_bytes)}</td>`;
|
||||
tr.querySelector("a").textContent = p.name;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: #f4f5f7;
|
||||
color: #1f2430;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #1f2430;
|
||||
color: #fff;
|
||||
}
|
||||
nav .brand { font-weight: 700; margin-right: 1rem; }
|
||||
nav a { color: #cdd3e0; text-decoration: none; }
|
||||
nav a:hover { color: #fff; }
|
||||
nav form { margin-left: auto; }
|
||||
main { max-width: 960px; margin: 0 auto; padding: 1.5rem; }
|
||||
h1 { margin-top: 0; }
|
||||
|
||||
.cards { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
.card h3 { margin: 0 0 0.5rem; font-size: 0.85rem; text-transform: uppercase; color: #6b7280; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; }
|
||||
th, td { text-align: left; padding: 0.6rem 0.8rem; border-bottom: 1px solid #e5e7eb; }
|
||||
thead th { background: #eef0f4; font-size: 0.8rem; text-transform: uppercase; color: #6b7280; }
|
||||
table.kv { background: transparent; }
|
||||
table.kv th { width: 200px; color: #6b7280; }
|
||||
|
||||
.badge { padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; }
|
||||
.badge.ok { background: #d1fae5; color: #065f46; }
|
||||
.badge.err { background: #fee2e2; color: #991b1b; }
|
||||
.badge.muted { background: #e5e7eb; color: #4b5563; }
|
||||
.muted { color: #6b7280; }
|
||||
.error { color: #991b1b; }
|
||||
|
||||
button, .button {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.9rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
button:hover, .button:hover { background: #1d4ed8; }
|
||||
button.danger { background: #dc2626; }
|
||||
button.danger:hover { background: #b91c1c; }
|
||||
button.link { background: none; padding: 0; color: #cdd3e0; }
|
||||
button.link:hover { color: #fff; background: none; }
|
||||
form.inline { display: inline; }
|
||||
.actions form { margin-right: 0.25rem; }
|
||||
.actions-row { margin-top: 1rem; display: flex; gap: 0.5rem; }
|
||||
|
||||
input {
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.add-peer { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.25rem; flex-wrap: wrap; }
|
||||
input.narrow { width: 80px; }
|
||||
input.mid { width: 160px; }
|
||||
.breadcrumbs { margin-bottom: 1rem; font-size: 0.85rem; color: #6b7280; }
|
||||
.breadcrumbs a { color: #2563eb; text-decoration: none; }
|
||||
.breadcrumbs a:hover { text-decoration: underline; }
|
||||
.config-card { margin-top: 1rem; }
|
||||
.config-card pre {
|
||||
background: #1f2430;
|
||||
color: #d6e2ff;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
form.stack label { display: block; margin-bottom: 0.5rem; }
|
||||
form.stack { margin-top: 0.75rem; }
|
||||
|
||||
.login-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
||||
.login-box {
|
||||
background: #fff;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
width: 320px;
|
||||
}
|
||||
.login-box h1 { font-size: 1.3rem; text-align: center; }
|
||||
.login-box label { display: block; margin-bottom: 0.75rem; font-size: 0.9rem; }
|
||||
.login-box input { width: 100%; margin-top: 0.25rem; }
|
||||
.login-box button { width: 100%; margin-top: 0.5rem; }
|
||||
|
||||
.qr { max-width: 220px; display: block; margin-bottom: 0.5rem; }
|
||||
code { word-break: break-all; font-size: 0.85em; }
|
||||
File diff suppressed because one or more lines are too long
+18
-16
@@ -4,24 +4,26 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}WireGuard Admin{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/tailwind.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<span class="brand">WireGuard Admin</span>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/peers">Peers</a>
|
||||
<a href="/settings">Settings</a>
|
||||
<form method="post" action="/logout" class="inline">
|
||||
<button type="submit" class="link">Logout</button>
|
||||
</form>
|
||||
<body class="min-h-screen bg-slate-100 text-slate-800">
|
||||
<nav class="bg-slate-900 text-slate-300">
|
||||
<div class="mx-auto flex max-w-5xl items-center gap-6 px-6 py-3">
|
||||
<span class="text-base font-bold tracking-wide text-white">WireGuard Admin</span>
|
||||
<a href="/" class="text-sm hover:text-white">Dashboard</a>
|
||||
<a href="/peers" class="text-sm hover:text-white">Peers</a>
|
||||
<a href="/settings" class="text-sm hover:text-white">Settings</a>
|
||||
<form method="post" action="/logout" class="ml-auto">
|
||||
<button type="submit" class="text-sm text-slate-400 hover:text-white">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="breadcrumbs">
|
||||
<a href="/">Home</a>
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
<main class="mx-auto max-w-5xl px-6 py-6">
|
||||
<div class="mb-4 flex items-center gap-1.5 text-sm text-slate-500">
|
||||
<a href="/" class="text-blue-600 hover:underline">Home</a>
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -2,34 +2,48 @@
|
||||
{% block title %}Dashboard - WireGuard Admin{% endblock %}
|
||||
{% block breadcrumbs %}<span>/</span> <span>Dashboard</span>{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<h3>Interface</h3>
|
||||
<p>{{ status.name }}
|
||||
{% if status.up %}<span class="badge ok">up</span>
|
||||
{% else %}<span class="badge err">down</span>{% endif %}
|
||||
<h1 class="mb-6 text-2xl font-bold">Dashboard</h1>
|
||||
|
||||
<div class="mb-8 grid gap-4 sm:grid-cols-3">
|
||||
<div class="rounded-xl bg-white p-5 shadow-sm">
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">Interface</h3>
|
||||
<p class="flex items-center gap-2 text-lg font-semibold">
|
||||
{{ status.name }}
|
||||
{% if status.up %}
|
||||
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">up</span>
|
||||
{% else %}
|
||||
<span class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">down</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="muted">Port {{ status.listen_port or settings.wg_port }}</p>
|
||||
<p class="mt-1 text-sm text-slate-500">Port {{ status.listen_port or settings.wg_port }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Peers</h3>
|
||||
<p><span id="peer-count">{{ peers | length }}</span> total,
|
||||
<span id="online-count">-</span> online</p>
|
||||
<div class="rounded-xl bg-white p-5 shadow-sm">
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">Peers</h3>
|
||||
<p class="text-lg font-semibold"><span id="peer-count">{{ peers | length }}</span> total</p>
|
||||
<p class="mt-1 text-sm text-slate-500"><span id="online-count">-</span> online</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Traffic</h3>
|
||||
<p>RX <span id="total-rx">{{ status.total_rx | fmt_bytes }}</span></p>
|
||||
<p>TX <span id="total-tx">{{ status.total_tx | fmt_bytes }}</span></p>
|
||||
<div class="rounded-xl bg-white p-5 shadow-sm">
|
||||
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">Traffic</h3>
|
||||
<p class="text-sm">RX <span id="total-rx" class="font-semibold">{{ status.total_rx | fmt_bytes }}</span></p>
|
||||
<p class="mt-1 text-sm">TX <span id="total-tx" class="font-semibold">{{ status.total_tx | fmt_bytes }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Peers</h2>
|
||||
<table id="peer-table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Address</th><th>Status</th><th>Handshake</th><th>RX</th><th>TX</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<h2 class="mb-3 text-lg font-semibold">Peers</h2>
|
||||
<div class="overflow-hidden rounded-xl bg-white shadow-sm">
|
||||
<table id="peer-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">Status</th>
|
||||
<th class="px-4 py-3 font-semibold">Handshake</th>
|
||||
<th class="px-4 py-3 font-semibold">RX</th>
|
||||
<th class="px-4 py-3 font-semibold">TX</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}<script src="/static/status.js"></script>{% endblock %}
|
||||
|
||||
@@ -4,16 +4,27 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login - WireGuard Admin</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/tailwind.js"></script>
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="login-box">
|
||||
<h1>WireGuard Admin</h1>
|
||||
{% if error %}<p class="error">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="/login">
|
||||
<label>Username <input name="username" autofocus required></label>
|
||||
<label>Password <input name="password" type="password" required></label>
|
||||
<button type="submit">Sign in</button>
|
||||
<body class="flex min-h-screen items-center justify-center bg-slate-900">
|
||||
<div class="w-80 rounded-xl bg-white p-8 shadow-2xl">
|
||||
<h1 class="mb-6 text-center text-xl font-bold text-slate-800">WireGuard Admin</h1>
|
||||
{% if error %}
|
||||
<p class="mb-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/login" class="space-y-4">
|
||||
<label class="block text-sm font-medium text-slate-600">Username
|
||||
<input name="username" autofocus required
|
||||
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">Password
|
||||
<input name="password" type="password" required
|
||||
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>
|
||||
<button type="submit"
|
||||
class="w-full rounded-md bg-blue-600 py-2 text-sm font-semibold text-white hover:bg-blue-700">
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
+148
-61
@@ -1,78 +1,165 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ peer.name }} - WireGuard Admin{% endblock %}
|
||||
{% block breadcrumbs %}<span>/</span> <a href="/peers">Peers</a> <span>/</span> <span>{{ peer.name }}</span>{% 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 %}
|
||||
<h1>{{ peer.name }}</h1>
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<h3>Details</h3>
|
||||
<p>Address: {{ peer.address }}</p>
|
||||
<p>Public key: <code>{{ peer.public_key }}</code></p>
|
||||
<p>Enabled:
|
||||
{% if peer.enabled %}<span class="badge ok">yes</span>
|
||||
{% else %}<span class="badge err">no</span>{% endif %}
|
||||
</p>
|
||||
<p class="muted">Created {{ peer.created_at.strftime("%Y-%m-%d %H:%M") }}</p>
|
||||
{% if peer.expires_at %}<p class="muted">Expires {{ peer.expires_at.strftime("%Y-%m-%d %H:%M") }}</p>{% endif %}
|
||||
<p>Usage: {{ peer.cum_total | fmt_bytes }}
|
||||
{% if peer.quota_bytes %} / {{ peer.quota_bytes | fmt_bytes }}
|
||||
{% if peer.over_quota %}<span class="badge err">over quota</span>{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<form method="post" action="/peers/{{ peer.id }}/update" class="stack">
|
||||
<label class="muted">Note <input name="note" value="{{ peer.note }}"></label>
|
||||
<label class="muted">DNS <input name="dns" value="{{ peer.dns }}" placeholder="server default"></label>
|
||||
<label class="muted">Quota GiB
|
||||
<input name="quota_gib" type="number" step="0.1" min="0" class="narrow"
|
||||
value="{{ '%.1f' % (peer.quota_bytes / 1073741824) if peer.quota_bytes else '' }}" placeholder="0 = unlimited">
|
||||
</label>
|
||||
<button type="submit">Save</button>
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold">{{ peer.name }}</h1>
|
||||
<div class="flex 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 class="card">
|
||||
<h3>Live status</h3>
|
||||
</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 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 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 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 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 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>
|
||||
<form method="post" action="/peers/{{ peer.id }}/update" class="mt-4 space-y-3 border-t border-slate-100 pt-4">
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
</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 %}
|
||||
<p>Status: {% if peer_status.online %}<span class="badge ok">online</span>{% else %}<span class="badge muted">offline</span>{% endif %}</p>
|
||||
<p>Endpoint: {{ peer_status.endpoint or "-" }}</p>
|
||||
<p>Handshake: {{ peer_status.latest_handshake.strftime("%Y-%m-%d %H:%M:%S") if peer_status.latest_handshake else "never" }}</p>
|
||||
<p>RX {{ peer_status.rx_bytes | fmt_bytes }} / TX {{ peer_status.tx_bytes | fmt_bytes }}</p>
|
||||
<dl class="space-y-2 text-sm">
|
||||
<div class="flex 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 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 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 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="muted">Not present on interface (disabled or interface down).</p>
|
||||
<p class="text-sm text-slate-400">Not present on interface (disabled or interface down).</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Client config</h3>
|
||||
<img src="/peers/{{ peer.id }}/qr" alt="QR code" class="qr">
|
||||
<p><a href="/peers/{{ peer.id }}/config" class="button">Download .conf</a>
|
||||
<button type="button" onclick="copyConfig()">Copy</button></p>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card config-card">
|
||||
<h3>{{ peer.name }}.conf</h3>
|
||||
<pre id="client-config">{{ client_config }}</pre>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
function copyConfig() {
|
||||
navigator.clipboard.writeText(document.getElementById("client-config").textContent);
|
||||
function copyConfig(btn) {
|
||||
navigator.clipboard.writeText(document.getElementById("client-config").textContent)
|
||||
.then(() => {
|
||||
btn.textContent = "Copied";
|
||||
setTimeout(() => { btn.textContent = "Copy"; }, 1500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="actions-row">
|
||||
<form method="post" action="/peers/{{ peer.id }}/toggle" class="inline">
|
||||
<button type="submit">{{ "Disable" if peer.enabled else "Enable" }}</button>
|
||||
</form>
|
||||
<form method="post" action="/peers/{{ peer.id }}/rotate" class="inline"
|
||||
onsubmit="return confirm('Rotate keys? The old client config stops working immediately.')">
|
||||
<button type="submit">Rotate keys</button>
|
||||
</form>
|
||||
<form method="post" action="/peers/{{ peer.id }}/reset-usage" class="inline"
|
||||
onsubmit="return confirm('Reset usage counters to zero?')">
|
||||
<button type="submit">Reset usage</button>
|
||||
</form>
|
||||
<form method="post" action="/peers/{{ peer.id }}/delete" class="inline"
|
||||
onsubmit="return confirm('Delete peer {{ peer.name }}?')">
|
||||
<button type="submit" class="danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+112
-42
@@ -2,48 +2,118 @@
|
||||
{% block title %}Peers - WireGuard Admin{% endblock %}
|
||||
{% block breadcrumbs %}<span>/</span> <span>Peers</span>{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Peers</h1>
|
||||
<h1 class="mb-6 text-2xl font-bold">Peers</h1>
|
||||
|
||||
{% if error %}<p class="error">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="/peers" class="add-peer">
|
||||
<input name="name" placeholder="Peer name" required>
|
||||
<input name="note" placeholder="Note (optional)">
|
||||
<input name="address" placeholder="IP (auto if empty)" class="mid">
|
||||
<input name="dns" placeholder="DNS (default {{ settings.wg_dns }})" class="mid">
|
||||
<label class="muted">Quota GiB <input name="quota_gib" type="number" step="0.1" min="0" class="narrow" placeholder="0"></label>
|
||||
<label class="muted">Count <input name="count" type="number" min="1" max="50" value="1" class="narrow"></label>
|
||||
<label class="muted">Expires <input name="expires_at" type="datetime-local"></label>
|
||||
<button type="submit">Add peer(s)</button>
|
||||
</form>
|
||||
<p class="muted">Count > 1 creates a batch named name-1, name-2, ... (custom IP ignored). Quota 0 = unlimited.</p>
|
||||
{% if error %}
|
||||
<p class="mb-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Address</th><th>Enabled</th><th>Status</th><th>Usage</th><th>Note</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for peer in peers %}
|
||||
{% set ps = status.peers.get(peer.public_key) %}
|
||||
<tr>
|
||||
<td><a href="/peers/{{ peer.id }}">{{ peer.name }}</a></td>
|
||||
<td>{{ peer.address }}</td>
|
||||
<td>{% if peer.enabled %}<span class="badge ok">enabled</span>{% elif peer.over_quota %}<span class="badge err">over quota</span>{% else %}<span class="badge err">disabled</span>{% endif %}</td>
|
||||
<td>{% if ps and ps.online %}<span class="badge ok">online</span>{% else %}<span class="badge muted">offline</span>{% endif %}</td>
|
||||
<td class="muted">{{ peer.cum_total | fmt_bytes }}{% if peer.quota_bytes %} / {{ peer.quota_bytes | fmt_bytes }}{% endif %}</td>
|
||||
<td class="muted">{{ peer.note }}</td>
|
||||
<td class="actions">
|
||||
<form method="post" action="/peers/{{ peer.id }}/toggle" class="inline">
|
||||
<button type="submit">{{ "Disable" if peer.enabled else "Enable" }}</button>
|
||||
</form>
|
||||
<form method="post" action="/peers/{{ peer.id }}/delete" class="inline"
|
||||
onsubmit="return confirm('Delete peer {{ peer.name }}?')">
|
||||
<button type="submit" class="danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="muted">No peers yet. Add one above.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<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</h2>
|
||||
<form method="post" action="/peers" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<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="{{ settings.wg_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>
|
||||
<div class="flex items-end">
|
||||
<button type="submit"
|
||||
class="w-full 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 > 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>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
||||
+37
-13
@@ -2,18 +2,42 @@
|
||||
{% block title %}Settings - WireGuard Admin{% endblock %}
|
||||
{% block breadcrumbs %}<span>/</span> <span>Settings</span>{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Settings</h1>
|
||||
<div class="card">
|
||||
<table class="kv">
|
||||
<tr><th>Interface</th><td>{{ settings.wg_interface }}</td></tr>
|
||||
<tr><th>Server public key</th><td><code>{{ server_public }}</code></td></tr>
|
||||
<tr><th>Endpoint host</th><td>{{ settings.wg_host }}</td></tr>
|
||||
<tr><th>Listen port</th><td>{{ settings.wg_port }}</td></tr>
|
||||
<tr><th>Subnet</th><td>{{ settings.wg_subnet }}</td></tr>
|
||||
<tr><th>Client DNS</th><td>{{ settings.wg_dns }}</td></tr>
|
||||
<tr><th>Client AllowedIPs</th><td>{{ settings.wg_allowed_ips }}</td></tr>
|
||||
<tr><th>Keepalive</th><td>{{ settings.wg_persistent_keepalive }}s</td></tr>
|
||||
</table>
|
||||
<p class="muted">Settings are read from environment variables / .env and require a restart to change.</p>
|
||||
<h1 class="mb-6 text-2xl font-bold">Settings</h1>
|
||||
<div class="rounded-xl bg-white p-5 shadow-sm">
|
||||
<dl class="divide-y divide-slate-100 text-sm">
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="text-slate-500">Interface</dt>
|
||||
<dd class="font-medium">{{ settings.wg_interface }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="shrink-0 text-slate-500">Server public key</dt>
|
||||
<dd class="break-all text-right font-mono text-xs">{{ server_public }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="text-slate-500">Endpoint host</dt>
|
||||
<dd class="font-medium">{{ settings.wg_host }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="text-slate-500">Listen port</dt>
|
||||
<dd class="font-medium">{{ settings.wg_port }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="text-slate-500">Subnet</dt>
|
||||
<dd class="font-mono text-xs">{{ settings.wg_subnet }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="text-slate-500">Client DNS</dt>
|
||||
<dd class="font-medium">{{ settings.wg_dns }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="text-slate-500">Client AllowedIPs</dt>
|
||||
<dd class="font-mono text-xs">{{ settings.wg_allowed_ips }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 py-3">
|
||||
<dt class="text-slate-500">Keepalive</dt>
|
||||
<dd class="font-medium">{{ settings.wg_persistent_keepalive }}s</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p class="mt-4 text-xs text-slate-400">Settings are read from environment variables / .env and require a restart to change.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user