← back to Commercialrealestate
CRCP: spec for per-city scoping of the *.crcp fleet (front-end filter + coverage reality)
81565408663fbc2042d755de97a79d2821829692 · 2026-08-20 12:43:42 -0700 · Steve Abrams
Files touched
A docs/recities-city-scoping-spec.md
Diff
commit 81565408663fbc2042d755de97a79d2821829692
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 12:43:42 2026 -0700
CRCP: spec for per-city scoping of the *.crcp fleet (front-end filter + coverage reality)
---
docs/recities-city-scoping-spec.md | 93 ++++++++++++++++++++++++++++++++++++++
1 file changed, 93 insertions(+)
diff --git a/docs/recities-city-scoping-spec.md b/docs/recities-city-scoping-spec.md
new file mode 100644
index 0000000..cf8fb86
--- /dev/null
+++ b/docs/recities-city-scoping-spec.md
@@ -0,0 +1,93 @@
+# Spec: per-city scoping for the `*.crcp.agentabrams.com` fleet
+
+**Goal:** when a visitor lands on `<city>.crcp.agentabrams.com`, the grid shows only that
+city's listings (today every subdomain serves the identical, unscoped app). `recities.` stays
+the launcher hub; base `crcp.` stays unscoped ("all markets").
+
+**Status:** spec only — not built. Author: main-loop, 2026-08-20 (TK: recities-standup).
+
+---
+
+## 1. Architecture reality (measured, not assumed)
+
+- The main grid is **100% front-end**: `index.html` does `fetch('data/ranked.json')` → `DATA` →
+ `DATA.ranked` → `recompute()`/`render()`. Prod (Kamatera) has **no Postgres**; it serves the
+ static `data/ranked.json` snapshot. **So scoping is a front-end filter, NOT a serve.js change.**
+ (My earlier "larger serve.js change" note was wrong — corrected here.)
+- `ranked.json`: **11,919 listings**, `meta.market = "Los Angeles County, CA"` but the data is
+ effectively **national** (top cities: Los Angeles 1255, Las Vegas 357, Houston 265, Miami 253,
+ Dallas 126, NYC 106…). Each property has a `.city` string (mixed case, some dupes like
+ `Reseda`/`RESEDA`).
+- Host→city mapping already exists as the hub's `LABELS`/`SLUGS` map (`recities.html`).
+
+## 2. THE CRUX — data coverage is thin and exact-match under-counts
+
+Exact city-name match of the 50 fleet slugs against `ranked.json`:
+
+- **Only 23/50 subdomains have ANY listings.** 27 render empty.
+- Populated but thin: Pasadena 137, Santa Monica 88, Beverly Hills 27, Malibu 23, Sherman Oaks 20,
+ Altadena 13, Greenville 13, Georgetown 11, Studio City 8, Redding 8 … then a long tail of
+ Somis 1, SJC 2, Ukiah 2, South Lake Tahoe 2, Somerset 2.
+- **False zeros:** `Encino` = 0 despite being prime SFV — its addresses are tagged `Los Angeles`.
+ Exact city match systematically under-counts neighborhoods that the source lumps into "Los Angeles".
+- The 27 empties are mostly small NorCal towns (Weed, Ukiah, Chester, Colfax…) provisioned as
+ subdomains ahead of any inventory.
+
+**Implication:** the hard part is not the filter — it's honesty about coverage. A naive
+"filter by city" ships 27 dead-empty city pages and mislabels Encino/Topanga as having none.
+
+## 3. Recommended design (phased)
+
+### Phase 1 — front-end hostname scope + honest hub (small, ship first)
+1. **`index.html`, in the `loadRanked().then` boot (~line 2307), before first `render()`:**
+ ```js
+ const CITY = cityFromHost(location.hostname); // null for crcp./recities./localhost
+ if (CITY) {
+ DATA.ranked = DATA.ranked.filter(p => sameCity(p.city, CITY));
+ DATA.meta.market = CITY; // sub-header shows the scoped market
+ }
+ ```
+ - `cityFromHost`: take label-0 of the host; if it's `crcp`/`recities`/`www`/an IP → return null
+ (unscoped). Else map slug→city via the shared `LABELS` table (single source of truth — extract
+ it to `public/crcp-cities.js` and `<script>`-include it in both `recities.html` and `index.html`).
+ - `sameCity(a,b)`: normalize `toLowerCase().replace(/[^a-z]/g,'')` both sides, compare equal.
+2. **Scope banner** (top of grid): `Showing {City} · {n} listings · [View all markets →]` where the
+ link clears scope (`https://crcp.agentabrams.com/`). Non-negotiable escape hatch.
+3. **Empty state** when `n===0`: `No CRCP listings in {City} yet · [Browse all markets →]` — never a
+ blank grid.
+4. **Hub honesty (`recities.html`):** badge each city in the dropdown/grid with its live count and
+ **grey out / sort-to-bottom the zero-count cities** (fetch `data/ranked.json` once, compute counts
+ client-side). So the launcher never sends someone into a dead city unknowingly.
+ - Optional toggle: "Hide empty markets" (default ON) → dropdown shows the ~23 real ones.
+
+**Effort:** ~1 file (`index.html`) + extract `crcp-cities.js` + hub badge logic. Front-end only →
+deploys via `push-frontend-kamatera.sh` (**no serve.js, no app restart**). ~half a day.
+
+### Phase 2 — fix the false zeros (medium, only if Steve wants marquee cities accurate)
+Exact-match misses neighborhood-of-LA cities. Add a **ZIP/alias table** for the marquee SFV/Westside
+subdomains so `Encino`, `Topanga`, `Bel-Air`, `Pacific Palisades` match by ZIP prefix or alias:
+```js
+ALIASES = { encino:{zips:['91316','91436'], names:['Encino']},
+ topanga:{zips:['90290']}, belair:{zips:['90077'], names:['Bel Air','Bel-Air']}, … }
+sameCity() also matches p.zip against the slug's zips.
+```
+Requires `ranked.json` rows to carry `zip` (they do). ~1 day incl. curating ZIPs for the ~10 cities
+that matter.
+
+### Phase 3 — server-side scope (only if SEO/perf ever matters; NOT recommended now)
+Pre-split `ranked.json` into `data/by-city/<slug>.json` at snapshot-build time and have serve.js pick
+by host. Adds a build step + a serve.js host-read. Unnecessary for an auth-gated internal tool where
+the whole snapshot already loads fine client-side.
+
+## 4. Decisions for Steve
+- **A. Ship Phase 1 alone?** (exact-match + honest empty-handling + count-badged hub) — recommended.
+- **B. Include Phase 2** ZIP-aliases so Encino/Topanga/Bel-Air aren't false-zero? (recommended for
+ the SFV cities Steve cares about).
+- **C. Empty subdomains:** hide from hub (default) · show greyed with "no inventory" · or **de-provision**
+ the 27 dead NorCal subdomains entirely (DNS+vhost cleanup) so the fleet == real coverage.
+- **D. Scope source of truth:** city-name (Phase 1) is quick but coarse; ZIP (Phase 2) is accurate.
+
+## 5. Non-goals / guardrails
+- No serve.js change in Phase 1/2 → no prod restart, blast radius = `index.html` + hub.
+- Keep base `crcp.` and `recities.` unscoped.
+- Don't fabricate a city for a listing; unknown/blank `p.city` never matches a scoped host.
← a337f9a loan-officers: seed 29 CA officers from firm public rosters
·
back to Commercialrealestate
·
5x: verify crcp list view — six-way green, 29 clickthrough f 34a021c →