← back to Commercialrealestate
CRCP: Phase-1 per-city scoping — host-keyed fetch filter + hide-empty hub (index.html untouched)
5cef9aefda7cba4fea34c04863c996d1743751e5 · 2026-08-20 12:54:15 -0700 · Steve Abrams
Files touched
A public/crcp-cities.jsA public/crcp-scope.jsM public/recities.htmlM scripts/serve.js
Diff
commit 5cef9aefda7cba4fea34c04863c996d1743751e5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 12:54:15 2026 -0700
CRCP: Phase-1 per-city scoping — host-keyed fetch filter + hide-empty hub (index.html untouched)
---
public/crcp-cities.js | 46 ++++++++++++++++
public/crcp-scope.js | 73 ++++++++++++++++++++++++++
public/recities.html | 142 +++++++++++++++++++++++++++++---------------------
scripts/serve.js | 11 +++-
4 files changed, 211 insertions(+), 61 deletions(-)
diff --git a/public/crcp-cities.js b/public/crcp-cities.js
new file mode 100644
index 0000000..e211b2b
--- /dev/null
+++ b/public/crcp-cities.js
@@ -0,0 +1,46 @@
+/* crcp-cities.js — single source of truth for the *.crcp.agentabrams.com city fleet.
+ * Used by recities.html (the launcher hub) AND crcp-scope.js (per-city grid scoping).
+ * To add/remove a city market: add its subdomain vhost + DNS, then add the slug here. */
+(function () {
+ // Multi-word / special-cased labels; single-word slugs fall back to Title Case.
+ var LABELS = {
+ belair: "Bel-Air", beverlyhills: "Beverly Hills", canyondam: "Canyon Dam", chinesecamp: "Chinese Camp",
+ crescentmills: "Crescent Mills", echolake: "Echo Lake", forestranch: "Forest Ranch", grassvalley: "Grass Valley",
+ grizzlyflats: "Grizzly Flats", klamathriver: "Klamath River", lakeelsinore: "Lake Elsinore", northfork: "North Fork",
+ pacificpalisades: "Pacific Palisades", paynescreek: "Paynes Creek", sanjuancapistrano: "San Juan Capistrano",
+ santamonica: "Santa Monica", shermanoaks: "Sherman Oaks", sierramadre: "Sierra Madre", southlaketahoe: "South Lake Tahoe",
+ studiocity: "Studio City", twinbridges: "Twin Bridges", woffordheights: "Wofford Heights"
+ };
+ var SLUGS = ["altadena", "belair", "beverlyhills", "caliente", "camarillo", "canyondam", "chester", "chico",
+ "chinesecamp", "clearlake", "cohasset", "colfax", "crescentmills", "doyle", "echolake", "encino", "foresthill",
+ "forestranch", "georgetown", "grassvalley", "greenville", "grizzlyflats", "havilah", "hemet", "janesville",
+ "klamathriver", "lakeelsinore", "lakehead", "malibu", "mariposa", "mineral", "northfork", "pacificpalisades",
+ "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 };
+
+ 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) };
+ });
+ var BY_SLUG = {}; CITIES.forEach(function (c) { BY_SLUG[c.slug] = c; });
+
+ // Resolve the current host to a city object, or null when unscoped (base crcp / recities / ip / localhost).
+ function cityFromHost(host) {
+ var label0 = String(host || "").toLowerCase().split(".")[0];
+ if (!label0 || RESERVED[label0] || /^\d+$/.test(label0)) return null;
+ return BY_SLUG[label0] || null;
+ }
+ // Does a listing's city string belong to this city object?
+ function sameCity(cityStr, cityObj) { return !!cityObj && norm(cityStr) === cityObj.norm; }
+
+ window.CRCP_CITIES = {
+ LABELS: LABELS, SLUGS: SLUGS, CITIES: CITIES, BY_SLUG: BY_SLUG,
+ cityFromHost: cityFromHost, sameCity: sameCity, norm: norm, title: title
+ };
+})();
diff --git a/public/crcp-scope.js b/public/crcp-scope.js
new file mode 100644
index 0000000..bc9186d
--- /dev/null
+++ b/public/crcp-scope.js
@@ -0,0 +1,73 @@
+/* crcp-scope.js — per-city scoping for <city>.crcp.agentabrams.com (Phase 1, front-end only).
+ *
+ * The grid loads a static data/ranked.json (prod is DB-less). To scope a city subdomain WITHOUT
+ * editing the big shared index.html, this script — injected in <head> by serve.js, BEFORE the app's
+ * inline boot — wraps window.fetch: when the app pulls ranked.json (and /api/condos), we filter the
+ * listings to the host's city. The app then renders the scoped set as if the snapshot only held that
+ * city. Unscoped hosts (base crcp., recities., localhost, IPs) are a no-op — the app runs untouched.
+ *
+ * Reversible: removing the two injected <script> tags (serve.js) fully disables this. Guarded: only
+ * URLs containing ranked.json / api/condos are touched; anything else passes straight through. */
+(function () {
+ if (!window.CRCP_CITIES) return; // cities table must load first
+ var CITY = window.CRCP_CITIES.cityFromHost(location.hostname);
+ if (!CITY) return; // base crcp / recities / localhost -> no scope
+
+ var scoped = { ranked: 0, total: 0 };
+ var _fetch = window.fetch;
+ window.fetch = function (input, init) {
+ var url = (typeof input === "string") ? input : (input && input.url) || "";
+ var p = _fetch.apply(this, arguments);
+ if (/ranked\.json/.test(url)) {
+ return p.then(function (r) {
+ return r.clone().json().then(function (d) {
+ if (d && Array.isArray(d.ranked)) {
+ scoped.total = d.ranked.length;
+ d.ranked = d.ranked.filter(function (x) { return window.CRCP_CITIES.sameCity(x.city, CITY); });
+ scoped.ranked = d.ranked.length;
+ if (d.meta) d.meta.market = CITY.label; // sub-header shows the scoped market name
+ }
+ return new Response(JSON.stringify(d), { status: 200, headers: { "Content-Type": "application/json" } });
+ }).catch(function () { return r; }); // parse failure -> pass original through
+ });
+ }
+ if (/\/api\/condos/.test(url)) { // keep the in-grid condo overlay city-consistent
+ return p.then(function (r) {
+ return r.clone().json().then(function (d) {
+ if (d && Array.isArray(d.condos)) d.condos = d.condos.filter(function (c) { return window.CRCP_CITIES.sameCity(c.city, CITY); });
+ return new Response(JSON.stringify(d), { status: 200, headers: { "Content-Type": "application/json" } });
+ }).catch(function () { return r; });
+ });
+ }
+ return p;
+ };
+
+ // Scope banner + empty-state, injected once the DOM is ready (does not touch app JS).
+ function paintBanner() {
+ if (document.getElementById("crcp-scope-banner")) return;
+ var bar = document.createElement("div");
+ bar.id = "crcp-scope-banner";
+ bar.style.cssText = "position:sticky;top:0;z-index:9999;display:flex;gap:12px;align-items:center;" +
+ "justify-content:center;padding:9px 16px;background:#12305a;color:#eaf1fb;font:13px/1.4 -apple-system," +
+ "BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;border-bottom:1px solid #24487e";
+ var n = scoped.ranked;
+ bar.innerHTML =
+ '<span>Scoped to <b>' + CITY.label + '</b>' + (n ? ' · ' + n + ' listing' + (n === 1 ? '' : 's') : '') + '</span>' +
+ '<a href="https://crcp.agentabrams.com/" style="color:#9ecbff;text-decoration:none;border:1px solid #2f5da3;border-radius:8px;padding:3px 10px">View all markets →</a>' +
+ '<a href="https://recities.crcp.agentabrams.com/" style="color:#9ecbff;text-decoration:none;border:1px solid #2f5da3;border-radius:8px;padding:3px 10px">All cities</a>';
+ document.body.insertBefore(bar, document.body.firstChild);
+
+ if (scoped.ranked === 0 && scoped.total > 0) { // scoped after data loaded, but city has none
+ var note = document.createElement("div");
+ note.style.cssText = "margin:22px;padding:18px 20px;border:1px solid #24487e;border-radius:12px;" +
+ "background:#0f1b30;color:#c7d5ea;font:14px/1.5 -apple-system,sans-serif;text-align:center";
+ note.innerHTML = "No CRCP listings in <b>" + CITY.label + "</b> yet. " +
+ '<a href="https://crcp.agentabrams.com/" style="color:#9ecbff">Browse all markets →</a>';
+ bar.insertAdjacentElement("afterend", note);
+ }
+ }
+ // ranked.json loads async; re-check the empty-state shortly after boot so the count is accurate.
+ function ready() { paintBanner(); setTimeout(paintBanner, 2500); }
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", ready);
+ else ready();
+})();
diff --git a/public/recities.html b/public/recities.html
index db5f854..61b2eb0 100644
--- a/public/recities.html
+++ b/public/recities.html
@@ -11,36 +11,36 @@
}
*{box-sizing:border-box}
body{margin:0;background:linear-gradient(180deg,#0f1419,#121924);color:var(--ink);
- font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
- min-height:100vh}
+ 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 .tag{color:var(--mut);font-size:14px}
.sub{color:var(--mut);margin:0 0 26px}
- .panel{background:var(--panel);border:1px solid var(--edge);border-radius:14px;padding:22px;
- box-shadow:0 8px 30px rgba(0,0,0,.25)}
+ .panel{background:var(--panel);border:1px solid var(--edge);border-radius:14px;padding:22px;box-shadow:0 8px 30px rgba(0,0,0,.25)}
.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}
+ label.lbl{color:var(--mut);font-size:13px;text-transform:uppercase;letter-spacing:.6px;display:block;margin-bottom:8px}
select,button,input[type=search]{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;
+ 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{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 10px}
- input#filter{width:100%;background:var(--panel2);color:var(--ink);border:1px solid var(--edge);
- border-radius:10px;padding:12px 14px;margin-top:22px}
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-top:14px}
- a.city{display:block;text-decoration:none;color:var(--ink);background:var(--panel2);
- border:1px solid var(--edge);border-radius:10px;padding:12px 14px;transition:.12s}
+ .row2{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:16px}
+ .count{color:var(--mut);font-size:13px}
+ .toggle{color:var(--mut);font-size:13px;display:flex;gap:7px;align-items:center;cursor:pointer;user-select:none}
+ input#filter{width:100%;background:var(--panel2);color:var(--ink);border:1px solid var(--edge);border-radius:10px;padding:12px 14px;margin-top:22px}
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-top:14px}
+ 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:#26313f;transform:translateY(-1px)}
+ a.city.empty{opacity:.5}
a.city .nm{font-weight:600}
- a.city .host{display:block;color:var(--mut);font-size:12px;margin-top:2px}
- .empty{color:var(--mut);padding:20px 4px}
+ 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:#1a222e;border:1px solid var(--edge);border-radius:20px;padding:2px 9px}
+ a.city .n.has{color:#bfe0ff;border-color:#2f5da3}
+ .empty-note{color:var(--mut);padding:20px 4px}
footer{color:var(--mut);font-size:12px;margin-top:34px}
</style>
</head>
@@ -50,7 +50,7 @@
<h1>CRCP · Cities</h1>
<span class="tag">Commercial Real Estate — pick a market</span>
</header>
- <p class="sub">Every city market we serve. Choose one to open its dashboard.</p>
+ <p class="sub">Markets with active listings. Choose one to open its scoped dashboard.</p>
<div class="panel">
<label class="lbl" for="city">Jump to a city</label>
@@ -59,64 +59,86 @@
<button class="go" id="go">Open →</button>
</div>
<input id="filter" type="search" placeholder="Filter cities…" autocomplete="off">
- <div class="count" id="count"></div>
+ <div class="row2">
+ <span class="count" id="count"></span>
+ <label class="toggle"><input type="checkbox" id="showEmpty"> Show empty markets</label>
+ </div>
<div class="grid" id="grid"></div>
</div>
<footer id="foot"></footer>
</div>
+<script src="/crcp-cities.js"></script>
<script>
-// The set of city markets served under *.crcp.agentabrams.com (each resolves + serves the CRCP app).
-// To add/remove a city: add its subdomain vhost + DNS, then add it here. slug = subdomain label.
-const LABELS = {
- belair:"Bel-Air", beverlyhills:"Beverly Hills", canyondam:"Canyon Dam", chinesecamp:"Chinese Camp",
- crescentmills:"Crescent Mills", echolake:"Echo Lake", forestranch:"Forest Ranch", grassvalley:"Grass Valley",
- grizzlyflats:"Grizzly Flats", klamathriver:"Klamath River", lakeelsinore:"Lake Elsinore", northfork:"North Fork",
- pacificpalisades:"Pacific Palisades", paynescreek:"Paynes Creek", sanjuancapistrano:"San Juan Capistrano",
- santamonica:"Santa Monica", shermanoaks:"Sherman Oaks", sierramadre:"Sierra Madre", southlaketahoe:"South Lake Tahoe",
- studiocity:"Studio City", twinbridges:"Twin Bridges", woffordheights:"Wofford Heights"
-};
-const SLUGS = ["altadena","belair","beverlyhills","caliente","camarillo","canyondam","chester","chico",
- "chinesecamp","clearlake","cohasset","colfax","crescentmills","doyle","echolake","encino","foresthill",
- "forestranch","georgetown","grassvalley","greenville","grizzlyflats","havilah","hemet","janesville",
- "klamathriver","lakeelsinore","lakehead","malibu","mariposa","mineral","northfork","pacificpalisades",
- "pasadena","paynescreek","redding","sanjuancapistrano","santamonica","shermanoaks","sierramadre","somerset",
- "somis","southlaketahoe","studiocity","topanga","twinbridges","ukiah","weed","woffordheights","wrightwood"];
+var CITIES = window.CRCP_CITIES.CITIES.slice(); // from the shared cities module
+var sameCity = window.CRCP_CITIES.sameCity;
+var counts = {}; // slug -> listing count (from ranked.json)
+var loaded = false;
+var urlOf = function (c) { return "https://" + c.host + "/"; };
-const title = s => s.replace(/(^|\s)\S/g, c => c.toUpperCase());
-const CITIES = SLUGS.map(slug => ({
- slug, label: LABELS[slug] || title(slug),
- host: slug + ".crcp.agentabrams.com"
-})).sort((a,b) => a.label.localeCompare(b.label));
-const urlOf = c => "https://" + c.host + "/";
+var sel = document.getElementById('city'), grid = document.getElementById('grid'),
+ count = document.getElementById('count'), filter = document.getElementById('filter'),
+ showEmpty = document.getElementById('showEmpty'), foot = document.getElementById('foot');
-const sel = document.getElementById('city');
-CITIES.forEach(c => { const o=document.createElement('option'); o.value=c.slug; o.textContent=c.label; sel.appendChild(o); });
+// Count listings per city from the same snapshot the app uses. Best-effort: if it fails, show all.
+fetch('/data/ranked.json').then(function (r) { return r.json(); }).then(function (d) {
+ var arr = (d && d.ranked) || [];
+ CITIES.forEach(function (c) {
+ counts[c.slug] = arr.filter(function (p) { return sameCity(p.city, c); }).length;
+ });
+ loaded = true; render();
+}).catch(function () { loaded = true; render(); }); // no counts -> treat all as shown
-const grid = document.getElementById('grid'), count = document.getElementById('count'),
- filter = document.getElementById('filter'), foot = document.getElementById('foot');
+function visible() {
+ var withData = CITIES.filter(function (c) { return (counts[c.slug] || 0) > 0; });
+ // Default: hide empty markets (Steve's choice). Toggle reveals all 50. Before counts load, show all.
+ var base = (!loaded || showEmpty.checked || withData.length === 0) ? CITIES : withData;
+ return base.slice().sort(function (a, b) {
+ var na = counts[a.slug] || 0, nb = counts[b.slug] || 0;
+ if (nb !== na) return nb - na; // most inventory first
+ return a.label.localeCompare(b.label);
+ });
+}
+
+function fillSelect() {
+ var list = visible(); sel.innerHTML = '';
+ list.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 draw(q){
- q = (q||'').trim().toLowerCase();
- const shown = CITIES.filter(c => !q || c.label.toLowerCase().includes(q) || c.slug.includes(q));
+function render() {
+ fillSelect();
+ var q = (filter.value || '').trim().toLowerCase();
+ var list = visible().filter(function (c) { return !q || c.label.toLowerCase().indexOf(q) >= 0 || c.slug.indexOf(q) >= 0; });
grid.innerHTML = '';
- if(!shown.length){ grid.innerHTML = '<div class="empty">No cities match “'+q+'”.</div>'; }
- shown.forEach(c => {
- const a = document.createElement('a');
- a.className='city'; a.href=urlOf(c);
- a.innerHTML = '<span class="nm">'+c.label+'</span><span class="host">'+c.host+'</span>';
+ if (!list.length) { grid.innerHTML = '<div class="empty-note">No cities match “' + q + '”.</div>'; }
+ list.forEach(function (c) {
+ var n = counts[c.slug] || 0;
+ var a = document.createElement('a');
+ a.className = 'city' + (loaded && n === 0 ? ' empty' : '');
+ a.href = urlOf(c);
+ a.innerHTML = '<span><span class="nm">' + c.label + '</span><span class="host">' + c.host + '</span></span>' +
+ '<span class="n' + (n ? ' has' : '') + '">' + (loaded ? n : '·') + '</span>';
grid.appendChild(a);
});
- count.textContent = shown.length + (shown.length===CITIES.length ? '' : ' of '+CITIES.length) + ' cities';
+ var shownCities = visible();
+ var withData = CITIES.filter(function (c) { return (counts[c.slug] || 0) > 0; }).length;
+ count.textContent = loaded
+ ? (shownCities.length + ' markets shown · ' + withData + ' with listings · ' + CITIES.length + ' total')
+ : (CITIES.length + ' city markets');
+ foot.textContent = 'CRCP · recities.crcp.agentabrams.com';
}
-function open(){ const c = CITIES.find(x=>x.slug===sel.value); if(c) window.location.href = urlOf(c); }
+function open() { var c = CITIES.find(function (x) { return x.slug === sel.value; }); if (c) window.location.href = urlOf(c); }
document.getElementById('go').addEventListener('click', open);
-sel.addEventListener('keydown', e => { if(e.key==='Enter') open(); });
-filter.addEventListener('input', e => draw(e.target.value));
-draw('');
-foot.textContent = CITIES.length + ' city markets · CRCP · recities.crcp.agentabrams.com';
+sel.addEventListener('keydown', function (e) { if (e.key === 'Enter') open(); });
+filter.addEventListener('input', render);
+showEmpty.addEventListener('change', render);
+render();
</script>
</body>
</html>
diff --git a/scripts/serve.js b/scripts/serve.js
index a783f69..1177149 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -67,6 +67,10 @@ app.use((req, res, next) => {
// so the sign-in page stays pristine. Anything not a readable public html file falls through.
const PUB_DIR = path.join(ROOT, 'public');
const BADGE_TAG = '<script src="/user-badge.js" defer></script>';
+// Per-city scoping: injected in <head> BEFORE the app's inline boot so crcp-scope.js can wrap
+// 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>';
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';
@@ -76,9 +80,14 @@ app.get(/(?:^\/$|\.html$)/, (req, res, next) => {
if (!file.startsWith(PUB_DIR + path.sep)) return next(); // path-traversal guard
fs.readFile(file, 'utf8', (err, html) => {
if (err) return next(); // not a public html file -> other routes
- const out = html.includes('/user-badge.js') ? html
+ 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')) {
+ out = out.includes('</head>') ? out.replace(/<\/head>/i, SCOPE_HEAD + '</head>')
+ : (out.includes('<body') ? out.replace(/<body[^>]*>/i, '$&' + SCOPE_HEAD) : SCOPE_HEAD + out);
+ }
res.type('html').send(out);
});
});
← 34a021c 5x: verify crcp list view — six-way green, 29 clickthrough f
·
back to Commercialrealestate
·
loan-officers: ingest CA DRE MLO List (LA-County active indi beeafe7 →