A Python CLI (typer) that bootstraps Node/pnpm and launches a Next.js 16 web app for running configured shell scripts: YAML config validated by a shared Zod schema, dynamic per-script forms mapped to shadcn controls, argv-safe execa execution with live WebSocket streaming, SQLite/Drizzle run history, and optional argon2 session + API token auth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
export const dynamic = "force-dynamic";
|
|
|
|
import Link from "next/link";
|
|
import { desc } from "drizzle-orm";
|
|
import { getDb } from "@/lib/db/client";
|
|
import { runs } from "@/lib/db/schema";
|
|
import { RunStatusBadge } from "@/components/runs/run-status-badge";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
|
|
function formatDuration(startedAt: Date | null, endedAt: Date | null): string {
|
|
if (!startedAt) return "-";
|
|
const end = endedAt ?? new Date();
|
|
const seconds = Math.max(
|
|
0,
|
|
Math.round((end.getTime() - startedAt.getTime()) / 1000),
|
|
);
|
|
if (seconds < 60) return `${seconds}s`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
return `${minutes}m ${seconds % 60}s`;
|
|
}
|
|
|
|
export default function RunsPage() {
|
|
const db = getDb();
|
|
const rows = db
|
|
.select()
|
|
.from(runs)
|
|
.orderBy(desc(runs.createdAt))
|
|
.limit(100)
|
|
.all();
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<h1 className="text-2xl font-semibold tracking-tight">Run History</h1>
|
|
{rows.length === 0 ? (
|
|
<p className="text-muted-foreground py-12 text-center">No runs yet.</p>
|
|
) : (
|
|
<div className="overflow-x-auto rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Script</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Triggered by</TableHead>
|
|
<TableHead>Started</TableHead>
|
|
<TableHead>Duration</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((run) => (
|
|
<TableRow key={run.id} className="cursor-pointer">
|
|
<TableCell>
|
|
<Link
|
|
href={`/runs/${run.id}`}
|
|
className="font-medium hover:underline"
|
|
>
|
|
{run.scriptName}
|
|
</Link>
|
|
</TableCell>
|
|
<TableCell>
|
|
<RunStatusBadge status={run.status} />
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{run.triggeredBy}
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{run.startedAt
|
|
? new Date(run.startedAt).toLocaleString()
|
|
: "-"}
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{formatDuration(run.startedAt, run.endedAt)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|