+
+ {/* SP5: live-tail status pill, right-aligned in the toolbar. */}
+
+
+
+
{isLoading ? (
{Array.from({ length: 5 }).map((_, i) => (
diff --git a/src/pages/Remittances.test.tsx b/src/pages/Remittances.test.tsx
index 49e0059..1bce185 100644
--- a/src/pages/Remittances.test.tsx
+++ b/src/pages/Remittances.test.tsx
@@ -10,6 +10,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Remittances } from "./Remittances";
import { api } from "@/lib/api";
+import { useTailStore } from "@/store/tail-store";
// Module-level mock: vitest hoists `vi.mock` calls above imports. We only
// stub the method this page touches via the `useRemittances` hook.
@@ -23,6 +24,18 @@ vi.mock("@/lib/api", () => ({
},
}));
+// Mock the live-tail hook so the page renders the pill in the settled
+// `live` state without driving the real streamTail parser. The hook's
+// own test file (`useTailStream.test.ts`) covers the lifecycle in depth.
+vi.mock("@/hooks/useTailStream", () => ({
+ useTailStream: vi.fn(() => ({
+ status: "live",
+ lastEventAt: null,
+ error: null,
+ forceReconnect: vi.fn(),
+ })),
+}));
+
/**
* Minimal `render` helper using react-dom/client + act(). Mirrors the
* `renderIntoContainer` helper in `Reconciliation.test.tsx` — see that
@@ -78,6 +91,10 @@ async function waitForText(
describe("Remittances", () => {
beforeEach(() => {
vi.clearAllMocks();
+ // Singleton tail-store: clear the remittances slice between tests
+ // so a tail-arrival case (if added later) doesn't see rows from a
+ // previous test.
+ useTailStore.getState().reset("remittances");
});
it("renders a CAS adjustment label inside the expanded detail row", async () => {
@@ -146,4 +163,36 @@ describe("Remittances", () => {
expect(document.body.textContent).toContain("PR-1");
unmount();
});
+
+ // -------------------------------------------------------------------
+ // Live-tail status pill (sub-project 5, Phase 5 Task 23, optional
+ // page-level test). Cheap smoke check: the toolbar renders the pill
+ // with the `live` label whenever the mocked stream is settled. The
+ // hook's lifecycle is covered exhaustively in its own test file.
+ // -------------------------------------------------------------------
+
+ it("test_status_pill_shows_live_in_toolbar", async () => {
+ (
+ api.listRemittances as unknown as ReturnType
+ ).mockResolvedValue({
+ items: [],
+ total: 0,
+ returned: 0,
+ has_more: false,
+ });
+
+ const { unmount } = renderIntoContainer(React.createElement(Remittances));
+
+ // Wait for the empty-state to mount — once the page body is rendered,
+ // the toolbar (and thus the pill) is in the DOM.
+ await waitForText("Remittances · awaiting first 835");
+
+ const pill = document.body.querySelector(
+ '[data-testid="tail-status-pill"]',
+ );
+ expect(pill).not.toBeNull();
+ expect(pill?.textContent).toContain("Live");
+
+ unmount();
+ });
});
\ No newline at end of file
diff --git a/src/pages/Remittances.tsx b/src/pages/Remittances.tsx
index 8ee2837..8159c3c 100644
--- a/src/pages/Remittances.tsx
+++ b/src/pages/Remittances.tsx
@@ -1,4 +1,4 @@
-import { Fragment, useState } from "react";
+import { Fragment, useMemo, useState } from "react";
import { ChevronDown, ChevronRight, Receipt } from "lucide-react";
import {
Table,
@@ -15,9 +15,12 @@ import { ErrorState } from "@/components/ui/error-state";
import { FilterChips, type FilterChipOption } from "@/components/ui/filter-chips";
import { Pagination } from "@/components/ui/pagination";
import { useRemittances } from "@/hooks/useRemittances";
+import { useTailStream } from "@/hooks/useTailStream";
+import { useMergedTail } from "@/hooks/useMergedTail";
+import { TailStatusPill } from "@/components/TailStatusPill";
import { fmt } from "@/lib/format";
import { cn } from "@/lib/utils";
-import type { CasAdjustment, RemittanceStatus } from "@/types";
+import type { CasAdjustment, Remittance, RemittanceStatus } from "@/types";
const PAGE_SIZE = 25;
@@ -64,7 +67,25 @@ export function Remittances() {
limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE,
});
- const items = data?.items ?? [];
+
+ // SP5 live-tail wiring (sub-project 5, Phase 5 Task 23). The tail
+ // stream emits every remittance that lands — including ones that
+ // don't match the user's current `status` filter chip. Drop those
+ // tail items so a newly-arrived remit that wouldn't match the page's
+ // intent doesn't flash into the table. Base items are NOT filtered
+ // here (they reflect whatever the list query returned, matching the
+ // existing page behavior).
+ const tailFilterFn = useMemo(
+ () => (r: Remittance) => {
+ if (status && r.status !== status) return false;
+ return true;
+ },
+ [status],
+ );
+
+ const { status: tailStatus, lastEventAt: tailLastEventAt, forceReconnect } =
+ useTailStream("remittances");
+ const items = useMergedTail("remittances", data?.items ?? [], tailFilterFn);
const total = items.reduce(
(acc, r) => ({
@@ -126,15 +147,25 @@ export function Remittances() {
-
{
- setStatus((v as RemittanceStatus | null) ?? null);
- setPage(1);
- }}
- eyebrow="Status"
- />
+
+
{
+ setStatus((v as RemittanceStatus | null) ?? null);
+ setPage(1);
+ }}
+ eyebrow="Status"
+ />
+ {/* SP5: live-tail status pill, right-aligned in the toolbar. */}
+
+
+
+