R-241 part 5: escalating reminders, and operator levers for a running countdown
REMINDERS (SEC 2.3). The offer epoch now stamps when it began, and the undecided reminder escalates in EMPHASIS at 1, 3, 7 and 14 days. THE READING IS STATED BECAUSE THE SPEC IS AMBIGUOUS, and it is written into the code where it can be corrected. For an ABANDONING box, 5/3/1 are unambiguously days REMAINING before a deletion. An undecided box has no deadline - nothing counts down to anything, because SEC 7.5 deliberately does NOT auto-abandon - so 14/7/3/1 cannot be "remaining" and are taken as days ELAPSED, with the wording firming up rather than the bar appearing and disappearing. If the operator meant something else, one function changes. The stamp is re-set on every entry into the offered state, so a box that settles and is later rebuilt starts its ladder again instead of inheriting an old one. OPERATOR LEVERS (SEC 7.5). --abandon-status, --abandon-extend=N and --abandon-stop on the controller CLI, beside the existing operator subcommands. They exist because the path that ACTUALLY happens is the customer telephoning, and support needs something to press. They live on the CLI and not in the customer UI deliberately: extending a deletion the customer asked for is an operator judgement, and a customer who wants it stopped already has the self-service route - they recover with their code, which cancels it. BOTH REFUSE RATHER THAN NO-OP, in two situations: when no countdown is running, and when the store has already been deleted. A silent success is the thing an operator most easily mistakes for "handled" - they would tell the customer their data was safe when it is gone. Pinned by two tests. --abandon-extend counts from NOW, not from the old due date, and a test proves the old date passes without deleting anything. Green: go build, go vet, go test ./... all pass; controller gates OK.
This commit is contained in:
@@ -78,6 +78,12 @@ func main() {
|
||||
recoverOffsiteCheck := flag.Bool("recover-offsite-check", false, "R-200 diagnostic: read the customer recovery code from STDIN, recover the offsite repository password from the hub-held sealed escrow via the agent, and report whether it matches the one on disk — BY HASH. Compares, never installs; writes nothing. Exit 0 = match, 2 = clean mismatch, 1 = a step failed.")
|
||||
recoverOffsiteInstall := flag.Bool("recover-offsite-install", false, "R-200: read the customer recovery code from STDIN, recover the offsite repository password, print both hashes, and — with --confirm-install — PLACE it so the existing repository reopens. Without --confirm-install it is a dry run that writes nothing.")
|
||||
confirmInstall := flag.Bool("confirm-install", false, "Required alongside --recover-offsite-install to actually write the recovered repository password. Deliberately a second invocation so the hashes are seen before any write is possible.")
|
||||
// §7.5 (R-241): the operator levers for a running abandonment countdown. The automatic 30-day
|
||||
// ending is deliberately NOT built (R-245) — what is built is the path that actually happens,
|
||||
// which is the customer getting in touch and support needing something to press.
|
||||
abandonStatus := flag.Bool("abandon-status", false, "R-241: print the state of this box's off-site abandonment countdown (set-aside path, due date, days left) and exit. Read-only.")
|
||||
abandonExtend := flag.Int("abandon-extend", 0, "R-241 (operator): extend a running abandonment countdown by N days from now, then exit. Refuses when no countdown is running.")
|
||||
abandonStop := flag.Bool("abandon-stop", false, "R-241 (operator): stop a running abandonment countdown, then exit. The set-aside history is kept and nothing is deleted. Refuses when no countdown is running.")
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
@@ -152,6 +158,52 @@ func main() {
|
||||
}))
|
||||
}
|
||||
|
||||
// §7.5 (R-241) — the operator's abandonment levers. Grouped in one block, before the server
|
||||
// starts, exactly like the other CLI subcommands: each loads config + settings, acts, and exits.
|
||||
if *abandonStatus || *abandonExtend > 0 || *abandonStop {
|
||||
cfg, err := config.LoadPermissive(*configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "abandon: loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
lg := log.New(os.Stderr, "", 0)
|
||||
sett, err := settings.Load(cfg.Paths.DataDir+"/settings.json", lg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "abandon: loading settings: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
mgr := backup.NewManager(cfg, sett, lg)
|
||||
st := mgr.AbandonStatus()
|
||||
switch {
|
||||
case *abandonStop:
|
||||
if serr := mgr.StopAbandon(); serr != nil {
|
||||
fmt.Fprintf(os.Stderr, "abandon-stop: %v\n", serr)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Printf("abandonment STOPPED — the set-aside history at %s is kept; nothing was deleted\n", st.RepoPath)
|
||||
case *abandonExtend > 0:
|
||||
due, eerr := mgr.ExtendAbandon(*abandonExtend)
|
||||
if eerr != nil {
|
||||
fmt.Fprintf(os.Stderr, "abandon-extend: %v\n", eerr)
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Printf("abandonment EXTENDED by %d day(s) — the set-aside history at %s is now deleted on %s\n",
|
||||
*abandonExtend, st.RepoPath, due.Format("2006-01-02"))
|
||||
default:
|
||||
if !st.Active && !st.PurgeRequested {
|
||||
fmt.Println("no abandonment countdown is running on this box")
|
||||
os.Exit(0)
|
||||
}
|
||||
if st.PurgeRequested {
|
||||
fmt.Printf("set-aside history DELETED at %s; awaiting the hub to drop the sealed package\n", st.RepoPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Printf("abandonment countdown RUNNING\n set-aside history : %s\n chosen on : %s\n deleted on : %s\n days left : %d\n",
|
||||
st.RepoPath, st.StartedAt.Format("2006-01-02"), st.DueAt.Format("2006-01-02"), st.DaysLeft)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *printResetCode {
|
||||
cfg, err := config.LoadPermissive(*configPath)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user