import { useI18n } from "@/i18n/I18nProvider";
import { listProjectInfoHistory } from "@/lib/queries";
import { useRealtimeWorkerEvents } from "@/lib/realtime";
import type { WorkerEvent } from "@/types";
import { format, isSameDay, parseISO } from "date-fns";
import { useEffect, useState } from "react";
type Group = { day: string; events: WorkerEvent[] };
function groupByDay(events: WorkerEvent[]): Group[] {
const out: Group[] = [];
for (const ev of events) {
const day = format(parseISO(ev.created_at), "yyyy-MM-dd");
const last = out[out.length - 1];
if (last && last.day === day) {
last.events.push(ev);
} else {
out.push({ day, events: [ev] });
}
}
return out;
}
export default function ProjectHistory({ project }: { project: string }) {
const { t } = useI18n();
const [events, setEvents] = useState<WorkerEvent[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let alive = true;
setLoading(true);
setError(null);
listProjectInfoHistory(project)
.then((r) => alive && setEvents(r))
.catch((e) => alive && setError(String(e?.message ?? e)))
.finally(() => alive && setLoading(false));
return () => {
alive = false;
};
}, [project]);
// Realtime: append new info events as they arrive
useRealtimeWorkerEvents(
{
onInsert: (ev) => {
if (ev.event_type !== "advice" || ev.severity !== "info") return;
setEvents((prev) => {
// append while keeping order; if it's later than the last entry,
// simply push; otherwise re-sort defensively.
const last = prev[prev.length - 1];
if (!last || ev.created_at >= last.created_at) {
return [...prev, ev];
}
return [...prev, ev].sort((a, b) =>
a.created_at.localeCompare(b.created_at),
);
});
},
},
project,
);
if (loading) {
return <div className="text-sm text-muted-foreground">{t("common.loading")}</div>;
}
if (error) {
return (
<div className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
);
}
if (events.length === 0) {
return <div className="text-sm text-muted-foreground p-4">{t("history.empty")}</div>;
}
const groups = groupByDay(events);
const today = new Date();
return (
<div className="space-y-6">
<p className="text-sm text-muted-foreground">{t("history.subtitle")}</p>
{groups.map((g) => {
const d = parseISO(`${g.day}T00:00:00`);
const label = isSameDay(d, today) ? "Today" : format(d, "MMM d, yyyy");
return (
<section key={g.day} className="space-y-2">
<h3 className="text-xs uppercase tracking-wider text-muted-foreground">{label}</h3>
<ol className="space-y-2 border-l-2 border-muted pl-4">
{g.events.map((ev) => (
<li key={ev.id} className="space-y-0.5">
<div className="text-xs text-muted-foreground">
{format(parseISO(ev.created_at), "HH:mm")}
</div>
<div className="text-sm leading-relaxed">{extractText(ev)}</div>
{ev.path && (
<div className="text-xs font-mono text-muted-foreground">{ev.path}</div>
)}
</li>
))}
</ol>
</section>
);
})}
</div>
);
}
function extractText(ev: WorkerEvent): string {
const p = ev.payload as Record<string, unknown>;
if (typeof p?.text === "string") return p.text;
return "";
}