From 2ed378fb8fc8b596ba07a9bbd23300b5d4005c25 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 13 May 2026 18:00:37 +0200 Subject: [PATCH] feat(audit): add /audit list and /audit/[flow_run_id] detail pages --- app/audit/[flow_run_id]/page.tsx | 125 +++++++++++++++++++++++++++++++ app/audit/page.tsx | 100 +++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 app/audit/[flow_run_id]/page.tsx create mode 100644 app/audit/page.tsx diff --git a/app/audit/[flow_run_id]/page.tsx b/app/audit/[flow_run_id]/page.tsx new file mode 100644 index 0000000..093493d --- /dev/null +++ b/app/audit/[flow_run_id]/page.tsx @@ -0,0 +1,125 @@ +import Link from 'next/link' +import { notFound, redirect } from 'next/navigation' +import { getCurrentUser } from '@/lib/session' +import { prisma } from '@/lib/prisma' + +export const dynamic = 'force-dynamic' + +type Props = { + params: Promise<{ flow_run_id: string }> +} + +const STATUS_STYLES: Record = { + pending: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400', + running: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', + success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', + failed: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', + cancelled: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400', +} + +export default async function AuditDetailPage({ params }: Props) { + const user = await getCurrentUser() + if (!user) redirect('/login') + + const { flow_run_id } = await params + + const run = await prisma.flowRun.findUnique({ + where: { id: flow_run_id }, + include: { steps: { orderBy: { step_index: 'asc' } } }, + }) + + if (!run || run.user_id !== user.id) notFound() + + const durationMs = + run.ended_at && run.started_at + ? run.ended_at.getTime() - run.started_at.getTime() + : null + + return ( +
+
+
+ + ← Audit Log + + / +

{run.flow_key}

+
+ +
+
+
+
Status
+ + {run.status} + +
+
+
Exit code
+ {run.exit_code != null ? run.exit_code : '—'} +
+
+
Started
+ {run.started_at.toLocaleString()} +
+
+
Duration
+ {durationMs != null ? `${(durationMs / 1000).toFixed(1)}s` : '—'} +
+
+
+ + {run.steps.map((step) => { + const args = step.args_json ? (JSON.parse(step.args_json) as string[]) : [] + return ( +
+
+ Step {step.step_index + 1} + {step.command_key} + {args.length > 0 && ( + {args.join(' ')} + )} +
+ + {(step.stdout || step.stderr) && ( +
+
+ {step.stdout && ( +
+                        {step.stdout}
+                      
+ )} + {step.stderr && ( +
+                        {step.stderr}
+                      
+ )} +
+
+ exit {step.exit_code != null ? step.exit_code : '—'} + {step.ended_at && step.started_at && ( + + {((step.ended_at.getTime() - step.started_at.getTime()) / 1000).toFixed(1)}s + + )} +
+
+ )} + + {!step.stdout && !step.stderr && ( +
+ No output recorded. +
+ )} +
+ ) + })} +
+
+ ) +} diff --git a/app/audit/page.tsx b/app/audit/page.tsx new file mode 100644 index 0000000..3d565f2 --- /dev/null +++ b/app/audit/page.tsx @@ -0,0 +1,100 @@ +import Link from 'next/link' +import { redirect } from 'next/navigation' +import { getCurrentUser } from '@/lib/session' +import { prisma } from '@/lib/prisma' + +export const dynamic = 'force-dynamic' + +const STATUS_STYLES: Record = { + pending: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400', + running: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', + success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', + failed: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', + cancelled: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400', +} + +export default async function AuditPage() { + const user = await getCurrentUser() + if (!user) redirect('/login') + + const runs = await prisma.flowRun.findMany({ + where: { user_id: user.id }, + orderBy: { started_at: 'desc' }, + take: 100, + }) + + return ( +
+
+
+
+

Audit Log

+

Recent write actions executed on this server

+
+
+ + {runs.length === 0 ? ( +
+ No actions have been run yet. +
+ ) : ( +
+ + + + + + + + + + + + {runs.map((run) => { + const durationMs = + run.ended_at && run.started_at + ? run.ended_at.getTime() - run.started_at.getTime() + : null + return ( + + + + + + + + ) + })} + +
CommandStatusExitStartedDuration
+ + {run.flow_key} + + + + {run.status} + + + {run.exit_code != null ? run.exit_code : '—'} + + {run.started_at.toLocaleString()} + + {durationMs != null ? `${(durationMs / 1000).toFixed(1)}s` : '—'} +
+
+ )} +
+
+ ) +}