feat: add Scheduler provider + multi-provider UX fixes

Scheduler provider:
- Virtual provider (no external service) that emits SCHEDULED_MESSAGE
  events on user-defined intervals or cron expressions
- Custom variables stored in tracker filters, flattened into template context
- fire_count persists across triggers via tracker state
- APScheduler CronTrigger support for cron-mode schedules
- Default templates (EN+RU), seeded on startup

Multi-provider UX fixes:
- Tracking config hides Immich-specific sections (periodic, scheduled,
  memory, asset display) for non-Immich providers
- Command config driven by provider capabilities — hides commands/settings
  for providers without bot commands
- Template config hides empty "Scheduled Messages" group
- Test menu on tracker targets is provider-aware (Immich shows all 4 test
  types, others show only basic)
- Removed redundant Test button from tracker card
- System-owned tracking configs (user_id=0) seeded for Gitea + Scheduler
- Fixed ownership checks to allow system configs in tracker-target links
- Capabilities cache shared across template-configs and command-configs
- Command tracker bot selector uses EntitySelect instead of raw select
- Sample context includes Gitea + Scheduler variables for template preview
This commit is contained in:
2026-03-22 15:50:51 +03:00
parent 6d28cfb8d8
commit 0562f78b35
30 changed files with 688 additions and 56 deletions
@@ -94,6 +94,8 @@ def _event_allowed_by_config(event: ServiceEvent, tc: TrackingConfig) -> bool:
"pr_merged": tc.track_pr_merged,
"pr_commented": tc.track_pr_commented,
"release_published": tc.track_release_published,
# Scheduler events
"scheduled_message": tc.track_scheduled_message,
}
return flag_map.get(event_type, True)
@@ -210,6 +212,8 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
provider_type = provider.type
provider_config = dict(provider.config)
provider_name = provider.name
tracker_name = tracker.name
tracker_filters = dict(tracker.filters) if tracker.filters else {}
collection_ids = list(tracker.collection_ids or [])
# Now create aiohttp session and poll
@@ -235,6 +239,15 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
# Gitea is webhook-based — events arrive via /api/webhooks/gitea endpoint.
# The scheduler still calls check_tracker but there's nothing to poll.
return {"status": "ok", "events_detected": 0, "collections_checked": 0}
elif provider_type == "scheduler":
from notify_bridge_core.providers.scheduler import SchedulerServiceProvider
custom_vars = tracker_filters.get("custom_variables", {})
sched = SchedulerServiceProvider(
name=provider_name,
tracker_name=tracker_name,
custom_variables=custom_vars,
)
events, new_state = await sched.poll(collection_ids, state_dict)
else:
return {"status": "error", "reason": f"unsupported provider type: {provider_type}"}