refactor(automations): source runs list from Zero

useAutomationRuns now reads from the zero_publication thin column set
and adapts rows to LiveRunSummary (RunSummary + step_results). The
detail hook stays on REST for the heavy fields.
This commit is contained in:
CREDO23 2026-06-01 21:02:40 +02:00
parent c73f7ef03a
commit 69eb64db08

View file

@ -1,42 +1,109 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import type { Run, RunListResponse } from "@/contracts/types/automation.types";
import { useQuery as useZeroQuery } from "@rocicorp/zero/react";
import { useQuery as useReactQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import type { Run, RunStepResult, RunSummary } from "@/contracts/types/automation.types";
import { automationsApiService } from "@/lib/apis/automations-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { queries } from "@/zero/queries";
const DEFAULT_LIMIT = 50;
const DEFAULT_OFFSET = 0;
/**
* Thin live row sourced from Zero. Strict superset of {@link RunSummary}
* existing consumers that only look at the summary fields keep working,
* while the run detail panel can read ``step_results`` directly for the
* live step ticker without a second REST round-trip.
*/
export interface LiveRunSummary extends RunSummary {
step_results: RunStepResult[];
}
export interface UseAutomationRunsOptions {
limit?: number;
offset?: number;
enabled?: boolean;
}
/** Paginated run history for one automation. Newest-first per backend. */
interface UseAutomationRunsResult {
data: { items: LiveRunSummary[]; total: number } | undefined;
isLoading: boolean;
error: Error | null;
}
/**
* Live run history for one automation, newest-first. Sourced from Zero's
* thin ``automation_runs`` publication so status and per-step progress
* tick in real time without polling. Heavy fields (output, artifacts,
* inputs, error, definition_snapshot) are still fetched lazily via
* {@link useAutomationRun}.
*/
export function useAutomationRuns(
automationId: number | undefined,
{ limit = DEFAULT_LIMIT, offset = DEFAULT_OFFSET, enabled = true }: UseAutomationRunsOptions = {}
) {
return useQuery<RunListResponse, Error>({
queryKey: cacheKeys.automations.runs(automationId ?? 0, limit, offset),
queryFn: () => automationsApiService.listRuns(automationId as number, { limit, offset }),
enabled: enabled && !!automationId,
staleTime: 30_000,
});
{ limit = DEFAULT_LIMIT }: UseAutomationRunsOptions = {}
): UseAutomationRunsResult {
const [rows, result] = useZeroQuery(
queries.automationRuns.byAutomation({ automationId: automationId ?? -1 })
);
const items = useMemo<LiveRunSummary[]>(() => {
if (!automationId) return [];
return rows.slice(0, limit).map(toLiveRunSummary);
}, [automationId, rows, limit]);
const total = automationId ? rows.length : 0;
// Pre-hydration window: nothing visible AND Zero hasn't confirmed
// completeness yet. After the first sync (even an empty set) we stop
// showing the skeleton so the empty-state copy can take over.
const isLoading = !!automationId && result.type !== "complete" && rows.length === 0;
return {
data: automationId ? { items, total } : undefined,
isLoading,
error: null,
};
}
/** Single run with the full snapshot, step results, output and artifacts. */
/**
* Full run record (definition snapshot, inputs, output, artifacts, error).
* Stays on REST: these fields are large and largely static after the run
* finishes, so they're not worth replicating to every connected client.
*/
export function useAutomationRun(
automationId: number | undefined,
runId: number | undefined,
options: { enabled?: boolean } = {}
) {
const { enabled = true } = options;
return useQuery<Run, Error>({
return useReactQuery<Run, Error>({
queryKey: cacheKeys.automations.run(automationId ?? 0, runId ?? 0),
queryFn: () => automationsApiService.getRun(automationId as number, runId as number),
enabled: enabled && !!automationId && !!runId,
staleTime: 30_000,
});
}
interface ZeroAutomationRunRow {
id: number;
automationId: number;
triggerId?: number | null;
status: string;
stepResults: unknown;
startedAt?: number | null;
finishedAt?: number | null;
createdAt: number;
}
/** Adapt a Zero camelCase row (epoch ms timestamps) to the snake_case
* ISO-string ``RunSummary`` shape the existing UI already consumes. */
function toLiveRunSummary(row: ZeroAutomationRunRow): LiveRunSummary {
return {
id: row.id,
automation_id: row.automationId,
trigger_id: row.triggerId ?? null,
status: row.status as RunSummary["status"],
started_at: row.startedAt ? new Date(row.startedAt).toISOString() : null,
finished_at: row.finishedAt ? new Date(row.finishedAt).toISOString() : null,
created_at: new Date(row.createdAt).toISOString(),
step_results: Array.isArray(row.stepResults) ? (row.stepResults as RunStepResult[]) : [],
};
}