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:
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -246,8 +246,8 @@ func parseWGConf(path string) (*parsedConf, error) {
|
||||
}
|
||||
out.Peers = append(out.Peers, *currentPeer)
|
||||
currentPeer = nil
|
||||
nextName = ""
|
||||
}
|
||||
nextName = ""
|
||||
}
|
||||
|
||||
sc := bufio.NewScanner(f)
|
||||
|
||||
Reference in New Issue
Block a user