1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
|
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="dark"><title>🗺️ Today's Map — Memory Graph</title>
<style>*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden;background:#070b14;color:#e2e8f0;font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif}
#cv{position:fixed;inset:0;cursor:grab;touch-action:none}
.panel{position:fixed;background:rgba(13,20,36,.82);border:1px solid rgba(148,163,184,.15);border-radius:14px;backdrop-filter:blur(12px);box-shadow:0 12px 40px rgba(0,0,0,.45)}
#top{top:10px;left:10px;right:10px;height:50px;display:flex;align-items:center;gap:10px;padding:0 12px;z-index:20}
#brand{font-weight:700;font-size:15px;white-space:nowrap}#brand small{display:block;font-size:9.5px;color:#7c8db0;letter-spacing:.08em;text-transform:uppercase}
#counts{font-size:12px;color:#67e8f9;border:1px solid rgba(34,211,238,.3);background:rgba(8,47,73,.45);padding:4px 10px;border-radius:999px;white-space:nowrap}
#src{font-size:11px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px}
#btns{margin-left:auto;display:flex;gap:6px}
.btn{height:30px;padding:0 10px;border-radius:8px;border:1px solid rgba(148,163,184,.18);background:rgba(30,41,59,.6);color:#cbd5e1;font-size:12.5px;cursor:pointer;display:inline-flex;align-items:center;white-space:nowrap}
.btn:hover{background:rgba(51,65,85,.9);color:#fff;border-color:rgba(34,211,238,.4)}
#side{top:70px;left:10px;bottom:96px;width:300px;padding:13px;overflow-y:auto;z-index:15}
h3{font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:#7c8db0;margin:14px 0 7px}h3:first-child{margin-top:0}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:7px}
.st{background:rgba(30,41,59,.45);border:1px solid rgba(148,163,184,.12);border-radius:9px;padding:7px 9px}
.st b{font-size:15px;display:block}.st i{font-size:9.5px;color:#7c8db0;text-transform:uppercase;letter-spacing:.06em;font-style:normal}
.chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;margin:0 5px 5px 0;border-radius:999px;border:1px solid rgba(148,163,184,.16);background:rgba(30,41,59,.5);font-size:11.5px;cursor:pointer;color:#cbd5e1}
.chip .d{width:8px;height:8px;border-radius:50%}.chip.off{opacity:.32;filter:saturate(.2)}
.row{display:flex;align-items:center;gap:7px;font-size:12px;padding:4px 6px;border-radius:7px;color:#cbd5e1;min-width:0}
.row:hover{background:rgba(34,211,238,.08)}.row .d{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.row .l{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row .v{color:#7c8db0;font-size:11px}
.leg{display:flex;align-items:center;gap:7px;font-size:11px;color:#94a3b8;padding:2px 4px}
.leg .ln{width:18px;height:3px;border-radius:2px;flex-shrink:0}
#det{top:70px;right:10px;width:320px;max-height:calc(100% - 116px);padding:13px;overflow-y:auto;z-index:16;display:none}
#det.on{display:block}#dt{font-size:14px;font-weight:700;word-break:break-word;margin-bottom:6px}
.kv{display:flex;gap:8px;font-size:12px;padding:3px 0;border-bottom:1px dashed rgba(148,163,184,.1)}
.kv i{color:#7c8db0;width:74px;flex-shrink:0;font-style:normal}.kv b{font-weight:400;word-break:break-all}
.er{display:flex;gap:6px;align-items:center;font-size:11px;padding:4px 6px;border-radius:6px;color:#cbd5e1;min-width:0;cursor:pointer}
.er:hover{background:rgba(34,211,238,.08)}.er .t{color:#67e8f9;font-size:9.5px;background:rgba(8,47,73,.5);padding:1px 5px;border-radius:5px;flex-shrink:0}
.er .l{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#tl{left:10px;right:10px;bottom:10px;height:78px;padding:8px 12px;z-index:20;display:flex;gap:10px;align-items:center}
#tlcv{flex:1;height:58px;cursor:ew-resize;border-radius:8px}#tread{font-size:11px;color:#94a3b8;white-space:nowrap;width:200px;line-height:1.5}
#tip{position:fixed;pointer-events:none;background:rgba(2,6,17,.95);border:1px solid rgba(148,163,184,.28);border-radius:8px;padding:7px 9px;font-size:12px;z-index:50;display:none;max-width:260px}
#ov{position:fixed;inset:0;background:rgba(4,7,14,.85);display:flex;align-items:center;justify-content:center;z-index:100}
#ov .c{background:rgba(13,20,36,.95);border:1px solid rgba(148,163,184,.2);border-radius:16px;padding:26px;max-width:440px;text-align:center}
#ov .c h2{font-size:16px;margin-bottom:8px}#ov .c p{font-size:12px;color:#94a3b8;line-height:1.6}</style></head>
<body><canvas id="cv"></canvas>
<header id="top" class="panel"><div id="brand">🗺️ Today's Map<small id="date"></small></div>
<span id="counts">…</span><span id="src"></span>
<div id="btns"><button class="btn" id="bPlay">▶ Replay</button><button class="btn" id="bSpd">1×</button>
<button class="btn" id="bNow">Now</button><button class="btn" id="bFit">Fit</button><button class="btn" id="bPng">PNG</button></div></header>
<aside id="side" class="panel"><h3>Today</h3><div class="grid" id="stats"></div><h3>Types</h3><div id="types"></div>
<h3>Top today</h3><div id="tops"></div><h3>Edges</h3><div id="legend"></div></aside>
<div id="det" class="panel"><div id="dt"></div><div id="dk"></div><div id="de"></div></div>
<footer id="tl" class="panel"><canvas id="tlcv"></canvas><div id="tread"></div></footer>
<div id="tip"></div><div id="ov"><div class="c"><h2>Loading today's map…</h2><p id="ovm">Reading the live memory graph.</p></div></div>
<script>'use strict';
const $=id=>document.getElementById(id);
const COLORS={session:'#fbbf24',task:'#a78bfa',tool:'#34d399',file:'#38bdf8',type:'#fb7185',concept:'#f97316',fact:'#f472b6'};
const EC={failed:'#ef4444',has_fact:'#f472b6',executed:'#a78bfa',used:'#38bdf8',accessed:'#38bdf8',maps_to:'#94a3b8',tagged_as:'#94a3b8',involves:'#f97316',has_type:'#94a3b8'};
const EW={failed:2,has_fact:2,executed:1.6,used:1.4,accessed:1.4,maps_to:1.2,tagged_as:1.2,involves:1,has_type:1};
const S={nodes:[],edges:[],src:'',t0:0,t1:0,t:Infinity,playing:false,speed:1,pt:0,filt:{},sel:null,hover:null,view:{x:0,y:0,k:1},bk:[],fails:[]};
let cv,ctx,tlc,tx,dpr=1,W=0,H=0,TLW=0,TLH=0;
const gunzip=async b=>{const s=new Blob([b]).stream().pipeThrough(new DecompressionStream('gzip'));return new Uint8Array(await new Response(s).arrayBuffer());};
const b64u=s=>{const x=atob(s),u=new Uint8Array(x.length);for(let i=0;i<x.length;i++)u[i]=x.charCodeAt(i);return u;};
const isN=a=>Array.isArray(a)&&a.length&&a[0]&&typeof a[0]==='object'&&'id'in a[0]&&'type'in a[0]&&!('source'in a[0]);
const isE=a=>Array.isArray(a)&&a.length&&a[0]&&typeof a[0]==='object'&&'source'in a[0]&&'target'in a[0];
function hunt(o,pred,d){d=d||0;if(o==null||d>5)return null;
if(typeof o==='string'){if(o.length>50&&(o[0]==='['||o[0]==='{')){try{return hunt(JSON.parse(o),pred,d+1);}catch(e){}}return null;}
if(Array.isArray(o)){if(pred(o))return o;for(const x of o){const r=hunt(x,pred,d+1);if(r)return r;}return null;}
if(typeof o==='object'){const ks=Object.keys(o);
for(const k of ks){if(/nodes\.json$|edges\.json$/.test(k)){const r=hunt(o[k],pred,d+1);if(r)return r;}}
for(const k of ks){const r=hunt(o[k],pred,d+1);if(r)return r;}}
return null;}
async function loadModule(){const c=[];
try{if(window.MemoryGraphModule)c.push(window.MemoryGraphModule);}catch(e){}
try{if(window.modules&&window.modules['MemoryGraphModule'])c.push(window.modules['MemoryGraphModule']);}catch(e){}
for(const M of c){try{if(typeof M.getGraph==='function'){const g=M.getGraph();if(g&&g.nodes&&g.nodes.length)return{nodes:g.nodes,edges:g.edges||[],src:'live module'};}
if(M.nodes&&M.nodes.length)return{nodes:M.nodes,edges:M.edges||[],src:'live module'};}catch(e){}}
return null;}
async function loadLS(){for(const k of['webx11_filesystem_v2','webx11_filesystem']){let v=null;try{v=localStorage.getItem(k);}catch(e){}
if(!v||v.length<100)continue;let txt=v;try{txt=new TextDecoder().decode(await gunzip(b64u(v)));}catch(e){}
let j=null;try{j=JSON.parse(txt);}catch(e){}if(!j)continue;
const n=hunt(j,isN),e2=hunt(j,isE);if(n)return{nodes:n,edges:e2||[],src:'VFS · '+k};}return null;}
async function loadIDB(){const open=n=>new Promise((res,rej)=>{const q=indexedDB.open(n);q.onsuccess=()=>res(q.result);q.onerror=()=>rej(q.error||new Error('open'));});
const kOf=(db,sn)=>new Promise(res=>{const ks=[];try{const tx=db.transaction(sn,'readonly');const q=tx.objectStore(sn).openKeyCursor();q.onsuccess=()=>{const c=q.result;c?(ks.push(c.key),c.continue()):res(ks);};q.onerror=()=>res(ks);}catch(e){res(ks);}});
const get=(db,sn,k)=>new Promise(res=>{try{const tx=db.transaction(sn,'readonly');const q=tx.objectStore(sn).get(k);q.onsuccess=()=>res(q.result);q.onerror=()=>res(null);}catch(e){res(null);}});
const dec=async rec=>{if(!rec)return null;
for(const f of['content','data','body','value','text','blob']){let v=rec[f];if(v==null)continue;
if(typeof v==='string'){try{return JSON.parse(v);}catch(e){}continue;}
let b=v instanceof Uint8Array?v:(v instanceof ArrayBuffer?new Uint8Array(v):(v instanceof Blob?new Uint8Array(await v.arrayBuffer()):null));
if(!b)continue;try{return JSON.parse(new TextDecoder().decode(b));}catch(e){}
try{return JSON.parse(new TextDecoder().decode(await gunzip(b)));}catch(e){}}
return null;};
try{const db=await open('webx11_vfs');
for(const sn of[...db.objectStoreNames]){const ks=await kOf(db,sn);
const nh=ks.filter(k=>String(k).endsWith('nodes.json')),eh=ks.filter(k=>String(k).endsWith('edges.json'));
if(nh.length){const nn=await dec(await get(db,sn,nh[nh.length-1]));
const n=nn&&(Array.isArray(nn)?nn:nn.nodes)||null;let e2=null;
if(eh.length){const ee=await dec(await get(db,sn,eh[eh.length-1]));e2=ee&&(Array.isArray(ee)?ee:ee.edges)||null;}
if(n)return{nodes:n,edges:e2||[],src:'IndexedDB · webx11_vfs'};}}}catch(e){}
return null;}
function normalize(){const now=Date.now();
S.nodes.forEach((n,i)=>{n.acc=n.access_count||n.accessCount||0;n.cre=n.created||n.createdAt||now;
n.deg=0;n.vz={x:0,y:0};n.vis=null;n.lab=n.label||n.id||'';
const a=i*2.39996,r=40+9*Math.sqrt(i+1);n.px=W/2+r*Math.cos(a);n.py=H/2+r*Math.sin(a);
n.rad=4+Math.log2(1+n.acc)*2.2;
if(n.type==='session'){n.px=W/2;n.py=H/2;n.fix=true;n.rad=13;}});
const nm=new Map(S.nodes.map(n=>[n.id,n]));
S.edges.forEach(e=>{e.s=nm.get(e.source)||null;e.tt=nm.get(e.target)||null;
e.born=e.created||e.last_used||Math.max(e.s?e.s.cre:0,e.tt?e.tt.cre:0)||now;
if(e.s)e.s.deg++;if(e.tt)e.tt.deg++;});
const ts=S.nodes.map(n=>n.cre);S.t0=Math.min(...ts);S.t1=Math.max(...ts);S.t=S.t1+1;}
function computeStats(){const bs=15*60*1000,arr=[];const b0=Math.floor(S.t0/bs)*bs,b1=Math.ceil((S.t1+1)/bs)*bs;
for(let t=b0;t<b1;t+=bs)arr.push({t,c:{}});
for(const n of S.nodes){const i=Math.floor((n.cre-b0)/bs);if(i>=0&&i<arr.length)arr[i].c[n.type]=(arr[i].c[n.type]||0)+1;}
S.bk=arr;S.fails=[...new Set(S.edges.filter(e=>e.type==='failed').map(e=>e.born))];
const hrs={};S.nodes.forEach(n=>{const h=new Date(n.cre).getHours();hrs[h]=(hrs[h]||0)+1;});
S.busy=Object.entries(hrs).sort((a,b)=>b[1]-a[1])[0]||['-',0];
S.sumA=S.nodes.reduce((s,n)=>s+n.acc,0);S.hours=Math.max(0.1,(S.t1-S.t0)/3600000);}
const esc=s=>String(s).replace(/[<>&]/g,c=>({'<':'<','>':'>','&':'&'}[c]));
const fmt=ms=>{const d=new Date(ms);return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');};
const fmtS=ms=>{const d=new Date(ms);return fmt(ms)+':'+String(d.getSeconds()).padStart(2,'0');};
const kv=(k,v)=>`<div class="kv"><i>${k}</i><b>${v}</b></div>`;
const stH=(k,v)=>`<div class="st"><b>${v}</b><i>${k}</i></div>`;
function buildUI(){$('date').textContent=new Date(S.t0).toDateString();
$('counts').textContent=S.nodes.length+' nodes · '+S.edges.length+' edges';$('src').textContent=S.src;
$('stats').innerHTML=stH('Nodes',S.nodes.length)+stH('Edges',S.edges.length)+stH('First event',fmt(S.t0))+
stH('Span',S.hours.toFixed(1)+'h')+stH('Busiest hour',S.busy[0]+':00 ('+S.busy[1]+')')+stH('Accesses',S.sumA)+
stH('Growth',(S.nodes.length/S.hours).toFixed(1)+' n/h');
const tc={};S.nodes.forEach(n=>tc[n.type]=(tc[n.type]||0)+1);
$('types').innerHTML=Object.entries(tc).sort((a,b)=>b[1]-a[1]).map(([t,c])=>`<span class="chip" data-t="${t}"><span class="d" style="background:${COLORS[t]||'#94a3b8'}"></span>${t} · ${c}</span>`).join('');
[...$('types').children].forEach(ch=>ch.onclick=()=>{const t=ch.dataset.t;S.filt[t]=!S.filt[t];ch.classList.toggle('off');});
$('tops').innerHTML=[...S.nodes].sort((a,b)=>b.acc-a.acc).slice(0,8).map(n=>`<div class="row"><span class="d" style="background:${COLORS[n.type]||'#94a3b8'}"></span><span class="l">${esc(n.lab)}</span><span class="v">${n.acc}x</span></div>`).join('');
const ec={};S.edges.forEach(e=>ec[e.type]=(ec[e.type]||0)+1);
$('legend').innerHTML=Object.entries(ec).sort((a,b)=>b[1]-a[1]).map(([t,c])=>`<div class="leg"><span class="ln" style="background:${EC[t]||'#64748b'}"></span>${t} · ${c}${t==='failed'?' ⚠':''}</div>`).join('');}
const vis=n=>S.t>=n.cre&&!S.filt[n.type];
const evis=e=>e.s&&e.tt&&S.t>=e.born&&!S.filt[e.s.type]&&!S.filt[e.tt.type];
function step(){const N=S.nodes;
for(let i=0;i<N.length;i++){const a=N[i];
for(let j=i+1;j<N.length;j++){const b=N[j];
let dx=b.px-a.px,dy=b.py-a.py;const d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2),f=2600/d2;dx/=d;dy/=d;
a.vz.x-=dx*f;a.vz.y-=dy*f;b.vz.x+=dx*f;b.vz.y+=dy*f;}}
for(const e of S.edges){if(!e.s||!e.tt)continue;const rest=110/(0.4+(e.weight||0.5));
let dx=e.tt.px-e.s.px,dy=e.tt.py-e.s.py;const d=Math.sqrt(dx*dx+dy*dy)||0.01,f=(d-rest)*0.02;dx/=d;dy/=d;
e.s.vz.x+=dx*f;e.s.vz.y+=dy*f;e.tt.vz.x-=dx*f;e.tt.vz.y-=dy*f;}
for(const n of N){if(n.fix){n.vz.x=n.vz.y=0;continue;}
n.vz.x+=(W/2-n.px)*0.0035;n.vz.y+=(H/2-n.py)*0.0035;
n.px+=n.vz.x*0.5;n.py+=n.vz.y*0.5;n.vz.x*=0.82;n.vz.y*=0.82;}}
const shortLab=n=>{let s=String(n.lab);return s.length>26?s.slice(0,25)+'…':s;};
function draw(){ctx.setTransform(dpr,0,0,dpr,0,0);ctx.fillStyle='#070b14';ctx.fillRect(0,0,W,H);
ctx.translate(S.view.x,S.view.y);ctx.scale(S.view.k,S.view.k);
const tn=performance.now();
for(const e of S.edges){if(!evis(e))continue;
ctx.strokeStyle=EC[e.type]||'#64748b';ctx.globalAlpha=0.18+0.6*(e.weight||0.5);
ctx.lineWidth=Math.max(0.6,(EW[e.type]||1)*Math.min(1,S.view.k+0.4));
ctx.setLineDash(e.type==='failed'?[6,5]:[]);
ctx.beginPath();ctx.moveTo(e.s.px,e.s.py);ctx.lineTo(e.tt.px,e.tt.py);ctx.stroke();}
ctx.setLineDash([]);ctx.globalAlpha=1;
for(const n of S.nodes){if(!vis(n)){n.vis=null;continue;}
if(n.vis===null)n.vis=tn;const r=n.rad*Math.min(1,(tn-n.vis)/350);
if(n.type==='session'){const g=ctx.createRadialGradient(n.px,n.py,2,n.px,n.py,r*4.5);
g.addColorStop(0,'rgba(251,191,36,.5)');g.addColorStop(1,'rgba(251,191,36,0)');
ctx.fillStyle=g;ctx.beginPath();ctx.arc(n.px,n.py,r*4.5,0,7);ctx.fill();}
ctx.fillStyle=COLORS[n.type]||'#94a3b8';
ctx.beginPath();ctx.arc(n.px,n.py,Math.max(0.5,r),0,7);ctx.fill();
if(n===S.sel||n===S.hover){ctx.strokeStyle='#e2e8f0';ctx.lineWidth=1.6;ctx.beginPath();ctx.arc(n.px,n.py,r+3,0,7);ctx.stroke();}}
for(const n of S.nodes){if(!vis(n))continue;
if(n.type==='session'||n.deg>=7||n===S.hover||n===S.sel||n.acc>=10){
ctx.font=(n.type==='session'?12:10.5)+'px system-ui';ctx.textAlign='center';
ctx.fillStyle='rgba(226,232,240,.85)';ctx.fillText(shortLab(n),n.px,n.py-n.rad-5);}}}
function drawTL(){const w=TLW,h=TLH,pad=26;tx.setTransform(dpr,0,0,dpr,0,0);tx.clearRect(0,0,w,h);
const span=Math.max(1,S.t1-S.t0),X=t=>pad+(t-S.t0)/span*(w-2*pad);
const nb=S.bk.length,maxC=Math.max(1,...S.bk.map(b=>Object.values(b.c).reduce((a,b)=>a+b,0)));
const bw=Math.max(2,(w-2*pad)/nb-2);
for(const b of S.bk){let y=h-14;for(const t in b.c){const c=b.c[t],bh=c/maxC*(h-26);
tx.fillStyle=COLORS[t]||'#94a3b8';tx.globalAlpha=.75;tx.fillRect(X(b.t)-bw/2,y-bh,bw,bh);y-=bh;}}
tx.globalAlpha=1;tx.font='9px system-ui';tx.fillStyle='#7c8db0';tx.textAlign='center';
for(const b of S.bk){if(new Date(b.t).getMinutes()===0)tx.fillText(fmt(b.t),X(b.t),h-3);}
tx.fillStyle='#ef4444';tx.font='10px system-ui';for(const ft of S.fails)tx.fillText('✕',X(ft),10);
const mt=Math.min(S.t,S.t1);tx.strokeStyle='#22d3ee';tx.lineWidth=2;
tx.beginPath();tx.moveTo(X(mt),4);tx.lineTo(X(mt),h-14);tx.stroke();
tx.fillStyle='#22d3ee';tx.beginPath();tx.arc(X(mt),6,4,0,7);tx.fill();
let nv=0,ev=0;for(const n of S.nodes)if(vis(n))nv++;for(const e of S.edges)if(evis(e))ev++;
$('tread').innerHTML=`<b style="color:#67e8f9">${S.t>=S.t1?'now':fmt(mt)}</b><br>${nv}/${S.nodes.length} nodes · ${ev}/${S.edges.length} edges`;}
function advancePlay(){const dur=20000/S.speed;
if(!S.pt)S.pt=performance.now();const el=(performance.now()-S.pt)/dur;
if(el>=1){S.playing=false;S.pt=0;S.t=S.t1+1;$('bPlay').textContent='▶ Replay';return;}
S.t=S.t0+el*(S.t1-S.t0);}
function loop(){step();draw();drawTL();if(S.playing)advancePlay();requestAnimationFrame(loop);}
function fit(){let x0=1e9,y0=1e9,x1=-1e9,y1=-1e9,c=0;
for(const n of S.nodes){if(!vis(n))continue;c++;
x0=Math.min(x0,n.px-n.rad);x1=Math.max(x1,n.px+n.rad);y0=Math.min(y0,n.py-n.rad);y1=Math.max(y1,n.py+n.rad);}
if(!c)return;S.view.k=Math.max(0.25,Math.min(2.5,Math.min((W-80)/(x1-x0),(H-160)/(y1-y0))));
S.view.x=W/2-(x0+x1)/2*S.view.k;S.view.y=H/2-(y0+y1)/2*S.view.k+20;}
const toWorld=e=>({x:(e.clientX-S.view.x)/S.view.k,y:(e.clientY-S.view.y)/S.view.k});
function hit(x,y){let best=null,bd=1e9;
for(const n of S.nodes){if(!vis(n))continue;const dx=n.px-x,dy=n.py-y,d=dx*dx+dy*dy;
const rr=(n.rad+5)*(n.rad+5);if(d<rr&&d<bd){bd=d;best=n;}}return best;}
function tip(n,e){const t=$('tip');
if(!n){t.style.display='none';return;}
t.innerHTML=`<b>${esc(shortLab(n))}</b><br><span style="color:#7c8db0">${n.type} · ${fmtS(n.cre)}</span><br>accesses ${n.acc} · degree ${n.deg}`;
t.style.display='block';t.style.left=Math.min(innerWidth-270,e.clientX+14)+'px';t.style.top=(e.clientY+14)+'px';}
function showDet(n){S.sel=n;$('det').classList.add('on');$('dt').textContent=n.lab;
$('dk').innerHTML=kv('type',n.type)+kv('id',esc(n.id))+kv('created',fmtS(n.cre))+kv('accesses',n.acc)+kv('degree',n.deg)+
kv('decay score',(1-((Date.now()-n.cre)/86400000)*0.9/30).toFixed(3));
$('de').innerHTML='';S.edges.filter(e=>e.s===n||e.tt===n).slice(0,14).forEach(e=>{const o=e.s===n?e.tt:e.s;
const d=document.createElement('div');d.className='er';
d.innerHTML=`<span class="t">${e.type}</span><span class="l">${esc(o?o.lab:'?')}</span><span style="color:#7c8db0">${e.weight||''} ${e.use_count!=null?'·'+e.use_count+'x':''}</span>`;
d.onclick=()=>{if(o)showDet(o);};$('de').appendChild(d);});}
function sizeCanvases(){dpr=window.devicePixelRatio||1;W=innerWidth;H=innerHeight;
cv.width=W*dpr;cv.height=H*dpr;cv.style.width=W+'px';cv.style.height=H+'px';
const r=tlc.getBoundingClientRect();TLW=r.width;TLH=r.height;
tlc.width=TLW*dpr;tlc.height=TLH*dpr;tlc.style.width=TLW+'px';tlc.style.height=TLH+'px';}
function initUI(){let pd=null;
cv.addEventListener('pointerdown',e=>{cv.setPointerCapture(e.pointerId);
const w=toWorld(e);pd={x0:e.clientX,y0:e.clientY,vx:S.view.x,vy:S.view.y,n:hit(w.x,w.y),moved:0,lx:e.clientX,ly:e.clientY};});
cv.addEventListener('pointermove',e=>{if(pd){pd.moved+=Math.abs(e.clientX-pd.lx)+Math.abs(e.clientY-pd.ly);pd.lx=e.clientX;pd.ly=e.clientY;
const w=toWorld(e);
if(pd.n){pd.n.px=w.x;pd.n.py=w.y;pd.n.vz.x=pd.n.vz.y=0;}
else{S.view.x=pd.vx+(e.clientX-pd.x0);S.view.y=pd.vy+(e.clientY-pd.y0);}}
else{const w=toWorld(e);S.hover=hit(w.x,w.y);tip(S.hover,e);}});
cv.addEventListener('pointerup',()=>{if(pd&&pd.moved<4){if(pd.n)showDet(pd.n);else{S.sel=null;$('det').classList.remove('on');}}pd=null;});
cv.addEventListener('wheel',e=>{e.preventDefault();
const k2=Math.max(0.15,Math.min(4,S.view.k*Math.exp(-e.deltaY*0.0012)));
const wx=(e.clientX-S.view.x)/S.view.k,wy=(e.clientY-S.view.y)/S.view.k;
S.view.k=k2;S.view.x=e.clientX-wx*k2;S.view.y=e.clientY-wy*k2;},{passive:false});
let ts=false;
tlc.addEventListener('pointerdown',e=>{ts=true;tlc.setPointerCapture(e.pointerId);scrub(e);});
tlc.addEventListener('pointermove',e=>{if(ts)scrub(e);});
tlc.addEventListener('pointerup',()=>ts=false);
$('bPlay').onclick=()=>{if(S.playing){S.playing=false;$('bPlay').textContent='▶ Replay';}
else{S.playing=true;S.pt=0;S.t=S.t0;$('bPlay').textContent='⏸ Pause';}};
$('bSpd').onclick=()=>{S.speed=S.speed===1?2:S.speed===2?4:1;$('bSpd').textContent=S.speed+'×';};
$('bNow').onclick=()=>{S.playing=false;S.pt=0;S.t=S.t1+1;$('bPlay').textContent='▶ Replay';};
$('bFit').onclick=()=>fit();
$('bPng').onclick=()=>{const a=document.createElement('a');a.download='memgraph-today.png';a.href=cv.toDataURL('image/png');a.click();};
addEventListener('resize',()=>{sizeCanvases();fit();});}
function scrub(e){const r=tlc.getBoundingClientRect(),pad=26;
const x=Math.max(pad,Math.min(r.width-pad,e.clientX-r.left));
S.t=S.t0+(x-pad)/(r.width-2*pad)*(S.t1-S.t0);S.playing=false;$('bPlay').textContent='▶ Replay';}
async function init(){cv=$('cv');ctx=cv.getContext('2d');tlc=$('tlcv');tx=tlc.getContext('2d');
sizeCanvases();initUI();
const d=(await loadModule())||(await loadLS())||(await loadIDB());
if(!d||!d.nodes||!d.nodes.length){
$('ovm').innerHTML='Could not reach the memory graph.<br>Tried: MemoryGraphModule → VFS (localStorage) → IndexedDB.<br>Open this file inside newOS4 (preview) so it can read <code>/.memory_graph/</code>.';
return;}
S.nodes=d.nodes;S.edges=d.edges;S.src=d.src;
normalize();computeStats();buildUI();fit();loop();}
init();</script></body></html>
|