SPB Git

spb/autoscholar

Public
3.1 KB · 99 lines tsx
1import { listRuns } from "@autoscholar/core";2import type { RunStatus } from "@autoscholar/schemas";3import Link from "next/link";4import { StatusBadge } from "@/components/StatusBadge";5import { ago, fmtUsd, modeLabel } from "@/lib/format";6import { db } from "@/lib/server/db";78export const dynamic = "force-dynamic";9const GROUPS: Record<string, string[]> = {10  active: [11    "queued",12    "provisioning",13    "running",14    "quality_check",15    "revising",16    "retrying",17    "publishing",18    "cancelling",19  ],20  blocked: ["quality_failed", "awaiting_approval", "ready_to_publish"],21  done: ["published", "failed", "cancelled"],22};2324export default async function RunsPage({ searchParams }: { searchParams: Promise<{ filter?: string }> }) {25  const { filter } = await searchParams;26  const statuses = filter && GROUPS[filter] ? (GROUPS[filter] as RunStatus[]) : undefined;27  const runs = await listRuns(db(), { status: statuses, limit: 200 });28  return (29    <div className="space-y-6">30      <header className="flex flex-wrap items-end justify-between gap-3">31        <div>32          <p className="eyebrow">Orchestration</p>33          <h1 className="serif mt-1 text-2xl font-semibold">Runs</h1>34        </div>35        <nav aria-label="Filter" className="flex gap-1">36          {[37            ["", "All"],38            ["active", "Active"],39            ["blocked", "Needs attention"],40            ["done", "Finished"],41          ].map(([k, l]) => (42            <Link43              key={k}44              href={k ? `/admin/runs?filter=${k}` : "/admin/runs"}45              className={`btn btn-sm ${(filter ?? "") === k ? "btn-primary" : ""}`}46            >47              {l}48            </Link>49          ))}50        </nav>51      </header>52      <table className="table">53        <thead>54          <tr>55            <th>Title</th>56            <th>Mode</th>57            <th>Status</th>58            <th>Stage</th>59            <th>Attempt</th>60            <th>Cost</th>61            <th>Auto-publish</th>62            <th>Updated</th>63          </tr>64        </thead>65        <tbody>66          {runs.length === 0 && (67            <tr>68              <td colSpan={8} className="muted py-8 text-center">69                No runs match.70              </td>71            </tr>72          )}73          {runs.map((r) => (74            <tr key={r.id}>75              <td>76                <Link href={`/admin/runs/${r.id}`}>{r.title}</Link>77                <p className="subtle font-mono text-[10px]">{r.id}</p>78              </td>79              <td>{modeLabel(r.researchMode)}</td>80              <td>81                <StatusBadge status={r.status} />82              </td>83              <td className="font-mono text-xs">{r.currentStage ?? "—"}</td>84              <td>85                {r.currentAttemptNo}/{r.maxAttempts}86              </td>87              <td>88                {fmtUsd(r.totalCostUsd)} <span className="subtle text-xs">/ {fmtUsd(r.maxBudgetUsd)}</span>89              </td>90              <td>{r.autoPublish ? "yes" : "no"}</td>91              <td className="subtle text-xs">{ago(r.updatedAt)}</td>92            </tr>93          ))}94        </tbody>95      </table>96    </div>97  );98}99
Files · 87b4e55c3b9fbcc2cd191a1e469b33de0a24ccc8
Menu

Theme