Skip to the content

Capsid menu

src/ops-feed.ts

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

Functions

The functions of ops-feed.ts
FunctionLinesComplexityCoveredCRAPMutation
portalCsrf81 to 893100%OkCRAP 3No data: mutation testing has not run on this repo yet
sqliteTime104 to 1041100%OkCRAP 1No data: mutation testing has not run on this repo yet
isoTime111 to 1143100%OkCRAP 3No data: mutation testing has not run on this repo yet
opsJobFrom148 to 17720100%WatchCRAP 20No data: mutation testing has not run on this repo yet
opsAgentFrom181 to 2001100%OkCRAP 1No data: mutation testing has not run on this repo yet
awaitingFrom205 to 215591%WatchCRAP 5No data: mutation testing has not run on this repo yet
refreshAllowedAt219 to 2254100%OkCRAP 4No data: mutation testing has not run on this repo yet
sizeFrom235 to 2384100%OkCRAP 4No data: mutation testing has not run on this repo yet
liveJobs240 to 26920%WatchCRAP 6No data: mutation testing has not run on this repo yet
<anonymous:241>241 to 24110%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:262>262 to 26540%WatchCRAP 20No data: mutation testing has not run on this repo yet
<anonymous:268>268 to 26810%OkCRAP 2No data: mutation testing has not run on this repo yet
livePrs271 to 28720%WatchCRAP 6No data: mutation testing has not run on this repo yet
<anonymous:279>279 to 28620%WatchCRAP 6No data: mutation testing has not run on this repo yet
opsSessionFrom295 to 3205100%OkCRAP 5No data: mutation testing has not run on this repo yet
liveSessions324 to 33520%WatchCRAP 6No data: mutation testing has not run on this repo yet
<anonymous:334>334 to 33410%OkCRAP 2No data: mutation testing has not run on this repo yet
liveMergePolicy339 to 34640%WatchCRAP 20No data: mutation testing has not run on this repo yet
opsLive348 to 38210%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:374>374 to 37410%OkCRAP 2No data: mutation testing has not run on this repo yet
scheduled390 to 39830%WatchCRAP 12No data: mutation testing has not run on this repo yet
opsFeed400 to 41238%WatchCRAP 10.1No data: mutation testing has not run on this repo yet
feedResponse414 to 4202100%OkCRAP 2No data: mutation testing has not run on this repo yet
handleOpsFeed430 to 4343100%OkCRAP 3No data: mutation testing has not run on this repo yet
handleOpsRefresh436 to 4951583%WatchCRAP 16.1No data: mutation testing has not run on this repo yet
<anonymous:473>473 to 4731100%OkCRAP 1No data: mutation testing has not run on this repo yet

Imports

It imports src/agents.ts, src/auth.ts, src/auto-merge-policy.ts, src/auto-merge-tick.ts, src/canon.ts, src/env.ts, src/http.ts, src/inbox.ts, src/job-signing.ts, src/jobs-holder.ts, src/jobs-schema.ts, src/log.ts, src/merge-safety.ts, src/ops-hooks.ts, src/ops-packages.ts, src/ops-sites.ts, src/ops-snapshot.ts, src/ops-types/feed.ts, src/overnight.ts, src/portal-auth.ts, src/portal-host.ts, src/roster.ts, src/status.ts, src/store-guards.ts, src/task-runs.ts, src/time.ts, src/watcher-digest.ts, src/watcher-gather.ts, src/watcher.ts. Imported by src/portal-actions.ts, src/portal-claims.ts, src/portal-convergence.ts, src/portal-library.ts, src/portal-maintenance.ts, src/portal-overnight.ts, src/portal-packages.ts, src/portal-stale.ts, src/portal-trends.ts, src/push.ts, src/routes.ts.

Source

import { gatherInbox } from "./inbox";
import { adminAgentForEmail } from "./agents";
import { getCookie } from "./auth";
import { AWAITING_SEAT_KEY } from "./auto-merge-tick";
import { loadMergePolicy } from "./auto-merge-policy";
import { readSafetyReport } from "./merge-safety";
import { PORTAL_CSRF_COOKIE, PORTAL_PREFIX, PORTAL_SESSION_TTL_SECONDS, type PortalUser, portalDisplay, portalGate, sourceAddress } from "./portal-auth";
import { portalCookiePath } from "./portal-host";
import type { Env } from "./env";
import { agentSummaries, type AgentSummary } from "./status";
import { APP_ROSTER } from "./roster";
import { commandFromSummary, isQuestionSummary, resumeMarkerIn } from "./jobs-holder";
import { checkJobText, type SignatureCheck } from "./job-signing";
import { OPEN_JOB_STATUSES } from "./jobs-schema";
import { readSiteConfig } from "./ops-sites";
import { readPackageConfig } from "./ops-packages";
import { readSnapshot } from "./ops-snapshot";
import { INCIDENT_FAILURES, NEEDS_INPUT_INCIDENT_MS } from "./ops-hooks";
import type { OpsAgent, OpsAwaitingSeat, OpsFeed, OpsJob, OpsJobStatus, OpsLive, OpsPr, OpsSession } from "./ops-types/feed";
import { overnightState } from "./overnight";
import { auditStatement } from "./store-guards";
import { readTaskRuns, taskStates, TASKS } from "./task-runs";
import { openQuietFindings } from "./watcher-digest";
import { DEFAULT_CADENCE_MINUTES, watcherTick, WATCHER_ACTOR, type Gathered, type WatcherReport } from "./watcher";
import { gatherFindings } from "./watcher-gather";
import { logEvent } from "./log";
import { pendingProposals } from "./canon";
import { textResponse } from "./http";
import { MINUTE_MS, DAY_MS } from "./time";

// The Watch Floor's one read (capsid/research/design-ops-console.md): GET
// /portal/api/ops returns OpsFeed (src/ops-types/feed.ts), the watcher's last pass from KV
// plus what changes between passes, read live. POST /portal/api/ops/refresh runs one
// watcher pass now and returns the new feed.
//
// Both answer to portalGate, as every Portal route does: the administrator's Access
// session and the ADMIN_EMAIL or CO_OWNER_EMAILS check on every request, and a 403 for any Authorization header.
// They are routes, not tools, so no grant is checked here
// (one enforcement point, test/route-gates.test.ts); src/scope.ts lists them among the routes gated some other way.
//
// READS PER FEED REQUEST, stated because the dashboard polls this and a per-namespace
// loop would multiply them. Asserted by test-integration/ops-feed.test.ts, which counts.
//   D1, 13 statements plus one per TASKS entry (9 today, so 22):
//     1  jobs: every open job and every job that ended in the last 24 hours
//     3  agentSummaries: the inventory, then loadRecordRows' two grouped reads
//     1  job_outcome_prs in the last 7 days
//     1  ops_sites: the site configuration, every row
//     1  ops_packages: the package configuration, every row
//     1  agent_sessions: live sessions from the hook receiver, at most 50
//     1  canon_proposals: the pending proposals joined to their documents, at most 50
//     9  task_runs: each scheduled task's newest runs, one keyed read per task in
//        one batch (src/task-runs.ts, TASKS)
//     1  watcher_findings: the open findings that post no job (src/watcher-digest.ts)
//     3  gatherInbox (src/inbox.ts): the blocked jobs, the applied migrations (a block
//        on a migration already applied is settled) and the site configuration again,
//        so Needs you counts exactly what GET /ops/inbox answers with
//   KV, 10 gets: gatherInbox's own
//     reads of the stale pull request cache (src/stale-jobs.ts), the manual steps cache
//     (src/manual-steps.ts), ops:snapshot, the awaiting-seat set and the merge units' report
//     (src/merge-units.ts), then ops:snapshot, the
//     awaiting-seat set, the auto-merge safety report (src/merge-safety.ts), the refresh stamp,
//     and the overnight switch's mode (its decision record is
//     read only while the mode is subscription).
// They run concurrently; the longest chain is agentSummaries' two steps.

export const OPS_FEED_PATH = "/portal/api/ops";
export const OPS_REFRESH_PATH = "/portal/api/ops/refresh";
// Where a sign-in started from one of these routes lands afterwards: the app.
export const OPS_RETURN_TO = PORTAL_PREFIX;

// D1 statements: one per task, and 14 more, one of them the signed policy's read (liveMergePolicy).
export const OPS_FEED_READS = { d1: 14 + TASKS.length, kv: 10 } as const;

// The Portal's double-submit CSRF cookie (OpsFeed.csrf), named in src/portal-auth.ts.
// Minted when absent or malformed and then left alone, never rotated per poll, so a
// preview and its perform carry one value.
export { PORTAL_CSRF_COOKIE };
const CSRF_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;

/** The request's Portal CSRF value, and the Set-Cookie that mints one when it has none. */
export function portalCsrf(request: Request): { value: string; setCookie: string | null } {
  const presented = getCookie(request, PORTAL_CSRF_COOKIE);
  if (presented !== null && CSRF_SHAPE.test(presented)) return { value: presented, setCookie: null };
  const value = crypto.randomUUID();
  return {
    value,
    setCookie: `${PORTAL_CSRF_COOKIE}=${value}; HttpOnly; Secure; SameSite=Lax; Path=${portalCookiePath(new URL(request.url))}; Max-Age=${PORTAL_SESSION_TTL_SECONDS}`,
  };
}

// The refresh's rate limit: one on-demand pass per two minutes, stamped in KV.
export const OPS_REFRESH_KEY = "ops:refresh:last";
const OPS_REFRESH_INTERVAL_MS = 2 * MINUTE_MS;
// A cross-site form cannot set a custom header, and a cross-site fetch that sets one
// is preflighted and refused, so its presence says the request came from this origin.
export const OPS_REFRESH_HEADER = "X-Capsid-Ops";
const OPS_REFRESH_HEADER_VALUE = "refresh";

const WEEK_MS = 7 * DAY_MS;
// The live-sessions read's bound.
const SESSION_ROWS_LIMIT = 50;

// D1's datetime('now') text form, for comparing against columns written by default.
const sqliteTime = (at: Date): string => at.toISOString().slice(0, 19).replace("T", " ");

/** An instant as ISO. Columns written by datetime('now') read "YYYY-MM-DD HH:MM:SS",
 *  which is UTC but which a browser's Date.parse reads as local time, so the feed
 *  hands every timestamp over in one unambiguous form. Anything else passes through. */
export function isoTime(value: string): string;
export function isoTime(value: string | null): string | null;
export function isoTime(value: string | null): string | null {
  if (value === null) return null;
  return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value) ? `${value.replace(" ", "T")}Z` : value;
}

export interface JobFeedRow {
  id: string;
  namespace: string;
  title: string;
  status: string;
  priority: number;
  posted_by: string;
  claimed_by: string | null;
  created_at: string;
  updated_at: string;
  blocked_count: number;
  resumed_count: number;
  gate_required: number;
  result_ref: string | null;
  result_summary: string | null;
  summary_sig?: string | null;
  // The started label (migrations/0042). Absent in a row read before it.
  started_by?: string | null;
  started_at?: string | null;
  // From the finding's watcher_findings row when this job is its current one
  // (src/watcher-findings.ts). Null, or absent, otherwise.
  finding_seen_count?: number | null;
  finding_last_seen?: string | null;
}

// The watcher's title carries its fingerprint in brackets at the end. The same
// pattern openWatcherFingerprints (src/watcher.ts) reads it with; test/ops-feed.test.ts
// runs both over one set of titles so the two cannot disagree.
const FINGERPRINT = /\[([^\]]+)\]\s*$/;

/** A job as the feed shows it. A blocked job's summary is split into what it waits on
 *  and the exact command (commandFromSummary), so the app can show the command to copy. */
export function opsJobFrom(row: JobFeedRow, commandSignature: SignatureCheck | null = null): OpsJob {
  const blocked = row.status === "blocked";
  const summary = row.result_summary;
  const marker = summary === null ? -1 : (resumeMarkerIn(summary)?.at ?? -1);
  const waitsOn = !blocked || summary === null ? null : (marker === -1 ? summary : summary.slice(0, marker)).trim() || null;
  const print = row.posted_by === WATCHER_ACTOR ? FINGERPRINT.exec(row.title) : null;
  return {
    id: row.id,
    namespace: row.namespace,
    title: row.title,
    status: row.status as OpsJobStatus,
    priority: row.priority,
    posted_by: row.posted_by,
    claimed_by: row.claimed_by,
    created_at: isoTime(row.created_at),
    updated_at: isoTime(row.updated_at),
    blocked_count: row.blocked_count,
    resumed_count: row.resumed_count,
    gate_required: row.gate_required === 1,
    waits_on: waitsOn,
    // A command whose signature does not match is withheld, so it cannot be copied.
    command: blocked && commandSignature !== "mismatch" ? commandFromSummary(summary) : null,
    command_signature: blocked ? commandSignature : null,
    question: blocked && commandSignature !== "mismatch" && isQuestionSummary(summary),
    ...(row.status === "parked" && summary !== null ? { parked_reason: summary } : {}),
    ...(row.started_by && row.started_at ? { started_by: row.started_by, started_at: isoTime(row.started_at) } : {}),
    result_ref: row.result_ref,
    finding: print ? { fingerprint: print[1], seen_count: row.finding_seen_count ?? null, last_seen: isoTime(row.finding_last_seen ?? null) } : null,
  };
}

/** One credential as the feed shows it: the inventory capsid_run_improve action status serves, with its
 *  record. Grants are left out; the contract carries flags only. */
export function opsAgentFrom(agent: AgentSummary): OpsAgent {
  const r = agent.record;
  return {
    name: agent.name,
    kind: agent.kind,
    namespaces: agent.namespaces,
    flags: agent.flags,
    last_seen: isoTime(agent.last_seen),
    revoked_at: isoTime(agent.revoked_at),
    archived_at: isoTime(agent.archived_at),
    jobs_done: r.jobs_done,
    jobs_failed: r.jobs_failed,
    jobs_blocked: r.jobs_blocked,
    prs_opened: r.prs_opened,
    prs_merged: r.prs_merged,
    pr_merge_rate: r.pr_merge_rate,
    ci_green_rate: r.ci_green_rate,
    median_duration_minutes: r.median_duration_minutes,
  };
}

/** The auto-merge tick's awaiting-seat set. A value that does not parse is logged and
 *  shown as empty, because the contract has no field to carry the reason; a KV read
 *  that fails throws and fails the feed. */
export function awaitingFrom(raw: string | null): OpsAwaitingSeat[] {
  if (raw === null) return [];
  try {
    const parsed: unknown = JSON.parse(raw);
    if (!Array.isArray(parsed)) throw new Error("not an array");
    return parsed as OpsAwaitingSeat[];
  } catch (err) {
    logEvent("error", "OPS_FEED_AWAITING_UNREADABLE", { message: `OPS_FEED_AWAITING_UNREADABLE ${AWAITING_SEAT_KEY}: ${err instanceof Error ? err.message : String(err)}` });
    return [];
  }
}

/** When the next on-demand pass is allowed, or null when one is allowed now. A stamp
 *  that does not parse allows one: the limiter's put writes a fresh stamp. */
export function refreshAllowedAt(last: string | null, now: Date): string | null {
  if (last === null) return null;
  const at = Date.parse(last);
  if (Number.isNaN(at)) return null;
  const next = at + OPS_REFRESH_INTERVAL_MS;
  return next > now.getTime() ? new Date(next).toISOString() : null;
}

// The store's size against D1's cap. Every D1 result carries the database's size in
// meta.size_after, so the jobs read below reports it and the feed makes no extra read
// (Foxhound reads it the same way: PRAGMA page_count is not authorized on D1). 10 GB is
// the per-database cap on Workers Paid (developers.cloudflare.com/d1/platform/limits);
// on Workers Free it is 500 MB, and this is the one line to change.
export const D1_CAP_BYTES = 10 * 1024 ** 3;

/** The size a D1 result reports, or null when its meta has no number for it. */
export function sizeFrom(meta: unknown): number | null {
  const size = (meta as { size_after?: unknown } | null | undefined)?.size_after;
  return typeof size === "number" && Number.isFinite(size) && size >= 0 ? size : null;
}

async function liveJobs(db: D1Database, now: Date, secret: string | undefined): Promise<{ jobs: OpsJob[]; sizeBytes: number | null }> {
  const open = OPEN_JOB_STATUSES.map((_, i) => `?${i + 2}`).join(", ");
  // datetime() on both sides: updated_at is written both as ISO and as D1's default
  // text form, and the two do not compare as text. The watcher's finding row joins
  // on the job it is currently filed as, so a watcher job shows how often its finding
  // has been seen; one row per fingerprint, so the join cannot multiply a job. A parked
  // job is not open (src/jobs-schema.ts) but is listed whatever its age, so it does not
  // vanish from the Queue a day after it was parked.
  const { results, meta } = await db
    .prepare(
      `SELECT j.id, j.namespace, j.title, j.status, j.priority, j.posted_by, j.claimed_by, j.created_at, j.updated_at,
              j.blocked_count, j.resumed_count, j.gate_required, j.result_ref, j.result_summary, j.summary_sig,
              j.started_by, j.started_at,
              wf.seen_count AS finding_seen_count, wf.last_seen_at AS finding_last_seen
       FROM jobs j LEFT JOIN watcher_findings wf ON wf.job_id = j.id
       WHERE j.status IN (${open}) OR j.status = 'parked' OR datetime(j.updated_at) >= datetime(?1)
       ORDER BY j.updated_at DESC`
    )
    .bind(new Date(now.getTime() - DAY_MS).toISOString(), ...OPEN_JOB_STATUSES)
    .all<JobFeedRow>();
  const rows = results ?? [];
  const checks = await Promise.all(
    rows.map((r) =>
      r.status === "blocked" && r.result_summary !== null
        ? checkJobText(secret, "summary", r.id, { summary: r.result_summary }, r.summary_sig ?? null)
        : Promise.resolve(null)
    )
  );
  return { jobs: rows.map((r, i) => opsJobFrom(r, checks[i])), sizeBytes: sizeFrom(meta) };
}

async function livePrs(db: D1Database, now: Date): Promise<OpsPr[]> {
  const { results } = await db
    .prepare(
      `SELECT job_id, pr_url, merged, merge_verified_at, recorded_at FROM job_outcome_prs
       WHERE recorded_at >= ?1 ORDER BY recorded_at DESC`
    )
    .bind(sqliteTime(new Date(now.getTime() - WEEK_MS)))
    .all<{ job_id: string; pr_url: string; merged: number | null; merge_verified_at: string | null; recorded_at: string }>();
  return (results ?? []).map((r) => ({
    job_id: r.job_id,
    pr_url: r.pr_url,
    // Three states, kept: null is never checked, not closed (migrations/0015).
    merged: r.merged === null ? null : r.merged === 1,
    merge_verified_at: isoTime(r.merge_verified_at),
    recorded_at: isoTime(r.recorded_at),
  }));
}

// The row as D1 holds it: needs_input is SQLite's 0 or 1, and the incident is decided
// when the feed is read.
export type SessionFeedRow = Omit<OpsSession, "needs_input" | "incident"> & { needs_input: number };

/** A session as the feed shows it, with its incident decided now: a failure a person
 *  must act on, or input awaited for more than ten minutes (src/ops-hooks.ts). */
export function opsSessionFrom(row: SessionFeedRow, now: Date): OpsSession {
  const needsInput = row.needs_input === 1;
  const waitedMs = now.getTime() - Date.parse(isoTime(row.last_event_at));
  const incident =
    row.last_failure !== null && INCIDENT_FAILURES.includes(row.last_failure)
      ? "failure"
      : needsInput && waitedMs > NEEDS_INPUT_INCIDENT_MS
        ? "waiting"
        : null;
  return {
    session_id: row.session_id,
    agent: row.agent,
    job_id: row.job_id,
    namespace: row.namespace,
    source: row.source,
    model: row.model,
    permission_mode: row.permission_mode,
    started_at: isoTime(row.started_at),
    last_event_at: isoTime(row.last_event_at),
    last_event: row.last_event,
    last_notification_type: row.last_notification_type,
    needs_input: needsInput,
    last_failure: row.last_failure,
    incident,
  };
}

// Through agent_sessions_live (ended_at, last_event_at): the NULL ended_at and the
// range on last_event_at are both on the index, which also gives the order.
async function liveSessions(db: D1Database, now: Date): Promise<OpsSession[]> {
  const { results } = await db
    .prepare(
      `SELECT session_id, agent, job_id, namespace, source, model, permission_mode, started_at, last_event_at, last_event,
              last_notification_type, needs_input, last_failure
       FROM agent_sessions WHERE ended_at IS NULL AND last_event_at >= ?1
       ORDER BY last_event_at DESC LIMIT ?2`
    )
    .bind(new Date(now.getTime() - DAY_MS).toISOString(), SESSION_ROWS_LIMIT)
    .all<SessionFeedRow>();
  return (results ?? []).map((row) => opsSessionFrom(row, now));
}

// The policy as the tick loads it. A throw is reported as not loading, not raised: one
// unreadable policy must not take the whole live feed down.
async function liveMergePolicy(env: Env): Promise<OpsLive["merge_policy"]> {
  try {
    const loaded = await loadMergePolicy(env);
    return "error" in loaded ? { loads: false, reason: loaded.error } : { loads: true, version: loaded.policy.version, enabled: loaded.policy.enabled };
  } catch (err) {
    return { loads: false, reason: `the policy could not be read: ${err instanceof Error ? err.message : String(err)}` };
  }
}

async function opsLive(env: Env, now: Date): Promise<OpsLive> {
  const [jobRead, agents, prs, awaitingRaw, mergeSafety, sites, sessions, packages, overnight, canonProposals, inbox, findings, mergePolicy] = await Promise.all([
    liveJobs(env.DB, now, env.IMPROVE_SCORE_SECRET),
    agentSummaries(env.DB),
    livePrs(env.DB, now),
    env.APP_KV.get(AWAITING_SEAT_KEY),
    readSafetyReport(env.APP_KV),
    readSiteConfig(env.DB),
    liveSessions(env.DB, now),
    readPackageConfig(env.DB),
    overnightState(env),
    pendingProposals(env.DB),
    gatherInbox(env, now, null),
    openQuietFindings(env.DB),
    liveMergePolicy(env),
  ]);
  return {
    generated: now.toISOString(),
    jobs: jobRead.jobs,
    agents: agents.map(opsAgentFrom),
    prs,
    awaiting_seat: awaitingFrom(awaitingRaw),
    merge_safety: mergeSafety,
    merge_policy: mergePolicy,
    overnight,
    sessions,
    namespaces: APP_ROSTER.map((name) => ({ name })),
    sites,
    packages,
    store: { size_bytes: jobRead.sizeBytes, cap_bytes: D1_CAP_BYTES },
    canon_proposals: canonProposals,
    inbox,
    findings,
  };
}

// The feed without its csrf, which comes off the request (portalCsrf) and is added by
// the handler, so opsFeed reads storage only.
export type OpsFeedData = Omit<OpsFeed, "csrf" | "user">;

/** The run ledger as the feed shows it. A failed read is said in the panel, not
 *  turned into a failure of the whole feed. */
async function scheduled(env: Env): Promise<{ read: Awaited<ReturnType<typeof readTaskRuns>> } | { error: string }> {
  try {
    return { read: await readTaskRuns(env.DB) };
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    logEvent("error", "OPS_FEED_TASK_RUNS_UNREADABLE", { message: `OPS_FEED_TASK_RUNS_UNREADABLE ${message}` });
    return { error: message };
  }
}

export async function opsFeed(env: Env, now: Date): Promise<OpsFeedData> {
  const [snapshot, live, last, runs] = await Promise.all([readSnapshot(env), opsLive(env, now), env.APP_KV.get(OPS_REFRESH_KEY), scheduled(env)]);
  return {
    snapshot,
    live,
    scheduled:
      "error" in runs
        ? { tasks: null, error: runs.error }
        : { tasks: taskStates(runs.read, now, snapshot?.cadence_min ?? DEFAULT_CADENCE_MINUTES), error: null },
    refresh_allowed_at: refreshAllowedAt(last, now),
    cloudflare_configured: Boolean(env.CF_OPS_TOKEN),
  };
}

function feedResponse(request: Request, user: PortalUser, data: OpsFeedData, extra: Record<string, string> = {}): Response {
  const csrf = portalCsrf(request);
  const feed: OpsFeed = { ...data, csrf: csrf.value, user: portalDisplay(user) };
  const headers = new Headers({ "Content-Type": "application/json", "Cache-Control": "no-store", ...extra });
  if (csrf.setCookie) headers.set("Set-Cookie", csrf.setCookie);
  return new Response(JSON.stringify(feed), { status: 200, headers });
}

// What the two handlers can be handed in a test in place of the live reads. The
// watcher pass itself is not injectable: the refresh always calls watcherTick with
// force, and only the gather under it is swapped.
export interface OpsDeps {
  feed?: (env: Env, now: Date) => Promise<OpsFeedData>;
  gather?: (env: Env, now: Date) => Promise<Gathered>;
}

export async function handleOpsFeed(request: Request, env: Env, now: Date = new Date(), deps: OpsDeps = {}): Promise<Response> {
  const gate = await portalGate(request, env, now, OPS_RETURN_TO);
  if (!gate.ok) return gate.response;
  return feedResponse(request, gate.user, await (deps.feed ?? opsFeed)(env, now));
}

export async function handleOpsRefresh(request: Request, env: Env, now: Date = new Date(), deps: OpsDeps = {}): Promise<Response> {
  const gate = await portalGate(request, env, now, OPS_RETURN_TO);
  if (!gate.ok) return gate.response;
  if (request.headers.get(OPS_REFRESH_HEADER) !== OPS_REFRESH_HEADER_VALUE) {
    return textResponse(`forbidden: a refresh must carry the header ${OPS_REFRESH_HEADER}: ${OPS_REFRESH_HEADER_VALUE}. The dashboard sends it; a cross-site form cannot.`, 403);
  }

  // The rate limit fails closed: a stamp that cannot be read or written refuses the
  // pass, because an unlimited Refresh is a button that runs every GitHub read the
  // watcher makes as often as it is clicked.
  let last: string | null;
  try {
    last = await env.APP_KV.get(OPS_REFRESH_KEY);
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    logEvent("error", "OPS_REFRESH_LIMIT_UNREADABLE", { message: `OPS_REFRESH_LIMIT_UNREADABLE ${message}` });
    return textResponse(`the refresh rate limit could not be read (${message}), so no pass was run. Try again shortly.`, 503);
  }
  const allowedAt = refreshAllowedAt(last, now);
  if (allowedAt !== null) {
    const seconds = Math.max(1, Math.ceil((Date.parse(allowedAt) - now.getTime()) / 1000));
    return textResponse(`a watcher pass was run on demand less than two minutes ago. The next is allowed at ${allowedAt}.`, 429, {
      "Retry-After": String(seconds),
    });
  }
  try {
    await env.APP_KV.put(OPS_REFRESH_KEY, now.toISOString());
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    logEvent("error", "OPS_REFRESH_LIMIT_UNWRITABLE", { message: `OPS_REFRESH_LIMIT_UNWRITABLE ${message}` });
    return textResponse(`the refresh rate limit could not be written (${message}), so no pass was run. Try again shortly.`, 503);
  }

  const actor = adminAgentForEmail(gate.user.email).actor;
  const gather = deps.gather ?? gatherFindings;
  let report: WatcherReport;
  try {
    report = await watcherTick(env, now, () => gather(env, now), { force: true });
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    logEvent("error", "OPS_REFRESH_PASS_FAILED", { message: `OPS_REFRESH_PASS_FAILED ${message}` });
    await env.DB.batch([auditStatement(env.DB, actor, "portal-ops-refresh", null, null, { ran: false, error: message, source_address: sourceAddress(request) })]);
    return textResponse(`the watcher pass failed: ${message}`, 500);
  }

  // The click's own audit row, as every Portal action writes one (src/portal-actions.ts).
  let warning: string | null = null;
  try {
    await env.DB.batch([
      auditStatement(env.DB, actor, "portal-ops-refresh", null, null, { ran: report.ran, note: report.note, posted: report.posted, cleared: report.cleared, source_address: sourceAddress(request) }),
    ]);
  } catch (err) {
    // The pass happened; only its audit row failed. Said in a header and the log, not
    // turned into a failure of a pass that ran.
    warning = `the pass ran, but the Portal audit row naming ${actor} was not written: ${err instanceof Error ? err.message : String(err)}`;
    logEvent("error", "OPS_REFRESH_AUDIT_FAILED", { message: warning });
  }
  const feed = await (deps.feed ?? opsFeed)(env, now);
  return feedResponse(request, gate.user, feed, warning ? { "X-Capsid-Warning": warning.replace(/[^\x20-\x7e]+/g, " ") } : {});
}