v0.309.0: the fill warning reaches the household in its own language; the root-filesystem disk event pinned end to end (R-521 F7)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -38,7 +38,6 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crashboot"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/fillwatch"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/infra"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/integrations"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/mailrelay"
|
||||
@@ -1593,21 +1592,12 @@ func main() {
|
||||
}
|
||||
})
|
||||
if notifier != nil {
|
||||
fillWatcher.SetNotify(func(e fillwatch.Event) {
|
||||
// The CUSTOMER's event — deliberately not operator-only. A customer can free space,
|
||||
// delete files or add a drive, so this alert is theirs (D-c). The dynamic Hungarian
|
||||
// message carries the label and the free space; the hub has NO customerMessages entry for
|
||||
// these two types precisely so the template cannot discard them.
|
||||
notifier.PushEvent(e.Band.EventType(), e.Band.Severity(), e.Message, map[string]any{
|
||||
"path": e.Target.Path, "label": e.Target.Label,
|
||||
"used_percent": e.Usage.UsedPercent, "avail_gb": e.Usage.AvailGB,
|
||||
"total_gb": e.Usage.TotalGB, "band": e.Band.String(),
|
||||
})
|
||||
})
|
||||
fillWatcher.SetNotify(fillEventPush(notifier))
|
||||
}
|
||||
// `09` §3 decision 36: the warning names the kept folders on the filling drive, with their sizes —
|
||||
// space the household can free on the kept-data list. Nothing is deleted by the box (D3).
|
||||
fillWatcher.SetExtra(func(t fillwatch.Target) string { return keptSpaceSentence(stackMgr, t.Path) })
|
||||
fillWatcher.SetExtra(func(t fillwatch.Target, lang string) string { return keptSpaceSentence(stackMgr, t.Path, lang) })
|
||||
fillWatcher.SetLang(sett.GetLanguage)
|
||||
sched.Daily("fill-watch", "03:30", func(ctx context.Context) error { return fillWatcher.Check() })
|
||||
sched.Every("fill-watch-interval", fillWatchInterval, func(ctx context.Context) error { return fillWatcher.Check() })
|
||||
|
||||
@@ -2532,28 +2522,52 @@ const fillWatchInterval = 10 * time.Minute
|
||||
// DECOMMISSIONED drive is dropped — it is deliberately out of service, and its fill is not news. A
|
||||
// DISCONNECTED one is kept in the list but will read as unreadable and be skipped by §8.4, which is
|
||||
// the correct outcome: its absence already has its own alert.
|
||||
// fillEventPusher is the one notifier method fillEventPush needs — an interface so the R-521 F7 test
|
||||
// can record what is pushed without a hub.
|
||||
type fillEventPusher interface {
|
||||
PushEventHousehold(eventType, severity, message, household string, details interface{})
|
||||
}
|
||||
|
||||
// fillEventPush turns a fillwatch crossing into the hub event.
|
||||
//
|
||||
// The CUSTOMER's event — deliberately not operator-only. A customer can free space, delete files or
|
||||
// add a drive, so this alert is theirs (D-c). The dynamic Hungarian message carries the label and the
|
||||
// free space; the hub has NO customerMessages entry for these two types precisely so the template
|
||||
// cannot discard them. R-521 F7: the household's own rendering rides beside the Hungarian (R-558's
|
||||
// `message_customer`), so an English household is told in English. Pinned by
|
||||
// TestR521F7_RootFilesystemSendsTheDiskEventInBothLanguages.
|
||||
func fillEventPush(n fillEventPusher) func(fillwatch.Event) {
|
||||
return func(e fillwatch.Event) {
|
||||
n.PushEventHousehold(e.Band.EventType(), e.Band.Severity(), e.Message, e.MessageHousehold, map[string]any{
|
||||
"path": e.Target.Path, "label": e.Target.Label,
|
||||
"used_percent": e.Usage.UsedPercent, "avail_gb": e.Usage.AvailGB,
|
||||
"total_gb": e.Usage.TotalGB, "band": e.Band.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fillTargets(cfg *config.Config, sett *settings.Settings) []fillwatch.Target {
|
||||
seen := make(map[string]bool)
|
||||
var out []fillwatch.Target
|
||||
add := func(path, label string) {
|
||||
add := func(path, label, labelKey string) {
|
||||
if path == "" || seen[path] {
|
||||
return
|
||||
}
|
||||
seen[path] = true
|
||||
out = append(out, fillwatch.Target{Path: path, Label: label})
|
||||
out = append(out, fillwatch.Target{Path: path, Label: label, LabelKey: labelKey})
|
||||
}
|
||||
|
||||
// The docker data-root as seen from inside the guest, and the system-data area.
|
||||
add(system.DockerVolumePath, "Alkalmazások területe")
|
||||
add(system.DockerVolumePath, "Alkalmazások területe", "event.fill.label_apps")
|
||||
if cfg != nil {
|
||||
add(cfg.Paths.SystemDataPath, "Rendszer- és mentési terület")
|
||||
add(cfg.Paths.SystemDataPath, "Rendszer- és mentési terület", "event.fill.label_system")
|
||||
}
|
||||
if sett != nil {
|
||||
for _, sp := range sett.GetStoragePaths() {
|
||||
if sp.Decommissioned {
|
||||
continue
|
||||
}
|
||||
add(sp.Path, sp.Label)
|
||||
add(sp.Path, sp.Label, "")
|
||||
}
|
||||
}
|
||||
return fillwatch.SortTargets(out)
|
||||
@@ -4071,7 +4085,7 @@ func budapestLoc() *time.Location {
|
||||
|
||||
// keptSpaceSentence is the drive-full warning's kept-data sentence for one drive (Hungarian, like the
|
||||
// warning it extends): each kept folder by app name and size. "" when the drive holds none.
|
||||
func keptSpaceSentence(sm *stacks.Manager, drive string) string {
|
||||
func keptSpaceSentence(sm *stacks.Manager, drive, lang string) string {
|
||||
items := sm.ListKept([]string{drive})
|
||||
if len(items) == 0 {
|
||||
return ""
|
||||
@@ -4080,5 +4094,5 @@ func keptSpaceSentence(sm *stacks.Manager, drive string) string {
|
||||
for _, it := range items {
|
||||
parts = append(parts, fmt.Sprintf("%s (%s)", it.DisplayName, appbackup.HumanizeBytes(it.SizeBytes)))
|
||||
}
|
||||
return util.Text(i18n.Default, "kept.diskwarn", strings.Join(parts, ", "))
|
||||
return util.Text(lang, "kept.diskwarn", strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/fillwatch"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
)
|
||||
|
||||
type fillPushRecord struct {
|
||||
eventType, severity, message, household string
|
||||
details interface{}
|
||||
}
|
||||
|
||||
type fakeFillPusher struct{ got []fillPushRecord }
|
||||
|
||||
func (f *fakeFillPusher) PushEventHousehold(eventType, severity, message, household string, details interface{}) {
|
||||
f.got = append(f.got, fillPushRecord{eventType, severity, message, household, details})
|
||||
}
|
||||
|
||||
// R-521 F7 (BIGNIGHT 2026-09-14): the guest's own system disk at 95 % produced only `health_degraded`,
|
||||
// whose mail a cooldown swallowed — no disk-specific event reached the hub. The consequence pinned here,
|
||||
// through the REAL target list (fillTargets), the REAL watcher and the REAL push closure (fillEventPush):
|
||||
// the guest's root filesystem at F7's numbers sends the hub's existing `disk_critical` event (severity
|
||||
// critical, a type the hub already allowlists), ONCE (edge-triggered), carrying the Hungarian sentence
|
||||
// AND the household's own (English here).
|
||||
func TestR521F7_RootFilesystemSendsTheDiskEventInBothLanguages(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
cfg.Paths.SystemDataPath = "/mnt/sys_drive"
|
||||
usage := func(p string) *fillwatch.Usage {
|
||||
if p == system.DockerVolumePath { // the guest's root filesystem as the controller sees it
|
||||
return &fillwatch.Usage{UsedPercent: 95, AvailGB: 3.5, UsedGB: 61.8, TotalGB: 68.7}
|
||||
}
|
||||
return &fillwatch.Usage{UsedPercent: 30, AvailGB: 50, UsedGB: 20, TotalGB: 70}
|
||||
}
|
||||
w := fillwatch.New(filepath.Join(t.TempDir(), "fill.json"), log.New(io.Discard, "", 0),
|
||||
func() []fillwatch.Target { return fillTargets(cfg, nil) }, usage)
|
||||
rec := &fakeFillPusher{}
|
||||
w.SetNotify(fillEventPush(rec))
|
||||
w.SetLang(func() string { return "en" })
|
||||
|
||||
for i := 0; i < 2; i++ { // the second check sees the same band: nothing new (edge-triggered)
|
||||
if err := w.Check(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(rec.got) != 1 {
|
||||
t.Fatalf("pushes = %d (%+v), want exactly 1 disk event for the root filesystem", len(rec.got), rec.got)
|
||||
}
|
||||
p := rec.got[0]
|
||||
if p.eventType != "disk_critical" || p.severity != "critical" {
|
||||
t.Errorf("pushed %s/%s, want disk_critical/critical", p.eventType, p.severity)
|
||||
}
|
||||
if d, _ := p.details.(map[string]any); d == nil || d["path"] != system.DockerVolumePath {
|
||||
t.Errorf("details = %v, want path %q", p.details, system.DockerVolumePath)
|
||||
}
|
||||
if !strings.Contains(p.message, "Alkalmaz") || !strings.Contains(p.message, "kritikusan") {
|
||||
t.Errorf("Hungarian message = %q", p.message)
|
||||
}
|
||||
if !strings.Contains(p.household, "App area") || !strings.Contains(p.household, "critically full") {
|
||||
t.Errorf("household (English) message = %q", p.household)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user