diff --git a/src/components/ActivityFeed.test.tsx b/src/components/ActivityFeed.test.tsx
new file mode 100644
index 0000000..6af07a1
--- /dev/null
+++ b/src/components/ActivityFeed.test.tsx
@@ -0,0 +1,50 @@
+// @vitest-environment happy-dom
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+import { describe, expect, it } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { ActivityFeed } from "./ActivityFeed";
+import type { Activity } from "@/types";
+
+const baseActivity: Activity = {
+ id: "a-1",
+ kind: "claim_submitted",
+ message: "Claim ABC-1 submitted",
+ timestamp: "2026-06-20T12:00:00Z",
+};
+
+describe("ActivityFeed", () => {
+ it("renders the empty-state copy when items is empty", () => {
+ render();
+ expect(screen.getByText("No activity yet.")).toBeTruthy();
+ });
+
+ it("renders each item's message", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Claim ABC-1 submitted")).toBeTruthy();
+ expect(screen.getByText("Remit XYZ received")).toBeTruthy();
+ });
+
+ it("does not crash on an unknown kind", () => {
+ // Backwards-compat guard: the backend may emit a kind the frontend
+ // hasn't been updated for yet (manual_match was the original case).
+ // The feed must fall back to a neutral icon instead of throwing.
+ const unknown = {
+ ...baseActivity,
+ id: "a-3",
+ message: "future_kind event arrived",
+ // Cast through `unknown` so the type-checker doesn't widen the
+ // `kind` literal away from the exhaustive union.
+ kind: "future_kind" as Activity["kind"],
+ };
+ expect(() => render()).not.toThrow();
+ expect(screen.getByText("future_kind event arrived")).toBeTruthy();
+ });
+});
diff --git a/src/components/ActivityFeed.tsx b/src/components/ActivityFeed.tsx
index 552417b..260afe7 100644
--- a/src/components/ActivityFeed.tsx
+++ b/src/components/ActivityFeed.tsx
@@ -1,9 +1,11 @@
import {
Banknote,
CheckCircle2,
+ Circle,
FileText,
Pill,
UserPlus,
+ Wrench,
XCircle,
type LucideIcon,
} from "lucide-react";
@@ -21,6 +23,22 @@ const kindConfig: Record<
claim_denied: { icon: XCircle, tone: "text-destructive" },
remit_received: { icon: Pill, tone: "text-[hsl(var(--warning))]" },
provider_added: { icon: UserPlus, tone: "text-foreground" },
+ // `manual_match` is emitted by the backend when an operator manually
+ // pairs a remit to a claim from the Reconciliation page — see
+ // `cyclone.store` `record_manual_match`. Wrench is the closest fit;
+ // we keep it neutral-tone so it doesn't read as a status.
+ manual_match: { icon: Wrench, tone: "text-muted-foreground" },
+};
+
+// Defensive fallback: if the backend ever emits a kind this UI doesn't
+// know about (e.g. a new kind landed server-side before the frontend
+// picked it up), render a neutral icon instead of crashing the whole
+// Activity Log page. The kindConfig map above stays exhaustive for
+// known kinds so TypeScript still catches missing entries at compile
+// time; this only protects the runtime path.
+const FALLBACK_KIND: { icon: LucideIcon; tone: string } = {
+ icon: Circle,
+ tone: "text-muted-foreground",
};
export function ActivityFeed({
@@ -40,7 +58,7 @@ export function ActivityFeed({
return (
{items.map((a) => {
- const cfg = kindConfig[a.kind];
+ const cfg = kindConfig[a.kind] ?? FALLBACK_KIND;
const Icon = cfg.icon;
return (
- {
+ proxy.on("proxyRes", (proxyRes) => {
+ proxyRes.headers["x-accel-buffering"] = "no";
+ });
+ },
+ },
+ },
},
});