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:
+16
-4
@@ -11,11 +11,14 @@ export class Zapper {
|
||||
* @param {object} els
|
||||
* @param {import('./player.js').Player} player
|
||||
* @param {import('./providers/source-provider.js').ChannelAggregator} agg
|
||||
* @param {{memory?:any, health?:import('./health.js').Health}} [opts]
|
||||
*/
|
||||
constructor(els, player, agg) {
|
||||
constructor(els, player, agg, opts = {}) {
|
||||
this.els = els;
|
||||
this.player = player;
|
||||
this.agg = agg;
|
||||
this.memory = opts.memory || null;
|
||||
this.health = opts.health || null;
|
||||
this.current = null; // current Channel
|
||||
this.previous = null; // for last-channel toggle
|
||||
this._entry = '';
|
||||
@@ -38,9 +41,16 @@ export class Zapper {
|
||||
async tuneToIndexOffset(delta) {
|
||||
const list = this.agg.channels;
|
||||
if (!list.length) return;
|
||||
let idx = this.current ? list.findIndex((c) => c.id === this.current.id) : -1;
|
||||
idx = (idx + delta + list.length) % list.length;
|
||||
return this.tune(list[idx]);
|
||||
const start = this.current ? list.findIndex((c) => c.id === this.current.id) : -1;
|
||||
let idx = start;
|
||||
// Step in the requested direction, skipping channels the Oracle knows are dead.
|
||||
for (let n = 0; n < list.length; n++) {
|
||||
idx = (idx + delta + list.length) % list.length;
|
||||
const ch = list[idx];
|
||||
if (!this.health || !this.health.isKnownDead(ch.id)) return this.tune(ch);
|
||||
}
|
||||
// everything known-dead → just tune the immediate neighbour
|
||||
return this.tune(list[(start + delta + list.length) % list.length]);
|
||||
}
|
||||
|
||||
async tuneLast() { if (this.previous) return this.tune(this.previous); }
|
||||
@@ -50,6 +60,8 @@ export class Zapper {
|
||||
if (!ch) return;
|
||||
if (this.current && this.current.id !== ch.id) this.previous = this.current;
|
||||
this.current = ch;
|
||||
if (this.memory) this.memory.rememberChannel(ch.id);
|
||||
if (this.health) this.health.probe(ch); // refresh this channel's reliability
|
||||
this._showStatic();
|
||||
this._safetyClear = setTimeout(() => this._clearStatic(), STATIC_MAX);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user