feat(acks): surface 999 rejections via sidebar badge + table sort

The 1,156 accepted acks were burying the 5 rejections — the operator
had to scroll the whole table to find them. Two changes:

1. Sidebar '999 ACKs' nav item gets a warning-toned badge showing
   the total rejected segment count from aggregates.rejected_count
   (mirrors the Reconciliation unmatched badge pattern). One number,
   visible from any page.

2. Acks page table now sorts rejected rows to the top, then
   newest-id-first. All 5 rejections fit on page 1 since they're a
   tiny fraction of the total — no pagination needed.

New files:
- src/hooks/useAckStats.ts (lightweight aggregates-only fetch)
- src/hooks/useAckStats.test.tsx (2 tests)
- src/components/Sidebar.test.tsx (3 tests)

Pre-existing baseline: 10 frontend failures (api.test.ts exportBatch837,
tail-stream.test.ts acks/ta1_acks targeting, Inbox/InboxHeader copy).
Unchanged.
This commit is contained in:
Nora
2026-07-02 10:59:46 -06:00
parent aca9667ff8
commit f88a7c7c4f
6 changed files with 277 additions and 2 deletions
+16 -2
View File
@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { CheckCircle2, Download, Mail, ShieldCheck } from "lucide-react";
import {
Table,
@@ -63,7 +63,21 @@ export function Acks() {
// 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;
// Display order: rejected rows float to the top, then newest-first
// by id. The server returns id-DESC so accepted rows already
// follow that order — we only need to bubble the rejections up
// (the operator flagged on 2026-07-02 that 5 rejections were
// buried inside 1,156 accepted acks and hard to find). All 5 fit
// on page 1 since they're a tiny fraction of the total — the
// operator never needs to paginate to find them.
const items = useMemo(() => {
return [...mergedItems].sort((a, b) => {
const ra = a.rejectedCount > 0 ? 1 : 0;
const rb = b.rejectedCount > 0 ? 1 : 0;
if (ra !== rb) return rb - ra; // rejected first
return b.id - a.id; // newest id first within each group
});
}, [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