{"slug":"la-socrata-ingester","total":103,"limit":100,"offset":0,"since":null,"commits":[{"hash":"30db64a","date":"2026-09-23 08:28:30 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-23T08:28:09 (1 data files) — data/canary-baseline.json","body":""},{"hash":"ac0fc9c","date":"2026-09-18 11:19:46 -0700","author":"Steve Abrams","subject":"buildingpermits viewer: remove VIEWER_NO_AUTH bypass, require un/pw","body":""},{"hash":"d0c1307","date":"2026-09-15 08:13:57 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-15T08:12:49 (1 data files) — data/canary-baseline.json","body":""},{"hash":"cbc32be","date":"2026-09-12 20:22:20 -0700","author":"Steve Abrams","subject":"fix(la-data): adaptive page-size backoff for oversized-geometry gateway 502s (TK-11500)","body":"Zoning stalled at 75% (43,957 of 58,953 rows) because the NavigateLA gateway 502s\non a GEOMETRY page whose response exceeds ~30s to serialize. Root cause (pre-measured\nby claude-run-10955, re-confirmed live 2026-09-12): the OBJECTID 42000-56000 band\ncarries ~22KB of geometry per feature, so a 2000-row page there is ~44MB. It is a\nfixed OBJECTID band, not a fixed depth, so neither offset- nor oid-cursor paging\navoids it — the constraint is response SIZE.\n\nFix: arcgisPages now halves resultRecordCount on a gateway size-failure and retries\nthe SAME cursor position (down to a floor, LA_ADAPTIVE_MIN_PAGE default 50), then\ngrows back x2 on success so the rest of the layer still pages fast (AIMD). General to\nboth offset and oid modes. fetchArcgis gains an httpTries knob so the descent fails\nfast while shrinking and spends the full retry budget only at the floor — a page that\n502s even at the floor is a real outage and is rethrown (fail-loud; the aggregate\nrun's allowFailure exemption still catches a genuinely stuck source). Only geometry\npages shrink; a non-geometry transient is a real upstream outage and is never masked.\n\nProven three ways: negative test test/arcgis-adaptive-page.test.mjs (red on the pre-fix\nbaseline, green with the fix; covers offset backoff, all-band completion, non-geometry\nfail-loud, oid mode); full existing suite unchanged; and a bounded read-only live run\nagainst the real OBJECTID>42000 band — real 502 at 2000 caught, halved to 1000,\ncompleted 2200 rows.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01FkjvkxHU1B2a7gfVQFKzxX"},{"hash":"1879ff0","date":"2026-09-12 19:42:52 -0700","author":"Steve Abrams","subject":"chore: v0.2.4 (session close)","body":"Version bump only — reflects the TK-10955 assessor_parcels allowFailure fix\n(93a7ac5) committed earlier this session. No lint issues found (no linter\nconfigured in this repo); reviewed the diff manually, no refactor needed —\nit mirrors the existing gis_zoning allowFailure pattern exactly.\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01MffyeUFB1BstaK2xS7ufYd"},{"hash":"93a7ac5","date":"2026-09-12 13:21:28 -0700","author":"Steve Abrams","subject":"fix(la-data): tolerate assessor_parcels' audited page-1 400 (TK-10955)","body":"FeatureServer has twice returned the same generic HTTP 400 (\"Cannot perform\nquery. Invalid query parameters.\") on page 1 before any row is fetched\n(2026-09-10, 2026-09-12), always ~55.3s after the request to within 50ms\nacross both occurrences -- consistent with an upstream execution-time\nceiling under load, not a bad query. Manual replay of the byte-identical\nruntime query succeeded live both times. Table is upsert-only (conflict on\nain+roll_year, never deleted) and the OID-cursor rescans the full current\nroll year every run, so a skipped day just leaves yesterday's 2.4M-row\nsnapshot in place one extra day -- no data loss, no corruption.\n\nAdded an allowFailure exemption scoped to the exact audited message,\nmirroring gis_zoning's existing precedent exactly: a genuinely different\nassessor_parcels error still fails the aggregate run loud, and a direct\n(non-aggregate) run stays strict. Extended test/run-policy.test.mjs with a\npositive control, a negative control, and a direct-run-stays-strict check.\n\nVerified: 4/4 suites pass (18 checks); live smoke test (--max=200) confirms\nnormal ingestion is unaffected; full 'cli.js all' aggregate re-run today\nexits 0 via the real classifyRunResults against real SOURCES (gis_zoning's\nknown 502 tolerated, assessor_parcels succeeded outright this run).\n\nCo-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01MffyeUFB1BstaK2xS7ufYd"},{"hash":"28fcd49","date":"2026-09-11 11:51:36 -0700","author":"Steve Abrams","subject":"fix(la-data): don't cache a failed catalog fetch; report probe errors honestly","body":"Two defects surfaced while running the shipped resolver past a second model. Codex\nwas spend-capped (HTTP 429) and Kimi's key is expired, so the lens was a local 8B\nmodel whose output was mostly a restatement of the questions — but two of its\nprompts were worth answering properly, and one was a real bug.\n\nREAL BUG — fetchCatalog memoised FAILURES. `Promise.resolve(...).catch(() => null)`\ncached a null for the life of the process, so one transient blip while resolving the\nFIRST drifted source silently downgraded every later source in the same run to the\nwindow-scan path. Not a correctness hole (the window path also demands full proof)\nbut needlessly narrower. Failures are now forgotten so the next source retries.\nProven by check 11 plus a negative control showing the old shape called the catalog\nonce where the new one calls it twice.\n\nMESSAGE HONESTY — probeLayer swallowed every error into `null`, which is\nindistinguishable from \"that layer isn't there\". It still fails loud either way\n(never ingests), but the error text could mislead a human into reading a flaky\nupstream as \"the layer genuinely moved away\". Probe errors are now counted and the\nfailure message says \"(N probe(s) ERRORED — absence here is not proof of absence)\".\n\nAnalysed and NOT changed: \"what if the catalog and the per-layer plane are BOTH\nstale?\" A consistently-old upstream is self-consistent — our pinned (new) id\nmismatches, the old catalog names the old id, the old per-layer plane re-proves it,\nand we repoint to the old id, which in that world IS our layer. Correct. The\ndangerous case is SKEW between the two planes, which is exactly what the re-proof\nstep catches and what check 3 covers. The 'unverifiable keeps a pin but cannot MOVE'\nasymmetry is likewise deliberate, not a false-green: it never lets weak evidence\nmove us onto a new layer, it only declines to break a pin that already matches.\n\n5/5 suites (11 auto-repoint checks). Live re-verified: 9/9 clean on the pinned ids,\n9/9 auto-repoint from stale ids. TK-10955. $0 local.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6"},{"hash":"482ed58","date":"2026-09-11 11:46:35 -0700","author":"Steve Abrams","subject":"feat(la-data): verified auto-repoint for drifting NavigateLA layer ids","body":"Steve's call (2026-09-11, DTD option B/D). NavigateLA layer ids are positional, so\nLA inserting one layer renumbers everything below it. That happened on 09-05 and\nagain on 09-11 (\"Special Event Permits\" at position 52, 452 -> 453 layers). The\nfail-loud-only posture ratified on 09-10 was right at the time but, at that\nrecurrence rate, means a roughly weekly outage where GIS layers sit stale until a\nhuman repoints. This reversal is deliberate and the new fact is the recurrence.\n\nNew policy: the pinned id is a HINT. On a name mismatch the adapter FINDS the layer\nby exact name and PROVES it before reading a single row; it still fails loud on\nanything it cannot prove.\n\n  resolveLayerIdentity(src) — two discovery paths, proof required either way:\n    1. service catalog (one cached GET) — covers any drift distance\n    2. bounded +/-N window scan of per-layer endpoints — the fallback for a STALE\n       catalog, which was MEASURED on 09-11: it served a 452-layer body (old index)\n       while per-layer endpoints already served 453, so resolving off the catalog\n       ALONE would have returned the WRONG layer. A catalog hit is never trusted on\n       its own; it is always re-proved on /MapServer/<id>?f=json, the plane /query\n       follows.\n\nProof required to MOVE: exact normalized name + type 'Feature Layer' + a field\nfingerprint (the source's oidField present, and at least one nameField when it\ndeclares any). That closes codex's stated risk — a replacement dataset reusing the\nexact name — and the documented layer-73 trap (same name, Group Layer). Measured\n9/9 live before enforcing, so it cannot false-alarm today.\n\nFails loud, never guesses: a rename, zero matches, MORE THAN ONE proved match (the\nwindow is scanned whole, so ambiguity is detected rather than masked), drift beyond\nthe window with no catalog, or right-name-wrong-schema on the pinned id (which\nnever triggers a search — a replaced dataset needs a human). Deliberate asymmetry:\nmetadata with no field list keeps a WORKING pin but is never enough to MOVE.\n\nLIVE PROOF, not just mocks: replaying the pre-fix ids against the real MapServer\nauto-repoints 9/9 to the correct layer via the catalog; with the catalog forced\nunavailable, the window scan recovers 419->420 and correctly fails loud on a drift\nof 419. Had this shipped a day earlier, today's 04:30 run would have self-healed.\n\ntest/auto-repoint.test.mjs — 10 checks, 8 of them NEGATIVE controls (Group Layer,\nwrong schema, ambiguity, rename, schema drift, kill switch, unpinned passthrough).\nCheck 9 earned its keep immediately: it caught that LA_NO_AUTO_REPOINT and\nLA_REPOINT_WINDOW were module-load consts, so the kill switch silently did nothing.\nBoth now read at call time.\n\nAlso closes a coverage regression the contrarian red-team found in 314f1dc: the old\ncheck 6 caught two sources sharing one layer id only as a side effect of asserting\nexact ids, and dropping it traded a static catch for a runtime-only one. Asserted\ndirectly now (check 7b), with a negative control.\n\n4/4 suites pass. TK-10955. $0 local (free public ArcGIS GETs).\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6"},{"hash":"314f1dc","date":"2026-09-11 11:24:41 -0700","author":"Steve Abrams","subject":"fix(la-data): repoint 9 NavigateLA layers after a SECOND upstream +1 insertion","body":"LA inserted the Feature Layer \"Special Event Permits\" at position 52 of the\nNavigateLA MapServer (layer count 452 -> 453), shifting every layer at id >= 52\nby +1 — the same mechanism as 2026-09-05, six days later.\n\nThis time the expectName guard from 88e40a8 caught it: the 2026-09-11 04:30 run\nfailed all 8 pinned NavigateLA sources loud at preflight and ingested NOTHING,\nso there was no corruption to clean up. la_gis_features was verified to still\nhold exactly the correct upstream counts. The exit=1 was a TRUE POSITIVE.\n\nRepointed +1 and verified 9/9 on BOTH planes that matter — exact upstream layer\nNAME and historical ROW COUNT: zoning 72->73, council_districts 419->420,\nneighborhood_councils 440->441, hpoz 76->77, community_plan_areas 415->416,\nfault_zones 120->121, liquefaction 125->126, flood 255->256, fire_vhfhsz\n359->360. Live: 9/9 preflight pass, 9/9 negative control (the now-stale ids are\nall rejected). Re-ingest of the 8 affected sources: 8/8 exit 0 with exactly the\nhistorical counts, content spot-checked semantically (real council members, real\ncommunity plan areas, real HPOZ districts) — not just matching counts.\n\ntest/layer-identity.test.mjs check 6 rewritten. It asserted specific numeric ids,\nwhich is a restatement of config: it proves nothing about correctness and turns\nevery legitimate repoint into a test edit. It now asserts the durable invariants —\nevery NavigateLA-served source is ARMED (pins an expectName, or the guard is a\nno-op for it) and no two sources expect the same upstream name. Both are proven\nto go red under injected faults.\n\nDocumented the trap that nearly shaped the fix wrongly: /MapServer?f=json is\ncacheable and was observed serving a STALE body (452 layers, old index) while the\nper-layer endpoints already served the new one. Resolving a layer by name off the\ncatalog alone would have returned the WRONG id. The authoritative plane is\n/MapServer/<id>?f=json, because that is what /query follows.\n\nTK-10955. $0 local (free public ArcGIS GETs + local Postgres).\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6"},{"hash":"f6042cd","date":"2026-09-10 08:01:31 -0700","author":"Steve","subject":"fix(la-data): scope allowFailure to the audited failure, not the source name","body":"Contrarian review (TK-10955) found that the identity guard shipped in 88e40a8 was\nsilently neutered for the one source that carries an exemption. classifyRunResults()\ntolerated ANY failure on a source whose allowFailure.scope matched, keyed purely on the\nsource NAME and never on the cause. So a future NavigateLA re-index that drifted zoning's\nlayer 72 would throw \"ArcGIS layer identity drift\" -> land in the tolerated bucket ->\nexit 0, and the cron exit code (the one alarm that actually caught the 2026-09-05\nincident) would never fire for the exact failure class the guard exists to make loud.\n\n- allowFailure gains an optional `match` array; a failure is tolerated only if its error\n  text contains one of the audited signatures. Omitting `match` keeps prior behaviour.\n- gis_zoning pins match to its audited gateway degradation in BOTH error shapes:\n  the HTTP-level form (\"HTTP 502 after 5 tries\") and the ArcGIS body-level form\n  (\"ArcGIS error: {\"code\":502,...}\").\n- test/run-policy.test.mjs: the audited 502 still exits 0; identity drift exits 1;\n  asserted against the live SOURCES config, not just a mock.\n\nThis is closer to the ratified 2026-08-30 DTD intent (\"exit 1 only for unexpected\nfailures\") than the previous blanket-tolerate: a genuine 400 on gis_zoning was also\nbeing swallowed and now correctly exits 1.\n\nTK-10955\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_019LHiEXMR2FCqRqUmoK4kQY"},{"hash":"88e40a8","date":"2026-09-10 07:40:43 -0700","author":"Steve","subject":"fix(la-data): repoint 9 NavigateLA layers after upstream +1 re-index; pin layers by NAME","body":"On 2026-09-05 LA inserted a layer into the NavigateLA MapServer and every layer id\nshifted +1. Sources pinned bare numeric ids, so all 9 GIS layers silently re-pointed\nat a different dataset. Only 2 failed loudly (gis_fault_zones hit a Group Layer,\ngis_flood hit a layer with no FLD_AR_ID) — those 400s are what has been failing the\nnightly run's exit code since 09-05. The other 7 landed on queryable layers and\ningested a stranger's rows under our labels for 6 days with no error at all\n(council_district 15->3, neighborhood_council 99->26, hpoz 35->7843,\ncommunity_plan_area 36->7, liquefaction 474->14181, fire_vhfhsz 14->247).\n\n- repoint 71->72, 75->76, 119->120, 124->125, 254->255, 358->359, 414->415,\n  418->419, 439->440; verified read-only that all 6 countable layers return their\n  exact historical row counts and that 120/255/72 carry the fields our mappers need.\n- add assertLayerIdentity(): every gisSrc now pins the expected upstream layer NAME\n  and the adapter verifies it before paging, so the next re-index fails LOUD instead\n  of silently ingesting the wrong layer. An ArcGIS layer id is a positional index,\n  not an identifier — this is the second id-drift incident (cf. 559875d).\n- test/layer-identity.test.mjs: zero-network/zero-DB proof, incl. that all 9 sources\n  are pinned to the post-re-index ids.\n\nDoes not touch the 2026-08-30 allowFailure exit policy (ece3ea5): run-policy and\narcgis-retry suites still pass.\n\nTK-10955\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_019LHiEXMR2FCqRqUmoK4kQY"},{"hash":"f5caca4","date":"2026-09-08 08:26:13 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-08T08:24:00 (1 data files) — data/canary-baseline.json","body":""},{"hash":"f524e0b","date":"2026-09-05 08:10:38 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-05T08:08:29 (1 data files) — data/canary-baseline.json","body":""},{"hash":"feb3908","date":"2026-09-01 08:30:10 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-01T08:28:16 (1 data files) — data/canary-baseline.json","body":""},{"hash":"79e59f8","date":"2026-08-30 08:10:12 -0700","author":"Steve Abrams","subject":"Record LA refresh exit-policy proof","body":""},{"hash":"ece3ea5","date":"2026-08-30 08:09:33 -0700","author":"Steve Abrams","subject":"Treat audited zoning failure as aggregate degradation","body":""},{"hash":"9460ee0","date":"2026-08-27 08:21:20 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-27T08:19:36 (1 data files) — data/canary-baseline.json","body":""},{"hash":"1d31e30","date":"2026-08-26 09:02:50 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-26T09:01:00 (1 data files) — package-lock.json","body":""},{"hash":"6069fd2","date":"2026-08-26 08:59:27 -0700","author":"claude-run-10749","subject":"chore: v0.2.3 (session close — arcgis transient-retry fix)","body":""},{"hash":"a8295e4","date":"2026-08-26 08:41:27 -0700","author":"claude-run-10749","subject":"arcgis: retry transient body-level errors (499/CONT_0001,429,5xx) so one flaky source can't poison the nightly batch exit code (TK-10851)","body":"ArcGIS tunnels errors as HTTP 200 with an {error:{code}} body, so http.js's\nstatus-based retry never saw them and cli.js:76-77 failed the whole 'all' run's\nexit code on a single transient 499. Add payload-aware fetchArcgis retry with\nbackoff; bare 499 (Token Required) stays fail-fast, only 499+CONT_0001 retries.\n+ zero-dep regression test (transient-retry / 400-fail-fast / auth-499-fail-fast)."},{"hash":"45c0d9f","date":"2026-08-26 08:06:09 -0700","author":"steve","subject":"chore: v0.2.2 (session close)","body":""},{"hash":"559875d","date":"2026-08-25 12:11:51 -0700","author":"steve","subject":"fix(la-data): repoint gis_parcels dead 2025 ArcGIS layer -> live 2026_gdb (400 Item-does-not-exist was failing the whole nightly run via pipefail). Verified 1-row smoke ingests clean.","body":""},{"hash":"93d1279","date":"2026-08-25 08:19:08 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-25T08:16:28 (1 data files) — data/canary-baseline.json","body":""},{"hash":"b16e3ff","date":"2026-08-18 08:10:32 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-18T08:10:11 (1 data files) — data/canary-baseline.json","body":""},{"hash":"e6f0356","date":"2026-08-12 11:57:41 -0700","author":"Steve Abrams","subject":"Viewer: CD/ZIP quick-jump control on the Map view","body":"- On-map 'Jump to' control (top-left, clear of the zoom control) with CD + ZIP\n  selects. Picking one sets the filter and the existing fitBounds flies the map\n  to that area; clearing removes it. Options carry live counts and are drawn from\n  a stable unfiltered facet list (allFacets, fetched once at boot) so every CD/ZIP\n  is always offered even when a filter is active. Composes with deep-links + saved\n  views via the shared state.filters.\n\nAuthor: Steve Abrams\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"48e81f3","date":"2026-08-12 11:51:21 -0700","author":"Steve Abrams","subject":"Viewer: per-column filter inputs in the List header","body":"- A sticky filter row under the column headers: each filterable column gets an\n  input wired to the matching server filter param (address->q contains, zip/cd/\n  work_type/building_type/status/era->exact with facet-fed <datalist> autocomplete,\n  issued->since date, project_value->min_value). Reuses state.filters + qs(), so it\n  composes with facets, deep-links, and saved views for free.\n- Focus + caret preserved across the reload-triggered re-render; address input\n  stays in sync with the top search box; inputs only appear for visible columns.\n\nAuthor: Steve Abrams\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"bcf4edb","date":"2026-08-12 11:46:39 -0700","author":"Steve Abrams","subject":"Viewer: saved views — named snapshots of the full view state","body":"- 📑 Views popover: name + save the current filters/sort/view/color/columns/density\n  as a preset, apply any saved view in one click, delete with ✕. Persists to\n  localStorage. Since syncURL() already serializes the whole view state to the\n  querystring, a saved view is just a named snapshot of that query.\n- Refactored boot into reusable hydrateFromQuery(strict) + syncControls(), shared\n  by page load (persisted-pref fallback) and saved-view apply (exact snapshot).\n\nAuthor: Steve Abrams\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"d7a05ae","date":"2026-08-12 11:41:20 -0700","author":"Steve Abrams","subject":"Viewer: column show/hide menu, / search shortcut, density+columns in deep-link","body":"- ▦ Columns popover (List view): checkbox per column to show/hide, show-all +\n  reset actions, always keeps >=1 column visible; persists to localStorage.\n- Keyboard: '/' focuses the search box, Esc closes the modal/popover; placeholder\n  hints the shortcut.\n- Deep-link now also encodes density (d=) and the visible column set+order\n  (cols=) so a shared URL restores the exact table layout. Verified round-trip.\n\nAuthor: Steve Abrams\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"0d560bd","date":"2026-08-12 11:28:24 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-12T11:28:02 (1 data files) — package-lock.json","body":""},{"hash":"57d3d5a","date":"2026-08-12 11:14:06 -0700","author":"Steve Abrams","subject":"Viewer: loading spinner + empty-state + fetch-error overlay","body":"- Loading spinner overlays the results area during every fresh fetch (valuable:\n  the unfiltered lead_score sort over 1.58M rows takes a couple seconds).\n- Empty state when a filter set returns 0 rows: clear message + a Clear-all-filters\n  button (wired to reset) instead of a blank table.\n- Network-error state shows the failure inline instead of a silent blank view.\n\nAuthor: Steve Abrams\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"f56acfa","date":"2026-08-12 11:07:59 -0700","author":"Steve Abrams","subject":"chore: v0.2.1 (session close — rate-limit script finalize)","body":""},{"hash":"d5d5fec","date":"2026-08-12 11:07:56 -0700","author":"Steve Abrams","subject":"Viewer: CSV export of current filtered view + fix blank Issued date column","body":"- ⬇ CSV button exports the current filtered+sorted permits as CSV (client-side,\n  pages /api/permits at 200/req up to a 5,000-row cap, UTF-8 BOM, quoted escaping,\n  filter-named file). Surfaces an explicit notice when the filtered total exceeds\n  the cap (no silent truncation).\n- Fix real bug: API sends the date as issue_date but List/Grid column key is\n  'issued' -> the Issued date rendered blank everywhere. Normalize issued from\n  issue_date on load (List, Grid, and CSV). Verified: column now shows the date.\n\nAuthor: Steve Abrams\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"eb45025","date":"2026-08-12 10:50:09 -0700","author":"Steve Abrams","subject":"rate-limit: prompt for token interactively when none supplied (avoids <bracket> shell-redirect trap)","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"d2328d9","date":"2026-08-12 10:40:08 -0700","author":"Steve Abrams","subject":"add rentv LinkedIn enricher via openclaw (dedup by firm, prioritized, resumable)","body":""},{"hash":"f06221a","date":"2026-08-12 10:31:09 -0700","author":"Steve Abrams","subject":"chore: refactor (ERA_DAYS const, indent), minor version bump (session close)","body":""},{"hash":"952a377","date":"2026-08-12 10:20:24 -0700","author":"Steve Abrams","subject":"property.js: one command → property + crew + each member's LinkedIn/email/phone (--linkedin opt-in openclaw fill)","body":""},{"hash":"258fd0a","date":"2026-08-12 10:16:14 -0700","author":"Steve Abrams","subject":"add targeted openclaw LinkedIn tool (real-browser Google; site-less deal crews; precision-guarded)","body":""},{"hash":"da8cbba","date":"2026-08-12 10:05:51 -0700","author":"Steve Abrams","subject":"yoloforever c1: disk-space preflight in run-refresh.sh (skip+CNCP note below 10G floor) — Cody FIX-FIRST, prevents ENOSPC PG crash at 04:30 on the 96%-full disk","body":""},{"hash":"298f4ef","date":"2026-08-12 10:03:01 -0700","author":"Steve Abrams","subject":"li-search: multi-engine rotation + bing/mojeek (built; PAUSED — $0 bulk LinkedIn search is bot-blocked, integrity risk)","body":""},{"hash":"cefbc8e","date":"2026-08-12 09:57:15 -0700","author":"Steve Abrams","subject":"contacts: per-row try/catch so one bad URL can't crash a shard worker","body":""},{"hash":"71dd4ab","date":"2026-08-12 09:56:01 -0700","author":"Steve Abrams","subject":"li-search: multi-source (cslb + rentv_licensed_targets) through one rate budget","body":""},{"hash":"7ba14e7","date":"2026-08-12 09:49:57 -0700","author":"Steve Abrams","subject":"contacts: fix $2 type inference in linkedin_source CASE (::text cast)","body":""},{"hash":"d5b07c9","date":"2026-08-12 09:48:16 -0700","author":"Steve Abrams","subject":"contacts: linkedin_source tier (site=authoritative vs search=candidate) + phase-2 site-less LinkedIn search","body":""},{"hash":"27b3127","date":"2026-08-12 09:43:42 -0700","author":"Steve Abrams","subject":"Add --testfire button to canary-alert (fires one real CNCP+George alert, synthetic, leaves armed state untouched)","body":""},{"hash":"90cf00f","date":"2026-08-12 09:40:12 -0700","author":"Steve Abrams","subject":"contacts: $0 own-site miner for linkedin/email/phone (NANP-validated, TOS-safe)","body":""},{"hash":"15c6ec1","date":"2026-08-12 09:28:22 -0700","author":"Steve Abrams","subject":"waf: period 10 (plan-allowed) — 20 req/10s = 120/min ceiling","body":""},{"hash":"b8b0d82","date":"2026-08-12 09:26:44 -0700","author":"Steve Abrams","subject":"Add transition-only canary alert wrapper (CNCP card + George email on NEW problem, silent otherwise; --selftest+DRY_RUN verified); point canary plist at it — enable is gated","body":""},{"hash":"cc6b19f","date":"2026-08-12 09:20:25 -0700","author":"Steve Abrams","subject":"Archive la-permits-live-refresh plist for one-copy rollback of the pending consolidation","body":""},{"hash":"5ef077c","date":"2026-08-12 09:19:48 -0700","author":"Steve Abrams","subject":"Add self-rotating refresh wrapper (bounds tmp/ingest.log to ~10MB, no extra cron job); point refresh plist at it — swap is gated","body":""},{"hash":"707ec14","date":"2026-08-12 09:12:53 -0700","author":"Steve Abrams","subject":"viewer: swap live ranking to permit_lead_score_v2 (0/1.58M penalized; teardown signal)","body":""},{"hash":"4c7b267","date":"2026-08-12 09:08:22 -0700","author":"Steve Abrams","subject":"Pre-stage lint-clean launchd plists for daily canary (08:00) + refresh (04:30) — install gated, awaiting Steve","body":""},{"hash":"e9415b8","date":"2026-08-12 09:07:16 -0700","author":"Steve Abrams","subject":"rate-limit: finalize apply-ratelimit.sh — token-free DRY_RUN preview, dedup rule, verified JSON+zone","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"c9ccae5","date":"2026-08-12 08:52:27 -0700","author":"steve","subject":"Unified property drill-down: address -> property + trade structure + NAMED CREW + contacts","body":"Folded getCrew() into property.js (permit-crew.js now exports it, CLI guarded). One\ncommand: node scripts/property.js \"<address>\" -> parcel + permit/trade structure +\ntimeline + full named crew (GC/subs/architect/engineer w/ license, phone, website).\n--no-crew skips the LADBS scrape. $0 (cached LADBS GET + local CSLB join)."},{"hash":"b291216","date":"2026-08-12 08:48:57 -0700","author":"steve","subject":"permit-crew c-fix (Cody gate): era-scope the crew + dates + entity decode + no-match flag","body":"Cody: merging 2018+ permits presented stale-era tradespeople as current crew. Now\nanchors to the latest building permit, includes only ~18mo era (Woodlake correctly\ndrops 2 older permits), shows each permit date, flags GC-appears-as-sub cross-role,\ndistinguishes 'no CSLB match' from not-tried, decodes HTML entities. DTD final: SHIP."},{"hash":"1cd9469","date":"2026-08-12 08:45:41 -0700","author":"steve","subject":"Named build crew (scripts/permit-crew.js) — the linkage layer: subs on any build","body":"Breakthrough (la-research-agent): LADBS PcisPermitDetail is a PUBLIC single GET with\nid1/id2/id3 (permit# split on -), not permitNumber. Chain: property -> all permits on\nparcel -> GET each detail -> Contractor/Architect/Engineer + CSLB license -> match to\nenriched cslb_raw (website/phone). GC on building permit + SUBS on electrical/plumbing/\nmech permits = full named crew. Proven: 7143 Tampa named 7-firm crew w/ phones. $0.\nCached in permit_crew. DTD-decided, la-research-discovered, Cody-gated next."},{"hash":"fa030eb","date":"2026-08-12 08:26:49 -0700","author":"steve","subject":"Property drill-down (scripts/property.js): full permit/trade crew-structure per parcel","body":"Assembles every permit on a parcel grouped by trade (GC building + electrical/plumbing/\nHVAC sub-permits = the crew) + parcel data + timeline. Answers 'who worked on this build'\nas trade structure; contractor names are the LADBS permit-detail scrape layer (next)."},{"hash":"012231a","date":"2026-08-12 08:20:14 -0700","author":"steve","subject":"enrich: domain-guessing pipeline (scripts/enrich-domain.js) — the real max-it unlock","body":"DDG rate-limits by IP so parallel search self-blocks. Domain-guessing hits each\ncontractor's OWN candidate domains (different hosts = no shared rate limit) + local\nmodel verifies the page belongs to them. Test: 5/10 real sites, $0, fully parallel."},{"hash":"14a832a","date":"2026-08-12 08:14:57 -0700","author":"steve","subject":"enrich: don't lose DDG-rate-limited rows (mark ddg-blocked, 45s cooldown, throw on 403/429)","body":"Max-it fan-out over-pressured DuckDuckGo (403s). Bug: errors stamped enriched_at =\nrate-limited contractors marked 'no site' (data loss). Now: 403/429 throws ddg-blocked,\nworker cools 45s, degraded rows reset for clean retry. Gentler concurrency."},{"hash":"daf3111","date":"2026-08-12 08:10:46 -0700","author":"steve","subject":"enrich: --shard=i/N + --ollama/--model for max-it fan-out across Mac2+Mac1 Ollama","body":""},{"hash":"5fc3e57","date":"2026-08-12 08:07:18 -0700","author":"steve","subject":"Contractor enrichment: deterministic directory blocklist (nulls LLM-accepted aggregators)","body":""},{"hash":"c5f4234","date":"2026-08-12 08:05:47 -0700","author":"steve","subject":"Contractor web-enrichment — $0 local (free DDG search + local Ollama verifier)","body":"NO paid API (Steve: Exa $2400 = hard no). Free DuckDuckGo HTML search finds candidate\nURLs; local qwen2.5 verifies + extracts official website/email/linkedin (rejects\ndirectories), matched to the CSLB name/city/phone. Writes website/email/linkedin/\nenrich_* fields on cslb_raw. Resumable (enriched_at stamp), prioritized by hottest\nrecent-permit ZIPs. Test: 4/5 real sites found, $0."},{"hash":"804a15e","date":"2026-08-12 07:55:00 -0700","author":"steve","subject":"yoloforever c8 FIX (Cody): status brief market = permit volume + median (not outlier-inflated sum)","body":"Cody c8: the Market section re-imported the sum(valuation) outlier problem killed in\nc2/c7 ($493M incl. the $375M unverified permit, unflagged). Now ranks hottest district\nby permit VOLUME (outlier-immune) + shows MEDIAN value. Consistent with the digest."},{"hash":"e1a34c1","date":"2026-08-12 07:52:24 -0700","author":"steve","subject":"yoloforever c8: consolidated platform status brief (scripts/status-brief.js)","body":"One dated internal brief tying the 8 cycles together: data footprint (16.6M rows),\ncanary verdict (reads its own heartbeat), top-5 deals, hottest market + contractor\nsupply. Reads each component's own output (truthful); missing artifacts show as gaps."},{"hash":"1d08e30","date":"2026-08-12 07:49:00 -0700","author":"Steve Abrams","subject":"viewer: make filters+sort shareable in the URL (deep-link/bookmark any filtered view)","body":"- syncURL() mirrors filters + sort + view + color into the URL on every render (history.replaceState, no history spam)\n- boot IIFE now restores era/work_type/building_type/status/cd/zip/q/min_value/since + sort/dir from the URL (previously only view+color)\n- verified: applying facets writes ?work_type=Bldg-New&cd=5&sort=...; cold-loading that URL restores active facets, filtered count (9,224), sort, and rows"},{"hash":"d115714","date":"2026-08-12 07:46:34 -0700","author":"steve","subject":"yoloforever c7 FIX (Cody): honest digest — verified-only headline + district table","body":"Cody c7 kill-shot: district table silently summed the flagged $375M into CD9's total\n(#1) while the deals list flagged it — $700M headline was 2x inflated. Now: headline\nsplits verified ($278M) vs flagged/unverified ($422M); district table sorts by verified\nvalue + marks outlier rows (CD11 Palisades now correctly #1; CD9's single-outlier row\ndemoted). Added stale-feed guard. Publishable."},{"hash":"02d9b82","date":"2026-08-12 07:43:12 -0700","author":"steve","subject":"yoloforever c7: RE-news deals-digest markdown generator (scripts/deals-digest.js)","body":"Reads the latest deals-feed JSON (single source of truth) and renders publishable\nmarkdown: top deals w/ council member + parcel value + map links + ⚠verify flags,\nby-council-district table, source disclaimer. Presentation layer only."},{"hash":"4aad896","date":"2026-08-12 07:33:08 -0700","author":"steve","subject":"yoloforever c6 FIX (Cody): drop invalid per-ZIP ratio, add valid county supply/demand + fix trade map","body":"Cody c6: permits_per_contractor by ZIP was a residential-ness artifact (wealthy ZIPs\nhave few contractor BUSINESS addresses, not undersupply) — a caveat can't rescue a\n'90272 underserved' headline that's false. Dropped it. Section 2 now county-level\nsupply vs permit demand by trade (Gen Building 2.7, Electrical 8.4, Plumbing 9.4\npermits/contractor/yr — no geo proxy). Trade-code map fixed (norm dash-stripped)."},{"hash":"e534fb9","date":"2026-08-12 07:29:05 -0700","author":"steve","subject":"yoloforever c6: contractor-market intelligence (scripts/contractor-intel.js)","body":"First use of the 243k CSLB dataset. Joins active LA-County contractors (by trade\nclassification) to permit activity: supply by trade (23,250 General Building etc.)\n+ per-ZIP demand-vs-local-supply (Palisades 90272 = 33.5 permits/contractor, fire-\nrebuild straining local supply). Honesty caveat labeled: contractor ZIP=business\naddress, supply proxy not work-location. INTERNAL (not public site)."},{"hash":"00f8def","date":"2026-08-12 07:06:10 -0700","author":"steve","subject":"yoloforever c5 FIX (Cody): gate teardown bonus on valuation>=$500k","body":"Cody c5: a $250k permit on a $6.9M parcel jumping to 98 rewarded parcel wealth, not\na teardown (that's a garage, not a rebuild). Added valuation>=$500k to the teardown\nCASE. Now: $250k/$6.9M example 98->90 (tier only); real $2.16M rebuild keeps +->100.\n192 true teardowns flagged; 0 penalized. v2 defensible."},{"hash":"48faabe","date":"2026-08-12 07:03:25 -0700","author":"steve","subject":"yoloforever c5: permit_lead_score_v2 — parcel-signal teardown/redevelopment ranking","body":"NEW function (live permit_lead_score untouched). v2 = v1 + up to 15pts parcel signal\n(cap 100, no rescale): teardown (new build on valuable old parcel) + assessed tier.\nValidated: 0 permits penalized (no-parcel = v1), 20,045 lifted, max +15. Surfaces\nhigh-end redevelopment v1 misses (e.g. $250k permit on $6.9M 1922 parcel: 83->98)."},{"hash":"60f8851","date":"2026-08-12 06:59:11 -0700","author":"steve","subject":"yoloforever c4 FIX (Cody): deals feed ranks by lead_score, dedups address, flags outliers","body":"Cody c4 (verified): raw-max-valuation surfaced LAX 3x + a $375M 'addition'. Now:\n(1) DISTINCT ON (address,zip) dedups same-project permits; (2) ORDER BY lead_score\nthen valuation = newsworthiness not brute dollars (ground-up dev now leads, airport\ngone); (3) valuation_outlier flag (>$100M or >150x parcel assessed) tags data\nartifacts for editorial verify. Now publishable as RE-news leads."},{"hash":"909b171","date":"2026-08-12 06:55:49 -0700","author":"steve","subject":"yoloforever c4: newsworthy permit deals feed (scripts/deals-feed.js)","body":"Surfaces top high-value permits (last 90d, >=$1M, issued) as human-readable\nheadlines + structured JSON feed for RENTV/RE-news synergy. Enriched w/ parcel\nvalue + council-member name. Fixed date formatting + 'built 0'. New local surface."},{"hash":"0377c1a","date":"2026-08-11 15:05:09 -0700","author":"steve","subject":"yoloforever c3 FIX (Cody): green-able canary + real CSLB load + time-anchored bleed","body":"Cody c3 (all verified): (1) permanently-FAIL canary = anti-canary -> demote KNOWN_BROKEN\nsources (gis_zoning, documented) to WARNING so verdict can be OK/WARN; a NEW error still\nFAILs. (2) bleed floor was run-count anchored (walks up, misses slow decline) -> time-anchored\nto ~30d. (3) CSLB state was a planted timestamp that lies -> scripts/cslb-load.sh writes\nreal state on actual load. Verdict now WARN (was perpetual FAIL); green = signal again."},{"hash":"f794850","date":"2026-08-11 15:00:48 -0700","author":"steve","subject":"yoloforever c3: close Cody-c1 canary gaps — CSLB tracking + rolling-window bleed detection","body":"(1) CSLB now recorded in la_ingest_state (cslb_master) so the source-staleness check\ncovers it (was the untracked blind spot). (2) Rolling history (canary-history.jsonl)\n+ BLEED status: flags a slow net decline vs earliest-observed count that the ratchet\nfloor misses. Verified: no false-bleed on stable data; verdict still correctly FAILs\non gis_zoning's real 502."},{"hash":"b3681eb","date":"2026-08-11 14:36:57 -0700","author":"steve","subject":"yoloforever c2 FIX (Cody): median_value + explicit scope label + CD-join diagnostic","body":"Cody: total_value is right-skewed vanity without a central figure. Added median\n(reveals CD11 $8.05B total vs $9k median = a few mega-projects). Scope label now\nstates building-only/current-dataset (excludes 1.17M historical+trade). Join\ndiagnostic prints parsed/matched (15/15) so a blank member column can't ship silently."},{"hash":"f57ce2c","date":"2026-08-11 14:33:40 -0700","author":"steve","subject":"yoloforever c2: per-CD + per-ZIP permit-activity rollups (scripts/rollups.js)","body":"Aggregates permits x lead_score joined to gis council-district NAMES; permits,\nissued, new-builds, last-90d, total/avg value, avg lead score per CD + top-60 ZIPs.\nCSV exports. New local analytics surface (not the live viewer). $0 local."},{"hash":"3687729","date":"2026-08-11 14:10:29 -0700","author":"steve","subject":"yoloforever c1 FIX (Cody gate): canary reads la_ingest_state for per-source staleness+errors","body":"Count-only was false comfort (idempotent upserts keep counts flat while a feed\ngoes stale/errors). Now checks all 21 sources' last_run age + last_status — verdict\nFAIL correctly surfaces gis_zoning stuck in HTTP 502. Covers the 5 count-only tables."},{"hash":"9a4905f","date":"2026-08-11 14:06:51 -0700","author":"steve","subject":"yoloforever c1: LA-data freshness+rowcount canary (baseline-aware, read-only)","body":"Flags row-count regressions vs a ratcheting baseline + stale feeds (max-age per\ntable). Report-only; CNCP/George alerting is a gated follow-up. Also: parcel tails\nnow 100% complete (12,099,614 = full 12.1M, all 5 roll years, OID-cursor fix)."},{"hash":"02a2dc3","date":"2026-08-11 14:02:32 -0700","author":"steve","subject":"Add monetization-readiness checklist (viewer/MONETIZATION-READINESS.md)","body":""},{"hash":"3ff3d2f","date":"2026-08-11 13:59:50 -0700","author":"steve","subject":"Add one-step rate-limit apply script (needs WAF-scoped token)","body":""},{"hash":"8d2dafc","date":"2026-08-11 13:56:09 -0700","author":"steve","subject":"Compliance hygiene: add Terms of Use + Privacy Policy pages + footer links","body":""},{"hash":"daffbc8","date":"2026-08-11 13:35:58 -0700","author":"steve@designerwallcoverings.com","subject":"chore: bump v0.1.0 → v0.1.1 (session close — compliance footer)","body":""},{"hash":"cf20498","date":"2026-08-11 13:04:04 -0700","author":"steve","subject":"Compliance: pull board.html (monetization deck) off public surface + add robots.txt","body":"vp-compliance flagged the public no-auth board.html resale/lead-gen deck as HIGH\n(self-documented commercial + owner-targeting intent on a free-viewer surface).\nMoved to viewer/private/ (preserved, not served). robots.txt disallows /api/ + deck."},{"hash":"69f49ff","date":"2026-08-11 13:01:03 -0700","author":"steve","subject":"Viewer: swap CARTO basemap -> OSM tiles (ToS/copyright) + OSM attribution","body":"CARTO CDN basemap is not licensed for public non-customer use; OSM standard\ntiles are attribution-only. Disclaimer/sources footer already present."},{"hash":"1efa08a","date":"2026-08-11 12:57:53 -0700","author":"steve@designerwallcoverings.com","subject":"viewer: mirror compliance footer onto table.html + board.html","body":"All three viewer entry points now carry source attribution, as-is warranty\ndisclaimer, and no-endorsement notice. board.html uses a self-contained\ninline-styled footer (different flex layout, no --muted var)."},{"hash":"6f64d55","date":"2026-08-11 12:49:13 -0700","author":"steve@designerwallcoverings.com","subject":"viewer: add source-attribution + as-is/no-endorsement compliance footer","body":"Data reuse verified: LA County eGIS Terms grant copy/publish/distribute +\ncommercial use; LA City permits are untagged-public open data. Footer keeps\nany external viewer compliant (attribution, as-is warranty disclaimer, no\nCounty/City endorsement)."},{"hash":"fa5796c","date":"2026-08-11 12:13:14 -0700","author":"steve","subject":"Deploy: buildingpermits.agentabrams.com public via Cloudflare Tunnel (pm2 + DEPLOY.md)","body":""},{"hash":"ff21d5c","date":"2026-08-11 12:05:40 -0700","author":"steve","subject":"Viewer: List/Grid/Map views + color-ID + shareable ?view=&color= deep links","body":"Map view (Leaflet) plots the current filtered set color-coded by lead score/\nstatus/value/work-type; grid = color-bordered cards; list = the sortable table.\nOne colorFor() model drives all three. ?view=map&color=project_value deep-links."},{"hash":"4862c20","date":"2026-08-11 11:53:23 -0700","author":"steve","subject":"GIS land use via hosted LADCP FeatureServer (56,901 features, reliable)","body":"Replaces the flaky NavigateLA gateway for land use. Zoning stays partial (75%,\ngateway-capped); complete LA City zoning is fragmented across ~15 per-community\nZone_Builder_Database services (documented follow-up)."},{"hash":"0ccaea8","date":"2026-08-11 09:27:04 -0700","author":"steve","subject":"GIS: parcel polygons via hosted FeatureServer (421,684 w/ geometry); no-order offset for gateway layers","body":"NavigateLA MapServer gateway 502s/500s on bulk geometry past ~offset 44k;\nparcels repointed to hosted Assessor_Parcels_Land_2025 FeatureServer (reliable).\nzoning partial (43,957), landuse blocked — need hosted-FeatureServer equivalents."},{"hash":"cc12805","date":"2026-08-11 09:12:06 -0700","author":"steve","subject":"GIS layers (NavigateLA ArcGIS) + CSLB contractor loader","body":"ArcGIS adapter: OBJECTID-cursor pagination + geometry support. GIS schema\n(la_parcel_geom, la_gis_features). Loaded zoning/land-use/council districts/\nneighborhood councils/HPOZ/community plan areas + fire/flood/fault/liquefaction\nhazard layers. CSLB free-portal ASP.NET session downloader -> 243,555 CA\ncontractors in cslb_raw. Assessor switched to OID-cursor (fixes deep-paging 400s)."},{"hash":"bc01699","date":"2026-08-11 09:11:35 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-08-11T09:10:52 (1 data files) — db/gis_schema.sql","body":""},{"hash":"d9ab84f","date":"2026-08-11 08:53:20 -0700","author":"Steve Abrams","subject":"Promote merged card+lead-score viewer to index.html; preserve table as table.html","body":"- index.html: card viewer (facet rail, search/sort/density, detail modal) + permit_lead_score Rank badge on every card, 'Rank (high->low)' sort, 'Lead score N/100' in modal; header nav Cards/Table/Board\n- table.html: previous sortable/movable-column table viewer, preserved + reachable from nav\n- cards.html: removed (promoted into index.html)\nSteve-directed (TK-10450). Verified in Chrome: / = cards w/ ranks, /table.html = table, /board.html = DTD board"},{"hash":"2efe81b","date":"2026-08-11 08:38:19 -0700","author":"steve","subject":"Viewer: VIEWER_NO_AUTH env bypass for local screenshot/dev (auth on by default)","body":""},{"hash":"f47742c","date":"2026-08-11 08:32:19 -0700","author":"steve","subject":"Viewer: sortable+movable-column table + permit_lead_score ranking (default sort)","body":"Every column server-sortable (whitelisted sort=<col>&dir); drag-to-reorder columns\npersisted to localStorage; lead_score (0-100) ranking as default sort. Row-density\nslider, faceted filter rail with live counts, detail modal w/ parcel enrichment +\nGoogle Maps + raw JSON. On port 9814."},{"hash":"bbdafa5","date":"2026-08-11 08:30:56 -0700","author":"Steve Abrams","subject":"LA Building Permits viewer frontend: faceted rail, search/sort/density, detail modal","body":"- public/index.html: professional teal/neutral data-tool UI over /api/permits + /api/facets\n- Left collapsible facet rail (Era/Work Type/Building Type/Status/CD/ZIP) with live counts, click-to-filter, active teal badge; active-facet options stay switchable (refetch minus own filter)\n- Top bar: debounced address search, live count, sort (newest/value/oldest/address), density slider -> --cols; sort+density persist in localStorage\n- Cards: address, work/building badges, formatted value, issue date + status pill, CD/era, assessed value/year built/sqft when present\n- Detail modal (draggable+resizable, persisted): full permit + parcel enrichment, Google Maps link, clickable filter values, raw-JSON toggle\n- Load-more pagination (no infinite scroll); stale-response seq guards on facets+permits\n- server.js: existing viewer server (was untracked)"},{"hash":"ed10986","date":"2026-08-11 08:28:00 -0700","author":"Steve Abrams","subject":"Add DTD-driven Opportunity Board (board.html)","body":"Hootsuite-style horizontal multi-column board of 7 data-product projects\nbuildable on the 1.58M LA permit + assessor dataset, with 21 suggested\nuser-segment cards (persona, why, exact permit/parcel fields, fit tag).\nColumn SET committed by the DTD 5-LLM debate panel (Claude+Codex+Qwen+Grok+Kimi),\n~$0.04 run. Matches index.html palette/tokens; served at /board.html behind\nexisting auth.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"00f0a40","date":"2026-08-11 08:08:15 -0700","author":"steve","subject":"Add palisades_rebuilds + current_permits_all views + pool/master lead exports","body":""},{"hash":"b9cec1a","date":"2026-08-11 08:03:21 -0700","author":"steve","subject":"Add current_permit_leads view + CSV export (2,034 live residential leads, $370M)","body":""},{"hash":"0c2fe12","date":"2026-08-11 07:03:03 -0700","author":"steve","subject":"Add --year=YYYY for resumable per-roll-year assessor loads","body":"Splits the fragile 12.1M full parcel crawl into idempotent ~2.4M/year chunks so\na mid-run failure only re-costs one year. Accurate run-mode log label too.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"}]}