[object Object]

← back to Re Flyer Aggregator

harden live.html vs freeze: 35fps cap + pause-render-when-tab-hidden + pixelRatio 1.5 + removed expensive backdrop-filter + try/catch surfaces render errors (no more silent freeze) + WebGL-context-lost auto-recover; persistent pm2 viewer server (serve-viewers.mjs, re-flyers-viewers :9862) so /live.html + /advertising stay up (TK-10708)

c943015174b857f1ab6679522df67511b8f180b1 · 2026-08-19 16:59:20 -0700 · Steve Abrams

Files touched

Diff

commit c943015174b857f1ab6679522df67511b8f180b1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 16:59:20 2026 -0700

    harden live.html vs freeze: 35fps cap + pause-render-when-tab-hidden + pixelRatio 1.5 + removed expensive backdrop-filter + try/catch surfaces render errors (no more silent freeze) + WebGL-context-lost auto-recover; persistent pm2 viewer server (serve-viewers.mjs, re-flyers-viewers :9862) so /live.html + /advertising stay up (TK-10708)
---
 .gitignore              |  2 ++
 public/flyers/live.html | 28 ++++++++++++++++++----------
 scripts/record-live.cjs | 20 ++++++++++++++++++++
 serve-viewers.mjs       | 14 ++++++++++++++
 4 files changed, 54 insertions(+), 10 deletions(-)

diff --git a/.gitignore b/.gitignore
index 6ca02e9..e9e339c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,5 @@ public/advertising/broker-ad-signals.json
 public/advertising/broker-ad-signals.csv
 data/broker-ad-signals.json
 data/ad-scan.log
+out/*.webm
+out/*.mp4
diff --git a/public/flyers/live.html b/public/flyers/live.html
index b829308..e3f070a 100644
--- a/public/flyers/live.html
+++ b/public/flyers/live.html
@@ -13,7 +13,8 @@
   #ticker .row{margin-top:3px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
   .chip{font-size:10px;font-weight:800;padding:2px 8px;border-radius:5px;color:#070a14}
   #mode{position:fixed;top:64px;right:24px;z-index:6;pointer-events:auto;background:#111a30;color:#cfe0ff;border:1px solid #24304f;border-radius:7px;padding:6px 10px;font-size:12px}
-  #drill{position:fixed;top:0;right:0;bottom:0;width:380px;max-width:90vw;z-index:10;background:#0b1120f2;backdrop-filter:blur(6px);border-left:1px solid #24304f;transform:translateX(100%);transition:transform .25s;overflow-y:auto;padding:18px 20px;pointer-events:auto}
+  #drill{position:fixed;top:0;right:0;bottom:0;width:380px;max-width:90vw;z-index:10;background:#0b1120fb;border-left:1px solid #24304f;transform:translateX(100%);transition:transform .25s;overflow-y:auto;padding:18px 20px;pointer-events:auto}
+  #err{position:fixed;bottom:60px;left:24px;z-index:20;color:#ff8a8a;font-size:12px;pointer-events:none}
   #drill.open{transform:none}
   #drill h3{margin:0 0 2px;font-size:16px} #drill .sub{color:#8aa;font-size:12px;margin-bottom:12px}
   #drill .x{position:absolute;top:14px;right:16px;cursor:pointer;color:#8aa;font-size:20px}
@@ -27,6 +28,7 @@
 <select id="mode" title="category axis"><option value="type">Group by: Type</option><option value="market">Group by: Market</option><option value="source">Group by: Source (from where)</option></select>
 <div id="drill"><span class="x" onclick="document.getElementById('drill').classList.remove('open')">✕</span><div id="drillbody"></div></div>
 <div class="hud" id="ticker"><div class="lbl">Latest — from where · click a bar to drill in</div><div class="row" id="last">Loading deal flow…</div></div>
+<div id="err"></div>
 <script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}</script>
 <script type="module">
 import * as THREE from 'three';
@@ -40,7 +42,8 @@ const CAP=140;                      // hard cap on live cubes (prevents unbounde
 const scene=new THREE.Scene(); scene.fog=new THREE.FogExp2(0x070a14,0.004);
 const cam=new THREE.PerspectiveCamera(52,innerWidth/innerHeight,.1,2000); cam.position.set(0,42,86);
 const rnd=new THREE.WebGLRenderer({canvas:c,antialias:true,powerPreference:'high-performance'});
-rnd.setSize(innerWidth,innerHeight); rnd.setPixelRatio(Math.min(devicePixelRatio,2));
+rnd.setSize(innerWidth,innerHeight); rnd.setPixelRatio(Math.min(devicePixelRatio,1.5));
+c.addEventListener('webglcontextlost',e=>{e.preventDefault();document.getElementById('err').textContent='WebGL context lost — reloading…';setTimeout(()=>location.reload(),1500);});
 const ctrl=new OrbitControls(cam,rnd.domElement); ctrl.enableDamping=true; ctrl.target.set(0,16,0); ctrl.autoRotate=true; ctrl.autoRotateSpeed=.25; ctrl.maxPolarAngle=Math.PI/2.05;
 scene.add(new THREE.AmbientLight(0x99aaff,.6)); const dl=new THREE.DirectionalLight(0xffffff,.8); dl.position.set(20,60,40); scene.add(dl);
 scene.add(new THREE.GridHelper(200,40,0x1b2340,0x101830));
@@ -120,15 +123,20 @@ function openDrill(cat,clicked){ const list=(dealsByCat[cat]||[]).slice().sort((
 }
 
 window.__diag=()=>({cubes:cubes.length,drops:drops.length,geos:rnd.info.memory.geometries,texs:rnd.info.memory.textures,calls:rnd.info.render.calls});
-let pulse=0;
-function anim(){ requestAnimationFrame(anim); ctrl.update();
-  for(let i=drops.length-1;i>=0;i--){ const f=drops[i]; f.t=Math.min(1,f.t+.035); const e=f.t*f.t;
-    f.cube.position.y=(f.landY+46)+(f.landY-(f.landY+46))*e; f.cube.position.z=30*(1-e);
-    if(f.t>=1){ f.cube.position.set(f.x,f.landY,0); drops.splice(i,1); } }
-  pulse+=.02; for(const k in typeMat) typeMat[k].emissiveIntensity=.55+Math.sin(pulse)*.15;
-  rnd.render(scene,cam);
+let pulse=0,lastT=0;
+function anim(now){ requestAnimationFrame(anim);
+  if(document.hidden) return;                 // pause render when tab not visible (prevents runaway)
+  if(now-lastT<28) return; lastT=now;          // cap ~35fps (retina-safe)
+  try{
+    ctrl.update();
+    for(let i=drops.length-1;i>=0;i--){ const f=drops[i]; f.t=Math.min(1,f.t+.05); const e=f.t*f.t;
+      f.cube.position.y=(f.landY+46)+(f.landY-(f.landY+46))*e; f.cube.position.z=30*(1-e);
+      if(f.t>=1){ f.cube.position.set(f.x,f.landY,0); drops.splice(i,1); } }
+    pulse+=.03; for(const k in typeMat) typeMat[k].emissiveIntensity=.55+Math.sin(pulse)*.15;
+    rnd.render(scene,cam);
+  }catch(err){ document.getElementById('err').textContent='render error: '+(err&&err.message||err); }
 }
-anim();
+requestAnimationFrame(anim);
 addEventListener('resize',()=>{cam.aspect=innerWidth/innerHeight;cam.updateProjectionMatrix();rnd.setSize(innerWidth,innerHeight);});
 document.getElementById('mode').onchange=()=>rebuild(document.getElementById('mode').value,false);
 
diff --git a/scripts/record-live.cjs b/scripts/record-live.cjs
new file mode 100644
index 0000000..3ab8f26
--- /dev/null
+++ b/scripts/record-live.cjs
@@ -0,0 +1,20 @@
+const { chromium } = require('/Users/macstudio3/Projects/nationalrealestate/node_modules/playwright');
+(async () => {
+  const out = require('path').join(process.cwd(), 'out');
+  const browser = await chromium.launch({ args: ['--use-gl=angle','--use-angle=swiftshader','--enable-webgl','--ignore-gpu-blocklist'] });
+  const ctx = await browser.newContext({ viewport:{width:1280,height:800}, recordVideo:{ dir: out, size:{width:1280,height:800} } });
+  const page = await ctx.newPage();
+  await page.goto('http://127.0.0.1:9862/live.html', { waitUntil:'domcontentloaded' });
+  await page.waitForTimeout(6500);                         // bars drop in
+  for (const m of ['market','source','type']) { await page.selectOption('#mode', m); await page.waitForTimeout(2600); }
+  // click a bar to open the drill (sweep the canvas via the page's own handler)
+  await page.evaluate(() => { const cv=document.querySelector('canvas'),r=cv.getBoundingClientRect();
+    for(let x=r.left+40;x<r.right-40;x+=20)for(let y=r.top+120;y<r.bottom-60;y+=20){
+      cv.dispatchEvent(new MouseEvent('click',{clientX:x,clientY:y,bubbles:true}));
+      if(document.getElementById('drill').classList.contains('open'))return; } });
+  await page.waitForTimeout(3500);                          // show drill panel
+  await page.click('#drill .x').catch(()=>{});              // close it
+  await page.waitForTimeout(1500);
+  await ctx.close(); await browser.close();
+  console.log('recorded to out/*.webm');
+})();
diff --git a/serve-viewers.mjs b/serve-viewers.mjs
new file mode 100644
index 0000000..a629184
--- /dev/null
+++ b/serve-viewers.mjs
@@ -0,0 +1,14 @@
+import http from 'node:http'; import { readFileSync, statSync } from 'node:fs'; import { join, extname, dirname } from 'node:path'; import { fileURLToPath } from 'node:url';
+const PUB = join(dirname(fileURLToPath(import.meta.url)), 'public');
+const FLY = join(PUB, 'flyers'), ADV = join(PUB, 'advertising');
+const MIME = {'.html':'text/html','.json':'application/json','.csv':'text/csv','.js':'text/javascript','.css':'text/css','.png':'image/png'};
+http.createServer((q,s)=>{
+  let u = decodeURIComponent(q.url.split('?')[0]);
+  let base = FLY, path = u;                                   // default: the live/flyers viewer
+  if (u==='/' ) path = '/live.html';
+  else if (u.startsWith('/advertising')) { base = ADV; path = u.replace(/^\/advertising\/?/, '/') || '/index.html'; if(path==='/')path='/index.html'; }
+  let fp = join(base, path);
+  try { if (statSync(fp).isDirectory()) fp = join(fp,'index.html'); } catch {}
+  try { const b = readFileSync(fp); s.writeHead(200,{'content-type':MIME[extname(fp)]||'text/html','cache-control':'no-store','access-control-allow-origin':'*'}); s.end(b); }
+  catch(e){ s.writeHead(404); s.end('not found: '+u); }
+}).listen(9862, ()=>console.log('viewers on :9862 — / and /live.html = flyers viz, /advertising = ad intel'));

← 2af6800 auto-data-snapshot: 2026-08-19T16:46:39 (6 data files) — dat  ·  back to Re Flyer Aggregator  ·  pivot to 'where the data comes from' dashboard (Steve: show c9ace0f →