← 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 &amp; LA County public records — provided &ldquo;as is,&rdquo; not independently verified. <b style="color:var(--teal,#0f766e)">Sources &amp; terms&nbsp;▸</b></summary>
    <div style="margin-top:6px;max-width:940px">
      <b>Sources.</b> Building permits: City of Los Angeles, Dept. of Building &amp; 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 &amp; 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>&ldquo;as is,&rdquo; 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>