From 6ee87174d35f70e683f58aea1a591b60150b91cc Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 20 Jun 2026 17:46:00 -0600 Subject: [PATCH] fix(activity): tolerate unknown activity kinds in ActivityFeed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live-tail stream (now reachable through the new Vite /api proxy) started delivering activity rows with kind="manual_match", a kind the ActivityFeed kindConfig map didn't know about. The map lookup returned undefined, and reading .icon on it threw — taking the whole Activity Log page down. Two fixes: 1. Add "manual_match" to the ActivityKind union in src/types/index.ts and to the kindConfig map in src/components/ActivityFeed.tsx (with a Wrench icon + neutral tone), so the kind is properly typed and rendered. 2. Add a FALLBACK_KIND guard so any future backend-emitted kind that arrives before the frontend is updated no longer crashes the page. kindConfig stays exhaustive for known kinds, so TypeScript still catches missing entries at compile time. Also adds src/components/ActivityFeed.test.tsx with three regression tests (empty state, multi-item render, unknown-kind safety). --- src/components/ActivityFeed.test.tsx | 50 ++++++++++++++++++++++++++++ src/components/ActivityFeed.tsx | 20 ++++++++++- src/types/index.ts | 6 +++- vite.config.ts | 21 ++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/components/ActivityFeed.test.tsx 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"; + }); + }, + }, + }, }, });