← back to Re Flyer Aggregator
full graphs on the dashboard (Chart.js 2D canvas — robust, no WebGL): items-pulled + deals by source, deals by method + market, broker ad-platforms, crawl breakdown; destroy-before-recreate so refresh never leaks; 6 charts verified rendering (TK-10708)
ae2397c7768f925a97dd9a4bb9d5fa38882b4716 · 2026-08-19 17:12:18 -0700 · Steve Abrams
Files touched
M public/sources/index.html
Diff
commit ae2397c7768f925a97dd9a4bb9d5fa38882b4716
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 19 17:12:18 2026 -0700
full graphs on the dashboard (Chart.js 2D canvas — robust, no WebGL): items-pulled + deals by source, deals by method + market, broker ad-platforms, crawl breakdown; destroy-before-recreate so refresh never leaks; 6 charts verified rendering (TK-10708)
---
public/sources/index.html | 42 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 40 insertions(+), 2 deletions(-)
diff --git a/public/sources/index.html b/public/sources/index.html
index f22e8e3..a939c75 100644
--- a/public/sources/index.html
+++ b/public/sources/index.html
@@ -39,12 +39,27 @@
.card{background:#fff;border-radius:10px;padding:16px 18px;box-shadow:0 2px 10px rgba(0,0,0,.05);margin-bottom:12px}
.kv{display:flex;gap:26px;flex-wrap:wrap} .kv div b{display:block;font-size:22px;color:var(--blue)} .kv div span{color:var(--gray);font-size:12px}
.links{margin:8px 0;font-size:13px} .links a{margin-right:14px}
-</style></head><body>
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
+ .grid .card h3{margin:0 0 8px;font-size:13px;color:var(--navy)} .grid .card{min-height:210px}
+ .grid canvas{max-height:230px}
+ @media(max-width:820px){.grid{grid-template-columns:1fr}}
+</style>
+<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
+</head><body>
<div id="nav"><div class="nh"><span>NAV AGENT</span><span><button id="navdock" title="dock left/right">⇄</button><button id="navcol" title="collapse">‹</button></span></div>
<div class="hint">FIELDS · CLICK A VALUE TO FILTER</div><div id="navfields"></div></div>
<header><h1>Where the data comes from</h1><div class="s">CRE deal pipeline · live provenance · Abrams</div><div class="flow" id="flow"></div></header>
<div class="wrap">
<div class="links" id="links"></div>
+ <h2>GRAPHS</h2>
+ <div class="grid">
+ <div class="card"><h3>Items pulled by source (top 12)</h3><canvas id="gPulled"></canvas></div>
+ <div class="card"><h3>CRE deals by source (top 12)</h3><canvas id="gDeals"></canvas></div>
+ <div class="card"><h3>Deals by pull method</h3><canvas id="gMethod"></canvas></div>
+ <div class="card"><h3>Deals by market</h3><canvas id="gMarket"></canvas></div>
+ <div class="card"><h3>Broker ad platforms (who advertises where)</h3><canvas id="gPlat"></canvas></div>
+ <div class="card"><h3>Broker crawl breakdown</h3><canvas id="gCrawl"></canvas></div>
+ </div>
<h2>DATA SOURCES — RSS feeds & proxies</h2>
<div class="controls"><input id="q" placeholder="search source…" size="18"><span class="clr" id="clr">clear filters</span><div class="stat" id="sstat"></div></div>
<table><thead><tr>
@@ -103,7 +118,29 @@ function renderSrc(){ if(!SRC)return;
}
document.querySelectorAll('th[data-k]').forEach(th=>th.onclick=()=>{const k=th.dataset.k;if(sortK===k)dir*=-1;else{sortK=k;dir=(k==='name'||k==='method'||k==='region')?1:-1;}renderSrc();});
document.getElementById('q').oninput=renderSrc;
-function loadSrc(){fetch('/sources.json?t='+Date.now()).then(r=>r.json()).then(d=>{SRC=d;renderFlow(d);buildNav();renderSrc();}).catch(()=>document.getElementById('sstat').textContent='sources load error');}
+// ---- charts (Chart.js 2D canvas — robust, destroy-before-recreate = no leak) ----
+const charts={};
+function mkBar(id,labels,data,color,horiz){ if(!window.Chart)return; if(charts[id])charts[id].destroy();
+ charts[id]=new Chart(document.getElementById(id),{type:'bar',data:{labels,datasets:[{data,backgroundColor:color,borderRadius:4}]},
+ options:{indexAxis:horiz?'y':'x',animation:{duration:400},plugins:{legend:{display:false}},scales:{x:{ticks:{font:{size:10}}},y:{ticks:{font:{size:10},autoSkip:false}}},maintainAspectRatio:false}});}
+function mkDon(id,labels,data,colors){ if(!window.Chart)return; if(charts[id])charts[id].destroy();
+ charts[id]=new Chart(document.getElementById(id),{type:'doughnut',data:{labels,datasets:[{data,backgroundColor:colors,borderWidth:0}]},
+ options:{animation:{duration:400},plugins:{legend:{position:'right',labels:{font:{size:11},boxWidth:12}}},maintainAspectRatio:false}});}
+function buildSrcCharts(){ if(!SRC||!window.Chart)return;
+ const bp=[...SRC.feeds].sort((a,b)=>b.pulled-a.pulled).slice(0,12); mkBar('gPulled',bp.map(f=>f.name),bp.map(f=>f.pulled),'#16357A',true);
+ const bd=[...SRC.feeds].filter(f=>f.deals).sort((a,b)=>b.deals-a.deals).slice(0,12); mkBar('gDeals',bd.map(f=>f.name),bd.map(f=>f.deals),'#0E7C4A',true);
+ const m={}; SRC.feeds.forEach(f=>m[f.method]=(m[f.method]||0)+f.deals); mkDon('gMethod',Object.keys(m),Object.values(m),['#0E7C4A','#8A5A00','#0b6fb8']);
+ const mk={}; SRC.feeds.forEach(f=>(f.samples||[]).forEach(s=>{if(s.market)mk[s.market]=(mk[s.market]||0)+1;}));
+ const mt=Object.entries(mk).sort((a,b)=>b[1]-a[1]).slice(0,10); mkBar('gMarket',mt.map(x=>x[0]),mt.map(x=>x[1]),'#E8A81C',false);
+}
+function buildCrawlCharts(cr){ if(!window.Chart)return; const done=cr.filter(r=>!r.error);
+ const P={google_ads:'Google',bing_ads:'Bing',meta:'Meta',linkedin:'LinkedIn',tiktok:'TikTok',twitter:'X',doubleclick:'DoubleClick',criteo:'Criteo',taboola:'Taboola',pinterest:'Pinterest'};
+ const nz=Object.keys(P).map(p=>[P[p],done.filter(r=>r[p]).length]).filter(x=>x[1]>0).sort((a,b)=>b[1]-a[1]);
+ mkBar('gPlat',nz.map(x=>x[0]),nz.map(x=>x[1]),'#0b6fb8',true);
+ const adv=done.filter(r=>r.paid_count>0).length, fly=done.filter(r=>r.flyer_count>0&&!r.paid_count).length, ga=done.filter(r=>r.google_analytics&&!r.paid_count&&!r.flyer_count).length;
+ mkDon('gCrawl',['Advertising','Flyers only','GA only','No signals'],[adv,fly,ga,Math.max(0,done.length-adv-fly-ga)],['#0E7C4A','#E8A81C','#8aa0c0','#dde3f0']);
+}
+function loadSrc(){fetch('/sources.json?t='+Date.now()).then(r=>r.json()).then(d=>{SRC=d;renderFlow(d);buildNav();renderSrc();buildSrcCharts();}).catch(()=>document.getElementById('sstat').textContent='sources load error');}
function loadCrawl(){fetch('/advertising/broker-ad-signals.json?t='+Date.now()).then(r=>r.json()).then(d=>{
const done=d.filter(r=>!r.error),adv=done.filter(r=>r.paid_count>0),fly=done.filter(r=>r.flyer_count>0),ga=done.filter(r=>r.google_analytics);
document.getElementById('crawl').innerHTML=`<div class="kv">
@@ -111,6 +148,7 @@ function loadCrawl(){fetch('/advertising/broker-ad-signals.json?t='+Date.now()).
<div><b>${fly.length}</b><span>with flyers/PDFs on site</span></div><div><b>${ga.length}</b><span>Google Analytics</span></div></div>
<div class="links"><a href="/advertising">open full broker ad-intel table ▸</a></div>
<div class="samp" style="margin-top:10px"><b>Advertisers:</b> ${adv.slice(0,14).map(r=>esc(r.domain)).join(' · ')||'—'}</div>`;
+ buildCrawlCharts(d);
}).catch(()=>document.getElementById('crawl').textContent='crawl data not available yet (running…)');}
applyDock(); loadSrc(); loadCrawl(); setInterval(()=>{loadSrc();loadCrawl();},20000);
</script></body></html>
← a4e3237 retire the 3D WebGL viz (Steve: bad UI, freezes, scroll-lock
·
back to Re Flyer Aggregator
·
auto-data-snapshot: 2026-08-19T17:18:59 (4 data files) — dat e5d7386 →