From 69eb64db08d16384522ce2f63bde46a347048fec Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 1 Jun 2026 21:02:40 +0200 Subject: [PATCH] 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. --- surfsense_web/hooks/use-automation-runs.ts | 99 ++++++++++++++++++---- 1 file changed, 83 insertions(+), 16 deletions(-) diff --git a/surfsense_web/hooks/use-automation-runs.ts b/surfsense_web/hooks/use-automation-runs.ts index c91c7bd6e..69e51ddc6 100644 --- a/surfsense_web/hooks/use-automation-runs.ts +++ b/surfsense_web/hooks/use-automation-runs.ts @@ -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({ - 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(() => { + 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({ + return useReactQuery({ 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[]) : [], + }; +}