// H0M3 page script. The page is static markup; this only does // a few small things off a single `/api/state` read: // 1. reveal the Matrix tile when the matrix GUI is enabled (same // gating as the dashboard's M4TR1X tab — no dead link otherwise); // 2. fill the swarm/hive identity line when configured; // 3. 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); if (state.matrix_gui_enabled) { const tile = $('home-tile-matrix'); if (tile) tile.hidden = false; } // Forge tile: reveal + point at the live forge only when the // hive-forge container is up. Prefer the gateway-served public URL // (set when forge.behindGateway=true), fall back to the direct :3000 // port — same precedence the dashboard uses for forge links. if (state.forge_present) { const tile = $('home-tile-forge'); if (tile) { tile.href = state.forge_public_url || `http://${location.hostname}:3000`; tile.hidden = false; } } 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; } } 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();