Adds services.hyperhive.forge.publicUrl (defaults to the gateway vhost URL when behindGateway=true, null otherwise). HIVE_FORGE_PUBLIC_URL is now sourced from it instead of hardcoding https://${forge.domain} whenever behindGateway is on. The 4 frontend call sites that built a forge link from state.forge_public_url now hide the link when that's absent, rather than guessing http://<browser-hostname>:3000 — a guess that's only correct by accident once the operator isn't on plain localhost. Fixes the dashboard H0M3 tile, per-agent-row forge links + agent menu, the approval-queue PR link, and the per-agent page's own meta-nav forge link (found during this pass, same defect, not in the original 3-site inventory). Docs + doc-comments updated to match.
148 lines
5.9 KiB
JavaScript
148 lines
5.9 KiB
JavaScript
// 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 only when the hive-forge container is up AND
|
|
// the operator has stated a public URL for it
|
|
// (services.hyperhive.forge.publicUrl, surfaced as
|
|
// state.forge_public_url) — hidden rather than guessed from
|
|
// `location.hostname` + the direct :3000 port, which is only
|
|
// correct by accident on deployments that aren't plain localhost.
|
|
if (state.forge_present && state.forge_public_url) {
|
|
const tile = $('home-tile-forge');
|
|
if (tile) {
|
|
tile.href = state.forge_public_url;
|
|
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;
|
|
}
|
|
|
|
// 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/<hash>-<pname>) — the part that actually varies between
|
|
// builds is the hash right after `/nix/store/`, not the trailing
|
|
// `-<pname>` (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();
|