baa-conductor


baa-conductor / apps / status-api / src
im_wower  ·  2026-03-26

render.ts

  1import type { StatusSnapshot } from "./contracts.js";
  2
  3const DEFAULT_STATUS_JSON_PATH = "/v1/status";
  4const DEFAULT_STATUS_HTML_PATHS = ["/", "/v1/status/ui"] as const;
  5
  6export interface StatusPageRenderOptions {
  7  htmlPaths?: readonly string[];
  8  jsonPath?: string;
  9}
 10
 11export function renderStatusPage(
 12  snapshot: StatusSnapshot,
 13  options: StatusPageRenderOptions = {}
 14): string {
 15  const modeLabel = formatMode(snapshot.mode);
 16  const leaderLabel = snapshot.leaderHost ?? snapshot.leaderId ?? "No active leader lease";
 17  const leaderDetail = snapshot.leaderId == null ? "Truth source did not report an active holder." : `holder_id=${snapshot.leaderId}`;
 18  const leaseLabel = snapshot.leaseExpiresAt == null ? "No lease expiry" : formatTimestamp(snapshot.leaseExpiresAt);
 19  const leaseDetail = snapshot.leaseActive ? "Lease is currently valid." : "Lease is missing or stale.";
 20  const jsonPath = normalizeStatusPath(options.jsonPath, DEFAULT_STATUS_JSON_PATH);
 21  const htmlPaths = normalizeStatusPathList(options.htmlPaths, DEFAULT_STATUS_HTML_PATHS);
 22
 23  return `<!doctype html>
 24<html lang="en">
 25  <head>
 26    <meta charset="utf-8" />
 27    <meta name="viewport" content="width=device-width, initial-scale=1" />
 28    <title>BAA Conductor Status</title>
 29    <style>
 30      :root {
 31        color-scheme: light;
 32        --bg: #f4ead7;
 33        --bg-deep: #e8d7b8;
 34        --panel: rgba(255, 250, 241, 0.88);
 35        --panel-strong: #fff7ea;
 36        --ink: #1e1a15;
 37        --muted: #6d6155;
 38        --line: rgba(30, 26, 21, 0.12);
 39        --accent: #005f73;
 40        --accent-soft: rgba(0, 95, 115, 0.12);
 41        --success: #2d6a4f;
 42        --warning: #9a3412;
 43        --shadow: 0 20px 60px rgba(83, 62, 35, 0.14);
 44      }
 45
 46      * {
 47        box-sizing: border-box;
 48      }
 49
 50      body {
 51        margin: 0;
 52        min-height: 100vh;
 53        font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
 54        color: var(--ink);
 55        background:
 56          radial-gradient(circle at top left, rgba(255, 255, 255, 0.55), transparent 38%),
 57          radial-gradient(circle at bottom right, rgba(0, 95, 115, 0.08), transparent 28%),
 58          linear-gradient(135deg, var(--bg), var(--bg-deep));
 59      }
 60
 61      body::before {
 62        content: "";
 63        position: fixed;
 64        inset: 0;
 65        pointer-events: none;
 66        background-image:
 67          linear-gradient(rgba(30, 26, 21, 0.03) 1px, transparent 1px),
 68          linear-gradient(90deg, rgba(30, 26, 21, 0.03) 1px, transparent 1px);
 69        background-size: 26px 26px;
 70        mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), transparent 85%);
 71      }
 72
 73      main {
 74        width: min(1040px, calc(100% - 32px));
 75        margin: 0 auto;
 76        padding: 48px 0 56px;
 77      }
 78
 79      .hero,
 80      .card,
 81      .footer {
 82        backdrop-filter: blur(14px);
 83        background: var(--panel);
 84        border: 1px solid var(--line);
 85        box-shadow: var(--shadow);
 86      }
 87
 88      .hero {
 89        padding: 28px;
 90        border-radius: 28px;
 91        margin-bottom: 18px;
 92      }
 93
 94      .eyebrow,
 95      .detail,
 96      .meta {
 97        font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;
 98      }
 99
100      .eyebrow {
101        margin: 0 0 14px;
102        color: var(--accent);
103        text-transform: uppercase;
104        letter-spacing: 0.16em;
105        font-size: 12px;
106      }
107
108      h1 {
109        margin: 0;
110        font-size: clamp(34px, 5vw, 62px);
111        line-height: 0.96;
112        max-width: 12ch;
113      }
114
115      .hero-copy {
116        margin: 16px 0 0;
117        max-width: 56ch;
118        color: var(--muted);
119        font-size: 18px;
120        line-height: 1.55;
121      }
122
123      .hero-strip {
124        display: flex;
125        flex-wrap: wrap;
126        gap: 10px;
127        margin-top: 22px;
128      }
129
130      .pill {
131        display: inline-flex;
132        align-items: center;
133        gap: 8px;
134        padding: 10px 14px;
135        border-radius: 999px;
136        font-family: "IBM Plex Mono", "SFMono-Regular", Menlo, monospace;
137        font-size: 13px;
138        background: var(--panel-strong);
139        border: 1px solid var(--line);
140      }
141
142      .pill::before {
143        content: "";
144        width: 9px;
145        height: 9px;
146        border-radius: 50%;
147        background: var(--accent);
148      }
149
150      .pill.running::before {
151        background: var(--success);
152      }
153
154      .pill.draining::before {
155        background: var(--warning);
156      }
157
158      .pill.paused::before {
159        background: var(--muted);
160      }
161
162      .grid {
163        display: grid;
164        grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
165        gap: 16px;
166      }
167
168      .card {
169        border-radius: 22px;
170        padding: 20px;
171      }
172
173      .label {
174        margin: 0 0 18px;
175        color: var(--muted);
176        font-size: 14px;
177        letter-spacing: 0.08em;
178        text-transform: uppercase;
179      }
180
181      .value {
182        margin: 0;
183        font-size: clamp(30px, 4vw, 42px);
184        line-height: 1.05;
185      }
186
187      .detail {
188        margin: 12px 0 0;
189        color: var(--muted);
190        font-size: 13px;
191        line-height: 1.55;
192        word-break: break-word;
193      }
194
195      .footer {
196        margin-top: 18px;
197        border-radius: 22px;
198        padding: 18px 20px;
199        display: grid;
200        gap: 8px;
201      }
202
203      .meta {
204        margin: 0;
205        font-size: 13px;
206        color: var(--muted);
207      }
208
209      .accent-panel {
210        background:
211          linear-gradient(160deg, var(--accent-soft), rgba(255, 255, 255, 0)),
212          var(--panel);
213      }
214
215      @media (max-width: 640px) {
216        main {
217          width: min(100% - 20px, 1040px);
218          padding-top: 20px;
219          padding-bottom: 28px;
220        }
221
222        .hero,
223        .card,
224        .footer {
225          border-radius: 20px;
226        }
227      }
228    </style>
229  </head>
230  <body>
231    <main>
232      <section class="hero">
233        <p class="eyebrow">BAA Conductor / Status Surface</p>
234        <h1>Readable automation state for people and browser controls.</h1>
235        <p class="hero-copy">
236          This page is the minimal control-facing surface for the conductor stack. It exposes the four fields the browser
237          panel needs first: mode, leader, queue depth, and active runs.
238        </p>
239        <div class="hero-strip">
240          <span class="pill ${escapeHtml(snapshot.mode)}">${escapeHtml(modeLabel)}</span>
241          <span class="pill">${escapeHtml(snapshot.source.toUpperCase())} snapshot</span>
242          <span class="pill">${escapeHtml(formatTimestamp(snapshot.observedAt))}</span>
243        </div>
244      </section>
245
246      <section class="grid" aria-label="status metrics">
247        ${renderMetricCard("Mode", modeLabel, describeMode(snapshot.mode), true)}
248        ${renderMetricCard("Leader", leaderLabel, leaderDetail)}
249        ${renderMetricCard("Queue Depth", String(snapshot.queueDepth), "Queued tasks reported by the current truth source.")}
250        ${renderMetricCard("Active Runs", String(snapshot.activeRuns), "Active runs reported by the current truth source.")}
251        ${renderMetricCard("Lease Expiry", leaseLabel, leaseDetail)}
252        ${renderMetricCard("Snapshot Updated", formatTimestamp(snapshot.updatedAt), "Latest timestamp reported by the truth source, or the observation time when none is exposed.")}
253      </section>
254
255      <section class="footer">
256        <p class="meta">JSON endpoint: ${renderEndpointList([jsonPath])}</p>
257        <p class="meta">HTML endpoint: ${renderEndpointList(htmlPaths)}</p>
258        <p class="meta">Observed at: ${escapeHtml(formatTimestamp(snapshot.observedAt))}</p>
259      </section>
260    </main>
261  </body>
262</html>`;
263}
264
265function normalizeStatusPath(value: string | undefined, fallback: string): string {
266  const normalized = value?.trim();
267
268  return normalized == null || normalized === "" ? fallback : normalized;
269}
270
271function normalizeStatusPathList(
272  value: readonly string[] | undefined,
273  fallback: readonly string[]
274): string[] {
275  const normalized = value
276    ?.map((entry) => entry.trim())
277    .filter((entry) => entry !== "");
278
279  return normalized == null || normalized.length === 0 ? [...fallback] : normalized;
280}
281
282function renderEndpointList(paths: readonly string[]): string {
283  return paths.map((path) => `<strong>${escapeHtml(path)}</strong>`).join(" or ");
284}
285
286function renderMetricCard(label: string, value: string, detail: string, accent = false): string {
287  return `<article class="card${accent ? " accent-panel" : ""}">
288    <p class="label">${escapeHtml(label)}</p>
289    <p class="value">${escapeHtml(value)}</p>
290    <p class="detail">${escapeHtml(detail)}</p>
291  </article>`;
292}
293
294function describeMode(mode: StatusSnapshot["mode"]): string {
295  switch (mode) {
296    case "running":
297      return "Workers may claim new work and continue normal scheduling.";
298    case "draining":
299      return "Existing work continues, but the leader should stop launching new steps.";
300    case "paused":
301      return "Automation is paused until a control-plane resume action is issued.";
302  }
303}
304
305function formatMode(mode: StatusSnapshot["mode"]): string {
306  switch (mode) {
307    case "running":
308      return "Running";
309    case "draining":
310      return "Draining";
311    case "paused":
312      return "Paused";
313  }
314}
315
316function formatTimestamp(value: string): string {
317  const date = new Date(value);
318
319  if (Number.isNaN(date.getTime())) {
320    return value;
321  }
322
323  return date.toISOString().replace(".000Z", "Z");
324}
325
326function escapeHtml(value: string): string {
327  return value
328    .replaceAll("&", "&amp;")
329    .replaceAll("<", "&lt;")
330    .replaceAll(">", "&gt;")
331    .replaceAll('"', "&quot;")
332    .replaceAll("'", "&#39;");
333}