emailBlock summary populated by the sync, so other parts of the app can answer "is this person actually emailable?" with one read.(memberId, listId, blockedAt) — visible on the existing Brevo tab inside the member admin modal as part of that member's history.mail.subscriptions are also flipped to unsubscribed for the lists Brevo holds them on, so the local subscription view matches reality.emailBlock, and writes an admin-override audit row. A two-step confirm prevents accidental clicks. Subscription state is not reset automatically — re-subscribe deliberately on the member's Brevo tab if appropriate.emailBlock — even if HQ's Email Marketing Consent column says yes. The local block is the source of truth for outbound deliverability.processCreateContactRequests payload now carries listIds directly, so a fresh bulk import creates the Brevo contact and assigns it to the configured lists in one round-trip. Previously contacts came in unassigned and only landed in their lists on a follow-up "Update Brevo Mailing Lists" pass — visible on a fresh import as two consecutive audit rows ("Contact created in Brevo" → "Contact Updated in Brevo: … List Ids → 0: N"). This was largely masked in production because most syncs are updates rather than creates; surfaced via the salesforce-mock synthetic-data path.Contact created in Brevo: Attributes -> ..., List Ids -> 0: N) instead of the previous bare Contact created in Brevo. Sets first(listIds) on the audit so the per-list filter on the Member Admin → Brevo tab works for create rows too./unsubscribe page instead of Brevo's hosted landing — recipients land on a page that wears the group's own branding (header, navbar, footer) rather than sendibt3.com.List-Unsubscribe: <https://…>, <mailto:…> and List-Unsubscribe-Post: List-Unsubscribe=One-Click), so Gmail and Apple Mail's native unsubscribe buttons hit the same API endpoint and complete in one click without rendering the page.updateContact(emailBlacklisted=true) and applies the local emailBlock + mailListAudit row directly (mirroring the events webhook handler), with source: "branded-unsubscribe". Optional reason submissions write a separate branded-unsubscribe-feedback audit row keyed off the same member.source value, so the existing sync's self-heal logic (which only undoes brevo-unsubscribes-sync rows) doesn't accidentally clear them.removeContactFromList) and flips only the matching local subscription. Account-level blocking is reserved for hard signals — Brevo's blocked / hard_bounce / spam / complaint events still propagate to the contact's emailBlock as before.mailListAudit rows now record MailListAuditListType (list-scoped vs account-wide) and MailListAuditSource, so the Member Admin → Brevo tab history shows which list each unsubscribe was for and where it came from.GET /unsubscribes/activity) and history view (GET /unsubscribes/history) on the server, surfaced through MailService.queryUnsubscribeActivity / queryUnsubscribeHistory for paginated, sortable display in the Mail Settings UI.DELETE /unsubscribes) for resetting the entire local blocklist in one step — for staging / test environments and recovery from sync drift. Two-step confirm at the UI.listId server-side when the caller omits it: if the recipient is subscribed to exactly one list, that list ID flows into the HMAC token. Welcome / expiry-warning emails (which don't pass emailRequest.listId) now go through the list-scoped path rather than the legacy "all local subscriptions disabled" fallback.APP_URL is now derived from urlService.baseUrl() rather than a hardcoded group.href, so unsubscribe links built while the app is running on localhost:4200 point at localhost rather than the production domain. Same for PW_RESET_LINK.?sender= params, or comma-separated). Caught exceptions on the unsubscribes / activity routes now console.error the underlying status code, response body, and stack unconditionally — no more silent debugLog when Brevo 5xx's.?sub-tab=blocks / ?sub-tab=unsubscribes are now honoured on initial load, alongside new start-date, end-date, and sender query params, so the entire filter state of the view is shareable as a URL.UnsubscribeSortField gained NAME and LIST_NAME values, with a client-side sortedActivity() getter applying the sort.timestamp $gte/$lte filter on the audit collection); previously the slider only affected the Blocks query.col + col-auto instead of col-sm-9 / col-sm-3) so the Clear-filters / Options buttons stop overlapping the "To Other — "Cara says nooooo!") instead of a badge plus a separate caption underneath.listNameById map when the server activity row doesn't carry an inline name.unsubscribed, blocked, hard_bounce, spam and complaint events in real time, with unsubscribe_revoked / unblocked automatically clearing the local block. The webhook URL and secret can be retrieved from GET /api/mail/webhooks/brevo-events/config and pasted into Brevo's dashboard.emailBlock self-healed on the next full sync, so the local state can never drift past Brevo's.pending / not-applicable / future synced / failed) and the matching membership number is captured at audit-write time, so the future Ramblers HQ Salesforce consent integration can iterate the audit collection directly when the consent endpoint becomes available.input-group with the label as input-group-text, the row uses flex-nowrap, and Member Action right-aligns via ms-auto instead of CSS float-end.After a deploy, tabs that had a stale index.html (referencing previous content-hashed bundles like main-GIDJEFWQ.js) would silently fail to boot. The SPA catch-all was happily serving index.html for the missing .js request, which tripped strict MIME checking ("Expected JavaScript, got text/html"), leaving a blank screen until the user thought to hard-refresh.
Hashed bundle/css/font/image assets remain safely long-cacheable via their content hashes - only the HTML entry point changes caching.
A new admin page at Mail Settings → Unsubscribes that gives committee members complete visibility of Brevo's suppression state — every contact who unsubscribed via an email link, the Brevo unsubscribe page, the Brevo API, plus contacts blocked by an admin, hard-bounced or flagged as spam.
The toggle at the top-right switches between:

Unsubscribes tab — member-level unsubscribe events from Brevo, with reason badges and the originating list

Blocks tab — Brevo's account-level blocklist (hard bounces, spam flags, admin blocks)
Each row that matches an NGX member writes a mailListAudit row keyed by (memberId, listId, blockedAt) so the block becomes part of that member's permanent audit trail (visible on the Brevo tab of the member admin modal). Each matched member's emailBlock summary is updated to reflect the latest Brevo state, and the contact's NGX mail.subscriptions are flipped to unsubscribed for the lists Brevo holds them on.
Remove from blocklist (trash icon) calls Brevo's DELETE /smtp/blockedContacts/{email}, then clears the matched member's local emailBlock and writes an audit row recording the manual override. Subscription state on the member is not changed automatically — re-subscribe explicitly via the member's Brevo tab if appropriate.
The Options dropdown also exposes a Clear all blocks action that removes every contact from Brevo's blocklist and clears every local emailBlock record. NGX member records are not deleted.
POST /api/mail/webhooks/brevo-events?token=...) writes the same audit and emailBlock updates in real time when Brevo emits unsubscribed, blocked, hard_bounce, spam, or complaint events. The webhook URL is configured inside Brevo's account settings.When a Ramblers HQ CSV is re-imported, members already flagged with emailBlock are still updated, but they will not be auto-subscribed to mail lists regardless of what HQ's Email Marketing Consent column says. The local block is the source of truth for outbound deliverability.
Replaces the dark-panel, chunky-button look across admin pages with the lighter, slim-row, sunrise-underline pattern from the ramblers-salesforce-mock admin UI, and adds a collapse-to-cards layout below 576px so admin tables work on phones.
Introduces a new partial at assets/styles/rsm-admin.scss, imported from styles.sass after the legacy frames and tables. The partial provides opt-in utility classes (.rsm-admin-page, .rsm-toolbar, .rsm-admin-table, .rsm-table-responsive, .rsm-tabs/.rsm-tab, .rsm-btn-cta/.rsm-btn-ghost/.rsm-btn-danger/.rsm-btn-small, .rsm-form/.rsm-form-inline/.rsm-form-actions, .rsm-card-grid/.rsm-card, .rsm-chip/.rsm-muted/.rsm-error) that honour existing tokens (--btn-min-height, --btn-pad-y/x, --radius-1) and reuse the existing $ramblers-colour-* variables for palette consistency.
The same partial scopes overrides under app-page so the existing admin wrappers — .thumbnail-admin-edit (used by 41 admin templates), .admin-frame, table.tbl-green-g, and tabset .nav-tabs — automatically inherit the rsm look without per-page edits. The dark granite background on .admin-frame becomes a soft white panel; tbl-green-g headers drop the granite/cloudy reverse-video for muted uppercase labels with a sunrise hover; admin tab strips gain the rsm sunrise underline and wrap on narrow widths.
Member Admin (the page singled out as looking dated) is restyled directly: the admin-frame/admin-header-background wrapper is replaced with .rsm-admin-page + .rsm-toolbar, the chunky btn-primary buttons become .rsm-btn-cta for the primary action with .rsm-btn-ghost for secondaries and .rsm-btn-danger for delete actions, the per-row Edit input becomes .rsm-btn-ghost.rsm-btn-small, and every