← back to La Socrata Ingester
viewer/private/board.html
282 lines
<!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>
<footer style="border-top:1px solid var(--line,#e3e7ec);background:var(--panel,#fff);padding:8px 20px;font:11px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#6b7480">
<details>
<summary style="cursor:pointer;list-style:none;outline:none">ⓘ Data from LA City & LA County public records — provided “as is,” not independently verified. <b style="color:var(--teal,#0f766e)">Sources & terms ▸</b></summary>
<div style="margin-top:6px;max-width:940px">
<b>Sources.</b> Building permits: City of Los Angeles, Dept. of Building & Safety, via the
<a href="https://data.lacity.org" target="_blank" rel="noopener noreferrer" style="color:var(--accent,#0e7490)">LA City Open Data portal</a>.
Parcel & assessment attributes: County of Los Angeles Office of the Assessor / Enterprise GIS.<br>
<b>Terms.</b> Used under the
<a href="https://egis-lacounty.hub.arcgis.com/pages/terms-of-use" target="_blank" rel="noopener noreferrer" style="color:var(--accent,#0e7490)">LA County eGIS Terms of Use</a>
and the LA City Open Data terms. The data is provided <b>“as is,” with no warranty</b> of accuracy, completeness, or fitness for any purpose, and must not be relied upon for legal, engineering, lending, or surveying decisions.<br>
<b>No endorsement.</b> This tool is not affiliated with, sponsored by, or endorsed by the City or County of Los Angeles.
</div>
</details>
</footer>
</body>
</html>