Files
pondering-orb/web/memory.js
T
Diablo_Rain a1e3e6852f 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>
2026-06-15 11:25:55 -04:00

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();