← back to Re Flyer Aggregator
interactive live viz (click color bar -> enter its records) + advertising.crcp viewer (sortable ad-intel table + CSV) + crawler = ALL CRE firms nationally, comprehensive ad detection (Bing/DoubleClick/Criteo/Taboola/etc) + flyer/PDF link extraction; gitignore generated crawl data (TK-10708)
3aabab21459fb5897d5f8efa76bc227a223c0542 · 2026-08-19 16:28:36 -0700 · Steve Abrams
Files touched
M .gitignoreA public/advertising/index.htmlM public/flyers/live.htmlM scripts/broker-ad-signals.mjs
Diff
commit 3aabab21459fb5897d5f8efa76bc227a223c0542
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 19 16:28:36 2026 -0700
interactive live viz (click color bar -> enter its records) + advertising.crcp viewer (sortable ad-intel table + CSV) + crawler = ALL CRE firms nationally, comprehensive ad detection (Bing/DoubleClick/Criteo/Taboola/etc) + flyer/PDF link extraction; gitignore generated crawl data (TK-10708)
---
.gitignore | 6 ++
public/advertising/index.html | 73 +++++++++++++++++++++++
public/flyers/live.html | 31 +++++++++-
scripts/broker-ad-signals.mjs | 134 +++++++++++++++++++++++++-----------------
4 files changed, 188 insertions(+), 56 deletions(-)
diff --git a/.gitignore b/.gitignore
index 519f80f..6ca02e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,9 @@ out/*.html
data/harvest-*.json
out/provenance-worklist-*.json
out/provenance-2*.json
+
+# generated crawl data (contains third-party public tracking IDs; regenerated by the crawler)
+public/advertising/broker-ad-signals.json
+public/advertising/broker-ad-signals.csv
+data/broker-ad-signals.json
+data/ad-scan.log
diff --git a/public/advertising/index.html b/public/advertising/index.html
new file mode 100644
index 0000000..92f2c73
--- /dev/null
+++ b/public/advertising/index.html
@@ -0,0 +1,73 @@
+<!doctype html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Broker Advertising Intel — CRCP</title>
+<style>
+ :root{--navy:#0E2350;--blue:#16357A;--gold:#E8A81C;--ink:#141414;--gray:#6B7280}
+ *{box-sizing:border-box} body{margin:0;font-family:Inter,-apple-system,Helvetica,Arial,sans-serif;background:#f4f6fb;color:var(--ink)}
+ header{background:var(--navy);color:#fff;padding:18px 26px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
+ header h1{margin:0;font-size:20px} header .s{color:var(--gold);font-size:12px}
+ .dl{background:var(--gold);color:#111;font-weight:700;font-size:13px;padding:8px 14px;border-radius:7px;text-decoration:none}
+ .controls{position:sticky;top:0;z-index:5;background:#fff;border-bottom:1px solid #e6e9f2;padding:12px 26px;display:flex;gap:14px;align-items:center;flex-wrap:wrap;font-size:13px}
+ .controls input,.controls select{padding:6px 9px;border:1px solid #d6dced;border-radius:6px;font-size:13px}
+ .stat{color:var(--gray);margin-left:auto;font-variant-numeric:tabular-nums}
+ table{border-collapse:collapse;width:100%;font-size:13px}
+ thead th{position:sticky;top:52px;background:#eef1f8;text-align:left;padding:9px 12px;color:#445;cursor:pointer;white-space:nowrap;border-bottom:2px solid #dde3f0}
+ td{padding:8px 12px;border-bottom:1px solid #eef0f6;vertical-align:top}
+ tr:hover td{background:#fbfcff}
+ a{color:var(--blue);text-decoration:none} a:hover{text-decoration:underline}
+ .chip{display:inline-block;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:4px;margin:1px 2px 1px 0}
+ .g{background:#0E7C4A} .b{background:#0b6fb8} .m{background:#1877F2} .l{background:#0a66c2} .t{background:#111} .x{background:#8A5A00}
+ .none{color:#c3c9d6} .flyer{color:#8A5A00;font-weight:600}
+ .paid{font-weight:800;color:#0E7C4A;text-align:center} .no{color:#c3c9d6;text-align:center}
+</style></head><body>
+<header>
+ <div><h1>Broker Advertising Intel</h1><div class="s">CRCP · who's running ads + flyers found on their sites · from page code</div></div>
+ <a class="dl" href="broker-ad-signals.csv" download>⬇ Download CSV</a>
+</header>
+<div class="controls">
+ <input id="q" placeholder="search firm / domain…" size="22">
+ <select id="filt">
+ <option value="">All brokers</option>
+ <option value="paid">Runs ANY paid ads</option>
+ <option value="google_ads">Google Ads</option>
+ <option value="bing_ads">Bing Ads</option>
+ <option value="meta">Meta</option>
+ <option value="linkedin">LinkedIn</option>
+ <option value="flyers">Has flyers/PDFs on site</option>
+ </select>
+ <div class="stat" id="stat"></div>
+</div>
+<table><thead><tr>
+ <th data-k="firm">Firm</th><th data-k="domain">Domain</th><th data-k="paid_count">Paid ads</th>
+ <th data-k="_plats">Platforms</th><th data-k="google_analytics">GA</th><th data-k="flyer_count">Flyers/PDFs</th>
+</tr></thead><tbody id="tb"></tbody></table>
+<script>
+const PLAT={google_ads:['g','Google'],bing_ads:['b','Bing'],meta:['m','Meta'],linkedin:['l','LinkedIn'],tiktok:['t','TikTok'],twitter:['t','X'],pinterest:['x','Pinterest'],reddit:['x','Reddit'],snap:['x','Snap'],amazon:['x','Amazon'],criteo:['x','Criteo'],taboola:['x','Taboola'],outbrain:['x','Outbrain'],adroll:['x','AdRoll'],doubleclick:['x','DoubleClick']};
+let DATA=[],sortK='paid_count',sortDir=-1;
+const plats=r=>Object.keys(PLAT).filter(p=>r[p]);
+function chips(r){const on=plats(r);return on.length?on.map(p=>`<span class="chip ${PLAT[p][0]}">${PLAT[p][1]}</span>`).join(''):'<span class="none">—</span>';}
+function flyerCell(r){if(!r.flyer_count)return '<span class="none">—</span>';
+ return `<span class="flyer">${r.flyer_count}</span> ` + (r.flyer_urls||[]).slice(0,3).map((u,i)=>`<a href="${u}" target="_blank" rel="noopener noreferrer">pdf${i+1}↗</a>`).join(' ');}
+function render(){
+ const q=document.getElementById('q').value.toLowerCase(),f=document.getElementById('filt').value;
+ let rows=DATA.filter(r=>!r.error);
+ if(q)rows=rows.filter(r=>((r.firm||'')+' '+r.domain).toLowerCase().includes(q));
+ if(f==='paid')rows=rows.filter(r=>r.paid_count>0);
+ else if(f==='flyers')rows=rows.filter(r=>r.flyer_count>0);
+ else if(f)rows=rows.filter(r=>r[f]);
+ rows.sort((a,b)=>{let x=a[sortK],y=b[sortK];if(sortK==='firm'||sortK==='domain'){return (''+x).localeCompare(''+y)*sortDir;}return ((x||0)-(y||0))*sortDir;});
+ document.getElementById('tb').innerHTML=rows.map(r=>`<tr>
+ <td><b>${(r.firm||'').replace(/</g,'<')||'—'}</b></td>
+ <td><a href="https://${r.domain}" target="_blank" rel="noopener noreferrer">${r.domain}</a></td>
+ <td class="${r.paid_count?'paid':'no'}">${r.paid_count||'·'}</td>
+ <td>${chips(r)}</td>
+ <td style="text-align:center">${r.google_analytics?'✓':'<span class="none">·</span>'}</td>
+ <td>${flyerCell(r)}</td></tr>`).join('');
+ const adv=rows.filter(r=>r.paid_count>0).length,fl=rows.filter(r=>r.flyer_count>0).length;
+ document.getElementById('stat').textContent=`${rows.length} brokers · ${adv} advertising · ${fl} with flyers`;
+}
+document.querySelectorAll('th').forEach(th=>th.onclick=()=>{const k=th.dataset.k;if(k==='_plats')return;if(sortK===k)sortDir*=-1;else{sortK=k;sortDir=(k==='firm'||k==='domain')?1:-1;}render();});
+document.getElementById('q').oninput=render;document.getElementById('filt').onchange=render;
+fetch('broker-ad-signals.json?t='+Date.now()).then(r=>r.json()).then(j=>{DATA=j;render();}).catch(e=>document.getElementById('stat').textContent='load error: '+e.message);
+setInterval(()=>fetch('broker-ad-signals.json?t='+Date.now()).then(r=>r.json()).then(j=>{DATA=j;render();}).catch(()=>{}),15000);
+</script></body></html>
diff --git a/public/flyers/live.html b/public/flyers/live.html
index 75042c8..3b8e56b 100644
--- a/public/flyers/live.html
+++ b/public/flyers/live.html
@@ -13,7 +13,16 @@
#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.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}
+ .rec{display:block;padding:10px 12px;border:1px solid #1c2540;border-radius:8px;margin-bottom:8px;text-decoration:none;color:#eaf0ff}
+ .rec:hover{border-color:#3a4a78;background:#111a30}
+ .rec .p{font-weight:800;color:#7fd7ff} .rec .t{font-size:13px;margin:3px 0;line-height:1.3}
+ .rec .m{color:#8aa;font-size:11px} .rec .src{color:#E8A81C;font-size:11px}
</style></head><body>
+<div id="drill"><span class="x" onclick="document.getElementById('drill').classList.remove('open')">✕</span><div id="drillbody"></div></div>
<canvas id="c"></canvas>
<div class="hud" id="top"><div id="title">CRE DEALS · LIVE<small>ABRAMS · FEEDS DROPPING INTO CATEGORIES</small></div>
<div id="stats"><b id="cnt">0</b> deals<br><span id="vol">$0</span> · <span id="mk">0 markets</span></div></div>
@@ -62,6 +71,8 @@ function drop(d){
const col=TYPE[d.type]||0x8aa0c0;
const cube=new THREE.Mesh(new THREE.BoxGeometry(BW,BH,BW),new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:.55,transparent:true,opacity:.96}));
cube.position.set(x, landY+46, 30); // spawn high + in FRONT (z=30)
+ cube.userData={deal:d,cat}; // clickable -> drill into this record
+ (dealsByCat[cat]=dealsByCat[cat]||[]).push(d);
colGroup.add(cube); stack[cat].push(cube);
drops.push({cube,x,landY,t:0});
// HUD
@@ -70,7 +81,25 @@ function drop(d){
S.mk.add(d.market||'US'); document.getElementById('mk').textContent=S.mk.size+' markets';
document.getElementById('last').innerHTML=`<span class="chip" style="background:${hex(col)}">${(d.type||'CRE').toUpperCase()}</span> <b>${d.price_label||money(d.price)}</b> ${d.market?'· '+d.market:''} — ${(d.title||'').slice(0,64)} <span style="color:#E8A81C">◂ ${d.source}</span>`;
}
-const S={cnt:0,vol:0,mk:new Set()};
+const S={cnt:0,vol:0,mk:new Set()}; const dealsByCat={};
+// ---- click a color bar / block to ENTER its records ----
+const ray=new THREE.Raycaster(), mouse=new THREE.Vector2();
+function onClick(ev){ const r=rnd.domElement.getBoundingClientRect();
+ mouse.x=((ev.clientX-r.left)/r.width)*2-1; mouse.y=-((ev.clientY-r.top)/r.height)*2+1;
+ ray.setFromCamera(mouse,cam); const hit=ray.intersectObjects(colGroup.children.filter(o=>o.userData&&o.userData.deal));
+ if(hit.length){ openDrill(hit[0].object.userData.cat, hit[0].object.userData.deal); }
+}
+rnd.domElement.addEventListener('click',onClick);
+function openDrill(cat,clicked){ const list=(dealsByCat[cat]||[]).slice().sort((a,b)=>(b.price||0)-(a.price||0));
+ const col=TYPE[clicked.type]||0x8aa0c0;
+ document.getElementById('drillbody').innerHTML=`<h3 style="color:${hex(col)}">${cat}</h3>
+ <div class="sub">${list.length} record${list.length!=1?'s':''} · ${money(list.reduce((s,d)=>s+(d.price||0),0))} total · click any to open source ↗</div>`+
+ list.map(d=>`<a class="rec" href="${d.link}" target="_blank" rel="noopener noreferrer" style="border-color:${d===clicked?hex(col):'#1c2540'}">
+ <span class="p">${d.price_label||money(d.price)}</span> <span class="m">· ${d.market||'US'} · ${(d.type||'CRE')}</span>
+ <div class="t">${(d.title||'').replace(/</g,'<')}</div>
+ <span class="src">◂ ${d.source}${d.buyer?' · buyer '+d.buyer:''}${d.seller?' · seller '+d.seller:''}</span></a>`).join('');
+ document.getElementById('drill').classList.add('open'); ctrl.autoRotate=false;
+}
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);
diff --git a/scripts/broker-ad-signals.mjs b/scripts/broker-ad-signals.mjs
index 9b243ec..6dfe4db 100644
--- a/scripts/broker-ad-signals.mjs
+++ b/scripts/broker-ad-signals.mjs
@@ -1,77 +1,101 @@
#!/usr/bin/env node
-// TK-10708 Ad-signal scan for CA/LA commercial brokers (ad-social-tracker Pass 1).
-// Slowly fetches each broker's front page and mines paid-ad tracking pixels (Google Ads,
-// Meta, TikTok, LinkedIn, etc.) via the ad-social-tracker regexes. READ-ONLY public pages,
-// rate-limited ("crawl slowly" — Steve). Competitive intel only (internal, not published).
+// TK-10708 Broker intel crawl: for every CA commercial broker, mine their page code for
+// (a) ANY ad-platform tracking pixel (Google/Bing/Meta/LinkedIn/TikTok/Twitter/Pinterest/
+// Reddit/Snap/Amazon/Criteo/Taboola/Outbrain/AdRoll/DoubleClick) and (b) flyer / OM / PDF
+// links on their site. Slow, polite, READ-ONLY public pages. Outputs JSON + CSV.
//
-// Usage: node scripts/broker-ad-signals.mjs [--n 20] [--delay 3500] [--state CA]
+// Usage: node scripts/broker-ad-signals.mjs [--n 300] [--delay 2500] [--state CA]
import { execFileSync } from 'node:child_process';
-import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const args = process.argv.slice(2);
const getArg = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
-const N = parseInt(getArg('--n', '20'), 10);
-const DELAY = parseInt(getArg('--delay', '3500'), 10); // slow: ~3.5s between fetches
+const N = parseInt(getArg('--n', '400'), 10);
+const DELAY = parseInt(getArg('--delay', '2500'), 10);
const STATE = getArg('--state', 'CA');
-const OUT = join(ROOT, 'data', 'broker-ad-signals.json');
+const JSONF = join(ROOT, 'public', 'advertising', 'broker-ad-signals.json');
+const CSVF = join(ROOT, 'public', 'advertising', 'broker-ad-signals.csv');
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
const sleep = ms => new Promise(r => setTimeout(r, ms));
-// ad-social-tracker pixel fingerprints (pure regex, no API/LLM)
-const SIG = {
- google_ads: /googleadservices\.com|AW-\d{6,}|google_conversion_id|gtag\(\s*['"]config['"]\s*,\s*['"]AW-/i,
- meta_pixel: /connect\.facebook\.net\/[^"']*fbevents\.js|fbq\(\s*['"]init['"]/i,
- tiktok_pixel: /analytics\.tiktok\.com|ttq\.load/i,
- pinterest_tag: /s\.pinimg\.com\/ct\.js|pintrk\(\s*['"]load['"]/i,
- linkedin_insight: /snap\.licdn\.com\/li\.lms-analytics|_linkedin_partner_id/i,
- twitter_pixel: /static\.ads-twitter\.com\/uwt\.js|twq\(\s*['"]init['"]/i,
- reddit_pixel: /redditstatic\.com\/ads\/pixel\.js|rdt\(\s*['"]init['"]/i,
- bing_uet: /bat\.bing\.com|\buetq\b/i,
- snap_pixel: /sc-static\.net\/scevent|snaptr\(\s*['"]init['"]/i,
- // measurement / analytics (NOT paid ads) — Steve asked to also flag GA + StatCounter
- google_analytics: /gtag\(\s*['"]config['"]\s*,\s*['"]G-|GTM-[A-Z0-9]+|google-analytics\.com\/analytics\.js|googletagmanager\.com\/gtag/i,
- statcounter: /statcounter\.com|sc_project|c\d+\.statcounter\.com|StatCounter/i,
+// ANY ad platform — signature lives in the page code (Steve). Bing included + native/programmatic.
+const AD = {
+ google_ads: /googleadservices\.com|AW-\d{6,}|google_conversion_id|gtag\(\s*['"]config['"]\s*,\s*['"]AW-|googleads\.g\.doubleclick/i,
+ bing_ads: /bat\.bing\.com|\buetq\b|uet_bing|\/bat\.js/i,
+ meta: /connect\.facebook\.net\/[^"']*fbevents\.js|fbq\(\s*['"]init['"]/i,
+ linkedin: /snap\.licdn\.com\/li\.lms-analytics|_linkedin_partner_id/i,
+ tiktok: /analytics\.tiktok\.com|ttq\.load/i,
+ twitter: /static\.ads-twitter\.com\/uwt\.js|twq\(\s*['"]init['"]/i,
+ pinterest: /s\.pinimg\.com\/ct\.js|pintrk\(\s*['"]load['"]/i,
+ reddit: /redditstatic\.com\/ads\/pixel\.js|rdt\(\s*['"]init['"]/i,
+ snap: /sc-static\.net\/scevent|snaptr\(\s*['"]init['"]/i,
+ amazon: /amazon-adsystem\.com|amzn\.to\/ads/i,
+ criteo: /criteo\.(com|net)|Criteo_/i,
+ taboola: /taboola\.com\/libtrc|_tfa\b/i,
+ outbrain: /outbrain\.com\/outbrain\.js|obApi\(/i,
+ adroll: /adroll\.com|__adroll/i,
+ doubleclick: /doubleclick\.net|dv360|fls\.doubleclick/i,
};
-const PAID = ['google_ads', 'meta_pixel', 'tiktok_pixel', 'pinterest_tag', 'linkedin_insight', 'twitter_pixel', 'reddit_pixel', 'bing_uet', 'snap_pixel'];
-const MEASURE = ['google_analytics', 'statcounter'];
+const google_analytics = /gtag\(\s*['"]config['"]\s*,\s*['"]G-|GTM-[A-Z0-9]+|google-analytics\.com\/analytics\.js|googletagmanager\.com\/gtag/i;
+const statcounter = /statcounter\.com|sc_project|StatCounter/i;
+const PLATS = Object.keys(AD);
-// distinct commercial broker domains from the usre firm registry
-// biggest firms first (highest agent_count) — most likely to advertise
-const rows = execFileSync('psql', ['usre', '-t', '-A', '-c',
- `SELECT website FROM firm WHERE hq_state='${STATE}' AND asset_class='commercial' AND website IS NOT NULL AND website<>'' ORDER BY agent_count DESC NULLS LAST LIMIT ${N * 4}`],
- { encoding: 'utf8' }).trim().split('\n').filter(Boolean);
-const domainOf = u => { try { return new URL(u.startsWith('http') ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return null; } };
-const domains = [...new Set(rows.map(domainOf).filter(Boolean))].slice(0, N);
+// flyer / OM / PDF links in the page code
+function flyers(html, base) {
+ const out = new Set();
+ const re = /href\s*=\s*["']([^"']+)["']/gi; let m;
+ while ((m = re.exec(html))) {
+ const href = m[1]; const low = href.toLowerCase();
+ const ctx = html.slice(Math.max(0, m.index - 80), m.index + 120).toLowerCase();
+ const isPdf = /\.pdf(\?|#|$)/.test(low);
+ const looksFlyer = /flyer|brochure|offering[-_ ]?mem|\bom\b|marketing[-_ ]?package|e-?brochure|property[-_ ]?package|listing[-_ ]?flyer|tour[-_ ]?book|deal[-_ ]?sheet|for[-_ ]?sale/.test(low + ' ' + ctx);
+ if (isPdf || (looksFlyer && /\.pdf|\/download|\/flyer|\/brochure|\/om/.test(low))) {
+ try { out.add(new URL(href, base).href); } catch {}
+ }
+ }
+ return [...out].slice(0, 8);
+}
-const prior = existsSync(OUT) ? JSON.parse(readFileSync(OUT, 'utf8')) : {};
-console.log(`Ad-signal scan: ${domains.length} ${STATE} commercial brokers, ${DELAY}ms apart (slow).`);
+const stateClause = STATE === 'ALL' ? '' : `AND hq_state='${STATE}'`;
+const rows = execFileSync('psql', ['usre', '-t', '-A', '-F', '\t', '-c',
+ `SELECT coalesce(name,''), website FROM firm WHERE asset_class='commercial' AND website IS NOT NULL AND website<>'' ${stateClause} ORDER BY agent_count DESC NULLS LAST LIMIT ${N * 3}`],
+ { encoding: 'utf8' }).trim().split('\n').filter(Boolean).map(l => l.split('\t'));
+const domOf = u => { try { return new URL(u.startsWith('http') ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return null; } };
+const seen = new Set(), targets = [];
+for (const [name, web] of rows) { const d = domOf(web); if (d && !seen.has(d)) { seen.add(d); targets.push({ name, domain: d }); } if (targets.length >= N) break; }
-for (const d of domains) {
+console.log(`Broker intel crawl: ${targets.length} ${STATE} commercial brokers, ${DELAY}ms apart (slow).`);
+const results = [];
+for (const t of targets) {
+ const rec = { firm: t.name, domain: t.domain };
try {
- const res = await fetch('https://' + d + '/', { headers: { 'User-Agent': UA }, redirect: 'follow', signal: AbortSignal.timeout(15000) });
+ const res = await fetch('https://' + t.domain + '/', { headers: { 'User-Agent': UA }, redirect: 'follow', signal: AbortSignal.timeout(15000) });
const html = await res.text();
- const sig = {}; for (const [k, re] of Object.entries(SIG)) sig[k] = re.test(html);
- sig.paid_ads_count = PAID.filter(k => sig[k]).length;
- sig.scanned_at = new Date().toISOString();
- prior[d] = sig;
- const on = PAID.filter(k => sig[k]).map(k => k.replace('_pixel', '').replace('_tag', '').replace('_insight', '').replace('_uet', '').replace('_ads', '-ads'));
- const meas = MEASURE.filter(k => sig[k]).map(k => k === 'google_analytics' ? 'GA' : 'StatCounter');
- console.log(` ${sig.paid_ads_count ? '💰' : ' '} ${d.padEnd(34)} ${on.join(', ') || '—'}${meas.length ? ' [' + meas.join(',') + ']' : ''}`);
- } catch (e) {
- prior[d] = { error: e.message.slice(0, 40), scanned_at: new Date().toISOString() };
- console.log(` ⚠ ${d.padEnd(34)} ${e.message.slice(0, 30)}`);
- }
- writeFileSync(OUT, JSON.stringify(prior, null, 1)); // persist after each (resumable)
+ for (const p of PLATS) rec[p] = AD[p].test(html);
+ rec.paid_count = PLATS.filter(p => rec[p]).length;
+ rec.google_analytics = google_analytics.test(html);
+ rec.statcounter = statcounter.test(html);
+ rec.flyer_urls = flyers(html, 'https://' + t.domain + '/');
+ rec.flyer_count = rec.flyer_urls.length;
+ const on = PLATS.filter(p => rec[p]);
+ console.log(` ${rec.paid_count ? '💰' : ' '}${rec.flyer_count ? '📄' : ' '} ${t.domain.padEnd(32)} ${on.join(',') || '-'}${rec.flyer_count ? ' ('+rec.flyer_count+' flyers)' : ''}`);
+ } catch (e) { rec.error = e.message.slice(0, 40); console.log(` ⚠ ${t.domain.padEnd(32)} ${e.message.slice(0, 30)}`); }
+ rec.scanned_at = new Date().toISOString();
+ results.push(rec);
+ writeFileSync(JSONF, JSON.stringify(results, null, 1)); // resumable
await sleep(DELAY);
}
-const scanned = Object.values(prior).filter(v => !v.error);
-const advertisers = scanned.filter(v => v.paid_ads_count > 0).length;
-const ga = scanned.filter(v => v.google_analytics).length, sc = scanned.filter(v => v.statcounter).length;
-console.log(`\nDone. ${scanned.length} scanned · ${advertisers} run PAID ads · ${ga} Google Analytics · ${sc} StatCounter. -> data/broker-ad-signals.json`);
-// list the advertisers found
-for (const [dom, v] of Object.entries(prior)) if (v.paid_ads_count > 0)
- console.log(` 💰 ${dom}: ${PAID.filter(k => v[k]).join(', ')}`);
+
+// CSV (easy list)
+const cols = ['firm', 'domain', ...PLATS, 'paid_count', 'google_analytics', 'statcounter', 'flyer_count', 'flyer_urls'];
+const esc = v => { v = v == null ? '' : Array.isArray(v) ? v.join(' ; ') : String(v); return /[",\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v; };
+const csv = [cols.join(','), ...results.map(r => cols.map(c => esc(c === 'google_analytics' || c === 'statcounter' || PLATS.includes(c) ? (r[c] ? 'yes' : '') : r[c])).join(','))].join('\n');
+writeFileSync(CSVF, csv);
+
+const adv = results.filter(r => r.paid_count > 0);
+console.log(`\nDone. ${results.filter(r => !r.error).length} scanned · ${adv.length} run ANY paid ads · ${results.filter(r => r.google_analytics).length} GA · ${results.filter(r => r.flyer_count > 0).length} have flyers/PDFs on site.`);
+console.log(`CSV -> public/flyers/broker-ad-signals.csv`);
← d5a10e2 live viz (reverted odd map -> clean XY bar chart w/ Z-depth)
·
back to Re Flyer Aggregator
·
FIX live.html crash: GPU memory leak (undisposed geometries/ 3c044a6 →