[object Object]

← back to Rentv 2026

Gate E (TK-10076): /services degraded-state banner on realestate outage (was silent 'No providers found') + zero-row services canary (4x/day → CNCP)

206b99541ba6e66e96fa8b228547b0ea81beca22 · 2026-08-06 13:39:08 -0700 · Steve

Files touched

Diff

commit 206b99541ba6e66e96fa8b228547b0ea81beca22
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 13:39:08 2026 -0700

    Gate E (TK-10076): /services degraded-state banner on realestate outage (was silent 'No providers found') + zero-row services canary (4x/day → CNCP)
---
 public/services.html       | 10 +++++++++-
 scripts/services-canary.sh | 41 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/public/services.html b/public/services.html
index 0c086b0d..486ed673 100644
--- a/public/services.html
+++ b/public/services.html
@@ -335,8 +335,16 @@ async function load(reset){
   if(reset){$('rows').innerHTML='';$('meta').innerHTML='<span class="spin"></span>Loading…';$('more').style.display='none';for(const k in BYID)delete BYID[k];}
   const u=new URLSearchParams(); u.set('cat',cat); u.set('market',market); u.set('sort',sort); u.set('limit',PAGE); u.set('offset',offset);
   if(q)u.set('q',q); if(city)u.set('city',city);
-  let d; try{ d=await (await fetch('/api/services?'+u.toString())).json(); }catch(e){ if(my===seq)$('meta').textContent='Services directory unavailable — try again.'; return; }
+  let res,d; try{ res=await fetch('/api/services?'+u.toString()); d=await res.json(); }catch(e){ d=null; }
   if(my!==seq)return;
+  // Degraded state: a DB/registry outage returns 502 {error} (or a network throw) — a JSON error body
+  // does NOT throw, so WITHOUT this check the code below falls through to "No providers found" and
+  // wrongly tells users the directory is EMPTY when it is actually DOWN. Show a distinct retry state.
+  if(!d || !res.ok || d.error){
+    $('meta').textContent='';
+    $('rows').innerHTML='<div class="empty"><h3>Directory temporarily unavailable</h3><div>The services registry is offline right now — this is a temporary issue, not an empty directory. <a href="#" onclick="location.reload();return false;" style="color:var(--gold,#b8965a);text-decoration:underline">Retry</a></div></div>';
+    $('more').style.display='none'; return;
+  }
   total=d.total||0;                          // registry rows only
   const featuredTotal=d.featuredTotal||0;    // curated firms (pinned, page 1 only)
   const featured=reset?(d.featured||[]):[];
diff --git a/scripts/services-canary.sh b/scripts/services-canary.sh
new file mode 100755
index 00000000..06d241f1
--- /dev/null
+++ b/scripts/services-canary.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+# services-canary.sh — catches a SILENT emptying of the public /services CRE directory.
+# TK-10076 Gate E: /services reads the `realestate` DB (rentv_licensed_targets). If that DB
+# drops/empties, the endpoint can return 0 rows with HTTP 200 — the frontend now shows a
+# degraded banner, but nothing ALERTS. This canary hits the live endpoint across the core
+# categories and fails loud (CNCP + log) if the grand total collapses to zero.
+set -uo pipefail
+BASE="${SERVICES_BASE:-https://rentv.agentabrams.com}"
+# /api/services + /api/services/facets are PUBLIC (verified 200 without auth) — no credential needed.
+LOG="${SERVICES_CANARY_LOG:-$HOME/.claude/services-canary.log}"
+FLOOR="${SERVICES_ROW_FLOOR:-500}"             # total licensed rows should be ~102k; <500 = broken
+mkdir -p "$(dirname "$LOG")"
+ts(){ date '+%Y-%m-%d %H:%M:%S %Z'; }
+
+# /api/services/facets gives the whole-directory row totals in one call (no per-cat paging).
+resp=$(curl -s -m 20 "$BASE/api/services/facets?market=Greater%20LA" 2>/dev/null)
+total=$(printf '%s' "$resp" | python3 -c "import sys,json
+try:
+    d=json.load(sys.stdin); cats=d.get('cats',[])
+    print(sum(int(c.get('count',0) or 0) for c in cats))
+except Exception: print(-1)" 2>/dev/null || echo -1)
+
+if [ "$total" -lt 0 ]; then
+  echo "[$(ts)] FAIL: /api/services/facets unreachable or unparseable (resp head: ${resp:0:120})" >> "$LOG"
+  verdict="CRITICAL: /services facets endpoint down"
+elif [ "$total" -lt "$FLOOR" ]; then
+  echo "[$(ts)] FAIL: services directory total=$total < floor $FLOOR — realestate may have emptied" >> "$LOG"
+  verdict="CRITICAL: /services near-empty ($total rows)"
+else
+  echo "[$(ts)] OK: services directory total=$total" >> "$LOG"
+  verdict=""
+fi
+
+# Fail loud to CNCP parking-lot when broken (best-effort, local, no auth).
+if [ -n "$verdict" ]; then
+  curl -s -m 5 -X POST http://127.0.0.1:3333/api/parking-lot \
+    -H 'Content-Type: application/json' \
+    -d "{\"project\":\"rentv\",\"item\":\"$verdict — check realestate DB + /services\",\"source\":\"services-canary\"}" >/dev/null 2>&1 || true
+  exit 1
+fi
+exit 0

← a3ae8ad5 auto-data-snapshot: 2026-08-06T13:27:53 (7 data files) — dat  ·  back to Rentv 2026  ·  harden(deals): parseAmount \s* binds magnitude across a typo 971e9e4b →