fix: comprehensive API/UI review — 26 bug fixes and improvements

Backend:
- Scheduler lifecycle sync: create/update/delete tracker now syncs APScheduler jobs
- Test-periodic/test-memory endpoints render actual Jinja2 templates with sample data
- Cascade cleanup on tracker delete (TrackerState removed, EventLog nullified)
- Fix user_id=0 FK violation for system-owned TemplateConfig (removed FK constraint)
- Fix API key leak: only attach x-api-key header for internal provider URLs
- Validate config ownership in tracker_targets create/update
- Fix _response() double-emit of created_at in template/tracking configs
- Add per-target-link test endpoints (test, test-periodic, test-memory)

Frontend:
- Fix orphaned provider on test exception in providers/new
- Add submitting guard + disabled state to targets save button
- Move test buttons from tracker card to per-target-link rows
- Fix Svelte 5 async $state reactivity (spread reassignment for all Record mutations)
- i18n for dashboard timeAgo and event type badges (EN + RU)
- Add required attribute to chat select dropdown in targets
- Fix font CSS vars to prioritize imported DM Sans / JetBrains Mono
- Standardize empty states with centered icon + text across all 6 list pages
- Add stagger-children animation class to all list containers
- Fix slide transition duration consistency (200ms everywhere)
- Standardize border-radius to rounded-md across all form inputs
- Fix providers/new page structure (h2 + mb-8 spacing)
- Fix tracker card action row overflow (flex-wrap justify-end)
- JinjaEditor dark mode reactivity (recreate editor on theme change)
- Add aria-labels to mobile nav items
- Make ConfirmModal confirm button label/icon configurable
- Remove double error reporting on providers page
- Add telegram bot edit functionality (name editing via PUT)
- i18n for External Domain label on provider forms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 14:26:20 +03:00
parent 9eec21a5b2
commit 91e5cd58e9
24 changed files with 3514 additions and 375 deletions
+448
View File
@@ -0,0 +1,448 @@
{
"app": {
"name": "Notify Bridge",
"tagline": "Уведомления о сервисах"
},
"nav": {
"dashboard": "Главная",
"providers": "Провайдеры",
"trackers": "Трекеры",
"trackingConfigs": "Отслеживание",
"templateConfigs": "Шаблоны",
"telegramBots": "Боты",
"targets": "Получатели",
"users": "Пользователи",
"logout": "Выход"
},
"auth": {
"signIn": "Войти",
"signInTitle": "Вход в аккаунт",
"signingIn": "Вход...",
"username": "Имя пользователя",
"password": "Пароль",
"confirmPassword": "Подтвердите пароль",
"setupTitle": "Добро пожаловать",
"setupDescription": "Создайте учётную запись администратора",
"createAccount": "Создать аккаунт",
"creatingAccount": "Создание...",
"passwordMismatch": "Пароли не совпадают",
"passwordTooShort": "Пароль должен быть не менее 6 символов",
"or": "или"
},
"dashboard": {
"title": "Главная",
"description": "Обзор настроек Notify Bridge",
"providers": "Провайдеры",
"activeTrackers": "Активные трекеры",
"targets": "Получатели",
"recentEvents": "Последние события",
"noEvents": "Событий пока нет. Создайте трекер для отслеживания.",
"loading": "Загрузка...",
"justNow": "только что",
"minutesAgo": "{n} мин назад",
"hoursAgo": "{n} ч назад",
"daysAgo": "{n} д назад",
"assetsAdded": "добавлены файлы",
"assetsRemoved": "удалены файлы",
"collectionRenamed": "альбом переименован",
"collectionDeleted": "альбом удалён",
"sharingChanged": "изменение доступа"
},
"providers": {
"title": "Провайдеры",
"description": "Управление подключениями к сервисам",
"addProvider": "Добавить провайдер",
"cancel": "Отмена",
"type": "Тип провайдера",
"name": "Название",
"url": "URL провайдера",
"urlPlaceholder": "http://provider:2283",
"apiKey": "API ключ",
"apiKeyKeep": "API ключ (оставьте пустым, чтобы сохранить текущий)",
"connecting": "Подключение...",
"noProviders": "Провайдеры не настроены.",
"delete": "Удалить",
"confirmDelete": "Удалить этот провайдер?",
"online": "В сети",
"offline": "Не в сети",
"checking": "Проверка...",
"loadError": "Не удалось загрузить провайдеры.",
"externalDomain": "Внешний домен",
"optional": "необязательно"
},
"trackers": {
"title": "Трекеры",
"description": "Отслеживание изменений в альбомах",
"newTracker": "Новый трекер",
"cancel": "Отмена",
"name": "Название",
"namePlaceholder": "Трекер семейных фото",
"server": "Провайдер",
"selectServer": "Выберите провайдер...",
"albums": "Альбомы",
"eventTypes": "Типы событий",
"notificationTargets": "Получатели уведомлений",
"scanInterval": "Интервал проверки (секунды)",
"createTracker": "Создать трекер",
"noTrackers": "Трекеров пока нет. Сначала добавьте провайдер, затем создайте трекер.",
"active": "Активен",
"paused": "Приостановлен",
"pause": "Пауза",
"resume": "Возобновить",
"delete": "Удалить",
"confirmDelete": "Удалить этот трекер?",
"albums_count": "альбом(ов)",
"every": "каждые",
"trackImages": "Отслеживать фото",
"trackVideos": "Отслеживать видео",
"favoritesOnly": "Только избранные",
"includePeople": "Включать людей в уведомления",
"includeAssetDetails": "Включать детали файлов",
"maxAssetsToShow": "Макс. файлов в уведомлении",
"sortBy": "Сортировка",
"sortOrder": "Порядок",
"sortNone": "Исходный порядок",
"sortDate": "Дата",
"sortRating": "Рейтинг",
"sortName": "Имя",
"sortRandom": "Случайный",
"ascending": "По возрастанию",
"descending": "По убыванию",
"quietHoursStart": "Тихие часы начало",
"quietHoursEnd": "Тихие часы конец",
"batchDuration": "Длительность пакета (секунды)",
"linkedTargets": "получатели",
"noLinkedTargets": "Нет привязанных получателей. Добавьте получателя ниже.",
"addTarget": "Добавить получателя"
},
"templates": {
"title": "Шаблоны",
"description": "Шаблоны сообщений Jinja2 для уведомлений",
"newTemplate": "Новый шаблон",
"cancel": "Отмена",
"name": "Название",
"body": "Текст шаблона (Jinja2)",
"variables": "Переменные",
"preview": "Предпросмотр",
"edit": "Редактировать",
"delete": "Удалить",
"confirmDelete": "Удалить этот шаблон?",
"create": "Создать шаблон",
"update": "Обновить шаблон",
"noTemplates": "Шаблонов пока нет. Без шаблона будет использован шаблон по умолчанию.",
"eventType": "Тип события",
"allEvents": "Все события",
"assetsAdded": "Добавлены файлы",
"assetsRemoved": "Удалены файлы",
"albumRenamed": "Альбом переименован",
"albumDeleted": "Альбом удалён"
},
"targets": {
"title": "Получатели",
"description": "Адреса уведомлений (Telegram, вебхуки)",
"addTarget": "Добавить получателя",
"cancel": "Отмена",
"type": "Тип",
"name": "Название",
"namePlaceholder": "Мои уведомления",
"botToken": "Токен бота",
"chatId": "ID чата",
"webhookUrl": "URL вебхука",
"create": "Добавить",
"test": "Тест",
"delete": "Удалить",
"confirmDelete": "Удалить этого получателя?",
"noTargets": "Получатели уведомлений не настроены.",
"testSent": "Тестовое уведомление отправлено!",
"aiCaptions": "Включить AI подписи",
"telegramSettings": "Настройки Telegram",
"maxMedia": "Макс. медиафайлов",
"maxGroupSize": "Макс. размер группы",
"chunkDelay": "Задержка между группами (мс)",
"maxAssetSize": "Макс. размер файла (МБ)",
"videoWarning": "Предупреждение о размере видео",
"disableUrlPreview": "Отключить превью ссылок",
"sendLargeAsDocuments": "Отправлять большие фото как документы"
},
"users": {
"title": "Пользователи",
"description": "Управление аккаунтами (только админ)",
"addUser": "Добавить пользователя",
"cancel": "Отмена",
"username": "Имя пользователя",
"password": "Пароль",
"role": "Роль",
"roleUser": "Пользователь",
"roleAdmin": "Администратор",
"create": "Создать",
"delete": "Удалить",
"confirmDelete": "Удалить этого пользователя?",
"joined": "зарегистрирован"
},
"telegramBot": {
"title": "Telegram боты",
"description": "Регистрация и управление Telegram ботами",
"addBot": "Добавить бота",
"name": "Отображаемое имя",
"namePlaceholder": "Бот семейных уведомлений",
"token": "Токен бота",
"tokenPlaceholder": "123456:ABC-DEF...",
"noBots": "Ботов пока нет.",
"chats": "Чаты",
"noChats": "Чатов не найдено. Сначала отправьте сообщение боту.",
"refreshChats": "Обновить",
"selectBot": "Выберите бота",
"selectChat": "Выберите чат",
"private": "Личный",
"group": "Группа",
"supergroup": "Супергруппа",
"channel": "Канал",
"confirmDelete": "Удалить этого бота?",
"commands": "Команды",
"enabledCommands": "Включённые команды",
"defaultCount": "Кол-во результатов",
"responseMode": "Режим ответа",
"modeMedia": "Медиа (отправка фото)",
"modeText": "Текст (ссылки)",
"botLocale": "Язык бота",
"rateLimits": "Ограничения частоты",
"rateSearch": "Кулдаун поиска",
"rateFind": "Кулдаун поиска файлов",
"rateDefault": "Кулдаун по умолчанию",
"syncCommands": "Синхронизировать с Telegram",
"discoverChats": "Обнаружить чаты из Telegram",
"clickToCopy": "Нажмите, чтобы скопировать ID чата",
"chatsDiscovered": "Чаты обнаружены",
"chatDeleted": "Чат удалён"
},
"trackingConfig": {
"title": "Конфигурации отслеживания",
"description": "Определите, на какие события и файлы реагировать",
"newConfig": "Новая конфигурация",
"name": "Название",
"namePlaceholder": "Основное отслеживание",
"noConfigs": "Конфигураций отслеживания пока нет.",
"eventTracking": "Отслеживание событий",
"assetsAdded": "Добавлены файлы",
"assetsRemoved": "Удалены файлы",
"albumRenamed": "Альбом переименован",
"albumDeleted": "Альбом удалён",
"sharingChanged": "Изменение доступа",
"trackImages": "Фото",
"trackVideos": "Видео",
"favoritesOnly": "Только избранные",
"assetDisplay": "Отображение файлов",
"includePeople": "Включать людей",
"includeDetails": "Включать детали",
"maxAssets": "Макс. файлов",
"sortBy": "Сортировка",
"sortOrder": "Порядок",
"periodicSummary": "Периодическая сводка",
"enabled": "Включено",
"intervalDays": "Интервал (дни)",
"startDate": "Дата начала",
"times": "Время (ЧЧ:ММ)",
"scheduledAssets": "Запланированные фото",
"albumMode": "Режим альбомов",
"limit": "Лимит",
"assetType": "Тип файлов",
"minRating": "Мин. рейтинг",
"memoryMode": "Воспоминания (В этот день)",
"test": "Тест",
"confirmDelete": "Удалить эту конфигурацию отслеживания?",
"sortNone": "Нет",
"sortDate": "Дата",
"sortRating": "Рейтинг",
"sortName": "Имя",
"orderDesc": "По убыванию",
"orderAsc": "По возрастанию",
"albumModePerAlbum": "По альбомам",
"albumModeCombined": "Объединённый",
"albumModeRandom": "Случайный",
"assetTypeAll": "Все",
"assetTypePhoto": "Фото",
"assetTypeVideo": "Видео"
},
"templateConfig": {
"title": "Конфигурации шаблонов",
"description": "Определите формат уведомлений",
"newConfig": "Новая конфигурация",
"name": "Название",
"namePlaceholder": "По умолчанию RU",
"descriptionPlaceholder": "напр. Русские шаблоны для семейных уведомлений",
"noConfigs": "Конфигураций шаблонов пока нет.",
"eventMessages": "Сообщения о событиях",
"assetsAdded": "Добавлены файлы",
"assetsRemoved": "Удалены файлы",
"albumRenamed": "Альбом переименован",
"albumDeleted": "Альбом удалён",
"sharingChanged": "Изменение доступа",
"assetFormatting": "Форматирование файлов",
"imageTemplate": "Шаблон фото",
"videoTemplate": "Шаблон видео",
"assetsWrapper": "Обёртка списка",
"moreMessage": "Сообщение \"ещё\"",
"peopleFormat": "Формат людей",
"dateLocation": "Дата и место",
"dateFormat": "Формат даты",
"commonDate": "Общая дата",
"uniqueDate": "Дата файла",
"locationFormat": "Формат места",
"commonLocation": "Общее место",
"uniqueLocation": "Место файла",
"favoriteIndicator": "Индикатор избранного",
"scheduledMessages": "Запланированные сообщения",
"periodicSummary": "Периодическая сводка",
"periodicAlbum": "Элемент альбома",
"scheduledAssets": "Запланированные фото",
"memoryMode": "Воспоминания",
"settings": "Настройки",
"previewAs": "Предпросмотр как",
"preview": "Предпросмотр",
"variables": "Переменные",
"assetFields": "Поля файла (в {% for asset in added_assets %})",
"albumFields": "Поля альбома (в {% for album in albums %})",
"confirmDelete": "Удалить эту конфигурацию шаблона?"
},
"templateVars": {
"message_assets_added": { "description": "Уведомление о добавлении файлов в альбом" },
"message_assets_removed": { "description": "Уведомление об удалении файлов из альбома" },
"message_album_renamed": { "description": "Уведомление о переименовании альбома" },
"message_album_deleted": { "description": "Уведомление об удалении альбома" },
"periodic_summary_message": { "description": "Периодическая сводка альбомов (планировщик не реализован)" },
"scheduled_assets_message": { "description": "Запланированная подборка фото (планировщик не реализован)" },
"memory_mode_message": { "description": "«В этот день» — воспоминания (планировщик не реализован)" },
"album_id": "ID альбома (UUID)",
"album_name": "Название альбома",
"album_url": "Публичная ссылка (пусто, если не расшарен)",
"added_count": "Количество добавленных файлов",
"removed_count": "Количество удалённых файлов",
"change_type": "Тип изменения (assets_added, assets_removed, album_renamed, album_deleted)",
"people": "Обнаруженные люди (список, {{ people | join(', ') }})",
"added_assets": "Список файлов ({% for asset in added_assets %})",
"removed_assets": "Список ID удалённых файлов (строки)",
"shared": "Общий альбом (boolean)",
"target_type": "Тип получателя: 'telegram' или 'webhook'",
"has_videos": "Содержат ли добавленные файлы видео (boolean)",
"has_photos": "Содержат ли добавленные файлы фото (boolean)",
"old_name": "Прежнее название альбома (при переименовании)",
"new_name": "Новое название альбома (при переименовании)",
"old_shared": "Был ли общим до переименования (boolean)",
"new_shared": "Является ли общим после переименования (boolean)",
"albums": "Список альбомов ({% for album in albums %})",
"assets": "Список файлов ({% for asset in assets %})",
"date": "Текущая дата",
"asset_id": "ID файла (UUID)",
"asset_filename": "Имя файла",
"asset_type": "IMAGE или VIDEO",
"asset_created_at": "Дата создания (ISO 8601)",
"asset_owner": "Имя владельца",
"asset_owner_id": "ID владельца",
"asset_description": "Описание (EXIF или пользовательское)",
"asset_people": "Люди на этом файле (список)",
"asset_is_favorite": "В избранном (boolean)",
"asset_rating": "Рейтинг (1-5 или null)",
"asset_latitude": "GPS широта (float или null)",
"asset_longitude": "GPS долгота (float или null)",
"asset_city": "Город",
"asset_state": "Регион",
"asset_country": "Страна",
"asset_url": "Ссылка для просмотра (если расшарен)",
"asset_download_url": "Ссылка для скачивания (если расшарен)",
"asset_photo_url": "URL превью (только фото, если расшарен)",
"asset_playback_url": "URL видео (только видео, если расшарен)",
"album_name_field": "Название альбома (в списке альбомов)",
"album_asset_count": "Всего файлов в альбоме",
"album_url_field": "Ссылка на альбом",
"album_shared": "Общий альбом"
},
"hints": {
"periodicSummary": "Отправляет плановую сводку по всем отслеживаемым альбомам в указанное время. Подходит для ежедневных/еженедельных дайджестов.",
"scheduledAssets": "Отправляет случайные или выбранные фото из альбомов по расписанию. Как ежедневная подборка фото.",
"memoryMode": "\"В этот день\" — отправляет фото, сделанные в этот день в прошлые годы. Ностальгические воспоминания.",
"favoritesOnly": "Включать только ассеты, отмеченные как избранные.",
"maxAssets": "Максимальное количество ассетов в одном уведомлении.",
"periodicStartDate": "Опорная дата для расчёта интервалов. Сводки отправляются каждые N дней от этой даты.",
"times": "Время отправки уведомлений в формате ЧЧ:ММ. Для нескольких значений через запятую: 09:00,18:00",
"albumMode": "По альбому: отдельное уведомление для каждого. Объединённый: одно уведомление со всеми. Случайный: выбирается один альбом.",
"minRating": "Включать только ассеты с рейтингом не ниже указанного (0 = без фильтра).",
"eventMessages": "Шаблоны уведомлений о событиях в реальном времени. Используйте {переменные} для динамического контента.",
"assetFormatting": "Форматирование отдельных ассетов в сообщениях уведомлений.",
"dateLocation": "Форматирование даты и местоположения. Использует синтаксис strftime для дат.",
"scheduledMessages": "Шаблоны для периодических сводок, подборок фото и воспоминаний «В этот день».",
"aiCaptions": "Использовать Claude AI для генерации описания уведомления вместо шаблона.",
"maxMedia": "Максимальное количество фото/видео в одном уведомлении (0 = только текст).",
"groupSize": "Медиагруппы Telegram содержат 2-10 элементов. Большие пакеты разбиваются на части.",
"chunkDelay": "Задержка в миллисекундах между отправкой порций медиа. Предотвращает ограничение Telegram.",
"maxAssetSize": "Пропускать файлы больше указанного размера в МБ. Лимит Telegram — 50 МБ.",
"trackingConfig": "Управляет тем, какие события вызывают уведомления и как фильтруются ассеты.",
"templateConfig": "Управляет форматом сообщений. Используются шаблоны по умолчанию, если не задано.",
"scanInterval": "Как часто опрашивать провайдер на предмет изменений (в секундах). Меньше = быстрее обнаружение, но больше запросов к API.",
"batchDuration": "Время накопления изменений перед отправкой уведомлений. 0 = отправлять сразу.",
"defaultCount": "Сколько результатов возвращать, если пользователь не указал количество (1-20).",
"responseMode": "Медиа: отправка фото. Текст: только имена файлов/ссылки. Медиа-режим использует больше трафика.",
"botLocale": "Язык описаний команд в меню Telegram и ответов бота.",
"rateLimits": "Кулдаун в секундах между использованиями команд в каждом чате. 0 = без ограничений."
},
"snack": {
"providerSaved": "Провайдер сохранён",
"providerDeleted": "Провайдер удалён",
"trackerCreated": "Трекер создан",
"trackerUpdated": "Трекер обновлён",
"trackerDeleted": "Трекер удалён",
"trackerPaused": "Трекер приостановлен",
"trackerResumed": "Трекер возобновлён",
"targetSaved": "Цель сохранена",
"targetDeleted": "Цель удалена",
"targetTestSent": "Тестовое уведомление отправлено",
"templateSaved": "Шаблон сохранён",
"templateDeleted": "Шаблон удалён",
"trackingConfigSaved": "Конфигурация сохранена",
"trackingConfigDeleted": "Конфигурация удалена",
"botRegistered": "Бот зарегистрирован",
"botDeleted": "Бот удалён",
"userCreated": "Пользователь создан",
"userDeleted": "Пользователь удалён",
"passwordChanged": "Пароль изменён",
"copied": "Скопировано",
"genericError": "Что-то пошло не так",
"commandsSaved": "Конфигурация команд сохранена",
"commandsSynced": "Команды синхронизированы с Telegram",
"targetLinked": "Получатель привязан",
"targetUnlinked": "Получатель отвязан",
"botUpdated": "Бот обновлён"
},
"common": {
"loading": "Загрузка...",
"save": "Сохранить",
"cancel": "Отмена",
"delete": "Удалить",
"edit": "Редактировать",
"description": "Описание",
"close": "Закрыть",
"confirm": "Подтвердить",
"error": "Ошибка",
"success": "Успешно",
"none": "Нет",
"noneDefault": "Нет (по умолчанию)",
"loadError": "Не удалось загрузить данные",
"headersInvalid": "Невалидный JSON",
"language": "Язык",
"theme": "Тема",
"light": "Светлая",
"dark": "Тёмная",
"system": "Системная",
"test": "Тест",
"create": "Создать",
"changePassword": "Сменить пароль",
"currentPassword": "Текущий пароль",
"newPassword": "Новый пароль",
"passwordChanged": "Пароль успешно изменён",
"expand": "Развернуть",
"collapse": "Свернуть",
"syntaxError": "Ошибка синтаксиса",
"undefinedVar": "Неизвестная переменная",
"line": "строка",
"add": "Добавить"
}
}