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:
2026-06-15 11:25:55 -04:00
parent c58fa9591b
commit a1e3e6852f
23 changed files with 789 additions and 70 deletions
+6
View File
@@ -158,6 +158,12 @@ img{display:block}
.guide-chcell .num{color:var(--red);font-weight:bold;font-variant-numeric:tabular-nums;min-width:24px}
.guide-chcell .nm{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.guide-chcell .skull{margin-left:auto;opacity:.55;font-size:12px}
.guide-chcell .pin{background:none;border:0;cursor:pointer;font-size:14px;line-height:1;padding:0 2px;
color:var(--ink-dim);margin-left:6px;flex:0 0 auto}
.guide-chcell .skull + .pin{margin-left:6px}
.guide-chcell .pin:not(.skull + .pin){margin-left:auto}
.guide-chcell .pin.on{color:var(--red);text-shadow:0 0 8px var(--red-glow)}
.guide-chcell .pin:hover{color:#fff}
.guide-progs{position:relative;flex:1}
.guide-prog{position:absolute;top:3px;bottom:3px;background:var(--panel-2);border:1px solid var(--line);
border-radius:4px;padding:5px 8px;font-size:12px;overflow:hidden;cursor:pointer;white-space:nowrap;text-overflow:ellipsis}