# 11-Apr-2026 — consolidated MongoDB + S3 incremental backup with cross-environment restore [#187](https://github.com/nbarrett/ngx-ramblers/issues/187)

## [build 585](https://github.com/nbarrett/ngx-ramblers/actions/runs/24284115184) — [commit ae900be](https://github.com/nbarrett/ngx-ramblers/commit/ae900be37df0ffc927918a3a0c219fe9edf14ca5)

_____

### What changed for users

- A single **Start Backup** action now captures both the MongoDB database AND the site's S3 objects in one timestamp-aligned operation, surfaced through the existing Backup/Restore admin tab
- A single **Start Restore** action rolls both Mongo and the S3 bucket back to the chosen snapshot, automatically walking cross-environment when the selected backup originated from a different environment
- Backup/Restore History gains an **S3 Objects** column showing per-session copied/skipped counts, bytes transferred, and status
- Backup and restore completion emails now include a structured S3 summary table (per-site status, totals, copied/restored and skipped counts, copied and total size) so notifications reflect the full scope of the consolidated operation
- S3 backups are **incremental** after the first run: unchanged objects (matched by `ETag` against the previous manifest) are recorded as `SKIPPED` rather than re-copied, dramatically reducing cost and time
- Each backup site has its own **Manifest History** view with Restore, Delete, and per-row status indicators
- The **Delete** button for a historical manifest is automatically disabled with an explanatory tooltip when deleting it would orphan `SKIPPED` references in any later manifest
- Legacy `/api/backup` route renamed to `/api/mongo-backup` and new `/api/s3-backup` introduced, so the two services are peer top-level endpoints rather than nested; frontend service uses matching `MONGO_BASE_URL` and `S3_BASE_URL` constants

### Technical — new service and data model

- Add `s3-backup-service` with:
- **Incremental backup** (`ETag` comparison against the previous manifest)
- **Point-in-time restore** that walks back through prior manifests via `$elemMatch` on `key` + `eTag` + `action` to find the physical source of `SKIPPED` entries, bounded by `timestamp <= snapshot`
- **Cross-site restore** via a separate target `SiteConfig` that supplies destination credentials and bucket
- Add `s3-backup-controller`, `s3-backup-routes`, and the `s3BackupManifest` Mongoose model with a `(site, timestamp)` compound index
- Add `startS3Backup` / `startS3Restore` with per-site `SiteConfig` resolution that supports **global-credentials inheritance** (target env inherits global credentials when per-env credentials absent) and falls back to `<bucket>-backups` when no global backup bucket

### Technical — consolidated flow

- Extend `BackupAndRestoreService.executeBackup` / `executeRestore` to trigger the S3 phase after Mongo succeeds (honouring `options.includeS3`, defaulting `true`) and store the summary on `BackupSession` as `s3Backups` / `s3Restores`
- Derive source environment from Mongo backup filenames via `extractSourceEnvironmentFromBackupName(name, dbName)` which uses the database name as a hint to disambiguate multi-segment environment names and multi-segment databases (e.g. `ngx-ramblers`)
- `BackupSession` model and Mongoose schema gain `s3Backups?`, `s3Restores?` (`S3BackupSummary[]`) and `options.includeS3?`
- CLI `backup` / `restore` commands inherit consolidation automatically because they share `BackupAndRestoreService`; CLI still exposes a dedicated `s3-backup` sub-command for S3-only operations

### Technical — deletability guard

- `collectCopiedKeys` + `countBlockingLaterManifests` compute whether deleting a candidate manifest would orphan `SKIPPED` references in any later manifest on the same site
- `manifests()` and `manifest()` enrich returned docs with `deletable` and `blockReason`
- `deleteManifests` returns `{deleted, blocked}` and refuses blocked ones
- `S3BackupManifest` model gains runtime-only `deletable?` and `blockReason?` fields populated by the enrichment helpers

### Technical — notifications

- `BackupNotificationService` renders a structured S3 summary HTML table inside the completion emails for both backup and restore, driven by `session.s3Backups` / `session.s3Restores`
- New `buildS3SummarySection` helper keeps the formatting consistent across backup and restore emails

### Technical — route and script cleanup

- Rename `backup-routes.ts` to `mongo-backup-routes.ts` and export `mongoBackupRoutes` to match the `/api/mongo-backup` mount; mount new `s3BackupRoutes` at `/api/s3-backup`
- Delete standalone `aws-backup/backup-s3-images.ts` script plus its npm script since the new service fully supersedes it

### Technical — UI

- Admin UI pulls in ngx-bootstrap `TooltipDirective` and switches the per-manifest Delete button to `[disabled]` + `[tooltip]` when blocked, with inline button-swap confirmation (no native `confirm()` dialogs)

### Technical — type safety

- Replace all stringly-typed `BackupSessionStatus` and `BackupSessionType` comparisons with enum references (`BackupSessionStatus.COMPLETED` / `.FAILED` / `.IN_PROGRESS` / `.PENDING` and `BackupSessionType.BACKUP` / `.RESTORE`) across the service, ws handler, notification builder, and admin component
- The admin component exposes both enums as `protected readonly` members so the template can reference them directly
- Remove dry-run persistence bug where manifests were saved to Mongo even when `dryRun` was `true`; dry runs now return an in-memory manifest

### Technical — tests

- 8 new unit tests (`backup-paths`: 6 `extractSourceEnvironmentFromBackupName` cases including path prefix stripping, multi-segment db, and missing timestamp; `s3-backup-service`: 2 `collectCopiedKeys` cases)
- Full server suite now **575 passing**