diff --git a/gui/app.py b/gui/app.py index 5e0300a..538f0c1 100644 --- a/gui/app.py +++ b/gui/app.py @@ -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() diff --git a/gui/static/style.css b/gui/static/style.css index f3eefab..e37824f 100644 --- a/gui/static/style.css +++ b/gui/static/style.css @@ -200,6 +200,11 @@ tbody tr.row-disabled td { opacity: 0.45; } .badge.offline { background: var(--bg3); color: var(--text-muted); } .badge.offline .dot { background: #9ca3af; } +.badge-ago { font-weight: 400; opacity: 0.75; font-size: 10.5px; margin-left: 2px; } + +.traffic { display: flex; flex-direction: column; gap: 1px; font-size: 12px; font-family: var(--mono); } +.traffic span { white-space: nowrap; } + /* ─── Buttons ────────────────────────────────────────────────── */ .btn { display: inline-flex; @@ -269,6 +274,23 @@ tbody tr.row-disabled td { opacity: 0.45; } .form-actions { display: flex; gap: 8px; margin-top: 24px; } +.optional { font-size: 11px; font-weight: 400; color: var(--text-muted); margin-left: 4px; } + +.name-hints { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 4px; } +.hint-label { font-size: 12px; color: var(--text-muted); } +.hint-btn { + padding: 3px 10px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--bg3); + color: var(--text); + font-size: 12px; + cursor: pointer; + font-family: var(--font); + transition: background 0.12s, border-color 0.12s; +} +.hint-btn:hover { background: #e8eaf6; border-color: var(--accent); color: var(--accent); } + /* ─── Peer detail ────────────────────────────────────────────── */ .peer-detail-grid { display: grid; grid-template-columns: auto 1fr; gap: 16px; align-items: start; } @media (max-width: 768px) { .peer-detail-grid { grid-template-columns: 1fr; } } diff --git a/gui/templates/index.html b/gui/templates/index.html index eecbba1..2ddbb71 100644 --- a/gui/templates/index.html +++ b/gui/templates/index.html @@ -22,7 +22,6 @@ IP Роуты Endpoint - Handshake RX / TX Публичный ключ Действия @@ -33,15 +32,19 @@ {{ p.name }} - - {{ p.status }} + + + {{ p.status }} + {{ p.handshake_ago }} {{ p.client_address }} {{ p.routes }} {{ p.endpoint }} - {{ p.latest_handshake }} - {{ p.rx }} / {{ p.tx }} + + ↓ {{ p.rx }} + ↑ {{ p.tx }} + {{ p.public_key[:20] }}…
@@ -77,7 +80,7 @@ {% else %} - Пиров нет. Добавить первый + Пиров нет. Добавить первый {% endfor %} diff --git a/gui/templates/new_peer.html b/gui/templates/new_peer.html index bb0eefb..a8b57ae 100644 --- a/gui/templates/new_peer.html +++ b/gui/templates/new_peer.html @@ -9,34 +9,44 @@
- - + + +
+ Быстрый выбор: + + + + + + +
+ Рекомендуем формат тип-место: laptop-home, router-office, phone-moscow
- +
- + - Сети, которые клиент анонсирует другим участникам. Необязательно. + Локальные сети за клиентом, доступные другим участникам VPN.
@@ -51,5 +61,14 @@ function toggleRoutes(el) { document.getElementById('allowed-ips-group').style.display = el.value === 'split' ? 'block' : 'none'; } +function setPrefix(type) { + const inp = document.getElementById('name-input'); + const cur = inp.value; + const m = cur.match(/^[a-z]+-?(.*)$/i); + const suffix = (m && m[1]) ? m[1] : ''; + inp.value = suffix ? type + '-' + suffix : type + '-'; + inp.focus(); + inp.setSelectionRange(inp.value.length, inp.value.length); +} {% endblock %}