mirror rev 4: evidence-verified rewrite, six-diagram artefact set, front-end and build-gate hardening

Content corrected against upstream docs, deployed-version source and the live
config: retention/GC semantics (untagged manifests including digest-only
pull-through entries are GC-eligible; keepUntagged is v2.1.19+, not deployed),
warm-cache guarantees (tag pulls revalidate upstream per open issue; digest
pulls serve locally), retry arithmetic (buildah --retry counts retries: 16 max,
not 12), five-upstream flat-namespace routing stated as observed risk with a
proposed prefix redesign, auth-canary design validated against longest-match
policy precedence with the Docker-client caveat, coalescing reframed from
deadlock to design with the restart as recovery-not-root-cause. Adds fact
panel, contents, failure-mode and cache test matrices, decision table, open
verification register and primary-source references. Front-end: grid full-bleed
(no viewport units - no scrollbar overflow), shell-level skip link, real
1600x872 intrinsic dims injected from bytes, lazy below-fold plates, AA
code-comment contrast, scene engine rewritten (IO-gated, rAF-coalesced, live
reduced-motion, hidden-tab pause, zero guards). Assembler: per-page gates plus
mirror-specific required/banned content assertions. README describes the
multi-page site honestly.
This commit is contained in:
2026-08-25 08:08:18 +10:00
parent 08e80d9f9f
commit 7f7035b98f
5 changed files with 912 additions and 435 deletions
+45 -20
View File
@@ -1,27 +1,52 @@
# The Exploded Cluster
# The Teaching Lab (learn.bztmon.com)
Interactive scroll-driven teardown of Kubernetes / OpenShift / Docker, in the estate's
Bat-Computer visual world (void `#070b14`, cyan `#3fbaf5` → magenta `#e879f9`).
A multi-page static technical learning site: scroll-driven teardowns of infrastructure the
author actually operates. Dark engineering-dossier aesthetic (void `#070b14`, cyan `#3fbaf5`,
magenta `#e879f9`), dependency-light by design, strict security posture.
- **v2** (tag `v2`) — CSS `preserve-3d` diagrams. Parked: eyeballed px positions, misaligned at some widths.
- **v3** (in progress) — ships PUBLIC at **learn.bztmon.com**.
- **v3 build** — Nano Banana 2 exploded hero shots, hand-sliced into `clip-path` parts and
scrubbed collapsed → hero with GSAP. Spec: `homelab/specs/SPEC-exploded-cluster-v3.md`.
## Pages
| Route | Page | Source |
|---|---|---|
| `/` | The Teaching Lab - chapter directory | `pilot/index.tpl.html` |
| `/cluster` | Chapter 01: The Exploded Cluster - eleven-course container-platform teardown | `pilot/arc.tpl.html` + `pilot/manifest-*.json` |
| `/mirror` | Chapter 02: The Mirror - a zot pull-through registry, its failure modes and operations | `pilot/mirror.tpl.html` + `pilot/mirror.js` |
## Build
`bash build.sh` -> `dist/`. The assembler (`pilot/assemble-arc.mjs`, plain node, no
dependencies) wraps each page in a shared document shell (canonical/OG metadata, build
revision, skip link), content-fingerprints every file under `/assets/` (immutable-cached
by nginx), splits page scripts into external fingerprinted files, injects intrinsic image
dimensions (WebP and JPEG parsed from the actual bytes) with deliberate lazy/eager loading,
and runs per-page preflight gates: no inline scripts (CSP `script-src 'self'` with no
exceptions), exactly one `h1`, unique ids, no dangling anchors, no unresolved placeholders,
and page-specific content assertions. A failed gate fails the build.
Ship: `podman build` (multi-stage: node builds `dist/`, `nginx-unprivileged` serves it,
both digest-pinned) -> push to the estate registry -> digest-pinned deploy via GitOps.
`nginx/` carries the runtime config: strict CSP, HSTS, no-cache HTML with immutable
fingerprinted assets, security headers on every location.
## Accessibility and performance
Skip link on every page (hidden until keyboard focus), one `h1` per page, stable section
anchors, reduced-motion honoured live (effects also pause when the tab is hidden or the
scene is off screen), intrinsic image dimensions to prevent layout shift, below-fold images
lazy-loaded, system font stacks (CSP permits no third-party fonts).
## Layout
| Path | What |
|---|---|
| `exploded-cluster.html` | v2 assembled single file (174KB) |
| `ec2-part1.html` / `ec2-part2.html` | v2 source split — markup+CSS / app JS |
| `gsap.min.js` · `st.min.js` | GSAP 3.13 + ScrollTrigger, inlined at assemble time |
| `prompts/` | NB2 prompt contract — master style block + per-scene prompts |
| `tools/slicer.html` | slice editor: load a hero, tune cut lines, export the manifest |
| `assets/raw/` | curated NB2 heroes, straight from the generator |
| `pilot/` | page templates, page scripts, scene manifests, the assembler |
| `assets/dist/` | production heroes and diagrams for `/cluster` |
| `assets/mirror/` | plates for `/mirror` |
| `content/` | course source notes |
| `nginx/` | runtime config baked into the image |
| `tools/slicer.html` | slice editor: load a hero, tune cut lines, export a scene manifest |
| `prompts/` | image prompt contract for the house visual style |
| `exploded-cluster.html`, `ec2-part*.html` | v2 (parked, superseded by `/cluster`) |
| `gsap.min.js` / `st.min.js` | GSAP 3.13 + ScrollTrigger, fingerprinted at assemble time |
Rebuild v2: `cat ec2-part1.html <(echo "<script>") gsap.min.js <(echo "</script><script>") st.min.js <(echo "</script>") ec2-part2.html > exploded-cluster.html`
## The slicing rule (load-bearing — read before touching the engine)
Parts occlude each other in 2D, so every slice carries a hidden notch where the part in front
covers it. Animating **collapsed → hero** only ever increases occlusion, so notches stay buried.
**Never overshoot past the hero layout and never fan parts sideways** — either exposes the tears.
Region boundaries overlap upward by `EPS`; expanding downward would ghost.
## The slicing rule (load-bearing - read before touching the arc engine)
Parts occlude each other in 2D, so every slice carries a hidden notch where the part in
front covers it. Animating **collapsed -> hero** only ever increases occlusion, so notches
stay buried. **Never overshoot past the hero layout and never fan parts sideways** - either
exposes the tears. Region boundaries overlap upward by `EPS`; expanding downward would ghost.
+56 -2
View File
@@ -40,6 +40,30 @@ function webpSize(buf) {
return null;
}
// --- intrinsic JPEG dimensions (SOF0/SOF2 markers) for the mirror plates ---
function jpegSize(buf) {
if (buf[0] !== 0xff || buf[1] !== 0xd8) return null;
let o = 2;
while (o + 9 < buf.length) {
if (buf[o] !== 0xff) { o++; continue; }
const marker = buf[o + 1];
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
return { h: buf.readUInt16BE(o + 5), w: buf.readUInt16BE(o + 7) };
}
o += 2 + buf.readUInt16BE(o + 2);
}
return null;
}
// skip link: emitted by the shared shell, so its accessibility CSS lives in the shell
// too - visually hidden until keyboard focus, visible and prominent when focused.
const SKIP_CSS = `<style>
a.skip{position:absolute;left:-9999px;top:0;z-index:99}
a.skip:focus{left:1rem;top:1rem;background:#070b14;color:#3fbaf5;
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8rem;
padding:10px 16px;border:1px solid #3fbaf5;border-radius:6px;outline:none}
</style>`;
let tpl = readFileSync(join(ROOT, "pilot/arc.tpl.html"), "utf8");
// split the app script (the last <script> block) out of the template
@@ -136,6 +160,7 @@ const shell = ({ title, desc, path, ogImg, scripts, bodyHtml }) => `<!doctype ht
<meta name="twitter:description" content="${desc}">
<meta name="twitter:image" content="${ogImg}">
<link rel="icon" href="${favicon}">
${SKIP_CSS}
</head>
<body>
<a class="skip" href="#main-content">Skip to content</a>
@@ -157,9 +182,14 @@ const MIRROR_DESC = "A pull-through container registry taken apart: why a lab ru
let mirror = readFileSync(join(ROOT, "pilot/mirror.tpl.html"), "utf8");
const mirrorPlates = {};
for (const n of ["20", "21", "22", "23", "24", "25"]) {
const { url } = emit(join(ROOT, "assets/mirror", `${n}.jpg`), `mirror-${n}`, "jpg");
const { url, buf } = emit(join(ROOT, "assets/mirror", `${n}.jpg`), `mirror-${n}`, "jpg");
mirrorPlates[n] = url;
mirror = mirror.replace(`__M${n}__`, url);
const dims = jpegSize(buf);
if (!dims) throw new Error(`mirror plate ${n}: JPEG dimensions unreadable`);
// plate 20 is the page's first image (near the fold) -> eager/LCP; the rest lazy.
const load = n === "20" ? "eager" : "lazy";
mirror = mirror.replace(`__M${n}__`,
`${url}" width="${dims.w}" height="${dims.h}" decoding="async" loading="${load}`);
}
const mirrorJs = readFileSync(join(ROOT, "pilot/mirror.js"));
const mirrorJsName = `mirror.${fp(mirrorJs)}.js`;
@@ -206,6 +236,30 @@ for (const [pg, doc] of [["cluster", html], ["mirror", mirrorHtml], ["index", di
if (m[1] && !pidset.has(m[1])) fail.push(`${pg}: dangling internal link #` + m[1]);
}
}
// mirror-page content gates: retired claims may be DISCUSSED as corrections, so these
// target the assertive phrasings and internal production jargon, not the topic words.
const mirrorBanned = [
[/never leaves the building/i, "retired absolute cache claim"],
[/up to twelve|twelve tries|12 attempts/i, "stale retry arithmetic (correct figure: sixteen)"],
[/plate-level/i, "internal production note leaked to readers"],
[/&middot; motion:/i, "art-direction caption leaked to readers"],
[/Phase B/, "internal phase jargon without explanation"],
[/RAID .*(is|are) (a )?backup/i, "snapshots/RAID-as-backup claim"],
];
for (const [re, msg] of mirrorBanned) if (re.test(mirrorHtml)) fail.push("mirror: " + msg);
const mirrorRequired = [
["zotregistry.dev", "primary zot references missing"],
["keepUntagged", "version-gated retention discussion missing"],
["/livez", "real health endpoints missing"],
['id="refs"', "references section missing"],
['id="open"', "open-verification register missing"],
['width="1600" height="872"', "plate intrinsic dimensions missing/incorrect"],
];
for (const [needle, msg] of mirrorRequired) if (!mirrorHtml.includes(needle)) fail.push("mirror: " + msg);
must((mirrorHtml.match(/width="1600" height="872"/g) || []).length === 6, "mirror: expected six correctly-dimensioned plates");
must(!/100vw/.test(mirrorHtml), "mirror: viewport-width unit reintroduces scrollbar overflow");
must(!mirrorHtml.includes("gsap"), "mirror: unexpected animation library reference");
// banned / obsolete strings (user-visible copy)
const banned = [
[/ArgoCD/, "user-visible 'ArgoCD' (use 'Argo CD')"],
-3
View File
@@ -34,9 +34,6 @@ h1 em{color:var(--cyan);font-style:normal}
footer{border-top:1px solid var(--line);padding:1.4rem 0 3rem;color:var(--dim);
font-family:var(--mono);font-size:.82rem}
footer a{color:var(--cyan)}
a.skip{position:absolute;left:-9999px}
a.skip:focus{left:1rem;top:1rem;background:var(--void);color:var(--cyan);
font-family:var(--mono);font-size:.8rem;padding:10px 16px;border:1px solid var(--cyan);border-radius:6px}
:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
@media (max-width:640px){.card{grid-template-columns:1fr}.card .thumb{max-height:170px}}
</style>
+96 -31
View File
@@ -1,39 +1,104 @@
(function(){
var rm = matchMedia('(prefers-reduced-motion: reduce)').matches;
var bar = document.getElementById('bar');
var s20 = document.querySelector('#s20 img');
var ring = document.getElementById('ring');
var glows = [].slice.call(document.querySelectorAll('.s22-glow'));
var s24 = document.querySelector('#s24 img');
function prog(el){
var r = el.getBoundingClientRect(), vh = innerHeight;
return Math.max(0, Math.min(1, (vh - r.top) / (vh + r.height)));
}
function onScroll(){
var d = document.documentElement;
bar.style.width = (100 * d.scrollTop / (d.scrollHeight - innerHeight)) + '%';
if (rm) return;
var p20 = prog(s20); /* dock-and-stow: assembles inward */
s20.style.transform = 'scale(' + (1.07 - 0.07 * Math.min(1, p20 * 1.6)) + ') translateY(' + (26 - 26 * Math.min(1, p20 * 1.6)) + 'px)';
var p21 = prog(ring); /* ring-walk: the cycle turns */
ring.style.transform = 'rotate(' + (p21 * 300) + 'deg)';
glows.forEach(function(g, i){ /* checkpoint procession: three beats */
var p = prog(g.parentElement);
var band = 0.18 + i * 0.22;
g.style.opacity = Math.max(0, Math.min(0.85, (p - band) * 6));
// The Mirror - scroll effects. External file: script-src 'self' holds, no inline JS.
// Design constraints: no perpetual work - scroll reads are coalesced into a single rAF,
// per-scene work only runs while that scene intersects the viewport, the pulse loop
// runs only while its scene is visible AND the tab is visible AND motion is allowed,
// and a change to prefers-reduced-motion takes effect live without a reload.
(function () {
"use strict";
var motionQuery = matchMedia("(prefers-reduced-motion: reduce)");
var bar = document.getElementById("bar");
var s20 = document.querySelector("#s20 img");
var ring = document.getElementById("ring");
var s22 = document.getElementById("s22");
var glows = s22 ? [].slice.call(s22.querySelectorAll(".s22-glow")) : [];
var s24 = document.querySelector("#s24 img");
var pulse = document.getElementById("pulse");
// visibility ledger: IntersectionObserver marks which scenes are on screen
var visible = {};
var watched = [
["s20", s20 && s20.closest(".scene")],
["s21", ring && ring.closest(".scene")],
["s22", s22 && s22.closest(".scene")],
["s23", pulse && pulse.closest(".scene")],
["s24", s24 && s24.closest(".scene")],
].filter(function (w) { return w[1]; });
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var hit = watched.find(function (w) { return w[1] === e.target; });
if (hit) visible[hit[0]] = e.isIntersecting;
});
var p24 = prog(s24); /* two-plane parallax */
s24.style.transform = 'translateY(' + ((p24 - 0.5) * -26) + 'px) scale(1.03)';
if (visible.s23) startPulse(); // (re)arm the breather when its scene returns
}, { rootMargin: "10% 0px" });
watched.forEach(function (w) { io.observe(w[1]); });
// progress of an element through the viewport, guarded against zero heights
function prog(el) {
var r = el.getBoundingClientRect();
var span = innerHeight + r.height;
if (!span) return 0;
return Math.max(0, Math.min(1, (innerHeight - r.top) / span));
}
addEventListener('scroll', onScroll, {passive: true});
addEventListener('resize', onScroll);
onScroll();
if (!rm){ /* the jam breathes; nothing else does */
var pulse = document.getElementById('pulse'), t0 = null;
requestAnimationFrame(function breathe(t){
// scroll work: one rAF per burst, only touching scenes that are on screen
var ticking = false;
function frame() {
ticking = false;
var d = document.documentElement;
var denom = d.scrollHeight - innerHeight;
if (bar) bar.style.width = (denom > 0 ? (100 * d.scrollTop / denom) : 0) + "%";
if (motionQuery.matches) return;
if (visible.s20 && s20) {
var p20 = Math.min(1, prog(s20) * 1.6);
s20.style.transform = "scale(" + (1.07 - 0.07 * p20) + ") translateY(" + (26 - 26 * p20) + "px)";
}
if (visible.s21 && ring) ring.style.transform = "rotate(" + (prog(ring) * 300) + "deg)";
if (visible.s22 && glows.length) {
var p22 = prog(s22);
glows.forEach(function (g, i) {
g.style.opacity = Math.max(0, Math.min(0.85, (p22 - (0.18 + i * 0.22)) * 6));
});
}
if (visible.s24 && s24) s24.style.transform = "translateY(" + ((prog(s24) - 0.5) * -26) + "px) scale(1.03)";
}
function onScroll() {
if (!ticking) { ticking = true; requestAnimationFrame(frame); }
}
addEventListener("scroll", onScroll, { passive: true });
addEventListener("resize", onScroll);
// the jam's breathing glow: runs only while worth running, stops itself otherwise
var breathing = false;
function startPulse() {
if (breathing || !pulse || motionQuery.matches || document.hidden || !visible.s23) return;
breathing = true;
var t0 = null;
requestAnimationFrame(function breathe(t) {
if (motionQuery.matches || document.hidden || !visible.s23) {
breathing = false;
pulse.style.opacity = 0.25;
return;
}
if (t0 === null) t0 = t;
pulse.style.opacity = 0.18 + 0.2 * (0.5 + 0.5 * Math.sin((t - t0) / 520));
requestAnimationFrame(breathe);
});
}
document.addEventListener("visibilitychange", function () { if (!document.hidden) { startPulse(); onScroll(); } });
// live preference change: clear transforms so the page settles to its static state
function motionChanged() {
if (motionQuery.matches) {
[s20, ring, s24].forEach(function (el) { if (el) el.style.transform = ""; });
glows.forEach(function (g) { g.style.opacity = 0.35; });
if (pulse) pulse.style.opacity = 0.25;
} else {
startPulse();
onScroll();
}
}
if (motionQuery.addEventListener) motionQuery.addEventListener("change", motionChanged);
startPulse();
onScroll();
})();
+715 -379
View File
File diff suppressed because it is too large Load Diff