spb/autoscholar
Public
1import { listRuns, listWorkers, overviewCounts } from "@autoscholar/core";2import { papers, securityEvents } from "@autoscholar/db";3import { STAGES } from "@autoscholar/schemas";4import { desc, sql } from "drizzle-orm";5import Link from "next/link";6import { StatusBadge } from "@/components/StatusBadge";7import { ago, fmtDateTime, fmtUsd } from "@/lib/format";8import { db } from "@/lib/server/db";910export const dynamic = "force-dynamic";1112export default async function Overview() {13 const d = db();14 const counts = await overviewCounts(d);15 const workers = await listWorkers(d);16 const [pubs] = await d.select({ n: sql<number>`count(*)::int` }).from(papers);17 const alerts = await d.select().from(securityEvents).orderBy(desc(securityEvents.createdAt)).limit(8);18 const recent = await listRuns(d, { limit: 10 });19 const active = recent.filter((r) =>20 ["running", "provisioning", "queued", "retrying", "quality_check", "publishing"].includes(r.status),21 );22 const tiles: Array<[string, number, string]> = [23 [24 "Running",25 (counts.byStatus.running ?? 0) + (counts.byStatus.provisioning ?? 0),26 "var(--color-info-500)",27 ],28 ["Queued", (counts.byStatus.queued ?? 0) + (counts.byStatus.retrying ?? 0), "var(--fg-muted)"],29 [30 "Needs attention",31 (counts.byStatus.quality_failed ?? 0) + (counts.byStatus.awaiting_approval ?? 0),32 "var(--color-warn-500)",33 ],34 ["Published", counts.byStatus.published ?? 0, "var(--color-ok-500)"],35 [36 "Failed / cancelled",37 (counts.byStatus.failed ?? 0) + (counts.byStatus.cancelled ?? 0),38 "var(--color-danger-500)",39 ],40 ];41 const online = workers.filter((w) => w.status === "online" || w.status === "draining");42 return (43 <div className="space-y-6">44 <header className="flex flex-wrap items-end justify-between gap-3">45 <div>46 <p className="eyebrow">Command center</p>47 <h1 className="title-2 mt-1">Overview</h1>48 </div>49 <Link href="/admin/new" className="btn btn-primary">50 New research51 </Link>52 </header>5354 <section aria-label="Job counts" className="grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-6">55 {tiles.map(([l, n, c]) => (56 <div key={l} className="surface p-4">57 <p className="label">{l}</p>58 <p className="serif mt-1 text-3xl" style={{ color: n > 0 ? c : undefined }}>59 {n}60 </p>61 </div>62 ))}63 <div className="surface p-4">64 <p className="label">Provider spend</p>65 <p className="serif mt-1 text-3xl">{fmtUsd(counts.totalCostUsd)}</p>66 <p className="subtle text-xs">client-side estimate</p>67 </div>68 </section>6970 {active.length > 0 && (71 <section className="surface p-4" aria-labelledby="active">72 <h2 id="active" className="title-3">73 Active research74 </h2>75 <ul className="mt-3 divide-y hairline">76 {active.map((r) => {77 const idx = STAGES.findIndex((s) => s.key === r.currentStage);78 const pct = Math.max(4, Math.round(((idx + 1) / STAGES.length) * 100));79 return (80 <li key={r.id} className="grid gap-2 py-3 sm:grid-cols-12 sm:items-center">81 <div className="min-w-0 sm:col-span-5">82 <Link83 href={`/admin/runs/${r.id}`}84 className="block truncate font-medium no-underline hover:underline"85 >86 {r.title}87 </Link>88 <p className="subtle mt-0.5 font-mono text-[11px]">{r.id}</p>89 </div>90 <div className="sm:col-span-4">91 <div className="flex items-center justify-between text-xs">92 <span className="font-mono">{r.currentStage ?? "—"}</span>93 <span className="subtle">{idx >= 0 ? `${idx + 1}/${STAGES.length}` : ""}</span>94 </div>95 <div className="mt-1 h-1.5 rounded-full" style={{ background: "var(--bg-muted)" }}>96 <div97 className="h-1.5 rounded-full"98 style={{ width: `${pct}%`, background: "var(--sci)" }}99 />100 </div>101 </div>102 <div className="flex items-center justify-between gap-2 text-xs sm:col-span-3 sm:justify-end">103 <StatusBadge status={r.status} />104 <span className="subtle">105 {fmtUsd(r.totalCostUsd)} · {ago(r.updatedAt)}106 </span>107 </div>108 </li>109 );110 })}111 </ul>112 </section>113 )}114115 <section className="grid gap-4 lg:grid-cols-2">116 <div className="surface p-4">117 <div className="flex items-center justify-between">118 <h2 className="title-3">MacLustr workers</h2>119 <Link href="/admin/maclustr" className="text-xs">120 Details121 </Link>122 </div>123 <p className="muted text-xs">124 {online.length} of {workers.length} online · {pubs?.n ?? 0} publications125 </p>126 <ul className="mt-3 space-y-2 text-sm">127 {workers.length === 0 && (128 <li className="muted">129 No worker has enrolled yet. Install the worker daemon on a MacLustr node (docs/OPERATIONS.md).130 </li>131 )}132 {workers.map((w) => (133 <li key={w.id} className="flex items-center justify-between gap-2">134 <span className="font-mono">{w.name}</span>135 <span className="flex items-center gap-2">136 <StatusBadge status={w.status} />137 <span className="subtle text-xs">138 {w.running}/{w.capacity} · {ago(w.lastHeartbeatAt)}139 </span>140 </span>141 </li>142 ))}143 </ul>144 </div>145 <div className="surface p-4">146 <div className="flex items-center justify-between">147 <h2 className="title-3">Security alerts</h2>148 <Link href="/admin/settings" className="text-xs">149 Audit log150 </Link>151 </div>152 <ul className="mt-3 space-y-1.5 text-xs">153 {alerts.length === 0 && <li className="muted">No security events recorded.</li>}154 {alerts.map((a) => (155 <li key={a.id} className="flex justify-between gap-2">156 <span className="min-w-0 truncate">157 <span className="font-mono">{a.kind}</span>{" "}158 <span className="muted">{JSON.stringify(a.detail).slice(0, 80)}</span>159 </span>160 <span className="subtle whitespace-nowrap">{fmtDateTime(a.createdAt)}</span>161 </li>162 ))}163 </ul>164 </div>165 </section>166167 <section className="surface p-4">168 <div className="flex items-center justify-between">169 <h2 className="title-3">Recent runs</h2>170 <Link href="/admin/runs" className="text-xs">171 All runs172 </Link>173 </div>174 <div className="table-wrap mt-3">175 <table className="table">176 <thead>177 <tr>178 <th>Title</th>179 <th>Status</th>180 <th>Stage</th>181 <th>Attempt</th>182 <th>Cost</th>183 <th>Updated</th>184 </tr>185 </thead>186 <tbody>187 {recent.length === 0 && (188 <tr>189 <td colSpan={6} className="muted py-6 text-center">190 No runs yet. Upload a research instruction to start.191 </td>192 </tr>193 )}194 {recent.map((r) => (195 <tr key={r.id}>196 <td className="max-w-[18rem]">197 <Link href={`/admin/runs/${r.id}`} className="block truncate">198 {r.title}199 </Link>200 </td>201 <td>202 <StatusBadge status={r.status} />203 </td>204 <td className="font-mono text-xs">{r.currentStage ?? "—"}</td>205 <td>206 {r.currentAttemptNo}/{r.maxAttempts}207 </td>208 <td>{fmtUsd(r.totalCostUsd)}</td>209 <td className="subtle text-xs">{ago(r.updatedAt)}</td>210 </tr>211 ))}212 </tbody>213 </table>214 </div>215 </section>216 </div>217 );218}219