hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
The hub has written every customer e-mail in Hungarian whatever the box was set to. The box has published its language since controller v0.247.0; nothing read it. Now it does. Nothing an operator reads changes. The Hungarian mails are byte-identical, and that is a diff rather than a reading: 56 goldens per language captured from v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after every sentence was routed through the new bundle. - internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0. - customerMessages/severityLabels are DERIVED from the bundle, so a sentence is written in one place and all 40+ tests that read those maps still work. - Language order: last reported -> created-with -> hu. reports.language defaults to EMPTY, never hu: "never told us" is not "chose Hungarian". - message_customer on POST /api/v1/event, additive and optional forever, for the sentences the box composes and the hub cannot translate. - The bind page is per-language, and its `expired` state stays Hungarian: it is the state an unknown token lands in, so rendering a real English customer's token in English would make the LANGUAGE answer what the TEXT refuses to. Two defects found inside the release: - R-581: the newest report was picked by received_at, which has SECOND granularity, so same-second reports tied and the winner was arbitrary. Ordered by the autoincrement id now. GetCustomers() still has the shape - row open. - R-582: the English copy-guard stems, ported word for word from Hungarian, convicted 141 honest sentences. The English claim is a phrase with a modal. R-555 closed: the language allowlist entry is out of wire_contract_gate.py. hub_copy_gate.py follows the sentences into the bundle - without that it would have scanned four files that no longer hold any customer text and reported success. Three new decoys incl. an innocent control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
+26
-13
@@ -2065,11 +2065,11 @@ var allowedEventTypes = map[string]bool{
|
||||
// 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
|
||||
// specifics via the templates.go fallback priority).
|
||||
"disk_health_degraded": true,
|
||||
"health_degraded": true,
|
||||
"health_critical": true,
|
||||
"health_recovered": true,
|
||||
"app_deployed": true,
|
||||
"disk_health_degraded": true,
|
||||
"health_degraded": true,
|
||||
"health_critical": true,
|
||||
"health_recovered": true,
|
||||
"app_deployed": true,
|
||||
// R-536 (controller v0.244.0). `app_deployed` used to fire beside the 202 that merely ACCEPTED a
|
||||
// deploy, so an install interrupted five seconds later stood on the timeline as a completed one
|
||||
// — measured 2026-09-16 on the drill box with mealie, which ended `not_deployed`. The accept-time
|
||||
@@ -2079,8 +2079,8 @@ var allowedEventTypes = map[string]bool{
|
||||
//
|
||||
// Both are customer-tier like `app_deployed` itself (NOT in operatorOnlyEvents): a household that
|
||||
// pressed „Telepítés" is the party who wants to know it did not finish.
|
||||
"app_deploy_started": true,
|
||||
"app_deploy_failed": true,
|
||||
"app_deploy_started": true,
|
||||
"app_deploy_failed": true,
|
||||
"app_removed": true,
|
||||
"app_start_failed": true, // controller fix-3 (CAMPAIGN-3): a deployed app is not running
|
||||
"disaster_recovery_started": true,
|
||||
@@ -2152,11 +2152,18 @@ func (h *Handler) handleEvent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
CustomerID string `json:"customer_id"`
|
||||
EventType string `json:"event_type"`
|
||||
Severity string `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
Details json.RawMessage `json:"details"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
EventType string `json:"event_type"`
|
||||
Severity string `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
// MessageCustomer (controller v0.256.0, R-558) — the SAME sentence as Message, written in
|
||||
// the household's language. Optional forever: an older controller sends none, and a box
|
||||
// that never will (a parked one) must keep working exactly as it does today.
|
||||
//
|
||||
// It is used ONLY to render the household's e-mail. Message stays the Hungarian one that is
|
||||
// stored, logged and mailed to the operator, so nothing an operator reads moves.
|
||||
MessageCustomer string `json:"message_customer"`
|
||||
Details json.RawMessage `json:"details"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
@@ -2208,6 +2215,11 @@ func (h *Handler) handleEvent(w http.ResponseWriter, r *http.Request) {
|
||||
payload.Severity = "info"
|
||||
}
|
||||
|
||||
// NO SEPARATE LENGTH CAP, and that is deliberate rather than an omission. `message` has never
|
||||
// had one either: both are bounded by the 1 MB LimitReader on this request body, which is the
|
||||
// only cap that has ever existed here. Adding one to the new field alone would make the two
|
||||
// sentences behave differently for no measured reason — and a byte-count truncation would cut a
|
||||
// UTF-8 sequence in half, turning a long Hungarian sentence into mojibake in a customer's inbox.
|
||||
// Store details as JSON string
|
||||
detailsStr := "{}"
|
||||
if len(payload.Details) > 0 && string(payload.Details) != "null" {
|
||||
@@ -2225,7 +2237,8 @@ func (h *Handler) handleEvent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Dispatch notifications (non-blocking)
|
||||
if h.dispatcher != nil {
|
||||
go h.dispatcher.ProcessEvent(payload.CustomerID, payload.EventType, payload.Severity, payload.Message, detailsStr, "controller")
|
||||
go h.dispatcher.ProcessBoxEvent(payload.CustomerID, payload.EventType, payload.Severity,
|
||||
payload.Message, payload.MessageCustomer, detailsStr, "controller")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
Reference in New Issue
Block a user