1// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2// saved-reports.js â Saved Alex reports (draft plans, class reads, asks) 3// 4// Dual persistence: 5// ⢠Local cache (WrStorage) â immediate, offline, survives the mock-auth 6// preview. Source of truth for instant UI. 7// ⢠Server sync (window.OD.saveAIAnalysis / loadAIHistory â ai_analysis 8// table, owner-scoped) â follows the user across devices when a real 9// session is present. Degrades to a no-op under mock/anon auth. 10// 11// Public API (window.WR.SavedReports): 12// listLocal(leagueId) â Array<Report> (sync) 13// save(leagueId, { title, prompt, content, kind }) â Report (async) 14// remove(leagueId, id) â void 15// syncFromServer(leagueId) â Array<Report> (async, merged) 16// 17// Report schema: 18// { id, title, prompt, content, kind, createdAt, server? } 19// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 20(function () { 21 // Resolve lazily: this plain script can load before window.App.WrStorage 22 // is initialized, so we must look it up at call time, not at module load. 23 function getStore() { return (window.App && window.App.WrStorage) || null; } 24 25 function key(leagueId) { return 'wr_saved_reports_' + (leagueId || 'global'); } 26 27 function readLocal(leagueId) { 28 const WrStorage = getStore(); 29 if (!WrStorage) return []; 30 const raw = WrStorage.get(key(leagueId), []); 31 return Array.isArray(raw) ? raw : []; 32 } 33 34 function writeLocal(leagueId, arr) { 35 const WrStorage = getStore(); 36 if (WrStorage) WrStorage.set(key(leagueId), arr.slice(0, 50)); 37 } 38 39 function genId() { 40 return 'r_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); 41 } 42 43 // Dedupe key so a locally-cached save and its server echo collapse into one row. 44 function dedupeKey(r) { 45 if (r.serverId) return 'sid:' + r.serverId; 46 if (typeof r.id === 'string' && r.id.indexOf('srv_') === 0) return 'sid:' + r.id.slice(4); 47 return (r.kind || '') + '|' + (r.title || '') + '|' + String(r.content || '').slice(0, 60); 48 } 49 50 function sortNewest(arr) { 51 return arr.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); 52 } 53 54 const API = { 55 listLocal(leagueId) { 56 return sortNewest(readLocal(leagueId)); 57 }, 58 59 async save(leagueId, { title, prompt, content, kind } = {}) { 60 const rec = { 61 id: genId(), 62 title: String(title || 'Report').trim().slice(0, 80), 63 prompt: String(prompt || ''), 64 content: String(content || ''), 65 kind: kind || 'report', 66 createdAt: Date.now(), 67 }; 68 const arr = readLocal(leagueId); 69 arr.unshift(rec); 70 writeLocal(leagueId, arr); 71 // Server sync. No-ops if not configured / not authed. Capture the 72 // new row id so this report can be deleted server-side later. 73 try { 74 if (window.OD && typeof window.OD.saveAIAnalysis === 'function') { 75 const serverId = await window.OD.saveAIAnalysis(leagueId, 'wr-' + rec.kind, rec.title, rec.content); 76 if (serverId) { 77 rec.serverId = serverId; 78 const cur = readLocal(leagueId); 79 const i = cur.findIndex(r => r.id === rec.id); 80 if (i >= 0) { cur[i].serverId = serverId; writeLocal(leagueId, cur); } 81 } 82 } 83 } catch (e) { 84 if (window.wrLog) window.wrLog('saved-reports.sync', e); 85 } 86 return rec; 87 }, 88 89 remove(leagueId, id) { 90 const arr = readLocal(leagueId); 91 const rec = arr.find(r => r.id === id); 92 let serverId = (rec && rec.serverId) || null; 93 // Server-only rows (from syncFromServer) carry the id as 'srv_<rowId>'. 94 if (!serverId && typeof id === 'string' && id.indexOf('srv_') === 0) serverId = id.slice(4); 95 if (serverId && window.OD && typeof window.OD.deleteAIAnalysis === 'function') { 96 try { window.OD.deleteAIAnalysis(serverId); } catch (e) { /* local removal still applies */ } 97 } 98 writeLocal(leagueId, arr.filter(r => r.id !== id)); 99 }, 100 101 async syncFromServer(leagueId) { 102 const local = readLocal(leagueId); 103 let server = []; 104 if (window.OD && typeof window.OD.loadAIHistory === 'function') { 105 try { 106 const rows = await window.OD.loadAIHistory(leagueId); 107 server = (Array.isArray(rows) ? rows : []) 108 .filter(r => typeof r.type === 'string' && r.type.indexOf('wr-') === 0) 109 .map(r => ({ 110 id: 'srv_' + r.id, 111 title: r.context_summary || 'Report', 112 prompt: '',
113 content: r.analysis || '', 114 kind: r.type.replace(/^wr-/, ''), 115 createdAt: r.created_at ? new Date(r.created_at).getTime() : Date.now(), 116 server: true, 117 })); 118 } catch (e) { 119 server = []; 120 } 121 } 122 const seen = new Set(); 123 const merged = []; 124 // Local first so local ids win on dedupe (keeps delete working in-session). 125 sortNewest(local.concat(server)).forEach(r => { 126 const k = dedupeKey(r); 127 if (seen.has(k)) return; 128 seen.add(k); 129 merged.push(r); 130 }); 131 return merged.slice(0, 50); 132 }, 133 }; 134 135 window.WR = window.WR || {}; 136 window.WR.SavedReports = API; 137})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.