ops/dossiers-ngi: cascade bar dates, center/mobile, soft refresh; serve /cascade

This commit is contained in:
Hernâni Marques 2026-09-30 15:54:46 +02:00
parent 737d37a76d
commit 84623b7c9a
No known key found for this signature in database
6 changed files with 391 additions and 110 deletions

View file

@ -2,8 +2,13 @@
position: sticky;
top: 0;
z-index: 10;
margin: -1.5rem -1.5rem 1rem;
padding: 0.55rem 1.5rem 0.65rem;
display: flex;
justify-content: center;
box-sizing: border-box;
width: 100vw;
max-width: 100vw;
margin: -1.25rem calc(50% - 50vw) 1rem;
padding: 0.55rem 0.75rem 0.65rem;
background: #10141a;
border-bottom: 1px solid #243040;
color: #c8d0d8;
@ -11,9 +16,16 @@
font-size: 12px;
line-height: 1.45;
}
.cascade-tree {
width: 100%;
max-width: 45em;
margin: 0 auto;
box-sizing: border-box;
}
.cascade-tree-lines {
white-space: pre;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
a.cascade-tree-row {
display: block;
@ -34,9 +46,13 @@ a.cascade-tree-row.is-active {
a.cascade-tree-row.is-active:hover {
text-decoration: none;
}
.cascade-bar code { color: inherit; font-size: inherit; }
.cascade-bar code {
color: inherit;
font-size: inherit;
}
.cascade-bar .tip {
color: #8a96a3;
white-space: pre-wrap;
}
.cascade-bar .cascade-default {
color: #6a9080;
@ -46,6 +62,17 @@ a.cascade-tree-row.is-active:hover {
color: #6a7888;
font-size: 11px;
}
.cascade-bar .tip {
white-space: pre-wrap;
@media (max-width: 40em) {
.cascade-bar {
margin-top: -1rem;
padding: 0.45rem 0.5rem 0.55rem;
font-size: 11px;
}
.cascade-tree {
max-width: 100%;
}
.cascade-timing {
font-size: 10px;
}
}

View file

@ -1,4 +1,6 @@
(function () {
var POLL_MS = 30000;
function cookieEdition() {
var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
if (!m) return "flask";
@ -17,12 +19,14 @@
.replace(/"/g, """);
}
/** ISO → "YYYY-MM-DD HH:MM" (date required; clock-only was misleading across days). */
function shortTime(iso) {
if (!iso) return "";
// keep local-looking wall clock from ISO when possible
var m = String(iso).match(/T(\d{2}:\d{2})/);
if (m) return m[1];
return String(iso).slice(0, 16);
var m = String(iso).match(
/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::\d{2})?/
);
if (m) return m[1] + " " + m[2];
return String(iso).slice(0, 16).replace("T", " ");
}
function fmtBuildSec(sec) {
@ -33,6 +37,51 @@
return Math.round(n) + "s";
}
function metaBits(row) {
var tip = row.tip || "";
var tipTime = row.tip_time || "";
var builtAt = row.built_at || "";
var buildSec = row.build_seconds;
var bits = [];
if (tip) bits.push("@" + tip);
if (tipTime) bits.push("tip " + shortTime(tipTime));
if (buildSec != null && buildSec !== "")
bits.push("build " + fmtBuildSec(buildSec));
if (builtAt) bits.push("at " + shortTime(builtAt));
return bits;
}
function rowTitle(row, buildSec) {
var tip = row.tip || "";
var tipTime = row.tip_time || "";
return (
"Switch build to " +
(row.branch || "") +
(tip ? " @ " + tip : "") +
(tipTime ? " tip=" + tipTime : "") +
(buildSec != null && buildSec !== ""
? " build=" + fmtBuildSec(buildSec)
: "")
);
}
function structureKey(list) {
var parts = [];
for (var i = 0; i < list.length; i++) {
var row = list[i];
parts.push(
(row.edition || "") +
"\t" +
(row.branch || "") +
"\t" +
(row.prefix || "") +
"\t" +
(row.parent || "")
);
}
return parts.join("\n");
}
function markActive(root, edition) {
var links = root.querySelectorAll("a[data-edition]");
for (var i = 0; i < links.length; i++) {
@ -51,7 +100,71 @@
}
}
function renderTree(data) {
function timingText(timing, listLen) {
if (!timing || timing.wall_seconds == null) return "";
return (
"last full pass ~" +
String(timing.wall_seconds) +
"s · built " +
String(timing.built || 0) +
" / skipped " +
String(timing.skipped || 0) +
" / " +
String(timing.branches || listLen)
);
}
function updateValues(mount, data) {
var list = (data && data.branches) || [];
var defaultBranch = (data && data.default_branch) || "feat/website-views";
var timing = (data && data.rebuild_timing) || null;
var rows = mount.querySelectorAll("a.cascade-tree-row[data-edition]");
if (rows.length !== list.length) return false;
for (var i = 0; i < list.length; i++) {
var row = list[i];
var a = rows[i];
if (a.getAttribute("data-edition") !== (row.edition || "")) return false;
if (a.getAttribute("data-branch") !== (row.branch || "")) return false;
var bits = metaBits(row);
var tipEl = a.querySelector(".tip");
var wantTip = bits.length ? " " + bits.join(" · ") : "";
if (tipEl) {
if (tipEl.textContent !== wantTip) tipEl.textContent = wantTip;
} else if (wantTip) {
tipEl = document.createElement("span");
tipEl.className = "tip";
tipEl.textContent = wantTip;
a.appendChild(tipEl);
}
var defEl = a.querySelector(".cascade-default");
var isDefault = (row.branch || "") === defaultBranch;
if (isDefault && !defEl) {
defEl = document.createElement("span");
defEl.className = "cascade-default";
defEl.textContent = " · default";
a.appendChild(defEl);
} else if (!isDefault && defEl) {
defEl.parentNode.removeChild(defEl);
}
a.setAttribute("title", rowTitle(row, row.build_seconds));
}
var timingEl = mount.querySelector(".cascade-timing");
var t = timingText(timing, list.length);
if (t) {
if (!timingEl) {
timingEl = document.createElement("div");
timingEl.className = "cascade-timing";
mount.appendChild(timingEl);
}
if (timingEl.textContent !== t) timingEl.textContent = t;
} else if (timingEl) {
timingEl.parentNode.removeChild(timingEl);
}
markActive(mount, cookieEdition());
return true;
}
function renderTree(data, soft) {
var mount = document.getElementById("cascade-tree");
if (!mount) return;
var list = (data && data.branches) || [];
@ -59,6 +172,9 @@
mount.textContent = "No branches in branch-status.json";
return;
}
if (soft && mount.getAttribute("data-structure") === structureKey(list)) {
if (updateValues(mount, data)) return;
}
var defaultBranch = (data && data.default_branch) || "feat/website-views";
var timing = (data && data.rebuild_timing) || null;
var html = [];
@ -71,22 +187,16 @@
var prefix = row.prefix || "";
var parent = row.parent || "";
var tipTime = row.tip_time || "";
var builtAt = row.built_at || "";
var buildSec = row.build_seconds;
var isDefault = branch === defaultBranch;
var metaBits = [];
if (tip) metaBits.push("@" + tip);
if (tipTime) metaBits.push("tip " + shortTime(tipTime));
if (buildSec != null && buildSec !== "")
metaBits.push("build " + fmtBuildSec(buildSec));
if (builtAt) metaBits.push("at " + shortTime(builtAt));
var bits = metaBits(row);
var label =
esc(prefix) +
"<code>" +
esc(branch) +
"</code>" +
(metaBits.length
? '<span class="tip"> ' + esc(metaBits.join(" · ")) + "</span>"
(bits.length
? '<span class="tip"> ' + esc(bits.join(" · ")) + "</span>"
: "") +
(isDefault ? '<span class="cascade-default"> · default</span>' : "");
html.push(
@ -94,53 +204,50 @@
esc(edition) +
'" data-edition="' +
esc(edition) +
'" data-branch="' +
esc(branch) +
'"' +
(parent ? ' data-parent="' + esc(parent) + '"' : "") +
(isDefault ? ' data-default="1"' : "") +
' title="Switch build to ' +
esc(branch) +
(tip ? " @ " + esc(tip) : "") +
(tipTime ? " tip=" + esc(tipTime) : "") +
(buildSec != null ? " build=" + esc(fmtBuildSec(buildSec)) : "") +
' title="' +
esc(rowTitle(row, buildSec)) +
'">' +
label +
"</a>"
);
}
html.push("</div>");
if (timing && timing.wall_seconds != null) {
html.push(
'<div class="cascade-timing">last full pass ~' +
esc(String(timing.wall_seconds)) +
"s · built " +
esc(String(timing.built || 0)) +
" / skipped " +
esc(String(timing.skipped || 0)) +
" / " +
esc(String(timing.branches || list.length)) +
"</div>"
);
var t = timingText(timing, list.length);
if (t) {
html.push('<div class="cascade-timing">' + esc(t) + "</div>");
}
mount.innerHTML = html.join("");
mount.setAttribute("data-structure", structureKey(list));
markActive(mount, cookieEdition());
}
var edition = cookieEdition();
var mount = document.getElementById("cascade-tree");
fetch("/branch-status.json", { cache: "no-store" })
.then(function (res) {
if (!res.ok) throw new Error("status");
return res.json();
})
.then(function (data) {
renderTree(data);
})
.catch(function () {
if (mount) {
mount.innerHTML =
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask"><code>flask</code></a>' +
'<span class="tip"> (branch-status unavailable)</span>';
markActive(mount, edition);
}
});
function loadStatus(soft) {
var mount = document.getElementById("cascade-tree");
return fetch("/branch-status.json", { cache: "no-store" })
.then(function (res) {
if (!res.ok) throw new Error("status");
return res.json();
})
.then(function (data) {
renderTree(data, !!soft);
})
.catch(function () {
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
mount.innerHTML =
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask" data-branch="flask"><code>flask</code></a>' +
'<span class="tip"> (branch-status unavailable)</span>';
markActive(mount, cookieEdition());
}
});
}
loadStatus(false);
setInterval(function () {
loadStatus(true);
}, POLL_MS);
})();