Add src/lib/time.ts formatEastern() — parses both backend timestamp shapes (the 'YYYY-MM-DD HH:MM:SS UTC' snapshot string and ISO offset transition times), renders them in America/New_York (DST-aware: EDT/EST) as 24h 'YYYY-MM-DD HH:MM:SS ZZZ'. Wire into TopBar (snapshot clock + tooltip) and the TaskDrawer timeline (entered_at). Adds time.test.ts (8 cases incl. DST + day boundary). 44 frontend tests green.
127 lines
3.3 KiB
TypeScript
127 lines
3.3 KiB
TypeScript
import { cn } from "@/lib/utils";
|
||
import { formatEastern } from "@/lib/time";
|
||
import type { StateResponse } from "../api";
|
||
|
||
interface Props {
|
||
state: StateResponse | null;
|
||
connected: boolean;
|
||
generatedAt: string | null;
|
||
}
|
||
|
||
interface ChipDef {
|
||
key: keyof StateResponse["summary"];
|
||
label: string;
|
||
numCls: string;
|
||
bgCls: string;
|
||
}
|
||
|
||
const CHIPS: ChipDef[] = [
|
||
{
|
||
key: "active",
|
||
label: "ACTIVE",
|
||
numCls: "text-status-active",
|
||
bgCls: "bg-status-active-bg",
|
||
},
|
||
{
|
||
key: "waiting",
|
||
label: "AWAITING HUMAN",
|
||
numCls: "text-status-waiting",
|
||
bgCls: "bg-status-waiting-bg",
|
||
},
|
||
{
|
||
key: "parked",
|
||
label: "PARKED",
|
||
numCls: "text-status-parked",
|
||
bgCls: "bg-status-parked-bg",
|
||
},
|
||
{
|
||
key: "open_questions",
|
||
label: "OPEN QUESTIONS",
|
||
numCls: "text-status-waiting",
|
||
bgCls: "bg-status-waiting-bg",
|
||
},
|
||
{
|
||
key: "total",
|
||
label: "TOTAL",
|
||
numCls: "text-muted-foreground",
|
||
bgCls: "bg-muted",
|
||
},
|
||
];
|
||
|
||
// Top status bar: brand, summary chips, connection health, and snapshot clock.
|
||
export function TopBar({ state, connected, generatedAt }: Props) {
|
||
const summary = state?.summary;
|
||
const snapshot = formatEastern(generatedAt);
|
||
const title = connected
|
||
? `Live — snapshot at ${snapshot}`
|
||
: `Disconnected — last snapshot at ${snapshot}`;
|
||
|
||
return (
|
||
<header
|
||
className="flex items-center gap-3 py-2 px-3 bg-card border-b border-border"
|
||
role="banner"
|
||
>
|
||
{/* Brand */}
|
||
<div className="flex items-center gap-2 shrink-0">
|
||
<span
|
||
className="w-3 h-3 rounded-full bg-gradient-to-br from-primary to-primary/50 shadow-sm"
|
||
aria-hidden
|
||
/>
|
||
<span className="text-sm font-bold text-foreground tracking-tight leading-none">
|
||
agent-team
|
||
</span>
|
||
<span className="text-xs text-muted-foreground leading-none">
|
||
pipeline
|
||
</span>
|
||
</div>
|
||
|
||
{/* Divider */}
|
||
<div className="w-px h-5 bg-border shrink-0" aria-hidden />
|
||
|
||
{/* Summary chips */}
|
||
<div
|
||
className="flex items-center gap-2 flex-1 min-w-0 overflow-x-auto"
|
||
role="list"
|
||
aria-label="Pipeline summary"
|
||
>
|
||
{CHIPS.map((c) => (
|
||
<div
|
||
key={c.key}
|
||
role="listitem"
|
||
className={cn(
|
||
"flex flex-col items-center justify-center rounded-md px-2.5 py-1 shrink-0",
|
||
c.bgCls,
|
||
)}
|
||
>
|
||
<span className={cn("text-sm font-bold leading-none tabular-nums", c.numCls)}>
|
||
{summary ? summary[c.key] : "–"}
|
||
</span>
|
||
<span className="text-[9px] font-semibold tracking-widest text-muted-foreground leading-none mt-0.5 whitespace-nowrap">
|
||
{c.label}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Health indicator */}
|
||
<div
|
||
className="flex items-center gap-2 shrink-0 ml-auto"
|
||
title={title}
|
||
aria-label={title}
|
||
>
|
||
<span
|
||
aria-hidden
|
||
className={cn(
|
||
"w-2.5 h-2.5 rounded-full",
|
||
connected
|
||
? "bg-status-active shadow-[0_0_6px_2px_hsl(var(--status-active)/0.45)]"
|
||
: "bg-status-failed",
|
||
)}
|
||
/>
|
||
<span className="text-xs text-muted-foreground tabular-nums">
|
||
{snapshot}
|
||
</span>
|
||
</div>
|
||
</header>
|
||
);
|
||
}
|