e06680b797
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
61 lines
2.3 KiB
Go
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
|
|
}
|