← back to Rentv
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
M public/services.htmlA scripts/services-canary.sh
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
·
harden(deals): parseAmount \s* binds magnitude across a typo 971e9e4b →