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
This commit is contained in:
Will Miao
2026-10-04 15:08:48 +08:00
parent ad2402724b
commit 7ed19c185c
27 changed files with 2142 additions and 86 deletions
+34 -2
View File
@@ -234,6 +234,9 @@
},
"groupByModel": {
"label": "קיבוץ לפי מודל"
},
"priceAlerts": {
"label": "[TODO: Translate] Price alerts"
}
},
"header": {
@@ -942,7 +945,9 @@
"action": "עדכונים",
"menuLabel": "הצגת אפשרויות עדכון",
"check": "בדוק עדכונים",
"checkTooltip": "בדיקת עדכונים עלולה לקחת זמן."
"checkTooltip": "בדיקת עדכונים עלולה לקחת זמן.",
"priceAlerts": "[TODO: Translate] Price alerts",
"priceAlertsTooltip": "[TODO: Translate] Buzz price alerts for paid versions"
}
},
"bulkOperations": {
@@ -2321,7 +2326,8 @@
"notificationsTitle": "מרכז התראות",
"tabs": {
"updates": "עדכונים",
"messages": "הודעות"
"messages": "הודעות",
"priceAlerts": "[TODO: Translate] Price Alerts"
},
"updateAvailable": "עדכון זמין",
"noChangelogAvailable": "אין יומן שינויים מפורט זמין. בדוק ב-GitHub למידע נוסף.",
@@ -2377,6 +2383,32 @@
"shown": "הוצג {time}",
"dismissed": "הוסר {time}",
"active": "פעיל"
},
"priceAlerts": {
"disabled": "[TODO: Translate] CivitAI does not publish prices in its public API, so prices are read from the model page. Turn on price tracking to see them here.",
"openSettings": "[TODO: Translate] Open settings",
"segmentsLabel": "[TODO: Translate] Alert kind",
"segments": {
"belowThreshold": "[TODO: Translate] Under threshold",
"becameFree": "[TODO: Translate] Became free"
},
"thresholdLabel": "[TODO: Translate] Alert threshold: {buzz} Buzz",
"empty": "[TODO: Translate] Nothing is under your price threshold right now.",
"emptyFree": "[TODO: Translate] No version has become free yet.",
"loadFailed": "[TODO: Translate] Could not refresh the alerts; showing the last known list.",
"lastChecked": "[TODO: Translate] Prices last checked {when}",
"justNow": "[TODO: Translate] just now",
"minutesAgo": "[TODO: Translate] {count} min ago",
"hoursAgo": "[TODO: Translate] {count} h ago",
"daysAgo": "[TODO: Translate] {count} d ago",
"freeNow": "[TODO: Translate] Free now",
"blueBuzz": "[TODO: Translate] Blue Buzz OK",
"earlyAccessUntil": "[TODO: Translate] Early access until {date}",
"inLibrary": "[TODO: Translate] In library",
"notInLibrary": "[TODO: Translate] Not in library",
"droppedAgo": "[TODO: Translate] dropped {when}",
"openCivitai": "[TODO: Translate] Open on CivitAI",
"openLocal": "[TODO: Translate] Open"
}
},
"support": {