| index acc47a06c..04bb6842b 100644 |
| --- a/internal/status/assets/index.html |
| +++ b/internal/status/assets/index.html |
| @@ -117,13 +117,18 @@ |
| 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; |
| + // Built as DOM nodes (not a joined string) so a null component renders |
| + // through asText/emDash — a titled "unknown" span, same as every other |
| + // null path on this page — rather than a bare "—" character that |
| + // carries no title for a reader to hover. |
| + var detailEl = document.getElementById("tile-instances-detail"); |
| + var detailNodes = []; |
| + routeCols.forEach(function (k, i) { |
| + if (i > 0) detailNodes.push(document.createTextNode(" · ")); |
| + detailNodes.push(document.createTextNode(k + " ")); |
| + detailNodes.push(asText(instances[k])); |
| + }); |
| + detailEl.replaceChildren.apply(detailEl, detailNodes); |
| |
| var capacity = f.capacity || {}; |
| document.getElementById("tile-capacity").replaceChildren(asText(capacity.level)); |
| @@ -189,16 +194,29 @@ |
| }); |
| } |
| |
| - // 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. |
| + // renderFallback builds the incident-fallback sentence — and its clickable |
| + // link — 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) is that this page must contain no http(s):// literal |
| + // of its own, yet it still needs to tell a reader, with a real <a>, where |
| + // the raw bucket copy lives once the serving box is gone. Built from |
| + // location.origin + location.pathname (not location.href) specifically so |
| + // a query string or hash on the viewed URL can never leak into the join. |
| 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 + "."; |
| + var dir = location.pathname.replace(/index\.html$/, ""); |
| + if (dir.charAt(dir.length - 1) !== "/") dir += "/"; // guard a root path with no trailing slash |
| + var url = location.origin + dir + "fleet.json"; |
| + |
| + var fallback = document.getElementById("fallback"); |
| + var link = document.createElement("a"); |
| + link.href = url; |
| + link.textContent = url; |
| + |
| + fallback.replaceChildren( |
| + document.createTextNode("If this page is unreachable, the same data is published at "), |
| + link, |
| + document.createTextNode(".") |
| + ); |
| } |
| |
| fetch("fleet.json") |