import { useEffect, useState } from "react";
import { z } from "zod";
const logRow = z.object({ seq: z.number(), step: z.number(), text: z.string() });
const snapshot = z.object({ rows: z.array(logRow), done: z.boolean() });
const event = z.discriminatedUnion("type", [
z.object({ type: z.literal("log"), seq: z.number(), step: z.number(), text: z.string() }),
z.object({ type: z.literal("step"), index: z.number(), status: z.string(), conclusion: z.string().nullable() }),
z.object({ type: z.literal("done"), conclusion: z.string().nullable() }),
]);
export type LogRow = z.infer<typeof logRow>;
type State = { rows: LogRow[]; live: boolean; error: string | null };
/** Loads a job's log and, while it runs, follows the Runner Durable Object's server-sent event stream. */
export function useJobLog(jobId: string, onStepChange: () => void) {
const [state, setState] = useState<State>({ rows: [], live: false, error: null });
useEffect(() => {
let source: EventSource | null = null;
let cancelled = false;
async function start() {
const response = await fetch(`/api/jobs/${jobId}/logs`);
if (!response.ok) {
setState((current) => ({ ...current, error: `Failed to load logs (${response.status})` }));
return;
}
const initial = snapshot.parse(await response.json());
if (cancelled) return;
setState({ rows: initial.rows, live: !initial.done, error: null });
if (initial.done) return;
const after = initial.rows.at(-1)?.seq ?? 0;
source = new EventSource(`/api/jobs/${jobId}/logs/stream?after=${after}`);
source.onmessage = (message) => {
const parsed = event.safeParse(JSON.parse(message.data));
if (!parsed.success) return;
const payload = parsed.data;
if (payload.type === "log") {
setState((current) =>