← 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
M apps/web/src/app/homes/[id]/NearbySubs.tsxM apps/web/src/app/homes/[id]/page.tsxM apps/web/src/lib/contractors.ts
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 →