refactor(fwlog): Live-Log als Child-Route /firewall/live statt Firewall-Tab

User-Feedback: Tab fühlt sich falsch an, will eine eigene Page mit
URL-Pfad unter /firewall.

UI:
- pages/Firewall/LiveLog.tsx → pages/FirewallLive/index.tsx
- FirewallPage entfernt den live-Tab aus tabs[]
- App.tsx routet /firewall/live → FirewallLivePage
- Sidebar: neuer Eintrag „Firewall-Log" eingerückt direkt unter
  „Firewall" in der Security-Section (child: true Flag → CSS-Klasse
  sidebar-menu-item--child mit padding-left 28px + dünnem vertikalem
  Trenn-Stab links). Sibling-Active-Logik exklusiv: /firewall matched
  NICHT mehr wenn /firewall/live aktiv ist.
- AppLayout PAGE_TITLES bekommt /firewall/live VOR /firewall damit
  der Title-Lookup den spezifischeren Pfad zuerst trifft.

Keine Backend-Änderungen.

Bekanntes Verhalten zu erklären: Im Live-Log sehen User aktuell nur
Smoke-Test-Events (oob.prefix=edgeguard:smoke / edgeguard:42, src/dst
127.0.0.1) — das sind die manuell-injizierten nft-Rules vom End-to-
End-Test der Pipeline. Reale Pakete fließen erst durch, wenn der
Operator auf einer firewall_rule den Log-Switch aktiviert (Firewall
→ Regeln → bearbeiten → Logging an). Aktuell hat keine einzige Rule
log=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-13 07:04:19 +02:00
parent b031725dfe
commit 24c40bc776
10 changed files with 55 additions and 14 deletions

View File

@@ -1 +1 @@
1.0.67 1.0.68

View File

@@ -52,7 +52,7 @@ import (
wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard" wgsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/wireguard"
) )
var version = "1.0.67" var version = "1.0.68"
func main() { func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR") addr := os.Getenv("EDGEGUARD_API_ADDR")

View File

@@ -9,7 +9,7 @@ import (
"os" "os"
) )
var version = "1.0.67" var version = "1.0.68"
const usage = `edgeguard-ctl — EdgeGuard CLI const usage = `edgeguard-ctl — EdgeGuard CLI

View File

@@ -25,7 +25,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
) )
var version = "1.0.67" var version = "1.0.68"
const ( const (
// renewTickInterval — how often we re-evaluate expiring certs. // renewTickInterval — how often we re-evaluate expiring certs.

View File

@@ -25,6 +25,7 @@ const ForwardProxyPage = lazy(() => import('./pages/ForwardProxy'))
const DNSPage = lazy(() => import('./pages/DNS')) const DNSPage = lazy(() => import('./pages/DNS'))
const NTPPage = lazy(() => import('./pages/NTP')) const NTPPage = lazy(() => import('./pages/NTP'))
const ClusterPage = lazy(() => import('./pages/Cluster')) const ClusterPage = lazy(() => import('./pages/Cluster'))
const FirewallLivePage = lazy(() => import('./pages/FirewallLive'))
const LogsPage = lazy(() => import('./pages/Logs')) const LogsPage = lazy(() => import('./pages/Logs'))
const BackupsPage = lazy(() => import('./pages/Backups')) const BackupsPage = lazy(() => import('./pages/Backups'))
const LicensePage = lazy(() => import('./pages/License')) const LicensePage = lazy(() => import('./pages/License'))
@@ -106,6 +107,7 @@ export default function App() {
<Route path="/ip-addresses" element={<IPAddressesPage />} /> <Route path="/ip-addresses" element={<IPAddressesPage />} />
<Route path="/ssl" element={<SSLPage />} /> <Route path="/ssl" element={<SSLPage />} />
<Route path="/firewall" element={<FirewallPage />} /> <Route path="/firewall" element={<FirewallPage />} />
<Route path="/firewall/live" element={<FirewallLivePage />} />
<Route path="/vpn/wireguard" element={<WireguardPage />} /> <Route path="/vpn/wireguard" element={<WireguardPage />} />
<Route path="/forward-proxy" element={<ForwardProxyPage />} /> <Route path="/forward-proxy" element={<ForwardProxyPage />} />
<Route path="/dns" element={<DNSPage />} /> <Route path="/dns" element={<DNSPage />} />

View File

@@ -16,6 +16,8 @@ const PAGE_TITLES: Record<string, string> = {
'/routing-rules': 'nav.routing', '/routing-rules': 'nav.routing',
'/networks': 'nav.networks', '/networks': 'nav.networks',
'/ip-addresses': 'nav.ipAddresses', '/ip-addresses': 'nav.ipAddresses',
'/firewall/live': 'nav.firewallLive',
'/firewall': 'nav.firewall',
'/cluster': 'nav.cluster', '/cluster': 'nav.cluster',
'/logs': 'nav.logs', '/logs': 'nav.logs',
'/backups': 'nav.backups', '/backups': 'nav.backups',

View File

@@ -7,6 +7,7 @@ import {
ClusterOutlined, ClusterOutlined,
CrownOutlined, CrownOutlined,
DashboardOutlined, DashboardOutlined,
EyeOutlined,
FileSearchOutlined, FileSearchOutlined,
DatabaseOutlined, DatabaseOutlined,
FireOutlined, FireOutlined,
@@ -27,6 +28,7 @@ interface NavItem {
path: string path: string
labelKey: string labelKey: string
icon: ReactNode icon: ReactNode
child?: boolean // visuell eingerückt unter dem Parent-Item
} }
interface NavSection { interface NavSection {
@@ -62,6 +64,7 @@ const NAV: NavSection[] = [
labelKey: 'nav.section.security', labelKey: 'nav.section.security',
items: [ items: [
{ path: '/firewall', labelKey: 'nav.firewall', icon: <FireOutlined /> }, { path: '/firewall', labelKey: 'nav.firewall', icon: <FireOutlined /> },
{ path: '/firewall/live', labelKey: 'nav.firewallLive', icon: <EyeOutlined />, child: true },
{ path: '/vpn/wireguard', labelKey: 'nav.wireguard', icon: <ThunderboltOutlined /> }, { path: '/vpn/wireguard', labelKey: 'nav.wireguard', icon: <ThunderboltOutlined /> },
{ path: '/forward-proxy', labelKey: 'nav.forwardProxy', icon: <CloudServerOutlined /> }, { path: '/forward-proxy', labelKey: 'nav.forwardProxy', icon: <CloudServerOutlined /> },
], ],
@@ -78,7 +81,7 @@ const NAV: NavSection[] = [
}, },
] ]
const VERSION = '1.0.67' const VERSION = '1.0.68'
// Sidebar-Pattern 1:1 aus netcell-webpanel (enconf) übernommen: // Sidebar-Pattern 1:1 aus netcell-webpanel (enconf) übernommen:
// - <nav> als root, dunkler Gradient + Teal/Blue-Accent // - <nav> als root, dunkler Gradient + Teal/Blue-Accent
@@ -104,13 +107,23 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) {
</div> </div>
<ul className="sidebar-menu"> <ul className="sidebar-menu">
{section.items.map((item) => { {section.items.map((item) => {
const isActive = location.pathname === item.path // exact-match → der genauere Pfad gewinnt; sonst würde
// /firewall den /firewall/live-Eintrag als „active"
// mitmarkieren. Sibling-Pfade müssen sich gegenseitig
// ausschließen.
const hasMoreSpecificSibling = section.items.some(
(other) => other.path !== item.path &&
other.path.startsWith(item.path + '/'),
)
const isActive = hasMoreSpecificSibling
? location.pathname === item.path
: location.pathname === item.path
|| location.pathname.startsWith(item.path + '/') || location.pathname.startsWith(item.path + '/')
const cls = 'sidebar-menu-item'
+ (isActive ? ' active' : '')
+ (item.child ? ' sidebar-menu-item--child' : '')
return ( return (
<li <li key={item.path} className={cls}>
key={item.path}
className={`sidebar-menu-item${isActive ? ' active' : ''}`}
>
<Link to={item.path} onClick={onClose}> <Link to={item.path} onClick={onClose}>
{item.icon} {item.icon}
<span>{t(item.labelKey)}</span> <span>{t(item.labelKey)}</span>

View File

@@ -10,7 +10,6 @@ import ServiceGroupsTab from './ServiceGroups'
import RulesTab from './Rules' import RulesTab from './Rules'
import NATRulesTab from './NATRules' import NATRulesTab from './NATRules'
import ZonesTab from './Zones' import ZonesTab from './Zones'
import LiveLogTab from './LiveLog'
export default function FirewallPage() { export default function FirewallPage() {
const { t } = useTranslation() const { t } = useTranslation()
@@ -18,7 +17,6 @@ export default function FirewallPage() {
const tabs = [ const tabs = [
{ key: 'rules', label: t('fw.tabs.rules'), children: <RulesTab /> }, { key: 'rules', label: t('fw.tabs.rules'), children: <RulesTab /> },
{ key: 'nat', label: t('fw.tabs.nat'), children: <NATRulesTab /> }, { key: 'nat', label: t('fw.tabs.nat'), children: <NATRulesTab /> },
{ key: 'live', label: t('fw.tabs.live'), children: <LiveLogTab /> },
{ key: 'zones', label: t('fw.tabs.zones'), children: <ZonesTab /> }, { key: 'zones', label: t('fw.tabs.zones'), children: <ZonesTab /> },
{ key: 'addrObj', label: t('fw.tabs.addrObj'), children: <AddressObjectsTab /> }, { key: 'addrObj', label: t('fw.tabs.addrObj'), children: <AddressObjectsTab /> },
{ key: 'addrGrp', label: t('fw.tabs.addrGrp'), children: <AddressGroupsTab /> }, { key: 'addrGrp', label: t('fw.tabs.addrGrp'), children: <AddressGroupsTab /> },

View File

@@ -7,12 +7,15 @@ import {
AlertOutlined, AlertOutlined,
ClearOutlined, ClearOutlined,
DownloadOutlined, DownloadOutlined,
EyeOutlined,
PauseCircleOutlined, PauseCircleOutlined,
PlayCircleOutlined, PlayCircleOutlined,
PoweroffOutlined, PoweroffOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import PageHeader from '../../components/PageHeader'
const { Text } = Typography const { Text } = Typography
interface Entry { interface Entry {
@@ -105,7 +108,7 @@ function toCSV(rows: Entry[]): string {
// DISCONNECTED — der WebSocket wird erst beim Klick auf „Start" // DISCONNECTED — der WebSocket wird erst beim Klick auf „Start"
// aufgebaut. Stop schließt explizit, Filter-Änderungen reconnecten // aufgebaut. Stop schließt explizit, Filter-Änderungen reconnecten
// nur wenn aktiv. // nur wenn aktiv.
export default function LiveLogTab() { export default function FirewallLivePage() {
const { t } = useTranslation() const { t } = useTranslation()
const [active, setActive] = useState(false) // Start/Stop master switch const [active, setActive] = useState(false) // Start/Stop master switch
@@ -287,6 +290,11 @@ export default function LiveLogTab() {
return ( return (
<div> <div>
<PageHeader
icon={<EyeOutlined />}
title={t('fwlog.title')}
subtitle={t('fwlog.intro')}
/>
{!active ? ( {!active ? (
<Card style={{ textAlign: 'center', padding: '32px 16px' }}> <Card style={{ textAlign: 'center', padding: '32px 16px' }}>
<Empty <Empty

View File

@@ -152,6 +152,24 @@ h1, h2, h3, h4, h5, h6 {
color: #CBD5E1; color: #CBD5E1;
} }
/* Child-Item: sub-nav unter einem Parent (z.B. /firewall/live unter
/firewall). Eingerückt + dünner-vertical-Stab links damit die
Hierarchie sofort sichtbar ist. */
.sidebar-menu-item--child a,
.sidebar-menu-item--child button {
padding-left: 28px;
font-size: 13px;
}
.sidebar-menu-item--child::after {
content: '';
position: absolute;
left: 14px;
top: 14px;
bottom: 14px;
width: 1px;
background: rgba(255,255,255,0.08);
}
.sidebar-menu-item.active::before { .sidebar-menu-item.active::before {
content: ''; content: '';
position: absolute; position: absolute;