ops: dossiers-ngi cascade bar dates, center/mobile, soft refresh; serve /cascade
This commit is contained in:
parent
ee82ef9b6e
commit
5677318ae2
6 changed files with 391 additions and 110 deletions
|
|
@ -87,18 +87,22 @@ rsync -avz --exclude .DS_Store "$SITE/branch-status.json" "$WWW/branch-status.js
|
||||||
`bar.js` fetches `/branch-status.json` and draws:
|
`bar.js` fetches `/branch-status.json` and draws:
|
||||||
|
|
||||||
```
|
```
|
||||||
main @ d2fbd79
|
main @ d2fbd79 · tip 2026-09-01 16:52 · build 8.4s · at 2026-09-30 15:23
|
||||||
├── feat/sm-content @ …
|
├── feat/sm-content @ … · tip YYYY-MM-DD HH:MM · …
|
||||||
│ └── …
|
│ └── …
|
||||||
└── project_template @ …
|
└── project_template @ …
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`tip` / `at` always include **date + HH:MM** (ISO already in JSON; clock-only was misleading across days). The bar is **viewport-centered** (max-width 45em, mobile-friendly). Poll every 30s **updates tip/build/at text in place** when the branch structure is unchanged (no full tree remount).
|
||||||
|
|
||||||
Each name links to `/_cascade/use/<edition>` (cookie + 302 `/`). Caddy map:
|
Each name links to `/_cascade/use/<edition>` (cookie + 302 `/`). Caddy map:
|
||||||
|
|
||||||
```
|
```
|
||||||
~^[A-Za-z0-9_-]+$ {http.request.cookie.tdpd_edition}
|
~^[A-Za-z0-9_-]+$ {http.request.cookie.tdpd_edition}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Also serve `/cascade/*` from the site www root (path stubs). Live Caddy must not use a trailing `~.* flask` catch-all.
|
||||||
|
|
||||||
so new branches need no Caddy edit — only an `editions/<slug>/` tree.
|
so new branches need no Caddy edit — only an `editions/<slug>/` tree.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@
|
||||||
root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
|
root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
|
||||||
file_server
|
file_server
|
||||||
}
|
}
|
||||||
|
# Path-root cascade stubs (feat-website-views, …) live under sites/…/cascade/
|
||||||
|
# and must not fall through to editions/{cookie}/ (that yields 404).
|
||||||
|
handle /cascade/* {
|
||||||
|
root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
|
||||||
|
file_server
|
||||||
|
try_files {path} {path}/ {path}/index.html
|
||||||
|
}
|
||||||
handle /branch-status.json {
|
handle /branch-status.json {
|
||||||
root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
|
root * /var/www/monitoring-sites/dossiers.ngi.hacktivism.ch
|
||||||
file_server
|
file_server
|
||||||
|
|
@ -28,6 +35,8 @@
|
||||||
|
|
||||||
# Dynamic editions: cookie value is the dir name under editions/
|
# Dynamic editions: cookie value is the dir name under editions/
|
||||||
# (hourly builder stages every origin/* tip as slug with / → -).
|
# (hourly builder stages every origin/* tip as slug with / → -).
|
||||||
|
# Do not append a catch-all `~.* flask` — that forces unknown cookies
|
||||||
|
# (and can mask missing edition dirs) onto the default freeze.
|
||||||
map {http.request.cookie.tdpd_edition} {edition_dir} {
|
map {http.request.cookie.tdpd_edition} {edition_dir} {
|
||||||
default flask
|
default flask
|
||||||
flask flask
|
flask flask
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,13 @@
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
margin: -1.5rem -1.5rem 1rem;
|
display: flex;
|
||||||
padding: 0.55rem 1.5rem 0.65rem;
|
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;
|
background: #10141a;
|
||||||
border-bottom: 1px solid #243040;
|
border-bottom: 1px solid #243040;
|
||||||
color: #c8d0d8;
|
color: #c8d0d8;
|
||||||
|
|
@ -11,9 +16,16 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
.cascade-tree {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 45em;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
.cascade-tree-lines {
|
.cascade-tree-lines {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
a.cascade-tree-row {
|
a.cascade-tree-row {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -34,9 +46,13 @@ a.cascade-tree-row.is-active {
|
||||||
a.cascade-tree-row.is-active:hover {
|
a.cascade-tree-row.is-active:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.cascade-bar code { color: inherit; font-size: inherit; }
|
.cascade-bar code {
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
.cascade-bar .tip {
|
.cascade-bar .tip {
|
||||||
color: #8a96a3;
|
color: #8a96a3;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
.cascade-bar .cascade-default {
|
.cascade-bar .cascade-default {
|
||||||
color: #6a9080;
|
color: #6a9080;
|
||||||
|
|
@ -46,6 +62,17 @@ a.cascade-tree-row.is-active:hover {
|
||||||
color: #6a7888;
|
color: #6a7888;
|
||||||
font-size: 11px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
(function () {
|
(function () {
|
||||||
|
var POLL_MS = 30000;
|
||||||
|
|
||||||
function cookieEdition() {
|
function cookieEdition() {
|
||||||
var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
|
var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
|
||||||
if (!m) return "flask";
|
if (!m) return "flask";
|
||||||
|
|
@ -17,12 +19,14 @@
|
||||||
.replace(/"/g, """);
|
.replace(/"/g, """);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ISO → "YYYY-MM-DD HH:MM" (date required; clock-only was misleading across days). */
|
||||||
function shortTime(iso) {
|
function shortTime(iso) {
|
||||||
if (!iso) return "";
|
if (!iso) return "";
|
||||||
// keep local-looking wall clock from ISO when possible
|
var m = String(iso).match(
|
||||||
var m = String(iso).match(/T(\d{2}:\d{2})/);
|
/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::\d{2})?/
|
||||||
if (m) return m[1];
|
);
|
||||||
return String(iso).slice(0, 16);
|
if (m) return m[1] + " " + m[2];
|
||||||
|
return String(iso).slice(0, 16).replace("T", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtBuildSec(sec) {
|
function fmtBuildSec(sec) {
|
||||||
|
|
@ -33,6 +37,51 @@
|
||||||
return Math.round(n) + "s";
|
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) {
|
function markActive(root, edition) {
|
||||||
var links = root.querySelectorAll("a[data-edition]");
|
var links = root.querySelectorAll("a[data-edition]");
|
||||||
for (var i = 0; i < links.length; i++) {
|
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");
|
var mount = document.getElementById("cascade-tree");
|
||||||
if (!mount) return;
|
if (!mount) return;
|
||||||
var list = (data && data.branches) || [];
|
var list = (data && data.branches) || [];
|
||||||
|
|
@ -59,6 +172,9 @@
|
||||||
mount.textContent = "No branches in branch-status.json";
|
mount.textContent = "No branches in branch-status.json";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (soft && mount.getAttribute("data-structure") === structureKey(list)) {
|
||||||
|
if (updateValues(mount, data)) return;
|
||||||
|
}
|
||||||
var defaultBranch = (data && data.default_branch) || "feat/website-views";
|
var defaultBranch = (data && data.default_branch) || "feat/website-views";
|
||||||
var timing = (data && data.rebuild_timing) || null;
|
var timing = (data && data.rebuild_timing) || null;
|
||||||
var html = [];
|
var html = [];
|
||||||
|
|
@ -71,22 +187,16 @@
|
||||||
var prefix = row.prefix || "";
|
var prefix = row.prefix || "";
|
||||||
var parent = row.parent || "";
|
var parent = row.parent || "";
|
||||||
var tipTime = row.tip_time || "";
|
var tipTime = row.tip_time || "";
|
||||||
var builtAt = row.built_at || "";
|
|
||||||
var buildSec = row.build_seconds;
|
var buildSec = row.build_seconds;
|
||||||
var isDefault = branch === defaultBranch;
|
var isDefault = branch === defaultBranch;
|
||||||
var metaBits = [];
|
var bits = metaBits(row);
|
||||||
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 label =
|
var label =
|
||||||
esc(prefix) +
|
esc(prefix) +
|
||||||
"<code>" +
|
"<code>" +
|
||||||
esc(branch) +
|
esc(branch) +
|
||||||
"</code>" +
|
"</code>" +
|
||||||
(metaBits.length
|
(bits.length
|
||||||
? '<span class="tip"> ' + esc(metaBits.join(" · ")) + "</span>"
|
? '<span class="tip"> ' + esc(bits.join(" · ")) + "</span>"
|
||||||
: "") +
|
: "") +
|
||||||
(isDefault ? '<span class="cascade-default"> · default</span>' : "");
|
(isDefault ? '<span class="cascade-default"> · default</span>' : "");
|
||||||
html.push(
|
html.push(
|
||||||
|
|
@ -94,53 +204,50 @@
|
||||||
esc(edition) +
|
esc(edition) +
|
||||||
'" data-edition="' +
|
'" data-edition="' +
|
||||||
esc(edition) +
|
esc(edition) +
|
||||||
|
'" data-branch="' +
|
||||||
|
esc(branch) +
|
||||||
'"' +
|
'"' +
|
||||||
(parent ? ' data-parent="' + esc(parent) + '"' : "") +
|
(parent ? ' data-parent="' + esc(parent) + '"' : "") +
|
||||||
(isDefault ? ' data-default="1"' : "") +
|
(isDefault ? ' data-default="1"' : "") +
|
||||||
' title="Switch build to ' +
|
' title="' +
|
||||||
esc(branch) +
|
esc(rowTitle(row, buildSec)) +
|
||||||
(tip ? " @ " + esc(tip) : "") +
|
|
||||||
(tipTime ? " tip=" + esc(tipTime) : "") +
|
|
||||||
(buildSec != null ? " build=" + esc(fmtBuildSec(buildSec)) : "") +
|
|
||||||
'">' +
|
'">' +
|
||||||
label +
|
label +
|
||||||
"</a>"
|
"</a>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
html.push("</div>");
|
html.push("</div>");
|
||||||
if (timing && timing.wall_seconds != null) {
|
var t = timingText(timing, list.length);
|
||||||
html.push(
|
if (t) {
|
||||||
'<div class="cascade-timing">last full pass ~' +
|
html.push('<div class="cascade-timing">' + esc(t) + "</div>");
|
||||||
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>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
mount.innerHTML = html.join("");
|
mount.innerHTML = html.join("");
|
||||||
|
mount.setAttribute("data-structure", structureKey(list));
|
||||||
markActive(mount, cookieEdition());
|
markActive(mount, cookieEdition());
|
||||||
}
|
}
|
||||||
|
|
||||||
var edition = cookieEdition();
|
function loadStatus(soft) {
|
||||||
var mount = document.getElementById("cascade-tree");
|
var mount = document.getElementById("cascade-tree");
|
||||||
fetch("/branch-status.json", { cache: "no-store" })
|
return fetch("/branch-status.json", { cache: "no-store" })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error("status");
|
if (!res.ok) throw new Error("status");
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
renderTree(data);
|
renderTree(data, !!soft);
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
if (mount) {
|
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
|
||||||
mount.innerHTML =
|
mount.innerHTML =
|
||||||
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask"><code>flask</code></a>' +
|
'<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>';
|
'<span class="tip"> (branch-status unavailable)</span>';
|
||||||
markActive(mount, edition);
|
markActive(mount, cookieEdition());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStatus(false);
|
||||||
|
setInterval(function () {
|
||||||
|
loadStatus(true);
|
||||||
|
}, POLL_MS);
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,13 @@
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
margin: -1.5rem -1.5rem 1rem;
|
display: flex;
|
||||||
padding: 0.55rem 1.5rem 0.65rem;
|
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;
|
background: #10141a;
|
||||||
border-bottom: 1px solid #243040;
|
border-bottom: 1px solid #243040;
|
||||||
color: #c8d0d8;
|
color: #c8d0d8;
|
||||||
|
|
@ -11,9 +16,16 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
.cascade-tree {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 45em;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
.cascade-tree-lines {
|
.cascade-tree-lines {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
a.cascade-tree-row {
|
a.cascade-tree-row {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -34,9 +46,13 @@ a.cascade-tree-row.is-active {
|
||||||
a.cascade-tree-row.is-active:hover {
|
a.cascade-tree-row.is-active:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.cascade-bar code { color: inherit; font-size: inherit; }
|
.cascade-bar code {
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
.cascade-bar .tip {
|
.cascade-bar .tip {
|
||||||
color: #8a96a3;
|
color: #8a96a3;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
.cascade-bar .cascade-default {
|
.cascade-bar .cascade-default {
|
||||||
color: #6a9080;
|
color: #6a9080;
|
||||||
|
|
@ -46,6 +62,17 @@ a.cascade-tree-row.is-active:hover {
|
||||||
color: #6a7888;
|
color: #6a7888;
|
||||||
font-size: 11px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
(function () {
|
(function () {
|
||||||
|
var POLL_MS = 30000;
|
||||||
|
|
||||||
function cookieEdition() {
|
function cookieEdition() {
|
||||||
var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
|
var m = document.cookie.match(/(?:^|; )tdpd_edition=([^;]*)/);
|
||||||
if (!m) return "flask";
|
if (!m) return "flask";
|
||||||
|
|
@ -17,12 +19,14 @@
|
||||||
.replace(/"/g, """);
|
.replace(/"/g, """);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ISO → "YYYY-MM-DD HH:MM" (date required; clock-only was misleading across days). */
|
||||||
function shortTime(iso) {
|
function shortTime(iso) {
|
||||||
if (!iso) return "";
|
if (!iso) return "";
|
||||||
// keep local-looking wall clock from ISO when possible
|
var m = String(iso).match(
|
||||||
var m = String(iso).match(/T(\d{2}:\d{2})/);
|
/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::\d{2})?/
|
||||||
if (m) return m[1];
|
);
|
||||||
return String(iso).slice(0, 16);
|
if (m) return m[1] + " " + m[2];
|
||||||
|
return String(iso).slice(0, 16).replace("T", " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtBuildSec(sec) {
|
function fmtBuildSec(sec) {
|
||||||
|
|
@ -33,6 +37,51 @@
|
||||||
return Math.round(n) + "s";
|
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) {
|
function markActive(root, edition) {
|
||||||
var links = root.querySelectorAll("a[data-edition]");
|
var links = root.querySelectorAll("a[data-edition]");
|
||||||
for (var i = 0; i < links.length; i++) {
|
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");
|
var mount = document.getElementById("cascade-tree");
|
||||||
if (!mount) return;
|
if (!mount) return;
|
||||||
var list = (data && data.branches) || [];
|
var list = (data && data.branches) || [];
|
||||||
|
|
@ -59,6 +172,9 @@
|
||||||
mount.textContent = "No branches in branch-status.json";
|
mount.textContent = "No branches in branch-status.json";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (soft && mount.getAttribute("data-structure") === structureKey(list)) {
|
||||||
|
if (updateValues(mount, data)) return;
|
||||||
|
}
|
||||||
var defaultBranch = (data && data.default_branch) || "feat/website-views";
|
var defaultBranch = (data && data.default_branch) || "feat/website-views";
|
||||||
var timing = (data && data.rebuild_timing) || null;
|
var timing = (data && data.rebuild_timing) || null;
|
||||||
var html = [];
|
var html = [];
|
||||||
|
|
@ -71,22 +187,16 @@
|
||||||
var prefix = row.prefix || "";
|
var prefix = row.prefix || "";
|
||||||
var parent = row.parent || "";
|
var parent = row.parent || "";
|
||||||
var tipTime = row.tip_time || "";
|
var tipTime = row.tip_time || "";
|
||||||
var builtAt = row.built_at || "";
|
|
||||||
var buildSec = row.build_seconds;
|
var buildSec = row.build_seconds;
|
||||||
var isDefault = branch === defaultBranch;
|
var isDefault = branch === defaultBranch;
|
||||||
var metaBits = [];
|
var bits = metaBits(row);
|
||||||
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 label =
|
var label =
|
||||||
esc(prefix) +
|
esc(prefix) +
|
||||||
"<code>" +
|
"<code>" +
|
||||||
esc(branch) +
|
esc(branch) +
|
||||||
"</code>" +
|
"</code>" +
|
||||||
(metaBits.length
|
(bits.length
|
||||||
? '<span class="tip"> ' + esc(metaBits.join(" · ")) + "</span>"
|
? '<span class="tip"> ' + esc(bits.join(" · ")) + "</span>"
|
||||||
: "") +
|
: "") +
|
||||||
(isDefault ? '<span class="cascade-default"> · default</span>' : "");
|
(isDefault ? '<span class="cascade-default"> · default</span>' : "");
|
||||||
html.push(
|
html.push(
|
||||||
|
|
@ -94,53 +204,50 @@
|
||||||
esc(edition) +
|
esc(edition) +
|
||||||
'" data-edition="' +
|
'" data-edition="' +
|
||||||
esc(edition) +
|
esc(edition) +
|
||||||
|
'" data-branch="' +
|
||||||
|
esc(branch) +
|
||||||
'"' +
|
'"' +
|
||||||
(parent ? ' data-parent="' + esc(parent) + '"' : "") +
|
(parent ? ' data-parent="' + esc(parent) + '"' : "") +
|
||||||
(isDefault ? ' data-default="1"' : "") +
|
(isDefault ? ' data-default="1"' : "") +
|
||||||
' title="Switch build to ' +
|
' title="' +
|
||||||
esc(branch) +
|
esc(rowTitle(row, buildSec)) +
|
||||||
(tip ? " @ " + esc(tip) : "") +
|
|
||||||
(tipTime ? " tip=" + esc(tipTime) : "") +
|
|
||||||
(buildSec != null ? " build=" + esc(fmtBuildSec(buildSec)) : "") +
|
|
||||||
'">' +
|
'">' +
|
||||||
label +
|
label +
|
||||||
"</a>"
|
"</a>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
html.push("</div>");
|
html.push("</div>");
|
||||||
if (timing && timing.wall_seconds != null) {
|
var t = timingText(timing, list.length);
|
||||||
html.push(
|
if (t) {
|
||||||
'<div class="cascade-timing">last full pass ~' +
|
html.push('<div class="cascade-timing">' + esc(t) + "</div>");
|
||||||
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>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
mount.innerHTML = html.join("");
|
mount.innerHTML = html.join("");
|
||||||
|
mount.setAttribute("data-structure", structureKey(list));
|
||||||
markActive(mount, cookieEdition());
|
markActive(mount, cookieEdition());
|
||||||
}
|
}
|
||||||
|
|
||||||
var edition = cookieEdition();
|
function loadStatus(soft) {
|
||||||
var mount = document.getElementById("cascade-tree");
|
var mount = document.getElementById("cascade-tree");
|
||||||
fetch("/branch-status.json", { cache: "no-store" })
|
return fetch("/branch-status.json", { cache: "no-store" })
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) throw new Error("status");
|
if (!res.ok) throw new Error("status");
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
renderTree(data);
|
renderTree(data, !!soft);
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
if (mount) {
|
if (!soft && mount && !mount.querySelector("a[data-edition]")) {
|
||||||
mount.innerHTML =
|
mount.innerHTML =
|
||||||
'<a class="cascade-tree-row" href="/_cascade/use/flask" data-edition="flask"><code>flask</code></a>' +
|
'<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>';
|
'<span class="tip"> (branch-status unavailable)</span>';
|
||||||
markActive(mount, edition);
|
markActive(mount, cookieEdition());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStatus(false);
|
||||||
|
setInterval(function () {
|
||||||
|
loadStatus(true);
|
||||||
|
}, POLL_MS);
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue