The owner could not tell from the UI what "Buzz Price Tracking" enabled, what the
"Price alert threshold" number meant, or what "Price Alerts" was alerting about.
That was not a copy problem: the implementation exposed our mechanism (a page
scrape) and our SQL predicates as the user's concepts. Two concrete defects came
from the same root:
- the alert population included versions the user already owns (neither the event
generator nor the query filtered on is_in_library; in the owner's library 28 of
52 gated versions were already downloaded, so most "alerts" were about files
already on disk, which cannot become cheaper *for them*);
- a threshold-filtered state list lived in a notification surface, so an empty
panel had three indistinguishable causes and read as a broken feature.
The information model is now the version plus ownership: cost is shown only where
a decision exists. Owned -> nothing. Not owned and free -> nothing. Not owned and
gated -> the price when it is known, `Paid` without a number when it is not, and
early access keeps its countdown because "free on <date>" decides between waiting
and paying. The numeric threshold has no place in that model: every decision is
categorical (wait / pay / skip), so the setting, the comparison and the whole
alert-state machine are gone.
- both alert-state columns are removed from the schema rather than left dead; a
database created by an unreleased build has them dropped on open (native
ALTER TABLE ... DROP COLUMN, guarded), which is a no-op for everyone else
- gate events are emitted only for versions the user does not have, and the
price-drop event goes with the threshold it belonged to
- both alert endpoints, PriceAlertsHandler and the service-registry adapter field
it needed are removed: events already reach the UI through the refresh response
- the bell tab, panel, CSS, both entry points, the unread watermark and their
locales are removed; the setting keeps only the enable flag and the refresh
interval and is framed as plumbing
- "Price unavailable" is replaced by `Paid`: the gate is certain from the public
API, only the number is best-effort, and that is our plumbing, not the user's
problem
Verified against a copy of the owner's real database: 52 gated versions ->
28 owned (now silent) + 24 the feature is actually about; the drop migration ran
and both removed endpoints 404.
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.