SPB Git

spb/autoscholar

Public
8.6 KB · 219 lines tsx
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
Files · main
Menu

Theme