[object Object]

← back to Homesonspec

HomesOnSpec: date-stamp nearby-subs — per-row CSLB issue/expire dates + data-as-of provenance + status=CLEAR

552d6aa2db3f78ddc50b91b209b4e2fd9fc1fb2b · 2026-08-12 10:35:45 -0700 · Steve Abrams

Completes the 'Licensed trades in this area' surface (86f64f4) to satisfy the
DATE EVERYTHING rule and the contractor-API contract (TK-10488):

- contractors.ts: extend ContractorMatch with issue_date/expire_date/
  cslb_last_update/source_as_of/updated_at; surface top-level source_as_of on
  MatchResult (falls back to the freshest per-row CSLB date); pass status=CLEAR
  (good-standing per contract, NOT 'Active') on the match call; fix stale doc.
- NearbySubs.tsx: fmtDate() local-TZ formatter; every sub row now shows
  'Issued <date> · Expires <date>' with the raw ISO in title=; section footer
  stamps 'CSLB data as of {source_as_of} — verify at cslb.ca.gov'.
- page.tsx: thread source_as_of through to NearbySubs (incl. non-CA short-circuit).

Verified against a mock API returning the full contract shape: real CA home
renders 3 rows across 2 trade blocks with correct issue/expire dates + CLEAR
badges + license deep-links + the data-as-of stamp; null-phone row omits its tel
link (2 tel links total); non-CA home short-circuits to the graceful empty-state;
auth-walled live API (9913) degrades to the empty-state; typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 552d6aa2db3f78ddc50b91b209b4e2fd9fc1fb2b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 12 10:35:45 2026 -0700

    HomesOnSpec: date-stamp nearby-subs — per-row CSLB issue/expire dates + data-as-of provenance + status=CLEAR
    
    Completes the 'Licensed trades in this area' surface (86f64f4) to satisfy the
    DATE EVERYTHING rule and the contractor-API contract (TK-10488):
    
    - contractors.ts: extend ContractorMatch with issue_date/expire_date/
      cslb_last_update/source_as_of/updated_at; surface top-level source_as_of on
      MatchResult (falls back to the freshest per-row CSLB date); pass status=CLEAR
      (good-standing per contract, NOT 'Active') on the match call; fix stale doc.
    - NearbySubs.tsx: fmtDate() local-TZ formatter; every sub row now shows
      'Issued <date> · Expires <date>' with the raw ISO in title=; section footer
      stamps 'CSLB data as of {source_as_of} — verify at cslb.ca.gov'.
    - page.tsx: thread source_as_of through to NearbySubs (incl. non-CA short-circuit).
    
    Verified against a mock API returning the full contract shape: real CA home
    renders 3 rows across 2 trade blocks with correct issue/expire dates + CLEAR
    badges + license deep-links + the data-as-of stamp; null-phone row omits its tel
    link (2 tel links total); non-CA home short-circuits to the graceful empty-state;
    auth-walled live API (9913) degrades to the empty-state; typecheck clean.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 apps/web/src/app/homes/[id]/NearbySubs.tsx | 48 +++++++++++++++++--
 apps/web/src/app/homes/[id]/page.tsx       |  3 +-
 apps/web/src/lib/contractors.ts            | 76 +++++++++++++++++++++++++++---
 3 files changed, 116 insertions(+), 11 deletions(-)

diff --git a/apps/web/src/app/homes/[id]/NearbySubs.tsx b/apps/web/src/app/homes/[id]/NearbySubs.tsx
index f934aa47..5ece402c 100644
--- a/apps/web/src/app/homes/[id]/NearbySubs.tsx
+++ b/apps/web/src/app/homes/[id]/NearbySubs.tsx
@@ -14,12 +14,28 @@ interface Props {
   matches: Record<string, ContractorMatch[]>;
   /** Reason the API returned nothing (unreachable / no matches) — for the empty-state. */
   error: string | null;
+  /** CSLB data-as-of date — stamped on the section (DATE EVERYTHING). */
+  sourceAsOf: string | null;
   /** Home location, echoed in the section subhead + the browse deep-link. */
   city: string | null;
   county: string | null;
   state: string;
 }
 
+// Date formatting (DATE EVERYTHING). Renders the license issue/expire dates and
+// the CSLB data-as-of stamp in the viewer's local timezone; a bad/empty value
+// degrades to a dash rather than "Invalid Date".
+function fmtDate(v: string | null | undefined): string {
+  if (!v) return "—";
+  const t = Date.parse(v);
+  if (Number.isNaN(t)) return String(v);
+  return new Date(t).toLocaleDateString(undefined, {
+    year: "numeric",
+    month: "short",
+    day: "numeric",
+  });
+}
+
 type SortMode = "trade" | "name" | "city";
 const SORTS: { value: SortMode; label: string }[] = [
   { value: "trade", label: "Build order (trade)" },
@@ -30,7 +46,7 @@ const SORTS: { value: SortMode; label: string }[] = [
 const SORT_KEY = "homesonspec.subs.sort";
 const COLS_KEY = "homesonspec.subs.cols";
 
-export default function NearbySubs({ matches, error, city, county, state }: Props) {
+export default function NearbySubs({ matches, error, sourceAsOf, city, county, state }: Props) {
   const [sort, setSort] = useState<SortMode>("trade");
   const [cols, setCols] = useState<number>(2);
 
@@ -202,6 +218,18 @@ export default function NearbySubs({ matches, error, city, county, state }: Prop
                           </a>
                         ) : null}
                       </div>
+                      {/* DATE EVERYTHING: license issue + expire dates on every row. */}
+                      <div
+                        className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-neutral-400"
+                        data-testid="subs-dates"
+                      >
+                        <span title={s.issue_date ? `License issued ${s.issue_date}` : undefined}>
+                          Issued {fmtDate(s.issue_date)}
+                        </span>
+                        <span title={s.expire_date ? `License expires ${s.expire_date}` : undefined}>
+                          Expires {fmtDate(s.expire_date)}
+                        </span>
+                      </div>
                     </li>
                   );
                 })}
@@ -211,9 +239,21 @@ export default function NearbySubs({ matches, error, city, county, state }: Prop
         </div>
       )}
 
-      <p className="mt-2 text-xs text-neutral-400">
-        Licensed-contractor facts come from the public California Contractors State License Board (CSLB)
-        record. HomesOnSpec does not endorse any contractor — always verify a license on CSLB before hiring.
+      {/* DATE EVERYTHING: CSLB data-as-of provenance stamp + verify pointer. */}
+      <p className="mt-2 text-xs text-neutral-400" data-testid="subs-source-stamp">
+        {sourceAsOf
+          ? `CSLB data as of ${fmtDate(sourceAsOf)} — verify at `
+          : "CSLB data — verify at "}
+        <a
+          href="https://www.cslb.ca.gov"
+          target="_blank"
+          rel="nofollow noopener noreferrer"
+          className="underline hover:text-neutral-600"
+        >
+          cslb.ca.gov
+        </a>
+        . Licensed-contractor facts come from the public California Contractors State License Board (CSLB)
+        record. HomesOnSpec does not endorse any contractor — always verify a license before hiring.
         {error && total === 0 ? ` (${error})` : ""}
       </p>
     </section>
diff --git a/apps/web/src/app/homes/[id]/page.tsx b/apps/web/src/app/homes/[id]/page.tsx
index e7fb6755..b21d1a28 100644
--- a/apps/web/src/app/homes/[id]/page.tsx
+++ b/apps/web/src/app/homes/[id]/page.tsx
@@ -63,7 +63,7 @@ export default async function HomeDetailPage({ params }: { params: Promise<{ id:
     // never throws — matchSubsForHome returns a graceful empty result on failure.
     home.state?.toUpperCase() === "CA"
       ? matchSubsForHome({ city: home.city, county: home.community.county, zip: home.zip })
-      : Promise.resolve({ matches: {}, criteria: null, ok: false, error: null }),
+      : Promise.resolve({ matches: {}, criteria: null, source_as_of: null, ok: false, error: null }),
   ]);
   const incentives = [...home.incentives, ...communityIncentives];
   // Media-rights safe default: builder photos are OFF unless BUILDER_IMAGES_ENABLED=1
@@ -287,6 +287,7 @@ export default async function HomeDetailPage({ params }: { params: Promise<{ id:
           <NearbySubs
             matches={subs.matches}
             error={subs.error}
+            sourceAsOf={subs.source_as_of}
             city={home.city}
             county={home.community.county}
             state={home.state}
diff --git a/apps/web/src/lib/contractors.ts b/apps/web/src/lib/contractors.ts
index a4cf2ed8..81a33037 100644
--- a/apps/web/src/lib/contractors.ts
+++ b/apps/web/src/lib/contractors.ts
@@ -3,11 +3,16 @@
 // credentials never reach the browser.
 //
 // Contract (per TK-10488):
-//   GET {base}/api/contractors/match?mode=home&city=&county=&zip=&trades=C-8,C-10,...
+//   GET {base}/api/contractors/match?mode=home&city=&county=&zip=&status=CLEAR&trades=C-8,C-10,...
 //     -> { criteria, matches: { "C-10": [ {license_no, business_name, city,
-//          county, phone, license_status, classifications, primary_class} ], ... } }
+//          county, phone, license_status, classifications, primary_class,
+//          issue_date, expire_date, cslb_last_update, source_as_of, updated_at} ], ... } }
 //        active subs grouped by trade class, nearest-first.
-//   GET {base}/api/contractors?county=&class=&status=Active&limit=  (plain browse)
+//   GET {base}/api/contractors?county=&class=&status=CLEAR&limit=  (plain browse)
+//
+// Good-standing status is 'CLEAR' (NOT 'Active') — we always pass status=CLEAR.
+// DATE EVERYTHING (Steve's hard rule): the issue/expire dates + the CSLB
+// data-as-of stamp flow all the way through to each rendered row.
 //
 // Base URL from process.env.CONTRACTORS_API_BASE (default http://localhost:9913).
 // GRACEFUL by design: if the API is unreachable / errors / returns junk, this
@@ -27,6 +32,16 @@ export interface ContractorMatch {
   license_status: string | null;
   classifications: string | null;
   primary_class: string | null;
+  /** CSLB license issue date (ISO or as-supplied) — shown on every row. */
+  issue_date: string | null;
+  /** CSLB license expiration date — shown on every row. */
+  expire_date: string | null;
+  /** When CSLB last updated this record (per the API). */
+  cslb_last_update: string | null;
+  /** Per-row CSLB data-as-of date (used as a fallback for the section stamp). */
+  source_as_of: string | null;
+  /** When our copy of this row was last synced. */
+  updated_at: string | null;
 }
 
 export interface MatchResult {
@@ -34,13 +49,26 @@ export interface MatchResult {
   matches: Record<string, ContractorMatch[]>;
   /** The criteria the API echoed back (city/county/zip/trades), when present. */
   criteria: Record<string, unknown> | null;
+  /**
+   * CSLB data-as-of date (the API's `source_as_of`), surfaced so the UI can
+   * stamp "CSLB data as of {source_as_of} — verify at cslb.ca.gov" on the
+   * section (DATE EVERYTHING). Falls back to the freshest per-row date we saw
+   * when the API omits a top-level value.
+   */
+  source_as_of: string | null;
   /** True when the API answered with a well-formed body. */
   ok: boolean;
   /** Set when the API was unreachable or errored — drives the empty-state copy. */
   error: string | null;
 }
 
-const EMPTY: MatchResult = { matches: {}, criteria: null, ok: false, error: null };
+const EMPTY: MatchResult = {
+  matches: {},
+  criteria: null,
+  source_as_of: null,
+  ok: false,
+  error: null,
+};
 
 interface MatchArgs {
   city?: string | null;
@@ -59,7 +87,8 @@ interface MatchArgs {
  */
 export async function matchSubsForHome(args: MatchArgs): Promise<MatchResult> {
   const trades = args.trades ?? DEFAULT_TRADES_PARAM;
-  const qs = new URLSearchParams({ mode: "home", trades });
+  // Good-standing filter: 'CLEAR' (NOT 'Active') per the contractor API contract.
+  const qs = new URLSearchParams({ mode: "home", status: "CLEAR", trades });
   if (args.city) qs.set("city", args.city);
   if (args.county) qs.set("county", args.county);
   if (args.zip) qs.set("zip", args.zip);
@@ -108,6 +137,11 @@ function normalize(body: unknown): MatchResult {
           license_status: strOrNull(r.license_status),
           classifications: strOrNull(r.classifications),
           primary_class: strOrNull(r.primary_class) ?? cls,
+          issue_date: strOrNull(r.issue_date),
+          expire_date: strOrNull(r.expire_date),
+          cslb_last_update: strOrNull(r.cslb_last_update),
+          source_as_of: strOrNull(r.source_as_of),
+          updated_at: strOrNull(r.updated_at),
         }))
         // Drop rows with no business name — never surface a blank sub.
         .filter((r) => r.business_name.length > 0);
@@ -117,8 +151,38 @@ function normalize(body: unknown): MatchResult {
     obj.criteria && typeof obj.criteria === "object"
       ? (obj.criteria as Record<string, unknown>)
       : null;
+  // DATE EVERYTHING: prefer the API's top-level source_as_of; else fall back to
+  // the freshest per-row CSLB date we saw so the "data as of" stamp is honest.
+  const topSourceAsOf =
+    strOrNull(obj.source_as_of) ??
+    (criteria ? strOrNull(criteria.source_as_of) : null);
+  const source_as_of = topSourceAsOf ?? freshestRowDate(matches);
   const total = Object.values(matches).reduce((n, a) => n + a.length, 0);
-  return { matches, criteria, ok: total > 0, error: total > 0 ? null : "no matches" };
+  return {
+    matches,
+    criteria,
+    source_as_of,
+    ok: total > 0,
+    error: total > 0 ? null : "no matches",
+  };
+}
+
+/** Freshest per-row CSLB source date across all matches (fallback for the stamp). */
+function freshestRowDate(matches: Record<string, ContractorMatch[]>): string | null {
+  let best: string | null = null;
+  let bestT = -Infinity;
+  for (const list of Object.values(matches)) {
+    for (const r of list) {
+      const cand = r.source_as_of ?? r.cslb_last_update ?? r.updated_at;
+      if (!cand) continue;
+      const t = Date.parse(cand);
+      if (!Number.isNaN(t) && t > bestT) {
+        bestT = t;
+        best = cand;
+      }
+    }
+  }
+  return best;
 }
 
 function str(v: unknown): string {

← 86f64f40 HomesOnSpec: 'Find subs near this home' — nearby licensed CA  ·  back to Homesonspec  ·  homesonspec: send scoped Basic auth to usre contractor API ( a2a0172b →