Files
obs-config/landing/index.html
Jakub Zych 5a56e2f8e9 Landing: rig name decode/scramble in top HUD; CLAUDE.md docs sync
Landing's top-left identifier upgraded from "OPHI-118" to
"OPHI-118 // [<RIG>]" where the rig portion (read from
window.__TEL.rig in landing/telemetry.js) does a cyberpunk
decoder reveal on first paint and re-glitches briefly every
~9 s. Bracket dimming + amber flash during scramble keep the
layout width-stable.

CLAUDE.md updated to reflect everything since the last sync:
new Landing rig display, Music Box terminal pattern + dedicated
chat status strip, widget at 549×880 (camera-paired), Desktop
HUD fork, bridge nextTitles[] payload, bot !queue/!info/per-user
cooldown + Mattermost going-live notifier (MM_HOOK / STATUS_URL).
Plus an OBS-detection note: Flatpak's inner binary is `obs`, not
`flatpak run …` — `flatpak ps` or `pgrep -x obs` are the only
reliable checks. The naive `pgrep -fa 'flatpak run …'` returns
"off" while OBS is up and led to JSON edits during a live session.
2026-04-27 03:45:47 +02:00

460 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OPHI-118 / STAND BY</title>
<style>
:root {
--bg: #07080d;
--ink: #e8e8e0;
--hud: #4fd2ff;
--hud-dim: #2a7fa6;
--accent: #e63a2e;
--warn: #ffd000;
--mono: 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', monospace;
--display: 'DejaVu Sans', 'Liberation Sans', 'Helvetica', 'Arial', sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: var(--bg);
color: var(--ink);
font-family: var(--display);
overflow: hidden;
user-select: none;
}
body {
position: relative;
display: grid;
grid-template-rows: auto 1fr auto auto;
padding: 40px 72px;
gap: 24px;
}
/* ───────── HUD top strip ───────── */
.hud {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
font-family: var(--mono);
font-size: 22px;
font-weight: 700;
color: var(--hud);
letter-spacing: 0.08em;
}
.hud .group { display: flex; gap: 28px; align-items: center; }
.hud > .group:nth-child(1) { justify-self: start; }
.hud > .group:nth-child(2) { justify-self: center; }
.hud > .group:nth-child(3) { justify-self: end; }
.hud .dim { color: var(--hud-dim); }
.hud .group > span { display: inline-flex; align-items: center; gap: 12px; }
/* "OPHI-118 // <RIG>" station identifier. The rig portion gets a subtle
monochrome glitch — character scramble that resolves back to the real
name every ~9s. CSS-only flicker; the scramble itself is in JS. */
.station {
display: inline-flex;
align-items: baseline;
gap: 0;
white-space: pre; /* preserve the " // " spacing as-is */
}
.station .sep { color: var(--hud-dim); margin: 0 0.45em; letter-spacing: 0.32em; }
.station .rig {
display: inline-block;
color: var(--hud);
text-shadow: 0 0 10px rgba(79, 210, 255, 0.55);
font-variant-numeric: tabular-nums;
/* Subtle width-stable bracket dimming. The brackets sit just outside the
name so the scramble glyphs (which can be wider than 1ch in mono) don't
visually push the layout around. */
}
.station .rig::before,
.station .rig::after {
color: var(--hud-dim);
font-weight: 700;
text-shadow: none;
}
.station .rig::before { content: '['; margin-right: 0.32em; }
.station .rig::after { content: ']'; margin-left: 0.32em; }
.station .rig.glitching {
color: var(--warn);
text-shadow: 0 0 12px rgba(255, 208, 0, 0.5);
}
.hud .led {
display: inline-block;
width: 11px; height: 11px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
/* Small upward nudge: flex centers on line-box mid, but caps-only text
reads centered around cap-height mid which sits a touch higher. */
margin-top: -0.08em;
animation: rec-blink 1.6s ease-in-out infinite;
}
/* The ▮/▯ glyphs in DejaVu Sans Mono are designed centered on x-height,
not cap-height, so against all-caps SIGNAL their visual middle sits low.
Lift by ~(cap-mid x-mid) ≈ 0.10em to put the glyph center on cap center. */
#signal {
display: inline-block;
transform: translateY(-0.10em);
}
@keyframes rec-blink {
0%, 55% { opacity: 1; }
65%, 100% { opacity: 0.2; }
}
/* ───────── Centered mark ───────── */
.stage {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 36px;
}
.mark {
width: 360px; height: 360px;
/* Static glow only — animated drop-shadow is CPU-expensive in CEF.
The static breathing feel comes from the rec-blink LED + cd-pulse
elsewhere; the mark just has a steady halo. */
filter: drop-shadow(0 0 22px rgba(58, 209, 255, 0.35));
}
.bars {
display: flex;
width: 720px;
height: 14px;
opacity: 0.32;
}
.bars i { flex: 1; display: block; }
.bars i:nth-child(1) { background: #c7c7c7; }
.bars i:nth-child(2) { background: #c7c700; }
.bars i:nth-child(3) { background: #00c7c7; }
.bars i:nth-child(4) { background: #00c700; }
.bars i:nth-child(5) { background: #c700c7; }
.bars i:nth-child(6) { background: #c70000; }
.bars i:nth-child(7) { background: #0000c7; }
/* ───────── Standby block ───────── */
.standby {
text-align: center;
}
.standby .big {
font-size: 130px;
font-weight: 900;
letter-spacing: 0.20em;
line-height: 1;
text-shadow: 0 0 18px rgba(79, 210, 255, 0.18);
animation: standby-pulse 3s ease-in-out infinite;
}
.standby .sub {
margin-top: 30px;
font-family: var(--mono);
font-size: 26px;
letter-spacing: 0.45em;
color: var(--hud-dim);
}
@keyframes standby-pulse {
0%, 100% { opacity: 0.90; }
50% { opacity: 1.00; }
}
/* ───────── Bottom HUD ───────── */
.foot {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--mono);
font-size: 19px;
color: var(--hud-dim);
letter-spacing: 0.12em;
}
.foot .warn { color: var(--warn); }
#rig { transition: opacity 0.4s ease; }
/* ───────── Overlays ───────── */
#static {
position: absolute; inset: 0;
width: 100%; height: 100%;
pointer-events: none;
opacity: 0.045;
mix-blend-mode: screen;
image-rendering: pixelated;
}
.scanlines {
position: absolute; inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
rgba(0,0,0,0) 0px,
rgba(0,0,0,0) 2px,
rgba(0,0,0,0.20) 3px,
rgba(0,0,0,0.20) 4px
);
opacity: 0.5;
mix-blend-mode: multiply;
}
.vignette {
position: absolute; inset: 0;
pointer-events: none;
background: radial-gradient(
ellipse at center,
rgba(0,0,0,0) 45%,
rgba(0,0,0,0.60) 100%
);
}
.flicker {
position: absolute; inset: 0;
pointer-events: none;
background: rgba(0,0,0,0);
animation: flicker 11s ease-in-out infinite;
}
@keyframes flicker {
0%, 100% { background: rgba(0,0,0,0); }
18% { background: rgba(0,0,0,0); }
18.3% { background: rgba(0,0,0,0.06); }
18.5% { background: rgba(0,0,0,0); }
47% { background: rgba(0,0,0,0); }
47.3% { background: rgba(0,0,0,0.08); }
47.5% { background: rgba(0,0,0,0); }
78% { background: rgba(0,0,0,0); }
78.4% { background: rgba(0,0,0,0.05); }
78.7% { background: rgba(0,0,0,0); }
}
</style>
</head>
<body>
<header class="hud">
<div class="group">
<span><span class="led"></span>TRANSMISSION</span>
<span class="station">
<span class="station-id">OPHI-118</span>
<span class="sep">//</span>
<span class="rig" id="rigName">--</span>
</span>
</div>
<div class="group">
<span class="dim">SIGNAL</span>
<span id="signal">&#x25AE;&#x25AE;&#x25AE;&#x25AF;&#x25AF;</span>
</div>
<div class="group">
<span class="dim">UTC</span>
<span id="clock">--:--:--</span>
</div>
</header>
<main class="stage">
<svg class="mark" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="92" fill="none" stroke="#3ad1ff" stroke-width="1.2" opacity="0.35"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#3ad1ff" stroke-width="2.5"/>
<g stroke="#3ad1ff" stroke-width="2" opacity="0.55">
<line x1="100" y1="8" x2="100" y2="22"/>
<line x1="100" y1="178" x2="100" y2="192"/>
<line x1="8" y1="100" x2="22" y2="100"/>
<line x1="178" y1="100" x2="192" y2="100"/>
</g>
<polygon points="100,52 148,140 52,140"
fill="none" stroke="#e63a2e" stroke-width="3.5" stroke-linejoin="miter"/>
<circle cx="100" cy="116" r="4" fill="#ffd000"/>
</svg>
<div class="bars"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
</main>
<section class="standby">
<div class="big">PLEASE STAND BY</div>
<div class="sub">&mdash; DO NOT ADJUST YOUR RECEIVER &mdash;</div>
</section>
<footer class="foot">
<span>CH 118.0 MHz</span>
<span id="rig">&mdash; TELEMETRY &mdash;</span>
<span class="warn">&#x25B2; AUDIO ACTIVE</span>
</footer>
<canvas id="static"></canvas>
<div class="scanlines"></div>
<div class="vignette"></div>
<div class="flicker"></div>
<!-- Static device specs as a global. Generated by telemetry.sh.
Loaded via <script> instead of fetch() because OBS's CEF blocks
file:// → file:// fetch (Chromium "unique origin" rule). -->
<script src="telemetry.js"></script>
<script>
// Live UTC clock
const clockEl = document.getElementById('clock');
const pad = n => String(n).padStart(2, '0');
const tickClock = () => {
const d = new Date();
clockEl.textContent = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
};
tickClock();
setInterval(tickClock, 1000);
// Signal bar fluctuation — 3 bars typical, occasional drop or spike
const sigEl = document.getElementById('signal');
const FILL = '▮', EMPTY = '▯';
const drawSig = n => FILL.repeat(n) + EMPTY.repeat(5 - n);
setInterval(() => {
const r = Math.random();
const n = r < 0.06 ? 2 : r < 0.70 ? 3 : r < 0.95 ? 4 : 5;
sigEl.textContent = drawSig(n);
}, 1500);
// Animated CRT static (small buffer, scaled by browser)
const cv = document.getElementById('static');
const ctx = cv.getContext('2d');
const W = 320, H = 180;
cv.width = W; cv.height = H;
const img = ctx.createImageData(W, H);
const drawNoise = () => {
const d = img.data;
for (let i = 0; i < d.length; i += 4) {
const v = (Math.random() * 255) | 0;
d[i] = d[i+1] = d[i+2] = v;
d[i+3] = 255;
}
ctx.putImageData(img, 0, 0);
};
drawNoise();
// 8 fps — visually almost identical to 12 fps but ~33% less paint work.
setInterval(drawNoise, 1000 / 8);
// ───────── Rig name in the top HUD with periodic glitch-decode ─────────
// Reads telemetry.js's window.__TEL.rig (set by telemetry.sh per-rig).
// Behaviour: settles on the real name, then every ~9s briefly scrambles
// each letter through random glyphs and resolves back left-to-right. Same
// effect family as the cyberpunk "decoder" trope; pairs with the CRT
// aesthetic rather than fighting it.
const rigNameEl = document.getElementById('rigName');
const RIG_NAME = (window.__TEL && window.__TEL.rig) || 'UNKNOWN';
// ASCII-only glyph pool — tabular_nums keeps width stable in DejaVu Mono.
const GLITCH_GLYPHS = '01ABCDEFGHIJKLMNOPQRSTUVWXYZ#@*?!§%';
const glyph = () => GLITCH_GLYPHS[(Math.random() * GLITCH_GLYPHS.length) | 0];
const upperRig = RIG_NAME.toUpperCase();
// Initial reveal: scramble for ~400ms, then resolve l→r.
// Subsequent: small idle scramble ~every 9s.
function scrambleOnce({ initial = false } = {}) {
const len = upperRig.length;
if (len === 0) { rigNameEl.textContent = '--'; return; }
rigNameEl.classList.add('glitching');
const totalScrambleMs = initial ? 600 : 360;
const stepMs = 50;
const scrambleSteps = Math.max(1, Math.floor(totalScrambleMs / stepMs));
let step = 0;
const scrambleTimer = setInterval(() => {
let s = '';
for (let i = 0; i < len; i++) s += glyph();
rigNameEl.textContent = s;
if (++step >= scrambleSteps) {
clearInterval(scrambleTimer);
// Resolve left-to-right
let resolved = 0;
const resolveTimer = setInterval(() => {
resolved++;
let s = upperRig.slice(0, resolved);
for (let i = resolved; i < len; i++) s += glyph();
rigNameEl.textContent = s;
if (resolved >= len) {
clearInterval(resolveTimer);
rigNameEl.textContent = upperRig;
rigNameEl.classList.remove('glitching');
}
}, 60);
}
}, stepMs);
}
// First reveal happens shortly after page load; then idle re-glitch.
setTimeout(() => scrambleOnce({ initial: true }), 600);
setInterval(scrambleOnce, 9000);
// ───────── Telemetry: rotating real-data subtitle ─────────
// All lines derive from telemetry.js (window.__TEL), generated by telemetry.sh.
const rigEl = document.getElementById('rig');
const tel = window.__TEL || null;
// Compact CPU model: drop "(R)/(TM)" and "X-Core Processor" filler
const shortCpu = (m) => (m || '')
.replace(/\(R\)|\(TM\)/g, '')
.replace(/\s+\S+-Core\s+Processor\b/i, '')
.replace(/\s+Processor\b/i, '')
.replace(/\s+CPU\b.*$/i, '')
.replace(/\s+@.*$/, '')
.replace(/\s+/g, ' ')
.trim();
const lines = [
// CPU + MEM — one combined line
() => {
if (!tel?.cpu?.model) return null;
let s = `CPU · ${shortCpu(tel.cpu.model)}`;
if (tel.cpu.threads) s += ` · ${tel.cpu.threads}T`;
if (tel.mem?.totalGB) s += ` · MEM ${tel.mem.totalGB} GB`;
return s;
},
// GPU
() => {
if (!tel?.gpu?.name) return null;
let s = `GPU · ${tel.gpu.name}`;
if (tel.gpu.vramTotalMB) s += ` · ${(tel.gpu.vramTotalMB / 1024).toFixed(0)} GB VRAM`;
return s;
},
// OBS output: resolution, fps, encoder
() => {
const o = tel?.obs;
if (!o?.output?.w) return null;
let s = `OUTPUT · ${o.output.w}×${o.output.h}`;
if (o.fps) s += ` @ ${o.fps} FPS`;
if (o.stream?.encoder) s += ` · ${o.stream.encoder}`;
return s;
},
// OBS stream: rate control + bitrate + h.264 profile
() => {
const s = tel?.obs?.stream;
if (!s) return null;
const parts = [];
if (s.rateControl) parts.push(s.rateControl);
if (s.bitrateKbps) parts.push(`${s.bitrateKbps} kbps`);
if (s.profile) parts.push(s.profile.toUpperCase());
return parts.length ? `STREAM · ${parts.join(' · ')}` : null;
},
// Kernel
() => tel?.host?.kernel ? `KERNEL · ${tel.host.kernel}` : null,
];
let lineIdx = 0;
const renderLine = () => {
for (let i = 0; i < lines.length; i++) {
const v = lines[lineIdx]();
if (v) { rigEl.textContent = v; return; }
lineIdx = (lineIdx + 1) % lines.length;
}
rigEl.textContent = '— TELEMETRY OFFLINE —';
};
// Soft crossfade: 0.4s out → swap text → 0.4s in (matches the CSS transition)
const FADE_MS = 400;
const swapLine = () => {
rigEl.style.opacity = '0';
setTimeout(() => { renderLine(); rigEl.style.opacity = '1'; }, FADE_MS);
};
renderLine();
rigEl.style.opacity = '1';
setInterval(() => {
lineIdx = (lineIdx + 1) % lines.length;
swapLine();
}, 6000);
</script>
</body>
</html>