// H0M3 page script. The page is static markup; this only does // a few small things off a single `/api/state` read: // 1. fill the swarm/hive identity line when configured; // 2. render the shared server-warnings banner (top of every page). // No SSE — a portal doesn't need live updates. // // It also drives the decorative matrix-rain backdrop (#matrix-rain) — // see startMatrixRain() at the foot. import { renderServerWarnings } from "./common.js"; const $ = (id) => document.getElementById(id); async function init() { let state; try { const resp = await fetch("/api/state"); if (!resp.ok) return; state = await resp.json(); } catch { return; // best-effort: the tiles still work without it } renderServerWarnings(state.server_warnings); const ident = $("hive-identity"); if (ident && (state.swarm_name || state.hive_name)) { const parts = [state.swarm_name, state.hive_name].filter(Boolean); ident.textContent = parts.join(" / "); ident.hidden = false; } // Rev line: absent when the flake ref isn't a local path pin (a bare // `github:` url has no on-disk rev to canonicalize) — hidden rather // than showing a placeholder in that case. // // `current_flake_rev` canonicalizes to a nix store path // (/nix/store/-) — the part that actually varies between // builds is the hash right after `/nix/store/`, not the trailing // `-` (constant across revs). Slice the hash out of the front // rather than truncating from the tail, so two different builds don't // render as the same string. Falls back to a plain head-slice for a // non-store path (e.g. a bare local dir during dev). const rev = $("hive-rev"); if (rev && state.hyperhive_rev) { const storeMatch = state.hyperhive_rev.match(/^\/nix\/store\/([^-]+)/); const short = storeMatch ? storeMatch[1].slice(0, 12) : state.hyperhive_rev.slice(0, 12); rev.textContent = `rev ${short}`; rev.title = state.hyperhive_rev; rev.hidden = false; } } init(); // ── matrix-rain backdrop ───────────────────────────────────────────── // Classic falling-glyph "digital rain" on the #matrix-rain canvas, purely // decorative. Colours are resolved from the live stylix palette (--green // glyphs, --bg fade-trail) so a theme swap re-colours it; the CSS already // dims it + parks it behind the content. Stepped via setInterval (the // steppy cadence is the look and costs little CPU); paused while the tab is // hidden, and skipped entirely under prefers-reduced-motion. function startMatrixRain() { const canvas = $("matrix-rain"); if (!canvas || !canvas.getContext) return; if ( window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches ) return; const ctx = canvas.getContext("2d"); // getComputedStyle on a custom property returns its *declared* value // (e.g. "var(--base0B)") unresolved, so resolve to a concrete rgb() by // probing a real `color` computation instead. function resolveColor(varName, fallback) { const probe = document.createElement("span"); probe.style.color = `var(${varName})`; probe.style.display = "none"; document.body.appendChild(probe); const c = getComputedStyle(probe).color; probe.remove(); return /^rgb/.test(c) ? c : fallback; } const glyphColor = resolveColor("--green", "rgb(166, 227, 161)"); // Fade-trail = the bg at low alpha. Pull r,g,b numerically rather than // string-rewriting the rgb() — robust to whatever rgb()/rgba() spacing // getComputedStyle hands back, so the trail always fades (never fills // solid) even if the format shifts. const bgRGB = resolveColor("--bg", "rgb(30, 30, 46)").match( /(\d+)[^\d]+(\d+)[^\d]+(\d+)/, ); const fadeColor = bgRGB ? `rgba(${bgRGB[1]}, ${bgRGB[2]}, ${bgRGB[3]}, 0.09)` : "rgba(30, 30, 46, 0.09)"; const GLYPHS = "アイウエオカキクケコサシスセソタチツテト0123456789:=*+-<>¦"; const CELL = 16; // glyph cell size (px) let cols = 0; let drops = []; function resize() { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; cols = Math.max(1, Math.ceil(canvas.width / CELL)); drops = Array.from({ length: cols }, () => Math.floor(Math.random() * -40)); ctx.font = `${CELL}px monospace`; // canvas reset on resize clears this } function tick() { // Translucent bg wash leaves a fading trail behind each glyph. ctx.fillStyle = fadeColor; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = glyphColor; for (let i = 0; i < cols; i++) { const ch = GLYPHS[(Math.random() * GLYPHS.length) | 0]; ctx.fillText(ch, i * CELL, drops[i] * CELL); if (drops[i] * CELL > canvas.height && Math.random() > 0.975) drops[i] = 0; drops[i]++; } } let timer = null; const play = () => { if (!timer) timer = setInterval(tick, 55); }; const pause = () => { if (timer) { clearInterval(timer); timer = null; } }; resize(); window.addEventListener("resize", resize); document.addEventListener("visibilitychange", () => document.hidden ? pause() : play(), ); play(); } startMatrixRain();