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:
2026-05-11 22:18:29 +03:00
parent 7a9ff7ad54
commit 4707db1c3b
11 changed files with 4455 additions and 1 deletions
+226
View File
@@ -14,6 +14,9 @@
},
"nav": {
"dashboard": "Dashboard",
"apps": "Apps",
"eventTriggers": "Triggers",
"logScanRules": "Log Rules",
"projects": "Projects",
"deploy": "Deploy",
"proxies": "Proxies",
@@ -1301,5 +1304,228 @@
"thresholds": "Thresholds",
"thresholdsDesc": "Tune when Tinyforge flags stale containers and warns about unused image disk usage.",
"dangerZone": "Danger zone"
},
"observability": {
"section": "Observability",
"manage": "manage",
"loading": "Loading…",
"anyEvent": "any event",
"noUrlSet": "No URL configured",
"configured": "CONFIGURED",
"clear": "Clear",
"advanced": "Advanced",
"cancel": "Cancel",
"save": "Save changes",
"saving": "Saving…",
"delete": "Delete",
"deleting": "Deleting…",
"refresh": "Refresh",
"open": "Open",
"edit": "Edit",
"back": "Back",
"regex": {
"sampleLabel": "Sample line",
"placeholder": "paste a representative log line here",
"promptType": "type a sample to test the pattern",
"noMatch": "NO MATCH",
"noMatchHint": "pattern did not match this line",
"match": "MATCH",
"invalid": "REGEX",
"captures": "Captures"
}
},
"triggers": {
"title": "Event triggers",
"titleNew": "Forge a new trigger",
"titleSingular": "Trigger",
"lede": "Filter event-log entries (deploy events, log scanner output, future sources) and dispatch a webhook when they match. Filters AND together; empty filters mean \"match anything.\"",
"ledeNew": "Create a filter+action rule. The dispatcher AND-composes all filter fields. Leave a field empty to skip that dimension.",
"stat": {
"total": "TOTAL",
"enabled": "ENABLED",
"disabled": "DISABLED"
},
"toolbar": {
"newButton": "New trigger",
"backToList": "Back to triggers"
},
"empty": {
"heading": "No triggers yet",
"body": "Configure a trigger to forward event-log entries to Slack, a notification bridge, or any HTTP receiver. Tinyforge signs requests with X-Hub-Signature-256 when a secret is set.",
"cta": "Create the first trigger"
},
"list": {
"name": "Name",
"filters": "Filters",
"action": "Action",
"status": "Status",
"open": "Open"
},
"detail": {
"config": "Configuration",
"configSub": "id #{id} · updated {updatedAt}",
"dangerZone": "Danger zone",
"dangerZoneSub": "Trigger deletion is immediate. No soft-delete.",
"sendTest": "Send test",
"sending": "Sending…",
"testHttp": "HTTP {code}",
"testSigned": "signed",
"testOk": "OK",
"testFail": "FAIL",
"deleteButton": "Delete trigger",
"deleteTitle": "Delete trigger?",
"deleteMessage": "Trigger \"{name}\" will be removed immediately. This cannot be undone."
},
"form": {
"name": "Name",
"namePlaceholder": "e.g. Slack #alerts on deploy failure",
"required": "REQUIRED",
"andComposed": "AND-COMPOSED",
"filtersLabel": "Filters",
"actionLabel": "Action",
"actionWebhookBadge": "WEBHOOK",
"severityCsv": "Severity (CSV)",
"severityPlaceholder": "warn,error",
"sourceCsv": "Source (CSV)",
"sourcePlaceholder": "deploy,logscan",
"messageRegex": "Message regex (optional)",
"messageRegexPlaceholder": "(?i)\\bpanic\\b",
"invalidRegex": "Invalid regex — server will reject.",
"urlLabel": "URL",
"urlPlaceholder": "https://hooks.slack.com/services/...",
"secretLabel": "HMAC secret (optional)",
"secretPlaceholder": "leave blank for unsigned delivery",
"secretHint": "Receivers verify X-Hub-Signature-256 against the raw body.",
"secretRotateHint": "Stored encrypted at rest. The value is never returned by the API after creation — leave the placeholder untouched to preserve the existing secret, type a new value to rotate, or clear and save to remove signing.",
"enabled": "Enabled",
"enabledHint": "Disabled triggers stay in the table but never dispatch.",
"submit": "Forge trigger",
"submitting": "Forging…",
"webhookUrl": "Webhook URL"
},
"status": {
"enabled": "enabled",
"disabled": "disabled"
}
},
"logscan": {
"title": "Log scan rules",
"titleNew": "Forge a new rule",
"titleSingular": "Rule",
"lede": "Regex patterns the scanner runs against every running container's log stream. Matched lines land in event_log with the rule's severity, where event triggers pick them up and fan out to operator-configured webhooks. {enabled} of {total} enabled.",
"ledeNew": "Tail container logs against a regex. Leave the workload field empty to create a global rule. To override an existing global for one workload, use the per-workload override action on the workload detail page.",
"stat": {
"total": "TOTAL",
"global": "GLOBAL",
"workload": "WORKLOAD",
"overrides": "OVERRIDES",
"activeTails": "ACTIVE TAILS",
"droppedBucket": "RATE-LIMITED",
"droppedCooldown": "COOLED DOWN",
"compileErrors": "COMPILE ERRORS"
},
"stats": {
"heading": "Scanner stats",
"headingSub": "Engine drop counters and last-snapshot compile errors. Counters reset on server restart.",
"noCompileErrors": "All rules compile cleanly.",
"compileErrorsHeading": "Compile errors (rule dropped from snapshot)",
"tailsExplain": "Per-container tail goroutines currently driven by the scanner manager."
},
"toolbar": {
"newButton": "New rule",
"backToList": "Back to rules"
},
"filter": {
"all": "ALL",
"global": "GLOBAL",
"workload": "WORKLOAD",
"overrides": "OVERRIDES"
},
"empty": {
"heading": "No rules yet",
"body": "Start with a global rule like (?i)\\bpanic\\b at severity error, then narrow per-workload via overrides on the workload detail page.",
"cta": "Create the first rule"
},
"list": {
"name": "Name",
"pattern": "Pattern",
"scope": "Scope",
"severity": "Severity",
"streams": "Streams",
"status": "Status",
"open": "Open"
},
"detail": {
"config": "Configuration",
"configSub": "id #{id} · scope {scope}",
"regexTest": "Regex test",
"regexTestSub": "Live preview uses the browser's JavaScript regex engine. Click \"Run server test\" to evaluate against Go's RE2 — authoritative and the only reliable signal for RE2-only constructs.",
"runServerTest": "Run server test",
"testing": "Testing…",
"serverTestHint": "Enter a sample line above first",
"serverTestSendHint": "Send sample to backend /test endpoint",
"serverMatch": "SERVER MATCH",
"serverNoMatch": "NO MATCH",
"serverNoMatchHint": "server regex did not match the sample",
"serverError": "ERROR",
"dangerZone": "Danger zone",
"dangerZoneSub": "Deleting a global rule cascade-removes its per-workload overrides.",
"deleteButton": "Delete rule",
"deleteTitle": "Delete rule?",
"deleteMessage": "Rule \"{name}\" will be removed immediately. Per-workload overrides referencing it will be cascade-deleted."
},
"form": {
"name": "Name",
"namePlaceholder": "e.g. Panic in worker",
"pattern": "Pattern",
"regex": "REGEX",
"patternPlaceholder": "(?i)\\bpanic\\b",
"invalidRegex": "Invalid regex — server will reject.",
"matchShape": "Match shape",
"matchShapeOpts": "SEVERITY · STREAMS · COOLDOWN",
"severity": "Severity",
"streams": "Streams",
"cooldown": "Cooldown (s)",
"cooldownHint": "Cooldown is per-rule per-container — the same rule firing on two containers stays independent. Token bucket caps per-container emissions at 10 events / 60s to prevent flooding event_log.",
"scope": "Scope",
"scopePlaceholder": "empty for global rule, or paste a workload id",
"scopeHint": "Workload-scoped rules apply only to that workload's containers. Per-workload overrides are easier to create from the workload detail page.",
"scopeGlobal": "Global (applies to every workload)",
"scopePick": "Pick workload…",
"scopePickTitle": "Pick a workload",
"scopeClear": "Make global",
"scopeSelected": "Workload",
"scopeUnknown": "Unknown workload",
"enabled": "Enabled",
"enabledHint": "Disabled rules stay in the table but never fire.",
"required": "REQUIRED",
"optional": "OPTIONAL",
"submit": "Forge rule",
"submitting": "Forging…"
},
"scope": {
"global": "global",
"workload": "workload {id}",
"override": "override of #{id}",
"overrideShort": "override #{id}"
},
"status": {
"enabled": "enabled",
"disabled": "disabled",
"on": "on",
"off": "off"
},
"panel": {
"heading": "Log rules",
"subEmpty": "No effective rules for this workload",
"subCount": "{count} effective rules",
"subCountOne": "1 effective rule",
"emptyHint": "This workload has no log scan rules applied. Create one via New rule — globals apply automatically; this workload can also have its own narrower rules or overrides.",
"newRule": "New rule",
"footerHint": "Global rules apply to every workload. Workload rules apply only here. Override rows substitute for a global on this workload — edit them to disable or change severity per-workload without touching the global.",
"override": "Override",
"overriding": "Overriding…",
"overrideTitle": "Create a per-workload override of this global rule"
}
}
}