feat(webhook): inbound delivery audit log
Build / build (push) Successful in 10m35s

Persists every inbound webhook hit (project + site) so users can debug
"why didn't my deploy fire?" without grepping daemon logs. Surfaces a
14-day rolling history under the WebhookPanel on each project + site
detail page; refreshes every 30s while open. Daily cron prunes records
older than 14 days alongside the existing event log prune.

Schema:
- webhook_deliveries(id, target_type, target_id, target_name, received_at,
  source_ip, signature_state, status_code, outcome, detail, body_size)
- indexes on (target_type,target_id,received_at) and (received_at)

Backend:
- store: WebhookDelivery model + Insert/List/Prune helpers
- webhook/handler: deferred recordDelivery() captures the final outcome
  on every return path including HMAC rejects, image mismatch, no-stage,
  auto_deploy=false, and successful deploys; signatureStateFor()
  classifies "unconfigured" vs "missing" vs "invalid" vs "valid"
- api: GET /api/{projects,sites}/{id}/webhook/deliveries with
  parseLimit() helper (default 50, max 200)
- main: daily prune cron retains the last 14 days

Frontend:
- WebhookDeliveryLog.svelte: panel with refresh button, status code +
  outcome + signature badges, relative time tooltip-on-hover for
  absolute time, source IP column
- Mounted below WebhookPanel on project + site detail pages
- en/ru i18n strings for outcome/signature enums and column labels
This commit is contained in:
2026-05-07 02:40:39 +03:00
parent 831b5c1a43
commit 0f60a7a5db
12 changed files with 591 additions and 16 deletions
+27
View File
@@ -1172,6 +1172,33 @@
"incoming": "Входящие вебхуки",
"incomingMovedDesc": "Входящие вебхуки теперь привязаны к конкретному проекту или сайту. Откройте страницу проекта или статического сайта, чтобы увидеть и перегенерировать URL."
},
"webhookLog": {
"title": "Последние доставки вебхуков",
"description": "Последние 14 дней входящих вебхуков — результат, состояние подписи и причина. Обновляется каждые 30 секунд.",
"refresh": "Обновить",
"loadFailed": "Не удалось загрузить журнал доставок",
"empty": "Пока нет доставок.",
"colTime": "Когда",
"colStatus": "Статус",
"colOutcome": "Результат",
"colSignature": "Подпись",
"colDetail": "Подробности",
"colSource": "Источник",
"outcome": {
"deploy": "Развёрнуто",
"skip": "Пропущено",
"rejected": "Отклонено",
"not_found": "Не найдено",
"bad_request": "Неверный запрос",
"error": "Ошибка"
},
"sig": {
"valid": "верна",
"invalid": "неверна",
"missing": "отсутствует",
"unconfigured": "выкл"
}
},
"webhookPanel": {
"copy": "Копировать",
"copied": "Webhook-URL скопирован в буфер обмена",