feat: umfangreiches UI+API-Polish (v1.1.36–1.1.42)
Backend: - Audit-Log: Search-Endpoint mit ILIKE-Filter (actor/action/subject/date) - NTP: /ntp/status via chronyc tracking (Stratum, Offset, Quelle) - System: /service-restart mit Allowlist (haproxy/squid/unbound/chrony/scheduler) - Domain-Response-Headers + Rate-Limit (Migration 0024) - Join-Tokens (Migration 0025), Cluster-mTLS, Aggregator-Fan-Out - apt-Service für Update-Banner (apt-get update + Versionsprüfung) - Backup-Retry mit exponential backoff (retry_apt 3×) - publish.sh fail-fast + cleanup-old.sh (max 10 Versionen) Frontend: - Audit-Log-Page (/audit) mit Filter + Pagination - ErrorBoundary an React-Root + Vite build-target festgenagelt (iOS 15+) - Storage-Schema-Stamp: auto-wipe bei Versions-Mismatch (blank-page-Fix) - EmptyState-Komponente überall ausgerollt - SSL: Aggregate-Karte (total/expiring/expired/errors) - Backups: Aggregate-Karte (letzter Backup/Größe/Fehlschläge 24h) + Backup-Now - NTP: Sync-Status-Karte (chronyc tracking live) - Domains: Backend-UP/DOWN-Chip aus HAProxy-Stats - Backends: HAProxy-Status-Spalte (UP/DEGRADED/DOWN) - Settings: Service-Neustart-Karte (haproxy/squid/unbound/chrony/scheduler) - Settings: Upgrade-Status-Card, Wartungsmodus, Auto-Update, Retention - Dashboard: Recent-Alerts, Cluster-Health, License-Chip, Onboarding-Hint - System-Regeln im Firewall als eigener Tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Button, Card, DatePicker, Input, Select, Space, Switch, Table, Tag, Tooltip, Typography, message,
|
||||
} from 'antd'
|
||||
@@ -72,18 +72,52 @@ function toCSV(rows: Entry[]): string {
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
// localStorage-Key für die Filter-Persistenz. Filter werden so über
|
||||
// Reloads + Tab-Wechsel hinweg gehalten — sonst muss der Operator nach
|
||||
// jedem F5 source/level/grep neu eintippen.
|
||||
const LOGS_FILTERS_KEY = 'edgeguard.logs.filters.v1'
|
||||
|
||||
// Filter aus localStorage lesen + sicher dekodieren. range wird als
|
||||
// String[2] (ISO) serialisiert; beim Lesen rekonstruieren wir es als
|
||||
// String — die DatePicker-Range erwartet Dayjs aber range-Restoration
|
||||
// vereinfachen wir hier zu null, weil Datumsbereiche meist nicht über
|
||||
// Sessions hinweg interessant sind (logs sind zeitnah).
|
||||
function loadStoredFilters(): Filters {
|
||||
const empty: Filters = { sources: [], levels: [], range: null, grep: '', limit: 200 }
|
||||
try {
|
||||
const raw = localStorage.getItem(LOGS_FILTERS_KEY)
|
||||
if (!raw) return empty
|
||||
const v = JSON.parse(raw) as Partial<Filters>
|
||||
return {
|
||||
sources: Array.isArray(v.sources) ? v.sources : [],
|
||||
levels: Array.isArray(v.levels) ? v.levels : [],
|
||||
range: null, // Range nicht persistieren (Logs sind „jetzt"-relevant)
|
||||
grep: typeof v.grep === 'string' ? v.grep : '',
|
||||
limit: typeof v.limit === 'number' && v.limit > 0 ? v.limit : 200,
|
||||
}
|
||||
} catch {
|
||||
return empty
|
||||
}
|
||||
}
|
||||
|
||||
export default function LogsPage() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [filters, setFilters] = useState<Filters>({
|
||||
sources: [], // [] = alle
|
||||
levels: [], // [] = alle
|
||||
range: null,
|
||||
grep: '',
|
||||
limit: 200,
|
||||
})
|
||||
const [filters, setFilters] = useState<Filters>(() => loadStoredFilters())
|
||||
const [autoRefresh, setAutoRefresh] = useState(true)
|
||||
|
||||
// Filter-State in localStorage persistieren. Range bewusst weglassen
|
||||
// (Datumsbereiche sind „jetzt"-relevant, nicht zwischen Sessions).
|
||||
useEffect(() => {
|
||||
try {
|
||||
const { range: _, ...persistable } = filters
|
||||
void _
|
||||
localStorage.setItem(LOGS_FILTERS_KEY, JSON.stringify(persistable))
|
||||
} catch {
|
||||
// localStorage voll / disabled — sw allowed, einfach ignorieren.
|
||||
}
|
||||
}, [filters])
|
||||
|
||||
// Sources-Liste vom Backend (statisch im internal/services/syslogs).
|
||||
const sourcesQuery = useQuery({
|
||||
queryKey: ['logs', 'sources'],
|
||||
@@ -236,6 +270,19 @@ export default function LogsPage() {
|
||||
options={[100, 200, 500, 1000].map((n) => ({ value: n, label: `${n} ${t('logs.limit')}` }))}
|
||||
style={{ width: 130 }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setFilters({ sources: [], levels: [], range: null, grep: '', limit: 200 })}
|
||||
disabled={
|
||||
filters.sources.length === 0 &&
|
||||
filters.levels.length === 0 &&
|
||||
!filters.range &&
|
||||
!filters.grep &&
|
||||
filters.limit === 200
|
||||
}
|
||||
>
|
||||
{t('logs.filter.reset')}
|
||||
</Button>
|
||||
<Text type="secondary">{t('logs.found', { n: entries.length })}</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user