Skip to content

fix(restore): name the restore in the hook log and in the install conflict (#493, #494) - #519

Merged
pofallon merged 1 commit into
mainfrom
fix/493-494-restore-log-vocab-and-conflict
Sep 22, 2026
Merged

pofallon merged 1 commit into
mainfrom
fix/493-494-restore-log-vocab-and-conflict

Conversation

@pofallon

Copy link
Copy Markdown
Contributor

Two operator-facing papercuts found on the same spec 007 disposable-VM pass (task T079), fixed together because they are the same class of bug — the platform knowing a restore is happening and not saying so. One branch, two independent sections.

#494 — a restore hook logged as "Running backup preHook"

Spec 007 runs restore hooks through runPreHooksFailClosed/runPostHooks verbatim (research R14). That sharing is right: declaration order, fail-closed and started-only cleanup are the same policy. What was not shareable is the noun — the runner hardcoded "backup", so the restore hook that loads a dump into the target announced itself as a backup hook, in exactly the log an operator reads when a failing restore hook has just failed their whole install (FR-021).

  • The runner is not forked. It takes a new required HookVocabulary ('backup' | 'restore') that every caller must name — no default, so a future third caller cannot silently inherit this one's word.
  • runBackupHookrunParticipationHook: a method called runBackupHook('restore') is the same confusion one level down.
  • The noun is now on every line of a hook run, not just the first. That is deliberate and is the one behaviour change on the backup path: preHook ok: … becomes backup preHook ok: …. The issue names this as part of the bug ("both say preHook, and only the deploymentId differs between quiescing the source and loading the target"), so a noun on the Running line alone does not fix it. Running backup preHook/postHook are unchanged, and the format matches the existing Running push postHook / push postHook failed precedent. Nothing parses these strings (grepped: server logger + job log only).

Before → after for the run in the issue:

Running backup preHook    {deploymentId: mealie-7f61df68}   # source quiesce — was right, still right
backup preHook ok: mealie-7f61df68/default
Running backup postHook   {deploymentId: mealie-7f61df68}
Running restore preHook   {deploymentId: mealie-22900b3f}   # was "Running backup preHook"
restore preHook ok: mealie-22900b3f/default                 # was "preHook ok: …"

The restore-source quiesce keeps the backup vocabulary, because those genuinely are the source's backup@1 hooks.

#493 — restoring from a live source always collides with the single-instance guard

A restore candidate is by definition live (FR-048/FR-004a), so for a single-instance app — most of the catalog, including every Postgres-backed app that most needs restore — --restore-from always trips spec 005's guard. allowMultiple is the normal path here, and nothing surfaced that.

Implemented as decided: a restore choice does NOT imply allowMultiple. I agree with the decision. The restored copy comes up live, holding the source's credentials, beside a source that keeps running — the exact risk spec 007's own summary acknowledgement warns about (jobs/webhooks firing against real systems). Inferring "run two live copies of this app" from "restore from this one" would take a consent the operator gave for one thing and spend it on another. The guard also stays honest for the disaster-recovery case: once a source can be gone (spec 008's provider origin), the collision disappears on its own rather than having been papered over.

So the refusal explains itself instead:

  1. Server (assertInstanceAllowed): details gains restore { candidateId, candidateIsExisting }, only when the refused install carried a restoreFrom. Added to the spec 005 shape, never repurposing it — the top-level CONFLICT and details.code are untouched, and a non-restore refusal is byte-identical (asserted with toEqual). candidateIsExisting separates the two honest sentences: the copy in the way is the source (ordinary local restore), or it is some other copy (a provider-held capture restored onto a host already running the app). The verdict is unchanged; the guard only got more explicable. Messages stay surface-neutral (SC-003).
  2. CLI: the ALREADY_INSTALLED branch now has two arms, both built from details and never from the message. With details.restore it pairs the flags and drops the channel advice (switching a channel is no route to a restore); without it, the spec 005 hint is byte-identical (asserted with toBe on the exact string).
  3. Wizard: the existing ALREADY_INSTALLED panel gains one sentence when details.restore is present ("…this install would be a second live copy…Continue only if you mean to run both"), and the restore step now says up front that the copy you restore from keeps running. No redesign: the way forward — "Install another copy (operator override)" — already existed and already sends allowMultiple: true. details.restore is narrowed separately and dropped when malformed, so a pre-restore-on-install: restoring from a live source needs allowMultiple for single-instance apps, and nothing says so #493 server never degrades the spec 005 panel.
  4. Docs: spec 007 contracts/cli.md (a section on the pairing + a row in the hint table + the "Unchanged" note corrected), contracts/api.md (the added details), docs/OPERATIONS.md (an operator-terms paragraph in Restore-on-install + the restore variant of the conflict hint), and the API explorer's CONFLICT row.

Tests

typecheck → lint → typecheck → test → build, all green, foreground. Server 1397 → 1400, web 377 → 379, CLI 286 → 289 (8 added, 0 failures anywhere; no web flake seen).

Revert-proofed — each production change reverted in turn, matching tests confirmed failing, then restored:

reverted failing tests
#494 vocabulary in the log strings 1 (#494: the restore hook logs restore vocabulary…)
#493 details.restore on the server 1 (#493: …refused with the restore named in details)
#493 CLI restore arm 2 (both #493 hint tests)
#493 wizard panel sentence 1 (#493: with restore details…)

In every case the paired regression test (non-restore refusal / hint / panel unchanged) kept passing, which is the point of having it.

Closes #493
Closes #494

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh

…flict (#493, #494)

Two operator-facing papercuts from the same spec 007 disposable-VM pass.

#494 — the shared hook runner logged the backup vocabulary for every
caller, so a restore hook announced itself as "Running backup preHook":
exactly the log an operator reads when a failing restore hook has failed
their whole install (FR-021), pointing them at the wrong subsystem. The
runner stays shared — declaration order, fail-closed and started-only
cleanup are the same policy (research R14) — and instead takes a required
`HookVocabulary` ('backup' | 'restore') that every caller must name, so a
future third caller cannot silently inherit the wrong noun. The noun is
now on every line of a hook run, not just the first
(`backup preHook ok:` / `restore preHook ok:`), which is what made the
source-quiesce and target-load phases distinguishable without
cross-referencing deployment ids. `runBackupHook` becomes
`runParticipationHook` for the same reason.

#493 — a restore source is by definition live, so for a single-instance
app the restoring install always trips spec 005's guard: `allowMultiple`
is the normal path here, and nothing surfaced that. A restore choice
deliberately still does NOT imply the override — the source keeps
running and the restored copy resumes with live credentials, which is
precisely what an operator should confirm rather than inherit from a
different choice — so the refusal explains itself instead:
`details.restore { candidateId, candidateIsExisting }` is ADDED to the
existing `ALREADY_INSTALLED` shape (same top-level CONFLICT, same
`details.code`, byte-identical when no restore is in play). The CLI
pairs `--restore-from` with `--allow-multiple` from `details`, never the
message; the wizard's conflict panel says the install would be a second
live copy, and its restore step now sets that expectation up front.

Docs: spec 007 contracts/cli.md + api.md record the pairing and the added
details, OPERATIONS.md's restore section says it in operator terms, and
the API explorer's CONFLICT row lists the new field.

Closes #493
Closes #494

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vck5KSX2CLxhohx14nb5Sh
@pofallon
pofallon merged commit d76bdd3 into main Sep 22, 2026
3 checks passed
@pofallon
pofallon deleted the fix/493-494-restore-log-vocab-and-conflict branch September 22, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant