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
@@ -4,6 +4,7 @@ import { getStorageItem, setStorageItem, removeStorageItem, getSessionItem, setS
import { showToast, openCivitaiByMetadata, isTypingContext } from '../../utils/uiHelpers.js';
import { eventManager } from '../../utils/EventManager.js';
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
import { openPriceAlertsPanel } from '../../managers/UpdateService.js';
import { sidebarManager } from '../SidebarManager.js';
import { initSortDropdown, applySortToSelect, randomizeSortValue } from './SortDropdown.js';
@@ -256,6 +257,17 @@ export class PageControls {
});
}
// Opens the notification bell on the price alerts tab (same action as the
// global context menu entry).
const priceAlertsOption = document.getElementById('priceAlertsMenuItem');
if (priceAlertsOption) {
priceAlertsOption.addEventListener('click', (e) => {
e.stopPropagation();
document.querySelector('.dropdown-group.active')?.classList.remove('active');
openPriceAlertsPanel();
});
}
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.dropdown-group')) {