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("&", "&")
329 .replaceAll("<", "<")
330 .replaceAll(">", ">")
331 .replaceAll('"', """)
332 .replaceAll("'", "'");
333}