feat: wire tracking-config display filters + per-tracker adaptive polling

Display filters (Immich tracking config):
- favorites_only drops events with no favorited new assets, or filters
  added_assets to favorites only
- assets_order_by/assets_order sort the rendered list
  (date / name / rating / random / none)
- max_assets_to_show caps rendered+attached media (default 5 -> 10)
- include_tags strips people from event extras and tags from each asset
- include_asset_details strips city/country/state/lat/lon/is_favorite/
  rating/description; load-bearing fields (thumbhash, file_size,
  playback_size, cache keys) preserved
- New apply_tracking_display_filters helper in dispatch_helpers; wired
  into watcher, webhooks, scheduled/periodic/memory, and manual
  test-dispatch
- Targets sharing a TrackingConfig dispatch together; targets with
  different TCs each see their own shaped event

Adaptive polling:
- Replace NotificationTracker.batch_duration with adaptive_max_skip
- Per-tracker opt-in: NULL/0 disables back-off (every tick runs);
  positive N caps the skip factor at (N-1)-in-N after long idle
- Scheduler caches the cap in module state for the tick fast-path
- Migration adds the new column; API schemas/responses, frontend types,
  i18n, and the tracker form updated to match
This commit is contained in:
2026-04-24 21:12:10 +03:00
parent 187b889c45
commit ab621b6abc
19 changed files with 367 additions and 72 deletions
@@ -62,7 +62,8 @@
// Tracker form
const defaultForm = () => ({
name: '', icon: '', provider_id: 0, collection_ids: [] as string[],
scan_interval: 60, batch_duration: 0,
scan_interval: 60,
adaptive_max_skip: null as number | null,
default_tracking_config_id: 0, default_template_config_id: 0,
filters: {} as Record<string, any>,
});
@@ -180,7 +181,8 @@
form = {
name: trk.name, icon: trk.icon || '', provider_id: trk.provider_id,
collection_ids: [...(trk.collection_ids || [])],
scan_interval: trk.scan_interval, batch_duration: trk.batch_duration ?? 0,
scan_interval: trk.scan_interval,
adaptive_max_skip: trk.adaptive_max_skip ?? null,
default_tracking_config_id: trk.default_tracking_config_id ?? 0,
default_template_config_id: trk.default_template_config_id ?? 0,
filters: trk.filters || {},
@@ -223,6 +225,12 @@
...form,
default_tracking_config_id: form.default_tracking_config_id || null,
default_template_config_id: form.default_template_config_id || null,
// Empty string, 0, or null all mean "disable adaptive polling".
// Coerce to null so the DB column stays NULL rather than 0.
adaptive_max_skip:
form.adaptive_max_skip && form.adaptive_max_skip > 1
? form.adaptive_max_skip
: null,
};
if (editing) {
await api(`/notification-trackers/${editing}`, { method: 'PUT', body: JSON.stringify(payload) });