spb/autoscholar
Public
1import { listRuns, listWorkers, overviewCounts } from "@autoscholar/core";2import { papers, securityEvents } from "@autoscholar/db";3import { desc, sql } from "drizzle-orm";4import Link from "next/link";5import { StatusBadge } from "@/components/StatusBadge";6import { ago, fmtDateTime, fmtUsd } from "@/lib/format";7import { db } from "@/lib/server/db";89export const dynamic = "force-dynamic";1011export default async function Overview() {12 const d = db();13 const counts = await overviewCounts(d);14 const workers = await listWorkers(d);15 const [pubs] = await d.select({ n: sql<number>`count(*)::int` }).from(papers);16 const alerts = await d.select().from(securityEvents).orderBy(desc(securityEvents.createdAt)).limit(8);17 const recent = await listRuns(d, { limit: 8 });18 const tiles: Array<[string, number]> = [19 ["Running", (counts.byStatus.running ?? 0) + (counts.byStatus.provisioning ?? 0)],20 ["Queued", (counts.byStatus.queued ?? 0) + (counts.byStatus.retrying ?? 0)],21 ["Blocked", (counts.byStatus.quality_failed ?? 0) + (counts.byStatus.awaiting_approval ?? 0)],22 ["Completed", counts.byStatus.published ?? 0],23 ["Failed", (counts.byStatus.failed ?? 0) + (counts.byStatus.cancelled ?? 0)],24 ];25 const online = workers.filter((w) => w.status === "online" || w.status === "draining");26 return (27 <div className="space-y-8">28 <header className="flex flex-wrap items-end justify-between gap-3">29 <div>30 <p className="eyebrow">Command center</p>31 <h1 className="serif mt-1 text-2xl font-semibold">Overview</h1>32 </div>33 <Link href="/admin/new" className="btn btn-primary">34 New research35 </Link>36 </header>37 <section aria-label="Job counts" className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">38 {tiles.map(([l, n]) => (39 <div key={l} className="surface p-4">40 <p className="label">{l}</p>41 <p className="serif mt-1 text-2xl">{n}</p>42 </div>43 ))}44 <div className="surface p-4">45 <p className="label">Provider spend</p>46 <p className="serif mt-1 text-2xl">{fmtUsd(counts.totalCostUsd)}</p>47 <p className="subtle text-xs">client-side estimate</p>48 </div>49 </section>50 <section className="grid gap-6 lg:grid-cols-2">51 <div className="surface p-4">52 <div className="flex items-center justify-between">53 <h2 className="serif text-lg font-semibold">MacLustr workers</h2>54 <Link href="/admin/maclustr" className="text-xs">55 Details56 </Link>57 </div>58 <p className="muted text-xs">59 {online.length} of {workers.length} online · {pubs?.n ?? 0} publications60 </p>61 <ul className="mt-3 space-y-2 text-sm">62 {workers.length === 0 && (63 <li className="muted">64 No worker has enrolled yet. Install the worker daemon on a MacLustr node (docs/OPERATIONS.md).65 </li>66 )}67 {workers.map((w) => (68 <li key={w.id} className="flex items-center justify-between gap-2">69 <span className="font-mono">{w.name}</span>70 <span className="flex items-center gap-2">71 <StatusBadge status={w.status} />72 <span className="subtle text-xs">73 {w.running}/{w.capacity} · {ago(w.lastHeartbeatAt)}74 </span>75 </span>76 </li>77 ))}78 </ul>79 </div>80 <div className="surface p-4">81 <div className="flex items-center justify-between">82 <h2 className="serif text-lg font-semibold">Security alerts</h2>83 <Link href="/admin/settings" className="text-xs">84 Audit log85 </Link>86 </div>87 <ul className="mt-3 space-y-1 text-xs">88 {alerts.length === 0 && <li className="muted">No security events recorded.</li>}89 {alerts.map((a) => (90 <li key={a.id} className="flex justify-between gap-2">91 <span>92 <span className="font-mono">{a.kind}</span>{" "}93 <span className="muted">{JSON.stringify(a.detail).slice(0, 80)}</span>94 </span>95 <span className="subtle whitespace-nowrap">{fmtDateTime(a.createdAt)}</span>96 </li>97 ))}98 </ul>99 </div>100 </section>101 <section className="surface p-4">102 <div className="flex items-center justify-between">103 <h2 className="serif text-lg font-semibold">Recent runs</h2>104 <Link href="/admin/runs" className="text-xs">105 All runs106 </Link>107 </div>108 <table className="table mt-3">109 <thead>110 <tr>111 <th>Title</th>112 <th>Status</th>113 <th>Stage</th>114 <th>Attempt</th>115 <th>Cost</th>116 <th>Updated</th>117 </tr>118 </thead>119 <tbody>120 {recent.length === 0 && (121 <tr>122 <td colSpan={6} className="muted py-6 text-center">123 No runs yet. Upload a research instruction to start.124 </td>125 </tr>126 )}127 {recent.map((r) => (128 <tr key={r.id}>129 <td>130 <Link href={`/admin/runs/${r.id}`}>{r.title}</Link>131 </td>132 <td>133 <StatusBadge status={r.status} />134 </td>135 <td className="font-mono text-xs">{r.currentStage ?? "—"}</td>136 <td>137 {r.currentAttemptNo}/{r.maxAttempts}138 </td>139 <td>{fmtUsd(r.totalCostUsd)}</td>140 <td className="subtle text-xs">{ago(r.updatedAt)}</td>141 </tr>142 ))}143 </tbody>144 </table>145 </section>146 </div>147 );148}149