Silent scraper breakage (200-with-empty-body) went unnoticed for 16 days
because nothing flagged it. Now, after each daily direct scrape, a health
check compares every enabled competitor's freshest captured rate against a
staleness threshold (default 5 days, config: scrape_health_stale_days).
State lives in a new direct_scrape_health table so we alert on transitions
only — one email when a competitor goes stale, one when it recovers, never a
daily repeat. Email uses the stack's shared SMTP integration via
central_settings (recipient: scrape_health_alert_email, else SMTP reply_to/
from) — no new mail secrets in this app. The current stale set also drives an
in-app warning banner on the Direct Rates page (GET /direct/health), with an
on-demand re-check endpoint (POST /direct/health/check).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- new GET /direct/hotels/{id}/config returns full hotel config plus
distinct room/rate IDs from scraped data (and any known via labels)
- Configure panel: benchmark room/rate selects, tier base room, per-room
£ offsets (base locked to 0), friendly room/rate names, room display
order (up/down) — saved via the existing PUT endpoint
- enables apples-to-apples estimated benchmark when only e.g. a suite
is left: bench = room_price - room_offset + bench_offset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>