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:
@@ -56,6 +56,36 @@ async function listPeers(ifaceID: number): Promise<WGPeer[]> {
|
||||
return (r.data.data as { peers?: WGPeer[] }).peers ?? []
|
||||
}
|
||||
|
||||
interface LiveStatusRow {
|
||||
interface: string
|
||||
peer_public_key: string
|
||||
endpoint?: string
|
||||
last_handshake_unix: number
|
||||
transfer_rx: number
|
||||
transfer_tx: number
|
||||
}
|
||||
async function listLiveStatus(): Promise<LiveStatusRow[]> {
|
||||
const r = await apiClient.get('/wireguard/status')
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { status?: LiveStatusRow[] }).status ?? []
|
||||
}
|
||||
|
||||
function fmtBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`
|
||||
const u = ['KiB', 'MiB', 'GiB', 'TiB']
|
||||
let v = n / 1024, i = 0
|
||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++ }
|
||||
return `${v.toFixed(1)} ${u[i]}`
|
||||
}
|
||||
function relTime(unix: number, never: string): string {
|
||||
if (!unix) return never
|
||||
const sec = Math.max(0, Math.floor(Date.now() / 1000) - unix)
|
||||
if (sec < 60) return `vor ${sec}s`
|
||||
if (sec < 3600) return `vor ${Math.floor(sec / 60)}m`
|
||||
if (sec < 86400) return `vor ${Math.floor(sec / 3600)}h`
|
||||
return `vor ${Math.floor(sec / 86400)}d`
|
||||
}
|
||||
|
||||
interface FwZoneLite { name: string; builtin: boolean }
|
||||
async function listZones(): Promise<FwZoneLite[]> {
|
||||
const r = await apiClient.get('/firewall/zones')
|
||||
@@ -270,6 +300,14 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
||||
queryFn: () => listPeers(ifaceID),
|
||||
enabled: open,
|
||||
})
|
||||
const { data: liveStatus } = useQuery({
|
||||
queryKey: ['wg', 'status'],
|
||||
queryFn: listLiveStatus,
|
||||
enabled: open,
|
||||
refetchInterval: 10_000,
|
||||
})
|
||||
const liveByPubkey = (live: LiveStatusRow[] | undefined, pk: string) =>
|
||||
(live ?? []).find(s => s.peer_public_key === pk)
|
||||
|
||||
const [editing, setEditing] = useState<WGPeer | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
@@ -302,8 +340,32 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
||||
render: (k: string) => <Text code copyable={{ text: k }} style={{ fontSize: 11 }}>{k.slice(0, 12)}…</Text>,
|
||||
},
|
||||
{
|
||||
title: t('wg.peer.lastHandshake'), dataIndex: 'last_handshake', key: 'last_handshake',
|
||||
render: (s?: string | null) => s ? new Date(s).toLocaleString() : <Tag>{t('wg.peer.never')}</Tag>,
|
||||
title: t('wg.peer.lastHandshake'), key: 'last_handshake',
|
||||
render: (_, row) => {
|
||||
const live = liveByPubkey(liveStatus, row.public_key)
|
||||
const online = live && live.last_handshake_unix > 0
|
||||
&& Date.now() / 1000 - live.last_handshake_unix < 180
|
||||
return (
|
||||
<Space size={4}>
|
||||
<StatusDot
|
||||
active={!!online}
|
||||
activeLabel={t('wg.peer.online')}
|
||||
inactiveLabel={t('wg.peer.offline')}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{live ? relTime(live.last_handshake_unix, t('wg.peer.never')) : t('wg.peer.never')}
|
||||
</Text>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('wg.peer.traffic'), key: 'traffic',
|
||||
render: (_, row) => {
|
||||
const live = liveByPubkey(liveStatus, row.public_key)
|
||||
if (!live) return '—'
|
||||
return <Text style={{ fontSize: 11 }}>▼{fmtBytes(live.transfer_rx)} ▲{fmtBytes(live.transfer_tx)}</Text>
|
||||
},
|
||||
},
|
||||
{ title: t('common.active'), dataIndex: 'enabled', key: 'enabled', render: (v: boolean) => <StatusDot active={v} /> },
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user