carlos / platform Public

status: the viewer rides the binary — same-origin, unknowns loud

Task 4: embed index.html + status.css (assets/*) into the carlos binary
and have PublishFleet Put css, then html, then fleet.json — assets
before the JSON so a reader who polls fleet.json right after a publish
never finds a page missing its own stylesheet or markup. The inline
script fetches fleet.json relative, renders band words as text, and
turns every null into a titled em-dash rather than formatting a number
it never received. No http(s):// literal and no <script src> anywhere
in the shipped HTML — the incident-fallback URL is built from
`location` at view time instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 5bba0d97780c699545e49281eacdd09375d54675 parent ea4ddee
5 files changed, +406 −4
  • internal/status/assets/index.html +221 −0
  • internal/status/assets/status.css +108 −0
  • internal/status/fleet.go +25 −4
  • internal/status/page.go +15 −0
  • internal/status/status_test.go +37 −0
diff --git a/internal/status/assets/index.html b/internal/status/assets/index.html
new file mode 100644
index 000000000..acc47a06c
--- /dev/null
+++ b/internal/status/assets/index.html
@@ -0,0 +1,221 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Fleet status</title>
+<link rel="stylesheet" href="status.css">
+</head>
+<body>
+<header class="masthead">
+ <h1>Fleet status</h1>
+ <p class="generated" id="generated">Loading…</p>
+</header>
+<main>
+ <section class="tiles" aria-label="Fleet summary">
+ <div class="tile">
+ <h2>Instances</h2>
+ <p class="tile-value" id="tile-instances"></p>
+ <p class="tile-detail" id="tile-instances-detail"></p>
+ </div>
+ <div class="tile">
+ <h2>Capacity</h2>
+ <p class="tile-value" id="tile-capacity"></p>
+ </div>
+ <div class="tile">
+ <h2>Replication</h2>
+ <p class="tile-value" id="tile-replication"></p>
+ </div>
+ </section>
+
+ <section>
+ <h2>Boxes</h2>
+ <div class="tablewrap">
+ <table>
+ <thead>
+ <tr>
+ <th>Label</th><th>Region</th><th>Load</th>
+ <th>Static</th><th>Resident</th><th>Hibernating</th><th>Awake</th><th>Asleep</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody id="boxes-body"></tbody>
+ </table>
+ </div>
+ </section>
+
+ <section>
+ <h2>Apps</h2>
+ <div class="tablewrap">
+ <table>
+ <thead>
+ <tr><th>Name</th><th>Instances</th><th>Healthy</th><th>Channel</th><th>Version</th></tr>
+ </thead>
+ <tbody id="apps-body"></tbody>
+ </table>
+ </div>
+ <p class="small" id="apps-unlisted"></p>
+ </section>
+
+ <section>
+ <h2>Services</h2>
+ <div class="chips" id="services-chips"></div>
+ </section>
+</main>
+<footer>
+ <p>
+ This page publishes aggregate counts and status words only — never
+ hostnames, machine identifiers, or raw utilization figures; anything
+ this page doesn't know is shown as an em-dash, never guessed.
+ <span id="fallback"></span>
+ </p>
+</footer>
+<script>
+(function () {
+ "use strict";
+
+ // emDash renders an unknown value loudly rather than silently: a titled
+ // "—" a reader can hover, instead of a blank cell that could be
+ // misread as zero or a JS "null"/"undefined" string leaking onto the page.
+ function emDash() {
+ var el = document.createElement("span");
+ el.textContent = "—";
+ el.title = "unknown";
+ return el;
+ }
+
+ // asText turns a JSON value into a DOM node: known values become plain
+ // text (never innerHTML — box labels, app names, and regions are
+ // operator-supplied strings, and this keeps them inert no matter what
+ // they contain); null/undefined become emDash(). This is the one place
+ // that decides "did we receive this number" so no other renderer below
+ // ever has to guess or format a value it didn't get.
+ function asText(value) {
+ if (value === null || value === undefined) return emDash();
+ return document.createTextNode(String(value));
+ }
+
+ // asBool renders a tri-state boolean (true/false/unknown) as a word, never
+ // as JavaScript's own "true"/"false" — those read as code, not status.
+ function asBool(value, whenTrue, whenFalse) {
+ if (value === null || value === undefined) return emDash();
+ return document.createTextNode(value ? whenTrue : whenFalse);
+ }
+
+ function td(content) {
+ var el = document.createElement("td");
+ el.appendChild(content instanceof Node ? content : asText(content));
+ return el;
+ }
+
+ // routeCols is the fixed key order both the boxes table and the
+ // instances tile detail line walk — snake_case, matching fleet.json's
+ // routes/instances shape exactly (see the design spec §2).
+ var routeCols = ["static", "resident", "hibernating", "awake", "asleep"];
+
+ function renderTiles(f) {
+ var instances = f.instances || {};
+ document.getElementById("tile-instances").replaceChildren(asText(instances.total));
+
+ var detail = routeCols
+ .map(function (k) {
+ var v = instances[k];
+ return k + " " + (v === null || v === undefined ? "—" : v);
+ })
+ .join(" · ");
+ document.getElementById("tile-instances-detail").textContent = detail;
+
+ var capacity = f.capacity || {};
+ document.getElementById("tile-capacity").replaceChildren(asText(capacity.level));
+
+ var repl = f.replication || {};
+ document.getElementById("tile-replication").replaceChildren(asBool(repl.healthy, "healthy", "unhealthy"));
+ }
+
+ function renderBoxes(f) {
+ var body = document.getElementById("boxes-body");
+ (f.boxes || []).forEach(function (b) {
+ var tr = document.createElement("tr");
+ tr.appendChild(td(asText(b.label)));
+ tr.appendChild(td(asText(b.region)));
+ tr.appendChild(td(asText(b.load)));
+ var routes = b.routes || {};
+ routeCols.forEach(function (k) {
+ tr.appendChild(td(asText(routes[k])));
+ });
+ var staleTd = document.createElement("td");
+ if (b.stale) {
+ // Bordered, not filled — a "trust this less" flag, not an outage
+ // marker; see status.css's .chip-stale comment.
+ var chip = document.createElement("span");
+ chip.className = "chip chip-stale";
+ chip.textContent = "stale";
+ staleTd.appendChild(chip);
+ }
+ tr.appendChild(staleTd);
+ body.appendChild(tr);
+ });
+ }
+
+ function renderApps(f) {
+ var body = document.getElementById("apps-body");
+ // Only what fleet.json actually carries: an app that opted in ships
+ // exactly these five fields (see the design spec's apps opt-in rule) —
+ // this loop never reaches for a field the JSON doesn't have.
+ (f.apps || []).forEach(function (a) {
+ var tr = document.createElement("tr");
+ tr.appendChild(td(asText(a.name)));
+ tr.appendChild(td(asText(a.instances)));
+ tr.appendChild(td(asBool(a.healthy, "healthy", "unhealthy")));
+ tr.appendChild(td(asText(a.channel)));
+ tr.appendChild(td(asText(a.version)));
+ body.appendChild(tr);
+ });
+
+ var unlisted = document.getElementById("apps-unlisted");
+ if (typeof f.apps_unlisted === "number") {
+ unlisted.textContent =
+ f.apps_unlisted + " more app" + (f.apps_unlisted === 1 ? "" : "s") + " running, not publicly named.";
+ }
+ }
+
+ function renderServices(f) {
+ var chips = document.getElementById("services-chips");
+ (f.services || []).forEach(function (s) {
+ var chip = document.createElement("span");
+ chip.className = "chip " + (s.up ? "chip-up" : "chip-down");
+ chip.textContent = s.name + " " + (s.up ? "up" : "down");
+ chips.appendChild(chip);
+ });
+ }
+
+ // renderFallback builds the incident-fallback sentence from `location` at
+ // view time rather than a hardcoded absolute URL baked into this file —
+ // the whole point of the same-origin rule (design spec §3): this page
+ // must contain no http(s):// literal of its own, yet still needs to tell
+ // a reader where the raw bucket copy lives once the serving box is gone.
+ function renderFallback() {
+ var base = location.href.replace(/index\.html$/, "");
+ var url = base + "fleet.json";
+ document.getElementById("fallback").textContent =
+ "If this page is unreachable, the same data is published at " + url + ".";
+ }
+
+ fetch("fleet.json")
+ .then(function (res) { return res.json(); })
+ .then(function (f) {
+ document.getElementById("generated").textContent = f.generated_at ? "Generated " + f.generated_at : "";
+ renderTiles(f);
+ renderBoxes(f);
+ renderApps(f);
+ renderServices(f);
+ })
+ .catch(function () {
+ document.getElementById("generated").textContent = "Unable to load fleet.json.";
+ });
+
+ renderFallback();
+})();
+</script>
+</body>
+</html>
diff --git a/internal/status/assets/status.css b/internal/status/assets/status.css
new file mode 100644
index 000000000..d3dde40a0
--- /dev/null
+++ b/internal/status/assets/status.css
@@ -0,0 +1,108 @@
+/* status.css — the fleet status page's one stylesheet. Same palette shape as
+ the console's inline styles (CSS custom properties flipped by
+ prefers-color-scheme — see internal/console/templates/layout.html), kept
+ in its own small file here because this page ships from an embedded FS,
+ not the console's template set, and the design spec calls for exactly one
+ stylesheet, no frameworks. */
+
+:root {
+ --bg: #ffffff;
+ --ink: #1d2029;
+ --muted: #64707f;
+ --accent: #0f5e85;
+ --accent-soft: rgba(15, 94, 133, 0.09);
+ --rule: #e7e9f0;
+ --good: #2f7a4f;
+ --bad: #b3403a;
+ --warn: #a6790f;
+}
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #14151c;
+ --ink: #eef0f6;
+ --muted: #93a4ba;
+ --accent: #5db8dd;
+ --accent-soft: rgba(93, 184, 221, 0.12);
+ --rule: #2a2e38;
+ --good: #6fbf8d;
+ --bad: #e07b76;
+ --warn: #e0b95c;
+ }
+}
+
+* { margin: 0; padding: 0; box-sizing: border-box; }
+
+body {
+ background: var(--bg);
+ color: var(--ink);
+ font: 400 1rem/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
+ padding: 0 1.25rem 3rem;
+}
+
+header.masthead {
+ max-width: 56rem;
+ margin: 0 auto;
+ padding: 1.75rem 0 1rem;
+ border-bottom: 1px solid var(--rule);
+}
+header.masthead h1 { font-size: 1.6rem; }
+.generated { color: var(--muted); font-size: 0.85rem; margin-top: 0.35rem; }
+
+main { max-width: 56rem; margin: 0 auto; }
+h2 { font-size: 1.1rem; margin: 2rem 0 0.75rem; }
+
+.tiles { display: flex; flex-wrap: wrap; gap: 1rem; margin: 1.5rem 0; }
+.tile {
+ flex: 1 1 10rem;
+ background: var(--accent-soft);
+ border-radius: 0.6rem;
+ padding: 1rem 1.1rem;
+}
+.tile h2 {
+ margin: 0 0 0.35rem;
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--muted);
+ font-weight: 600;
+}
+.tile-value { font-size: 1.6rem; font-weight: 600; }
+.tile-detail { color: var(--muted); font-size: 0.8rem; margin-top: 0.25rem; }
+
+.tablewrap { overflow-x: auto; }
+table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
+th, td { padding: 0.5rem 0.9rem 0.5rem 0; border-bottom: 1px solid var(--rule); text-align: left; }
+th {
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--muted);
+ font-weight: 600;
+}
+
+/* .chip-stale is deliberately bordered-only (no fill) per the design spec:
+ staleness is a "trust this less" signal, not an outage — it should read
+ as a flag on the row, not as loud as the up/down service chips. */
+.chip {
+ display: inline-block;
+ font-size: 0.78rem;
+ font-weight: 600;
+ padding: 0.15rem 0.55rem;
+ border-radius: 999px;
+ margin: 0.15rem 0.3rem 0.15rem 0;
+}
+.chip-stale { border: 1px solid var(--warn); color: var(--warn); }
+.chip-up { background: var(--accent-soft); color: var(--good); }
+.chip-down { background: var(--accent-soft); color: var(--bad); }
+.chips { display: flex; flex-wrap: wrap; }
+
+.small { color: var(--muted); font-size: 0.85rem; margin-top: 0.5rem; }
+
+footer {
+ max-width: 56rem;
+ margin: 2.5rem auto 0;
+ padding-top: 1.25rem;
+ border-top: 1px solid var(--rule);
+ color: var(--muted);
+ font-size: 0.85rem;
+}
diff --git a/internal/status/fleet.go b/internal/status/fleet.go
index 2eb750047..cf1af6998 100644
--- a/internal/status/fleet.go
+++ b/internal/status/fleet.go
@@ -342,11 +342,32 @@ func buildServices(routes []registry.Route) []struct {
return services
}
-// PublishFleet writes fleet.json to status/. Page assets (index.html, the
-// stylesheet) are embedded and published alongside it starting in Task 4 —
-// this function's single Put is the seam that loop lands in; it isn't a
-// TODO so much as the honest scope of what this task builds.
+// pageAssets is the fixed publish order for assetsFS's contents: css before
+// html, both before fleet.json (see PublishFleet). It is NOT fs.ReadDir
+// order — "index.html" sorts alphabetically before "status.css", which
+// would flip the required css-before-html order — so the sequence is
+// spelled out explicitly rather than derived from a directory walk.
+var pageAssets = []string{"status.css", "index.html"}
+
+// PublishFleet writes the whole viewer: the embedded page assets first,
+// then fleet.json last. Order mirrors Eleven's upload-styles-first rule —
+// fleet.json is the object readers poll most often, so a reader who fetches
+// it the instant after a publish must never be able to load a page that
+// references a stylesheet or markup that hasn't landed yet. Every object
+// goes through putOverwrite: PublishFleet republishes all three keys on
+// every pass (every 5 minutes, per the plan), so a create-only Put would
+// fail from the second pass onward.
func PublishFleet(ctx context.Context, s release.Store, f *Fleet) error {
+ for _, name := range pageAssets {
+ body, err := assetsFS.ReadFile("assets/" + name)
+ if err != nil {
+ return err
+ }
+ if err := putOverwrite(ctx, s, "status/"+name, body); err != nil {
+ return err
+ }
+ }
+
body, err := json.MarshalIndent(f, "", " ")
if err != nil {
return err
diff --git a/internal/status/page.go b/internal/status/page.go
new file mode 100644
index 000000000..318373f98
--- /dev/null
+++ b/internal/status/page.go
@@ -0,0 +1,15 @@
+package status
+
+import "embed"
+
+// assetsFS embeds the fleet status page's viewer — index.html and
+// status.css — straight into the carlos binary. This is the "the viewer
+// rides the binary" half of the design (see the design spec §3): page
+// changes deploy with a binary release, data changes deploy every 5
+// minutes when the status-lease holder republishes fleet.json. Kept in its
+// own file (rather than fleet.go) because it's a pure asset declaration
+// with no logic of its own — PublishFleet in fleet.go is what actually
+// reads out of this FS and Puts each file to the bucket.
+//
+//go:embed assets/*
+var assetsFS embed.FS
diff --git a/internal/status/status_test.go b/internal/status/status_test.go
index 163f2de8b..c36769a44 100644
--- a/internal/status/status_test.go
+++ b/internal/status/status_test.go
@@ -383,6 +383,43 @@ func TestPublishFleetWritesAndOverwrites(t *testing.T) {
}
}
+// ---- Task 4: embedded viewer page -----------------------------------------
+
+// TestPublishWritesPageAssets proves PublishFleet ships the whole viewer —
+// fleet.json alongside the embedded index.html and status.css — and that the
+// page is same-origin only: no absolute http(s):// URL and no <script src>
+// anywhere in the shipped markup. (Simplified from the plan's step-1 text,
+// per the operator's ambiguity resolution: the footer's incident-fallback
+// URL is injected as text by inline JS from `location` at view time, never
+// baked into the HTML as a hardcoded absolute href, so a flat "no
+// http(s):// substring, no <script src" check is the honest same-origin
+// assertion here — no carve-out needed for a rel="me" link that doesn't
+// exist.)
+func TestPublishWritesPageAssets(t *testing.T) {
+ s := teststore(t)
+ f, err := status.BuildFleet(context.Background(), s, nil, time.Now(), 5*time.Minute)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := status.PublishFleet(context.Background(), s, f); err != nil {
+ t.Fatal(err)
+ }
+ for _, key := range []string{"status/fleet.json", "status/index.html", "status/status.css"} {
+ if _, _, err := s.Get(context.Background(), key); err != nil {
+ t.Errorf("%s not published: %v", key, err)
+ }
+ }
+ html, _, err := s.Get(context.Background(), "status/index.html")
+ if err != nil {
+ t.Fatalf("Get status/index.html: %v", err)
+ }
+ for _, banned := range []string{"http://", "https://", "<script src"} {
+ if strings.Contains(string(html), banned) {
+ t.Errorf("page must be same-origin only; found %q in index.html", banned)
+ }
+ }
+}
+
// TestWriteBoxReturnsErrorOnDoubleCASConflict proves WriteBox does NOT retry
// a second time: if the retried Put also loses the race, that error must
// reach the caller rather than being swallowed or retried forever.