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
@@ -0,0 +1,73 @@
<script lang="ts">
import Modal from './Modal.svelte';
import MdiIcon from './MdiIcon.svelte';
import { t } from '$lib/i18n';
let { open = false, title = '', message = '', confirmLabel = '', confirmIcon = 'mdiDelete', onconfirm, oncancel } = $props<{
open: boolean;
title?: string;
message?: string;
confirmLabel?: string;
confirmIcon?: string;
onconfirm: () => void;
oncancel: () => void;
}>();
</script>
<Modal {open} title={title || t('common.confirm')} onclose={oncancel}>
<div class="flex items-start gap-3 mb-5">
<div class="flex items-center justify-center w-9 h-9 rounded-full flex-shrink-0"
style="background: var(--color-error-bg); color: var(--color-error-fg);">
<MdiIcon name="mdiAlertCircle" size={20} />
</div>
<p class="text-sm mt-1.5" style="color: var(--color-muted-foreground);">{message}</p>
</div>
<div class="flex gap-2 justify-end">
<button onclick={oncancel}
class="confirm-btn-cancel">
{t('common.cancel')}
</button>
<button onclick={onconfirm}
class="confirm-btn-delete">
<MdiIcon name={confirmIcon} size={15} />
{confirmLabel || t('common.delete')}
</button>
</div>
</Modal>
<style>
.confirm-btn-cancel {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
border: 1px solid var(--color-border);
background: transparent;
color: var(--color-foreground);
cursor: pointer;
transition: all 0.2s ease;
}
.confirm-btn-cancel:hover {
background: var(--color-muted);
}
.confirm-btn-delete {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
background: var(--color-destructive);
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.confirm-btn-delete:hover {
box-shadow: 0 0 16px rgba(239, 68, 68, 0.3);
transform: translateY(-1px);
}
</style>
@@ -0,0 +1,162 @@
<script lang="ts">
import { onMount } from 'svelte';
import { EditorView, Decoration, placeholder as cmPlaceholder, type DecorationSet } from '@codemirror/view';
import { EditorState, StateField, StateEffect } from '@codemirror/state';
import { StreamLanguage } from '@codemirror/language';
import { oneDark } from '@codemirror/theme-one-dark';
import { getTheme } from '$lib/theme.svelte';
let { value = '', onchange, rows = 6, placeholder = '', errorLine = null } = $props<{
value: string;
onchange: (val: string) => void;
rows?: number;
placeholder?: string;
errorLine?: number | null;
}>();
let container: HTMLDivElement;
let view: EditorView;
const theme = getTheme();
// Error line highlight effect and field
const setErrorLine = StateEffect.define<number | null>();
const errorLineField = StateField.define<DecorationSet>({
create() { return Decoration.none; },
update(decorations, tr) {
for (const e of tr.effects) {
if (e.is(setErrorLine)) {
if (e.value === null) return Decoration.none;
const lineNum = e.value;
if (lineNum < 1 || lineNum > tr.state.doc.lines) return Decoration.none;
const line = tr.state.doc.line(lineNum);
return Decoration.set([
Decoration.line({ class: 'cm-error-line' }).range(line.from),
]);
}
}
return decorations;
},
provide: f => EditorView.decorations.from(f),
});
// Simple Jinja2 stream parser for syntax highlighting
const jinjaLang = StreamLanguage.define({
token(stream) {
if (stream.match('{#')) {
stream.skipTo('#}') && stream.match('#}');
return 'comment';
}
if (stream.match('{{')) {
while (!stream.eol()) {
if (stream.match('}}')) return 'variableName';
stream.next();
}
return 'variableName';
}
if (stream.match('{%')) {
while (!stream.eol()) {
if (stream.match('%}')) return 'keyword';
stream.next();
}
return 'keyword';
}
while (stream.next()) {
if (stream.peek() === '{') break;
}
return null;
},
});
onMount(() => {
const extensions = [
jinjaLang,
errorLineField,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onchange(update.state.doc.toString());
}
}),
EditorView.lineWrapping,
EditorView.theme({
'&': { fontSize: '13px', fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace" },
'.cm-content': { minHeight: `${rows * 1.5}em`, padding: '8px' },
'.cm-editor': { borderRadius: '0.375rem', border: '1px solid var(--color-border)' },
'.cm-focused': { outline: '2px solid var(--color-primary)', outlineOffset: '0px' },
'.cm-error-line': { backgroundColor: 'rgba(239, 68, 68, 0.2)', outline: '1px solid rgba(239, 68, 68, 0.4)' },
'.ͼc': { color: '#e879f9' },
'.ͼd': { color: '#38bdf8' },
'.ͼ5': { color: '#6b7280' },
}),
];
if (theme.isDark) {
extensions.push(oneDark);
}
if (placeholder) {
extensions.push(cmPlaceholder(placeholder));
}
view = new EditorView({
state: EditorState.create({ doc: value, extensions }),
parent: container,
});
return () => view.destroy();
});
$effect(() => {
if (view && view.state.doc.toString() !== value) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value },
});
}
});
$effect(() => {
if (view) {
view.dispatch({ effects: setErrorLine.of(errorLine ?? null) });
}
});
// Recreate editor when theme changes
let lastIsDark: boolean | undefined;
$effect(() => {
const isDark = theme.isDark;
if (lastIsDark !== undefined && lastIsDark !== isDark && view) {
const currentDoc = view.state.doc.toString();
view.destroy();
const extensions = [
jinjaLang,
errorLineField,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onchange(update.state.doc.toString());
}
}),
EditorView.lineWrapping,
EditorView.theme({
'&': { fontSize: '13px', fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace" },
'.cm-content': { minHeight: `${rows * 1.5}em`, padding: '8px' },
'.cm-editor': { borderRadius: '0.375rem', border: '1px solid var(--color-border)' },
'.cm-focused': { outline: '2px solid var(--color-primary)', outlineOffset: '0px' },
'.cm-error-line': { backgroundColor: 'rgba(239, 68, 68, 0.2)', outline: '1px solid rgba(239, 68, 68, 0.4)' },
'.ͼc': { color: '#e879f9' },
'.ͼd': { color: '#38bdf8' },
'.ͼ5': { color: '#6b7280' },
}),
];
if (isDark) extensions.push(oneDark);
if (placeholder) extensions.push(cmPlaceholder(placeholder));
view = new EditorView({
state: EditorState.create({ doc: currentDoc, extensions }),
parent: container,
});
}
lastIsDark = isDark;
});
</script>
<div bind:this={container}></div>
+448
View File
@@ -0,0 +1,448 @@
{
"app": {
"name": "Notify Bridge",
"tagline": "Service notifications"
},
"nav": {
"dashboard": "Dashboard",
"providers": "Providers",
"trackers": "Trackers",
"trackingConfigs": "Tracking",
"templateConfigs": "Templates",
"telegramBots": "Bots",
"targets": "Targets",
"users": "Users",
"logout": "Logout"
},
"auth": {
"signIn": "Sign in",
"signInTitle": "Sign in to your account",
"signingIn": "Signing in...",
"username": "Username",
"password": "Password",
"confirmPassword": "Confirm password",
"setupTitle": "Welcome",
"setupDescription": "Create your admin account to get started",
"createAccount": "Create account",
"creatingAccount": "Creating account...",
"passwordMismatch": "Passwords do not match",
"passwordTooShort": "Password must be at least 6 characters",
"or": "or"
},
"dashboard": {
"title": "Dashboard",
"description": "Overview of your Notify Bridge setup",
"providers": "Providers",
"activeTrackers": "Active Trackers",
"targets": "Targets",
"recentEvents": "Recent Events",
"noEvents": "No events yet. Create a tracker to start monitoring.",
"loading": "Loading...",
"justNow": "just now",
"minutesAgo": "{n}m ago",
"hoursAgo": "{n}h ago",
"daysAgo": "{n}d ago",
"assetsAdded": "assets added",
"assetsRemoved": "assets removed",
"collectionRenamed": "collection renamed",
"collectionDeleted": "collection deleted",
"sharingChanged": "sharing changed"
},
"providers": {
"title": "Providers",
"description": "Manage service provider connections",
"addProvider": "Add Provider",
"cancel": "Cancel",
"type": "Provider Type",
"name": "Name",
"url": "Provider URL",
"urlPlaceholder": "http://provider:2283",
"apiKey": "API Key",
"apiKeyKeep": "API Key (leave empty to keep current)",
"connecting": "Connecting...",
"noProviders": "No providers configured yet.",
"delete": "Delete",
"confirmDelete": "Delete this provider?",
"online": "Online",
"offline": "Offline",
"checking": "Checking...",
"loadError": "Failed to load providers.",
"externalDomain": "External Domain",
"optional": "optional"
},
"trackers": {
"title": "Trackers",
"description": "Monitor albums for changes",
"newTracker": "New Tracker",
"cancel": "Cancel",
"name": "Name",
"namePlaceholder": "Family photos tracker",
"server": "Provider",
"selectServer": "Select provider...",
"albums": "Albums",
"eventTypes": "Event Types",
"notificationTargets": "Notification Targets",
"scanInterval": "Scan Interval (seconds)",
"createTracker": "Create Tracker",
"noTrackers": "No trackers yet. Add a provider first, then create a tracker.",
"active": "Active",
"paused": "Paused",
"pause": "Pause",
"resume": "Resume",
"delete": "Delete",
"confirmDelete": "Delete this tracker?",
"albums_count": "album(s)",
"every": "every",
"trackImages": "Track images",
"trackVideos": "Track videos",
"favoritesOnly": "Favorites only",
"includePeople": "Include people in notifications",
"includeAssetDetails": "Include asset details",
"maxAssetsToShow": "Max assets to show",
"sortBy": "Sort by",
"sortOrder": "Sort order",
"sortNone": "Original order",
"sortDate": "Date",
"sortRating": "Rating",
"sortName": "Name",
"sortRandom": "Random",
"ascending": "Ascending",
"descending": "Descending",
"quietHoursStart": "Quiet hours start",
"quietHoursEnd": "Quiet hours end",
"batchDuration": "Batch duration (seconds)",
"linkedTargets": "targets",
"noLinkedTargets": "No targets linked. Add a target below.",
"addTarget": "Add target"
},
"templates": {
"title": "Templates",
"description": "Jinja2 message templates for notifications",
"newTemplate": "New Template",
"cancel": "Cancel",
"name": "Name",
"body": "Template Body (Jinja2)",
"variables": "Variables",
"preview": "Preview",
"edit": "Edit",
"delete": "Delete",
"confirmDelete": "Delete this template?",
"create": "Create Template",
"update": "Update Template",
"noTemplates": "No templates yet. A default template will be used if none is configured.",
"eventType": "Event type",
"allEvents": "All events",
"assetsAdded": "Assets added",
"assetsRemoved": "Assets removed",
"albumRenamed": "Album renamed",
"albumDeleted": "Album deleted"
},
"targets": {
"title": "Targets",
"description": "Notification destinations (Telegram, webhooks)",
"addTarget": "Add Target",
"cancel": "Cancel",
"type": "Type",
"name": "Name",
"namePlaceholder": "My notifications",
"botToken": "Bot Token",
"chatId": "Chat ID",
"webhookUrl": "Webhook URL",
"create": "Add Target",
"test": "Test",
"delete": "Delete",
"confirmDelete": "Delete this target?",
"noTargets": "No notification targets configured yet.",
"testSent": "Test sent successfully!",
"aiCaptions": "Enable AI captions",
"telegramSettings": "Telegram Settings",
"maxMedia": "Max media to send",
"maxGroupSize": "Max group size",
"chunkDelay": "Delay between groups (ms)",
"maxAssetSize": "Max asset size (MB)",
"videoWarning": "Video size warning",
"disableUrlPreview": "Disable link previews",
"sendLargeAsDocuments": "Send large photos as documents"
},
"users": {
"title": "Users",
"description": "Manage user accounts (admin only)",
"addUser": "Add User",
"cancel": "Cancel",
"username": "Username",
"password": "Password",
"role": "Role",
"roleUser": "User",
"roleAdmin": "Admin",
"create": "Create User",
"delete": "Delete",
"confirmDelete": "Delete this user?",
"joined": "joined"
},
"telegramBot": {
"title": "Telegram Bots",
"description": "Register and manage Telegram bots",
"addBot": "Add Bot",
"name": "Display name",
"namePlaceholder": "Family notifications bot",
"token": "Bot Token",
"tokenPlaceholder": "123456:ABC-DEF...",
"noBots": "No bots registered yet.",
"chats": "Chats",
"noChats": "No chats found. Send a message to the bot first.",
"refreshChats": "Refresh",
"selectBot": "Select bot",
"selectChat": "Select chat",
"private": "Private",
"group": "Group",
"supergroup": "Supergroup",
"channel": "Channel",
"confirmDelete": "Delete this bot?",
"commands": "Commands",
"enabledCommands": "Enabled Commands",
"defaultCount": "Default result count",
"responseMode": "Response mode",
"modeMedia": "Media (send photos)",
"modeText": "Text (send links)",
"botLocale": "Bot language",
"rateLimits": "Rate Limits",
"rateSearch": "Search cooldown",
"rateFind": "Find cooldown",
"rateDefault": "Default cooldown",
"syncCommands": "Sync to Telegram",
"discoverChats": "Discover chats from Telegram",
"clickToCopy": "Click to copy chat ID",
"chatsDiscovered": "Chats discovered",
"chatDeleted": "Chat removed"
},
"trackingConfig": {
"title": "Tracking Configs",
"description": "Define what events and assets to react to",
"newConfig": "New Config",
"name": "Name",
"namePlaceholder": "Default tracking",
"noConfigs": "No tracking configs yet.",
"eventTracking": "Event Tracking",
"assetsAdded": "Assets added",
"assetsRemoved": "Assets removed",
"albumRenamed": "Album renamed",
"albumDeleted": "Album deleted",
"sharingChanged": "Sharing changed",
"trackImages": "Track images",
"trackVideos": "Track videos",
"favoritesOnly": "Favorites only",
"assetDisplay": "Asset Display",
"includePeople": "Include people",
"includeDetails": "Include asset details",
"maxAssets": "Max assets to show",
"sortBy": "Sort by",
"sortOrder": "Sort order",
"periodicSummary": "Periodic Summary",
"enabled": "Enabled",
"intervalDays": "Interval (days)",
"startDate": "Start date",
"times": "Times (HH:MM)",
"scheduledAssets": "Scheduled Assets",
"albumMode": "Album mode",
"limit": "Limit",
"assetType": "Asset type",
"minRating": "Min rating",
"memoryMode": "Memory Mode (On This Day)",
"test": "Test",
"confirmDelete": "Delete this tracking config?",
"sortNone": "None",
"sortDate": "Date",
"sortRating": "Rating",
"sortName": "Name",
"orderDesc": "Descending",
"orderAsc": "Ascending",
"albumModePerAlbum": "Per album",
"albumModeCombined": "Combined",
"albumModeRandom": "Random",
"assetTypeAll": "All",
"assetTypePhoto": "Photo",
"assetTypeVideo": "Video"
},
"templateConfig": {
"title": "Template Configs",
"description": "Define how notification messages are formatted",
"newConfig": "New Config",
"name": "Name",
"namePlaceholder": "Default EN",
"descriptionPlaceholder": "e.g. English templates for family notifications",
"noConfigs": "No template configs yet.",
"eventMessages": "Event Messages",
"assetsAdded": "Assets added",
"assetsRemoved": "Assets removed",
"albumRenamed": "Album renamed",
"albumDeleted": "Album deleted",
"sharingChanged": "Sharing changed",
"assetFormatting": "Asset Formatting",
"imageTemplate": "Image item",
"videoTemplate": "Video item",
"assetsWrapper": "Assets wrapper",
"moreMessage": "More message",
"peopleFormat": "People format",
"dateLocation": "Date & Location",
"dateFormat": "Date format",
"commonDate": "Common date",
"uniqueDate": "Per-asset date",
"locationFormat": "Location format",
"commonLocation": "Common location",
"uniqueLocation": "Per-asset location",
"favoriteIndicator": "Favorite indicator",
"scheduledMessages": "Scheduled Messages",
"periodicSummary": "Periodic summary",
"periodicAlbum": "Per-album item",
"scheduledAssets": "Scheduled assets",
"memoryMode": "Memory mode",
"settings": "Settings",
"previewAs": "Preview as",
"preview": "Preview",
"variables": "Variables",
"assetFields": "Asset fields (in {% for asset in added_assets %})",
"albumFields": "Album fields (in {% for album in albums %})",
"confirmDelete": "Delete this template config?"
},
"templateVars": {
"message_assets_added": { "description": "Notification when new assets are added to an album" },
"message_assets_removed": { "description": "Notification when assets are removed from an album" },
"message_album_renamed": { "description": "Notification when an album is renamed" },
"message_album_deleted": { "description": "Notification when an album is deleted" },
"periodic_summary_message": { "description": "Periodic album summary (scheduler not yet implemented)" },
"scheduled_assets_message": { "description": "Scheduled asset delivery (scheduler not yet implemented)" },
"memory_mode_message": { "description": "\"On This Day\" memories (scheduler not yet implemented)" },
"album_id": "Album ID (UUID)",
"album_name": "Album name",
"album_url": "Public share URL (empty if not shared)",
"added_count": "Number of assets added",
"removed_count": "Number of assets removed",
"change_type": "Type of change (assets_added, assets_removed, album_renamed, album_deleted)",
"people": "Detected people names (list, use {{ people | join(', ') }})",
"added_assets": "List of asset dicts (use {% for asset in added_assets %})",
"removed_assets": "List of removed asset IDs (strings)",
"shared": "Whether album is shared (boolean)",
"target_type": "Target type: 'telegram' or 'webhook'",
"has_videos": "Whether added assets contain videos (boolean)",
"has_photos": "Whether added assets contain photos (boolean)",
"old_name": "Previous album name (rename events)",
"new_name": "New album name (rename events)",
"old_shared": "Was album shared before rename (boolean)",
"new_shared": "Is album shared after rename (boolean)",
"albums": "List of album dicts (use {% for album in albums %})",
"assets": "List of asset dicts (use {% for asset in assets %})",
"date": "Current date string",
"asset_id": "Asset ID (UUID)",
"asset_filename": "Original filename",
"asset_type": "IMAGE or VIDEO",
"asset_created_at": "Creation date/time (ISO 8601)",
"asset_owner": "Owner display name",
"asset_owner_id": "Owner user ID",
"asset_description": "User or EXIF description",
"asset_people": "People detected in this asset (list)",
"asset_is_favorite": "Whether asset is favorited (boolean)",
"asset_rating": "Star rating (1-5 or null)",
"asset_latitude": "GPS latitude (float or null)",
"asset_longitude": "GPS longitude (float or null)",
"asset_city": "City name",
"asset_state": "State/region name",
"asset_country": "Country name",
"asset_url": "Public viewer URL (if shared)",
"asset_download_url": "Direct download URL (if shared)",
"asset_photo_url": "Preview image URL (images only, if shared)",
"asset_playback_url": "Video playback URL (videos only, if shared)",
"album_name_field": "Album name (in album list)",
"album_asset_count": "Total assets in album",
"album_url_field": "Album share URL",
"album_shared": "Whether album is shared"
},
"hints": {
"periodicSummary": "Sends a scheduled summary of all tracked albums at specified times. Great for daily/weekly digests.",
"scheduledAssets": "Sends random or selected photos from tracked albums on a schedule. Like a daily photo pick.",
"memoryMode": "\"On This Day\" — sends photos taken on this date in previous years. Nostalgic flashbacks.",
"favoritesOnly": "Only include assets marked as favorites.",
"maxAssets": "Maximum number of asset details to include in a single notification message.",
"periodicStartDate": "The reference date for calculating periodic intervals. Summaries are sent every N days from this date.",
"times": "Time(s) of day to send notifications, in HH:MM format. Use commas for multiple times: 09:00,18:00",
"albumMode": "Per album: separate notification per album. Combined: one notification with all albums. Random: pick one album randomly.",
"minRating": "Only include assets with at least this star rating (0 = no filter).",
"eventMessages": "Templates for real-time event notifications. Use {variables} for dynamic content.",
"assetFormatting": "How individual assets are formatted within notification messages.",
"dateLocation": "Date and location formatting in notifications. Uses strftime syntax for dates.",
"scheduledMessages": "Templates for periodic summaries, scheduled photo picks, and On This Day memories.",
"aiCaptions": "Use Claude AI to generate a natural-language caption for notifications instead of the template.",
"maxMedia": "Maximum number of photos/videos to attach per notification (0 = text only).",
"groupSize": "Telegram media groups can contain 2-10 items. Larger batches are split into chunks.",
"chunkDelay": "Delay in milliseconds between sending media chunks. Prevents Telegram rate limiting.",
"maxAssetSize": "Skip assets larger than this size in MB. Telegram limits files to 50 MB.",
"trackingConfig": "Controls which events trigger notifications and how assets are filtered.",
"templateConfig": "Controls the message format. Uses default templates if not set.",
"scanInterval": "How often to poll the provider for changes, in seconds. Lower = faster detection but more API calls.",
"batchDuration": "Time to accumulate changes before dispatching notifications. 0 = send immediately.",
"defaultCount": "How many results to return when the user doesn't specify a count (1-20).",
"responseMode": "Media: send actual photos. Text: send filenames/links only. Media mode uses more bandwidth.",
"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."
},
"snack": {
"providerSaved": "Provider saved",
"providerDeleted": "Provider deleted",
"trackerCreated": "Tracker created",
"trackerUpdated": "Tracker updated",
"trackerDeleted": "Tracker deleted",
"trackerPaused": "Tracker paused",
"trackerResumed": "Tracker resumed",
"targetSaved": "Target saved",
"targetDeleted": "Target deleted",
"targetTestSent": "Test notification sent",
"templateSaved": "Template config saved",
"templateDeleted": "Template config deleted",
"trackingConfigSaved": "Tracking config saved",
"trackingConfigDeleted": "Tracking config deleted",
"botRegistered": "Bot registered",
"botDeleted": "Bot deleted",
"userCreated": "User created",
"userDeleted": "User deleted",
"passwordChanged": "Password changed",
"copied": "Copied to clipboard",
"genericError": "Something went wrong",
"commandsSaved": "Commands config saved",
"commandsSynced": "Commands synced to Telegram",
"targetLinked": "Target linked",
"targetUnlinked": "Target unlinked",
"botUpdated": "Bot updated"
},
"common": {
"loading": "Loading...",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"description": "Description",
"close": "Close",
"confirm": "Confirm",
"error": "Error",
"success": "Success",
"none": "None",
"noneDefault": "None (default)",
"loadError": "Failed to load data",
"headersInvalid": "Invalid JSON",
"language": "Language",
"theme": "Theme",
"light": "Light",
"dark": "Dark",
"system": "System",
"test": "Test",
"create": "Create",
"changePassword": "Change Password",
"currentPassword": "Current password",
"newPassword": "New password",
"passwordChanged": "Password changed successfully",
"expand": "Expand",
"collapse": "Collapse",
"syntaxError": "Syntax error",
"undefinedVar": "Unknown variable",
"line": "line",
"add": "Add"
}
}
+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": "Добавить"
}
}