GitHub
09/03/2026, 5:51 AMalerts.TestSend(type, config) builds a sender from the existing
ChannelRegistry and sends TestHit() (rule_name: osctrl-channel-test),
bounded by a 15s backstop since the SMTP sender carries no timeout of its own.
The goroutine is buffered-by-one so an abandoned send exits instead of leaking.
• POST /api/v1/alerts/channels/test (types.AlertChannelTestRequest) — global
admin, audit-logged, same gate as the rest of the alerts API. A config the
registry refuses is *400*; a relay that refuses us is 502, so an operator
mistake and a broken endpoint don't look alike. No new capability: an admin
could already save a channel and wait for it to fire.
• Passing id (edit mode) merges stored secrets through the existing
MergeChannelSecrets, so testing an untouched secret sends the real one
instead of the *** placeholder.
"Errors reported" premade alert
The node detail page's alert modal offered "node inactive" and "log match". It
now offers a third, and so does the Alerts page's rule editor:
• Node modal: "Errors reported by this node" → a status_log rule scoped to
the node with status_severity: error and no pattern, named <hostname>-errors.
The three kinds now live in one NODE_ALERT_KINDS table (suffix, title, help,
rule fields), which replaced two copy-pasted <label> blocks and the ternary
chain at the submit site.
• Rule editor: a "Start from a premade rule" row (create mode) whose
Errors reported button fills in name, source and severity — same rule
without the node scope, so it covers every node in the environment.
No new matcher machinery: a severity floor with an empty substring already means
"every error line".
Bugs found on the way
• invalid value told the operator nothing. respondAlertsErr collapsed
every rejected channel config, channel type and rule source into that string,
so a channel test failed with no reason. It now echoes the actual error
(admin-only endpoints), e.g. webhook target "127.0.0.1" is a private/loopback address; set allowPrivateTargets to override (dev only).
• That private-target guard had no UI. allowPrivateTargets was reachable
only via API/YAML, so a local relay could never be configured — let alone
tested — from the browser. Exposed as an off-by-default checkbox with help
text saying what it is for.
• Pattern-less rules alerted with no context. An empty substring returned
itself as the hit detail, so any rule without a pattern (the new preset
included) would have paged with an empty Detail. substringDetail now falls
back to the value that matched. Affects every pattern-less rule.
• The rule editor could not express "every error line". submit() demanded a
match value for any pattern source. A status-log rule with a severity floor is
now complete without one (patternOptional), and the label says so. Result and
query rules still require a pattern — an empty one there matches every row.
• Unexpected end of JSON input on every DELETE. apiFetch ended in
res.json(), which throws on the 204 that all five delete endpoints return —
alert rules, alert channels, log sinks, auth providers, env packages. Each
deleted the row server-side, then reported a failure and skipped its cache
invalidation, so the row lingered until reload. It now reads the body as text
and parses only when there are bytes. Audited the rest of the API: those five
(plus GET /health when the DB is degraded, which the SPA never calls) are the
only empty-body responses; every other route returns JSON.
• osctrl-api exited without draining on restart. The apply handler wrote its
202 and signalled `RestartCh`; main called os.Exit(1) immediately, racing the
response flush the handler's own docs promised to wait for. It now calls
srv.Shutdown with a 5s bound first.
Tests
Go — channel test send (200 / 502 / 400, and that a 400 names what it rejected),
stored-secret merge (asserts X-Osctrl-Signature arrives), node-scoped error rule
matching (fires on the node's error line only, and carries the message), and the
API restart branch of the apply handler, which had no coverage (explicit
service: "api", bodyless POST, and 503 without a wired channel).
Frontend — the Send test success and failure paths, the preset button and the body
it saves, the node modal's error kind, and apiFetch body handling (204 →
undefined, JSON still parsed, error body still raises ApiError).
go build ./... && go test ./pkg/alerts/ ./cmd/api/... pass; 314 frontend tests
pass; tsc --noEmit clean.
Deploy note
Requires an osctrl-api restart for the new endpoint and the registry field.
jmpsec/osctrlGitHub
09/03/2026, 6:06 AM