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
+1
View File
@@ -100,4 +100,5 @@ export const previewTargetTypeItems = (): GridItem[] => [
export const providerTypeItems = (): GridItem[] => [
{ value: 'immich', icon: 'mdiCamera', label: t('providers.typeImmich') },
{ value: 'gitea', icon: 'mdiGit', label: t('providers.typeGitea') },
{ value: 'scheduler', icon: 'mdiClockOutline', label: t('providers.typeScheduler') },
];
+11
View File
@@ -107,6 +107,7 @@
"checking": "Checking...",
"typeImmich": "Immich",
"typeGitea": "Gitea",
"typeScheduler": "Scheduler",
"loadError": "Failed to load providers.",
"externalDomain": "External Domain",
"optional": "optional",
@@ -134,6 +135,14 @@
"eventTypes": "Event Types",
"notificationTargets": "Notification Targets",
"scanInterval": "Scan Interval (seconds)",
"scheduleType": "Schedule",
"intervalMode": "Interval",
"cronMode": "Cron expression",
"cronExpression": "Cron expression",
"cronHint": "Standard 5-field cron: minute hour day month weekday. Example: 0 9 * * 1-5 (weekdays at 9:00)",
"customVariables": "Custom Variables",
"customVariablesHint": "Define key-value pairs available in templates as {{ key }}.",
"addVariable": "Add variable",
"createTracker": "Create Tracker",
"noTrackers": "No trackers yet. Add a provider first, then create a tracker.",
"active": "Active",
@@ -310,6 +319,7 @@
"rateSearch": "Search cooldown",
"rateFind": "Find cooldown",
"rateDefault": "Default cooldown",
"noCommandsForProvider": "This provider type does not support bot commands.",
"syncCommands": "Sync with Telegram",
"discoverChats": "Discover chats from Telegram",
"clickToCopy": "Click to copy chat ID",
@@ -365,6 +375,7 @@
"prMerged": "PR merged",
"prCommented": "PR commented",
"releasePublished": "Release published",
"scheduledMessage": "Scheduled message",
"trackImages": "Track images",
"trackVideos": "Track videos",
"favoritesOnly": "Favorites only",
+11
View File
@@ -107,6 +107,7 @@
"checking": "Проверка...",
"typeImmich": "Immich",
"typeGitea": "Gitea",
"typeScheduler": "Планировщик",
"loadError": "Не удалось загрузить провайдеры.",
"externalDomain": "Внешний домен",
"optional": "необязательно",
@@ -134,6 +135,14 @@
"eventTypes": "Типы событий",
"notificationTargets": "Получатели уведомлений",
"scanInterval": "Интервал проверки (секунды)",
"scheduleType": "Расписание",
"intervalMode": "Интервал",
"cronMode": "Cron выражение",
"cronExpression": "Cron выражение",
"cronHint": "Стандартный 5-полевой cron: минута час день месяц день_недели. Пример: 0 9 * * 1-5 (будни в 9:00)",
"customVariables": "Пользовательские переменные",
"customVariablesHint": "Определите пары ключ-значение, доступные в шаблонах как {{ ключ }}.",
"addVariable": "Добавить переменную",
"createTracker": "Создать трекер",
"noTrackers": "Трекеров пока нет. Сначала добавьте провайдер, затем создайте трекер.",
"active": "Активен",
@@ -310,6 +319,7 @@
"rateSearch": "Кулдаун поиска",
"rateFind": "Кулдаун поиска файлов",
"rateDefault": "Кулдаун по умолчанию",
"noCommandsForProvider": "Этот тип провайдера не поддерживает команды бота.",
"syncCommands": "Синхронизировать с Telegram",
"discoverChats": "Обнаружить чаты из Telegram",
"clickToCopy": "Нажмите, чтобы скопировать ID чата",
@@ -365,6 +375,7 @@
"prMerged": "PR влит",
"prCommented": "Комментарий к PR",
"releasePublished": "Релиз опубликован",
"scheduledMessage": "Запланированное сообщение",
"trackImages": "Фото",
"trackVideos": "Видео",
"favoritesOnly": "Только избранные",
+17
View File
@@ -53,6 +53,23 @@ export const commandTemplateConfigsCache = createEntityCache<CommandTemplateConf
/** Command trackers — used by Command Trackers page. */
export const commandTrackersCache = createEntityCache<CommandTracker>('/command-trackers');
/** Provider capabilities — used by Template Configs, Command Configs. */
export const capabilitiesCache = (() => {
let data = $state<Record<string, any>>({});
let fetchedAt = $state(0);
const TTL = 60_000; // 1 minute
return {
get items() { return data; },
async fetch(force = false): Promise<Record<string, any>> {
if (!force && Object.keys(data).length > 0 && Date.now() - fetchedAt < TTL) return data;
const { api } = await import('$lib/api');
data = await api('/providers/capabilities');
fetchedAt = Date.now();
return data;
},
};
})();
/**
* All caches keyed by entity type — for search palette and crosslink resolution.
*/