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": "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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user