// --------------------------------------------------------------------------- // Live-tail status pill (sub-project 5, Phase 5 Task 21). // // Tiny presentational component used in the toolbar of `Claims`, // `Remittances`, and `ActivityLog` to surface the connection status of // `useTailStream`. Renders the existing `` (variants from // `src/components/ui/badge.tsx`) plus a "Last event: 12s ago" subtitle // that re-ticks every second, plus a "↻ Reconnect" button that only // appears when the connection has visibly failed (`stalled` or `error`). // // Variant mapping (per spec): // live → success // connecting → warning // reconnecting → warning // closed → destructive // stalled → destructive // error → destructive // --------------------------------------------------------------------------- import { useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import type { TailStatus } from "@/hooks/useTailStream"; export interface TailStatusPillProps { status: TailStatus; lastEventAt: Date | null; onReconnect: () => void; } const STATUS_LABEL: Record = { connecting: "Connecting…", live: "Live", reconnecting: "Reconnecting…", closed: "Closed", stalled: "Stalled", error: "Error", }; type BadgeVariant = "success" | "warning" | "destructive"; const STATUS_VARIANT: Record = { live: "success", connecting: "warning", reconnecting: "warning", closed: "destructive", stalled: "destructive", error: "destructive", }; /** * Render a duration in seconds as a compact human label. We deliberately * keep the units small — under a minute is the common case for a healthy * tail (the stall detector fires at 30s, so anything beyond is a real * outage the user will want to investigate). */ function formatAge(seconds: number): string { if (seconds < 0) return "0s"; if (seconds < 60) return `${seconds}s`; const m = Math.floor(seconds / 60); const s = seconds % 60; if (m < 60) return s === 0 ? `${m}m` : `${m}m ${s}s`; const h = Math.floor(m / 60); return `${h}h ${m % 60}m`; } export function TailStatusPill({ status, lastEventAt, onReconnect, }: TailStatusPillProps): React.JSX.Element { // Re-tick every second so the "Last event: 12s ago" subtitle stays // current without the parent having to re-render. We mount the // interval only once per pill instance. const [now, setNow] = useState(() => Date.now()); useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []); const ageSeconds = lastEventAt ? Math.max(0, Math.floor((now - lastEventAt.getTime()) / 1000)) : null; const subtitle = ageSeconds === null ? "Last event: never" : `Last event: ${formatAge(ageSeconds)} ago`; // Only show the manual reconnect affordance when the connection has // visibly failed. `closed` is reserved for the post-unmount state and // is intentionally not actionable — the component is about to vanish. const showReconnect = status === "stalled" || status === "error"; return (
{STATUS_LABEL[status]} {subtitle} {showReconnect && ( )}
); }