spb/autoscholar
Public
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="title-2 mt-1">Runs</h1>34 </div>35 <nav aria-label="Filter" className="flex flex-wrap 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 <ul className="divide-y hairline md:hidden">53 {runs.length === 0 && <li className="muted py-8 text-center text-sm">No runs match.</li>}54 {runs.map((r) => (55 <li key={r.id} className="py-3">56 <Link href={`/admin/runs/${r.id}`} className="block font-medium no-underline hover:underline">57 {r.title}58 </Link>59 <div className="mt-1 flex flex-wrap items-center gap-2 text-xs">60 <StatusBadge status={r.status} />61 <span className="font-mono">{r.currentStage ?? "—"}</span>62 <span className="muted">{modeLabel(r.researchMode)}</span>63 <span className="muted">64 {r.currentAttemptNo}/{r.maxAttempts}65 </span>66 <span className="muted">{fmtUsd(r.totalCostUsd)}</span>67 <span className="subtle">{ago(r.updatedAt)}</span>68 </div>69 </li>70 ))}71 </ul>72 <div className="table-wrap hidden md:block">73 <table className="table">74 <thead>75 <tr>76 <th>Title</th>77 <th>Mode</th>78 <th>Status</th>79 <th>Stage</th>80 <th>Attempt</th>81 <th>Cost</th>82 <th>Auto-publish</th>83 <th>Updated</th>84 </tr>85 </thead>86 <tbody>87 {runs.length === 0 && (88 <tr>89 <td colSpan={8} className="muted py-8 text-center">90 No runs match.91 </td>92 </tr>93 )}94 {runs.map((r) => (95 <tr key={r.id}>96 <td>97 <Link href={`/admin/runs/${r.id}`}>{r.title}</Link>98 <p className="subtle font-mono text-[10px]">{r.id}</p>99 </td>100 <td>{modeLabel(r.researchMode)}</td>101 <td>102 <StatusBadge status={r.status} />103 </td>104 <td className="font-mono text-xs">{r.currentStage ?? "—"}</td>105 <td>106 {r.currentAttemptNo}/{r.maxAttempts}107 </td>108 <td>109 {fmtUsd(r.totalCostUsd)} <span className="subtle text-xs">/ {fmtUsd(r.maxBudgetUsd)}</span>110 </td>111 <td>{r.autoPublish ? "yes" : "no"}</td>112 <td className="subtle text-xs">{ago(r.updatedAt)}</td>113 </tr>114 ))}115 </tbody>116 </table>117 </div>118 </div>119 );120}121