Files
legal-ai/web-ui/src/components/cases/agent-activity-preview.tsx
Chaim 67c2c43777
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
fix(web-ui): display all human-facing timestamps in Asia/Jerusalem (deterministic)
Storage stays UTC (DB TIMESTAMPTZ, API ISO-UTC) — only the display layer is
localized, and now deterministically: every timestamp renders pinned to
Asia/Jerusalem via a single Intl-based formatter, so SSR (UTC container) and
the browser agree on any runtime. No layout/visible-format change — only the tz.

- New single date formatter web-ui/src/lib/format-date.ts (G2): formatDate /
  formatDateShort / formatDateLong / formatDateTime / formatDateTimeFull /
  formatTime / formatIsoDate + Israel helpers getIsraelYear / israelDayKey /
  israelMidnightMs / israelParts + formatRelative (long/short/tight wording).
- Routed all ad-hoc toLocaleDateString/toLocaleTimeString/toLocaleString +
  hand-rolled new Date(...).get*() / toISOString().slice(0,10) timestamp call
  sites (30 files) through it. Number toLocaleString left untouched.
- Spec: added INV-UI9 to docs/spec/X6 (UTC storage, Asia/Jerusalem display).

Display-only; no layout/design/IA change → Claude Design gate N/A.
Invariants: G2 (single date formatter, no parallel ad-hoc formatting), INV-UI9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:18:09 +00:00

106 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Link from "next/link";
import { Loader2, CheckCircle2, Clock } from "lucide-react";
import { useAgentActivity } from "@/lib/api/agents";
import type { PaperclipIssue } from "@/lib/api/agents";
import { formatRelative } from "@/lib/format-date";
const STATUS_DOT: Record<string, string> = {
in_progress: "bg-gold",
todo: "bg-rule",
backlog: "bg-rule",
done: "bg-success",
cancelled: "bg-ink-muted/40",
};
const STATUS_LABEL: Record<string, string> = {
in_progress: "בביצוע",
todo: "לביצוע",
backlog: "ממתין",
done: "הושלם",
cancelled: "בוטל",
};
function timeAgo(iso: string | null): string {
return formatRelative(iso, { units: "tight", fallback: "—" });
}
function IssueRow({ issue }: { issue: PaperclipIssue }) {
const dot = STATUS_DOT[issue.status] ?? "bg-rule";
const lbl = STATUS_LABEL[issue.status] ?? issue.status;
const isDone = issue.status === "done";
return (
<div className="flex items-start gap-3 py-3 border-b border-rule-soft last:border-0">
<span
className={`mt-1.5 w-2 h-2 rounded-full flex-none ${dot} ${issue.status === "in_progress" ? "animate-pulse" : ""}`}
/>
<div className="flex-1 min-w-0">
<p className={`text-[0.88rem] font-medium leading-snug ${isDone ? "text-ink-muted line-through" : "text-navy"}`}>
{issue.title}
</p>
<p className="text-[0.75rem] text-ink-muted mt-0.5">
{issue.assignee_name ?? lbl}
</p>
</div>
<div className="flex-none flex flex-col items-end gap-1">
{isDone ? (
<CheckCircle2 className="w-3.5 h-3.5 text-success mt-0.5" />
) : issue.status === "in_progress" ? (
<Loader2 className="w-3.5 h-3.5 text-gold animate-spin mt-0.5" />
) : (
<Clock className="w-3.5 h-3.5 text-ink-muted mt-0.5" />
)}
<span className="text-[0.72rem] text-ink-muted tabular-nums">
{timeAgo(issue.completed_at ?? issue.started_at ?? issue.created_at)}
</span>
</div>
</div>
);
}
export function AgentActivityPreview({
caseNumber,
}: {
caseNumber: string;
}) {
const { data, isLoading } = useAgentActivity(caseNumber);
const issues = [...(data?.issues ?? [])]
.sort((a, b) => {
const ta = a.started_at ?? a.created_at ?? "";
const tb = b.started_at ?? b.created_at ?? "";
return tb.localeCompare(ta);
})
.slice(0, 4);
return (
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment flex items-center justify-between">
<span className="text-[0.92rem] font-semibold text-navy">פעילות סוכנים</span>
<Link
href={`/cases/${caseNumber}#agents`}
className="text-[0.78rem] font-semibold text-gold-deep hover:underline"
>
כל הפעילות
</Link>
</div>
<div className="px-5">
{isLoading ? (
<div className="flex items-center justify-center py-8 text-ink-muted gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-sm">טוען...</span>
</div>
) : issues.length === 0 ? (
<p className="py-8 text-center text-sm text-ink-muted">
אין פעילות סוכנים עדיין
</p>
) : (
issues.map((issue) => <IssueRow key={issue.id} issue={issue} />)
)}
</div>
</div>
);
}