Skip to the content

Capsid menu

src/portal-overnight.ts

33 lines and 2 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.

Functions

The functions of portal-overnight.ts
FunctionLinesComplexityCoveredCRAPMutation
handlePortalPlan21 to 27257%OkCRAP 2.3No data: mutation testing has not run on this repo yet
handlePortalDigest30 to 37475%OkCRAP 4.3No data: mutation testing has not run on this repo yet

Imports

It imports src/env.ts, src/http.ts, src/ops-feed.ts, src/ops-types/views.ts, src/overnight-plan.ts, src/portal-auth.ts. Imported by src/routes.ts.

Source

import type { Env } from "./env";
import { OPS_RETURN_TO } from "./ops-feed";
import type { PortalDigest, PortalPlan } from "./ops-types/views";
import { digestSinceFrom, readOvernightDigest, readOvernightPlan } from "./overnight-plan";
import { portalGate } from "./portal-auth";
import { jsonResponse as json } from "./http";

// The Portal's Tonight plan and its "since you last looked" strip
// (docs/design/design-portal-evaluation.md, DECIDE 3, ruled 2026-10-09):
// GET /portal/api/plan and GET /portal/api/digest. The readers behind `capsid_work_jobs` action list
// with view "plan" and view "digest" (src/overnight-plan.ts), over every namespace, so the
// Portal and chat cannot disagree. Their own routes, read when the page that shows them
// opens, since OPS_FEED_READS is pinned and the feed does not grow a read. Routes, not
// tools, so no grant is checked here (one enforcement point, test/route-gates.test.ts): each
// answers to portalGate, and src/scope.ts lists them among the routes gated some other way.

export const PORTAL_PLAN_PATH = "/portal/api/plan";
export const PORTAL_DIGEST_PATH = "/portal/api/digest";

/** GET /portal/api/plan: what an overnight run would work, per repo, and what it skips. */
export async function handlePortalPlan(request: Request, env: Env, now: Date = new Date()): Promise<Response> {
  const gate = await portalGate(request, env, now, OPS_RETURN_TO);
  if (!gate.ok) return gate.response;
  const { for_namespace: _all, ...plan } = await readOvernightPlan(env, {}, now);
  const body: PortalPlan = plan;
  return json(body);
}

/** GET /portal/api/digest?since=: what finished, blocked and is ready since then. */
export async function handlePortalDigest(request: Request, env: Env, now: Date = new Date()): Promise<Response> {
  const gate = await portalGate(request, env, now, OPS_RETURN_TO);
  if (!gate.ok) return gate.response;
  const since = digestSinceFrom(new URL(request.url).searchParams.get("since") ?? undefined, now);
  if (!since.ok) return new Response(since.refusal, { status: 400, headers: { "Content-Type": "text/plain; charset=utf-8" } });
  const body: PortalDigest = await readOvernightDigest(env, { since: since.since }, now);
  return json(body);
}