← back to Rentv V1
Date every contractor entry: source_as_of + verify-at-cslb.ca.gov (TK-10488)
ff4651c712e5b3fcd29e2e61420b002cf09ca0e9 · 2026-08-12 10:35:14 -0700 · Steve Abrams
DATE EVERYTHING per spec — the upstream usre /api/contractors/match carries no
dataset-level freshness date, so the drop-in now surfaces one itself:
- router.js: adds source_as_of (per-record upstream value -> CONTRACTORS_SOURCE_AS_OF
env -> known load date 2026-08-07) + verify_url:"cslb.ca.gov" to every response
(incl. the fail-soft path); also passes license_status through (Active/CLEAR).
- widget.html: stamps "CSLB data as of {source_as_of} — verify at cslb.ca.gov"
under EACH contractor entry (text-only, no link-out); shows license_status.
- README.md: documents the DATE-EVERYTHING behavior, the CONTRACTORS_SOURCE_AS_OF
env, the status/CLEAR handling, and the corrected response shape.
New files only, no edits to any existing/live RENTV file. Publish/deploy gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M contrib/contractors/README.mdM contrib/contractors/router.jsM contrib/contractors/widget.html
Diff
commit ff4651c712e5b3fcd29e2e61420b002cf09ca0e9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 12 10:35:14 2026 -0700
Date every contractor entry: source_as_of + verify-at-cslb.ca.gov (TK-10488)
DATE EVERYTHING per spec — the upstream usre /api/contractors/match carries no
dataset-level freshness date, so the drop-in now surfaces one itself:
- router.js: adds source_as_of (per-record upstream value -> CONTRACTORS_SOURCE_AS_OF
env -> known load date 2026-08-07) + verify_url:"cslb.ca.gov" to every response
(incl. the fail-soft path); also passes license_status through (Active/CLEAR).
- widget.html: stamps "CSLB data as of {source_as_of} — verify at cslb.ca.gov"
under EACH contractor entry (text-only, no link-out); shows license_status.
- README.md: documents the DATE-EVERYTHING behavior, the CONTRACTORS_SOURCE_AS_OF
env, the status/CLEAR handling, and the corrected response shape.
New files only, no edits to any existing/live RENTV file. Publish/deploy gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
contrib/contractors/README.md | 33 +++++++++++++++++++++++++++++++--
contrib/contractors/router.js | 32 +++++++++++++++++++++++++++++---
contrib/contractors/widget.html | 12 +++++++++++-
3 files changed, 71 insertions(+), 6 deletions(-)
diff --git a/contrib/contractors/README.md b/contrib/contractors/README.md
index c756ba2..26d1448 100644
--- a/contrib/contractors/README.md
+++ b/contrib/contractors/README.md
@@ -26,10 +26,35 @@ contrib/contractors/
It flattens + dedupes (a GC appears under several class codes) into a slim,
**text-only** shape and caches responses in-memory (5 min TTL).
- **`widget.html`** renders that list on a story — **name + trade + phone**,
- plus city + license number. **No links out, no re-hosted assets** (honors the
+ plus city, license number, and license status. **Every entry is dated**:
+ it shows `CSLB data as of {source_as_of} — verify at cslb.ca.gov`
+ (text-only, no link). **No links out, no re-hosted assets** (honors the
RENTV no-rentv.com-link-out rule). Fails soft: if the sidecar is down or the
market has no matches, the widget **hides itself** so the story stays clean.
+### DATE EVERYTHING — `source_as_of`
+
+The upstream `/api/contractors/match` response does **not** carry a
+dataset-level freshness date, so the router adds one. Resolution order:
+
+1. a per-record `source_as_of` if the upstream ever starts emitting it, else
+2. the `CONTRACTORS_SOURCE_AS_OF` env var, else
+3. the known load date of the shared registry: **`2026-08-07`**
+ (`ca_contractors.source_as_of`, verified 230,964 rows all dated 2026-08-07).
+
+Every response includes `source_as_of` + `verify_url: "cslb.ca.gov"`, and the
+widget stamps `CSLB data as of {source_as_of} — verify at cslb.ca.gov` under
+**each** contractor. **When the CSLB registry is refreshed, bump
+`CONTRACTORS_SOURCE_AS_OF`** so the date readers see stays honest.
+
+### status / CLEAR
+
+The upstream `/match` already filters to `license_status = 'Active'` server-side,
+so only in-good-standing licenses come back. The router passes each record's
+`license_status` through to the widget (rendered in the meta line), so a
+reader sees the standing (e.g. `Active` / `CLEAR`) per contractor. No client
+can widen the filter — the "clean only" guarantee lives in the shared API.
+
---
## Mount the router (RENTV `server.js`)
@@ -68,6 +93,7 @@ That mounts:
| `CONTRACTORS_API_TIMEOUT_MS` | `6000` | upstream fetch timeout |
| `CONTRACTORS_DEFAULT_LIMIT` | `6` | contractors returned when `limit` omitted |
| `CONTRACTORS_CACHE_TTL_MS` | `300000` | in-memory cache TTL |
+| `CONTRACTORS_SOURCE_AS_OF` | `2026-08-07` | CSLB dataset freshness date shown per entry — bump on registry refresh |
> The usre API is behind Basic Auth (CRCP pattern). On the same host / loopback
> it may not require creds; if it does, set `CONTRACTORS_API_AUTH="admin:…"`.
@@ -129,7 +155,10 @@ on the story — pass whatever the article's parcel/deal object carries.
# router is wired?
curl -s localhost:9704/contrib/contractors/health | jq .
-# lookup (returns {market,count,contractors:[{name,trade,phone,city,license_no}],attribution}):
+# lookup — returns:
+# { market, count,
+# contractors:[{name,trade,phone,city,county,license_no,license_status}],
+# source_as_of:"2026-08-07", verify_url:"cslb.ca.gov", attribution }
curl -s 'localhost:9704/contrib/contractors/api/contractors-for-market?county=Los%20Angeles&city=Los%20Angeles&limit=6' | jq .
```
diff --git a/contrib/contractors/router.js b/contrib/contractors/router.js
index 27fd6cf..a42fdab 100644
--- a/contrib/contractors/router.js
+++ b/contrib/contractors/router.js
@@ -38,6 +38,14 @@ const API_TIMEOUT_MS = Number(process.env.CONTRACTORS_API_TIMEOUT_MS || 6000);
const DEFAULT_LIMIT = Number(process.env.CONTRACTORS_DEFAULT_LIMIT || 6);
const MAX_LIMIT = 24;
+// CSLB dataset freshness date the widget must display ("CSLB data as of {date}").
+// The upstream /match response does not currently carry a dataset-level date, so
+// we surface one here. Priority: any per-record source_as_of the upstream may add
+// later -> CONTRACTORS_SOURCE_AS_OF env -> the known load date of the shared
+// registry (2026-08-07, ca_contractors.source_as_of). RENTV/ops bump the env when
+// the CSLB registry is refreshed so the date the reader sees stays honest.
+const SOURCE_AS_OF = process.env.CONTRACTORS_SOURCE_AS_OF || '2026-08-07';
+
// Short in-memory cache so a hot news story doesn't hammer the upstream.
const CACHE_TTL_MS = Number(process.env.CONTRACTORS_CACHE_TTL_MS || 5 * 60 * 1000);
const _cache = new Map(); // key -> { at, data }
@@ -88,6 +96,7 @@ function flattenMatches(payload, limit) {
? payload.matches : {};
const seen = new Set(); // dedupe by license_no (a GC can appear under multiple class codes)
const out = [];
+ let upstreamAsOf = null; // newest per-record source_as_of the upstream carried, if any
for (const code of Object.keys(matches)) {
const list = Array.isArray(matches[code]) ? matches[code] : [];
for (const c of list) {
@@ -96,6 +105,11 @@ function flattenMatches(payload, limit) {
if (seen.has(key)) continue;
seen.add(key);
+ // Track the freshest per-record CSLB date if the upstream ever exposes one.
+ if (c.source_as_of && (!upstreamAsOf || c.source_as_of > upstreamAsOf)) {
+ upstreamAsOf = String(c.source_as_of);
+ }
+
// Prefer the human-readable trade title for the matched class code; fall
// back to the contractor's primary_class or the raw code.
let trade = null;
@@ -114,9 +128,13 @@ function flattenMatches(payload, limit) {
license_no: c.license_no ? String(c.license_no) : null,
license_status: c.license_status ? String(c.license_status) : null,
});
- if (out.length >= limit) return out;
+ if (out.length >= limit) {
+ out._source_as_of = upstreamAsOf; // non-enumerable-ish sidecar (array prop)
+ return out;
+ }
}
}
+ out._source_as_of = upstreamAsOf;
return out;
}
@@ -158,14 +176,20 @@ function createContractorsRouter(opts = {}) {
try {
const payload = await fetchUpstream(url);
const contractors = flattenMatches(payload, limit);
+ const sourceAsOf = contractors._source_as_of || SOURCE_AS_OF;
+ const plain = contractors.slice(); // drop the sidecar prop from the wire array
const result = {
market: {
city: (payload.criteria && payload.criteria.city) || city || null,
county: (payload.criteria && payload.criteria.county) || county || null,
mode,
},
- count: contractors.length,
- contractors,
+ count: plain.length,
+ contractors: plain,
+ // DATE EVERYTHING — the widget renders "CSLB data as of {source_as_of} —
+ // verify at cslb.ca.gov" on every entry. verify_url is text-only, no link.
+ source_as_of: sourceAsOf,
+ verify_url: 'cslb.ca.gov',
attribution: 'CA CSLB licensed contractors via shared usre registry',
source: 'usre-contractors',
};
@@ -181,6 +205,8 @@ function createContractorsRouter(opts = {}) {
market: { city: city || null, county: county || null, mode },
count: 0,
contractors: [],
+ source_as_of: SOURCE_AS_OF,
+ verify_url: 'cslb.ca.gov',
});
}
});
diff --git a/contrib/contractors/widget.html b/contrib/contractors/widget.html
index d96d85e..8a00198 100644
--- a/contrib/contractors/widget.html
+++ b/contrib/contractors/widget.html
@@ -32,6 +32,7 @@
.rentv-contractors__trade { color: #555; }
.rentv-contractors__phone { color: #333; }
.rentv-contractors__meta { color: #888; font-size: 12px; }
+ .rentv-contractors__asof { color: #999; font-size: 11px; margin-top: .2em; font-style: italic; }
.rentv-contractors__attr { color: #999; font-size: 11px; margin-top: .6em; }
.rentv-contractors[hidden] { display: none; }
</style>
@@ -57,6 +58,12 @@
if (!contractors.length) { el.hidden = true; return; } // nothing to show -> hide, story stays clean
var label = marketLabel(data.market);
+ // DATE EVERYTHING — every entry carries the CSLB freshness date + a text-only
+ // "verify at cslb.ca.gov" (no link-out, honoring the RENTV no-link-out rule).
+ var asOf = (data && data.source_as_of) ? String(data.source_as_of) : 'unknown';
+ var verify = (data && data.verify_url) ? String(data.verify_url) : 'cslb.ca.gov';
+ var asOfLine = 'CSLB data as of ' + esc(asOf) + ' — verify at ' + esc(verify);
+
var rows = contractors.map(function (c) {
var bits = [];
bits.push('<span class="rentv-contractors__name">' + esc(c.name) + '</span>');
@@ -67,10 +74,13 @@
var meta = [];
if (c.city) meta.push(esc(c.city));
if (c.license_no) meta.push('Lic. ' + esc(c.license_no));
+ if (c.license_status) meta.push(esc(c.license_status));
var metaLine = meta.length
? '<div class="rentv-contractors__meta">' + meta.join(' · ') + '</div>'
: '';
- return '<li class="rentv-contractors__item">' + bits.join('') + phone + metaLine + '</li>';
+ // per-entry dated attribution — required by TK-10488
+ var asOfPerEntry = '<div class="rentv-contractors__asof">' + asOfLine + '</div>';
+ return '<li class="rentv-contractors__item">' + bits.join('') + phone + metaLine + asOfPerEntry + '</li>';
}).join('');
el.innerHTML =
← 64f7dfe Add drop-in contractor lookup package (contrib/contractors,
·
back to Rentv V1
·
(newest)