End-to-end verification against the live site found the price capture broken for
a whole class of users: the civitai page hosts are not interchangeable, and the
user's civitai_host preference was silently fatal. With civitai_host=civitai.red
the update DB held zero prices even with tracking enabled.
- civitai.red refuses non-browser HTTP clients outright (Cloudflare challenge,
403 for any User-Agent, aiohttp and httpx alike), while civitai.com and
civitai.green answer normally for anonymously visible models and 404 for
mature ones. An earlier manual check with curl passed on TLS fingerprint luck,
which is why this was missed.
- get_model_prices now tries the configured host first, then the others, and
takes the first parseable payload. The host that worked is remembered, and a
host that refuses outright is parked for 15 minutes so a library full of
mature models does not pay three requests each; a 404 is model-specific and
does not park the host. Links keep using the configured host, which is where
the user's own browser has clearance.
- Mature models still have no price source anywhere, so that is now stated
instead of silent: price_check_attempted_at separates "tried and unreadable"
from "never looked", gated versions show a muted "Price unavailable" badge,
and the alerts panel reports unavailableCount.
- Failures are logged at warning level, once per host per TTL, with the
per-host reason, instead of only at debug level.
- The recorded alternatives (internal tRPC with the user's API key, or an
extension-assisted fetch from the user's browser) and the strengthened
upstream ask for a public price field are documented in the plan.
P5a of docs/plans/paid-model-price-tracking.md: one surface that answers "what
got cheaper / became free", without a permanent button (the grid filter was
dropped by owner decision, so the panel carries the actions itself).
- price_alert_since records when an alert started, so the panel can say
"dropped 3 d ago" and count what is new since the user last looked; it is set
on the first sight of an already-cheap version, preserved while the alert
stands, and cleared when the price rises back above the threshold
- get_price_alerts() compares the threshold at read time (editing it takes
effect immediately, no refresh needed) and returns both kinds in one list;
model_type=None covers every type, which the shared update DB makes a single
query. "became free" needs no price data, so it is reported even while price
tracking is off
- GET /api/lm/price-alerts, registered once in MiscRoutes rather than per model
type, decorating rows best-effort with the local model name and file path from
the scanner indexes (a cold cache just omits them)
- a third tab in the notification bell: segments for under-threshold and
became-free, the three states (tracking off / nothing matching / stale), and
per-row actions (CivitAI always, Open when the model is local)
- two non-permanent entry points share one helper: the controls-bar updates
dropdown and the global context menu, whose label carries the unread count
- unread state stays client-side (localStorage watermark); the count is fetched
once on init and only when price tracking is enabled
- the per-type frontend client method is removed as dead code; the per-type
backend route stays for the companion extension