{"id":"69da58067db299162ebc0ea9","title":"2026 04 11 Issue 187","path":"how-to/committee/release-notes/2026-04-11-issue-187","contentMarkdown":"# 11-Apr-2026 — consolidated MongoDB + S3 incremental backup with cross-environment restore [#187](https://github.com/nbarrett/ngx-ramblers/issues/187)\n\n## [build 585](https://github.com/nbarrett/ngx-ramblers/actions/runs/24284115184) — [commit ae900be](https://github.com/nbarrett/ngx-ramblers/commit/ae900be37df0ffc927918a3a0c219fe9edf14ca5)\n\n_____\n\n### What changed for users\n\n- 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\n- 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\n- Backup/Restore History gains an **S3 Objects** column showing per-session copied/skipped counts, bytes transferred, and status\n- 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\n- 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\n- Each backup site has its own **Manifest History** view with Restore, Delete, and per-row status indicators\n- 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\n- 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\n\n### Technical — new service and data model\n\n- Add `s3-backup-service` with:\n- **Incremental backup** (`ETag` comparison against the previous manifest)\n- **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`\n- **Cross-site restore** via a separate target `SiteConfig` that supplies destination credentials and bucket\n- Add `s3-backup-controller`, `s3-backup-routes`, and the `s3BackupManifest` Mongoose model with a `(site, timestamp)` compound index\n- 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\n\n### Technical — consolidated flow\n\n- 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`\n- 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`)\n- `BackupSession` model and Mongoose schema gain `s3Backups?`, `s3Restores?` (`S3BackupSummary[]`) and `options.includeS3?`\n- CLI `backup` / `restore` commands inherit consolidation automatically because they share `BackupAndRestoreService`; CLI still exposes a dedicated `s3-backup` sub-command for S3-only operations\n\n### Technical — deletability guard\n\n- `collectCopiedKeys` + `countBlockingLaterManifests` compute whether deleting a candidate manifest would orphan `SKIPPED` references in any later manifest on the same site\n- `manifests()` and `manifest()` enrich returned docs with `deletable` and `blockReason`\n- `deleteManifests` returns `{deleted, blocked}` and refuses blocked ones\n- `S3BackupManifest` model gains runtime-only `deletable?` and `blockReason?` fields populated by the enrichment helpers\n\n### Technical — notifications\n\n- `BackupNotificationService` renders a structured S3 summary HTML table inside the completion emails for both backup and restore, driven by `session.s3Backups` / `session.s3Restores`\n- New `buildS3SummarySection` helper keeps the formatting consistent across backup and restore emails\n\n### Technical — route and script cleanup\n\n- 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`\n- Delete standalone `aws-backup/backup-s3-images.ts` script plus its npm script since the new service fully supersedes it\n\n### Technical — UI\n\n- 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)\n\n### Technical — type safety\n\n- 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\n- The admin component exposes both enums as `protected readonly` members so the template can reference them directly\n- Remove dry-run persistence bug where manifests were saved to Mongo even when `dryRun` was `true`; dry runs now return an in-memory manifest\n\n### Technical — tests\n\n- 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)\n- Full server suite now **575 passing**","contentHtml":"<h1>11-Apr-2026 — consolidated MongoDB + S3 incremental backup with cross-environment restore <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/187\">#187</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/24284115184\">build 585</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/ae900be37df0ffc927918a3a0c219fe9edf14ca5\">commit ae900be</a></h2>\n<hr>\n<h3>What changed for users</h3>\n<ul>\n<li>A single <strong>Start Backup</strong> action now captures both the MongoDB database AND the site&#39;s S3 objects in one timestamp-aligned operation, surfaced through the existing Backup/Restore admin tab</li>\n<li>A single <strong>Start Restore</strong> 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</li>\n<li>Backup/Restore History gains an <strong>S3 Objects</strong> column showing per-session copied/skipped counts, bytes transferred, and status</li>\n<li>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</li>\n<li>S3 backups are <strong>incremental</strong> after the first run: unchanged objects (matched by <code>ETag</code> against the previous manifest) are recorded as <code>SKIPPED</code> rather than re-copied, dramatically reducing cost and time</li>\n<li>Each backup site has its own <strong>Manifest History</strong> view with Restore, Delete, and per-row status indicators</li>\n<li>The <strong>Delete</strong> button for a historical manifest is automatically disabled with an explanatory tooltip when deleting it would orphan <code>SKIPPED</code> references in any later manifest</li>\n<li>Legacy <code>/api/backup</code> route renamed to <code>/api/mongo-backup</code> and new <code>/api/s3-backup</code> introduced, so the two services are peer top-level endpoints rather than nested; frontend service uses matching <code>MONGO_BASE_URL</code> and <code>S3_BASE_URL</code> constants</li>\n</ul>\n<h3>Technical — new service and data model</h3>\n<ul>\n<li>Add <code>s3-backup-service</code> with:</li>\n<li><strong>Incremental backup</strong> (<code>ETag</code> comparison against the previous manifest)</li>\n<li><strong>Point-in-time restore</strong> that walks back through prior manifests via <code>$elemMatch</code> on <code>key</code> + <code>eTag</code> + <code>action</code> to find the physical source of <code>SKIPPED</code> entries, bounded by <code>timestamp &lt;= snapshot</code></li>\n<li><strong>Cross-site restore</strong> via a separate target <code>SiteConfig</code> that supplies destination credentials and bucket</li>\n<li>Add <code>s3-backup-controller</code>, <code>s3-backup-routes</code>, and the <code>s3BackupManifest</code> Mongoose model with a <code>(site, timestamp)</code> compound index</li>\n<li>Add <code>startS3Backup</code> / <code>startS3Restore</code> with per-site <code>SiteConfig</code> resolution that supports <strong>global-credentials inheritance</strong> (target env inherits global credentials when per-env credentials absent) and falls back to <code>&lt;bucket&gt;-backups</code> when no global backup bucket</li>\n</ul>\n<h3>Technical — consolidated flow</h3>\n<ul>\n<li>Extend <code>BackupAndRestoreService.executeBackup</code> / <code>executeRestore</code> to trigger the S3 phase after Mongo succeeds (honouring <code>options.includeS3</code>, defaulting <code>true</code>) and store the summary on <code>BackupSession</code> as <code>s3Backups</code> / <code>s3Restores</code></li>\n<li>Derive source environment from Mongo backup filenames via <code>extractSourceEnvironmentFromBackupName(name, dbName)</code> which uses the database name as a hint to disambiguate multi-segment environment names and multi-segment databases (e.g. <code>ngx-ramblers</code>)</li>\n<li><code>BackupSession</code> model and Mongoose schema gain <code>s3Backups?</code>, <code>s3Restores?</code> (<code>S3BackupSummary[]</code>) and <code>options.includeS3?</code></li>\n<li>CLI <code>backup</code> / <code>restore</code> commands inherit consolidation automatically because they share <code>BackupAndRestoreService</code>; CLI still exposes a dedicated <code>s3-backup</code> sub-command for S3-only operations</li>\n</ul>\n<h3>Technical — deletability guard</h3>\n<ul>\n<li><code>collectCopiedKeys</code> + <code>countBlockingLaterManifests</code> compute whether deleting a candidate manifest would orphan <code>SKIPPED</code> references in any later manifest on the same site</li>\n<li><code>manifests()</code> and <code>manifest()</code> enrich returned docs with <code>deletable</code> and <code>blockReason</code></li>\n<li><code>deleteManifests</code> returns <code>{deleted, blocked}</code> and refuses blocked ones</li>\n<li><code>S3BackupManifest</code> model gains runtime-only <code>deletable?</code> and <code>blockReason?</code> fields populated by the enrichment helpers</li>\n</ul>\n<h3>Technical — notifications</h3>\n<ul>\n<li><code>BackupNotificationService</code> renders a structured S3 summary HTML table inside the completion emails for both backup and restore, driven by <code>session.s3Backups</code> / <code>session.s3Restores</code></li>\n<li>New <code>buildS3SummarySection</code> helper keeps the formatting consistent across backup and restore emails</li>\n</ul>\n<h3>Technical — route and script cleanup</h3>\n<ul>\n<li>Rename <code>backup-routes.ts</code> to <code>mongo-backup-routes.ts</code> and export <code>mongoBackupRoutes</code> to match the <code>/api/mongo-backup</code> mount; mount new <code>s3BackupRoutes</code> at <code>/api/s3-backup</code></li>\n<li>Delete standalone <code>aws-backup/backup-s3-images.ts</code> script plus its npm script since the new service fully supersedes it</li>\n</ul>\n<h3>Technical — UI</h3>\n<ul>\n<li>Admin UI pulls in ngx-bootstrap <code>TooltipDirective</code> and switches the per-manifest Delete button to <code>[disabled]</code> + <code>[tooltip]</code> when blocked, with inline button-swap confirmation (no native <code>confirm()</code> dialogs)</li>\n</ul>\n<h3>Technical — type safety</h3>\n<ul>\n<li>Replace all stringly-typed <code>BackupSessionStatus</code> and <code>BackupSessionType</code> comparisons with enum references (<code>BackupSessionStatus.COMPLETED</code> / <code>.FAILED</code> / <code>.IN_PROGRESS</code> / <code>.PENDING</code> and <code>BackupSessionType.BACKUP</code> / <code>.RESTORE</code>) across the service, ws handler, notification builder, and admin component</li>\n<li>The admin component exposes both enums as <code>protected readonly</code> members so the template can reference them directly</li>\n<li>Remove dry-run persistence bug where manifests were saved to Mongo even when <code>dryRun</code> was <code>true</code>; dry runs now return an in-memory manifest</li>\n</ul>\n<h3>Technical — tests</h3>\n<ul>\n<li>8 new unit tests (<code>backup-paths</code>: 6 <code>extractSourceEnvironmentFromBackupName</code> cases including path prefix stripping, multi-segment db, and missing timestamp; <code>s3-backup-service</code>: 2 <code>collectCopiedKeys</code> cases)</li>\n<li>Full server suite now <strong>575 passing</strong></li>\n</ul>\n"}