src/ops-types/views.ts
340 lines and 0 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
None: it only declares types or values.
Imports
It imports no other module of this repo. Imported by src/digest.ts, src/live-checks.ts, src/maintenance.ts, src/model-drift.ts, src/ops-cloudflare.ts, src/overnight-plan.ts, src/portal-convergence.ts, src/portal-library.ts, src/portal-maintenance.ts, src/portal-overnight.ts, src/portal-stale.ts, src/push.ts, src/rule-docs.ts, src/secret-presence.ts, src/site-convergence.ts, src/stale-jobs.ts, src/trends.ts.
Source
/** One Web Analytics site as the live checks read it. The site token is never kept. */
export interface WebAnalyticsSite {
host: string;
// Whether the snippet is injected automatically for the host's orange-clouded traffic.
auto_install: boolean | null;
// The site's ruleset switch, where Cloudflare reports one.
enabled: boolean | null;
}
// The daily email digest (src/digest.ts), as Settings shows it: on, or the reason it is
// off, where it goes, the hour it is sent, and the last send or refusal.
export interface DigestState {
on: boolean;
problem: string | null;
to: string | null;
hour_utc: number;
last: { at: string; outcome: "sent" | "failed"; note: string } | null;
}
// GET /portal/api/push -> PortalPush. Phone alerts (src/push.ts): the VAPID public key a
// browser subscribes with (null, with the problem, when the key is not set), and the
// devices with alerts on. Never an endpoint.
export interface PortalPush {
generated: string;
public_key: string | null;
problem: string | null;
devices: { id: number; label: string | null; created_at: string; last_sent_at: string | null; last_status: number | null }[];
// The daily email digest beside the alerts, so Settings says why it is off.
digest: DigestState;
// Present after op "test": each device's push service answer (0 when unreachable).
results?: { id: number; status: number }[];
}
// GET /portal/api/stale -> PortalStale. The jobs that look stuck, from the reader behind
// `capsid_work_jobs` action list with stale: true (src/stale-jobs.ts), over every namespace. rule is
// which test the job met, the most actionable first: prs-settled (blocked, and every pull
// request it names is merged or closed), steps-seen (blocked, and every manual step its
// "check:" lines name is seen, src/manual-steps.ts), resumed-not-completed (the
// merge-resume step resumed it 24 hours or more ago and it is not done), unchanged
// (blocked or claimed, and its row has not changed in 3 days).
export type StaleRule = "unchanged" | "resumed-not-completed" | "prs-settled" | "steps-seen";
export interface OpsStaleJob {
id: string;
namespace: string;
title: string;
status: string;
updated_at: string;
rule: StaleRule;
reason: string;
}
export interface PortalStale {
generated: string;
// Oldest change first, at most 200.
rows: OpsStaleJob[];
truncated: boolean;
// Set when rows may be missing: more than 200, or the pull request cache unreadable.
note: string | null;
}
// GET /portal/api/library -> PortalLibrary (src/portal-library.ts). The read-only Library
// over the knowledge store. One of
// three answers, by the query string: the shelves (no query), a search (?q=) or one
// document (?ns=&path=). Writing stays in chat and with drivers.
export type LibraryShelf = "decisions" | "rulings" | "research";
export interface LibraryEntry {
namespace: string;
path: string;
title: string | null;
type: string;
status: string;
updated_at: string;
}
interface LibraryHit extends LibraryEntry {
// FTS5's snippet: each matched word between U+0002 and U+0003 (LIBRARY_MARK_OPEN and
// LIBRARY_MARK_CLOSE in src/portal-library.ts), gaps as " ... ".
snippet: string;
}
interface LibraryDocument extends LibraryEntry {
tags: string | null;
created_at: string;
body: string;
}
export type PortalLibrary =
| { kind: "shelves"; generated: string; shelves: Record<LibraryShelf, LibraryEntry[]>; truncated: boolean }
| { kind: "search"; generated: string; query: string; rows: LibraryHit[]; truncated: boolean }
| {
kind: "document";
generated: string;
doc: LibraryDocument;
outgoing: Array<{ type: string; to_ns: string; to_path: string }>;
incoming: Array<{ type: string; from_ns: string; from_path: string }>;
};
// GET /portal/api/trends?weeks= -> PortalTrends (src/trends.ts, src/portal-trends.ts). The
// series the Portal's charts need and the feed does not carry (design-portal-evaluation.md
// section 3, ruled 2026-10-09). Read-only; every time is UTC; weeks start on Monday.
export interface TrendMinutesWeek {
week: string; // ISO week, "2026-W41"
start: string; // its Monday, YYYY-MM-DD
minutes: number;
sessions: number;
decisions: number;
}
export interface TrendQueueDay {
day: string;
posted: number;
ended: number;
// Open (queued, claimed, blocked or parked) at the day's end.
open: number;
}
export interface TrendMergeWeek {
week: string;
start: string;
auto: number; // merged by the signed policy on its own
approved: number; // merged by the executor on an approval
seat: number; // merged by the seat through capsid_close_pr
closed: number; // closed without merging, from chat or the Portal
}
export interface TrendCostWeek {
week: string;
start: string;
// Model spend of the jobs that ended that week, from their outcome rows' telemetry
// cost. Null when no outcome that week carried a cost, which is not a spend of zero.
usd: number | null;
// Outcome rows that carried a cost.
jobs: number;
}
export interface TrendBackupSlot {
key: string; // a day, a week's Monday, or a month (YYYY-MM)
filled: boolean;
}
export interface TrendCiDay {
day: string;
runs: number;
failed: number;
// The default branch at the day's end: the last completed run that day, carried over a
// day with none; "none" before the first run in the window.
state: "green" | "red" | "none";
}
export interface TrendCiRepo {
namespace: string;
repo: string;
days: TrendCiDay[];
// More runs than one page (100) in the window: the earliest days may be missing runs.
truncated: boolean;
}
export interface PortalTrends {
generated: string;
weeks: number;
minutes: { weeks: TrendMinutesWeek[]; truncated: boolean; note: string };
queue: TrendQueueDay[];
merges: { weeks: TrendMergeWeek[]; rollbacks: Array<{ at: string; repo: string; pr: number | null; outcome: "rolled_back" | "rollback_failed" }> };
// Visual 6: model spend per week (a read-only sum over job_outcomes.cost_usd).
cost: { weeks: TrendCostWeek[] };
// null slots and runs, with error set, when the bucket could not be listed.
backups: { slots: { daily: TrendBackupSlot[]; weekly: TrendBackupSlot[]; monthly: TrendBackupSlot[] } | null; runs: number | null; error: string | null };
// Read from GitHub and kept an hour; a repo that could not be read is in not_read.
ci: { read_at: string; repos: TrendCiRepo[]; not_read: Array<{ namespace: string; error: string }> };
}
// GET /portal/api/maintenance -> PortalMaintenance. The daily maintenance pass's list
// (src/maintenance.ts), as stored by its last run: what the pass found that it did not act
// on, and what it acted on (an auto-resume, a pruned branch). A "-not-checked" rule is a
// read that failed, shown so a missing read is never taken for a clean result.
export type MaintenanceRule =
| "later-passed"
| "shipped-elsewhere"
| "followups-missing"
| "auto-resumed"
| "pr-awaiting-seat"
| "pr-red"
| "prs-not-checked"
| "branch-merged"
| "branch-pruned"
| "branch-stale"
| "branches-not-checked"
| "undeployed-merge"
| "deploys-not-checked"
| "model-drift"
| "model-not-checked"
| "rule-doc"
| "rule-docs-not-checked"
// The release-tag rule (src/maintenance-releases.ts): a shared package's version tagged,
// left untagged (switched off, over the cap, not found), or below its newest tag.
| "release-tagged"
| "release-untagged"
| "release-backwards"
| "releases-not-checked";
export interface OpsMaintenanceItem {
/** Which rule found it. */
rule: MaintenanceRule;
namespace: string;
/** The job the line is about, or null for a line that names none. */
job: string | null;
/** The pull request the line is about, for the pull request rules. */
pr?: string;
/** One plain line for the seat: what is wrong and what to do. */
line: string;
}
export interface PortalMaintenance {
/** When the pass that wrote the list ran; null when no pass has run yet. */
generated: string | null;
items: OpsMaintenanceItem[];
/** What the pass read, so an empty list can be told from one that read nothing: open
* pull requests, branches and compared site deploys summed over the repos read, the
* repos read of the roster. */
read: { prs: number; branches: number; repos: number; roster: number; deploys: number };
}
// GET /portal/api/convergence -> PortalConvergence (src/site-convergence.ts). Per site
// that opted into Capsid calling its operator API: what that API's sync_status says, what
// its health route says per check with the repair each maps to, the site Worker's secrets
// by name (never a value), and the last repairs Capsid ran. Read live on each request.
export interface SecretPresence {
name: string;
set: boolean;
// false: the Worker has it, and the site's configuration does not name it.
expected: boolean;
}
export interface ConvergenceCheck {
name: string;
ok: boolean;
detail: string | null;
expected: number | null;
present: number | null;
// The repair the site's configuration maps this check to, and why Capsid may not run
// it, where it may not (null: it may).
repair: string | null;
repair_refusal: string | null;
}
export interface ConvergenceSite {
namespace: string;
name: string;
origin: string;
read_at: string;
// Null with `problem` set when the site has no usable operator configuration.
operator: { path: string; auth_var: string; secret_set: boolean; repairs: Record<string, string>; weekly: string[] } | null;
problem: string | null;
// sync_status, as label and text; error says why it was not read.
status: { ok: boolean; fields: Array<{ key: string; value: string }>; error: string | null } | null;
health: { ok: boolean; http_status: number | null; checks: ConvergenceCheck[]; error: string | null } | null;
// The watcher's last read of the names (read_at), or why there is none.
secrets: { state: "ok" | "none" | "error"; script: string | null; rows: SecretPresence[]; reason: string | null; read_at?: string } | null;
// Newest first.
recent: Array<{ at: string; actor: string; tool: string; via: string | null; converged: boolean | null; error: string | null }>;
}
export interface PortalConvergence {
generated: string;
sites: ConvergenceSite[];
}
// THE OVERNIGHT PLAN AND THE MORNING DIGEST (src/overnight-plan.ts): the `capsid_work_jobs` action list views
// "plan" and "digest", and the Portal's GET /portal/api/plan and GET /portal/api/digest,
// which serve the same readers.
interface PlannedJob {
id: string;
namespace: string;
title: string;
priority: number;
estimate_minutes: number;
estimate_source: "policy" | "record" | "default";
/** The job needs a human confirmation for a risky step (gate_required): the session
* does the ordinary work and lists that step in the pull request for the seat. */
gated: boolean;
}
export interface PlanLane {
repo: string;
namespaces: string[];
heavy: boolean;
jobs: PlannedJob[];
planned_minutes: number;
}
export interface SkippedJob {
id: string;
namespace: string;
title: string;
reason: string;
}
export interface OvernightPlan {
generated: string;
budget_minutes: number;
// Where the heavy/light split came from, so "every repo is heavy" is never a surprise.
policy: "document" | "default" | "invalid";
policy_note: string | null;
// Heavy lanes share this budget; their sessions run one at a time.
heavy_planned_minutes: number;
lanes: PlanLane[];
skipped: SkippedJob[];
// Counts beside the verdict, so an empty plan cannot pass for a clean one.
queued_read: number;
truncated: boolean;
}
export interface UsageTotals {
cost_usd: number | null;
active_seconds: number | null;
tokens_input: number | null;
tokens_output: number | null;
tokens_cache_read: number | null;
tokens_cache_creation: number | null;
}
export interface DigestJob {
id: string;
namespace: string;
title: string;
status: string;
result_kind: string | null;
duration_minutes: number | null;
blocked_count: number | null;
usage: UsageTotals;
usage_source: UsageSource;
}
export interface DigestBlocked {
id: string;
namespace: string;
title: string;
at: string;
// The reason and the command, as the driver wrote them, cut to a readable length.
summary: string;
pull_requests: string[];
usage: UsageTotals;
usage_source: UsageSource;
}
export interface DigestPr {
job_id: string;
namespace: string;
url: string;
// merged: GitHub says merged. open: not merged (open or closed). unchecked: never read.
state: "merged" | "open" | "unchecked";
source: "outcome" | "blocked summary";
}
export interface OvernightDigest {
generated: string;
since: string;
for_namespace: string | null;
finished: DigestJob[];
blocked: DigestBlocked[];
pull_requests_ready: DigestPr[];
pull_requests_merged: number;
// Per source, never one added to the other: a job in both would count twice.
totals: { telemetry: UsageTotals & { jobs: number }; reported: UsageTotals & { jobs: number }; jobs_without_usage: number };
truncated: string[];
}
export type UsageSource = "telemetry" | "reported" | "none";
// GET /portal/api/plan -> PortalPlan: the overnight plan over every namespace.
export type PortalPlan = OvernightPlan;
// GET /portal/api/digest?since= -> PortalDigest: what happened since then, over every
// namespace. since defaults to the last 24 hours; one that is not an ISO time is a 400.
export type PortalDigest = OvernightDigest;