[object Object]

← 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

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) + ' &mdash; 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(' &middot; ') + '</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)