fix(gui): correct wg dump indexes, status, traffic, UX improvements

- Fix off-by-one bug in wg_dump(): handshake was read from parts[5] (rx_bytes),
  now correctly reads from parts[4]; rx/tx shifted accordingly
- Run wg show via sudo to work under unprivileged wgadmin user
- Remove NoNewPrivileges from systemd service (needed for sudo)
- Merge Handshake column into Status badge (shows "online · 2м назад")
- Add humanize_ago() for human-readable handshake time
- Add next_free_ip() to suggest next available IP in new peer form
- Add device type quick-select buttons (Phone/Laptop/PC/Router/Server/Tablet)
- Placeholder in AllowedIPs now shows the real next free IP
- Traffic column shows ↓ rx / ↑ tx separately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 10:18:38 +03:00
parent 904582e7fa
commit fe1cba2d02
4 changed files with 100 additions and 26 deletions
+41 -11
View File
@@ -149,7 +149,7 @@ def parse_wg_conf_peer_meta():
def wg_dump():
try:
out = run(["wg", "show", WG_INTERFACE, "dump"])
out = run(["sudo", "/usr/bin/wg", "show", WG_INTERFACE, "dump"])
except Exception:
return []
rows = out.splitlines()
@@ -158,20 +158,18 @@ def wg_dump():
parts = line.split("\t")
if len(parts) < 8:
continue
# dump format: pubkey psk endpoint allowed_ips handshake_ts rx tx keepalive
latest_ts = 0
latest = "never"
if parts[5].isdigit() and int(parts[5]) > 0:
latest_ts = int(parts[5])
latest = datetime.utcfromtimestamp(latest_ts).strftime("%Y-%m-%d %H:%M:%S UTC")
if parts[4].isdigit() and int(parts[4]) > 0:
latest_ts = int(parts[4])
peers.append(
{
"public_key": parts[0],
"endpoint": parts[2] or "-",
"allowed_ips": parts[3],
"latest_handshake": latest,
"latest_handshake_ts": latest_ts,
"rx_bytes": int(parts[6] or 0),
"tx_bytes": int(parts[7] or 0),
"rx_bytes": int(parts[5] or 0),
"tx_bytes": int(parts[6] or 0),
}
)
return peers
@@ -186,6 +184,37 @@ def bytes_h(n):
x /= 1024
def humanize_ago(ts: int, now: int) -> str:
if not ts:
return "никогда"
diff = now - ts
if diff < 60:
return f"{diff}с назад"
if diff < 3600:
return f"{diff // 60}м назад"
if diff < 86400:
return f"{diff // 3600}ч назад"
return f"{diff // 86400}д назад"
def next_free_ip(network: str) -> str:
import ipaddress
try:
net = ipaddress.ip_network(network, strict=False)
except ValueError:
return ""
with db_conn() as conn:
used = {
row[0].split("/")[0]
for row in conn.execute("SELECT client_address FROM peers WHERE client_address IS NOT NULL")
}
# skip network address and first host (server usually takes .1)
for host in list(net.hosts())[1:]:
if str(host) not in used:
return f"{host}/{net.prefixlen}"
return ""
def gen_keypair_psk():
priv = run(["wg", "genkey"])
pub = subprocess.check_output(["wg", "pubkey"], input=priv, text=True).strip()
@@ -268,7 +297,7 @@ def index():
"routes": row.get("advertised_routes") or "-",
"allowed_ips": rt.get("allowed_ips", "-"),
"endpoint": rt.get("endpoint", "-"),
"latest_handshake": rt.get("latest_handshake", "offline"),
"handshake_ago": humanize_ago(ts, now),
"rx": bytes_h(rt.get("rx_bytes", 0)),
"tx": bytes_h(rt.get("tx_bytes", 0)),
"status": "online" if is_online else "offline",
@@ -308,7 +337,7 @@ def index():
"routes": imported_routes,
"allowed_ips": rt.get("allowed_ips", "-"),
"endpoint": rt.get("endpoint", "-"),
"latest_handshake": rt.get("latest_handshake", "offline"),
"handshake_ago": humanize_ago(ts, now),
"rx": bytes_h(rt.get("rx_bytes", 0)),
"tx": bytes_h(rt.get("tx_bytes", 0)),
"status": "online" if is_online else "offline",
@@ -323,7 +352,8 @@ def index():
def new_peer():
meta = load_meta()
if request.method == "GET":
return render_template("new_peer.html", meta=meta)
next_ip = next_free_ip(meta.get("WG_NETWORK", "10.66.66.0/24"))
return render_template("new_peer.html", meta=meta, next_ip=next_ip)
name = request.form.get("name", "").strip()
mode = request.form.get("mode", "full").strip()