a1e3e6852f
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>
79 lines
2.7 KiB
JavaScript
79 lines
2.7 KiB
JavaScript
// memory.js — "The Oracle's Memory" (upgrade #5).
|
|
//
|
|
// Local-first persistence: session resurrection (last channel / volume / modes),
|
|
// per-channel rolling uptime history (feeds the guide reliability glyphs and
|
|
// auto-skip), and pinned channels. Everything in localStorage, namespaced.
|
|
|
|
const NS = 'orb.';
|
|
const K = {
|
|
settings: NS + 'settings',
|
|
health: NS + 'health',
|
|
pins: NS + 'pins',
|
|
};
|
|
const HISTORY_DECAY = 0.85; // exponential weighting toward recent samples
|
|
|
|
function read(key, fallback) {
|
|
try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; }
|
|
catch { return fallback; }
|
|
}
|
|
function write(key, val) {
|
|
try { localStorage.setItem(key, JSON.stringify(val)); } catch { /* quota / private mode */ }
|
|
}
|
|
|
|
class OracleMemory {
|
|
constructor() {
|
|
this.settings = read(K.settings, {
|
|
lastChannelId: null,
|
|
volume: 1,
|
|
muted: false,
|
|
modes: { theater: false, dim: false, ambient: false },
|
|
});
|
|
/** @type {Object<string,{score:number,samples:number,last:string,ts:number}>} */
|
|
this.health = read(K.health, {});
|
|
this.pins = new Set(read(K.pins, []));
|
|
}
|
|
|
|
// ---- session resurrection ----
|
|
get lastChannelId() { return this.settings.lastChannelId; }
|
|
rememberChannel(id) { this.settings.lastChannelId = id; this._saveSettings(); }
|
|
rememberVolume(volume, muted) {
|
|
this.settings.volume = volume; this.settings.muted = muted; this._saveSettings();
|
|
}
|
|
rememberModes(modes) { this.settings.modes = { ...modes }; this._saveSettings(); }
|
|
_saveSettings() { write(K.settings, this.settings); }
|
|
|
|
// ---- uptime history (fed by health probes) ----
|
|
/** @param {string} id @param {'live'|'degraded'|'dead'} state */
|
|
recordHealth(id, state) {
|
|
const ok = state === 'live' ? 1 : state === 'degraded' ? 0.5 : 0;
|
|
const prev = this.health[id];
|
|
const score = prev ? prev.score * HISTORY_DECAY + ok * (1 - HISTORY_DECAY) : ok;
|
|
this.health[id] = {
|
|
score,
|
|
samples: (prev?.samples || 0) + 1,
|
|
last: state,
|
|
ts: Date.now(),
|
|
};
|
|
this._healthDirty = true;
|
|
this._scheduleHealthSave();
|
|
}
|
|
_scheduleHealthSave() {
|
|
if (this._healthT) return;
|
|
this._healthT = setTimeout(() => { this._healthT = 0; write(K.health, this.health); }, 1500);
|
|
}
|
|
/** @returns {{score:number,samples:number,last:string,ts:number}|null} */
|
|
reliability(id) { return this.health[id] || null; }
|
|
isKnownDead(id) { return this.health[id]?.last === 'dead'; }
|
|
|
|
// ---- pins ----
|
|
isPinned(id) { return this.pins.has(id); }
|
|
togglePin(id) {
|
|
this.pins.has(id) ? this.pins.delete(id) : this.pins.add(id);
|
|
write(K.pins, [...this.pins]);
|
|
return this.pins.has(id);
|
|
}
|
|
pinned() { return [...this.pins]; }
|
|
}
|
|
|
|
export const memory = new OracleMemory();
|