← 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
M .gitignoreM public/flyers/live.htmlA scripts/record-live.cjsA serve-viewers.mjs
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 →