hyperhive/frontend/packages/dashboard/src/home.js
atlas 39b95c2ede treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.

Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.

None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
2026-09-02 15:25:07 +02:00

145 lines
5.2 KiB
JavaScript

// 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/<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();