feat(ui): Pages auf neues Design + Dashboard + WG-Live-Status + Routing-Rules-Verstecken

Pages auf PageHeader/StatusDot/ActionButtons-Pattern migriert:
* Dashboard — Komplett-Rewrite. KPI-Tiles (Domains, Backends, Iface,
  FW-Rules, NAT, WG), Detail-Cards (WireGuard live status, Firewall
  zone overview, SSL expiring soon, Cluster nodes, Routing summary,
  System info). Polled queries pro Card.
* Domains, Backends, RoutingRules, Networks, IPAddresses, SSL,
  Cluster, Settings, Firewall (index) — alle inline Action-Buttons
  → ActionButtons; alle Yes/No-Renders → StatusDot; Add-Button in
  DataTable.extraActions; PageHeader oben.

WireGuard
---------
* Neuer /wireguard/status-Endpoint parsed `wg show all dump`,
  liefert {iface, peer_pubkey, endpoint, last_handshake_unix, rx, tx}.
  Sudoers im postinst um `wg show` erweitert.
* Server-Drawer Peer-Liste zeigt jetzt Live-Status (Online/Offline-
  Dot, "vor Xs", Traffic-Counter) per 10s-Polling. Importierte
  "Unify Home" peer kann jetzt im UI verifiziert werden.
* Importer-Bug fixed: nextName ("# Unify Home" comment) wurde beim
  Sektionswechsel zu früh geresettet — jetzt nur nach echtem
  flushPeer.

Routing-Rules
-------------
* Aus Sidebar entfernt. URL bleibt funktional, aber für 90% der
  Setups reicht domains.primary_backend_id (das HAProxy ohnehin
  als default_backend rendert). Path-basiertes Routing ist ein
  Advanced-Feature und kommt später als Domain-Modal-Tab zurück.
* nav.routing-Sidebar-Eintrag + BranchesOutlined-Import entfernt.

Misc
----
* "Firewall (v2)" → "Firewall" im Nav (DE).
* Dashboard-i18n Block in DE+EN.
* Version 1.0.11 → 1.0.12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-10 21:07:38 +02:00
parent 85904d0c36
commit fd294a273e
21 changed files with 439 additions and 162 deletions

View File

@@ -7,7 +7,9 @@ import (
"fmt"
"log/slog"
"net/http"
"os/exec"
"strconv"
"strings"
"github.com/gin-gonic/gin"
qrcode "github.com/skip2/go-qrcode"
@@ -76,6 +78,63 @@ func (h *WireguardHandler) Register(rg *gin.RouterGroup) {
// embeds the same text so mobile apps can import directly.
g.GET("/peers/:pid/config", h.PeerConfig)
g.GET("/peers/:pid/qr", h.PeerQR)
// Live runtime status from `wg show <iface> dump`. Returns one
// row per (iface, peer) with last_handshake + transfer counters.
// Polled by the UI every 10s; no DB write.
g.GET("/status", h.Status)
}
// ── Live wg-show status ─────────────────────────────────────────────
// wgStatus is the wire shape returned to the UI. We don't update
// the DB rows from this — kernel state is the source of truth at
// the moment of the call, the DB is metadata.
type wgStatus struct {
Interface string `json:"interface"`
PeerPublicKey string `json:"peer_public_key"`
Endpoint string `json:"endpoint,omitempty"`
AllowedIPs string `json:"allowed_ips,omitempty"`
LastHandshake int64 `json:"last_handshake_unix"` // 0 = never
TransferRX int64 `json:"transfer_rx"`
TransferTX int64 `json:"transfer_tx"`
}
func (h *WireguardHandler) Status(c *gin.Context) {
// `wg show all dump` per iface — output:
// line 1: iface_private_key, iface_pubkey, listen_port, fwmark
// line 2..N: pubkey, psk, endpoint, allowed_ips, latest_handshake, rx, tx, persistent_keepalive
out, err := exec.CommandContext(c.Request.Context(), "sudo", "-n", "/usr/bin/wg", "show", "all", "dump").Output()
if err != nil {
// wg not installed or no ifaces up — return empty list, not error.
response.OK(c, gin.H{"status": []wgStatus{}})
return
}
rows := []wgStatus{}
for _, line := range strings.Split(string(out), "\n") {
if line == "" {
continue
}
fields := strings.Split(line, "\t")
// Lines starting an iface have 5 columns; peer lines have 9.
if len(fields) != 9 {
continue
}
ifaceName := fields[0]
hs, _ := strconv.ParseInt(fields[5], 10, 64)
rx, _ := strconv.ParseInt(fields[6], 10, 64)
tx, _ := strconv.ParseInt(fields[7], 10, 64)
rows = append(rows, wgStatus{
Interface: ifaceName,
PeerPublicKey: fields[2],
Endpoint: fields[3],
AllowedIPs: fields[4],
LastHandshake: hs,
TransferRX: rx,
TransferTX: tx,
})
}
response.OK(c, gin.H{"status": rows})
}
// ── Keygen ────────────────────────────────────────────────────────

View File

@@ -246,8 +246,8 @@ func parseWGConf(path string) (*parsedConf, error) {
}
out.Peers = append(out.Peers, *currentPeer)
currentPeer = nil
nextName = ""
}
nextName = ""
}
sc := bufio.NewScanner(f)