# CONTEXT — Volume Snapshot Restore Working memory across phases. The orchestrator owns this file. ## Settings (from PLAN.md header) - Mode: **Automated** · Execution: **Hybrid** (backend Direct, Phase 4 frontend implementer) · Strategy: **Incremental** - Base: `main` · Branch: `feature/volume-snapshot-restore` · Remote: origin (Gitea) - Build: `go build ./...` · Test: `go test ./internal/...` + `npm run test` · Lint: `go vet ./internal/...` + `npm run check` ## Key codebase facts (verified during planning) - **Deploy choke point:** every deploy entrypoint calls `deployer.DispatchPlugin` → put the per-workload lock there (C1). Entrypoints: `deployPluginWorkload`, `rollbackWorkload`, `promoteFromWorkload`, `dispatchGeneric`, webhook `fireBinding`/`handlePreviewIntent`. - **`activeWg`/`drainMu`** in `deployer.go` = global drain barrier, NOT a per-workload lock. - **Image idempotency short-circuit** (`image.go` Deploy ~L170-181) only fires for a *verified-running* container → after stop, redeploy makes a fresh container; blue-green `enforceMaxInstances` reaps the old stopped one. ⇒ stop→swap→redeploy (C4) is correct. - **Scope resolution** (`internal/volume/resolver.go`): stage/project → `//` (shared per-workload dir); absolute → operator's allowed path. Stage tmp/old siblings under the live dir's PARENT so renames are same-fs (R2). - **`volsnap.Engine`** has `e.mu` taken by Create/Delete/pruneWorkload/CleanOrphans. `Restore` must NOT hold `e.mu` (R1). - **Archive layout:** gzip tar, each volume under integer subdir `0/`,`1/`…, `manifest.json` at root = `[]SnapshotVolume{Index,Target,Scope,Source}`. `supportedScopes` = absolute/stage/project (volumes.go). - **Precedent:** `internal/api/backups.go` `restoreBackup` — X-Confirm-Restore==id, `restoreInFlight` CAS→409, pre-restore safety backup, atomic rename swap. - **Composition root:** `cmd/server/main.go` constructs `deployer.New` + `volsnap.New` + `docker` + `store`; calls `CleanOrphans` at startup (wire `RecoverInterruptedRestores` there). - **Frontend:** `WorkloadSnapshotsPanel.svelte`; api fns `web/src/lib/api.ts` ~L581; i18n `apps.detail.snapshots.*` in en.json + ru.json. - `golang.org/x/sys v0.33.0` already in go.mod (indirect); build-tag precedent exists (`lockfile_windows.go`/`lockfile_unix.go`). ## Decisions / invariants - `Engine.Restore` holds NO `e.mu`; per-workload `Lifecycle.Lock` is the serialization. - Extract ALL tmp dirs BEFORE any rename; swap is pure renames; journal tracks per-volume `swapped`. - Pre-restore snapshot captured AFTER stop, BEFORE first rename (durable escape hatch). - Redeploy pins the newest-running container's tag (same version back up). - Mixed per-volume state after a mid-restore crash is an accepted v1 limit (each volume intact; pre-restore snapshot = full revert). ## Deferred / out of scope - Named/project_named/instance/ephemeral scopes (consistent with capture). - Non-image sources. - Fully-atomic all-volumes-or-nothing restore (v1 is per-volume atomic + journal recovery). ## Failed approaches / gotchas - (none yet) ## Phase handoffs - Phase 1 → 2: _(filled after Phase 1)_ - Phase 2 → 3: _(filled after Phase 2)_ - Phase 3 → 4: _(filled after Phase 3)_