Files
felhom-controller/controller/cmd/controller/opactions_wiring.go
T
admin e06680b797 Operator actions in the report reply (D1, R-314/R-279/R-177, decision 185)
The hub may ask the running controller for a CLOSED list of actions,
carried in the report ACK (operator_actions) and answered on the next
report (operator_action_results): offsite_backup_now, abandon_stop,
abandon_extend (1-30 days), run_job (fill-watch, offsite-integrity,
offsite-proof, disk-health-check). Unknown action/job/argument -> refused,
nothing called. Once per id (in memory; every action is safe to repeat).

- internal/report/opactions.go: the executor; results re-sent until the
  hub stops listing the id.
- scheduler.RunNow: refuses unknown / already-running jobs; OnDemand(ctx)
  makes an operator's offsite-integrity run even when not due.
- ExtendAbandon never shortens the countdown and refuses in the hub phase;
  StopAbandon reports failure when the hub cancel failed (it said success).
- Report ACK read cap 4 KiB -> 64 KiB (an ACK over the cap dropped every
  field in it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-08 14:49:27 +02:00

61 lines
2.3 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"log"
"sync/atomic"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/report"
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
)
// opActionReportFire holds the out-of-cycle report trigger once main has built it (func()). The
// executor is built earlier than the trigger, so it reads it through this holder.
var opActionReportFire atomic.Value
// operatorActionHandlers wires the four operator actions (R-314/R-279/R-177, `09` §3 decision 185)
// to the running process. ONE wiring site, called from main and from the wiring test, so the test
// exercises the doors production uses (the seam-built-but-never-wired trap).
//
// offsite_backup_now makes the SAME four checks as the household's „back up now"
// (offboxRunHandler) and then runs the same call, so an operator press can never do what the
// household's own press cannot.
func operatorActionHandlers(backupMgr *backup.Manager, sched *scheduler.Scheduler, resultReady func(), logger *log.Logger) report.OperatorActionHandlers {
h := report.OperatorActionHandlers{ResultReady: resultReady}
if sched != nil {
h.RunJob = sched.RunNow
}
if backupMgr == nil {
return h
}
h.OffsiteBackupNow = func() (string, func(ctx context.Context) error) {
switch {
case !backupMgr.OffboxConfigured():
return "no off-site target is set on this box", nil
case !backupMgr.OffboxRunnable():
return "the off-site key step (escrow) is not done yet — the run is refused until it is", nil
case backupMgr.OffboxOrphaned():
return "the off-site store is orphaned (written with an older key) — a run cannot write to it", nil
case backupMgr.IsRunning():
return "a backup run is already in flight", nil
}
return "", func(ctx context.Context) error {
if logger != nil {
logger.Printf("[INFO] [opaction] off-site backup started at the operator's request")
}
err := backupMgr.RunOffboxBackupWithProgress(ctx)
if errors.Is(err, backup.ErrOffboxRunInFlight) {
return fmt.Errorf("%w: %v", report.ErrActionRefused, err)
}
return err
}
}
h.AbandonStop = backupMgr.StopAbandon
h.AbandonExtend = func(days int) (time.Time, error) { return backupMgr.ExtendAbandon(days) }
return h
}