fix(fwlog): Live-Log war seit Tagen still tot + Seitengröße wirkungslos

Zwei unabhängige Bugs hinter "Live-Log zeigt Einträge, aber nichts Neues":

1) ulogd2 stirbt beim nächtlichen Logrotate. Der postinst nahm an, ulogd
   laufe als root — die Debian-Unit startet aber `ulogd --daemon --uid ulog`.
   Beim Start öffnet ulogd die JSONL noch als root und schreibt danach über
   den offenen fd weiter, egal wem sie gehört. Nachts schickt das Distro-
   Profil /etc/logrotate.d/ulogd2 ein SIGHUP; das Reopen läuft dann als
   `ulog` und scheitert an root:edgeguard 0640 ("can't open JSON log file:
   Permission denied"). ulogd wertet das als fatal und beendet sich mit
   Exit-Code 0 — Restart=on-failure hätte also nicht gegriffen, und ohne
   Restart= blieb der Dienst tot (auf utm-1 5 Tage unbemerkt). Die API
   servierte derweil weiter ihren In-Memory-Ring von vor der Rotation,
   deshalb sah die UI Einträge, aber nie neue.
   Fix: Owner ulog (Schreiber) : edgeguard (Leser), logrotate `create`
   passend, plus Drop-in Restart=always als Selbstheilung.

2) Seitengröße liess sich nicht umstellen. Die Tabellen übergaben ein
   literales `pagination={{ pageSize: N }}`. antd merged via
   extendsObject(innerPagination, paginationObj) — der Prop überschreibt
   bei jedem Render den State, den der Size-Changer gerade gesetzt hat.
   Bei einem Live-Log rendert das im Sekundentakt, der Klick auf 20/100
   war also sofort wieder weg. Fix: defaultPageSize (unkontrolliert).
   Betraf ausser dem Live-Log auch Logs, Backups-History, Routes,
   Alerts und CrowdSec.

Ausserdem: `t` aus den WS-Effect-Deps genommen. i18n wechselt dessen
Identität bei Store-Updates, was den Effect neu laufen liess — inklusive
setEntries([]), d.h. der Live-Puffer leerte sich ohne erkennbaren Grund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-09-11 09:32:43 +02:00
parent 3e05c7fe49
commit 33cfc1a90d
7 changed files with 52 additions and 16 deletions

View File

@@ -438,7 +438,7 @@ export default function AlertsPage() {
onChange: (keys) => setSelectedIds(keys as number[]),
}}
rowClassName={(r) => (r.acknowledged_at ? 'eg-row-muted' : '')}
pagination={{ pageSize: 25 }}
pagination={{ defaultPageSize: 25 }}
locale={{ emptyText: (
<EmptyState
icon={<BellOutlined />}

View File

@@ -350,7 +350,7 @@ export default function HistoryTab() {
loading={list.isFetching}
dataSource={list.data ?? []}
columns={columns}
pagination={{ pageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
pagination={{ defaultPageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
locale={{ emptyText: t('backups.empty') }}
/>

View File

@@ -225,7 +225,7 @@ function DecisionsTab() {
loading={isLoading}
dataSource={decisions ?? []}
columns={columns}
pagination={{ pageSize: 20 }}
pagination={{ defaultPageSize: 20 }}
/>
<Modal
title={t('cs.decision.banModal')}
@@ -321,7 +321,7 @@ function AlertsTab() {
loading={isLoading}
dataSource={alerts ?? []}
columns={columns}
pagination={{ pageSize: 20 }}
pagination={{ defaultPageSize: 20 }}
/>
</div>
)
@@ -379,7 +379,7 @@ function BouncersTab() {
loading={isLoading}
dataSource={bouncers ?? []}
columns={columns}
pagination={{ pageSize: 20 }}
pagination={{ defaultPageSize: 20 }}
/>
</div>
)
@@ -436,7 +436,7 @@ function MachinesTab() {
loading={isLoading}
dataSource={machines ?? []}
columns={columns}
pagination={{ pageSize: 20 }}
pagination={{ defaultPageSize: 20 }}
/>
</div>
)
@@ -515,7 +515,7 @@ function CollectionsTab() {
loading={isLoading}
dataSource={collections ?? []}
columns={columns}
pagination={{ pageSize: 50 }}
pagination={{ defaultPageSize: 50 }}
/>
</div>
)

View File

@@ -129,6 +129,12 @@ export default function FirewallLivePage() {
pausedRef.current = paused
const pendingDuringPauseRef = useRef<Entry[]>([])
const wsRef = useRef<WebSocket | null>(null)
// tRef statt `t` in den WS-Effect-Deps: `t` wechselt bei jedem
// i18n-Store-Update die Identität. Stünde es in den Deps, würde der
// Effect neu laufen — inkl. `setEntries([])` und WS-Reconnect, d. h.
// der Live-Puffer wäre ohne erkennbaren Grund plötzlich leer.
const tRef = useRef(t)
useEffect(() => { tRef.current = t }, [t])
useEffect(() => {
const t = setTimeout(() => setAppliedFilters(filters), 300)
@@ -187,7 +193,7 @@ export default function FirewallLivePage() {
if (!cancelled && active) scheduleReconnect()
}
ws.onerror = () => {
setError(t('fwlog.connError'))
setError(tRef.current('fwlog.connError'))
}
}
@@ -204,7 +210,7 @@ export default function FirewallLivePage() {
if (reconnectTimer) clearTimeout(reconnectTimer)
if (wsRef.current) { wsRef.current.close(); wsRef.current = null }
}
}, [active, query, t])
}, [active, query])
// Resume: gebufferte Events in die Tabelle mergen.
useEffect(() => {
@@ -412,7 +418,7 @@ export default function FirewallLivePage() {
size="small"
dataSource={[...entries].reverse()}
columns={columns}
pagination={{ pageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
pagination={{ defaultPageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
locale={{ emptyText: connected ? t('fwlog.empty') : t('fwlog.connecting') }}
/>
</>

View File

@@ -293,7 +293,7 @@ export default function LogsPage() {
loading={logsQuery.isFetching}
dataSource={entries}
columns={columns}
pagination={{ pageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
pagination={{ defaultPageSize: 50, showSizeChanger: true, pageSizeOptions: [25, 50, 100, 200] }}
/>
{entries.length === 0 && !logsQuery.isFetching && (

View File

@@ -228,7 +228,7 @@ export default function RoutesTab() {
size="small"
dataSource={live.data ?? []}
columns={liveColumns}
pagination={{ pageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
pagination={{ defaultPageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }}
locale={{ emptyText: t('routes.liveEmpty') }}
style={{ marginTop: 12 }}
/>

View File

@@ -410,9 +410,25 @@ SYSCTL
if [ ! -f /var/log/edgeguard/firewall.jsonl ]; then
: > /var/log/edgeguard/firewall.jsonl
fi
# ulogd2 läuft als root (eigener Daemon); File muss von ihm
# schreibbar UND von edgeguard-API lesbar sein.
chown root:"$EG_USER" /var/log/edgeguard/firewall.jsonl
# WICHTIG (Incident 2026-09-06): ulogd2 läuft NICHT als root
# die Debian-Unit startet `ulogd --daemon --uid ulog`, der Daemon
# dropped also nach dem Öffnen seiner Files auf den User `ulog`.
# Solange er läuft schreibt er über den offenen fd weiter, egal
# wem die Datei gehört. Aber: das Distro-Profil
# /etc/logrotate.d/ulogd2 schickt nachts SIGHUP an ulogd, damit
# es seine Logfiles neu öffnet — und DAS passiert als `ulog`.
# Gehörte die Datei root:edgeguard 0640, scheitert das Reopen mit
# "can't open JSON log file: Permission denied", ulogd wertet das
# als fatal und BEENDET sich. Ohne Restart= blieb der Dienst dann
# tagelang still tot (5 Tage unbemerkt) → Firewall-Live-Log zeigte
# nur noch den alten In-Memory-Ring der API, nie neue Events.
# Daher: Owner = ulog (Schreiber), Gruppe = edgeguard (Leser).
if getent passwd ulog >/dev/null 2>&1; then
chown ulog:"$EG_USER" /var/log/edgeguard/firewall.jsonl
else
# ulogd2 (noch) nicht installiert — Fallback wie bisher.
chown root:"$EG_USER" /var/log/edgeguard/firewall.jsonl
fi
chmod 0640 /var/log/edgeguard/firewall.jsonl
cat > /etc/ulogd.conf <<'ULOGD'
@@ -460,7 +476,9 @@ ULOGD
compress
delaycompress
copytruncate
create 0640 root edgeguard
# Owner muss ulog sein — siehe Kommentar oben: ulogd re-öffnet die
# Datei bei SIGHUP als `ulog` und stirbt sonst an EACCES.
create 0640 ulog edgeguard
}
LOGROTATE
chmod 0644 /etc/logrotate.d/edgeguard-firewall
@@ -469,6 +487,18 @@ LOGROTATE
# da ist (Dependency-Konflikt o.ä.), nur warnen — die Firewall
# läuft auch ohne Logger.
if systemctl list-unit-files ulogd2.service >/dev/null 2>&1; then
# Restart=always als Selbstheilung: ulogd beendet sich bei
# einem fehlgeschlagenen Reopen mit Exit-Code 0 (also KEIN
# Restart=on-failure — das würde nicht greifen). Ohne das
# bleibt Firewall-Logging nach einem Rotate-Hiccup still tot.
install -d -m 0755 /etc/systemd/system/ulogd2.service.d
cat > /etc/systemd/system/ulogd2.service.d/edgeguard-restart.conf <<'ULOGDUNIT'
# Managed by edgeguard — re-installation overwrites this file.
[Service]
Restart=always
RestartSec=5s
ULOGDUNIT
systemctl daemon-reload
systemctl enable ulogd2.service >/dev/null 2>&1 || true
systemctl restart ulogd2.service || \
echo "postinst: ulogd2.service restart failed (firewall logs disabled until fixed)" >&2