service_down nudges name the service again (V-534)

nudgeValues filled {service} from State.Fact("service_down"), an exact key
mavpoll stopped writing when per-monitor facts landed. The lookup could never
hit, so every variant carrying {service} was rejected as unfillable and the one
nameless variant was the only usable template, every time. A sev4 reaching him
on telegram said only that a service was down.

It now reads loop.DownServices, the same helper the rule fires on, so the
message cannot name a service that is up. Dropped the nameless variant and the
{since} one: service_down facts are keyed by monitor and the rule is
edge-triggered, so neither can fill. service_down joins routine and morning as
a family that always carries a name.

The tests passed through all of this because cand() built the pre-per-monitor
aggregate shape. downCand() builds what a tick actually produces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x5DgnExQ5XZy8TZPs5bot
This commit is contained in:
2026-08-04 23:13:59 +04:00
parent 2e64c8ce94
commit 06ddf41228
3 changed files with 79 additions and 12 deletions
+13 -5
View File
@@ -165,17 +165,25 @@ func nudgeValues(c loop.Candidate) map[string]string {
vals := map[string]string{}
rule := c.Rule.Name
// {service} — one fact per kuma monitor, keyed "service_down:<name>", so
// the name lives in the key SUFFIX and there is no fact called plain
// "service_down" to read. loop.DownServices is the same helper the rule
// fired on, which is what stops the message naming a service that is up.
// This used to read c.State.Fact(rule) — the pre-per-monitor aggregate —
// and so never filled, leaving the one nameless variant as the only
// fillable template every time (Vikunja #534).
if down := loop.DownServices(c.State); len(down) > 0 {
vals["service"] = strings.Join(down, ", ")
}
// {since} — only at hour scale. Below an hour the phrase would be minutes,
// and none of the templates read well with "сорок минут".
// and none of the templates read well with "сорок минут". service_down has
// no {since} to offer: its facts are keyed by monitor, and the rule is
// edge-triggered, so it fires on the transition rather than hours later.
if d, ok := c.State.Since(rule); ok && d >= time.Hour {
if s := ruSinceWords(d); s != "" {
vals["since"] = s
}
}
// {service} — the aggregate fact's key carries the service name.
if f, ok := c.State.Fact(rule); ok && f.Key != "" && f.Key != rule {
vals["service"] = f.Key
}
// {what} — the Russian suffix of "routine:таблетки" / "morning:утро".
if i := strings.IndexByte(rule, ':'); i > 0 && i+1 < len(rule) {
vals["what"] = rule[i+1:]