Add failover channel events to activity journal
Build and Deploy Gateway / build-and-push (push) Successful in 24s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-27 14:36:08 +03:00
parent 4a566e082a
commit 8f2f418569
11 changed files with 388 additions and 77 deletions
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { assertActivityJournalPage, type ActivityJournalEvent } from '../../../shared/activityJournal.js';
import { compactActivityJournalEvents, refreshStreakCopy, type ActivityJournalDisplayItem } from './activityJournalModel.js';
import { compactActivityJournalEvents, activityJournalEventCopy, refreshStreakCopy, type ActivityJournalDisplayItem } from './activityJournalModel.js';
import { Drawer } from '../../ui/Drawer.js';
import { RailAction } from '../../ui/RailAction.js';
@@ -56,28 +56,19 @@ export function ActivityJournalToggle({ feature, open, onToggle }: {
</RailAction>;
}
function eventCopy(event: ActivityJournalEvent) {
const value = event.data;
const copies: Record<string, [string, string]> = {
'connection.started': ['VPN включён', [value.profileLabel, value.serverLabel].filter(Boolean).join(' · ')],
'connection.stopped': ['VPN выключен', 'Остановлен пользователем'],
'connection.failed': ['VPN не запущен', String(value.errorCode || '')],
'subscription.added': ['Подписка добавлена', `${value.profileLabel || ''} · серверов: ${value.serverCount || 0}`],
'subscription.refreshed': ['Подписка обновлена', `${value.profileLabel || ''} · серверов: ${value.serverCount || 0} · +${value.added || 0} / ${value.removed || 0}`],
'subscription.refresh_failed': ['Подписка не обновлена', `${value.profileLabel || ''} · ${value.errorCode || ''}`],
'subscription.deleted': ['Подписка удалена', String(value.profileLabel || '')],
'failover.enabled': ['Резервный канал включён', 'Мониторинг начнётся после активации dual-config'],
'failover.disabled': ['Резервный канал выключен', 'Автоматика полностью остановлена'],
'failover.paused': ['Автопереключение на паузе', 'Проверки продолжаются'],
'failover.resumed': ['Автопереключение возобновлено', ''],
'failover.waiting_for_idle': ['Переключение отложено', 'Обнаружен активный трафик'],
'failover.switched': ['Новые соединения переключены', `${value.fromRole || ''}${value.toRole || ''} · ${value.reason || ''}`],
'failover.switch_failed': ['Переключение не выполнено', String(value.errorCode || '')],
'failover.both_unhealthy': ['Оба канала недоступны', 'Текущий маршрут сохранён'],
'failover.recovered': ['Основной канал восстановлен', String(value.reason || '')],
'journal.recovered': ['Журнал восстановлен', 'Повреждённый файл сохранён отдельно'],
};
return copies[event.type] || ['Системное событие', ''];
function eventTargetRole(event: ActivityJournalEvent) {
if (event.type === 'failover.switched') return event.data.toRole;
if (
event.type === 'failover.reserve_unavailable'
|| event.type === 'failover.reserve_recovered'
|| (event.type === 'failover.recovered' && event.data.role === 'reserve')
) return 'reserve';
if (
event.type === 'failover.primary_unavailable'
|| event.type === 'failover.primary_recovered'
|| (event.type === 'failover.recovered' && event.data.role === 'primary')
) return 'primary';
return undefined;
}
function dayLabel(value: string) {
@@ -155,13 +146,13 @@ export function ActivityJournalPanel({ feature, loadPage }: {
<h3>{group.label}</h3>
<ol>{group.items.map((item) => {
const { event } = item;
const [title, details] = refreshStreakCopy(item) || eventCopy(event);
const [title, details] = refreshStreakCopy(item) || activityJournalEventCopy(event);
return <li
key={event.id}
className="client-journal-event"
data-event-type={event.type}
data-severity={event.severity}
data-target-role={event.type === 'failover.switched' ? event.data.toRole : undefined}
data-target-role={eventTargetRole(event)}
>
<div className="client-journal-time"><time dateTime={event.occurredAt}>{new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit' }).format(new Date(event.occurredAt))}</time><span>{event.source}</span></div>
<div><strong>{title}</strong>{details && <span>{details}</span>}{event.severity !== 'info' && <em>{event.severity === 'error' ? 'Ошибка' : 'Внимание'}</em>}</div>