feat(apps): per-app deploy/activity timeline

Every deploy across all four source kinds now writes a workload-scoped
event via a shared plugin.EmitDeployEvent helper (replacing the inline
emit duplicated in static/dockerfile, standardizing static's metadata
key site_id->workload_id, and adding emission to image+compose which
were silent). New indexed event_log.workload_id column, EventLogFilter
.WorkloadID, and GET /api/workloads/{id}/events (id pinned from path).

Frontend: a forge "Activity" panel on /apps/[id] reusing EventLogEntry,
live SSE prepend filtered by workload_id, load-more pagination, an
All/Errors severity filter, and a shared toEventLogEntry mapper. en/ru
i18n parity.

Security: compose's failure status emits a generic reason instead of raw
`docker compose up` output, which can echo app secrets and egresses to
operator webhooks (NotificationURL + event-trigger actions); full detail
stays only in the returned error. Rune-safe 256-rune status cap.

Reviewed: go + typescript APPROVE; security HIGH fixed.
This commit is contained in:
2026-05-29 13:51:17 +03:00
parent 3071cda512
commit 93b6911b34
19 changed files with 814 additions and 223 deletions
+13
View File
@@ -545,6 +545,9 @@
},
"source": {
"deploy": "Развёртывание",
"image": "Образ",
"compose": "Compose",
"dockerfile": "Dockerfile",
"static_site": "Статический сайт",
"stale_scanner": "Сканер устаревших",
"stale_cleanup": "Очистка устаревших",
@@ -1406,6 +1409,16 @@
"deployError": "Деплой не удался",
"saveError": "Сохранение не удалось",
"deleteError": "Удаление не удалось",
"activity": {
"title": "Активность",
"subtitle": "Недавние деплои и события этого приложения",
"empty": "Пока нет активности. Деплои и события появятся здесь.",
"recentNote": "Показана недавняя активность.",
"loadMore": "Загрузить ещё",
"filterAll": "Все",
"filterErrors": "Ошибки",
"noErrors": "Нет ошибок в загруженной активности."
},
"runtimeState": {
"title": "Статус синхронизации",
"sub": "Последняя успешная синхронизация репозитория и текущее состояние контейнера.",