Files
pondering-orb/web/lexicon.js
T
Diablo_Rain c58fa9591b feat: initial release of The Pondering Orb
A retro CRT-themed web TV that channel-surfs live HLS streams through a
security-hardened, same-origin Rust relay.

Front end (vanilla JS + HLS.js, no framework/build step):
- SourceProvider abstraction + canonical Channel/Program model so public
  streams and future self-hosted media share one UI (MockProvider built first
  to keep the UI provably source-agnostic; IptvOrgProvider for live data)
- Red/black phosphor CRT theme, virtualized 1994-style guide (The Almanac),
  cable-box zapping with 7-segment OSD, scrying-static transitions
- Theater/Dim/Ambient immersion modes, dead-stream "orb clouds over" handling
- The Lexicon in-world naming with mundane-mode; full a11y + reduced-motion
- HLS.js pinned + Subresource-Integrity verified

Rust relay (hyper 1.x + tokio-rustls/ring + hickory-resolver):
- Serves the SPA and proxies HLS from one origin (no CORS, untainted canvas)
- SSRF defense: resolve -> reject private/loopback/link-local/metadata IPs ->
  connect to the validated IP (no re-resolve, defeats DNS-rebind), fail-closed
- HTTPS-only egress, re-validated redirects, manifest URL rewrite, size/
  concurrency caps, strict CSP + security headers, path-traversal guard
- /metrics observability + structured logs (host only, never raw URLs)

Verified: all SSRF probes rejected, CSP clean, real Apple HLS manifest
rewritten, ~39k iptv-org channels fetched end-to-end, tests green, zero warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 05:34:09 -04:00

56 lines
2.0 KiB
JavaScript

// lexicon.js — in-world UI vocabulary (upgrade #3).
//
// Lore names are flavor layered on top of clear function. Visual labels get the
// lore word; ARIA labels ALWAYS stay plain-English (set directly in markup), so
// screen readers and the "mundane mode" toggle never lose meaning.
export const LEXICON = {
guide: 'The Almanac',
guideShort: 'Almanac',
favorites: 'The Grimoire',
search: 'Scry',
channelEntry: 'Incantation',
sleepTimer: 'The Orb Dims',
volume: 'Resonance',
noSignal: 'THE ORB CLOUDS OVER',
noSignalSub: 'no signal — seeking another vision',
loading: 'Summoning…',
nextUp: 'What Fate Foretells',
live: 'LIVE',
help: 'Incantations', // keyboard-help title
theater: 'Theater',
dim: 'Dim the Hall',
ambient: 'Orb Glow',
brand: 'THE PONDERING ORB',
};
const PLAIN = {
guide: 'TV Guide', guideShort: 'Guide', favorites: 'Favorites', search: 'Search',
channelEntry: 'Channel', sleepTimer: 'Sleep timer', volume: 'Volume',
noSignal: 'NO SIGNAL', noSignalSub: 'stream unavailable — trying another',
loading: 'Loading…', nextUp: 'Next up', live: 'LIVE', help: 'Keyboard shortcuts',
theater: 'Theater', dim: 'Dim', ambient: 'Ambient glow', brand: 'THE PONDERING ORB',
};
const KEY = 'orb.mundaneMode';
let mundane = (() => { try { return localStorage.getItem(KEY) === '1'; } catch { return false; } })();
/** @param {string} k @returns {string} */
export function lex(k) { return (mundane ? PLAIN : LEXICON)[k] ?? PLAIN[k] ?? k; }
export function isMundane() { return mundane; }
export function setMundane(on) {
mundane = !!on;
try { localStorage.setItem(KEY, mundane ? '1' : '0'); } catch { /* ignore */ }
applyLexicon();
}
/** Fill every [data-lex] element's textContent. Never touches aria-label. */
export function applyLexicon(root = document) {
for (const el of root.querySelectorAll('[data-lex]')) {
const k = el.getAttribute('data-lex');
el.textContent = lex(k);
}
}