[object Object]

← back to Re Flyer Aggregator

sources dashboard: click-to-drill on every graph → modal of underlying records (deals + brokers), verified all 6 charts in Chrome

c543659fee3aeccd440d8d04db504bff64f383c7 · 2026-08-19 17:32:08 -0700 · Steve Abrams

Files touched

Diff

commit c543659fee3aeccd440d8d04db504bff64f383c7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 17:32:08 2026 -0700

    sources dashboard: click-to-drill on every graph → modal of underlying records (deals + brokers), verified all 6 charts in Chrome
---
 public/sources/index.html | 67 +++++++++++++++++++++++++++++++++++++----------
 1 file changed, 53 insertions(+), 14 deletions(-)

diff --git a/public/sources/index.html b/public/sources/index.html
index 94b1124..86a70ce 100644
--- a/public/sources/index.html
+++ b/public/sources/index.html
@@ -42,6 +42,18 @@
  .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}
+ .grid .card canvas{cursor:pointer}
+ /* ---- drill-down modal ---- */
+ .mask{position:fixed;inset:0;background:rgba(8,12,24,.55);z-index:60;display:none;align-items:flex-start;justify-content:center;padding:56px 16px}
+ .mask.show{display:flex}
+ .box{background:#fff;border-radius:12px;max-width:720px;width:100%;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.32)}
+ .mh{display:flex;justify-content:space-between;align-items:center;padding:15px 20px;border-bottom:1px solid #eef0f6}
+ .mh span{font-weight:800;color:var(--navy);font-size:15px} .mh .mc{color:var(--gray);font-weight:600;font-size:12px;margin-left:8px}
+ .mh button{background:#f4f6fb;border:1px solid #e0e5f0;border-radius:8px;cursor:pointer;font-size:14px;padding:4px 11px}
+ .mbody{overflow-y:auto;padding:6px 20px 18px}
+ .mrow{padding:9px 0;border-top:1px solid #f1f3f9;font-size:13px;line-height:1.5} .mrow:first-child{border-top:none}
+ .mrow .md{color:#8A5A00;font-size:12px;margin-right:6px} .mrow .mm{color:#9aa;font-size:12px} .mrow b{color:#0E7C4A}
+ .mtag{display:inline-block;background:#eef0f6;color:#445;border-radius:4px;padding:1px 7px;font-size:11px;margin:2px 4px 0 0}
  @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>
@@ -69,11 +81,33 @@
  <h2>DATABASE CRAWL — brokers (ads + flyers on their sites)</h2>
  <div class="card" id="crawl">Loading crawl status…</div>
 </div>
+<div id="modal" class="mask"><div class="box"><div class="mh"><span id="mtitle"></span><button id="mx" title="close">✕</button></div><div id="mbody" class="mbody"></div></div></div>
 <script>
 const MB={'RSS direct':'direct','RSS (social-UA)':'socialua','Google News proxy':'proxy'};
 let SRC=null,sortK='deals',dir=-1; let F={method:'',region:''};
 const esc=s=>(s||'').replace(/[<>&]/g,c=>({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]));
 const dt=s=>{if(!s)return'';const d=new Date(s);return isNaN(d)?(''+s).slice(0,11):d.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'2-digit'});};
+let CR=null; // broker-crawl records, kept for drill-down
+const PLAT={google_ads:'Google',bing_ads:'Bing',meta:'Meta',linkedin:'LinkedIn',tiktok:'TikTok',twitter:'X',pinterest:'Pinterest',reddit:'Reddit',snap:'Snap',amazon:'Amazon',criteo:'Criteo',taboola:'Taboola',outbrain:'Outbrain',adroll:'AdRoll',doubleclick:'DoubleClick'};
+// ---- drill-down modal: click any graph → the underlying records ----
+function openModal(title,count,items,kind){
+ document.getElementById('mtitle').innerHTML=esc(title)+(count!=null?` <span class="mc">${count} record${count===1?'':'s'}</span>`:'');
+ const b=document.getElementById('mbody');
+ if(!items||!items.length){ b.innerHTML='<div class="mrow" style="color:#9aa">no records for this slice</div>'; }
+ else if(kind==='broker'){
+  b.innerHTML=items.map(r=>{const dom=esc(r.domain||r.url||'');const href=r.url||('https://'+(r.domain||''));
+   const tags=Object.keys(PLAT).filter(k=>r[k]).map(k=>`<span class="mtag">${PLAT[k]}</span>`).join('');
+   return `<div class="mrow"><a href="${href}" target="_blank" rel="noopener">${dom}</a> ${r.flyer_count?`<span class="mtag">📄 ${r.flyer_count} flyer${r.flyer_count>1?'s':''}</span>`:''} ${r.google_analytics?'<span class="mtag">GA</span>':''} ${tags}</div>`;}).join('');
+ } else {
+  const s=items.slice().sort((a,b)=>(''+(b.date||'')).localeCompare(''+(a.date||'')));
+  b.innerHTML=s.map(x=>`<div class="mrow"><span class="md">${dt(x.date)}</span>${x.price?'<b>'+esc(x.price)+'</b> ':''}${x.link?`<a href="${x.link}" target="_blank" rel="noopener">${esc(x.title)}</a>`:esc(x.title)} <span class="mm">${esc(x.market||'')}</span></div>`).join('');
+ }
+ document.getElementById('modal').classList.add('show');
+}
+function closeModal(){document.getElementById('modal').classList.remove('show');}
+document.getElementById('mx').onclick=closeModal;
+document.getElementById('modal').onclick=e=>{if(e.target.id==='modal')closeModal();};
+document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal();});
 // ---- Nav Agent panel ----
 const NAVFIELDS=[['method','METHOD'],['region','MARKET']];
 function buildNav(){ if(!SRC)return; let h='';
@@ -121,28 +155,33 @@ document.querySelectorAll('th[data-k]').forEach(th=>th.onclick=()=>{const k=th.d
 document.getElementById('q').oninput=renderSrc;
 // ---- 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();
+function pickOpts(pick){return pick?{onClick:(e,els)=>{if(els.length)pick(els[0].index);},onHover:(e,els)=>{e.native.target.style.cursor=els.length?'pointer':'default';}}:{};}
+function mkBar(id,labels,data,color,horiz,pick){ 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();
+  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,...pickOpts(pick)}});}
+function mkDon(id,labels,data,colors,pick){ 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}});}
+  options:{animation:{duration:400},plugins:{legend:{position:'right',labels:{font:{size:11},boxWidth:12}}},maintainAspectRatio:false,...pickOpts(pick)}});}
 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 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,i=>openModal(bp[i].name+' — recent items',(bp[i].samples||[]).length,bp[i].samples||[],'deal'));
+ 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,i=>openModal(bd[i].name+' — recent deals',(bd[i].samples||[]).length,bd[i].samples||[],'deal'));
+ const m={}; SRC.feeds.forEach(f=>m[f.method]=(m[f.method]||0)+f.deals); const mkeys=Object.keys(m);
+ mkDon('gMethod',mkeys,Object.values(m),['#0E7C4A','#8A5A00','#0b6fb8'],i=>{const meth=mkeys[i];const s=[];SRC.feeds.filter(f=>f.method===meth).forEach(f=>(f.samples||[]).forEach(x=>s.push(x)));openModal('Method · '+meth,s.length,s,'deal');});
  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);
+ 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,i=>{const mkt=mt[i][0];const s=[];SRC.feeds.forEach(f=>(f.samples||[]).forEach(x=>{if(x.market===mkt)s.push(x);}));openModal('Market · '+mkt,s.length,s,'deal');});
 }
 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']);
+ const nz=Object.keys(PLAT).map(p=>[p,PLAT[p],done.filter(r=>r[p]).length]).filter(x=>x[2]>0).sort((a,b)=>b[2]-a[2]);
+ mkBar('gPlat',nz.map(x=>x[1]),nz.map(x=>x[2]),'#0b6fb8',true,i=>{const key=nz[i][0];const b=done.filter(r=>r[key]);openModal(nz[i][1]+' advertisers',b.length,b,'broker');});
+ const bAdv=done.filter(r=>r.paid_count>0), bFly=done.filter(r=>r.flyer_count>0&&!r.paid_count), bGa=done.filter(r=>r.google_analytics&&!r.paid_count&&!r.flyer_count), bNo=done.filter(r=>!r.paid_count&&!r.flyer_count&&!r.google_analytics);
+ const buckets=[['Advertising',bAdv],['Flyers only',bFly],['GA only',bGa],['No signals',bNo]];
+ mkDon('gCrawl',buckets.map(x=>x[0]),buckets.map(x=>x[1].length),['#0E7C4A','#E8A81C','#8aa0c0','#dde3f0'],i=>openModal('Brokers · '+buckets[i][0],buckets[i][1].length,buckets[i][1],'broker'));
 }
 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=>{
+function loadCrawl(){fetch('/advertising/broker-ad-signals.json?t='+Date.now()).then(r=>r.json()).then(d=>{ CR=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">
    <div><b>${d.length}</b><span>brokers crawled</span></div><div><b>${adv.length}</b><span>running paid ads</span></div>

← a6904f3 add RENTV posts (Steve's outlet) as a source — its recent RE  ·  back to Re Flyer Aggregator  ·  advertising viewer: add sortable Scanned date+time column (S 02c0a11 →