diff --git a/CLAUDE.md b/CLAUDE.md
index ee18c9d..4bebe74 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -42,19 +42,25 @@ Default test account: username `admin`, password `admin1`.
- **Environment vars**: `NOTIFY_BRIDGE_DATA_DIR`, `NOTIFY_BRIDGE_SECRET_KEY`, `NOTIFY_BRIDGE_DATABASE_URL`
- Core package includes `jinja2` dependency (template rendering lives in core, not server).
-## Entity Relationships (Phase 6)
+## Entity Relationships
```
-ServiceProvider → type: "immich", config: JSON (url, api_key, external_domain)
-Tracker → provider_id, tracking_config_id, target_ids: JSON list, collection_ids: JSON list
-TrackingConfig → provider_type (must match provider), event flags, scheduling
-TemplateConfig → provider_type (must match provider), Jinja2 slots per event type
-NotificationTarget → template_config_id, type: "telegram"/"webhook", config: JSON
+ServiceProvider → type: "immich" (inferred capabilities: notifications, commands)
+NotificationTracker → provider_id, collection_ids, scan_interval, batch_duration, enabled
+NotificationTrackerTarget → notification_tracker_id, target_id, tracking_config_id, template_config_id, quiet_hours, enabled
+TrackingConfig → provider_type, event flags, scheduling rules
+TemplateConfig → provider_type, Jinja2 template slots per event type
+NotificationTarget → type: "telegram"/"webhook", config JSON, chat_action (telegram only)
+CommandConfig → provider_type, enabled_commands, locale, response_mode, default_count, rate_limits
+CommandTracker → provider_id, command_config_id, enabled
+CommandTrackerListener → command_tracker_id, listener_type ("telegram_bot"), listener_id
+TelegramBot → token, update_mode, bot_username (used as notification target backend + commands listener)
```
-- TrackingConfig owned by Tracker (what to watch), TemplateConfig owned by Target (how to format)
+- NotificationTrackerTarget links a tracker to a target with per-link tracking/template config and quiet hours
+- CommandTrackerListener links a command tracker to a listener (e.g. TelegramBot) for slash-command handling
- `user_id=0` on TemplateConfig = system default (EN/RU seeded on first startup)
-- DB: SQLite + async SQLAlchemy via sqlmodel, auto-created on startup
+- DB: SQLite + async SQLAlchemy via sqlmodel, auto-created on startup with migrations
- API: All CRUD routes under `/api/`, auth via JWT Bearer, `NOTIFY_BRIDGE_` env prefix
## Template System Sync Rules
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a319386..6cc1318 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -19,7 +19,6 @@
"codemirror": "^6.0.2"
},
"devDependencies": {
- "@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
@@ -1082,15 +1081,6 @@
"acorn": "^8.9.0"
}
},
- "node_modules/@sveltejs/adapter-auto": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz",
- "integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==",
- "dev": true,
- "peerDependencies": {
- "@sveltejs/kit": "^2.0.0"
- }
- },
"node_modules/@sveltejs/adapter-static": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz",
@@ -3186,13 +3176,6 @@
"dev": true,
"requires": {}
},
- "@sveltejs/adapter-auto": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz",
- "integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==",
- "dev": true,
- "requires": {}
- },
"@sveltejs/adapter-static": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz",
diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json
index b91731d..f249520 100644
--- a/frontend/src/lib/i18n/en.json
+++ b/frontend/src/lib/i18n/en.json
@@ -6,11 +6,13 @@
"nav": {
"dashboard": "Dashboard",
"providers": "Providers",
- "trackers": "Trackers",
+ "notificationTrackers": "Notif. Trackers",
"trackingConfigs": "Tracking",
"templateConfigs": "Templates",
"telegramBots": "Bots",
"targets": "Targets",
+ "commandConfigs": "Cmd Configs",
+ "commandTrackers": "Cmd Trackers",
"users": "Users",
"settings": "Settings",
"logout": "Logout"
@@ -93,8 +95,8 @@
"testAndSave": "Test & Save",
"saveWithoutTest": "Save without testing"
},
- "trackers": {
- "title": "Trackers",
+ "notificationTracker": {
+ "title": "Notification Trackers",
"description": "Monitor albums for changes",
"newTracker": "New Tracker",
"cancel": "Cancel",
@@ -198,7 +200,9 @@
"maxAssetSize": "Max asset size (MB)",
"videoWarning": "Video size warning",
"disableUrlPreview": "Disable link previews",
- "sendLargeAsDocuments": "Send large photos as documents"
+ "sendLargeAsDocuments": "Send large photos as documents",
+ "chatAction": "Chat action",
+ "chatActionNone": "None (no action)"
},
"users": {
"title": "Users",
@@ -474,6 +478,47 @@
"botLocale": "Language for command descriptions in Telegram's menu and bot response messages.",
"rateLimits": "Cooldown in seconds between uses of each command category per chat. 0 = no limit."
},
+ "commandConfig": {
+ "title": "Command Configs",
+ "description": "Define command settings for Telegram bot interactions",
+ "newConfig": "New Config",
+ "name": "Name",
+ "namePlaceholder": "Default commands",
+ "providerType": "Provider Type",
+ "enabledCommands": "Enabled Commands",
+ "locale": "Locale",
+ "responseMode": "Response Mode",
+ "modeMedia": "Media (photos)",
+ "modeText": "Text only",
+ "defaultCount": "Default Count",
+ "rateLimits": "Rate Limits",
+ "searchCooldown": "Search cooldown (s)",
+ "defaultCooldown": "Default cooldown (s)",
+ "noConfigs": "No command configs yet.",
+ "confirmDelete": "Delete this command config?",
+ "commands": "commands"
+ },
+ "commandTracker": {
+ "title": "Command Trackers",
+ "description": "Manage command trackers and their listeners",
+ "newTracker": "New Tracker",
+ "name": "Name",
+ "namePlaceholder": "Family commands",
+ "provider": "Provider",
+ "selectProvider": "Select provider...",
+ "commandConfig": "Command Config",
+ "selectCommandConfig": "Select command config...",
+ "listeners": "Listeners",
+ "addListener": "Add Listener",
+ "removeListener": "Remove",
+ "noTrackers": "No command trackers yet.",
+ "confirmDelete": "Delete this command tracker?",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "noListeners": "No listeners attached.",
+ "selectBot": "Select bot...",
+ "listenerType": "telegram_bot"
+ },
"snackbar": {
"showDetails": "Show details",
"hideDetails": "Hide details"
@@ -504,7 +549,16 @@
"commandsSynced": "Commands synced to Telegram",
"targetLinked": "Target linked",
"targetUnlinked": "Target unlinked",
- "botUpdated": "Bot updated"
+ "botUpdated": "Bot updated",
+ "commandConfigSaved": "Command config saved",
+ "commandConfigDeleted": "Command config deleted",
+ "commandTrackerCreated": "Command tracker created",
+ "commandTrackerUpdated": "Command tracker updated",
+ "commandTrackerDeleted": "Command tracker deleted",
+ "commandTrackerEnabled": "Command tracker enabled",
+ "commandTrackerDisabled": "Command tracker disabled",
+ "listenerAdded": "Listener added",
+ "listenerRemoved": "Listener removed"
},
"common": {
"loading": "Loading...",
diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json
index d2de73a..519df3e 100644
--- a/frontend/src/lib/i18n/ru.json
+++ b/frontend/src/lib/i18n/ru.json
@@ -6,11 +6,13 @@
"nav": {
"dashboard": "Главная",
"providers": "Провайдеры",
- "trackers": "Трекеры",
+ "notificationTrackers": "Трекеры увед.",
"trackingConfigs": "Отслеживание",
"templateConfigs": "Шаблоны",
"telegramBots": "Боты",
"targets": "Получатели",
+ "commandConfigs": "Конф. команд",
+ "commandTrackers": "Трекеры команд",
"users": "Пользователи",
"settings": "Настройки",
"logout": "Выход"
@@ -93,8 +95,8 @@
"testAndSave": "Проверить и сохранить",
"saveWithoutTest": "Сохранить без проверки"
},
- "trackers": {
- "title": "Трекеры",
+ "notificationTracker": {
+ "title": "Трекеры уведомлений",
"description": "Отслеживание изменений в альбомах",
"newTracker": "Новый трекер",
"cancel": "Отмена",
@@ -198,7 +200,9 @@
"maxAssetSize": "Макс. размер файла (МБ)",
"videoWarning": "Предупреждение о размере видео",
"disableUrlPreview": "Отключить превью ссылок",
- "sendLargeAsDocuments": "Отправлять большие фото как документы"
+ "sendLargeAsDocuments": "Отправлять большие фото как документы",
+ "chatAction": "Действие в чате",
+ "chatActionNone": "Нет (без действия)"
},
"users": {
"title": "Пользователи",
@@ -474,6 +478,47 @@
"botLocale": "Язык описаний команд в меню Telegram и ответов бота.",
"rateLimits": "Кулдаун в секундах между использованиями команд в каждом чате. 0 = без ограничений."
},
+ "commandConfig": {
+ "title": "Конфигурации команд",
+ "description": "Настройки команд для взаимодействия с Telegram-ботами",
+ "newConfig": "Новая конфигурация",
+ "name": "Название",
+ "namePlaceholder": "Команды по умолчанию",
+ "providerType": "Тип провайдера",
+ "enabledCommands": "Включённые команды",
+ "locale": "Язык",
+ "responseMode": "Режим ответа",
+ "modeMedia": "Медиа (фото)",
+ "modeText": "Только текст",
+ "defaultCount": "Кол-во по умолчанию",
+ "rateLimits": "Ограничения частоты",
+ "searchCooldown": "Кулдаун поиска (с)",
+ "defaultCooldown": "Кулдаун по умолчанию (с)",
+ "noConfigs": "Конфигураций команд пока нет.",
+ "confirmDelete": "Удалить эту конфигурацию команд?",
+ "commands": "команд"
+ },
+ "commandTracker": {
+ "title": "Трекеры команд",
+ "description": "Управление трекерами команд и их слушателями",
+ "newTracker": "Новый трекер",
+ "name": "Название",
+ "namePlaceholder": "Семейные команды",
+ "provider": "Провайдер",
+ "selectProvider": "Выберите провайдер...",
+ "commandConfig": "Конфигурация команд",
+ "selectCommandConfig": "Выберите конфигурацию...",
+ "listeners": "Слушатели",
+ "addListener": "Добавить слушателя",
+ "removeListener": "Удалить",
+ "noTrackers": "Трекеров команд пока нет.",
+ "confirmDelete": "Удалить этот трекер команд?",
+ "enabled": "Включён",
+ "disabled": "Отключён",
+ "noListeners": "Нет подключённых слушателей.",
+ "selectBot": "Выберите бота...",
+ "listenerType": "telegram_bot"
+ },
"snackbar": {
"showDetails": "Показать детали",
"hideDetails": "Скрыть детали"
@@ -504,7 +549,16 @@
"commandsSynced": "Команды синхронизированы с Telegram",
"targetLinked": "Получатель привязан",
"targetUnlinked": "Получатель отвязан",
- "botUpdated": "Бот обновлён"
+ "botUpdated": "Бот обновлён",
+ "commandConfigSaved": "Конфигурация команд сохранена",
+ "commandConfigDeleted": "Конфигурация команд удалена",
+ "commandTrackerCreated": "Трекер команд создан",
+ "commandTrackerUpdated": "Трекер команд обновлён",
+ "commandTrackerDeleted": "Трекер команд удалён",
+ "commandTrackerEnabled": "Трекер команд включён",
+ "commandTrackerDisabled": "Трекер команд отключён",
+ "listenerAdded": "Слушатель добавлен",
+ "listenerRemoved": "Слушатель удалён"
},
"common": {
"loading": "Загрузка...",
diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte
index 3920bc3..94f7c47 100644
--- a/frontend/src/routes/+layout.svelte
+++ b/frontend/src/routes/+layout.svelte
@@ -39,11 +39,13 @@
const baseNavItems = [
{ href: '/', key: 'nav.dashboard', icon: 'mdiViewDashboard' },
{ href: '/providers', key: 'nav.providers', icon: 'mdiServer' },
- { href: '/trackers', key: 'nav.trackers', icon: 'mdiRadar' },
+ { href: '/notification-trackers', key: 'nav.notificationTrackers', icon: 'mdiRadar' },
{ href: '/tracking-configs', key: 'nav.trackingConfigs', icon: 'mdiCog' },
{ href: '/template-configs', key: 'nav.templateConfigs', icon: 'mdiFileDocumentEdit' },
{ href: '/telegram-bots', key: 'nav.telegramBots', icon: 'mdiRobot' },
{ href: '/targets', key: 'nav.targets', icon: 'mdiTarget' },
+ { href: '/command-trackers', key: 'nav.commandTrackers', icon: 'mdiConsoleLine' },
+ { href: '/command-configs', key: 'nav.commandConfigs', icon: 'mdiCog' },
];
const navItems = $derived(auth.isAdmin
? [...baseNavItems, { href: '/users', key: 'nav.users', icon: 'mdiAccountGroup' }, { href: '/settings', key: 'nav.settings', icon: 'mdiCogOutline' }]
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte
index 7429584..ee908dc 100644
--- a/frontend/src/routes/+page.svelte
+++ b/frontend/src/routes/+page.svelte
@@ -127,6 +127,9 @@
animateCount(0, status.trackers.active, (v) => displayActive = v);
animateCount(0, status.trackers.total, (v) => displayTotal = v);
animateCount(0, status.targets, (v) => displayTargets = v);
+ if (status.command_trackers !== undefined) {
+ animateCount(0, status.command_trackers, (v) => displayCommandTrackers = v);
+ }
}, 200);
} catch (err: any) {
error = err.message || t('common.error');
@@ -135,10 +138,13 @@
}
}
+ let displayCommandTrackers = $state(0);
+
const statCards = $derived(status ? [
{ icon: 'mdiServer', label: 'dashboard.providers', value: displayProviders, color: '#0d9488' },
{ icon: 'mdiRadar', label: 'dashboard.activeTrackers', value: displayActive, suffix: ` / ${displayTotal}`, color: '#6366f1' },
{ icon: 'mdiTarget', label: 'dashboard.targets', value: displayTargets, color: '#f59e0b' },
+ ...(status.command_trackers !== undefined ? [{ icon: 'mdiConsoleLine', label: 'nav.commandTrackers', value: displayCommandTrackers, color: '#8b5cf6' }] : []),
] : []);
function timeAgo(dateStr: string): string {
diff --git a/frontend/src/routes/command-configs/+page.svelte b/frontend/src/routes/command-configs/+page.svelte
new file mode 100644
index 0000000..11719dc
--- /dev/null
+++ b/frontend/src/routes/command-configs/+page.svelte
@@ -0,0 +1,240 @@
+
+
+
{cfg.name}
+ {cfg.provider_type} + + {(cfg.enabled_commands || []).length} {t('commandConfig.commands')} + + {cfg.locale?.toUpperCase()} ++ {t('commandConfig.responseMode')}: {cfg.response_mode === 'media' ? t('commandConfig.modeMedia') : t('commandConfig.modeText')} + · {t('commandConfig.defaultCount')}: {cfg.default_count} +
+{trk.name}
+ {providerName(trk.provider_id)} + {configName(trk.command_config_id)} + + {trk.enabled ? t('commandTracker.enabled') : t('commandTracker.disabled')} + ++ {trk.listener_count} {t('commandTracker.listeners').toLowerCase()} +
+ {/if} +{t('common.loading')}
+ {:else if (listeners[trk.id] || []).length === 0} +{t('commandTracker.noListeners')}
+ {:else} +