feat(tracking): per-config quiet hours with app-level IANA timezone

Add quiet_hours_enabled/start/end to TrackingConfig (HH:MM strings
interpreted in the app-level timezone AppSetting). The dispatch path
loads the app timezone once per run and passes it through
event_allowed_by_config -> in_quiet_hours, so overnight windows like
22:00-07:00 work correctly in any IANA tz.

Frontend exposes a Timezone field under Settings and a Quiet Hours
section on the Immich tracking-config form with time-picker inputs.
This commit is contained in:
2026-04-22 02:31:48 +03:00
parent 56993d2ca3
commit 6c3dd67c1b
12 changed files with 113 additions and 13 deletions
@@ -22,6 +22,7 @@ _SETTING_KEYS = {
"telegram_webhook_secret": "NOTIFY_BRIDGE_TELEGRAM_WEBHOOK_SECRET",
"telegram_cache_ttl_hours": None, # no env fallback, default 48
"supported_locales": None, # comma-separated locale codes
"timezone": "NOTIFY_BRIDGE_TIMEZONE", # IANA tz (e.g. "Europe/Warsaw"); empty = UTC
}
_DEFAULTS = {
@@ -29,6 +30,7 @@ _DEFAULTS = {
"telegram_webhook_secret": "",
"telegram_cache_ttl_hours": "48",
"supported_locales": "en,ru",
"timezone": "UTC",
}
@@ -50,6 +52,7 @@ class SettingsUpdate(BaseModel):
telegram_webhook_secret: str | None = None
telegram_cache_ttl_hours: str | None = None
supported_locales: str | None = None
timezone: str | None = None
@router.get("")
@@ -54,6 +54,9 @@ class TrackingConfigCreate(BaseModel):
memory_favorite_only: bool = False
memory_asset_type: str = "all"
memory_min_rating: int = 0
quiet_hours_enabled: bool = False
quiet_hours_start: str | None = None
quiet_hours_end: str | None = None
class TrackingConfigUpdate(BaseModel):
@@ -93,6 +96,9 @@ class TrackingConfigUpdate(BaseModel):
memory_favorite_only: bool | None = None
memory_asset_type: str | None = None
memory_min_rating: int | None = None
quiet_hours_enabled: bool | None = None
quiet_hours_start: str | None = None
quiet_hours_end: str | None = None
@router.get("")
@@ -27,7 +27,11 @@ from ..database.models import (
ServiceProvider,
WebhookPayloadLog,
)
from ..services.dispatch_helpers import event_allowed_by_config, load_link_data
from ..services.dispatch_helpers import (
event_allowed_by_config,
get_app_timezone,
load_link_data,
)
_LOGGER = logging.getLogger(__name__)
@@ -144,6 +148,8 @@ async def _dispatch_webhook_event(
if not link_data:
continue
app_tz = await get_app_timezone(session)
# Log event
extra_details = {k: v for k, v in event.extra.items() if k in detail_keys}
session.add(EventLog(
@@ -164,7 +170,7 @@ async def _dispatch_webhook_event(
# Dispatch to targets
dispatcher = NotificationDispatcher()
target_configs = _build_target_configs(event, link_data, provider_config)
target_configs = _build_target_configs(event, link_data, provider_config, app_tz)
if target_configs:
results = await dispatcher.dispatch(event, target_configs)
for r in results:
@@ -513,12 +519,13 @@ def _build_target_configs(
event: ServiceEvent,
link_data: list[dict[str, Any]],
provider_config: dict[str, Any],
app_tz: str = "UTC",
) -> list[TargetConfig]:
"""Build TargetConfig objects for dispatch, applying tracking config filters."""
target_configs: list[TargetConfig] = []
for ld in link_data:
tc = ld["tracking_config"]
if tc and not event_allowed_by_config(event, tc):
if tc and not event_allowed_by_config(event, tc, app_tz):
continue
tmpl = ld["template_config"]