A walk with no stored url (an awaiting-leader slot, or a walk whose title had been cleared) still produced a title-or-date based slug for its links. That fabricated slug matched nothing on lookup, so clicking a walk title went to "Page not found", and the Edit button silently rendered a blank default walk dated today - closing which dropped the real walk from the in-memory programme until a refresh.
The slug logic was duplicated across six link builders (walkSlug, groupEventLink, event-contact, email-composer, committee-query, the walk-notification email footer), each fabricating slightly differently, so any single fix left the rest broken. Separately, clearing a title blanked the walk's url, and the copy-walk action never regenerated one.
eventSlug(event) in functions/walks/event-slug.ts = stored url, else groupEvent.id, else id. It never fabricates a title slug: the client kebabCase and the server convertTitleToSlug differ (stop-words), so a client-fabricated slug cannot be relied on to match a walk's own stored url. All six link builders now call it.WalksAndEventsService.urlFor(walk) = titled walks resolve through the backend urlFromTitle service (stop-words stripped, uniqueness -N suffix); title-less slots get the dated slug. Copy-walk, walk title-change, group-event title-change, and a new save-time backfill all route through it, so a walk is never persisted without a resolvable url.url when the title is emptied.Existing walks with a missing url become reachable again by id and pick up a proper url the next time they are saved; no data migration was needed.
The Serenity walk-upload scenario hardcoded the actor to "Walks Admin", so the steps could not distinguish Nick from Kerry. The uploader's name already reached the metadata file but was dropped by the extractor and ignored by the scenario.
ramblersUser added to WalkRequestParameters and the metadata extractor.Notification image overrides are stored as relative paths (api/aws/s3/site-content/...jpeg). The batch send rewrote these to absolute URLs, but the generic transactional path - which sends the welcome email - passed the raw relative path to Brevo. In the in-app preview a relative src resolves against the browser page origin so it looked fine; a real email client has no base href, so the images failed (as seen in the new-joiner welcomes).
renderBrandedTemplate, which every branded send and the preview go through, using the APP_URL already in params.The cookie-banner dismissal ran on every navigation, cluttering the upload report even when there was nothing to clear. It now runs only when a banner is actually present (a Check performs the clearing in its branch), so its dismissal steps appear only when there is something to dismiss. Fully hiding the check's own line (the IsHidden/IsSilent reporting markers) lives in serenity-js itself, on the feat/hidden-and-silent-activities fork branch, rather than as a patch here.
Every Chromium launch (walks upload, site migration, integration worker) now goes through one resolveHeadless() / launchChromium() in server/lib/shared/playwright-browser.ts, with the default in a clearly-named DEFAULT_HEADLESS constant and overridable by PLAYWRIGHT_HEADLESS via the shared asBoolean() helper. The CLI --no-headless flag feeds the same one place.
Documentation updated to reflect the Angular 21 upgrade: AGENTS.md, README.md, codebase-evolution-stats.html and docs/contributing/agentic-development.md moved from Angular 20 to Angular 21.