feat: route commands for peer clients (Linux/Windows) + footer signature

- scripts page: new card with Linux/macOS and Windows route commands
  per peer that has advertised_routes, with OS tab switcher and copy buttons
- Made by Galyaviev moved from sidebar to bottom-center page footer on all pages
- page-footer style: Dancing Script 20px, clickable mailto:ruslan@ipcom.su

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 10:54:57 +03:00
parent 530a260849
commit 3bfb650b80
4 changed files with 107 additions and 12 deletions
+31 -2
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import base64 import base64
import io import io
import ipaddress
import os import os
import secrets import secrets
import sqlite3 import sqlite3
@@ -658,6 +659,15 @@ def peer_rename(peer_id: int):
return ("", 204) return ("", 204)
def _cidr_parts(cidr: str):
"""Return (network_address, netmask) for a CIDR string."""
try:
net = ipaddress.ip_network(cidr.strip(), strict=False)
return str(net.network_address), str(net.netmask)
except Exception:
return cidr.strip(), ""
@app.route("/scripts") @app.route("/scripts")
def scripts(): def scripts():
commands = { commands = {
@@ -666,14 +676,33 @@ def scripts():
"apply_wg": "wg syncconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)", "apply_wg": "wg syncconf wg0 <(wg-quick strip /etc/wireguard/wg0.conf)",
} }
paths = [ paths = [
"sudo", "/usr/local/sbin/wg-peerctl", "/usr/local/sbin/wg-peerctl",
"/etc/wireguard/wg0.conf", "/etc/wireguard/wg0.conf",
"/etc/wireguard/wg-meta.env", "/etc/wireguard/wg-meta.env",
"/var/log/wireguard-server-install.log", "/var/log/wireguard-server-install.log",
"/var/log/wireguard-client-install.log", "/var/log/wireguard-client-install.log",
"/var/log/wireguard-peerctl.log", "/var/log/wireguard-peerctl.log",
] ]
return render_template("scripts.html", commands=commands, paths=paths)
route_peers = []
with db_conn() as conn:
cur = conn.cursor()
cur.execute(
"SELECT name, client_address, advertised_routes FROM peers "
"WHERE advertised_routes IS NOT NULL AND advertised_routes != '' ORDER BY name"
)
for row in cur.fetchall():
wg_ip = (row["client_address"] or "").split("/")[0]
routes = []
for r in (row["advertised_routes"] or "").split(","):
r = r.strip()
if r:
net, mask = _cidr_parts(r)
routes.append({"cidr": r, "net": net, "mask": mask})
if wg_ip and routes:
route_peers.append({"name": row["name"], "wg_ip": wg_ip, "routes": routes})
return render_template("scripts.html", commands=commands, paths=paths, route_peers=route_peers)
if __name__ == "__main__": if __name__ == "__main__":
+9 -8
View File
@@ -103,24 +103,25 @@ body {
.logout-btn:hover { background: #fee2e2; color: #dc2626; } .logout-btn:hover { background: #fee2e2; color: #dc2626; }
.made-by { .page-footer {
font-family: 'Dancing Script', cursive;
font-size: 18px;
color: #b0b8cc;
text-align: center; text-align: center;
padding: 8px 0 4px; padding: 18px 32px 24px;
font-family: 'Dancing Script', cursive;
font-size: 20px;
color: #c4cad8;
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.made-by a { .page-footer a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
transition: color 0.15s;
} }
.made-by a:hover { color: var(--accent); } .page-footer a:hover { color: var(--accent); }
/* ─── Layout ────────────────────────────────────────────────── */ /* ─── Layout ────────────────────────────────────────────────── */
.layout { margin-left: var(--sidebar-w); flex: 1; display: flex; flex-direction: column; } .layout { margin-left: var(--sidebar-w); flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
main { padding: 28px 32px; flex: 1; max-width: 1400px; width: 100%; } main { padding: 28px 32px; flex: 1; max-width: 1400px; width: 100%; }
/* ─── Page header ───────────────────────────────────────────── */ /* ─── Page header ───────────────────────────────────────────── */
+3 -1
View File
@@ -32,7 +32,6 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
Выйти Выйти
</a> </a>
<div class="made-by"><a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a></div>
</div> </div>
</aside> </aside>
<div class="layout"> <div class="layout">
@@ -49,6 +48,9 @@
{% endwith %} {% endwith %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer class="page-footer">
<a href="mailto:ruslan@ipcom.su">Made by Galyaviev</a>
</footer>
</div> </div>
</body> </body>
</html> </html>
+64 -1
View File
@@ -12,13 +12,58 @@
<div class="script-label">{{ key }}</div> <div class="script-label">{{ key }}</div>
<div class="script-cmd-wrap"> <div class="script-cmd-wrap">
<pre class="script-cmd">{{ cmd }}</pre> <pre class="script-cmd">{{ cmd }}</pre>
<button class="btn btn-sm copy-btn" onclick="copyText(this, '{{ cmd | replace("'", "\\'") }}')">Копировать</button> <button class="btn btn-sm copy-btn" onclick="copyText(this, {{ cmd | tojson }})">Копировать</button>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% if route_peers %}
<div class="card">
<h3>Маршруты к клиентам за peer</h3>
<p style="font-size:13px;color:var(--text-muted);margin-bottom:18px;">
Команды для других VPN-клиентов, чтобы они могли достучаться до сетей за peer-ом.
</p>
<div class="os-tabs" style="display:flex;gap:6px;margin-bottom:18px;">
<button class="btn btn-sm os-tab active" data-os="linux" onclick="switchOS('linux', this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
Linux / macOS
</button>
<button class="btn btn-sm os-tab" data-os="windows" onclick="switchOS('windows', this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="8" height="8"/><rect x="13" y="3" width="8" height="8"/><rect x="3" y="13" width="8" height="8"/><rect x="13" y="13" width="8" height="8"/></svg>
Windows
</button>
</div>
<div class="script-list">
{% for peer in route_peers %}
<div class="script-item">
<div class="script-label">{{ peer.name }} &nbsp;<span style="font-weight:400;opacity:.7">— WG IP: {{ peer.wg_ip }}</span></div>
{% for route in peer.routes %}
<div class="script-cmd-wrap" style="margin-bottom:6px;">
<pre class="script-cmd os-cmd linux">sudo ip route add {{ route.cidr }} via {{ peer.wg_ip }}</pre>
<pre class="script-cmd os-cmd windows" style="display:none">route add {{ route.net }} mask {{ route.mask }} {{ peer.wg_ip }}</pre>
<button class="btn btn-sm copy-btn"
data-linux="sudo ip route add {{ route.cidr }} via {{ peer.wg_ip }}"
data-windows="route add {{ route.net }} mask {{ route.mask }} {{ peer.wg_ip }}"
onclick="copyOS(this)">Копировать</button>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div style="margin-top:16px;padding:12px 14px;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;font-size:12.5px;color:#92400e;line-height:1.6;">
<strong>Постоянные маршруты:</strong><br>
Linux: добавьте в <code>/etc/rc.local</code> или конфиг интерфейса.<br>
Windows: добавьте флаг <code>-p</code><code>route -p add ...</code> — маршрут сохранится после перезагрузки.
</div>
</div>
{% endif %}
<div class="card"> <div class="card">
<h3>Важные пути</h3> <h3>Важные пути</h3>
<ul class="path-list"> <ul class="path-list">
@@ -29,6 +74,24 @@
</div> </div>
<script> <script>
var currentOS = 'linux';
function switchOS(os, btn) {
currentOS = os;
document.querySelectorAll('.os-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.os-cmd.linux').forEach(el => el.style.display = os === 'linux' ? '' : 'none');
document.querySelectorAll('.os-cmd.windows').forEach(el => el.style.display = os === 'windows' ? '' : 'none');
}
function copyOS(btn) {
var text = btn.getAttribute('data-' + currentOS);
navigator.clipboard.writeText(text).then(() => {
btn.textContent = 'Скопировано!';
setTimeout(() => btn.textContent = 'Копировать', 2000);
});
}
function copyText(btn, text) { function copyText(btn, text) {
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
btn.textContent = 'Скопировано!'; btn.textContent = 'Скопировано!';