carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1<!doctype html>
2<html lang="en">
3<head>
4<meta charset="utf-8">
5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title>Fleet status</title>
7<link rel="stylesheet" href="status.css">
8</head>
9<body>
10<header class="masthead">
11 <h1>Fleet status</h1>
12 <p class="generated" id="generated">Loading…</p>
13</header>
14<main>
15 <section class="tiles" aria-label="Fleet summary">
16 <div class="tile">
17 <h2>Instances</h2>
18 <p class="tile-value" id="tile-instances"></p>
19 <p class="tile-detail" id="tile-instances-detail"></p>
20 </div>
21 <div class="tile">
22 <h2>Capacity</h2>
23 <p class="tile-value" id="tile-capacity"></p>
24 </div>
25 <div class="tile">
26 <h2>Replication</h2>
27 <p class="tile-value" id="tile-replication"></p>
28 </div>
29 <div class="tile">
30 <h2>Adoption</h2>
31 <p class="tile-value" id="tile-adoption"></p>
32 </div>
33 <div class="tile" id="tile-expired-canaries-wrap" hidden>
34 <h2>Expired canaries</h2>
35 <p class="tile-value" id="tile-expired-canaries"></p>
36 </div>
37 </section>
38
39 <section>
40 <h2>Boxes</h2>
41 <div class="tablewrap">
42 <table>
43 <thead>
44 <tr>
45 <th>Label</th><th>Region</th><th>Load</th>
46 <th>Static</th><th>Resident</th><th>Hibernating</th><th>Awake</th><th>Asleep</th>
47 <th>Adoption</th>
48 <th>Config sync</th>
49 <th></th>
50 </tr>
51 </thead>
52 <tbody id="boxes-body"></tbody>
53 </table>
54 </div>
55 </section>
56
57 <section>
58 <h2>Apps</h2>
59 <div class="tablewrap">
60 <table>
61 <thead>
62 <tr><th>Name</th><th>Instances</th><th>Healthy</th><th>Channel</th><th>Version</th></tr>
63 </thead>
64 <tbody id="apps-body"></tbody>
65 </table>
66 </div>
67 <p class="small" id="apps-unlisted"></p>
68 </section>
69
70 <section>
71 <h2>Services</h2>
72 <div class="chips" id="services-chips"></div>
73 </section>
74</main>
75<footer>
76 <p>
77 This page publishes aggregate counts and status words only — never
78 hostnames, machine identifiers, or raw utilization figures; anything
79 this page doesn't know is shown as an em-dash, never guessed.
80 <span id="fallback"></span>
81 </p>
82</footer>
83<script>
84(function () {
85 "use strict";
86
87 // emDash renders an unknown value loudly rather than silently: a titled
88 // "—" a reader can hover, instead of a blank cell that could be
89 // misread as zero or a JS "null"/"undefined" string leaking onto the page.
90 function emDash() {
91 var el = document.createElement("span");
92 el.textContent = "—";
93 el.title = "unknown";
94 return el;
95 }
96
97 // asText turns a JSON value into a DOM node: known values become plain
98 // text (never innerHTML — box labels, app names, and regions are
99 // operator-supplied strings, and this keeps them inert no matter what
100 // they contain); null/undefined become emDash(). This is the one place
101 // that decides "did we receive this number" so no other renderer below
102 // ever has to guess or format a value it didn't get.
103 function asText(value) {
104 if (value === null || value === undefined) return emDash();
105 return document.createTextNode(String(value));
106 }
107
108 // asBool renders a tri-state boolean (true/false/unknown) as a word, never
109 // as JavaScript's own "true"/"false" — those read as code, not status.
110 function asBool(value, whenTrue, whenFalse) {
111 if (value === null || value === undefined) return emDash();
112 return document.createTextNode(value ? whenTrue : whenFalse);
113 }
114
115 function td(content) {
116 var el = document.createElement("td");
117 el.appendChild(content instanceof Node ? content : asText(content));
118 return el;
119 }
120
121 // routeCols is the fixed key order both the boxes table and the
122 // instances tile detail line walk — snake_case, matching fleet.json's
123 // routes/instances shape exactly (see the design spec §2).
124 var routeCols = ["static", "resident", "hibernating", "awake", "asleep"];
125
126 function renderTiles(f) {
127 var instances = f.instances || {};
128 document.getElementById("tile-instances").replaceChildren(asText(instances.total));
129
130 // Built as DOM nodes (not a joined string) so a null component renders
131 // through asText/emDash — a titled "unknown" span, same as every other
132 // null path on this page — rather than a bare "—" character that
133 // carries no title for a reader to hover.
134 var detailEl = document.getElementById("tile-instances-detail");
135 var detailNodes = [];
136 routeCols.forEach(function (k, i) {
137 if (i > 0) detailNodes.push(document.createTextNode(" · "));
138 detailNodes.push(document.createTextNode(k + " "));
139 detailNodes.push(asText(instances[k]));
140 });
141 detailEl.replaceChildren.apply(detailEl, detailNodes);
142
143 var capacity = f.capacity || {};
144 document.getElementById("tile-capacity").replaceChildren(asText(capacity.level));
145
146 var repl = f.replication || {};
147 document.getElementById("tile-replication").replaceChildren(asBool(repl.healthy, "healthy", "unhealthy"));
148
149 // "not running" is the loud reading this tile exists for: a fleet whose
150 // adoption timers aren't armed is one where nothing picks up a release on
151 // its own, which used to be invisible from anywhere.
152 document.getElementById("tile-adoption").replaceChildren(asBool(f.adoption_running, "running", "not running"));
153
154 // The expired-canaries tile is deliberately absent, not zeroed, when
155 // there is nothing to see (pipelines spec §5): a fleet with no expired
156 // canaries — the ordinary, common case — should not carry a permanent
157 // "0" tile competing for attention with the three that always matter.
158 // Unknown (null) also stays hidden: "we couldn't check" is not the same
159 // claim as "there are some", and this tile only ever asserts the latter.
160 var wrap = document.getElementById("tile-expired-canaries-wrap");
161 if (typeof f.expired_canaries === "number" && f.expired_canaries > 0) {
162 wrap.hidden = false;
163 document.getElementById("tile-expired-canaries").replaceChildren(asText(f.expired_canaries));
164 } else {
165 wrap.hidden = true;
166 }
167 }
168
169 function renderBoxes(f) {
170 var body = document.getElementById("boxes-body");
171 (f.boxes || []).forEach(function (b) {
172 var tr = document.createElement("tr");
173 tr.appendChild(td(asText(b.label)));
174 tr.appendChild(td(asText(b.region)));
175 tr.appendChild(td(asText(b.load)));
176 var routes = b.routes || {};
177 routeCols.forEach(function (k) {
178 tr.appendChild(td(asText(routes[k])));
179 });
180 tr.appendChild(td(asBool(b.adoption_running, "running", "not running")));
181 // Config sync is a SEPARATE unit from adoption (carlos-adopt.path vs
182 // carlos-adopt.timer) and fails separately: a box can adopt releases
183 // fine while its unit-backed config quietly waits out the hour. Its
184 // own column, never folded into Adoption's word.
185 tr.appendChild(td(asBool(b.config_path_running, "armed", "not armed")));
186 var staleTd = document.createElement("td");
187 if (b.stale) {
188 // Bordered, not filled — a "trust this less" flag, not an outage
189 // marker; see status.css's .chip-stale comment.
190 var chip = document.createElement("span");
191 chip.className = "chip chip-stale";
192 chip.textContent = "stale";
193 staleTd.appendChild(chip);
194 }
195 tr.appendChild(staleTd);
196 body.appendChild(tr);
197 });
198 }
199
200 function renderApps(f) {
201 var body = document.getElementById("apps-body");
202 // Only what fleet.json actually carries: an app that opted in ships
203 // exactly these five fields (see the design spec's apps opt-in rule) —
204 // this loop never reaches for a field the JSON doesn't have.
205 (f.apps || []).forEach(function (a) {
206 var tr = document.createElement("tr");
207 tr.appendChild(td(asText(a.name)));
208 tr.appendChild(td(asText(a.instances)));
209 tr.appendChild(td(asBool(a.healthy, "healthy", "unhealthy")));
210 tr.appendChild(td(asText(a.channel)));
211 tr.appendChild(td(asText(a.version)));
212 body.appendChild(tr);
213 });
214
215 var unlisted = document.getElementById("apps-unlisted");
216 if (typeof f.apps_unlisted === "number") {
217 unlisted.textContent =
218 f.apps_unlisted + " more app" + (f.apps_unlisted === 1 ? "" : "s") + " running, not publicly named.";
219 }
220 }
221
222 function renderServices(f) {
223 var chips = document.getElementById("services-chips");
224 (f.services || []).forEach(function (s) {
225 var chip = document.createElement("span");
226 chip.className = "chip " + (s.up ? "chip-up" : "chip-down");
227 chip.textContent = s.name + " " + (s.up ? "up" : "down");
228 chips.appendChild(chip);
229 });
230 }
231
232 // renderFallback builds the incident-fallback sentence — and its clickable
233 // link — from `location` at view time rather than a hardcoded absolute
234 // URL baked into this file: the whole point of the same-origin rule
235 // (design spec §3) is that this page must contain no http(s):// literal
236 // of its own, yet it still needs to tell a reader, with a real <a>, where
237 // the raw bucket copy lives once the serving box is gone. Built from
238 // location.origin + location.pathname (not location.href) specifically so
239 // a query string or hash on the viewed URL can never leak into the join.
240 function renderFallback() {
241 var dir = location.pathname.replace(/index\.html$/, "");
242 if (dir.charAt(dir.length - 1) !== "/") dir += "/"; // guard a root path with no trailing slash
243 var url = location.origin + dir + "fleet.json";
244
245 var fallback = document.getElementById("fallback");
246 var link = document.createElement("a");
247 link.href = url;
248 link.textContent = url;
249
250 fallback.replaceChildren(
251 document.createTextNode("If this page is unreachable, the same data is published at "),
252 link,
253 document.createTextNode(".")
254 );
255 }
256
257 fetch("fleet.json")
258 .then(function (res) { return res.json(); })
259 .then(function (f) {
260 document.getElementById("generated").textContent = f.generated_at ? "Generated " + f.generated_at : "";
261 renderTiles(f);
262 renderBoxes(f);
263 renderApps(f);
264 renderServices(f);
265 })
266 .catch(function () {
267 document.getElementById("generated").textContent = "Unable to load fleet.json.";
268 });
269
270 renderFallback();
271})();
272</script>
273</body>
274</html>
275