1c47030854
Restore a captured volume snapshot onto an image workload's live host-bind
data volumes, then redeploy — the most destructive workload action, built to
the adversarially-reviewed design (C1–C6) with all data-loss guards.
- Engine.Restore (engine-owned): all-or-nothing pre-flight re-resolution from
the workload's CURRENT config (never the tamperable manifest), per-filesystem
disk pre-check, per-workload lock, container quiesce, extract-to-tmp, durable
pre-restore snapshot, write-ahead journal, atomic rename swap, redeploy, and
crash-recovery sweep (RecoverInterruptedRestores) wired before serving.
- internal/keyedmutex: shared per-key lock; deployer now serializes every
deploy entrypoint per workload via DispatchPlugin (+ LockWorkload/RedeployLocked
for the restore re-dispatch, no deadlock).
- Untrusted-archive extractor: zip-slip containment, type allow-list (reg/dir
only), decompression-bomb cap, manifest-index bounds.
- POST /api/workloads/{id}/snapshots/{sid}/restore: admin, X-Confirm-Restore
header (CSRF), per-workload single-flight (409).
- WebUI: Restore button + danger ConfirmDialog + busy state + i18n (en/ru).
Scope: image-source only; scopes absolute/stage/project (driven off the same
supportedScopes constant capture uses).
Plan-reviewed before coding; per-phase go/security/ts reviews; final review
READY TO MERGE. Security review caught + fixed a CRITICAL manifest-Source path
traversal (re-derive target from current config + base containment).
Plan: plans/volume-snapshot-restore/
71 lines
3.6 KiB
Markdown
71 lines
3.6 KiB
Markdown
# Phase 4: UI Restore button + ConfirmDialog + i18n en+ru
|
|
|
|
**Status:** ⬜ Not Started
|
|
**Parent plan:** [PLAN.md](./PLAN.md)
|
|
**Domain:** frontend
|
|
|
|
Built by the **frontend implementer agent**. Must follow project conventions: Svelte 5
|
|
runes, `ConfirmDialog` for the destructive action (NEVER `window.confirm`), `$t` with
|
|
**en+ru parity**, the existing `.panel`/`.forge-btn-ghost` vocabulary in
|
|
`WorkloadSnapshotsPanel.svelte`.
|
|
|
|
## Tasks
|
|
|
|
- [ ] **`web/src/lib/api.ts`**: `restoreSnapshot(workloadId: string, sid: string): Promise<void>`
|
|
— POST `/api/workloads/${workloadId}/snapshots/${sid}/restore` with header
|
|
`X-Confirm-Restore: ${sid}`. Check how the DB restore sends its `X-Confirm-Restore` header
|
|
and reuse that fetch mechanism (the typed `post<T>` may need a header-capable variant or a
|
|
raw `fetch` like `download` already uses).
|
|
- [ ] **`WorkloadSnapshotsPanel.svelte`**:
|
|
- add a **Restore** action per snapshot row (beside Download/Delete) → opens `ConfirmDialog`.
|
|
- ConfirmDialog: strong destructive copy — title + message making clear it **overwrites
|
|
live data and restarts the app**, and that a **pre-restore snapshot is auto-captured**;
|
|
`confirmVariant="danger"`.
|
|
- on confirm: call `restoreSnapshot`, show a "restoring…" busy state (disable row actions),
|
|
toast success/failure, then `load()` to refresh.
|
|
- update the file's top comment (currently "Restore is intentionally NOT here yet") to
|
|
reflect that restore now ships.
|
|
- [ ] **i18n**: add `apps.detail.snapshots.restore`, `.restoring`, `.restored`,
|
|
`.restoreFailed`, `.confirmRestoreTitle`, `.confirmRestoreMessage` to BOTH
|
|
`web/src/lib/i18n/en.json` and `web/src/lib/i18n/ru.json`. Verify parity manually (a missing
|
|
key is NOT a build error — `$t` returns the key string).
|
|
|
|
## Files to Modify/Create
|
|
|
|
- `web/src/lib/api.ts` — `restoreSnapshot`
|
|
- `web/src/lib/components/WorkloadSnapshotsPanel.svelte` — Restore button + ConfirmDialog + busy state
|
|
- `web/src/lib/i18n/en.json`, `web/src/lib/i18n/ru.json` — restore keys (parity)
|
|
|
|
## Acceptance Criteria
|
|
|
|
- `npm run check` 0 errors; `npm run build` succeeds; `npm run test` green.
|
|
- en/ru key parity equal (every new key in both files).
|
|
- ConfirmDialog used (no native confirm/alert); danger variant; copy warns about data overwrite + app restart.
|
|
- Restore button disabled while a restore is in flight.
|
|
- Restart dev server (`./scripts/dev-server.sh`).
|
|
|
|
## Notes
|
|
|
|
- Only image-source workloads expose snapshots, so no source-kind gating is needed in the panel beyond what already exists.
|
|
- Keep the Restore button visually subordinate to Download but clearly destructive (danger styling) — it's the most dangerous action in the panel.
|
|
|
|
## Review Checklist
|
|
|
|
- [ ] All tasks completed
|
|
- [ ] Code follows project conventions (ToggleSwitch/ConfirmDialog rules, runes)
|
|
- [ ] No unintended side effects
|
|
- [ ] Build passes (`npm run check` + `npm run build`)
|
|
- [ ] Tests pass; i18n parity verified
|
|
|
|
## Handoff to Next Phase
|
|
|
|
Implemented: `api.restoreSnapshot(workloadId, sid)` (POST + `X-Confirm-Restore` header,
|
|
mirrors `restoreBackup`); `WorkloadSnapshotsPanel.svelte` Restore button per row →
|
|
`ConfirmDialog` (danger, warns: overwrites live data + restarts app + auto pre-restore
|
|
snapshot) → `doRestore` with busy state (`restoringId` disables all row actions, active row
|
|
shows "Restoring…"); i18n `apps.detail.snapshots.restore*` in en+ru (parity verified).
|
|
|
|
Verify: `npm run check` 0 errors, `npm run build` OK, `npm run test` 26 pass; i18n parity
|
|
equal; dev server restarted on :9000. typescript-reviewer: APPROVE (no blockers; one cosmetic
|
|
wording note on the success toast addressed). Final phase — done.
|