hub v0.81.0 — E-2: backup_target_absent gets its own signal (ships first)
An event type the hub does not allowlist makes POST /event return 400 and the
event vanishes (R-97a). The controller cannot emit backup_target_absent until
this is live, so the hub half ships first.
E-2 Phase 0 established that an absent backup target has NO prompt signal today.
The controller's drive-gate path stops apps and logs a WARN but emits nothing:
NotifyStorageDisconnected is defined and never called anywhere (verified against
the gitignored-cmd/ trap with a positive control). A drive that is ONLY a backup
target has no apps to stop, so it is entirely silent. The sole signal is the
tier's own failure at its next due cycle -- up to ~24h on the daily local tier,
which is the R-100 shape: a real fault visible only after a deadline elapses.
Added to BOTH registers, because each half fails differently:
allowedEventTypes -- without it the event is lost at the door;
customerMessages -- without it the event IS delivered but in the controller's
raw operator English, and nothing looks broken.
backup_target_absent is deliberately NOT folded into storage_disconnected: that
says "a drive went away and some apps may have stopped"; this says "the thing
that makes your backup survive a disk failure is gone".
Hungarian copy names the consequence, not just the fact. backup_target_restored
is the paired recovery at info severity -- severityNotifies NOT widened.
Three tests pin the pair and the copy's substance. All red-proofed with the
mutation VERIFIED to have landed first: the initial attempt silently no-op'd
(gofmt had realigned the map) and the test "passed" -- a false proof that would
have been reported as evidence.
Green gate: build + vet + test rc=0, run separately from this commit.
This commit is contained in:
@@ -1,3 +1,40 @@
|
||||
## v0.81.0 — E-2: the absent backup target gets its own signal (2026-07-29)
|
||||
|
||||
**Hub half of E-2, and it ships FIRST by necessity:** an event type the hub does not allowlist makes
|
||||
`POST /event` return 400 and the event vanishes (the R-97a failure). The controller cannot emit
|
||||
`backup_target_absent` until this is live.
|
||||
|
||||
E-2's Phase 0 established that an absent backup target has **no prompt signal today**. The drive-gate
|
||||
path stops apps and logs a WARN but emits nothing — `NotifyStorageDisconnected` is defined and never
|
||||
called anywhere in the controller (verified against the gitignored-`cmd/` trap with a positive
|
||||
control). A drive that is *only* a backup target has no apps to stop, so it is entirely silent. The
|
||||
sole signal is the tier's own failure at its next due cycle, i.e. **up to ~24 h** on the daily local
|
||||
tier — the R-100 shape, where a real fault is visible only after a deadline elapses.
|
||||
|
||||
Added to BOTH registers, because each half fails differently and the second failure is the quiet one:
|
||||
|
||||
- `allowedEventTypes` (`internal/api/handler.go`) — without it the event is lost at the door;
|
||||
- `customerMessages` (`internal/notify/templates.go`) — without it the event IS delivered, but the
|
||||
customer receives the controller's raw operator English instead of Hungarian, and nothing looks
|
||||
broken.
|
||||
|
||||
`backup_target_absent` is deliberately **not** folded into `storage_disconnected`. That one says "a
|
||||
drive went away and some apps may have stopped"; this one says "the thing that makes your backup
|
||||
survive a disk failure is gone" — a different customer action and a different operator urgency.
|
||||
|
||||
Hungarian copy states the CONSEQUENCE, not just the fact:
|
||||
> „A rendszermentés meghajtója nem érhető el — amíg vissza nem csatlakoztatod, a teljes rendszermentés nem készül el."
|
||||
|
||||
`backup_target_restored` is the paired recovery (`info` severity — the existing recovery pattern;
|
||||
`severityNotifies` is untouched and NOT widened).
|
||||
|
||||
**Tests + red-proofs.** `api.TestBackupTargetEventTypesAreAllowlisted` and
|
||||
`notify.TestBackupTargetCustomerMessagesArePresent` pin the pair; a third test pins that the copy
|
||||
names what is at risk and what happens, so a future shortening to a bare „Meghajtó hiányzik." cannot
|
||||
pass. All three red-proofed with the mutation VERIFIED to have landed first — the initial attempt
|
||||
silently no-op'd (gofmt had realigned the map to three spaces) and the test "passed", which would
|
||||
have been a false proof.
|
||||
|
||||
## v0.80.0 — R-100: staleness counts from the last SUCCESS (2026-07-28)
|
||||
|
||||
`OffsiteChecker.isStale` counted from `last_run`, which the controller writes **unconditionally** at the
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
|
||||
// E-2 — the absent-backup-target signal needs BOTH registers, not one.
|
||||
//
|
||||
// Phase 0 of E-2 established that an absent backup target has no prompt signal today: the drive-gate
|
||||
// path stops apps and logs, but emits nothing, and the tier's own failure only lands at its next due
|
||||
// cycle (up to ~24 h on the daily local tier). `backup_target_absent` is that missing signal.
|
||||
//
|
||||
// A new event type is a PAIR, and each half fails differently:
|
||||
// - missing from allowedEventTypes → POST /event returns 400 and the event VANISHES (R-97a);
|
||||
// - missing from customerMessages → it is delivered, but the customer gets the controller's raw
|
||||
// English operator string instead of Hungarian.
|
||||
//
|
||||
// The second failure is the quiet one — delivery "works", so nothing looks broken. Both are pinned
|
||||
// here, in the same test, because fixing one and not the other is the realistic mistake.
|
||||
func TestBackupTargetEventTypesAreAllowlisted(t *testing.T) {
|
||||
for _, et := range []string{"backup_target_absent", "backup_target_restored"} {
|
||||
if !allowedEventTypes[et] {
|
||||
t.Fatalf("%s must be in allowedEventTypes, or POST /event 400s and the absent backup "+
|
||||
"target goes silent until the tier misses its deadline", et)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1595,8 +1595,16 @@ var allowedEventTypes = map[string]bool{
|
||||
"offbox_repo_reset": true,
|
||||
"storage_disconnected": true,
|
||||
"storage_reconnected": true,
|
||||
"disk_warning": true,
|
||||
"disk_critical": true,
|
||||
// controller v0.184.0 (E-2) — the assigned whole-guest backup TARGET drive is absent. Distinct
|
||||
// from storage_disconnected on purpose: that one says "a drive went away and some apps may have
|
||||
// stopped"; this one says "the thing that makes a backup survive a disk failure is gone", which
|
||||
// is a different action for the customer and a different urgency for the operator. Before this
|
||||
// the only signal was the tier's own failure at its next due cycle — up to ~24 h on the daily
|
||||
// local tier — i.e. the R-100 shape: a real fault visible only after a deadline elapsed.
|
||||
"backup_target_absent": true,
|
||||
"backup_target_restored": true,
|
||||
"disk_warning": true,
|
||||
"disk_critical": true,
|
||||
// controller v0.169.0 — per-disk SMART degradation (Rendben→Figyelmeztetés/Hiba). The controller
|
||||
// sends a dynamic Hungarian message (disk label + the triggering attribute names), so — like
|
||||
// offbox_enlarge_blocked — there is deliberately NO customerMessages entry (which would discard the
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// E-2 — the Hungarian half of the backup_target_absent pair.
|
||||
//
|
||||
// Companion to api.TestBackupTargetEventTypesAreAllowlisted, which pins the allowlist half. They are
|
||||
// deliberately two tests in two packages because the registers live in two packages, and the failure
|
||||
// modes differ: an allowlist miss makes POST /event 400 (loud, the event is lost); a customerMessages
|
||||
// miss delivers the event but falls back to the controller's raw operator English
|
||||
// (templates.go FormatCustomerEmail: hunMessage == "" → message). The customer would receive
|
||||
// "Backup target drive absent: ..." in English and nothing would look broken.
|
||||
func TestBackupTargetCustomerMessagesArePresent(t *testing.T) {
|
||||
for _, et := range []string{"backup_target_absent", "backup_target_restored"} {
|
||||
msg, ok := customerMessages[et]
|
||||
if !ok || strings.TrimSpace(msg) == "" {
|
||||
t.Fatalf("customerMessages[%q] is missing — the event would be delivered to the customer "+
|
||||
"in the controller's operator English instead of Hungarian", et)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The absent message must name the CONSEQUENCE, not just the fact. "A drive is missing" is a fact the
|
||||
// customer can do nothing with; "the whole-system backup will not be made until you reconnect it" is
|
||||
// the thing that makes them act. This pins the distinction the copy was written for — a future
|
||||
// shortening to a bare "Meghajtó hiányzik." would pass the presence test above and lose the point.
|
||||
func TestBackupTargetAbsentMessageNamesTheConsequence(t *testing.T) {
|
||||
msg := customerMessages["backup_target_absent"]
|
||||
if !strings.Contains(msg, "rendszermentés") {
|
||||
t.Errorf("backup_target_absent must name WHAT is at risk (the rendszermentés), got: %q", msg)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(msg), "nem készül") {
|
||||
t.Errorf("backup_target_absent must state the consequence (the backup will not be made), got: %q", msg)
|
||||
}
|
||||
}
|
||||
@@ -59,14 +59,14 @@ var customerMessages = map[string]string{
|
||||
// Customer-claim arc (v0.50.0)
|
||||
"claim_lockout": "Túl sok hibás beállító/visszaállító kód próbálkozás történt — a beállító oldal 15 percre zárolva lett. Ha nem te próbálkoztál, jelezd az üzemeltetőnek.",
|
||||
// Backup events
|
||||
"backup_completed": "A biztonsági mentés sikeresen elkészült.",
|
||||
"backup_failed": "A biztonsági mentés sikertelen! Kérjük, ellenőrizd a rendszert.",
|
||||
"db_dump_completed": "Az adatbázis mentés sikeresen elkészült.",
|
||||
"db_dump_failed": "Az adatbázis mentés sikertelen!",
|
||||
"backup_integrity_ok": "A mentés integritás ellenőrzés sikeres.",
|
||||
"backup_integrity_failed": "A mentés integritás ellenőrzés hibát talált!",
|
||||
"crossdrive_completed": "A másodlagos mentés sikeresen elkészült.",
|
||||
"crossdrive_failed": "A másodlagos mentés sikertelen!",
|
||||
"backup_completed": "A biztonsági mentés sikeresen elkészült.",
|
||||
"backup_failed": "A biztonsági mentés sikertelen! Kérjük, ellenőrizd a rendszert.",
|
||||
"db_dump_completed": "Az adatbázis mentés sikeresen elkészült.",
|
||||
"db_dump_failed": "Az adatbázis mentés sikertelen!",
|
||||
"backup_integrity_ok": "A mentés integritás ellenőrzés sikeres.",
|
||||
"backup_integrity_failed": "A mentés integritás ellenőrzés hibát talált!",
|
||||
"crossdrive_completed": "A másodlagos mentés sikeresen elkészült.",
|
||||
"crossdrive_failed": "A másodlagos mentés sikertelen!",
|
||||
// Offsite-repo continuity (controller v0.142.0)
|
||||
"offbox_repo_orphaned": "A távoli mentési tároló elárvult: a benne lévő mentések egy korábbi, már nem elérhető kulccsal készültek (jellemzően újratelepítés után). Új mentés a tároló visszaállításáig nem készül — nyisd meg a Távoli mentés oldalt.",
|
||||
"offbox_repo_reset": "A távoli mentési tároló visszaállítva: a régi előzmény félretéve (nem törölve), és egy üres, új tároló jött létre a mostani kulccsal.",
|
||||
@@ -87,6 +87,12 @@ var customerMessages = map[string]string{
|
||||
"storage_disconnected": "Egy meghajtó leválasztva — a mentések szünetelhetnek.",
|
||||
"storage_reconnected": "A meghajtó újra csatlakoztatva.",
|
||||
|
||||
// E-2 — the drive the whole-system backup is written to is gone. Deliberately NOT folded into
|
||||
// storage_disconnected: the customer action differs (reconnect THIS drive, or the system backup
|
||||
// stops surviving a disk failure), and the fallback is honest about what still protects them.
|
||||
"backup_target_absent": "A rendszermentés meghajtója nem érhető el — amíg vissza nem csatlakoztatod, a teljes rendszermentés nem készül el.",
|
||||
"backup_target_restored": "A rendszermentés meghajtója újra elérhető — a mentés folytatódik.",
|
||||
|
||||
// Staleness events (Hub-generated)
|
||||
"node_stale": "A szerver nem küldött jelentést az elmúlt időszakban.",
|
||||
"node_down": "A szerver nem elérhető!",
|
||||
|
||||
Reference in New Issue
Block a user