← back to Ad Signals Rolodex

build-rolodex.py

207 lines

#!/usr/bin/env python3
"""Unified ad-signals rolodex — combines all 5 cohorts into one HTML viewer.
ventura-corridor + lawyer + animals + doctor + lacountyeats. No LLM.
"""
import csv, json, os, sys
from datetime import datetime
from collections import Counter

ROOT = os.path.expanduser('~/Projects/ad-signals-rolodex')
os.makedirs(ROOT, exist_ok=True)
OUT = f'{ROOT}/rolodex.html'

SOURCES = [
    {
        'project': 'ventura-corridor',
        'label': 'LA Local Biz',
        'color': '#f5b800',
        'csv': os.path.expanduser('~/Projects/ventura-corridor/exports/paid-ads-targets-2026-05-05.csv'),
        'fields': {
            'name': 'name', 'category': 'category', 'city': 'city', 'state': lambda r: 'CA',
            'website': 'website', 'paid_pixels': 'paid_pixels', 'tier': 'tier',
        },
    },
    {
        'project': 'lawyer',
        'label': 'CA Law Firms',
        'color': '#ff7070',
        'csv': os.path.expanduser('~/Projects/lawyer-directory-builder/exports/lawyer-paid-ads-targets-2026-05-05.csv'),
        'fields': {
            'name': 'name', 'category': lambda r: 'law firm', 'city': 'city', 'state': 'state',
            'website': 'website', 'paid_pixels': 'paid_pixels', 'tier': 'tier',
        },
    },
    {
        'project': 'animals',
        'label': 'Pet Industry',
        'color': '#88dd88',
        'csv': os.path.expanduser('~/Projects/animals/exports/animals-paid-ads-targets-2026-05-05.csv'),
        'fields': {
            'name': 'name', 'category': 'category', 'city': 'city', 'state': 'state',
            'website': 'website', 'paid_pixels': 'paid_pixels', 'tier': 'tier',
        },
    },
    {
        'project': 'doctor',
        'label': 'Healthcare/Wellness',
        'color': '#88ccff',
        'csv': os.path.expanduser('~/Projects/professional-directory/exports/doctor-paid-ads-targets-2026-05-05.csv'),
        'fields': {
            'name': 'name', 'category': 'org_type', 'city': 'city', 'state': 'state',
            'website': 'website', 'paid_pixels': 'paid_pixels', 'tier': 'tier',
        },
    },
    {
        'project': 'lacountyeats',
        'label': 'LA Restaurants',
        'color': '#ff9966',
        'csv': os.path.expanduser('~/Projects/restaurant-directory/exports/lacountyeats-paid-ads-targets-2026-05-05.csv'),
        'fields': {
            'name': 'name', 'category': lambda r: 'restaurant', 'city': 'city', 'state': 'state',
            'website': 'website', 'paid_pixels': 'paid_pixels', 'tier': 'tier',
        },
    },
]

def get_field(row, spec):
    if callable(spec):
        return spec(row)
    return row.get(spec, '') or ''

all_rows = []
project_counts = {}
for src in SOURCES:
    if not os.path.exists(src['csv']):
        print(f'  ! missing: {src["csv"]}', file=sys.stderr)
        project_counts[src['project']] = 0
        continue
    with open(src['csv'], encoding='utf-8') as f:
        rows = list(csv.DictReader(f))
    project_counts[src['project']] = len(rows)
    for r in rows:
        all_rows.append({
            'project': src['project'],
            'project_label': src['label'],
            'project_color': src['color'],
            'name': get_field(r, src['fields']['name']),
            'category': get_field(r, src['fields']['category']),
            'city': get_field(r, src['fields']['city']),
            'state': get_field(r, src['fields']['state']),
            'website': get_field(r, src['fields']['website']),
            'paid_pixels': int(get_field(r, src['fields']['paid_pixels']) or 0),
            'tier': get_field(r, src['fields']['tier']),
        })

# Sort: TIER1 → TIER2 → TIER3, then by paid_pixels desc
TIER_RANK = {'TIER1_HEAVY': 1, 'TIER2_MODERATE': 2, 'TIER3_SINGLE': 3, 'VERIFIED_ACTIVE': 0, 'PIXEL_ONLY': 2, 'OTHER_PLATFORM': 3}
all_rows.sort(key=lambda r: (TIER_RANK.get(r['tier'], 9), -r['paid_pixels'], r['name']))

total = len(all_rows)
tier_counts = Counter(r['tier'] for r in all_rows)
top_cities = Counter(r['city'].upper() for r in all_rows if r['city']).most_common(10)
generated = datetime.now().strftime('%Y-%m-%d %H:%M PT')

stats_html = ''.join([f'<div class="stat" style="border-left:3px solid {s["color"]}"><div class="v">{project_counts.get(s["project"], 0)}</div><div class="l">{s["label"]}</div></div>' for s in SOURCES])

HTML = f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<title>Ad-Signals Rolodex — {total} businesses · INTERNAL</title>
<script>(function(){{const t=localStorage.getItem('theme')||'dark';document.documentElement.setAttribute('data-theme',t);}})();</script>
<style>
:root[data-theme="dark"]{{--bg:#0a0a0a;--fg:#e8e8e8;--muted:#888;--accent:#fff;--card:#161616;--border:#2a2a2a;--banner:#3a1a00;--banner-fg:#ffaa44}}
:root[data-theme="light"]{{--bg:#fafafa;--fg:#1a1a1a;--muted:#666;--accent:#000;--card:#fff;--border:#ddd;--banner:#fff4d6;--banner-fg:#7a4f00}}
*{{box-sizing:border-box}}body{{margin:0;font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--fg);font-size:13px;line-height:1.4}}
.banner{{background:var(--banner);color:var(--banner-fg);padding:10px 20px;font-weight:600;font-size:13px;border-bottom:2px solid var(--banner-fg);display:flex;justify-content:space-between;align-items:center}}
.hd{{padding:18px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:flex-start;gap:20px;flex-wrap:wrap}}
h1{{margin:0 0 4px;font-size:22px}}.sub{{color:var(--muted);font-size:12px}}
.theme-tog{{background:none;border:1px solid var(--border);color:var(--fg);padding:6px 12px;border-radius:4px;cursor:pointer}}
.stats{{padding:14px 20px;display:flex;gap:24px;flex-wrap:wrap;border-bottom:1px solid var(--border);background:var(--card)}}
.stat{{padding-left:12px}}.stat .v{{font-size:24px;font-weight:600;color:var(--accent)}}.stat .l{{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px}}
.controls{{padding:12px 20px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;border-bottom:1px solid var(--border)}}
.controls input[type=text]{{flex:1;min-width:200px;padding:8px;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:4px}}
.controls label{{font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer}}
table{{width:100%;border-collapse:collapse;font-size:12px}}
thead th{{text-align:left;padding:8px 10px;background:var(--card);border-bottom:1px solid var(--border);cursor:pointer;position:sticky;top:0;user-select:none}}
thead th:hover{{color:var(--accent)}}tbody td{{padding:8px 10px;border-bottom:1px solid var(--border)}}tbody tr:hover{{background:var(--card)}}
.proj-badge{{display:inline-block;padding:2px 8px;border-radius:99px;font-size:10px;font-weight:600;color:#000}}
.tier-tier1{{display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:600;background:#5a1a1a;color:#ffaaaa}}
.tier-tier2{{display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:600;background:#5a3d00;color:#ffd180}}
.tier-tier3{{display:inline-block;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:600;background:#333;color:#aaa}}
:root[data-theme="light"] .tier-tier1{{background:#ffd6d6;color:#8b0000}}
:root[data-theme="light"] .tier-tier2{{background:#fff0c2;color:#7a4f00}}
:root[data-theme="light"] .tier-tier3{{background:#eee;color:#444}}
a{{color:var(--accent);text-decoration:none}}a:hover{{text-decoration:underline}}
.compliance{{padding:12px 20px;background:var(--card);border-top:1px solid var(--border);font-size:11px;color:var(--muted);line-height:1.6}}
</style></head><body>
<div class="banner">
  <span>🔒 INTERNAL — Cross-Vertical Sales Rolodex — Not For Public Distribution</span>
  <span style="font-size:11px;opacity:0.8">{total} businesses across 5 directories · {generated}</span>
</div>
<div class="hd">
  <div><h1>📇 Ad-Signals Rolodex — Cross-Vertical</h1>
  <div class="sub">{total} CA businesses paying for online customer acquisition · LA local biz, lawyers, pet industry, healthcare/wellness, restaurants</div></div>
  <button class="theme-tog" onclick="toggleTheme()" id="themeBtn">☀ light</button>
</div>
<div class="stats">
  <div class="stat"><div class="v">{total}</div><div class="l">Total</div></div>
  {stats_html}
</div>
<div class="controls">
  <input type="text" id="filter" placeholder="Filter by name, category, city, project…" oninput="render()">
  <label><input type="checkbox" data-proj="ventura-corridor" checked onchange="render()"> LA Local</label>
  <label><input type="checkbox" data-proj="lawyer" checked onchange="render()"> Lawyers</label>
  <label><input type="checkbox" data-proj="animals" checked onchange="render()"> Pet Industry</label>
  <label><input type="checkbox" data-proj="doctor" checked onchange="render()"> Healthcare</label>
  <label><input type="checkbox" data-proj="lacountyeats" checked onchange="render()"> Restaurants</label>
  <span style="border-left:1px solid var(--border);padding-left:12px">
  <label><input type="checkbox" id="t1" checked onchange="render()"> 3+ pixels</label>
  <label><input type="checkbox" id="t2" checked onchange="render()"> 2 pixels</label>
  <label><input type="checkbox" id="t3" checked onchange="render()"> 1 pixel</label>
  </span>
  <span id="visible-count" style="margin-left:auto;color:var(--muted);font-size:11px"></span>
</div>
<div style="overflow-x:auto"><table><thead><tr>
  <th onclick="sortBy('project_label')">Project</th>
  <th onclick="sortBy('name')">Business</th>
  <th onclick="sortBy('category')">Category</th>
  <th onclick="sortBy('city')">City</th>
  <th onclick="sortBy('paid_pixels')">Pixels</th>
  <th onclick="sortBy('tier')">Tier</th>
  <th>Website</th>
</tr></thead><tbody id="tbody"></tbody></table></div>
<div class="compliance">Generated {generated}. Pure tracking-pixel detection on public websites. Not a verified spend list. Internal sales targeting only. Source CSVs: ventura-corridor, lawyer-directory-builder, animals, professional-directory, restaurant-directory.</div>
<script>
const DATA = {json.dumps(all_rows)};
let sortKey='paid_pixels',sortAsc=false;
function toggleTheme(){{const c=document.documentElement.getAttribute('data-theme');const n=c==='dark'?'light':'dark';document.documentElement.setAttribute('data-theme',n);localStorage.setItem('theme',n);document.getElementById('themeBtn').textContent=n==='dark'?'☀ light':'☾ dark';}}
document.getElementById('themeBtn').textContent=document.documentElement.getAttribute('data-theme')==='dark'?'☀ light':'☾ dark';
function sortBy(k){{if(sortKey===k)sortAsc=!sortAsc;else{{sortKey=k;sortAsc=false;}}render();}}
function eh(s){{return String(s).replace(/[&<>\"']/g,c=>({{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}})[c]);}}
function tierClass(t){{if(t.includes('TIER1')||t.includes('VERIFIED'))return 'tier-tier1';if(t.includes('TIER2')||t.includes('PIXEL_ONLY'))return 'tier-tier2';return 'tier-tier3';}}
function tierBucket(t){{if(t.includes('TIER1')||t.includes('VERIFIED'))return 1;if(t.includes('TIER2')||t.includes('PIXEL_ONLY'))return 2;return 3;}}
function render(){{
  const q=document.getElementById('filter').value.toLowerCase();
  const projs=new Set([...document.querySelectorAll('input[data-proj]:checked')].map(i=>i.dataset.proj));
  const tierShow={{1:document.getElementById('t1').checked,2:document.getElementById('t2').checked,3:document.getElementById('t3').checked}};
  const f=DATA.filter(r=>projs.has(r.project)&&tierShow[tierBucket(r.tier)]&&(!q||r.name.toLowerCase().includes(q)||r.category.toLowerCase().includes(q)||r.city.toLowerCase().includes(q)||r.project_label.toLowerCase().includes(q)));
  f.sort((a,b)=>{{const va=a[sortKey],vb=b[sortKey];let r=(typeof va==='number')?(va-vb):String(va).localeCompare(String(vb));return sortAsc?r:-r;}});
  document.getElementById('tbody').innerHTML=f.map(r=>`<tr>
    <td><span class="proj-badge" style="background:${{r.project_color}}">${{eh(r.project_label)}}</span></td>
    <td><strong>${{eh(r.name)}}</strong></td>
    <td>${{eh(r.category)}}</td>
    <td>${{eh(r.city)}}, ${{eh(r.state)}}</td>
    <td>${{r.paid_pixels}}</td>
    <td><span class="${{tierClass(r.tier)}}">${{r.tier.replace(/_/g,' ')}}</span></td>
    <td>${{r.website?`<a href="${{eh(r.website)}}" target="_blank" rel="noopener noreferrer">${{eh(r.website.replace(/^https?:\\/\\//,'').slice(0,40))}}</a>`:''}}</td>
  </tr>`).join('');
  document.getElementById('visible-count').textContent=`${{f.length}} of ${{DATA.length}}`;
}}
render();
</script></body></html>"""

with open(OUT, 'w', encoding='utf-8') as f:
    f.write(HTML)

print(f'Wrote {OUT} · {len(HTML):,} bytes · {total} businesses across {sum(1 for v in project_counts.values() if v > 0)} projects')
print(f'Counts: {project_counts}')