{"id":"69da90a1b519c70a7a3ad3e8","title":"2026 04 11 Build 586","path":"how-to/committee/release-notes/2026-04-11-build-586","contentMarkdown":"# 11-Apr-2026 — Incremental S3 backup aligned with MongoDB backup timestamps [#187](https://github.com/nbarrett/ngx-ramblers/issues/187)\n\n## [build 586](https://github.com/nbarrett/ngx-ramblers/actions/runs/24288444538) — [commit 37ea501](https://github.com/nbarrett/ngx-ramblers/commit/37ea501064ae59aba57af2a779a3667283b3b803)\n\n_____\n\n## Consolidate admin UI, per-object diagnostics, shared-bucket dedupe and manifest analysis\n\n### What changed for users\n\n- Backup admin tabs reduced from six to four. The standalone **S3 Backup** and **Backups** tabs are gone; their capabilities now live where they logically belong: the consolidated Backup/Restore flow covers S3 object sync automatically, bulk deletion of stored Mongo backup files is a collapsible **Manage stored backups** panel on the Restore tab, and the S3 snapshot analysis and deletion actions are merged directly onto the matching rows in Backup/Restore History\n- **One row per backup operation** in History. Each session row now shows the matching S3 snapshot's actions inline — View Logs, Analyse S3 snapshot, Delete S3 snapshot — so a Mongo backup and its aligned S3 manifest no longer duplicate each other. Sessions without a matching manifest (e.g. restores, failed S3 phases, Mongo-only backups) still render with just the View Logs button\n- **Analyse S3 snapshot** opens an inline panel with three tables: breakdown by top-level prefix, breakdown by file extension, and the twenty largest individual objects. Surfaces what's actually driving a bucket's size so a ballooning backup is easy to diagnose\n- **Environment and Collections selectors now carry their own label** that reads e.g. \"Environments (3 of 14 selected)\" and updates live as the selection changes, replacing the previous static \"Environment(s)\" heading and the redundant \"n environments selected\" helper text\n- **History tab gains a single environment filter** that applies to both Mongo sessions and (when shown) S3 manifest data. The filter uses the same select-all-capable multi-select component as Backup, and an empty filter shows everything with an explicit \"No filter applied — showing sessions for all environments\" helper line\n- **Backup and restore operations stream periodic S3 progress** into the session log (every few seconds and at completion), so the admin UI History row reveals how many of how many objects have been processed rather than a silent gap until the phase finishes\n- **Shared S3 buckets** across multiple environments no longer produce duplicate manifest chains or risk destructive in-place restores. When two environments point at the same source bucket, the backup runs once under the alphabetically-first (\"primary\") site with a note in the session log, and the restore applies the snapshot once to the shared physical bucket\n- **\"In progress\" session status pill** no longer wraps onto two lines when the Actions column is narrow; the spinner and text stay on one line\n- **Expanded logs on an in-progress session** now show a \"Waiting for log output...\" placeholder until the first log line arrives, instead of a visually empty panel\n- **Auto-refresh** on the History tab now reloads S3 manifests alongside sessions, so a newly completed backup's Analyse and Delete actions appear on its row without the user having to leave and re-enter the tab\n- **CLI `backup s3 analyse <site>`** command runs the same analysis (prefix, extension, top-N) from the terminal with optional `--timestamp`, `--depth` and `--top` flags, giving operators a quick way to inspect a snapshot without opening the admin UI\n\n### Technical — s3-backup-service\n\n- Add a `ProgressCallback` type and thread it through `performIncrementalBackup`, `performRestore`, `startS3Backup` and `startS3Restore` so callers can subscribe to aggregate progress messages without touching the stderr debug log\n- Enable the service's `debug()` logger and emit per-object lines: `COPY START`, `COPY DONE`, `COPY FAIL`, `COPY DRY-RUN`, `SKIPPED`, `ALREADY-OK`, `COPY FROM snapshot`, `COPY FROM earlier`, `MISS`, `DELETE-EXTRA` — each with the `[site] [N/total]` prefix, file size and eTag deltas where relevant\n- Throttle progress callbacks via `PROGRESS_EVERY_MS` (3 seconds) plus a guaranteed final message, so the session log gets useful but not spammy updates\n- Wrap every `CopyObjectCommand` in `copyObjectWithETagGuard`, a recursive async helper that passes `CopySourceIfMatch: expectedETag` and catches 412 Precondition Failed by re-heading the object, updating the eTag and retrying up to 3 times — prevents a silent integrity mismatch when an object is mutated (e.g. resized) between list and copy\n- Record the eTag that was physically copied rather than the one seen at listing time, so the manifest entry is always consistent with the stored bytes\n- Extract `rawSiteConfigs()` and `dedupeSiteConfigsByBucket()`, make `siteConfigs()` return one entry per unique source bucket with a new `sharedWith?: string[]` field populated with the aliased environment names, and teach `siteConfigFor()` to resolve either the primary or any alias to the primary's config\n- Teach `manifestByTimestamp()` to resolve aliases to the primary site before querying so cross-environment restore through a shared bucket finds the right manifest\n\n### Technical — consolidated backup/restore flow\n\n- `BackupAndRestoreService.executeBackup` and `executeRestore` now pass `(message) => this.addLog(sessionId, message)` as the `ProgressCallback` into `startS3Backup` / `startS3Restore`, so aggregate S3 progress flows into the same session log that the admin UI streams\n- Cross-environment restore now looks up the source environment from the backup file name (via the existing `extractSourceEnvironmentFromBackupName` helper) and passes it alongside the target, so a restore triggered through the main Restore tab automatically performs a cross-environment S3 restore when the selected backup came from a different source\n\n### Technical — manifest analysis\n\n- New pure module `projects/ngx-ramblers/src/app/functions/s3-manifest-analysis.ts` exports `groupEntriesByPrefix(entries, depth)`, `groupEntriesByExtension(entries)` and `topLargestEntries(entries, count)`. Shared verbatim by the CLI analyse command and by the admin UI expand panel, so the three analyses live in exactly one place\n- `S3ManifestBreakdown` interface moved from the functions module to `backup-session.model.ts` in line with the codebase rule that exported interfaces live in model files\n\n### Technical — CLI\n\n- New `backup s3 analyse <site>` command (alias `analyze`) with `--timestamp`, `--depth` and `--top` flags. Prints manifest summary, prefix and extension breakdowns, and the top N largest objects. Reuses the shared analysis helpers so behaviour matches the admin UI exactly\n- Retired the local `PrefixBucket` helpers in the CLI command in favour of the shared module\n\n### Technical — admin UI\n\n- `BackupRestoreTab` enum reduced to `BACKUP`, `RESTORE`, `HISTORY`, `SETTINGS`. The `S3_BACKUP` and `BACKUPS` entries are gone, as are their entire tab templates and all the state and methods that only served the standalone S3 backup form (`s3BackupAllSites`, `s3BackupDryRun`, `s3BackupInProgress`, `s3BackupResults`, `s3SelectedSite`, `s3RestoreResults`, `startS3Backup()`, `filteredS3Manifests()`)\n- Restore tab gains a **Manage stored backups** collapsible button that reveals the Mongo backup bulk selector and delete UI previously living in the Backups tab\n- History tab's session row template now includes `manifestForSession(session)` lookup and conditionally renders Analyse and Delete buttons plus the inline expand panel when a matching manifest exists. Matching is by timestamp parsed from `session.sessionId` (format `backup-{YYYY-MM-DD-HH-mm-ss}-{env}-{db}`) against `manifest.mongoTimestamp` and `manifest.site === session.environment`\n- `handleTabChange` now loads S3 manifests when entering the History tab (was previously the S3 Backup tab) and the auto-refresh subscription also reloads manifests on every tick so newly completed backups surface their actions without the user leaving and returning to the tab\n- `EnvironmentSelectComponent` gains a `label?: string` input and an internal `labelText()` helper that renders the label with a live `\"(X of Y selected)\"` suffix in multiple mode. The four existing call sites in the admin component drop their external `<label>` elements\n- `CollectionsMultiSelectComponent` gains the same `label?: string` + `labelText()` treatment, and its two call sites on the Backup and Restore tabs drop their external labels and redundant \"n collections selected\" helper text\n- History tab gains a `historyFilterEnvironments: EnvironmentInfo[]` + `filteredSessions()` strict-filter that falls back to all sessions when the filter is empty, with a live helper line explaining the state. The same filter is now reused for the S3 manifest match (the standalone `filteredS3Manifests()` method was removed when the separate S3 history table was folded into the session rows)\n- Session row action column switches to compact icon-only buttons (`faFileLines` for logs, `faChartBar` for analyse, `faTrash` for delete) wrapped in a `d-inline-flex gap-1 align-items-center` container with `text-nowrap` so nothing wraps. Each button carries an ngx-bootstrap tooltip describing the action\n- `.session-status` gets `white-space: nowrap` and `display: inline-block` so the \"In progress\" pill never line-wraps inside a narrow Actions column\n- Expanded session logs now render a `<fa-icon [spin]>` + \"Waiting for log output...\" placeholder when the expanded session is `IN_PROGRESS` with an empty `logs` array, instead of a visually empty panel\n- Analysis panel's heading block sits in a `.manifest-analysis-header` with `min-height: 5.5rem` so both side-by-side breakdown tables start at the same vertical position regardless of description wrapping\n- Descriptions under each breakdown heading shortened to one-line form (`Bytes per top-level folder.`, `Bytes per file type.`, `Largest individual files in this snapshot.`) to further reduce wrap risk\n- The Manage stored backups toggle uses `btn-sm btn-warning` rather than the default Bootstrap link style to stay consistent with the Ramblers palette on the Restore tab\n\n### Technical — tests\n\n- New unit tests in `s3-backup-service.spec.ts` cover `collectCopiedKeys` (copy-only filtering, empty-backup case) and extend the existing `siteConfigs` suite to exercise the new dedupe logic\n- Pre-existing `backup-paths.spec.ts` gains tests for `extractSourceEnvironmentFromBackupName` including multi-segment environment names, path prefix stripping, and the \"timestamp missing\" fallback\n- Full server suite remains green","contentHtml":"<h1>11-Apr-2026 — Incremental S3 backup aligned with MongoDB backup timestamps <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/24288444538\">build 586</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/37ea501064ae59aba57af2a779a3667283b3b803\">commit 37ea501</a></h2>\n<hr>\n<h2>Consolidate admin UI, per-object diagnostics, shared-bucket dedupe and manifest analysis</h2>\n<h3>What changed for users</h3>\n<ul>\n<li>Backup admin tabs reduced from six to four. The standalone <strong>S3 Backup</strong> and <strong>Backups</strong> tabs are gone; their capabilities now live where they logically belong: the consolidated Backup/Restore flow covers S3 object sync automatically, bulk deletion of stored Mongo backup files is a collapsible <strong>Manage stored backups</strong> panel on the Restore tab, and the S3 snapshot analysis and deletion actions are merged directly onto the matching rows in Backup/Restore History</li>\n<li><strong>One row per backup operation</strong> in History. Each session row now shows the matching S3 snapshot&#39;s actions inline — View Logs, Analyse S3 snapshot, Delete S3 snapshot — so a Mongo backup and its aligned S3 manifest no longer duplicate each other. Sessions without a matching manifest (e.g. restores, failed S3 phases, Mongo-only backups) still render with just the View Logs button</li>\n<li><strong>Analyse S3 snapshot</strong> opens an inline panel with three tables: breakdown by top-level prefix, breakdown by file extension, and the twenty largest individual objects. Surfaces what&#39;s actually driving a bucket&#39;s size so a ballooning backup is easy to diagnose</li>\n<li><strong>Environment and Collections selectors now carry their own label</strong> that reads e.g. &quot;Environments (3 of 14 selected)&quot; and updates live as the selection changes, replacing the previous static &quot;Environment(s)&quot; heading and the redundant &quot;n environments selected&quot; helper text</li>\n<li><strong>History tab gains a single environment filter</strong> that applies to both Mongo sessions and (when shown) S3 manifest data. The filter uses the same select-all-capable multi-select component as Backup, and an empty filter shows everything with an explicit &quot;No filter applied — showing sessions for all environments&quot; helper line</li>\n<li><strong>Backup and restore operations stream periodic S3 progress</strong> into the session log (every few seconds and at completion), so the admin UI History row reveals how many of how many objects have been processed rather than a silent gap until the phase finishes</li>\n<li><strong>Shared S3 buckets</strong> across multiple environments no longer produce duplicate manifest chains or risk destructive in-place restores. When two environments point at the same source bucket, the backup runs once under the alphabetically-first (&quot;primary&quot;) site with a note in the session log, and the restore applies the snapshot once to the shared physical bucket</li>\n<li><strong>&quot;In progress&quot; session status pill</strong> no longer wraps onto two lines when the Actions column is narrow; the spinner and text stay on one line</li>\n<li><strong>Expanded logs on an in-progress session</strong> now show a &quot;Waiting for log output...&quot; placeholder until the first log line arrives, instead of a visually empty panel</li>\n<li><strong>Auto-refresh</strong> on the History tab now reloads S3 manifests alongside sessions, so a newly completed backup&#39;s Analyse and Delete actions appear on its row without the user having to leave and re-enter the tab</li>\n<li><strong>CLI <code>backup s3 analyse &lt;site&gt;</code></strong> command runs the same analysis (prefix, extension, top-N) from the terminal with optional <code>--timestamp</code>, <code>--depth</code> and <code>--top</code> flags, giving operators a quick way to inspect a snapshot without opening the admin UI</li>\n</ul>\n<h3>Technical — s3-backup-service</h3>\n<ul>\n<li>Add a <code>ProgressCallback</code> type and thread it through <code>performIncrementalBackup</code>, <code>performRestore</code>, <code>startS3Backup</code> and <code>startS3Restore</code> so callers can subscribe to aggregate progress messages without touching the stderr debug log</li>\n<li>Enable the service&#39;s <code>debug()</code> logger and emit per-object lines: <code>COPY START</code>, <code>COPY DONE</code>, <code>COPY FAIL</code>, <code>COPY DRY-RUN</code>, <code>SKIPPED</code>, <code>ALREADY-OK</code>, <code>COPY FROM snapshot</code>, <code>COPY FROM earlier</code>, <code>MISS</code>, <code>DELETE-EXTRA</code> — each with the <code>[site] [N/total]</code> prefix, file size and eTag deltas where relevant</li>\n<li>Throttle progress callbacks via <code>PROGRESS_EVERY_MS</code> (3 seconds) plus a guaranteed final message, so the session log gets useful but not spammy updates</li>\n<li>Wrap every <code>CopyObjectCommand</code> in <code>copyObjectWithETagGuard</code>, a recursive async helper that passes <code>CopySourceIfMatch: expectedETag</code> and catches 412 Precondition Failed by re-heading the object, updating the eTag and retrying up to 3 times — prevents a silent integrity mismatch when an object is mutated (e.g. resized) between list and copy</li>\n<li>Record the eTag that was physically copied rather than the one seen at listing time, so the manifest entry is always consistent with the stored bytes</li>\n<li>Extract <code>rawSiteConfigs()</code> and <code>dedupeSiteConfigsByBucket()</code>, make <code>siteConfigs()</code> return one entry per unique source bucket with a new <code>sharedWith?: string[]</code> field populated with the aliased environment names, and teach <code>siteConfigFor()</code> to resolve either the primary or any alias to the primary&#39;s config</li>\n<li>Teach <code>manifestByTimestamp()</code> to resolve aliases to the primary site before querying so cross-environment restore through a shared bucket finds the right manifest</li>\n</ul>\n<h3>Technical — consolidated backup/restore flow</h3>\n<ul>\n<li><code>BackupAndRestoreService.executeBackup</code> and <code>executeRestore</code> now pass <code>(message) =&gt; this.addLog(sessionId, message)</code> as the <code>ProgressCallback</code> into <code>startS3Backup</code> / <code>startS3Restore</code>, so aggregate S3 progress flows into the same session log that the admin UI streams</li>\n<li>Cross-environment restore now looks up the source environment from the backup file name (via the existing <code>extractSourceEnvironmentFromBackupName</code> helper) and passes it alongside the target, so a restore triggered through the main Restore tab automatically performs a cross-environment S3 restore when the selected backup came from a different source</li>\n</ul>\n<h3>Technical — manifest analysis</h3>\n<ul>\n<li>New pure module <code>projects/ngx-ramblers/src/app/functions/s3-manifest-analysis.ts</code> exports <code>groupEntriesByPrefix(entries, depth)</code>, <code>groupEntriesByExtension(entries)</code> and <code>topLargestEntries(entries, count)</code>. Shared verbatim by the CLI analyse command and by the admin UI expand panel, so the three analyses live in exactly one place</li>\n<li><code>S3ManifestBreakdown</code> interface moved from the functions module to <code>backup-session.model.ts</code> in line with the codebase rule that exported interfaces live in model files</li>\n</ul>\n<h3>Technical — CLI</h3>\n<ul>\n<li>New <code>backup s3 analyse &lt;site&gt;</code> command (alias <code>analyze</code>) with <code>--timestamp</code>, <code>--depth</code> and <code>--top</code> flags. Prints manifest summary, prefix and extension breakdowns, and the top N largest objects. Reuses the shared analysis helpers so behaviour matches the admin UI exactly</li>\n<li>Retired the local <code>PrefixBucket</code> helpers in the CLI command in favour of the shared module</li>\n</ul>\n<h3>Technical — admin UI</h3>\n<ul>\n<li><code>BackupRestoreTab</code> enum reduced to <code>BACKUP</code>, <code>RESTORE</code>, <code>HISTORY</code>, <code>SETTINGS</code>. The <code>S3_BACKUP</code> and <code>BACKUPS</code> entries are gone, as are their entire tab templates and all the state and methods that only served the standalone S3 backup form (<code>s3BackupAllSites</code>, <code>s3BackupDryRun</code>, <code>s3BackupInProgress</code>, <code>s3BackupResults</code>, <code>s3SelectedSite</code>, <code>s3RestoreResults</code>, <code>startS3Backup()</code>, <code>filteredS3Manifests()</code>)</li>\n<li>Restore tab gains a <strong>Manage stored backups</strong> collapsible button that reveals the Mongo backup bulk selector and delete UI previously living in the Backups tab</li>\n<li>History tab&#39;s session row template now includes <code>manifestForSession(session)</code> lookup and conditionally renders Analyse and Delete buttons plus the inline expand panel when a matching manifest exists. Matching is by timestamp parsed from <code>session.sessionId</code> (format <code>backup-{YYYY-MM-DD-HH-mm-ss}-{env}-{db}</code>) against <code>manifest.mongoTimestamp</code> and <code>manifest.site === session.environment</code></li>\n<li><code>handleTabChange</code> now loads S3 manifests when entering the History tab (was previously the S3 Backup tab) and the auto-refresh subscription also reloads manifests on every tick so newly completed backups surface their actions without the user leaving and returning to the tab</li>\n<li><code>EnvironmentSelectComponent</code> gains a <code>label?: string</code> input and an internal <code>labelText()</code> helper that renders the label with a live <code>&quot;(X of Y selected)&quot;</code> suffix in multiple mode. The four existing call sites in the admin component drop their external <code>&lt;label&gt;</code> elements</li>\n<li><code>CollectionsMultiSelectComponent</code> gains the same <code>label?: string</code> + <code>labelText()</code> treatment, and its two call sites on the Backup and Restore tabs drop their external labels and redundant &quot;n collections selected&quot; helper text</li>\n<li>History tab gains a <code>historyFilterEnvironments: EnvironmentInfo[]</code> + <code>filteredSessions()</code> strict-filter that falls back to all sessions when the filter is empty, with a live helper line explaining the state. The same filter is now reused for the S3 manifest match (the standalone <code>filteredS3Manifests()</code> method was removed when the separate S3 history table was folded into the session rows)</li>\n<li>Session row action column switches to compact icon-only buttons (<code>faFileLines</code> for logs, <code>faChartBar</code> for analyse, <code>faTrash</code> for delete) wrapped in a <code>d-inline-flex gap-1 align-items-center</code> container with <code>text-nowrap</code> so nothing wraps. Each button carries an ngx-bootstrap tooltip describing the action</li>\n<li><code>.session-status</code> gets <code>white-space: nowrap</code> and <code>display: inline-block</code> so the &quot;In progress&quot; pill never line-wraps inside a narrow Actions column</li>\n<li>Expanded session logs now render a <code>&lt;fa-icon [spin]&gt;</code> + &quot;Waiting for log output...&quot; placeholder when the expanded session is <code>IN_PROGRESS</code> with an empty <code>logs</code> array, instead of a visually empty panel</li>\n<li>Analysis panel&#39;s heading block sits in a <code>.manifest-analysis-header</code> with <code>min-height: 5.5rem</code> so both side-by-side breakdown tables start at the same vertical position regardless of description wrapping</li>\n<li>Descriptions under each breakdown heading shortened to one-line form (<code>Bytes per top-level folder.</code>, <code>Bytes per file type.</code>, <code>Largest individual files in this snapshot.</code>) to further reduce wrap risk</li>\n<li>The Manage stored backups toggle uses <code>btn-sm btn-warning</code> rather than the default Bootstrap link style to stay consistent with the Ramblers palette on the Restore tab</li>\n</ul>\n<h3>Technical — tests</h3>\n<ul>\n<li>New unit tests in <code>s3-backup-service.spec.ts</code> cover <code>collectCopiedKeys</code> (copy-only filtering, empty-backup case) and extend the existing <code>siteConfigs</code> suite to exercise the new dedupe logic</li>\n<li>Pre-existing <code>backup-paths.spec.ts</code> gains tests for <code>extractSourceEnvironmentFromBackupName</code> including multi-segment environment names, path prefix stripping, and the &quot;timestamp missing&quot; fallback</li>\n<li>Full server suite remains green</li>\n</ul>\n"}