diff --git a/internal/capture/analysis.go b/internal/capture/analysis.go index a2dc67e..7e2116e 100644 --- a/internal/capture/analysis.go +++ b/internal/capture/analysis.go @@ -187,8 +187,7 @@ func (e *Engine) Statistics(ctx context.Context, id string, buckets int) (Stats, if buckets <= 0 || buckets > 2000 { buckets = 300 } - var s Stats - s.Counters = map[string]int64{} + s := Stats{Counters: map[string]int64{}, Protocols: []NameCount{}, SizeBuckets: []NameCount{}, Timeline: []TimeBucket{}} err := e.DB.QueryRowContext(ctx, `SELECT COUNT(*), COALESCE(SUM(len),0), COALESCE(MIN(ts),0), COALESCE(MAX(ts),0) FROM packets WHERE session_id=?`, id). Scan(&s.Packets, &s.Bytes, &s.Start, &s.End) if err != nil || s.Packets == 0 { @@ -552,7 +551,7 @@ func (e *Engine) TLS(ctx context.Context, id string, limit int) ([]TLSRecord, [] certFields := []string{"ip.src", "ipv6.src", "x509if.printableString", "x509sat.uTF8String", "x509ce.dNSName", "x509af.notBefore", "x509af.notAfter"} certRows, err := e.extractFields(ctx, id, "tls.handshake.type==11", certFields, limit) if err != nil { - return recs, nil, nil // handshake records are still useful + return recs, []CertRecord{}, nil // handshake records are still useful } certs := []CertRecord{} for _, c := range certRows { diff --git a/web/dist/.gitkeep b/web/dist/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/web/src/App.tsx b/web/src/App.tsx index 5801d84..bce5301 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { api, setCSRF, connectEvents, disconnectEvents } from "./api"; import { useHash, usePersist, Toaster, go } from "./ui"; +import { ErrorBoundary } from "./components/ErrorBoundary"; import Login from "./pages/Login"; import Setup from "./pages/Setup"; import Overview from "./pages/Overview"; @@ -121,7 +122,7 @@ export default function App() { v{sess.version} -
{page}
+
{page}
); diff --git a/web/src/components/ErrorBoundary.tsx b/web/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..31e21ae --- /dev/null +++ b/web/src/components/ErrorBoundary.tsx @@ -0,0 +1,25 @@ +import { Component, type ReactNode } from "react"; + +// A page crash must never blank the whole appliance UI. This shows the error +// and a reload, keeping the surrounding navigation usable. +export class ErrorBoundary extends Component<{ children: ReactNode; where: string }, { err: Error | null }> { + state = { err: null as Error | null }; + static getDerivedStateFromError(err: Error) { return { err }; } + componentDidUpdate(prev: { children: ReactNode; where: string }) { + if (prev.where !== this.props.where && this.state.err) this.setState({ err: null }); + } + render() { + if (this.state.err) { + return ( +
+
+ This page hit an error. + {String(this.state.err.message || this.state.err)}. The rest of the appliance is unaffected — pick another page, or reload. +
+ +
+ ); + } + return this.props.children; + } +} diff --git a/web/src/pages/Library.tsx b/web/src/pages/Library.tsx index de4c75d..8a9d69c 100644 --- a/web/src/pages/Library.tsx +++ b/web/src/pages/Library.tsx @@ -47,7 +47,7 @@ export default function Library() { {s.ended_at ? ((s.ended_at - s.started_at) / 1000).toFixed(0) + " s" : "–"} {s.packets.toLocaleString()} {fmtBytes(s.bytes)} - {s.kind === "import" ? "import" : s.interfaces.join(",")}{s.capture_filter &&
{s.capture_filter}
} + {s.kind === "import" ? "import" : (s.interfaces ?? []).join(",")}{s.capture_filter &&
{s.capture_filter}
}
Export diff --git a/web/src/pages/Overview.tsx b/web/src/pages/Overview.tsx index f7decb5..00f9a0d 100644 --- a/web/src/pages/Overview.tsx +++ b/web/src/pages/Overview.tsx @@ -23,7 +23,7 @@ export default function Overview() { }), []); const fwd = h?.forwarding; - const live = h?.capture?.session as Session | undefined; + const live = h?.capture?.running ? (h.capture.session as Session) : undefined; const assoc = clients?.clients.filter((c) => c.associated) ?? []; const alerts: [string, "warn" | "bad"][] = []; if (herr) alerts.push(["Health endpoint unavailable: " + herr, "bad"]); @@ -48,7 +48,7 @@ export default function Overview() {

Capture

{live ? live.packets.toLocaleString() : "idle"}{live && packets}
-
{live ? {live.paused ? "paused" : "capturing"} on {live.interfaces.join(", ")} : not capturing}
+
{live ? {live.paused ? "paused" : "capturing"} on {(live.interfaces ?? []).join(", ")} : not capturing}

Traffic rate

{rate ? Math.round(rate.pps).toLocaleString() : "–"}pkt/s
diff --git a/web/src/pages/TlsExplorer.tsx b/web/src/pages/TlsExplorer.tsx index c651a3b..d57fa27 100644 --- a/web/src/pages/TlsExplorer.tsx +++ b/web/src/pages/TlsExplorer.tsx @@ -26,8 +26,8 @@ export default function TlsExplorer({ sessionId }: { sessionId: string }) { {Object.keys(verCount).length > 0 &&
{Object.entries(verCount).map(([v, n]) => {v}: {n})}
}
- - + +
setQ(e.target.value)} />