feat: Phase 1.5 — health prober, Oracle's Memory, off-thread EPG, PWA
Resilience + scale pass on top of the v1 core. Stream health prober: - New SSRF-validated /probe endpoint (HEAD -> ranged-GET fallback, 6s timeout, 5-min TTL cache, concurrency-capped, no-store) + orb_probe_total metrics - Front-end health.js: lazy bounded-concurrency probing (current + sampled channels, not the whole catalog), LIVE/DEGRADED/DEAD scoring, guide skull glyph, instant no-signal on zap to a known-dead channel Oracle's Memory (memory.js): - Session resurrection (last channel, volume, mute, theater/dim/ambient) - Exponential-decay per-channel uptime history; pinning; dead-channel auto-skip Off-thread XMLTV: - lib/xmltv-parse.js (pure, hand-rolled scanner, no DOMParser/regex backtracking) run in epg-worker.js module worker; wired into the iptv-org provider - Unit-tested in node: entity decode, TZ offsets, 20k programmes off-thread Service worker / PWA: - manifest.webmanifest + orb icons; sw.js (cache-first shell + hls.js, stale-while-revalidate for JSON-over-relay, never caches streams) - CSP gains worker-src + manifest-src; relay sends no-store on /relay + /probe so the SW is the sole metadata freshness authority; webmanifest MIME fix Docs: README de-emojified (title only) and reframed as a peer parallel build. Verified: SSRF probes blocked, legit probe + cache, new headers present, PWA assets served, parser tests 8/8, JS syntax clean, zero Rust warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// guide.js — "The Almanac": 1994-style scrolling EPG overlay.
|
||||
// Virtualized rows (fixed height) so thousands of channels stay smooth.
|
||||
|
||||
import { memory } from './memory.js';
|
||||
|
||||
const ROW_H = 46;
|
||||
const CH_COL = 150;
|
||||
const HOUR_W = 180; // px per hour
|
||||
@@ -126,6 +128,19 @@ export class Guide {
|
||||
const sk = document.createElement('span'); sk.className = 'skull'; sk.textContent = '☠';
|
||||
sk.title = 'no signal'; cell.appendChild(sk);
|
||||
}
|
||||
// pin (Oracle's Memory) — click the rune to inscribe/banish a channel
|
||||
const pin = document.createElement('button');
|
||||
pin.className = 'pin' + (memory.isPinned(ch.id) ? ' on' : '');
|
||||
pin.textContent = memory.isPinned(ch.id) ? '★' : '☆';
|
||||
pin.setAttribute('aria-label', memory.isPinned(ch.id) ? 'Unpin channel' : 'Pin channel');
|
||||
pin.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const on = memory.togglePin(ch.id);
|
||||
pin.classList.toggle('on', on);
|
||||
pin.textContent = on ? '★' : '☆';
|
||||
pin.setAttribute('aria-label', on ? 'Unpin channel' : 'Pin channel');
|
||||
});
|
||||
cell.appendChild(pin);
|
||||
cell.addEventListener('click', () => { this.onTune(ch); this.hide(); });
|
||||
|
||||
const progs = document.createElement('div');
|
||||
|
||||
Reference in New Issue
Block a user