Backend: * Migration 0009_networks: network_interfaces (ethernet|vlan|bond| bridge|wireguard, role wan|lan|dmz|mgmt|cluster, parent + vlan_id für VLANs) + ip_addresses (interface_id FK, address+prefix, is_vip + vip_priority für Cluster-Failover-VIPs). * Repos services/networkifs + services/ipaddresses + Models + Handler /api/v1/network-interfaces (CRUD + /:id/ip-addresses) und /api/v1/ip-addresses (CRUD). * /api/v1/system/interfaces refactored auf Go-natives net.Interfaces() statt `ip -j addr show` shell-out — die systemd-Sandbox blockt AF_NETLINK auch für Go's runtime, deswegen edgeguard-api.service RestrictAddressFamilies um AF_NETLINK ergänzt. Output-Shape bleibt identisch (ifindex, ifname, flags[], mtu, link_type, address, addr_info[]) — Frontend muss nicht angepasst werden. Frontend: * Networks-Page (/networks): "System-discovered Interfaces" read-only Tags-Card oben, deklarierte Interfaces unten als Tabelle mit Modal-CRUD; Type-Switch zeigt parent+vlan_id-Felder bei type=vlan; Role-Tags farbig (wan blau, lan grün, dmz orange, mgmt purple, cluster magenta). * IPAddresses-Page (/ip-addresses): Tabelle pro Interface, VIP- Toggle blendet vip_priority-Eingabe ein. Goldenes VIP-Tag in der Liste. * Sidebar erweitert um Networks + IP-Adressen + section-grouping. Design 1:1 von mail-gateway/management-ui/ übernommen: * enterprise.css verbatim (Inter-Font via Google CDN statt local woff2), Sidebar 240px dunkler Gradient #0B1426→#101D33→#0D1829, branding-accent #1677ff für Active-State, abgerundete Cards mit shadow-Token, Header weiß mit subtilem backdrop-filter. * AntD-Theme-Tokens: colorPrimary #0EA5E9, fontSize 13, fontFamily 'Inter', controlHeight 34, borderRadius 6. * Layout-Komponenten neu strukturiert: AppLayout/Sidebar/Header matchen mailguard-Klassen-Naming (.app-layout, .main-content, .sidebar-section, .sidebar-menu-item.active, .header-left, …). * Sidebar mit 4 Sektionen (Übersicht / Routing / Netzwerk / System) + Logo-Header + Versions-Footer. Live-deployed auf 89.163.205.6: Networks-Endpoint listet eth0 (89.163.205.6/24, MAC bc:24:11:64:29:e8) + lo, frontend zeigt sie als System-Tags in der Networks-Page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
111 lines
4.3 KiB
TypeScript
111 lines
4.3 KiB
TypeScript
import { Suspense, lazy, useEffect, type ReactNode } from 'react'
|
|
import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
|
import { ConfigProvider, Spin } from 'antd'
|
|
import deDE from 'antd/locale/de_DE'
|
|
import enUS from 'antd/locale/en_US'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import AppLayout from './components/Layout/AppLayout'
|
|
import apiClient, { isEnvelope } from './api/client'
|
|
import { useAuthStore, type SessionUser } from './stores/auth'
|
|
|
|
const LoginPage = lazy(() => import('./pages/Login'))
|
|
const SetupPage = lazy(() => import('./pages/Setup'))
|
|
const DashboardPage = lazy(() => import('./pages/Dashboard'))
|
|
const DomainsPage = lazy(() => import('./pages/Domains'))
|
|
const BackendsPage = lazy(() => import('./pages/Backends'))
|
|
const RoutingRulesPage = lazy(() => import('./pages/RoutingRules'))
|
|
const NetworksPage = lazy(() => import('./pages/Networks'))
|
|
const IPAddressesPage = lazy(() => import('./pages/IPAddresses'))
|
|
const ClusterPage = lazy(() => import('./pages/Cluster'))
|
|
const SettingsPage = lazy(() => import('./pages/Settings'))
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: 1, refetchOnWindowFocus: false },
|
|
},
|
|
})
|
|
|
|
// Theme tokens 1:1 wie mail-gateway/enconf — colorPrimary, font,
|
|
// borderRadius, controlHeight. enterprise.css ergänzt mit eigenen
|
|
// Layout-Klassen (.app-layout, .sidebar, .header, …).
|
|
const antdTheme = {
|
|
token: {
|
|
colorPrimary: '#0EA5E9',
|
|
borderRadius: 6,
|
|
borderRadiusLG: 8,
|
|
fontSize: 13,
|
|
fontWeightStrong: 600,
|
|
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
colorBgContainer: '#FFFFFF',
|
|
colorBgLayout: '#F8FAFC',
|
|
colorBorder: '#E2E8F0',
|
|
colorText: '#0F172A',
|
|
colorTextSecondary: '#64748B',
|
|
controlHeight: 34,
|
|
},
|
|
}
|
|
|
|
function RequireAuth({ children }: { children: ReactNode }) {
|
|
const user = useAuthStore((s) => s.user)
|
|
const location = useLocation()
|
|
if (!user) {
|
|
return <Navigate to="/login" replace state={{ from: location }} />
|
|
}
|
|
return <>{children}</>
|
|
}
|
|
|
|
function SetupGate({ children }: { children: ReactNode }) {
|
|
const location = useLocation()
|
|
useEffect(() => {
|
|
if (location.pathname.startsWith('/setup')) return
|
|
apiClient.get('/setup/status').then((r) => {
|
|
const env = r.data
|
|
if (isEnvelope(env)) {
|
|
const data = env.data as { completed?: boolean } | undefined
|
|
if (data && data.completed === false) {
|
|
window.location.replace('/setup')
|
|
}
|
|
}
|
|
}).catch(() => { /* setup-gate is best-effort */ })
|
|
}, [location.pathname])
|
|
return <>{children}</>
|
|
}
|
|
|
|
export default function App() {
|
|
const { i18n } = useTranslation()
|
|
const antdLocale = i18n.language?.startsWith('de') ? deDE : enUS
|
|
|
|
return (
|
|
<ConfigProvider theme={antdTheme} locale={antdLocale}>
|
|
<QueryClientProvider client={queryClient}>
|
|
<BrowserRouter>
|
|
<SetupGate>
|
|
<Suspense fallback={<div className="loader-center"><Spin size="large" /></div>}>
|
|
<Routes>
|
|
<Route path="/setup" element={<SetupPage onComplete={(u: SessionUser) => useAuthStore.getState().set(u)} />} />
|
|
<Route path="/login" element={<LoginPage onLogin={(u: SessionUser) => useAuthStore.getState().set(u)} />} />
|
|
|
|
<Route element={<RequireAuth><AppLayout /></RequireAuth>}>
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
<Route path="/dashboard" element={<DashboardPage />} />
|
|
<Route path="/domains" element={<DomainsPage />} />
|
|
<Route path="/backends" element={<BackendsPage />} />
|
|
<Route path="/routing-rules" element={<RoutingRulesPage />} />
|
|
<Route path="/networks" element={<NetworksPage />} />
|
|
<Route path="/ip-addresses" element={<IPAddressesPage />} />
|
|
<Route path="/cluster" element={<ClusterPage />} />
|
|
<Route path="/settings" element={<SettingsPage />} />
|
|
</Route>
|
|
|
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</SetupGate>
|
|
</BrowserRouter>
|
|
</QueryClientProvider>
|
|
</ConfigProvider>
|
|
)
|
|
}
|