← back to La Socrata Ingester
Add DTD-driven Opportunity Board (board.html)
ed109861d2fb243613b9a5d06811d9e39b32da3d · 2026-08-11 08:28:00 -0700 · Steve Abrams
Hootsuite-style horizontal multi-column board of 7 data-product projects
buildable on the 1.58M LA permit + assessor dataset, with 21 suggested
user-segment cards (persona, why, exact permit/parcel fields, fit tag).
Column SET committed by the DTD 5-LLM debate panel (Claude+Codex+Qwen+Grok+Kimi),
~$0.04 run. Matches index.html palette/tokens; served at /board.html behind
existing auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A viewer/public/board.html
Diff
commit ed109861d2fb243613b9a5d06811d9e39b32da3d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 11 08:28:00 2026 -0700
Add DTD-driven Opportunity Board (board.html)
Hootsuite-style horizontal multi-column board of 7 data-product projects
buildable on the 1.58M LA permit + assessor dataset, with 21 suggested
user-segment cards (persona, why, exact permit/parcel fields, fit tag).
Column SET committed by the DTD 5-LLM debate panel (Claude+Codex+Qwen+Grok+Kimi),
~$0.04 run. Matches index.html palette/tokens; served at /board.html behind
existing auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
viewer/public/board.html | 267 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 267 insertions(+)
diff --git a/viewer/public/board.html b/viewer/public/board.html
new file mode 100644
index 0000000..c9e126f
--- /dev/null
+++ b/viewer/public/board.html
@@ -0,0 +1,267 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Permit Data — Opportunity Board</title>
+<style>
+ :root{
+ --bg:#f4f5f7; --panel:#ffffff; --ink:#1b1f24; --ink-soft:#5b636d; --ink-faint:#8a919b;
+ --line:#e2e5ea; --line-strong:#d3d8df; --teal:#0d9488; --teal-ink:#0b7d72; --teal-bg:#e6f6f4;
+ --amber:#b45309; --shadow:0 1px 2px rgba(16,24,40,.06),0 1px 3px rgba(16,24,40,.04);
+ --shadow-lg:0 12px 40px rgba(16,24,40,.22);
+ --r:8px; --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+ --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;height:100%}
+ body{background:var(--bg);color:var(--ink);font:14px/1.45 var(--font);-webkit-font-smoothing:antialiased;display:flex;flex-direction:column;height:100vh;overflow:hidden}
+ a{color:inherit}
+ button{font:inherit;cursor:pointer;color:inherit}
+
+ /* ---------- top bar ---------- */
+ header.top{background:var(--panel);border-bottom:1px solid var(--line);padding:10px 16px;display:flex;align-items:center;gap:14px;flex-wrap:wrap;z-index:5;flex:none}
+ .brand{display:flex;align-items:baseline;gap:8px;min-width:230px}
+ .brand b{font-size:15px;letter-spacing:-.2px}
+ .brand .tag{font-size:11px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:.6px}
+ .spacer{flex:1}
+ .meta{display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ink-soft);flex-wrap:wrap}
+ .meta .dtd{display:inline-flex;align-items:center;gap:6px;background:var(--teal-bg);color:var(--teal-ink);border:1px solid #bfe8e2;border-radius:999px;padding:3px 10px;font-weight:600}
+ .meta .cost{font-variant-numeric:tabular-nums}
+ .backlink{display:inline-flex;align-items:center;gap:6px;padding:8px 13px;border:1px solid var(--line-strong);border-radius:var(--r);background:#fff;font-weight:600;font-size:13px;text-decoration:none;color:var(--ink);white-space:nowrap}
+ .backlink:hover{border-color:var(--teal);color:var(--teal-ink)}
+
+ /* ---------- subhead ---------- */
+ .subhead{background:var(--bg);padding:9px 16px 3px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;flex:none}
+ .subhead .lede{font-size:12.5px;color:var(--ink-soft);max-width:900px}
+ .subhead .lede b{color:var(--ink)}
+
+ /* ---------- board (horizontal columns) ---------- */
+ .board{flex:1;display:flex;gap:14px;padding:12px 16px 18px;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x proximity}
+ .col{flex:0 0 320px;width:320px;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-top:3px solid var(--accent,#0d9488);border-radius:var(--r);box-shadow:var(--shadow);max-height:100%;scroll-snap-align:start;overflow:hidden}
+ .col-hd{padding:12px 13px 11px;border-bottom:1px solid var(--line);background:linear-gradient(#fff,#fcfdfe)}
+ .col-hd .rank{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border-radius:6px;background:var(--accent,#0d9488);color:#fff;font-size:11px;font-weight:800;font-variant-numeric:tabular-nums;vertical-align:1px;margin-right:7px}
+ .col-hd .name{font-weight:800;font-size:14.5px;letter-spacing:-.2px;display:inline}
+ .col-hd .pitch{font-size:12px;color:var(--ink-soft);margin-top:6px;line-height:1.4}
+ .col-hd .foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:9px}
+ .col-hd .users-n{font-size:11px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:.5px;font-weight:600}
+ .col-hd .opt{font-size:10.5px;color:var(--ink-faint);font-variant-numeric:tabular-nums;border:1px solid var(--line);border-radius:999px;padding:1px 8px}
+
+ .col-body{overflow-y:auto;padding:10px 10px 14px;display:flex;flex-direction:column;gap:9px}
+
+ /* ---------- persona cards ---------- */
+ .card{background:#fff;border:1px solid var(--line);border-radius:var(--r);padding:11px 12px;display:flex;flex-direction:column;gap:7px;box-shadow:var(--shadow);transition:border-color .12s,box-shadow .12s}
+ .card:hover{border-color:var(--accent,#0d9488);box-shadow:0 2px 8px rgba(13,148,136,.12)}
+ .card .top-line{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
+ .card .persona{font-weight:700;font-size:13.5px;letter-spacing:-.15px;line-height:1.25}
+ .fit{flex:none;font-size:10px;font-weight:700;padding:2px 8px;border-radius:999px;text-transform:uppercase;letter-spacing:.4px;white-space:nowrap}
+ .fit.high{background:#eafaf1;color:#1a7f47;border:1px solid #bce9cf}
+ .fit.medium{background:#fef6e7;color:#a16207;border:1px solid #f5e0a8}
+ .card .why{font-size:12px;color:var(--ink-soft);line-height:1.42}
+ .card .flds{display:flex;flex-wrap:wrap;gap:4px;border-top:1px dashed var(--line);padding-top:8px}
+ .card .flds .lbl{font-size:9.5px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:.5px;width:100%;margin-bottom:1px}
+ .fld{font:11px/1 var(--mono);color:var(--teal-ink);background:var(--teal-bg);border:1px solid #cdeae5;border-radius:5px;padding:3px 6px;white-space:nowrap}
+
+ /* scrollbars a touch subtler */
+ .board::-webkit-scrollbar,.col-body::-webkit-scrollbar{height:10px;width:9px}
+ .board::-webkit-scrollbar-thumb,.col-body::-webkit-scrollbar-thumb{background:#d3d8df;border-radius:6px}
+ .board::-webkit-scrollbar-thumb:hover,.col-body::-webkit-scrollbar-thumb:hover{background:#b9c0c9}
+
+ @media(max-width:640px){.col{flex-basis:86vw;width:86vw}}
+</style>
+</head>
+<body>
+ <header class="top">
+ <div class="brand"><b>Permit Data — Opportunity Board</b><span class="tag">City of Los Angeles</span></div>
+ <div class="spacer"></div>
+ <div class="meta">
+ <span class="dtd" id="srcTag" title="Column set decided by the DTD 5-LLM debate panel">◆ DTD panel</span>
+ <span class="cost" id="costTag">run cost ~$0.04</span>
+ </div>
+ <a class="backlink" href="/" title="Back to the permits browser">← Permits browser</a>
+ </header>
+
+ <div class="subhead">
+ <div class="lede">Each column is a <b>data-product project</b> buildable on <b>1,578,997</b> LA City building permits enriched with LA County assessor parcel data. Cards inside are the <b>user segments</b> who would pay for or use it — with the exact permit/parcel fields each cares about. Scroll →</div>
+ </div>
+
+ <div class="board" id="board"></div>
+
+<script>
+// ── Opportunity board data ──────────────────────────────────────────────
+// Column SET committed by the DTD 5-LLM debate panel (Claude + Codex gpt-5.3-codex
+// + Qwen3:14b + Grok-4.5 + Kimi-k2.5). Panel converged on the same 7-project slate;
+// tops split (Claude=B investor, Codex=H API, Grok=A contractor, Qwen=B, Kimi=E solar).
+// Merged verdict: lead-gen/opportunity products anchor (A/B/E), enterprise-embedded
+// products defend (C/D/H), public-sector product anchors credibility (F).
+// Ranking rationale = buyer immediacy × per-seat willingness-to-pay × field fit.
+const DTD = {
+ ran: true,
+ cost: "~$0.04", // Codex + Grok + Kimi metered; Qwen local $0; Claude tokens
+ panel: "Claude · Codex gpt-5.3-codex · Qwen3-14b (local) · Grok-4.5 · Kimi-k2.5",
+ note: "5/5 panelists converged on this 7-project slate; #1 split B/H/A/B/E → merged by immediacy × WTP × field fit."
+};
+
+const COLUMNS = [
+ {
+ accent:"#0d9488", opt:"opt B", name:"Flip / Investor Opportunity Scanner",
+ pitch:"Surface under-improved parcels & distressed-signal permits before they hit the MLS — a ranked \"act now\" list, not a passive lookup.",
+ users:[
+ {persona:"House flippers & wholesalers", fit:"high",
+ why:"Find low-valuation rehab permits on high-assessed-value parcels — the classic under-improved-asset arbitrage.",
+ fields:["valuation","assessed_value","year_built","work_type","status","address","lat/lon"]},
+ {persona:"Buy-and-hold investors", fit:"high",
+ why:"Spot ADU / unit-add permits that signal density and rent upside on a block before prices adjust.",
+ fields:["work_scope","use","sqft","council_district","zip"]},
+ {persona:"Hard-money & bridge lenders", fit:"medium",
+ why:"Gauge live rehab activity by area to size loan demand and price risk.",
+ fields:["valuation","work_type","issue_date","cd"]},
+ ]
+ },
+ {
+ accent:"#2563eb", opt:"opt A", name:"Contractor / Trade Lead-Gen Feed",
+ pitch:"A live feed of freshly issued permits by trade + geography so contractors reach the owner first, the day the permit issues.",
+ users:[
+ {persona:"GCs & remodelers", fit:"high",
+ why:"Bid on $100k+ additions & alterations near them before demolition begins.",
+ fields:["work_type","work_scope","valuation","issue_date","zip","address"]},
+ {persona:"Roofing / HVAC / electrical subs", fit:"high",
+ why:"Scope-keyword-filtered permits deliver trade-fit jobs matched to their crews.",
+ fields:["work_scope","building_type","work_type","address"]},
+ {persona:"Permit expediters", fit:"medium",
+ why:"Track stalled / pending permits by district to pitch a rescue service.",
+ fields:["status","issue_date","council_district","apn"]},
+ ]
+ },
+ {
+ accent:"#7c3aed", opt:"opt E", name:"Solar / ADU / Renovation Targeting",
+ pitch:"Geo-target owners actively improving — the highest-intent, install-ready lead pool in the county.",
+ users:[
+ {persona:"Solar installers (EPCs)", fit:"high",
+ why:"Recent reroof / addition permits = install-ready roofs on affluent, large-sqft homes.",
+ fields:["work_scope","year_built","sqft","lat/lon","zip"]},
+ {persona:"ADU builders", fit:"high",
+ why:"Filter single-family use + lot/parcel profile for conversion-ready candidates.",
+ fields:["use","sqft","assessed_value","building_type","cd"]},
+ {persona:"Home-improvement lenders", fit:"medium",
+ why:"Owners mid-project are pre-qualified demand for renovation financing.",
+ fields:["assessed_value","valuation","issue_date","zip"]},
+ ]
+ },
+ {
+ accent:"#0891b2", opt:"opt C", name:"Appraisal & AVM Comps Enrichment",
+ pitch:"A permit-adjusted condition & capex signal most automated valuation models miss — sold into existing valuation workflows.",
+ users:[
+ {persona:"Appraisers & AMCs", fit:"high",
+ why:"Recent-improvement flag corrects effective age and comp selection.",
+ fields:["valuation","work_scope","year_built","sqft","assessed_value","apn"]},
+ {persona:"AVM / PropTech data teams", fit:"high",
+ why:"Permit recency & frequency is a high-lift feature for pricing models.",
+ fields:["valuation","issue_date","permit_status","use","sqft"]},
+ {persona:"Mortgage underwriters", fit:"medium",
+ why:"Verify the renovation behind a value bump before funding collateral.",
+ fields:["valuation","work_type","status","assessed_value"]},
+ ]
+ },
+ {
+ accent:"#dc2626", opt:"opt D", name:"Insurance Underwriting & Roof/Systems Risk",
+ pitch:"Turn permit history into a property-risk score — roof age, electrical/plumbing recency, deferred maintenance.",
+ users:[
+ {persona:"P&C home insurers & MGAs", fit:"high",
+ why:"Re-roof + electrical permit recency overrides generic depreciation tables.",
+ fields:["work_scope","year_built","issue_date","building_type","lat/lon"]},
+ {persona:"Insurtech pricing teams", fit:"high",
+ why:"Systems-age & renovation features sharpen automated pricing and inspection waivers.",
+ fields:["work_scope","work_type","valuation","use"]},
+ {persona:"CAT modelers & reinsurers", fit:"medium",
+ why:"Spatial vulnerability layers from building age + permit recency.",
+ fields:["lat/lon","year_built","building_type","council_district"]},
+ ]
+ },
+ {
+ accent:"#ca8a04", opt:"opt F", name:"Urban-Planning & Permit-Velocity Dashboards",
+ pitch:"Council-district development pace, approval-time and pipeline analytics for the public sector and civic orgs.",
+ users:[
+ {persona:"City planners & council staff", fit:"high",
+ why:"Permit velocity, SLA bottlenecks and valuation inflow by district.",
+ fields:["council_district","status","issue_date","valuation","building_type"]},
+ {persona:"Economic-development orgs", fit:"medium",
+ why:"Track investment concentration and where upgrades lag by ZIP.",
+ fields:["valuation","zip","use","assessed_value"]},
+ {persona:"Commercial-RE researchers", fit:"medium",
+ why:"A forward pipeline of new construction and major alterations.",
+ fields:["work_type","valuation","building_type","issue_date"]},
+ ]
+ },
+ {
+ accent:"#475569", opt:"opt H", name:"PropTech API / Data-Licensing Layer",
+ pitch:"The horizontal \"Stripe for LA permit intelligence\" — a normalized, entity-resolved permit+parcel feed via API & bulk export.",
+ users:[
+ {persona:"PropTech startups & SaaS", fit:"high",
+ why:"Bulk permit+parcel join keyed on APN/geo powers search, alerts and listing histories.",
+ fields:["apn","address","lat/lon","issue_date","work_type","valuation","assessed_value"]},
+ {persona:"Title / escrow & due-diligence vendors", fit:"medium",
+ why:"Property-change timelines for diligence and change-of-condition checks.",
+ fields:["apn","work_scope","work_type","issue_date","status"]},
+ {persona:"Data marketplaces & fintech", fit:"medium",
+ why:"Resell or embed a cleaned, queryable feed as a licensed data source.",
+ fields:["assessed_value","valuation","issue_date","use","apn"]},
+ ]
+ },
+];
+
+// ── render ──────────────────────────────────────────────────────────────
+const esc = s => String(s==null?"":s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+
+function fieldChips(fields){
+ return `<div class="flds"><span class="lbl">Fields they query</span>${
+ fields.map(f=>`<span class="fld">${esc(f)}</span>`).join('')
+ }</div>`;
+}
+function cardEl(u){
+ return `<div class="card">
+ <div class="top-line">
+ <div class="persona">${esc(u.persona)}</div>
+ <span class="fit ${u.fit}">${esc(u.fit)}</span>
+ </div>
+ <div class="why">${esc(u.why)}</div>
+ ${fieldChips(u.fields)}
+ </div>`;
+}
+function columnEl(col, i){
+ const el = document.createElement('div');
+ el.className = 'col';
+ el.style.setProperty('--accent', col.accent);
+ el.innerHTML = `
+ <div class="col-hd">
+ <div><span class="rank">${i+1}</span><span class="name">${esc(col.name)}</span></div>
+ <div class="pitch">${esc(col.pitch)}</div>
+ <div class="foot">
+ <span class="users-n">${col.users.length} suggested user${col.users.length===1?'':'s'}</span>
+ <span class="opt">${esc(col.opt)}</span>
+ </div>
+ </div>
+ <div class="col-body">${col.users.map(cardEl).join('')}</div>`;
+ return el;
+}
+
+(function render(){
+ const board = document.getElementById('board');
+ COLUMNS.forEach((c,i)=>board.appendChild(columnEl(c,i)));
+
+ // source + cost tags reflect the DTD run (or fallback)
+ const srcTag = document.getElementById('srcTag');
+ const costTag = document.getElementById('costTag');
+ if(DTD.ran){
+ srcTag.textContent = '◆ DTD panel';
+ srcTag.title = DTD.panel + ' — ' + DTD.note;
+ costTag.textContent = 'DTD run cost ' + DTD.cost;
+ costTag.title = 'Codex + Grok + Kimi metered · Qwen3 local $0 · plus Claude tokens';
+ } else {
+ srcTag.textContent = '◆ analyst fallback (DTD unavailable)';
+ costTag.textContent = '$0 (local)';
+ }
+})();
+</script>
+</body>
+</html>
← 00f0a40 Add palisades_rebuilds + current_permits_all views + pool/ma
·
back to La Socrata Ingester
·
LA Building Permits viewer frontend: faceted rail, search/so bbdafa5 →