From 39348c94919c5378f26188f38358cbdef3cb6fde Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Mon, 20 Jul 2026 15:18:36 +0100 Subject: [PATCH] feat(web): live SSE log tail and log retention setting --- web/app/settings/page.tsx | 35 ++++++++- web/app/workflows/[id]/runs/[runId]/page.tsx | 78 +++++++++++++++----- web/lib/api.ts | 22 +++++- 3 files changed, 112 insertions(+), 23 deletions(-) diff --git a/web/app/settings/page.tsx b/web/app/settings/page.tsx index f2fbf4e..41ae69a 100644 --- a/web/app/settings/page.tsx +++ b/web/app/settings/page.tsx @@ -168,6 +168,9 @@ export default function SettingsPage() { const [toAddrs, setToAddrs] = useState(""); // comma-separated in UI const [useTLS, setUseTLS] = useState(false); + // Workflow log retention (days). 0 = keep forever. + const [logRetentionDays, setLogRetentionDays] = useState(30); + const [saved, setSaved] = useState(false); useEffect(() => { @@ -183,11 +186,15 @@ export default function SettingsPage() { setFromAddr(settings.email?.from_addr ?? ""); setToAddrs((settings.email?.to_addrs ?? []).join(", ")); setUseTLS(settings.email?.use_tls ?? false); + setLogRetentionDays(settings.workflow_log_retention_days ?? 30); }, [settings]); const { mutate: save, isPending } = useMutation({ - mutationFn: (payload: { alerts: AlertSettings; email: EmailSettings }) => - api.saveSettings(payload), + mutationFn: (payload: { + alerts: AlertSettings; + email: EmailSettings; + workflow_log_retention_days?: number | null; + }) => api.saveSettings(payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["settings"] }); setSaved(true); @@ -218,6 +225,7 @@ export default function SettingsPage() { to_addrs: toList, use_tls: useTLS, }, + workflow_log_retention_days: logRetentionDays, }); } @@ -374,6 +382,29 @@ export default function SettingsPage() { + {/* Workflow logs */} + + + Workflow Logs + +

+ How long to keep workflow run logs on the server before they are + automatically deleted. +

+ + setLogRetentionDays(Number(e.target.value))} + className="w-32 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30" + /> + +
+
{sr.steps.map((st: StepRun) => ( -
- - {st.name} - - - attempts: {st.attempts} - {st.status === "failed" ? ` · exit ${st.exit_code}` : ""} - - + {st.name} + + + attempts: {st.attempts} + {st.status === "failed" ? ` · exit ${st.exit_code}` : ""} - - {(st.stdout || st.stderr) && ( -
-                      {st.stdout}
-                      {st.stderr ? `\n${st.stderr}` : ""}
-                    
- )} -
+ + +
))} {sr.steps.length === 0 && (

No steps yet.

)} + ))} {run.server_runs.length === 0 && ( diff --git a/web/lib/api.ts b/web/lib/api.ts index 6d13f38..157f87b 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -95,6 +95,7 @@ export interface Settings { alerts: AlertSettings; email: EmailSettings; secrets: SecretsSettings; + workflow_log_retention_days?: number | null; } export interface SecretGroupSummary { @@ -171,8 +172,7 @@ export interface StepRun { status: string; attempts: number; exit_code: number; - stdout: string; - stderr: string; + log_offset: number; output_env: Record; started_at?: string; finished_at?: string; @@ -290,7 +290,11 @@ export const api = { return request("/settings"); }, - saveSettings(settings: { alerts: AlertSettings; email: EmailSettings }): Promise<{ saved: boolean }> { + saveSettings(settings: { + alerts: AlertSettings; + email: EmailSettings; + workflow_log_retention_days?: number | null; + }): Promise<{ saved: boolean }> { return request<{ saved: boolean }>("/settings", { method: "PUT", body: JSON.stringify(settings), @@ -460,4 +464,16 @@ export const api = { cancelRun(runId: string): Promise { return request(`/runs/${runId}/cancel`, { method: "POST" }); }, + + async getServerRunLog(runId: string, serverId: string): Promise { + const res = await fetch(`/api/runs/${runId}/servers/${serverId}/logs`, { + credentials: "include", + }); + if (!res.ok) throw new Error("no logs"); + return res.text(); + }, + + serverRunLogStreamUrl(runId: string, serverId: string): string { + return `/api/runs/${runId}/servers/${serverId}/logs/stream`; + }, };