diff --git a/frontend/src/lib/components/EventDetailModal.svelte b/frontend/src/lib/components/EventDetailModal.svelte new file mode 100644 index 0000000..3cb8778 --- /dev/null +++ b/frontend/src/lib/components/EventDetailModal.svelte @@ -0,0 +1,254 @@ + + + + {#if event} +
+ +
+ +
+
{event.collection_name || event.event_type}
+
+ {event.event_type} + · + {fmtDateTime(event.created_at)} +
+
+
+ + +
+ {#if event.bot_name} +
{t('events.bot')}
+
{event.bot_name}
+ {/if} + {#if event.collection_id && isCommand} +
{t('events.chat')}
+
{event.collection_id}
+ {/if} + {#if issuerText} +
{t('events.issuer')}
+
+ {issuerText} + {#if issuer?.id}(id {issuer.id}){/if} +
+ {/if} + {#if event.command_tracker_name} +
{t('events.commandTracker')}
+
{event.command_tracker_name}
+ {/if} + {#if event.tracker_name} +
{t('events.tracker')}
+
{event.tracker_name}
+ {/if} + {#if event.action_name} +
{t('events.action')}
+
{event.action_name}
+ {/if} + {#if event.provider_name} +
{t('events.provider')}
+
{event.provider_name}
+ {/if} + {#if event.assets_count > 0} +
{t('events.assetsCount')}
+
{event.assets_count}
+ {/if} +
+ + +
+ {#if event.provider_id} + + {/if} + {#if event.telegram_bot_id && isCommand} + + {/if} + {#if event.command_tracker_id && isCommand} + + {/if} + {#if event.action_id && isAction} + + {/if} + {#if !isCommand && !isAction && event.tracker_id} + + {/if} +
+ + + {#if detailsJson && detailsJson !== '{}'} +
+ {t('events.rawDetails')} +
{detailsJson}
+
+ {/if} +
+ {/if} +
+ + diff --git a/frontend/src/lib/grid-items.ts b/frontend/src/lib/grid-items.ts index 0102a96..86d65dc 100644 --- a/frontend/src/lib/grid-items.ts +++ b/frontend/src/lib/grid-items.ts @@ -108,6 +108,9 @@ export const eventTypeFilterItems = (): GridItem[] => [ { value: 'action_success', icon: 'mdiPlayCircle', label: t('dashboard.filterActionSuccess'), desc: t('gridDesc.actionSuccess') }, { value: 'action_partial', icon: 'mdiAlertCircle', label: t('dashboard.filterActionPartial'), desc: t('gridDesc.actionPartial') }, { value: 'action_failed', icon: 'mdiCloseCircle', label: t('dashboard.filterActionFailed'), desc: t('gridDesc.actionFailed') }, + { value: 'command_handled', icon: 'mdiChat', label: t('dashboard.filterCommandHandled'), desc: t('gridDesc.commandHandled') }, + { value: 'command_rate_limited', icon: 'mdiTimerSandPaused', label: t('dashboard.filterCommandRateLimited'), desc: t('gridDesc.commandRateLimited') }, + { value: 'command_failed', icon: 'mdiAlertCircle', label: t('dashboard.filterCommandFailed'), desc: t('gridDesc.commandFailed') }, ]; // --- Sort filter (dashboard) --- @@ -117,6 +120,19 @@ export const sortFilterItems = (): GridItem[] => [ { value: 'oldest', icon: 'mdiSortClockAscending', label: t('dashboard.oldestFirst'), desc: t('gridDesc.oldestFirst') }, ]; +// --- Auto-refresh interval (dashboard events list) --- +// +// Values are seconds (0 = off). Keep these in sync with REFRESH_OPTIONS +// in routes/+page.svelte if you add or remove cadences. + +export const refreshIntervalItems = (): GridItem[] => [ + { value: 0, icon: 'mdiPause', label: t('dashboard.refreshOff'), desc: t('gridDesc.refreshOff') }, + { value: 10, icon: 'mdiTimerSand', label: t('dashboard.refresh10s'), desc: t('gridDesc.refresh10s') }, + { value: 30, icon: 'mdiTimerOutline', label: t('dashboard.refresh30s'), desc: t('gridDesc.refresh30s') }, + { value: 60, icon: 'mdiTimer', label: t('dashboard.refresh60s'), desc: t('gridDesc.refresh60s') }, + { value: 300, icon: 'mdiClockOutline', label: t('dashboard.refresh5m'), desc: t('gridDesc.refresh5m') }, +]; + // --- Chat action (Telegram targets) --- export const chatActionItems = (): GridItem[] => [ diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 2bf0630..097af6a 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -87,6 +87,15 @@ "actionSuccess": "action run", "actionPartial": "action partial", "actionFailed": "action failed", + "commandHandled": "command handled", + "commandRateLimited": "rate limited", + "commandFailed": "command failed", + "autoRefreshTitle": "Auto-refresh interval for the events list", + "refreshOff": "Off", + "refresh10s": "10s", + "refresh30s": "30s", + "refresh60s": "1m", + "refresh5m": "5m", "searchEvents": "Search events...", "allEvents": "All Events", "filterAssetsAdded": "Assets Added", @@ -97,6 +106,9 @@ "filterActionSuccess": "Action Success", "filterActionPartial": "Action Partial", "filterActionFailed": "Action Failed", + "filterCommandHandled": "Command Handled", + "filterCommandRateLimited": "Rate Limited", + "filterCommandFailed": "Command Failed", "allProviders": "All Providers", "newestFirst": "Newest first", "oldestFirst": "Oldest first", @@ -141,6 +153,23 @@ "newTracker": "New tracker", "eventsTotal": "Events" }, + "events": { + "detailTitle": "Event details", + "bot": "Bot", + "chat": "Chat", + "issuer": "Issued by", + "commandTracker": "Command tracker", + "tracker": "Tracker", + "action": "Action", + "provider": "Provider", + "assetsCount": "Assets", + "openProvider": "Open provider", + "openBot": "Open bot", + "openCommandTracker": "Open command tracker", + "openAction": "Open action", + "openTracker": "Open tracker", + "rawDetails": "Raw details" + }, "providers": { "title": "Service", "titleEmphasis": "providers", @@ -889,6 +918,7 @@ "titleEmphasis": "configs", "countLabel": "configs", "title": "Command Configs", + "noCommandsForProvider": "No commands available for this provider type.", "description": "Define command settings for Telegram bot interactions", "newConfig": "New Config", "name": "Name", @@ -1025,6 +1055,8 @@ "edit": "Edit", "description": "Description", "close": "Close", + "hide": "Hide", + "show": "Show", "confirm": "Confirm", "cannotDelete": "Cannot delete", "blockedByIntro": "Referenced by:", @@ -1149,6 +1181,14 @@ "actionSuccess": "Scheduled action completed", "actionPartial": "Scheduled action partially succeeded", "actionFailed": "Scheduled action failed", + "commandHandled": "Bot command served", + "commandRateLimited": "Bot command throttled", + "commandFailed": "Bot command crashed", + "refreshOff": "Auto-refresh disabled", + "refresh10s": "Refresh every 10 seconds", + "refresh30s": "Refresh every 30 seconds", + "refresh60s": "Refresh every minute", + "refresh5m": "Refresh every 5 minutes", "newestFirst": "Most recent events on top", "oldestFirst": "Oldest events on top", "chatActionNone": "No indicator shown", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index a569a6c..ed1d5c3 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -87,6 +87,15 @@ "actionSuccess": "действие выполнено", "actionPartial": "действие частично", "actionFailed": "действие провалено", + "commandHandled": "команда обработана", + "commandRateLimited": "ограничение частоты", + "commandFailed": "команда упала", + "autoRefreshTitle": "Интервал авто-обновления списка событий", + "refreshOff": "Выкл", + "refresh10s": "10с", + "refresh30s": "30с", + "refresh60s": "1м", + "refresh5m": "5м", "searchEvents": "Поиск событий...", "allEvents": "Все события", "filterAssetsAdded": "Добавление файлов", @@ -97,6 +106,9 @@ "filterActionSuccess": "Действие выполнено", "filterActionPartial": "Действие частично", "filterActionFailed": "Действие провалено", + "filterCommandHandled": "Команда обработана", + "filterCommandRateLimited": "Ограничение частоты", + "filterCommandFailed": "Команда упала", "allProviders": "Все провайдеры", "newestFirst": "Сначала новые", "oldestFirst": "Сначала старые", @@ -141,6 +153,23 @@ "newTracker": "Новый трекер", "eventsTotal": "Событий" }, + "events": { + "detailTitle": "Детали события", + "bot": "Бот", + "chat": "Чат", + "issuer": "Отправитель", + "commandTracker": "Командный трекер", + "tracker": "Трекер", + "action": "Действие", + "provider": "Провайдер", + "assetsCount": "Файлов", + "openProvider": "Открыть провайдера", + "openBot": "Открыть бота", + "openCommandTracker": "Открыть командный трекер", + "openAction": "Открыть действие", + "openTracker": "Открыть трекер", + "rawDetails": "Сырые данные" + }, "providers": { "title": "Сервисные", "titleEmphasis": "провайдеры", @@ -889,6 +918,7 @@ "titleEmphasis": "конфигурации", "countLabel": "конфигураций", "title": "Конфигурации команд", + "noCommandsForProvider": "Для этого типа провайдера нет доступных команд.", "description": "Настройки команд для взаимодействия с Telegram-ботами", "newConfig": "Новая конфигурация", "name": "Название", @@ -1025,6 +1055,8 @@ "edit": "Редактировать", "description": "Описание", "close": "Закрыть", + "hide": "Скрыть", + "show": "Показать", "confirm": "Подтвердить", "cannotDelete": "Невозможно удалить", "blockedByIntro": "На объект ссылаются:", @@ -1149,6 +1181,14 @@ "actionSuccess": "Запланированное действие выполнено", "actionPartial": "Запланированное действие выполнено частично", "actionFailed": "Запланированное действие провалено", + "commandHandled": "Команда бота обработана", + "commandRateLimited": "Команда бота ограничена по частоте", + "commandFailed": "Команда бота вызвала ошибку", + "refreshOff": "Автообновление выключено", + "refresh10s": "Обновлять каждые 10 секунд", + "refresh30s": "Обновлять каждые 30 секунд", + "refresh60s": "Обновлять каждую минуту", + "refresh5m": "Обновлять каждые 5 минут", "newestFirst": "Сначала новые события", "oldestFirst": "Сначала старые события", "chatActionNone": "Индикатор не показывается", diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 2edf024..4417af3 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -217,9 +217,16 @@ export interface EventLog { event_type: string; collection_id: string; collection_name: string; + tracker_id?: number | null; tracker_name: string; provider_name: string; provider_id: number | null; + action_id?: number | null; + action_name?: string; + command_tracker_id?: number | null; + command_tracker_name?: string; + telegram_bot_id?: number | null; + bot_name?: string; assets_count: number; details: Record; created_at: string; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index b09daf0..7fc8fe4 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -16,12 +16,13 @@ import EventChart from '$lib/components/EventChart.svelte'; import IconGridSelect from '$lib/components/IconGridSelect.svelte'; import ConfirmModal from '$lib/components/ConfirmModal.svelte'; + import EventDetailModal from '$lib/components/EventDetailModal.svelte'; import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte'; - import { eventTypeFilterItems, sortFilterItems, providerDefaultIcon } from '$lib/grid-items'; + import { eventTypeFilterItems, refreshIntervalItems, sortFilterItems, providerDefaultIcon } from '$lib/grid-items'; import { globalProviderFilter } from '$lib/stores/provider-filter.svelte'; import { getDescriptor } from '$lib/providers'; - import type { DashboardStatus } from '$lib/types'; + import type { DashboardStatus, EventLog } from '$lib/types'; const SECTIONS_KEY = 'dashboard_section_state'; type SectionKey = 'stream' | 'on_watch' | 'pulse' | 'wires'; @@ -75,10 +76,53 @@ return stored ? parseInt(stored, 10) || 10 : 10; } + // Auto-refresh: 0 = off, otherwise seconds between refreshes. + // Allowed cadences are defined in ``refreshIntervalItems()`` — keep + // this whitelist in sync with that helper so a stale localStorage + // value can't smuggle in an unsupported interval (e.g. someone + // hand-edits to 1). + const EVENTS_REFRESH_KEY = 'dashboard_events_refresh_seconds'; + const ALLOWED_REFRESH_SECONDS = new Set([0, 10, 30, 60, 300]); + function loadRefreshSeconds(): number { + if (typeof localStorage === 'undefined') return 0; + const stored = localStorage.getItem(EVENTS_REFRESH_KEY); + const v = stored ? parseInt(stored, 10) : 0; + return ALLOWED_REFRESH_SECONDS.has(v) ? v : 0; + } + let eventsLimit = $state(loadEventsPerPage()); let eventsOffset = $state(0); let eventsLoading = $state(false); let confirmClearEvents = $state(false); + let refreshSeconds = $state(loadRefreshSeconds()); + let selectedEvent = $state(null); + + // Auto-refresh ticker — re-creates the interval whenever the user + // changes the cadence. ``$effect`` returns a cleanup that fires on + // destroy AND on any tracked dep change, so the prior timer is torn + // down before a new one starts. + $effect(() => { + if (refreshSeconds <= 0) return; + // Pause auto-refresh when the tab is hidden so we don't burn API + // calls on a tab the user can't see — we'll catch up on the next + // visibility flip via ``visibilitychange`` below. + const tick = () => { + if (typeof document !== 'undefined' && document.hidden) return; + loadEvents(); + loadChart(); + }; + const handle = setInterval(tick, refreshSeconds * 1000); + return () => clearInterval(handle); + }); + + // Persist whenever the cadence changes (the IconGridSelect mutates + // ``refreshSeconds`` directly via bind:value). + let _refreshHydrated = false; + $effect(() => { + const v = refreshSeconds; + if (!_refreshHydrated) { _refreshHydrated = true; return; } + if (typeof localStorage !== 'undefined') localStorage.setItem(EVENTS_REFRESH_KEY, String(v)); + }); async function clearEvents() { try { @@ -360,6 +404,9 @@ action_success: 'dashboard.actionSuccess', action_partial: 'dashboard.actionPartial', action_failed: 'dashboard.actionFailed', + command_handled: 'dashboard.commandHandled', + command_rate_limited: 'dashboard.commandRateLimited', + command_failed: 'dashboard.commandFailed', }; const eventIcons: Record = { @@ -367,6 +414,7 @@ collection_renamed: 'mdiRename', collection_deleted: 'mdiDeleteAlert', sharing_changed: 'mdiShareVariant', scheduled_message: 'mdiCalendarClock', action_success: 'mdiPlayCircle', action_partial: 'mdiAlertCircle', action_failed: 'mdiCloseCircle', + command_handled: 'mdiChat', command_rate_limited: 'mdiTimerSandPaused', command_failed: 'mdiAlertCircle', }; // Aurora gradient palette per event type — used for the avatar tile @@ -380,6 +428,9 @@ action_success: ['var(--color-mint)', 'var(--color-primary)'], action_partial: ['var(--color-citrus)', 'var(--color-orchid)'], action_failed: ['var(--color-coral)', 'var(--color-orchid)'], + command_handled: ['var(--color-sky)', 'var(--color-primary)'], + command_rate_limited:['var(--color-citrus)', 'var(--color-orchid)'], + command_failed: ['var(--color-coral)', 'var(--color-orchid)'], }; const STAT_ACCENTS = [ @@ -554,6 +605,11 @@
{#if !globalProviderFilter.id}
{/if}
+
+ +
{#snippet paginator()} @@ -598,7 +654,10 @@ {:else}
{#each status.recent_events as event, i} -
+
+ {/each}
@@ -790,6 +871,8 @@ confirmClearEvents = false} /> + selectedEvent = null} /> +