// 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} */ 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();