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.
+