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`;
+ },
};