29-Apr-2026 — rebuild draft leader on copy and backfill stale walk_leader closes #251
build 629 — commit 59d78a4
Two related fixes for one root cause: the legacy walks schema's flat contactId field was fanned out at migration time into four new-schema fields (groupEvent.walk_leader.id/name, fields.contactDetails.contactId, fields.publishing.ramblers.contactName), and the current edit UI only keeps three of those four in sync when a leader is selected. Any stale override on the legacy contactId therefore stayed embedded in groupEvent.walk_leader indefinitely, and the copy-from-previous-walk flow then propagated it onto every walk copied from such a record.
Reported by Kerry O'Grady after Jenny Brown's Bearsted Circular walk copied on 2026-04-19 came through with Nick Barrett as the Ramblers contact. Tracing the affected 2026 walk and its 2024 source confirmed the chain: a 2024 admin override of contactId, faithfully migrated, then faithfully spread-merged forward in 2026.
Code fix - projects/ngx-ramblers/src/app/pages/walks/walk-edit/walk-edit-copy-from.ts
copyDetailsFromPreviousWalk() previously did { ...targetFields, ...sourceFields } { ...targetGroupEvent, ...sourceGroupEvent } which let the source walk's leader-shaped fields win the merge.
Now, after the merge, rebuildLeaderIdentity re-derives every leader field from the draft's own pre-merge contactDetails.memberId via eventDefaultsService.memberToContact:
- fields.contactDetails (memberId, contactId, displayName, email, phone)
- fields.publishing.ramblers.contactName (member.contactId)
- fields.publishing.meetup.contactName (null; meetup has its own flow)
- groupEvent.walk_leader (Contact built from the current Member)
The publish booleans on both channels are preserved so the copier's on/off choices survive. groupEvent.status is also deleted on the clone so the source walk's "confirmed" status does not silently promote the new draft past its approval gate.
The previous deletes of fields.contactDetails, fields.publishing, and groupEvent.walk_leader on the clone are no longer needed - the rebuild is now authoritative for all four leader fields, anchored on the draft's pre-merge memberId.
Data backfill - server/lib/mongo/migrations/database/20260424000000-backfill-walk-leader-from-member.ts
The code fix prevents new leakage but does not heal walks already in the database with stale groupEvent.walk_leader from the migration-time fan-out. Idempotent backfill:
- Scope: fields.inputSource = "manually-created" with a non-null fields.contactDetails.memberId. Ramblers-cached and file-import walks are deliberately out of scope - their walk_leader.id is a Ramblers Walks Manager contactId, not a Member _id, and rewriting it would break the Ramblers submission path.
- For each walk, look up the Member by memberId and compute the expected walk_leader using the same shape memberToContact produces on new walks (is_overridden, id=member._id.toString(), name, telephone, has_email).
- Compare current walk_leader to expected field-by-field. Equal: skip. Missing or different: replace.
- Walks whose memberId no longer resolves to a Member are logged and left inert rather than silently cleared.
Re-runs find every doc already matching and perform no writes, so the migration is safe to replay during rollout or after partial runs. fields.contactDetails.contactId and fields.publishing.ramblers.contactName are intentionally not touched: the leader-toggle UI in walk-edit-leader.ts lets users diverge them from the member record on purpose, and the backfill should not undo that.
Runs automatically on deploy alongside the rest of the schema migrations
- not registered under manualMigrations in migrations-config.ts. The backfill is idempotent and the scope filter (manually-created walks with a memberId) keeps it cheap on environments that have no affected documents, so there is no benefit to gating it behind an explicit admin trigger.