feat(observability): event-triggers + log-scan-rules UI + i18n
Operator-facing surfaces for the two backend features:
- /event-triggers — list (filter summary, status pill),
/event-triggers/new (form with regex validation), and
/event-triggers/[id] (edit + Send-test + delete) with
CONFIGURED secret badge + clear-to-rotate flow, ConfirmDialog
for delete, aria-live regions on async result slots.
- /log-scan-rules — list with scope filter chips and stats panel
(active tails, RATE-LIMITED, COOLED DOWN, COMPILE ERRORS),
/log-scan-rules/new (with EntityPicker for workload scope and
inline RegexTestBox), /log-scan-rules/[id] (edit + server-side
/test + delete + live RegexTestBox panel).
- web/src/lib/components/RegexTestBox.svelte — reusable
client-side regex test with sample input + captures display.
- web/src/lib/api.ts — typed wrappers for EventTrigger and
LogScanRule CRUD + /test + getLogScanStats +
getEffectiveLogScanRules.
- web/src/routes/+layout.svelte — nav entries for both surfaces.
- web/src/lib/i18n/{en,ru}.json — ~90 keys under observability.*,
triggers.*, logscan.* namespaces; Russian translations cover
every key.
Design + a11y polish per a frontend-design review pass: all
boolean inputs use ToggleSwitch, all destructive actions use
ConfirmDialog with confirmVariant="danger" / onconfirm /
oncancel, hand-rolled .btn-primary replaced with global
forge-btn classes, hex colors replaced with var(--*) tokens,
role="alert" on error banners, aria-invalid + aria-describedby
on invalid-regex inputs, aria-busy on async forms, mobile
breakpoints (hide-md columns, .row.three collapsing 3→2→1,
.table-wrap overflow-x).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,9 @@
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Панель",
|
||||
"apps": "Приложения",
|
||||
"eventTriggers": "Триггеры",
|
||||
"logScanRules": "Лог-правила",
|
||||
"projects": "Проекты",
|
||||
"deploy": "Деплой",
|
||||
"proxies": "Прокси",
|
||||
@@ -1301,5 +1304,228 @@
|
||||
"thresholds": "Пороги",
|
||||
"thresholdsDesc": "Настройте, когда Tinyforge помечает контейнеры как устаревшие и предупреждает о неиспользуемых образах.",
|
||||
"dangerZone": "Опасная зона"
|
||||
},
|
||||
"observability": {
|
||||
"section": "Наблюдаемость",
|
||||
"manage": "управление",
|
||||
"loading": "Загрузка…",
|
||||
"anyEvent": "любое событие",
|
||||
"noUrlSet": "URL не настроен",
|
||||
"configured": "НАСТРОЕН",
|
||||
"clear": "Очистить",
|
||||
"advanced": "Расширенно",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение…",
|
||||
"delete": "Удалить",
|
||||
"deleting": "Удаление…",
|
||||
"refresh": "Обновить",
|
||||
"open": "Открыть",
|
||||
"edit": "Изменить",
|
||||
"back": "Назад",
|
||||
"regex": {
|
||||
"sampleLabel": "Пример строки",
|
||||
"placeholder": "вставьте сюда характерную строку лога",
|
||||
"promptType": "введите образец для проверки шаблона",
|
||||
"noMatch": "НЕТ СОВПАДЕНИЯ",
|
||||
"noMatchHint": "шаблон не совпал с этой строкой",
|
||||
"match": "СОВПАЛО",
|
||||
"invalid": "REGEX",
|
||||
"captures": "Группы"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"title": "Триггеры событий",
|
||||
"titleNew": "Новый триггер",
|
||||
"titleSingular": "Триггер",
|
||||
"lede": "Фильтруйте записи журнала событий (события деплоев, вывод сканера логов, будущие источники) и отправляйте webhook при совпадении. Фильтры объединяются по И; пустой фильтр означает «совпадает всё».",
|
||||
"ledeNew": "Создайте правило «фильтр + действие». Диспетчер объединяет фильтры по И. Оставьте поле пустым, чтобы пропустить это измерение.",
|
||||
"stat": {
|
||||
"total": "ВСЕГО",
|
||||
"enabled": "ВКЛЮЧЕНО",
|
||||
"disabled": "ВЫКЛЮЧЕНО"
|
||||
},
|
||||
"toolbar": {
|
||||
"newButton": "Новый триггер",
|
||||
"backToList": "К списку триггеров"
|
||||
},
|
||||
"empty": {
|
||||
"heading": "Триггеров пока нет",
|
||||
"body": "Настройте триггер, чтобы пересылать записи журнала событий в Slack, мост уведомлений или любой HTTP-приёмник. Tinyforge подписывает запросы заголовком X-Hub-Signature-256, если задан секрет.",
|
||||
"cta": "Создать первый триггер"
|
||||
},
|
||||
"list": {
|
||||
"name": "Имя",
|
||||
"filters": "Фильтры",
|
||||
"action": "Действие",
|
||||
"status": "Статус",
|
||||
"open": "Открыть"
|
||||
},
|
||||
"detail": {
|
||||
"config": "Конфигурация",
|
||||
"configSub": "id #{id} · обновлено {updatedAt}",
|
||||
"dangerZone": "Опасная зона",
|
||||
"dangerZoneSub": "Удаление триггера происходит сразу. Восстановления нет.",
|
||||
"sendTest": "Отправить тест",
|
||||
"sending": "Отправка…",
|
||||
"testHttp": "HTTP {code}",
|
||||
"testSigned": "подписано",
|
||||
"testOk": "OK",
|
||||
"testFail": "ОШИБКА",
|
||||
"deleteButton": "Удалить триггер",
|
||||
"deleteTitle": "Удалить триггер?",
|
||||
"deleteMessage": "Триггер «{name}» будет удалён немедленно. Действие необратимо."
|
||||
},
|
||||
"form": {
|
||||
"name": "Имя",
|
||||
"namePlaceholder": "например, Slack #alerts при сбое деплоя",
|
||||
"required": "ОБЯЗАТЕЛЬНО",
|
||||
"andComposed": "ОБЪЕДИНЕНИЕ ПО И",
|
||||
"filtersLabel": "Фильтры",
|
||||
"actionLabel": "Действие",
|
||||
"actionWebhookBadge": "WEBHOOK",
|
||||
"severityCsv": "Уровень (CSV)",
|
||||
"severityPlaceholder": "warn,error",
|
||||
"sourceCsv": "Источник (CSV)",
|
||||
"sourcePlaceholder": "deploy,logscan",
|
||||
"messageRegex": "Регулярное выражение сообщения (необязательно)",
|
||||
"messageRegexPlaceholder": "(?i)\\bpanic\\b",
|
||||
"invalidRegex": "Некорректный regex — сервер отклонит.",
|
||||
"urlLabel": "URL",
|
||||
"urlPlaceholder": "https://hooks.slack.com/services/...",
|
||||
"secretLabel": "HMAC-секрет (необязательно)",
|
||||
"secretPlaceholder": "оставьте пустым для неподписанной доставки",
|
||||
"secretHint": "Приёмники проверяют X-Hub-Signature-256 по сырому телу запроса.",
|
||||
"secretRotateHint": "Хранится в зашифрованном виде. После создания API не возвращает значение — оставьте плейсхолдер без изменений, чтобы сохранить существующий секрет, введите новое значение для смены или очистите и сохраните, чтобы отключить подпись.",
|
||||
"enabled": "Включён",
|
||||
"enabledHint": "Выключенные триггеры остаются в таблице, но не срабатывают.",
|
||||
"submit": "Создать триггер",
|
||||
"submitting": "Создание…",
|
||||
"webhookUrl": "URL webhook"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "включён",
|
||||
"disabled": "выключен"
|
||||
}
|
||||
},
|
||||
"logscan": {
|
||||
"title": "Правила сканирования логов",
|
||||
"titleNew": "Новое правило",
|
||||
"titleSingular": "Правило",
|
||||
"lede": "Регулярные выражения, которые сканер применяет к потоку логов каждого работающего контейнера. Совпавшие строки попадают в event_log с уровнем правила, откуда триггеры событий передают их на настроенные webhook-приёмники. Включено {enabled} из {total}.",
|
||||
"ledeNew": "Сканируйте логи контейнеров по регулярному выражению. Оставьте поле «нагрузка» пустым, чтобы создать глобальное правило. Чтобы переопределить глобальное для одной нагрузки, используйте действие «Переопределить» на странице нагрузки.",
|
||||
"stat": {
|
||||
"total": "ВСЕГО",
|
||||
"global": "ГЛОБАЛЬНЫЕ",
|
||||
"workload": "НАГРУЗКА",
|
||||
"overrides": "ПЕРЕОПРЕДЕЛЕНИЯ",
|
||||
"activeTails": "АКТИВНЫХ TAIL",
|
||||
"droppedBucket": "ЛИМИТ",
|
||||
"droppedCooldown": "COOLDOWN",
|
||||
"compileErrors": "ОШИБКИ КОМПИЛЯЦИИ"
|
||||
},
|
||||
"stats": {
|
||||
"heading": "Статистика сканера",
|
||||
"headingSub": "Счётчики отбрасываний движка и ошибки компиляции из последнего снимка. Счётчики сбрасываются при перезапуске сервера.",
|
||||
"noCompileErrors": "Все правила компилируются без ошибок.",
|
||||
"compileErrorsHeading": "Ошибки компиляции (правило отброшено из снимка)",
|
||||
"tailsExplain": "Сейчас открыто goroutine-tail'ов по контейнерам у менеджера сканера."
|
||||
},
|
||||
"toolbar": {
|
||||
"newButton": "Новое правило",
|
||||
"backToList": "К списку правил"
|
||||
},
|
||||
"filter": {
|
||||
"all": "ВСЕ",
|
||||
"global": "ГЛОБАЛЬНЫЕ",
|
||||
"workload": "НАГРУЗКА",
|
||||
"overrides": "ПЕРЕОПРЕДЕЛЕНИЯ"
|
||||
},
|
||||
"empty": {
|
||||
"heading": "Правил пока нет",
|
||||
"body": "Начните с глобального правила вроде (?i)\\bpanic\\b с уровнем error, затем сужайте по нагрузкам через переопределения на странице нагрузки.",
|
||||
"cta": "Создать первое правило"
|
||||
},
|
||||
"list": {
|
||||
"name": "Имя",
|
||||
"pattern": "Шаблон",
|
||||
"scope": "Область",
|
||||
"severity": "Уровень",
|
||||
"streams": "Потоки",
|
||||
"status": "Статус",
|
||||
"open": "Открыть"
|
||||
},
|
||||
"detail": {
|
||||
"config": "Конфигурация",
|
||||
"configSub": "id #{id} · область {scope}",
|
||||
"regexTest": "Проверка regex",
|
||||
"regexTestSub": "Предпросмотр использует JavaScript-движок regex в браузере. Нажмите «Проверить на сервере», чтобы получить авторитетную проверку Go RE2 — это единственный надёжный сигнал для конструкций, специфичных для RE2.",
|
||||
"runServerTest": "Проверить на сервере",
|
||||
"testing": "Проверка…",
|
||||
"serverTestHint": "Сначала введите пример строки выше",
|
||||
"serverTestSendHint": "Отправить пример на backend /test",
|
||||
"serverMatch": "СОВПАЛО (СЕРВЕР)",
|
||||
"serverNoMatch": "НЕТ СОВПАДЕНИЯ",
|
||||
"serverNoMatchHint": "серверный regex не совпал с примером",
|
||||
"serverError": "ОШИБКА",
|
||||
"dangerZone": "Опасная зона",
|
||||
"dangerZoneSub": "Удаление глобального правила каскадно удаляет его переопределения для нагрузок.",
|
||||
"deleteButton": "Удалить правило",
|
||||
"deleteTitle": "Удалить правило?",
|
||||
"deleteMessage": "Правило «{name}» будет удалено немедленно. Переопределения по нагрузкам, ссылающиеся на него, также удалятся."
|
||||
},
|
||||
"form": {
|
||||
"name": "Имя",
|
||||
"namePlaceholder": "например, Panic в воркере",
|
||||
"pattern": "Шаблон",
|
||||
"regex": "REGEX",
|
||||
"patternPlaceholder": "(?i)\\bpanic\\b",
|
||||
"invalidRegex": "Некорректный regex — сервер отклонит.",
|
||||
"matchShape": "Параметры совпадения",
|
||||
"matchShapeOpts": "УРОВЕНЬ · ПОТОКИ · COOLDOWN",
|
||||
"severity": "Уровень",
|
||||
"streams": "Потоки",
|
||||
"cooldown": "Cooldown (с)",
|
||||
"cooldownHint": "Cooldown — на правило × на контейнер: одно правило, срабатывающее в двух контейнерах, считается независимо. Token bucket ограничивает выдачу на контейнер до 10 событий / 60с, чтобы не переполнить event_log.",
|
||||
"scope": "Область",
|
||||
"scopePlaceholder": "пусто для глобального правила или вставьте id нагрузки",
|
||||
"scopeHint": "Правила области нагрузки применяются только к её контейнерам. Переопределения для отдельных нагрузок проще создавать со страницы нагрузки.",
|
||||
"scopeGlobal": "Глобально (применяется ко всем нагрузкам)",
|
||||
"scopePick": "Выбрать нагрузку…",
|
||||
"scopePickTitle": "Выберите нагрузку",
|
||||
"scopeClear": "Сделать глобальным",
|
||||
"scopeSelected": "Нагрузка",
|
||||
"scopeUnknown": "Неизвестная нагрузка",
|
||||
"enabled": "Включено",
|
||||
"enabledHint": "Выключенные правила остаются в таблице, но не срабатывают.",
|
||||
"required": "ОБЯЗАТЕЛЬНО",
|
||||
"optional": "НЕОБЯЗАТЕЛЬНО",
|
||||
"submit": "Создать правило",
|
||||
"submitting": "Создание…"
|
||||
},
|
||||
"scope": {
|
||||
"global": "глобальное",
|
||||
"workload": "нагрузка {id}",
|
||||
"override": "переопределение #{id}",
|
||||
"overrideShort": "переопр. #{id}"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "включено",
|
||||
"disabled": "выключено",
|
||||
"on": "вкл",
|
||||
"off": "выкл"
|
||||
},
|
||||
"panel": {
|
||||
"heading": "Лог-правила",
|
||||
"subEmpty": "Для этой нагрузки правил нет",
|
||||
"subCount": "Действует правил: {count}",
|
||||
"subCountOne": "Действует 1 правило",
|
||||
"emptyHint": "Для этой нагрузки нет правил сканирования логов. Создайте через «Новое правило» — глобальные правила применяются автоматически; для этой нагрузки также можно завести свои или переопределения.",
|
||||
"newRule": "Новое правило",
|
||||
"footerHint": "Глобальные правила применяются ко всем нагрузкам. Правила нагрузки — только здесь. Переопределения замещают глобальное для этой нагрузки — изменяйте уровень или отключайте их, не трогая исходное глобальное.",
|
||||
"override": "Переопределить",
|
||||
"overriding": "Переопределение…",
|
||||
"overrideTitle": "Создать переопределение глобального правила для этой нагрузки"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user