Commit Graph
25 Commits
Author SHA1 Message Date
Will Miao e42d649df0 refactor(update)!: make obtainability a property of updates, not a surface
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.
2026-10-05 08:54:08 +08:00
Will Miao 156d2e5eb9 fix(ui): explain a 0 Buzz threshold in the price alerts panel
The default threshold is 0 ("only tell me when a version becomes free") and the
settings copy says so, but the panel did not: a real instance with 52 priced paid
versions and an untouched threshold showed "Nothing is under your price threshold
right now" with only a small "Alert threshold: 0 Buzz" in the corner, which reads
as a broken feature.

- the payload now carries pricedCount, so the empty state can say how many paid
  versions already have a known price
- when the threshold is 0 and prices are known, the empty state says so and
  points at Settings - Library instead of implying there is nothing to show
- the read-time threshold comparison means setting one takes effect immediately;
  measured on a copy of that instance: 0 Buzz -> 0 alerts, 100 -> 44,
  500 -> 48, 5000 -> 51
2026-10-04 20:28:57 +08:00
Will Miao 425d90a912 fix(update): decouple price capture from the metadata TTL
Found in a real instance: after enabling price tracking, a normal "Check updates"
captured exactly one price out of 718 models, so the alerts panel looked broken
while the log said the refresh completed.

Price capture only ran when the version list was re-fetched, so it inherited the
metadata TTL: with 24 h metadata and 24 h price TTLs, only the handful of models
whose metadata happened to be stale that round were ever priced.

- the cached record already carries the gate, so the price pass now runs off
  whichever version list is available (freshly fetched or stored) and applies the
  result without touching last_checked_at, so a price-only pass cannot silently
  extend the metadata TTL
- a failed attempt now satisfies the price TTL, so a mature model whose page no
  host will serve is not retried on every single update check
- an explicitly forced check re-prices within the TTL

Verified by copying a real instance's update DB into a sandbox and running a
non-forced check: bulk metadata fetches 0 (version lists entirely from cache)
while priced versions went 1 -> 20 and the panel listed 19 alerts.
2026-10-04 20:20:50 +08:00
Will Miao ec5fef512b fix(update): read model-page prices from a host that answers
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.
2026-10-04 20:00:29 +08:00
Will Miao 7ed19c185c feat(update): add a Buzz price alerts panel to the notification bell
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
2026-10-04 15:08:48 +08:00
Will Miao ad2402724b feat(update): track buzz prices and alert below a threshold
CivitAI's public API deliberately omits prices — paidAccess is trimmed to
{permanent, endsAt} because "pricing belongs to the purchase flow" — but the
public model page embeds the site's own model.getById result, including
paidAccess.terms, in its server-rendered payload. That is read anonymously
(no API key, no internal endpoint, no forged Origin), one request per gated
model, so only the ~2% of models that actually carry a gate pay for it.

- optional capture, off by default: price_tracking_enabled,
  price_alert_threshold_buzz (0 = alert on "became free" only) and
  price_check_ttl_hours; prices refresh on their own TTL and immediately when a
  gate changes, and a failed fetch keeps the stored price instead of blanking it
- versions that stop carrying a gate are marked free (persisted gate_lapsed_at)
  and gate transitions are reported as events on the refresh response, so a
  version already in the library can announce that it became free
- price_alert_state plus a price_drop edge event; new
  GET /api/lm/{type}/updates/price-alerts lists what is under the threshold
- versions tab shows the price (effective, with the list price struck through
  and a Blue Buzz note) and a Free Now badge; an update check toasts the
  transitions in one message
- the parser and the alerts query are unit-tested against a trimmed page
  fixture, and every route definition is now asserted to resolve to a handler

Plan, verification notes and the deviations from it are in
docs/plans/paid-model-price-tracking.md.
2026-10-04 08:53:24 +08:00
Will Miao 5f4054265d fix(update): treat a timed paidAccess gate with no recorded end as active
CivitAI only returns a non-null paidAccess for an *active* gate: a lapsed gate
stays in the database as a tombstone and is filtered out server-side, so
{"permanent": false, "endsAt": null} — a timed gate whose window end has not
been recorded yet — is still enforced. Verified live: on model 1802980 that
version reports canDownload: false while its lapsed siblings report true.

Both the update service and the download gate dropped that shape, so such
versions read as free and "Hide Early Access Updates" missed them — the class
of bug reported in #1060.

The interpretation now lives in py/utils/paid_access.py and is shared, so the
badge, the update filter and the download warning cannot disagree.
2026-10-04 08:53:12 +08:00
Will Miao 08895f77ff fix(update): align update-check summary count with Updates filter scope (#1083) 2026-08-25 20:36:46 +08:00
Will Miao da071e8452 feat(versions): add file-variant badge and hide download button for in-library versions (#1058) 2026-08-24 23:17:10 +08:00
Will Miao ef3e7d7bf4 feat(update): detect CivitAI paidAccess versions and add hide paid updates (#1060)
CivitAI's PaidAccess cutover deprecated the availability=EarlyAccess and
earlyAccessEndsAt signals; gated versions now report availability=Public
with a paidAccess DTO that LoRA Manager previously ignored, so "Hide
Early Access Updates" missed paid/early-access models and downloads
failed with 401.

Parse and persist paidAccess from model-level, bulk, and by-hash
responses; treat timed paid gates as early access and permanent paid
versions as a distinct is_paid state; add a hide_paid_updates setting
with a "Paid" badge in the versions tab; warn before downloading gated
versions. Includes SQLite migration, i18n for all locales, and
backend/frontend tests.
2026-08-15 18:08:14 +08:00
Will Miao d2f955266d fix(types): resolve pre-existing basedpyright errors in tests
Fix ~790 basedpyright errors across the test suite:
- Type stub subclasses of real production classes with super().__init__()
- Add missing generic type arguments and Dict[str, Any] annotations
- Add None guards before subscript/member access
- Adapt tests to production API changes (removed dead handlers,
  PersistentModelCache.get_default, _i18n_filter_added location)
2026-08-08 20:12:59 +08:00
Will Miao 3d207b6744 fix(updates): mark cross-folder versions as in-library during folder-filtered refresh (#997)
When refreshing updates with a folder filter, versions already present in
other folders were excluded from the is_in_library check, making them
appear as available updates. When the user tried to download, the global
check found the file already exists and returned 'model already exists'.

Fix by also collecting the cross-folder version set when folder_path is
provided, and using the union (folder-filtered + cross-folder) for
is_in_library in both _build_record_from_remote and
_merge_with_local_versions.
2026-06-26 17:40:41 +08:00
Will MiaoandClaude Opus 4.7 055e94d77b fix(updates): chunk bulk queries to avoid SQLite variable limit (#914)
_split _get_records_bulk into 500-id batches so the WHERE IN clause
never exceeds SQLite's 999-parameter ceiling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:15:44 +08:00
Will Miao f199c9b591 feat: propagate version info to model update record creation. Fixes #756
- Pass `version_info` parameter through download manager to model update service
- Enhance `_create_record` to use version info when creating records for missing versions
- Add `_extract_single_version` helper method for consistent version extraction
- Improve handling of version metadata during library synchronization
2026-01-06 08:46:55 +08:00
Will Miao 837c32c42f feat: implement task cancellation for model scanning and bulk operations 2026-01-02 18:48:28 +08:00
Will Miao 0e73db0669 feat: implement same_base update strategy for model annotations
Add support for configurable update flag strategy with new "same_base" mode that considers base model versions when determining update availability. The strategy is controlled by the "update_flag_strategy" setting.

When strategy is set to "same_base":
- Uses get_records_bulk instead of has_updates_bulk
- Compares model versions against highest local versions per base model
- Provides more granular update detection based on base model relationships

Fallback to existing bulk or individual update checks when:
- Strategy is not "same_base"
- Bulk operations fail
- Records are unavailable

This enables more precise update flagging for models sharing common bases.
2025-11-17 19:26:41 +08:00
pixelpaws 1be3235564 fix(model-updates): support per-model version ids 2025-10-30 23:15:23 +08:00
Will Miao cf4d654c4b feat: improve file size extraction logic with primary model preference
Refactor `_extract_size_bytes` method to prioritize primary model files when calculating size. The new implementation:
- Extracts size parsing into separate `parse_size` function
- Adds logic to prefer files marked as both "model" type and "primary"
- Falls back to first valid size if no primary model file found
- Adds comprehensive tests for primary preference and fallback behavior

This ensures more accurate size reporting for model files, particularly when multiple file types are present in the response.
2025-10-29 09:16:29 +08:00
pixelpaws de05b59f29 test(routes): cover snake case model id payload 2025-10-29 07:33:58 +08:00
pixelpaws dc1f7ab6fe fix: handle civitai not found responses 2025-10-28 21:47:30 +08:00
Will Miao 427e7a36d5 feat: improve model update detection logic
Update ModelUpdateRecord.has_update() to only detect updates when a newer remote version exists than the latest local version. Previously, any missing remote version would trigger an update, which could include older versions that shouldn't be considered updates.

- Add logic to find the maximum version ID in library
- Only return True for remote versions newer than the latest local version
- Add comprehensive unit tests for the new update detection behavior
- Update docstring to reflect the new logic
2025-10-25 21:31:01 +08:00
Will Miao d9a6db3359 feat: optimize model update checking with bulk operations
- Refactor update filter logic to use bulk update checks when available
- Add annotation method to attach update flags to response items
- Improve performance by reducing API calls for update status checks
- Maintain backward compatibility with fallback to individual checks
- Handle edge cases and logging for failed update status resolutions
2025-10-25 15:31:33 +08:00
Will Miao 58cafdb713 feat: add model version management endpoints
- Add set_version_update_ignore endpoint to toggle ignore status for specific versions
- Add get_model_versions endpoint to retrieve version details with optional refresh
- Update serialization to include version-specific data and preview overrides
- Modify database schema to support version-level ignore tracking
- Improve error handling for rate limiting and missing models

These changes enable granular control over version updates and provide better visibility into model version status.
2025-10-25 14:54:23 +08:00
pixelpaws 21a1bc1a01 feat(metadata): batch refresh model versions 2025-10-15 20:47:30 +08:00
pixelpaws ee0d241c75 refactor(routes): limit update endpoints to essentials 2025-10-15 15:37:35 +08:00