feat(sp25): wire live-tail triplet into Acks page

The 999 register opens /api/acks/stream via useTailStream("acks"),
merges the snapshot + tail through useMergedTail("acks", ...) so
new rows appear without a manual refresh, and surfaces the
connection state via <TailStatusPill> in the hero. The TA1 section
gets the same triplet against /api/ta1-acks/stream.

Both pills sit in the page header so the operator can see at a
glance whether the live-tail connection is healthy, without having
to open the drawer or refresh the page. A stalled/error stream
shows a Reconnect button inline.

The Acks test mock adds useTailStream at the module level so the
page renders without opening a real fetch. New test asserts both
TailStatusPills mount in the page tree.
This commit is contained in:
Nora
2026-07-02 09:12:19 -06:00
parent 191bc935c3
commit 146cb6d17d
2 changed files with 102 additions and 8 deletions
+51 -8
View File
@@ -20,7 +20,10 @@ import { Pagination } from "@/components/ui/pagination";
import { useAckDrawerUrlState } from "@/hooks/useAckDrawerUrlState";
import { useAcks } from "@/hooks/useAcks";
import { useTa1Acks } from "@/hooks/useTa1Acks";
import { useMergedTail } from "@/hooks/useMergedTail";
import { useTailStream } from "@/hooks/useTailStream";
import { useRowKeyboard } from "@/hooks/useRowKeyboard";
import { TailStatusPill } from "@/components/TailStatusPill";
import { api } from "@/lib/api";
import { fmt } from "@/lib/format";
import { cn } from "@/lib/utils";
@@ -47,7 +50,20 @@ export function Acks() {
limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE,
});
const items = data?.items ?? [];
// SP25: subscribe to /api/acks/stream so new 999 acks (whether from
// the SFTP poller or a manual upload) land in the table the moment
// they hit the database. The status surfaces in the hero pill so the
// operator can see whether the connection is healthy.
const {
status: tailStatus,
lastEventAt: tailLastEventAt,
forceReconnect,
} = useTailStream("acks");
// Merge the page's base list with the live-tail delta. The hook
// dedupes by id (first write wins), so a snapshot replay on
// reconnect can't double-count an existing row.
const mergedItems = useMergedTail("acks", data?.items ?? []);
const items = mergedItems;
const totalCount = data?.total ?? 0;
// Server-side aggregates — sum over the *full* ACK set, not just the
// visible page. Without this, the KPI strip silently under-reports
@@ -213,6 +229,16 @@ export function Acks() {
<ShieldCheck className="h-3.5 w-3.5" strokeWidth={1.75} />
{anyRejected ? "Mixed envelope" : "Envelope accepted"}
</div>
{/* SP25: live-tail connection status. Sits next to the
envelope-accepted pill so the operator can see at a
glance whether the stream is healthy without having to
open the drawer. Reconnect button only renders when
the stream is in a visibly-failed state. */}
<TailStatusPill
status={tailStatus}
lastEventAt={tailLastEventAt}
onReconnect={forceReconnect}
/>
</div>
{/* Keyboard hint — same rhythm as Reconciliation, sits under
@@ -604,7 +630,14 @@ function downloadBlob(filename: string, content: string) {
// ---------------------------------------------------------------------------
function Ta1AcksSection() {
const { data, isLoading, isError, error, refetch } = useTa1Acks({ limit: 50 });
const items = data?.items ?? [];
// SP25: same live-tail triplet as the 999 register above, just
// bound to /api/ta1-acks/stream and the ta1Acks store slice.
const {
status: tailStatus,
lastEventAt: tailLastEventAt,
forceReconnect,
} = useTailStream("ta1_acks");
const items = useMergedTail("ta1_acks", data?.items ?? []);
const totals = items.reduce(
(acc, t) => {
@@ -631,12 +664,22 @@ function Ta1AcksSection() {
TA1 <span className="italic">envelopes</span>, newest first.
</h2>
</div>
<p className="text-[12.5px] text-muted-foreground/80 max-w-sm">
One row per inbound ISA/IEA interchange the
envelope-level sibling of the 999. Gainwell's Colorado
MFT does not ship TA1s today; this section surfaces them
when they appear.
</p>
<div className="flex items-center gap-3 flex-wrap">
<p className="text-[12.5px] text-muted-foreground/80 max-w-sm">
One row per inbound ISA/IEA interchange the
envelope-level sibling of the 999. Gainwell's Colorado
MFT does not ship TA1s today; this section surfaces them
when they appear.
</p>
{/* SP25: live-tail status for the TA1 stream. Quietly
placed after the description so the eyebrow / display
title remains the dominant scan target. */}
<TailStatusPill
status={tailStatus}
lastEventAt={tailLastEventAt}
onReconnect={forceReconnect}
/>
</div>
</div>
<div className="grid gap-3 grid-cols-2 md:grid-cols-4 pt-2">