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"
)
var version = "1.0.67"
var version = "1.0.68"
func main() {
addr := os.Getenv("EDGEGUARD_API_ADDR")

View File

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

View File

@@ -25,7 +25,7 @@ import (
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
)
var version = "1.0.67"
var version = "1.0.68"
const (
// 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 NTPPage = lazy(() => import('./pages/NTP'))
const ClusterPage = lazy(() => import('./pages/Cluster'))
const FirewallLivePage = lazy(() => import('./pages/FirewallLive'))
const LogsPage = lazy(() => import('./pages/Logs'))
const BackupsPage = lazy(() => import('./pages/Backups'))
const LicensePage = lazy(() => import('./pages/License'))
@@ -106,6 +107,7 @@ export default function App() {
<Route path="/ip-addresses" element={<IPAddressesPage />} />
<Route path="/ssl" element={<SSLPage />} />
<Route path="/firewall" element={<FirewallPage />} />
<Route path="/firewall/live" element={<FirewallLivePage />} />
<Route path="/vpn/wireguard" element={<WireguardPage />} />
<Route path="/forward-proxy" element={<ForwardProxyPage />} />
<Route path="/dns" element={<DNSPage />} />

View File

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

View File

@@ -7,6 +7,7 @@ import {
ClusterOutlined,
CrownOutlined,
DashboardOutlined,
EyeOutlined,
FileSearchOutlined,
DatabaseOutlined,
FireOutlined,
@@ -27,6 +28,7 @@ interface NavItem {
path: string
labelKey: string
icon: ReactNode
child?: boolean // visuell eingerückt unter dem Parent-Item
}
interface NavSection {
@@ -62,6 +64,7 @@ const NAV: NavSection[] = [
labelKey: 'nav.section.security',
items: [
{ 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: '/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:
// - <nav> als root, dunkler Gradient + Teal/Blue-Accent
@@ -104,13 +107,23 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) {
</div>
<ul className="sidebar-menu">
{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 + '/')
const cls = 'sidebar-menu-item'
+ (isActive ? ' active' : '')
+ (item.child ? ' sidebar-menu-item--child' : '')
return (
<li
key={item.path}
className={`sidebar-menu-item${isActive ? ' active' : ''}`}
>
<li key={item.path} className={cls}>
<Link to={item.path} onClick={onClose}>
{item.icon}
<span>{t(item.labelKey)}</span>

View File

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

View File

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

View File

@@ -152,6 +152,24 @@ h1, h2, h3, h4, h5, h6 {
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 {
content: '';
position: absolute;