[object Object]

← back to Commercialrealestate

CRCP: firecities.crcp hub — 2025 burn-zone cities (Eaton+Palisades) grouped by incident

9cfd192bd75bfaafafb5834b3a0400c40e400c58 · 2026-08-20 13:37:37 -0700 · Steve Abrams

Files touched

Diff

commit 9cfd192bd75bfaafafb5834b3a0400c40e400c58
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 20 13:37:37 2026 -0700

    CRCP: firecities.crcp hub — 2025 burn-zone cities (Eaton+Palisades) grouped by incident
---
 public/crcp-cities.js  |  17 +++++--
 public/firecities.html | 135 +++++++++++++++++++++++++++++++++++++++++++++++++
 scripts/serve.js       |  12 +++--
 3 files changed, 155 insertions(+), 9 deletions(-)

diff --git a/public/crcp-cities.js b/public/crcp-cities.js
index e211b2b..09382b3 100644
--- a/public/crcp-cities.js
+++ b/public/crcp-cities.js
@@ -18,17 +18,26 @@
     "pasadena", "paynescreek", "redding", "sanjuancapistrano", "santamonica", "shermanoaks", "sierramadre", "somerset",
     "somis", "southlaketahoe", "studiocity", "topanga", "twinbridges", "ukiah", "weed", "woffordheights", "wrightwood"];
 
-  // Hosts that are NOT a city (serve the app unscoped, or the hub). label-0 of these -> no scope.
-  var RESERVED = { crcp: 1, recities: 1, www: 1, localhost: 1, "127": 1 };
+  // Hosts that are NOT a city (serve the app unscoped, or a hub). label-0 of these -> no scope.
+  var RESERVED = { crcp: 1, recities: 1, firecities: 1, www: 1, localhost: 1, "127": 1 };
+
+  // Fire-affected burn-zone cities (2025 LA firestorm) — the firecities.crcp hub scopes to these.
+  // fire = the incident label shown in the hub. Only slugs that are also real fleet cities.
+  var FIRE = {
+    altadena: 'Eaton Fire', pasadena: 'Eaton Fire', sierramadre: 'Eaton Fire',
+    pacificpalisades: 'Palisades Fire', malibu: 'Palisades Fire', topanga: 'Palisades Fire',
+    santamonica: 'Palisades Fire'
+  };
 
   function title(s) { return s.replace(/(^|\s)\S/g, function (c) { return c.toUpperCase(); }); }
   function norm(s) { return String(s == null ? "" : s).toLowerCase().replace(/[^a-z]/g, ""); }
 
   var CITIES = SLUGS.map(function (slug) {
     var label = LABELS[slug] || title(slug);
-    return { slug: slug, label: label, host: slug + ".crcp.agentabrams.com", norm: norm(label) };
+    return { slug: slug, label: label, host: slug + ".crcp.agentabrams.com", norm: norm(label), fire: FIRE[slug] || null };
   });
   var BY_SLUG = {}; CITIES.forEach(function (c) { BY_SLUG[c.slug] = c; });
+  var FIRE_CITIES = CITIES.filter(function (c) { return c.fire; });
 
   // Resolve the current host to a city object, or null when unscoped (base crcp / recities / ip / localhost).
   function cityFromHost(host) {
@@ -40,7 +49,7 @@
   function sameCity(cityStr, cityObj) { return !!cityObj && norm(cityStr) === cityObj.norm; }
 
   window.CRCP_CITIES = {
-    LABELS: LABELS, SLUGS: SLUGS, CITIES: CITIES, BY_SLUG: BY_SLUG,
+    LABELS: LABELS, SLUGS: SLUGS, CITIES: CITIES, BY_SLUG: BY_SLUG, FIRE: FIRE, FIRE_CITIES: FIRE_CITIES,
     cityFromHost: cityFromHost, sameCity: sameCity, norm: norm, title: title
   };
 })();
diff --git a/public/firecities.html b/public/firecities.html
new file mode 100644
index 0000000..7ac5b03
--- /dev/null
+++ b/public/firecities.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>CRCP · Fire Cities</title>
+<style>
+  :root{
+    --bg:#160f0d; --panel:#221513; --panel2:#2c1a16; --edge:#43261f;
+    --ink:#f3e9e6; --mut:#c39a8e; --accent:#ff7a45; --accent2:#c2410c; --ember:#ff5722;
+  }
+  *{box-sizing:border-box}
+  body{margin:0;background:linear-gradient(180deg,#160f0d,#1c1210);color:var(--ink);
+       font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;min-height:100vh}
+  .wrap{max-width:1080px;margin:0 auto;padding:40px 20px 80px}
+  header.top{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-bottom:6px}
+  header.top h1{font-size:26px;font-weight:700;margin:0;letter-spacing:.2px}
+  header.top h1 .fl{color:var(--ember)}
+  header.top .tag{color:var(--mut);font-size:14px}
+  .sub{color:var(--mut);margin:0 0 24px;max-width:70ch}
+  .panel{background:var(--panel);border:1px solid var(--edge);border-radius:14px;padding:22px;box-shadow:0 8px 30px rgba(0,0,0,.35)}
+  .pickrow{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
+  label.lbl{color:var(--mut);font-size:13px;text-transform:uppercase;letter-spacing:.6px;display:block;margin-bottom:8px}
+  select,button{font:inherit}
+  select#city{flex:1 1 320px;min-width:240px;background:var(--panel2);color:var(--ink);border:1px solid var(--edge);
+    border-radius:10px;padding:13px 14px;appearance:none;
+    background-image:linear-gradient(45deg,transparent 50%,var(--mut) 50%),linear-gradient(135deg,var(--mut) 50%,transparent 50%);
+    background-position:calc(100% - 20px) 20px,calc(100% - 14px) 20px;background-size:6px 6px,6px 6px;background-repeat:no-repeat}
+  button.go{background:var(--accent2);color:#fff;border:1px solid var(--accent);border-radius:10px;padding:13px 22px;cursor:pointer;font-weight:600}
+  button.go:hover{background:var(--accent)}
+  .count{color:var(--mut);font-size:13px;margin:16px 0 6px}
+  .group{margin-top:22px}
+  .group h2{font-size:13px;text-transform:uppercase;letter-spacing:.8px;color:var(--ember);margin:0 0 10px;display:flex;align-items:center;gap:8px}
+  .group h2 .dot{width:8px;height:8px;border-radius:50%;background:var(--ember);box-shadow:0 0 8px var(--ember)}
+  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px}
+  a.city{display:flex;justify-content:space-between;align-items:center;gap:10px;text-decoration:none;color:var(--ink);
+    background:var(--panel2);border:1px solid var(--edge);border-radius:10px;padding:12px 14px;transition:.12s}
+  a.city:hover{border-color:var(--accent);background:#33201a;transform:translateY(-1px)}
+  a.city.empty{opacity:.55}
+  a.city .nm{font-weight:600}
+  a.city .host{display:block;color:var(--mut);font-size:12px;margin-top:2px;font-weight:400}
+  a.city .n{flex:0 0 auto;font-size:12px;color:var(--mut);background:#1c1210;border:1px solid var(--edge);border-radius:20px;padding:2px 9px}
+  a.city .n.has{color:#ffcaa8;border-color:var(--accent2)}
+  .links{margin-top:26px;display:flex;gap:12px;flex-wrap:wrap}
+  .links a{color:#ffb38a;text-decoration:none;border:1px solid var(--edge);border-radius:8px;padding:6px 12px;font-size:13px}
+  .links a:hover{border-color:var(--accent)}
+  footer{color:var(--mut);font-size:12px;margin-top:30px}
+</style>
+</head>
+<body>
+<div class="wrap">
+  <header class="top">
+    <h1><span class="fl">🔥</span> CRCP · Fire Cities</h1>
+    <span class="tag">Commercial real estate in the 2025 LA burn zones</span>
+  </header>
+  <p class="sub">The fire-affected markets from the January 2025 Eaton and Palisades fires. Choose a
+     city to open its scoped CRE dashboard — rebuild activity, listings, and broker coverage.</p>
+
+  <div class="panel">
+    <label class="lbl" for="city">Jump to a burn-zone city</label>
+    <div class="pickrow">
+      <select id="city" aria-label="Select a fire-affected city"></select>
+      <button class="go" id="go">Open →</button>
+    </div>
+    <div class="count" id="count"></div>
+    <div id="groups"></div>
+    <div class="links">
+      <a href="https://recities.crcp.agentabrams.com/">← All cities (recities)</a>
+      <a href="https://crcp.agentabrams.com/">All markets (base CRCP)</a>
+    </div>
+  </div>
+  <footer id="foot"></footer>
+</div>
+
+<script src="/crcp-cities.js"></script>
+<script>
+var FIRE = window.CRCP_CITIES.FIRE_CITIES.slice();      // burn-zone cities from the shared module
+var sameCity = window.CRCP_CITIES.sameCity;
+var counts = {}, loaded = false;
+var urlOf = function (c) { return "https://" + c.host + "/"; };
+
+var sel = document.getElementById('city'), groups = document.getElementById('groups'),
+    count = document.getElementById('count'), foot = document.getElementById('foot');
+
+fetch('/data/ranked.json').then(function (r) { return r.json(); }).then(function (d) {
+  var arr = (d && d.ranked) || [];
+  FIRE.forEach(function (c) { counts[c.slug] = arr.filter(function (p) { return sameCity(p.city, c); }).length; });
+  loaded = true; render();
+}).catch(function () { loaded = true; render(); });
+
+function ordered() {
+  return FIRE.slice().sort(function (a, b) {
+    var na = counts[a.slug] || 0, nb = counts[b.slug] || 0;
+    if (nb !== na) return nb - na;
+    return a.label.localeCompare(b.label);
+  });
+}
+function fillSelect() {
+  sel.innerHTML = '';
+  ordered().forEach(function (c) {
+    var o = document.createElement('option'); o.value = c.slug;
+    o.textContent = c.label + (loaded ? ' (' + (counts[c.slug] || 0) + ')' : '');
+    sel.appendChild(o);
+  });
+}
+function card(c) {
+  var n = counts[c.slug] || 0;
+  return '<a class="city' + (loaded && n === 0 ? ' empty' : '') + '" href="' + urlOf(c) + '">' +
+    '<span><span class="nm">' + c.label + '</span><span class="host">' + c.host + '</span></span>' +
+    '<span class="n' + (n ? ' has' : '') + '">' + (loaded ? n : '·') + '</span></a>';
+}
+function render() {
+  fillSelect();
+  // group by incident (fire label)
+  var byFire = {};
+  ordered().forEach(function (c) { (byFire[c.fire] = byFire[c.fire] || []).push(c); });
+  var html = '';
+  Object.keys(byFire).forEach(function (fire) {
+    var tot = byFire[fire].reduce(function (s, c) { return s + (counts[c.slug] || 0); }, 0);
+    html += '<div class="group"><h2><span class="dot"></span>' + fire +
+            (loaded ? ' · ' + tot + ' listings' : '') + '</h2><div class="grid">' +
+            byFire[fire].map(card).join('') + '</div></div>';
+  });
+  groups.innerHTML = html;
+  var total = FIRE.reduce(function (s, c) { return s + (counts[c.slug] || 0); }, 0);
+  count.textContent = loaded ? (FIRE.length + ' burn-zone cities · ' + total + ' listings') : (FIRE.length + ' burn-zone cities');
+  foot.textContent = 'CRCP · firecities.crcp.agentabrams.com';
+}
+function open() { var c = FIRE.find(function (x) { return x.slug === sel.value; }); if (c) window.location.href = urlOf(c); }
+document.getElementById('go').addEventListener('click', open);
+sel.addEventListener('keydown', function (e) { if (e.key === 'Enter') open(); });
+render();
+</script>
+</body>
+</html>
diff --git a/scripts/serve.js b/scripts/serve.js
index 1177149..baedb92 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -71,10 +71,12 @@ const BADGE_TAG = '<script src="/user-badge.js" defer></script>';
 // window.fetch and filter ranked.json to <city>.crcp.agentabrams.com. Unscoped hosts are a no-op.
 // (sync scripts, order matters: cities table then the scoper.)
 const SCOPE_HEAD = '<script src="/crcp-cities.js"></script><script src="/crcp-scope.js"></script>';
+// Hub hosts serve a launcher page at '/' instead of the app: recities. = all cities,
+// firecities. = 2025 burn-zone cities. Every other host gets the app (index.html).
+const HUB_PAGES = { 'recities.crcp.agentabrams.com': 'recities.html', 'firecities.crcp.agentabrams.com': 'firecities.html' };
 app.get(/(?:^\/$|\.html$)/, (req, res, next) => {
-  // recities.crcp.agentabrams.com serves the city-launcher hub at '/'; every other host gets the app.
-  const isRecities = (req.hostname || '').toLowerCase() === 'recities.crcp.agentabrams.com';
-  const rel = req.path === '/' ? (isRecities ? 'recities.html' : 'index.html') : req.path.replace(/^\/+/, '');
+  const hub = HUB_PAGES[(req.hostname || '').toLowerCase()];
+  const rel = req.path === '/' ? (hub || 'index.html') : req.path.replace(/^\/+/, '');
   if (rel === 'login.html') return next();                 // sign-in page stays badge-free
   const file = path.join(PUB_DIR, rel);
   if (!file.startsWith(PUB_DIR + path.sep)) return next();  // path-traversal guard
@@ -83,8 +85,8 @@ app.get(/(?:^\/$|\.html$)/, (req, res, next) => {
     let out = html.includes('/user-badge.js') ? html
       : html.includes('</body>') ? html.replace(/<\/body>/i, BADGE_TAG + '</body>')
       : html + BADGE_TAG;
-    // recities.html loads crcp-cities.js itself; the scoper is only meaningful on the app pages.
-    if (rel !== 'recities.html' && !out.includes('/crcp-scope.js')) {
+    // hub pages load crcp-cities.js themselves; the scoper is only meaningful on the app pages.
+    if (rel !== 'recities.html' && rel !== 'firecities.html' && !out.includes('/crcp-scope.js')) {
       out = out.includes('</head>') ? out.replace(/<\/head>/i, SCOPE_HEAD + '</head>')
           : (out.includes('<body') ? out.replace(/<body[^>]*>/i, '$&' + SCOPE_HEAD) : SCOPE_HEAD + out);
     }

← 8376503 auto-data-snapshot: 2026-08-20T13:34:43 (1 data files) — scr  ·  back to Commercialrealestate  ·  chore: add broker-name normalizer + version bump (session cl f30d3c7 →