feat(lanes): lane runtime UI, docs, and env additions (A1+A2)
Client-side rendering for the per-lane runtime facts (slot, ports, database, Redis index, service liveness) added in the server-side A1/A2 work, plus the doc updates (README, CLAUDE.md, docs/API.md, client/server READMEs) describing the new profile.env keys, hook environment contract, and REST endpoints.
This commit is contained in:
@@ -392,6 +392,7 @@ import type {
|
||||
Lane,
|
||||
LaneCounts,
|
||||
LaneGitFacts,
|
||||
LaneRuntime,
|
||||
ModelPricing,
|
||||
Session,
|
||||
SessionDrillIn,
|
||||
@@ -1932,6 +1933,40 @@ export const api = {
|
||||
* readable git repository, which is a normal state rather than an error.
|
||||
*/
|
||||
git: (id: number) => request<LaneGitFacts>(`/lanes/${id}/git`),
|
||||
/**
|
||||
* GET /api/lanes/:id/runtime — the lane's own application stack: slot,
|
||||
* ports, service liveness, and the last boot error.
|
||||
*
|
||||
* Its own endpoint rather than a field on the polled lane list because it
|
||||
* probes ports and stats pid files. Like `/git`, a lane with no profile
|
||||
* answers `available: false` with HTTP 200 — a normal state, not a fault.
|
||||
* @param id The lane id.
|
||||
* @returns {@link LaneRuntime}
|
||||
*/
|
||||
runtime: (id: number) => request<LaneRuntime>(`/lanes/${id}/runtime`),
|
||||
/**
|
||||
* POST /api/lanes/:id/up — boot the lane's stack through its profile hooks.
|
||||
* Returns 202: the boot runs in the background and streams progress as
|
||||
* `lane_hook_output`, finishing with a `lane_runtime` message.
|
||||
* @param id The lane id.
|
||||
* @param body `build: false` reuses an existing build.
|
||||
*/
|
||||
up: (id: number, body: { build?: boolean } = {}) =>
|
||||
request<{ ok: true; laneId: number }>(`/lanes/${id}/up`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
/**
|
||||
* POST /api/lanes/:id/down — stop the lane's stack. Idempotent, and a no-op
|
||||
* for a lane that was never brought up.
|
||||
* @param id The lane id.
|
||||
* @returns The pids stopped and the resulting runtime facts.
|
||||
*/
|
||||
down: (id: number) =>
|
||||
request<{ ok: true; killed: number[]; runtime: LaneRuntime }>(`/lanes/${id}/down`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
}),
|
||||
/**
|
||||
* POST /api/lanes — create a new lane.
|
||||
* @param body Optional lane initialization fields.
|
||||
|
||||
+88
-2
@@ -1671,7 +1671,13 @@ export interface WSMessage {
|
||||
| "workflow_upserted"
|
||||
| "remote_source.status"
|
||||
| "remote_data.updated"
|
||||
| "lane_update";
|
||||
| "lane_update"
|
||||
/** One output line from a lane's profile hook, while it runs. */
|
||||
| "lane_hook_output"
|
||||
/** A lane's stack finished coming up (or failed to), with fresh facts. */
|
||||
| "lane_runtime"
|
||||
/** A lane's profile hook exited, with its status code. */
|
||||
| "lane_hook_result";
|
||||
/** The message body, whose concrete shape is selected by `type` above. */
|
||||
data:
|
||||
| Session
|
||||
@@ -1687,7 +1693,10 @@ export interface WSMessage {
|
||||
| WorkflowRun
|
||||
| RemoteSourceStatusPayload
|
||||
| RemoteDataUpdatedPayload
|
||||
| { lane?: Lane; removed?: number };
|
||||
| { lane?: Lane; removed?: number }
|
||||
| LaneHookOutputPayload
|
||||
| LaneRuntimePayload
|
||||
| LaneHookResultPayload;
|
||||
/** ISO timestamp the server broadcast this message (not necessarily the
|
||||
* same instant the underlying event occurred). */
|
||||
timestamp: string;
|
||||
@@ -2312,8 +2321,85 @@ export interface Lane {
|
||||
/** The tool-event signal that produced `detected_stage` (e.g. a command
|
||||
* name), capped at 120 characters server-side; null if `detected_stage` is null. */
|
||||
detected_signal: string | null;
|
||||
/** Runtime slot, or null until the lane's stack is first brought up. Every
|
||||
* per-lane runtime fact (ports today, database name later) derives from it. */
|
||||
slot: number | null;
|
||||
/** Ports the lane ACTUALLY bound, by declared name. Empty until first boot.
|
||||
* May differ from `base + slot` when the preferred number was taken. */
|
||||
ports: Record<string, number>;
|
||||
}
|
||||
|
||||
/** One line a lane's profile hook wrote, pushed while the hook is still running
|
||||
* so a multi-minute boot shows progress instead of reading as a hang. */
|
||||
export interface LaneHookOutputPayload {
|
||||
laneId: number;
|
||||
/** Hook that produced it — `"up"` for the boot/health pair driven by `POST /up`. */
|
||||
hook: string;
|
||||
stream: "stdout" | "stderr";
|
||||
line: string;
|
||||
}
|
||||
|
||||
/** A lane's stack finished coming up, or failed to. Carries the fresh facts so a
|
||||
* listener does not have to re-request them. */
|
||||
export interface LaneRuntimePayload {
|
||||
laneId: number;
|
||||
runtime?: LaneRuntime;
|
||||
error?: { code: string; message: string };
|
||||
}
|
||||
|
||||
/** A lane's profile hook exited. `code` is null when it could not be started. */
|
||||
export interface LaneHookResultPayload {
|
||||
laneId: number;
|
||||
hook: string;
|
||||
code: number | null;
|
||||
error?: { code: string; message: string };
|
||||
}
|
||||
|
||||
/** One declared port: the number in use, the number the base implies, and whether
|
||||
* anything is answering on it right now. */
|
||||
export interface LaneRuntimePort {
|
||||
port: number | null;
|
||||
expected: number;
|
||||
listening: boolean;
|
||||
}
|
||||
|
||||
/** A service the boot hook started, as recorded in its pid file. */
|
||||
export interface LaneRuntimeService {
|
||||
name: string;
|
||||
pid: number;
|
||||
alive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is actually running for a lane, recomputed by the server on every read
|
||||
* rather than cached — a process can die without telling anyone.
|
||||
*
|
||||
* `available: false` means the lane has no `.ccam/profile`, which is a normal
|
||||
* state (most lanes never run a stack), not an error.
|
||||
*/
|
||||
export type LaneRuntime =
|
||||
| { available: false; searched?: string[] }
|
||||
| { available: true; provisioned: false; hooks: string[]; ports: Record<string, never> }
|
||||
| {
|
||||
available: true;
|
||||
provisioned: true;
|
||||
slot: number;
|
||||
kind: Lane["kind"];
|
||||
hooks: string[];
|
||||
profileDir: string;
|
||||
services: LaneRuntimeService[];
|
||||
ports: Record<string, LaneRuntimePort>;
|
||||
/** True when any port differs from `base + slot`; the card flags it. */
|
||||
steppedAside: boolean;
|
||||
/** Any recorded service process is alive. */
|
||||
up: boolean;
|
||||
/** Every declared port is answering. */
|
||||
healthy: boolean;
|
||||
logs: string[];
|
||||
logDir: string;
|
||||
lastError: { at: string; code: string | null; message: string } | null;
|
||||
};
|
||||
|
||||
/** Facts returned before a reset or remove that must be echoed to the server. */
|
||||
export interface LaneWorktreePreflight {
|
||||
action: "reset" | "remove";
|
||||
|
||||
Reference in New Issue
Block a user