a6acb0f762
gates / gates (push) Successful in 6m27s
Security review of 50e47e29: requested_at is the box's clock — a future value is cut to the hub's
now; CustomerDeliberatelyOff now needs every host of the customer held (one machine of two no
longer silences the other), and fails closed on a read error or no host. Both red-proofed.
75 lines
2.8 KiB
Go
75 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
|
)
|
|
|
|
// Event types of the household power-off (R-998, doc 14). Both are info: recorded on the customer's event list,
|
|
// never mailed (the dispatcher mails warning and above).
|
|
const (
|
|
eventHostDeliberateOff = "host_deliberate_off"
|
|
eventHostDeliberateOffEnded = "host_deliberate_off_ended"
|
|
)
|
|
|
|
// checkDeliberateOff keeps the host row's deliberate-off time in step with the report: set while the report
|
|
// carries the field, cleared by the first report without it (the box is in a new boot — switched back on).
|
|
func (h *Handler) checkDeliberateOff(hostID, custID string, rep *hostReportPayload) {
|
|
var at *time.Time
|
|
if rep.DeliberateOff != nil && rep.DeliberateOff.RequestedAt != "" {
|
|
t, err := time.Parse(time.RFC3339, rep.DeliberateOff.RequestedAt)
|
|
if err != nil {
|
|
h.logger.Printf("[WARN] host-report from %s: unreadable deliberate_off.requested_at %q — ignored", hostID, rep.DeliberateOff.RequestedAt)
|
|
return
|
|
}
|
|
t = t.UTC()
|
|
// The time is the BOX's clock (security review 2026-10-13): a future value would stretch the hold past 14
|
|
// days, so it is cut to the hub's own now. An earlier value only shortens the hold — the safe direction.
|
|
if now := time.Now().UTC().Truncate(time.Second); t.After(now) {
|
|
h.logger.Printf("[WARN] host-report from %s: deliberate_off.requested_at %s is in the future — using the hub's time", hostID, t.Format(time.RFC3339))
|
|
t = now
|
|
}
|
|
at = &t
|
|
}
|
|
prev, err := h.store.SetHostDeliberateOff(hostID, at)
|
|
if err != nil {
|
|
h.logger.Printf("[WARN] host-report from %s: deliberate-off not stored: %v", hostID, err)
|
|
return
|
|
}
|
|
switch {
|
|
case at != nil && (prev == nil || !prev.Equal(*at)):
|
|
msg := "Switched off by the household at " + at.Format("2006-01-02 15:04 UTC") +
|
|
" — silence alarms held for " + holdDays() + " days"
|
|
h.logger.Printf("[INFO] host %s: deliberate power-off recorded (%s)", hostID, at.Format(time.RFC3339))
|
|
h.saveInfoEvent(custID, eventHostDeliberateOff, msg)
|
|
case at == nil && prev != nil:
|
|
msg := "Reporting again after the household's power-off of " + prev.Format("2006-01-02 15:04 UTC")
|
|
h.logger.Printf("[INFO] host %s: deliberate power-off ended (was %s)", hostID, prev.Format(time.RFC3339))
|
|
h.saveInfoEvent(custID, eventHostDeliberateOffEnded, msg)
|
|
}
|
|
}
|
|
|
|
func holdDays() string {
|
|
d := int(store.DeliberateOffHold / (24 * time.Hour))
|
|
return itoa(d)
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var b []byte
|
|
for n > 0 {
|
|
b = append([]byte{byte('0' + n%10)}, b...)
|
|
n /= 10
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func (h *Handler) saveInfoEvent(custID, eventType, msg string) {
|
|
if _, err := h.store.SaveEvent(custID, eventType, "info", msg, "{}", "hub"); err != nil {
|
|
h.logger.Printf("[WARN] save %s event: %v", eventType, err)
|
|
}
|
|
}
|