Skip to main content

Give feedback

Back to all releases
Changelog

2.1.0

Released 2026-09-22

Security

  • Aligned event registration retention operations with their declared permissions.

Added

  • Added an authenticated, buyer- and tenant-scoped marketplace checkout outcome lookup so native recovery can find an existing order without replaying checkout or payment.

  • Added atomic course section and lesson reorder APIs with expected-order checks, owner authorization, replay-safe target ordering, and transactional position updates.

  • Added an authorized, read-only offline attendance batch lookup for recovering saved client requests after a lost response, including batches outside recent history.

  • Native knowledge articles now show structured content, related articles, video links, authenticated attachments, and saved helpfulness feedback with recovery for failed requests.

  • Shared native article rendering now supports HTML and Markdown structure, accessible headings and links, and literal plain text, with reproducible dependency patches that avoid unused imports.

  • Added a native organiser invitation history and revocation screen with permission-controlled recipient details, reason validation, separate confirmation, explicit saved-request recovery and seven-language labels.

  • Added saved invitation-revocation recovery with stable retry keys, duplicate-action guards and screen lifecycle checks; organiser UI integration is pending.

  • Added paginated organiser invitation history with permission-controlled recipient details and a validated native client contract.

  • Added validated native invitation-revocation requests with preserved retry keys and minimal completion receipts; organiser screen integration is pending.

  • Added a native organiser retention workspace with paginated history, UTC preview validation, saved-action recovery, and a separate review and confirmation before anonymisation.

  • Added owner- and event-scoped saved retention actions that preserve original retry keys through uncertain outcomes and store validated completion receipts. Lifecycle controls prevent automatic replay, duplicate submissions and stale screen callbacks.

  • Added a permission-gated, paginated event retention history API and native contracts for history, preview and apply receipts, with event binding and caller-owned retry keys. The native retention screen is not yet connected.

  • Connected native event invitation management to registration settings, with paginated campaign and preview review, event-time-zone scheduling and expiry, cancellation confirmation, and explicit durable request recovery.

  • Added the native invitation recipient editor component with member, email, group, CSV and full audience-filter inputs, seven-language labels, and recipient-selection validation. Campaign screen integration remains in progress.

  • Added lifecycle handling for native invitation campaign operations: account, event, permission and focus changes invalidate stale actions; recovery remains explicit and failures stay visible. Campaign screen integration remains pending.

  • Native invitation campaign operations now preserve encrypted pending requests for explicit recovery, block overlapping or replacement requests, and retain minimal confirmed receipts. Screen integration remains pending.

  • Added native organiser invitation campaign API contracts for paginated reads, preview, issue, scheduling and cancellation with caller-owned retry keys and scoped response validation. The native campaign screen and saved-request recovery are not yet implemented.

  • Added the native organiser guest roster and attendance screen with paginated records, permission-gated contact details, confirmed attendance actions, required undo reasons, and explicit saved-action recovery and stale-version review.

  • Added native registration submission review and CSV export with independent pagination, restricted identities, purged-answer labels, explicit audit reasons and separate export/sensitive-answer permissions. Export streams an authenticated POST to a private file, prevents duplicate requests, cancels preparation after departure or account changes, and opens the native save/share sheet without claiming cancellation as success. Shared source files receive a one-hour grace period before foreground cleanup; active transfers and unrelated cache files are preserved. Android/iOS acceptance remains tracked separately.

  • Added native organiser registration-form API contracts for reading draft/published definitions and creating, updating, forking and publishing forms with explicit revisions and caller-owned retry keys. An owner-scoped saved-action journal preserves form requests for explicit recovery, validates correlated receipts and blocks overlapping or replacement requests. Definitive revision conflicts preserve the original draft for explicit review against current forms and settings. Native question metadata controls and draft conversion preserve authored rules and validate required privacy and consent fields, answer limits, condition operators and earlier-question references before requests are saved. Answer choices retain embedded line breaks, related question types preserve authored validation rules, and reorder/removal helpers preserve conditional question dependencies. A native draft editor adds form details, guarded question ordering/removal, unsaved-change protection and retryable save feedback. The organiser route and lifecycle hook connect draft editing, explicit recovery, version creation and confirmed publication; translated text-length, answer-format, choice-count and conditional-display controls support multiple conditions, all/any matching and typed scalar/list values. Conflict review compares full current and proposed question definitions and can create a confirmed new version when another organiser has published the edited draft. The registration policy now links to form management; both screens use the app modal presentation, the forms list supports pull-to-refresh without replacing editor drafts, and acknowledged saves and publication show translated success feedback. Confirmed form versions remain current in the list across subsequent operations. Native form recovery now uses verified encrypted files for larger definitions, reads older saved requests until migration succeeds, and prevents cleared or corrupt files from reviving obsolete requests.

  • Added a native Registration policy screen for organiser approval rules, dates, member limits, guests and retention settings. It preserves uncertain requests for explicit recovery and restores rejected drafts against refreshed policy and schedule for review. Published changes require confirmation; unsaved edits are protected. The management entry opens this screen. Android read-only rendering is verified; native writes/restart recovery and the remaining organiser form, invitation and submission workspaces are not yet verified or complete.

  • Added an internal native agenda registration journal preserving owner-scoped pending request identities and minimal acknowledged receipts through required saved-draft storage. Panel integration and restart recovery are not yet enabled.

  • Added a dedicated native organiser People workspace for filtered roster reads, member-name invitations, per-person history and registration changes. Individual or eligible-page selection, required cancellation/rejection reasons, confirmation and explicit recovery use a durable journal preserving original request keys and partial receipts. It blocks overlapping submissions, unresolved work and rows older than confirmed changes, checks screen ownership before sending, ignores obsolete saved-state reads and announces recovery failures. Invitation searches can retry unchanged text after failure. History follows server permissions and clears when the selected person changes or access is refused. CSV export reviews included/excluded fields before an authenticated download of all filtered rows. The organiser People entry now opens this workspace; Check-in retains its attendance screen. Native-device verification remains unfinished.

  • Quiz submissions accept optional retry identities that replay the saved result without consuming another attempt, including concurrent retries at the limit. Changed answers under one identity are rejected. Learner reads expose the caller's latest grade summary and remaining attempts. Apply the quiz-attempt replay migration before enabling keyed clients; rolling it back removes saved retry identities.

  • Native Knowledge articles support helpfulness voting with saved selections, server-confirmed counts, duplicate-tap protection, retry feedback, and refusal recovery.

  • Native Knowledge articles expose their video links with a translated action and a clear notice that playback opens in the device's video app or browser.

  • Native Knowledge articles expose related-article navigation and authenticated attachment downloads, with duplicate-tap protection, retryable failure feedback, and unavailable-file handling.

  • The accessible frontend now publishes the account-deletion and child-safety-standards pages, so both footers offer the same pages. They were the last two links in the React footer with no accessible equivalent — both 404ed. Verified by comparing the two footers' destinations and requesting every one: nothing the React footer links is missing from the accessible site, and all 23 accessible footer links resolve. The accessible footer keeps four extras, each deliberate: Volunteering, Features, the cookie-settings link, and "report a problem" (WCAG 2.2 §3.2.6 Consistent Help, which React covers with its floating reporter instead).

    • 🔴 Both pages have ONE source, shared with the React pages, generated by web-uk/scripts/build-legal-text.js from react-frontend/public/locales/*/legal.json. This is compliance copy: it names the operating company, a charity number, a registered address, the Google Play package id, a designated child-safety contact and an effective date, and the child-safety page is what Google Play is pointed at. Two hand-maintained copies would eventually disagree, and the day they did, one frontend would be telling members something untrue about deleting their data or about how a child-safety report is handled. A test asserts those specific details are identical to React's.
    • They sit at /account-deletion and /child-safety — React's own paths, and not under /legal/, because account_deletion and child_safety are not in LegalController::VALID_TYPES: there is no per-community version to fetch, no version history, and nothing for the /legal/:type/versions routes to answer.
    • Built strictly to the Design System: numbered steps as ordered lists, the zero-tolerance statement in a warning-text component, the reporting and request routes as start buttons via govukButton, and what is deleted versus what may be retained as two separately headed lists rather than one mixed one.
  • The deploy's crawler check now also asks whether the page is the CURRENT one. It compares each probed page's data-build-commit against the commit being deployed and fails loudly when a snapshot's build is older than 24 hours (NEXUS_DELIVERY_MAX_SNAPSHOT_AGE_HOURS). It reported 16 of 16 pages OK on 2026-09-17 while app.project-nexus.ie had been serving a crawler snapshot frozen at one commit through three deploys: the page was a genuine 58 KB document with an h1 and a meta description, so every signal the check measured was satisfied. Real content is not the same as current content, and the difference was invisible. Deliberately age-based rather than an equality test: the prerender pipeline is incremental by design, so a page nothing has invalidated is supposed to keep its snapshot across a deploy, and failing on "not the newest commit" would fire on every deploy for every unchanged page. New BLOCKING gate Crawler delivery probe detects a frozen snapshot; scripts/deploy.sh passes the deployed sha, pinned by scenario 8 of test-deploy-probe-after-prerender.sh.

Fixed

  • Native event Safety reads and mutation responses now reject data for a different event instead of displaying a mismatched safety projection.

  • Native event Team expiry requests now use the API's whole-second precision before saving, preventing successful timed role assignments from being mistaken for mismatched recovery receipts.

  • Native event Team search and confirmations distinguish members with identical privacy-limited names using their public member number, without revealing hidden surnames or contact details.

  • Retrying an accepted event staff grant after its expiry now returns its original receipt without renewing access or creating another history entry. New grants with expired dates remain rejected.

  • Native event federation diagnostics now reflow after live text-size changes, format dates for the community, and explain permission failures without incorrectly claiming the organiser has no management tools.

  • Search results named an organisation by its contact person. scripts/sync_search_index.php built author_name (listings) and organizer_name (events) with a raw CONCAT(first_name, last_name), so an organisation account appeared under the person held in those fields rather than its organization_name. Both now use UserDisplayName::sql(). 🔴 The display-name gate scans only app/ and react-frontend/src/, so it could not see these: 31 further raw concatenations sit outside its scan paths (19 in web-uk/src, 6 in scripts/, 6 in mobile/), measured but not changed here.

  • An organisation endorser was listed under its contact person's name. EndorsementService::getGroupedByUser() built legacy_name with a raw CONCAT(first_name, last_name), which names the contact held on an organisation account rather than the organisation itself — the same query builds its other display name correctly with UserDisplayName::sql() a few methods below. endorsed_by_names now shows the organisation. Caught by the display-name concatenation gate, whose ceiling is 0.

  • Two live event endpoints were absent from openapi.json: GET /api/v2/events/{id}/registration-product/invitations and GET /api/v2/events/{id}/registration-product/retention. The coverage test asserts in a loop and so reported only the first, which is why fixing the reported one alone would have left the gate red; the full spec-versus-routes gap was computed in both directions instead (2 missing paths, 0 missing methods, 0 stale entries).

  • Two group-exchange API messages (group_exchange_terms_changed, group_exchange_not_ready_to_complete) were added to lang/en/api.php without their ten translated counterparts, so the key-set parity gate was failing on main. Hand-translated into all ten locales rather than copied from English, which also moved the untranslated-value ratchet 84 values below its baseline. A member outside an English locale would have seen an English string mid-flow when confirming a group exchange.

  • The "Accessible version" link in the navbar and mobile drawer could not be pointed anywhere but production. src/lib/accessible-frontend.ts reads VITE_ACCESSIBLE_FRONTEND_BASE_URL and falls back to the hardcoded https://accessible.project-nexus.ie, but neither frontend image declared that build argument — so a --build-arg was ignored and the fallback was the only reachable value in any containerised build. It stayed hidden because the resolver prefers a tenant's own accessible_domain and every existing tenant row has one; a tenant without one falls through to the hardcoded host, and creating a tenant is exactly what a super-admin does. On a staging or test deployment that link sent the user to the live production site. Both images now declare the argument, defaulting to the production host so production builds stay byte-identical, compose.bluegreen.yml now passes it from ACCESSIBLE_FRONTEND_BASE_URL so a deployment sets it in its environment file rather than by editing compose, and AccessibleFrontendUrlIsConfigurableTest pins that both images accept and forward it, that compose passes it from the environment, that the default is unchanged, and that the source still reads it — any one of those missing makes the destination hardcoded again. Note that react-frontend/.env.example had documented this variable all along, so the override was advertised while the build could not honour it.

  • Queued work no longer stops silently outside production and local. config/horizon.php defined worker supervisors for production and local only, so under any other APP_ENV Horizon started its master supervisor, logged "Horizon started successfully" and spawned zero workers — queued jobs, including password-reset and notification mail, never ran, and the container healthcheck failed with nothing in the logs to explain it. Added staging and testing supervisors, with HorizonEnvironmentCoverageTest pinning every deployed environment, asserting each supervisor can run at least one process, and requiring staging to mirror production's supervisor shape.

  • The browser no longer blocks the app from reaching its own API outside production. The canonical SPA Content-Security-Policy hardcoded https://api.project-nexus.ie in connect-src with no substitution, while ${NEXUS_API_UPSTREAM} a few lines below it was substituted — so any deployment on a different API hostname was refused permission by the browser. It failed silently in every place an operator would check: the page loaded, assets returned 200, DNS and TLS were valid, CORS preflight returned 204 with the correct Access-Control-Allow-Origin, curl to the API worked and navigating straight to the API returned JSON, while the app showed "Unable to connect" because CSP blocks the request before one is made. Added a ${NEXUS_CSP_EXTRA_ORIGINS} placeholder to img-src, connect-src and frame-src — every directive that lists the API as a source — empty by default so production behaviour is byte-identical, declared as an ENV in both frontend images (envsubst only replaces defined variables) and passed through by compose.bluegreen.yml. report-uri is deliberately excluded because it takes a single URI rather than a source list. Patching connect-src alone was an incomplete first fix: the app could then load its data, but img-src still refused images served from the API origin, so default avatars failed with nothing but a bare "Failed to fetch". SpaContentSecurityPolicyApiOriginTest pins the whole chain — the placeholder in all three source directives and absent from report-uri, both configs holding an identical policy, both installed as nginx templates, both images declaring the variable, and compose passing it — because the fix is inert if any link is missing.

  • Made the course-builder stale-confirmation regression explicitly await section deletion, retaining checks that pending content remains visible and a late lesson confirmation cannot delete it.

  • Native marketplace, course authoring, goal, poll, podcast, hiring and volunteering retry reservations now validate account and community identity for every caller sharing a pending operation.

  • Native message retry reservations now recheck account and community identity for every concurrent caller before sending, while preserving the original retry key.

  • Native listing creation now rechecks account and community identity for every caller sharing a pending retry reservation, preserving the original retry record on identity changes.

  • Android now refreshes native layout and JavaScript font-scale metrics after application configuration updates, remeasuring shared text without losing open drafts when text size changes.

  • Exchange API confirmation and creation timestamps now include UTC timezone information, matching history dates across Android, iOS and web clients.

  • Native exchange confirmation warnings use singular or plural hours for the confirmed amount.

  • Mobile confirmation dialogs dismiss the keyboard when opened so discard and confirmation actions remain reachable.

  • Native wallet recipient search, selection and confirmation now include public usernames when available so members sharing a first name can be distinguished without exposing private surnames.

  • Native offline event check-in now offers Settings after permanent camera refusal and refreshes permission on return, preserving manual entry and attendance controls.

  • Native marketplace QR scanners now offer Settings after permanent camera refusal, refresh permission on return, and handle permission/settings failures with translated recovery feedback.

  • Podcast browse and show episode counts now reflect the episodes visible to listeners instead of including hidden or unavailable episodes.

  • Native form field labels and action-footer guidance now remeasure after system font-size changes without resetting inputs or drafts.

  • Corrected the native group-exchange draft exit warning to use shared translated unsaved-change wording instead of calling the exchange a listing.

  • Native confirmation dialogs now dismiss when their screen loses focus, preventing stale actions from appearing over a newly opened deep link.

  • Native challenge and group-exchange creation now prevent duplicate submissions and ignore delayed mutation callbacks after leaving the form.

  • Native post creation now blocks rapid duplicate submissions and ignores success or failure callbacks after the composer closes.

  • Native marketplace checkout now retains purchase identity across reopening and offers explicit recovery with the original choices, including sold-out listings, without automatically restarting card payment.

  • Fixed the mobile bundle-budget export on Windows by reusing the bounded Node shutdown-crash retry wrapper. Ordinary export errors still fail; the size ceiling is unchanged.

  • Fixed the native image viewer share-failure handler to use the shared safe error-description helper, satisfying the app-wide failure-message consistency check.

  • Fixed native onboarding retaining the previous account after a delayed response: account/community changes now reset the screen, and stale media, profile-save and completion callbacks cannot publish state or navigation after departure.

  • Fixed native group-exchange actions after lost responses: confirmation, completion and cancellation now require fresh server details before another mutation, preserving read-only recovery through offline retries.

  • Fixed native image viewing with cross-platform gesture zoom, a canvas measured below the safe-area toolbar, and translated labels for untitled images.

  • Native image viewer now explains image-loading failures and offers retry instead of leaving a blank canvas.

  • Native image-viewer close and share controls now sit inside the safe-area layout instead of being clipped beneath the Android status bar.

  • Native image viewer now handles unavailable sharing with translated feedback while keeping the image open.

  • Native marketplace QR scanning now accepts one camera event per opening, preventing rapid duplicate pickup or coupon callbacks while allowing a fresh scan after reopening.

  • Native federation setup now blocks progress when saved preferences cannot be loaded and offers a retry, preserving existing choices after recovery.

  • Restrict group-exchange settlement to exchanges awaiting confirmation, preserving terminal-state errors.

  • Clear group-exchange confirmations when participant membership changes so revised credit shares require renewed consent; duplicate additions and repeated removals preserve fresh confirmations.

  • Require renewed group-exchange confirmations after hours or split rules change, serialize these edits with settlement, and reject stale edits after completion or cancellation.

  • Validate group-exchange settlement under the participant mutation lock so a concurrent removal cannot leave credits paid from an outdated participant split.

  • Accept group-exchange participant confirmation only while the exchange is awaiting confirmation, with the state check and write protected by a shared exchange lock.

  • Preserve participant records on completed or cancelled group exchanges, while retaining normal participant editing and repeat-safe removal on open exchanges.

  • Keep group-exchange starts from overwriting a concurrent cancellation or repeating participant notifications after another start has already succeeded.

  • Prevent repeated native group-exchange actions after acceptance while refreshed details are pending; failed refreshes retry the read without replaying the action.

  • Group-exchange cancellation can no longer overwrite a completed settlement; repeated cancellation remains safe and cancelled exchanges cannot complete.

  • Mobile listing-load recovery now consults the shared refusal-status policy; podcast retry regression coverage explicitly controls response completion.

  • Mobile group-exchange confirmations close when their displayed information changes or refreshes, cannot submit stale or refused reads, and reject malformed group-exchange links.

  • Mobile group exchanges prevent duplicate pending action submissions and ignore departed-screen responses or old confirmations after switching exchange, account or community.

  • Mobile listing links reject malformed or unsafe numeric IDs before loading, and temporary listing-load failures offer Retry without reopening the screen.

  • Mobile listing reports no longer show late failure alerts or vibration after leaving the screen or switching listing, account or community; current-screen failures still preserve the draft for retry.

  • Mobile course sections now show deletion progress and temporarily lock their editors and actions until the request finishes, restoring editing after a failed deletion.

  • Mobile course authoring now keeps sections intact while their section, lesson or quiz-question edits are unfinished or saving, including when an earlier delete confirmation is accepted.

  • Native course editor: Clarify that leaving discards unsaved changes rather than deleting the course, and explain that course details and individual lessons are saved separately.

  • Native course authoring: Preserve newer lesson text and quiz-question drafts when an earlier save finishes, keeping unsaved-work protection active for those newer edits.

  • Native course authoring: Include unsaved lesson edits, section renames and quiz-question drafts in the leave-screen warning, and keep protection active while their saves are pending.

  • Courses: Serialize lesson creation, updates and deletion with curriculum changes, preventing concurrent lesson writes from referring to a section that has been deleted.

  • Courses: Make section deletion and preservation of its lessons atomic, keeping the curriculum unchanged when deletion fails or is vetoed.

  • Native course builder: Preserve accepted lessons when their section is deleted during creation, prevent creation against removed sections, and ignore stale lesson-deletion confirmations after section removal.

  • Native course authoring: Keep preserved lessons visible after section deletion and let instructors assign them to another section, with confirmed saves, retry after failure and duplicate-tap protection.

  • Native courses: Keep lessons preserved after section deletion visible in the course syllabus and player, including existing lesson availability restrictions. Course detail responses now include unassigned lessons for both ID and slug lookups.

  • Native curriculum moves now use atomic ordering, wait for confirmed results, and read back uncertain saves before allowing another move; recovery preserves lesson drafts and independently saved curriculum changes.

  • Native course section renames now save in order and preserve other successfully saved curriculum changes when a rename fails; failed title edits remain available to retry.

  • Native curriculum editing now ignores departed confirmations and stops new section, lesson, quiz, and question requests after leaving, while retaining acknowledgements for creations already accepted by the server.

  • Native course authoring rejects malformed edit links, resets drafts when the course, account, or community changes, and prevents departed saves and publishing results from affecting another editor.

  • Native course editing now shows a retry state after an unsuccessful initial load and prevents saving, publishing, or adding cohorts until the existing course is loaded successfully.

  • Appreciations stay cleared after an access-refused response while a new refresh is pending, and only show notes again after a successful read.

  • Reduced the Android Hermes bundle by about 502 KB by disabling inline requires, bringing it below the existing bundle-size ceiling. Verified Android emulator navigation and iOS export; signed-release startup and physical-device acceptance remain separate checks.

  • Appreciations reload when the signed-in account or community changes and ignore reaction callbacks from the previous context.

  • Corrected the mobile translation check to recognize the French invitation-reference label as legitimately identical to English, without changing untranslated-phrase limits.

  • Listing edits now send an empty tag list to remove all skills and retain that instruction when a tag save needs retrying. Keyboard and narrow-screen regression inventories have been reconciled with the current UI.

  • Appreciations preserve reaction state across overlapping reads, reconcile uncertain writes before allowing another toggle, and recover failed list pages without losing loaded notes. Wall responses include the signed-in viewer's reaction; mobile links reject invalid member IDs. Recovery messages are available in all seven mobile locales.

  • Mobile messaging prevents overlapping photo pickers, preserves drafts when native selection fails, ignores picker results after leaving a conversation, and removes newly retained attachments that finish copying after departure. Opening attachment options dismisses the keyboard.

  • Mobile search exposes all saved searches, preserves drafts on failed saves, retries failed result pages in place, and improves large-text layouts and loading/error feedback.

  • Mobile wallet recipient searches no longer overwrite newer choices; transfer drafts reset on account/community changes, and departed panels stop unsent operations while preserving confirmed-operation cleanup.

  • Mobile group tools preserve selected wiki pages and questions through late responses, provide media/task/wiki recovery states, retain usable tasks when statistics fail, and ignore actions after departure.

  • Mobile group discussion links reject array-valued, fractional and unsafe group or discussion IDs before fetching a thread.

  • Mobile group discussions preserve replies and drafts through recovery, prevent duplicate sends, ignore outdated paging results, and keep the Android reply composer above the keyboard.

  • Mobile event details reject array-valued, fractional and unsafe numeric route IDs before enabling event reads.

  • Mobile event details replace RSVP counts with newly refreshed event data and correctly apply RSVP updates after StrictMode lifecycle replay.

  • Mobile exchange actions prevent overlapping submissions, ignore departed-screen completions, and refresh status after uncertain network/server responses before allowing further actions.

  • Mobile exchange lists retain loaded requests after refresh or pagination failure and retry the failed page without restarting the list.

  • Mobile jobs, applications and owner postings retain loaded results after a page failure and retry the failed page without restarting the list.

  • Mobile polls preserve earlier questions when a later page fails, show a retry instead of an end marker, and retry the failed page without restarting the list.

  • Mobile external-link errors no longer appear after the screen that opened the link has unmounted.

  • Mobile club directories now reach all result pages, preserve earlier clubs when loading fails, retry the failed page, and display complete club descriptions.

  • Mobile connection decisions update promptly without waiting for vibration; departing the screen prevents subsequent recovery reads and delayed error feedback.

  • Mobile connections retain loaded members and confirmed decisions during refresh recovery; Retry resumes a failed later page without discarding earlier connections.

  • Mobile activity history keeps loaded totals and timeline visible during refresh recovery, offers retry after failure, and waits for successful loading before displaying totals.

  • Native articles reject ambiguous route values without crashing, reset comment state between articles, preserve loaded content through transient refresh failures, and handle sharing errors without affecting departed screens. Refused access hides the article.

  • Native blog browsing preserves loaded articles and retries the failed pagination cursor, with explicit recovery instead of automatic repeated requests after an error.

  • Native podcast browsing shows refresh progress, preserves loaded shows after a page failure, and retries the failed page instead of restarting the catalogue. Automatic pagination waits for explicit recovery after an error.

  • Native organisation detail actions ignore departed share and website callbacks, and suppress website errors that arrive after navigation.

  • Native organisation registration validates website URLs, preserves field-specific server feedback, scrolls to the first invalid field, and rejects departed submissions. Organisation browsing retries failed pages without losing loaded records and clears refused content; detail links reject ambiguous IDs and website links use the shared validated opener.

  • Native profile-load retries wait for active profile or photo updates, preventing recovery reads from racing with a save.

  • Native profile editing retries failed full-profile loads without losing drafts, saves only edited fields, keeps newer queued edits open after saving, and ignores photo selection results after departure.

  • Native translation settings preserve the latest recovered preferences when reads overlap, block saves before a successful load or after departure, and retain selected preferences for retry after a failed save.

  • Native data-export history ignores older read results after a newer refresh, so a delayed initial response cannot hide a completed export or replace current history with an obsolete error.

  • Native personal-data export cancels sharing when its screen is left, rejects departed request callbacks, and hides history counts until loading succeeds. Account deletion reports uncertain outcomes accurately and cleans up only the submitting session after confirmed deletion, including after departure.

  • Native password changes finish only the submitting session after departure, and ignore departed submit callbacks. Blocked-user settings reject departed unblock actions, preserve failed unblocks for retry, and show counts only after a successful load.

  • Native offline check-in now restores encrypted pending device setup and validated offline rosters, preserves scan queues across concurrent scans, synchronization and roster refresh, and reuses durable batch identities for explicit recovery. Removed sessions and obsolete confirmations cannot recreate or delete changed attendance data.

  • Native offline attendance validates returned batch identities, retains unresolved or stopped work for review, and checks submitted results after roster expiry without resending attendance. Recovery controls include seven-language guidance and guarded camera permission handling.

  • Native event tickets persist unresolved requests for explicit recovery, preserve the original quantity and cancellation reason, and reject obsolete retries instead of creating another claim.

  • Failed native comment reactions now restore the previous reaction even if the recovery read also fails or the sheet is closed, without replacing newer comment text.

  • Native comments preserve drafts and reaction state through failed or overlapping requests, expire obsolete Send callbacks, and refresh after successful writes even when an older comment read is pending.

  • Native marketplace load failures now show translated recovery guidance instead of empty-catalogue advice; category and featured-list failures remain visible with a guarded retry while available listings are preserved.

  • Native marketplace recovery now retries the failed page without discarding loaded listings, prevents repeated scroll retries after failure, serializes saves, and preserves save changes against older refresh responses.

  • Native listing creation and editing prevent duplicate saves and retry only failed skills or photos after a partial save. Retry progress blocks premature continuation, departed forms ignore retained actions, and delayed description suggestions preserve newer edits.

  • Native group join-request confirmations expire when the group, management permission, or server access changes. Returning to the previous context requires fresh confirmation, and older responses cannot clear a newer action or show stale feedback.

  • Native notification permission checks now show a retryable error when the device cannot read permission state. Retry honours saved choices without opening a permission prompt or recording a new decision.

  • Native notification permission choices prevent duplicate permission requests and conflicting dismissals, with progress shown on the selected action.

  • Native offline banners reserve measured layout space above navigation, including enlarged text. Feedback defaults above an open keyboard so validation messages remain visible.

  • Native two-step sign-in binds verification and recovery credentials to their original session and challenge, discards superseded setup responses, validates keyboard submissions, and retains accessible names on busy actions.

  • Native sign-in, session recovery and biometric-lock actions prevent duplicate requests, retain clear busy labels, and allow retry after failure. Sign-out supersedes older restore or unlock completions so those completions cannot clear its progress or uncover protected content.

  • Native community selection serializes taps, keeps busy state through rollback, and ignores superseded configuration loads. Confirmed switches preserve newer sessions during preflight or sign-out and avoid misleading reassurance after sign-out has started.

  • Native registration shares session persistence and navigation with sign-in, prevents duplicate submissions, and ignores delayed responses after logout or a replacement sign-in. Session cleanup preserves newer sign-ins and completes even when offline cleanup fails.

  • Native password recovery and verification resend prevent duplicate submissions, retain retryable forms after failure, ignore departed-screen results, and keep replacement reset links independent. Recovery actions retain explicit accessible names while busy.

  • Native bundles share the Zod and Sentry core implementations across import styles while preserving native browser-replay exclusions; bundle-export failures now include their exit status.

  • Article attachment and feedback controls retain meaningful accessible names during loading and clear stale busy descriptions after completion or cancellation.

  • Native article text now refreshes when the system font size changes, preventing stale enlarged or undersized body text.

  • Native Resources keeps earlier results during page retries, reaches later knowledge-search pages, resolves saved files independently, and improves category recovery and keyboard layout.

  • Resource and knowledge-base pagination now follows displayed ordering, including tied rows; native API helpers support later search pages, exact saved-resource lookup, and confirmed feedback totals.

  • Native course enrolment now submits the confirmed credit price; the API rejects changed prices before charging and asks the member to refresh and confirm again.

  • Preserve course lists during refresh failures and invalidate stale native enrolment confirmations when the displayed course price or availability changes.

  • Keep loaded Groups and Listings visible after a later request fails, with retry feedback and recovery controls alongside existing results.

  • Show retryable loading errors alongside retained Home feed and message-list content, keeping existing results available during recovery.

  • Keep hashtag feeds and discovery results stable across navigation and failed refreshes, and refresh the active hashtag search instead of unrelated trending topics.

  • Keep member-directory and message-recipient totals tied to the current search, and expose retry controls without hiding loaded directory results.

  • Added guarded pull-to-refresh to native invitation and retention lists, preserving active drafts and blocking refresh during requests; corrected singular retention confirmation wording across supported languages.

  • Keep native pagination metadata aligned with accepted rows, preserve it during recoverable refresh failures, and clear it with rows when access is withdrawn.

  • Keep reviewed invitation states current when returning to the native organiser list, preventing obsolete revocation actions from reappearing after concurrent acceptance.

  • Distinguished rejected invitation revocations from uncertain requests, allowing explicit current-state review while preserving original retry keys for unknown outcomes.

  • Clarified that the native retention history control opens a saved run without reloading or applying it.

  • Retention previews clear stale date validation after a valid submission, and confirmation wording handles single-record previews correctly.

  • Corrected the refresh button translation in the native retention workspace.

  • Retention previews now report missing registration settings as a validation error instead of a server error, and remain retryable after settings are configured.

  • Retention preview retries now return their saved snapshot after its records change or are anonymised, without creating another run; changed dates and reused apply keys still conflict. Cancelled and issued native invitation campaigns no longer display a future-send snapshot notice.

  • Native invitation conflicts now show their specific recovery explanation without a duplicate generic failure message; failures while reviewing the current campaign remain visible.

  • Native invitation management now preserves definitively rejected changes for explicit, read-only campaign review, rechecks organiser access, and prevents stale replacements while keeping uncertain requests on their original retry keys.

  • Invitation campaign conflicts now identify definitive revision refusals separately from uncertain retry-key collisions, enabling safe client review without treating a previously successful send as rejected.

  • Improved native event organiser chevron contrast in dark mode and corrected the invitation CSV data label in all seven mobile languages.

  • Hide native invitation sending and scheduling controls when an event has already started or has no valid start time, while keeping campaign cancellation available.

  • Fixed retries of newly created invitation previews after audience membership changes by matching the submitted request to its frozen snapshot, retaining current group access checks and legacy preview compatibility.

  • Fixed invitation campaign scheduling recovery after the scheduled time: an exact retry returns the existing campaign without rescheduling, while new invalid send times remain rejected.

  • Exact retries of successfully issued event invitation campaigns now return their saved result after invitation expiry, without issuing or queuing anything again. New issuance still requires a valid future expiry.

  • Android guest attendance forms now explicitly resize above the keyboard, keeping undo reasons and scrollable confirmation controls usable at enlarged text sizes.

  • Native guest attendance hides Undo when the current history entry cannot be undone, using server-confirmed availability while preserving valid check-out actions.

  • Invalid guest-attendance transitions and repeated undo attempts now expose definitive rejection details, allowing native saved-action review instead of trapping organisers in an endless recovery retry. Unmarked failures remain uncertain.

  • Guest attendance conflicts now identify a definitively rejected attendance version separately from idempotency conflicts, allowing clients to review stale state without misclassifying uncertain retries.

  • Registration answer-review and export failures now scroll into view after layout, retain entered evidence and expose updated feedback through an accessibility live region. Submitting dismisses the keyboard, and departed-form callbacks cannot move a newly opened form.

  • Fixed native audited CSV exports failing after access-token expiry: a read-only session check now uses normal token renewal before the single export POST, while retaining the originating account and screen guards. The audited export itself is never automatically replayed.

  • Registration answer exports now validate audit purpose and reference even when no submissions match, and accept maximum-length references without overflowing per-submission audit keys. Existing shorter-reference audit hashes remain unchanged.

  • Native registration answer reads now accept empty answer maps returned by PHP instead of reporting a contract error; organiser review preserves purged-answer status and rejects unexpected sensitive answer disclosure.

  • Registration form revision conflicts and immutable published-form refusals now identify the rejected form field, allowing clients to offer draft review without treating uncertain retry-key conflicts as rejected writes. The native form journal captures the rejection reason before its deferred storage callback.

  • Registration policy saves now retain a stable request fingerprint so explicit recovery of new saves survives intervening event schedule or policy changes without applying the change twice. Legacy saved requests retain their existing comparison behavior.

  • Native registration policy confirmations and retry actions now use the existing translated Cancel and Retry labels.

  • Explicit UTC registration-setting dates now work for events in other timezones; legacy local-time fields retain event-offset validation.

  • Registration settings revision conflicts now identify the rejected version field while retaining the existing conflict code, allowing clients to distinguish them from uncertain retry-key conflicts without discarding saved changes.

  • Registration settings now honour explicit date removal for registration windows and cancellation cutoffs, while preserving omitted dates and idempotent retries.

  • Native button labels and the People heading remeasure when system text size changes, fixing clipped labels after returning from Android Settings while preserving surrounding form state. Verified on the Android emulator at 100% and 200% text with the search draft retained.

  • Native agenda session panels now load owner-scoped saved requests before enabling changes and expose explicit recovery after uncertain results or reopening. Storage failures offer a separate reload, unresolved work blocks new requests, recovery failures remain visible, and older receipts cannot replace newer registration projections. Physical restart verification remains open.

  • Native agenda recovery state now checks saved work before allowing changes, blocks fresh requests while an unresolved operation exists, exposes failed recovery and storage retry states, and ignores obsolete loads or departed completions. Recovery controls are not yet connected to the session panel.

  • Native agenda recovery transport now requires a saved request before dispatch, reuses its original action/version/key, blocks overlapping sends and preserves accepted receipts after screen departure. Recovery is explicit; panel integration remains unfinished.

  • Native agenda registration and withdrawal retries preserve their request key and original version after uncertain failures while the panel remains mounted, allowing server replay instead of a new request. Changed registration versions receive distinct keys; restart recovery remains unfinished.

  • Native agenda session registration blocks rapid duplicate submissions, ignores confirmations after departure or changed registration permissions/version, and suppresses late completion updates after leaving the panel.

  • Native event attendance ignores delayed no-show confirmations after the screen unmounts, preventing departed confirmations from submitting changes.

  • Native organiser People explains per-member registration failures for changed records, full capacity, withdrawn access and unavailable members in all seven app languages. A read-only Refresh action helps review current records without replaying successful changes; unknown failures retain a translated fallback.

  • Native organiser People reloads its roster and permissions after returning to the screen or foregrounding the app, discards departed confirmations and ignores late background reads while preserving pending actions for explicit recovery.

  • Native organiser People keeps its invitation form collapsed until requested, making member states and History reachable in the first Android viewport.

  • Native authenticated file downloads retain Unicode filenames, isolate temporary files, clean up interrupted transfers and check account/community and screen identity before sharing.

  • Native event management resets stale data and permissions when the account, community or event changes, ignores previous requests, and rejects malformed event links without a dead retry action.

  • Native event-management section links now respect the event permissions used by the management workspace instead of automatically redirecting members to unavailable tools.

  • Native event management removes retained event details and actions after access is refused, keeping them hidden during retry while preserving the workspace through temporary server failures.

  • Native event attendance rejects malformed, fractional, unsafe and repeated-array event links before loading the roster or offline device tools.

  • Corrected native offline check-in guidance: enabled time-credit rewards are processed by the server after attendance synchronization; attendance does not universally exclude rewards.

  • Native event check-in puts attendee search and the live roster before offline device administration, reducing scrolling for routine attendance. Offline guidance no longer assumes where the roster appears.

  • Native event attendance retains the server-confirmed check-in state while the roster refreshes, suppresses older actions and totals, and waits for current roster permissions before offering the next action.

  • Native event attendance clears the roster and summary after access is refused, preventing old attendance details and actions from reappearing during a later refresh.

  • Native donation receipts clear immediately when the account, community, or receipt changes and ignore responses from the previous selection.

  • Native donation receipts interpret Laravel database timestamps as UTC and validate calendar dates before display, avoiding device-dependent date shifts and invalid-date output.

  • Native donation receipts validate the supported donation lifecycle states and reserve success styling for completed donations, rather than treating unknown statuses as successful.

  • Native donation receipts clear retained data when the API withdraws availability, removing stale retry controls while preserving recoverable refresh behavior for temporary failures.

  • Native donation receipts display existing translated labels for known payment methods instead of raw wire codes such as bank_transfer.

  • Malformed native donation-receipt links no longer request invalid records or show retry controls that cannot succeed.

  • Native donation receipts now map the API community, reference and donation identity correctly, and reject malformed amounts instead of displaying them as zero.

  • Late event communication list and pagination responses no longer replace a newer confirmed message version or restore obsolete lifecycle actions in the native app.

  • Native event drafts now explain that recipient and delivery totals are finalized when scheduled instead of displaying misleading zero counts; audience preview labels also handle singular counts across all seven locales.

  • Native event organizers can correct a past-time schedule after the API confirms it was rejected; uncertain failures retain the original request for recovery.

  • Recovering an accepted event communication schedule now returns its existing result after the scheduled time has passed, while new requests to schedule in the past remain rejected.

  • Native event communications now offer explicit saved-action recovery, isolate recovery by account and event, preserve newer wording, and block conflicting writes while the previous result is uncertain.

  • The native event recovery transport reuses saved requests after uncertain responses, checks ownership after storage, and records accepted work after departure; recovery screen integration remains in progress.

  • The native event recovery journal preserves exact pending requests and validated receipts, refuses conflicting unresolved actions, and keeps replay identities when storage fails; screen integration remains in progress.

  • The native encrypted draft journal supports bounded full-length event communication snapshots, including JSON-escaped text, in preparation for durable retry recovery. Other draft limits remain unchanged.

  • Native event communication composing waits for a successful event load and stays unavailable for invalid, loading, failed or refused event states.

  • Native event scheduling, cancellation and retry actions prevent duplicate submissions and ignore late responses after the screen closes.

  • Native event communications reset event-specific forms when changing events and ignore stale list or pagination responses after departure.

  • Native event draft saves preserve wording edited during a pending save, continue against the saved draft version, and prevent duplicate submissions or late failure alerts after closure.

  • Native event communications preserve a new message when an older draft finishes loading and ignore abandoned draft-load failures.

  • Native event audience previews discard stale results after composer changes and suppress late failures after the screen closes.

  • Endorsement skill grouping preserves database case and accent equivalence when returning structured member records.

  • Native endorsements use structured API records to preserve member names, nullable avatars, comments, timestamps and unique endorsement identifiers. Existing grouped response fields remain available.

  • Native skills and endorsements show retry controls after failed refreshes without hiding loaded content; discovery pull-to-refresh reloads categories and tracks its loading state.

  • Native skills header text expands fully instead of truncating at enlarged text sizes.

  • Native skills tabs remain reachable by horizontal scrolling at enlarged text sizes.

  • Native skills preserve text edited during a save and prevent duplicate removals or late removal messages after leaving the screen.

  • Native skill creation prevents duplicate submissions and keeps an older save from closing a replacement form.

  • Native skill discovery distinguishes failed requests from empty results and provides inline retries for categories, skills and members.

  • Native skill discovery keeps category and member results aligned with the latest selection and ignores errors from superseded searches.

  • Native skill cards show their authoritative endorsement totals instead of matching unrelated grouped display identifiers.

  • Native AI chat resets conversation and draft state when the member or community changes, while preserving drafts during profile refreshes.

  • Native AI chat dismisses feedback notes on departure and prevents delayed feedback prompts or errors from appearing on a later visit.

  • Native AI chat keeps voting disabled while a feedback note is saving, including after closing the note dialog.

  • AI chat feedback updates now consistently validate member ownership.

  • Native AI feedback notes prevent duplicate submissions, keep text available for retry with visible failure feedback, and ignore old save responses after a note is closed or replaced.

  • Native AI chat serializes feedback votes, restores the saved selection when a change fails, and prevents old feedback responses from opening a note prompt in a new conversation.

  • Native AI chat isolates pending sends: starting a new conversation or reaching the response timeout prevents an older reply or error from replacing the current conversation, and repeated send gestures no longer issue duplicate requests.

  • Corrected the course maintenance guide to describe transactional completion and retryable certificate, notification and reward delivery, and replaced references to deleted regression tests.

  • Native instructor publishing dismisses confirmations when leaving the screen and suppresses delayed messages from an earlier visit. Returning refreshes course status, including writes that finish after the return refresh.

  • Native instructor dashboards show recoverable refresh failures without discarding courses, keep the refresh indicator accurate, clear refused content, and dismiss stale publish confirmations. Shared confirmations reject dismissed callbacks and preserve replacement dialogs when an older action finishes.

  • Native instructor publishing prevents duplicate submissions, ignores stale confirmations after a course disappears or changes status, and suppresses late publish results after leaving the screen.

  • Native course analytics retains loaded results through refresh and temporary failures, tracks actual refresh progress, removes refused data without dead retry actions, rejects malformed course links, and draws zero completions as empty bars.

  • The learner course list now excludes unpassed quizzes from displayed progress, matching the player while retaining historical completion records. Course-list and prerequisite quiz eligibility uses batched queries rather than querying each quiz separately.

  • Course prerequisites now honour the graded-quiz completion rule: an older completed enrolment cannot unlock a dependent course while its quizzes are missing, failed or awaiting review. Historical enrolment records remain intact.

  • Native data reads can discard retained content after access is refused, keeping it hidden during recovery while preserving content after temporary failures; the course player uses this policy.

  • Quiz recovery storage preserves pending submission identities and confirmed receipts separately for each account, community and quiz; it serializes writes, refuses conflicting unresolved answers, and reserves space for the result receipt.

  • Native quizzes show the learner’s server-saved grade when no local receipt exists, preserve newly edited answers during refresh, and remove answer prompts and editing controls when attempts are exhausted.

  • Native quizzes use the learner’s remaining-attempt count to disable exhausted submissions before another request, while allowing a saved unresolved attempt to recover with its original identity. A fresh server allowance also clears an older saved limit refusal when an instructor grants more attempts.

  • Native grading clears obsolete unsaved-work warnings when a refreshed queue removes an assessment or instructor access is revoked, while preserving editable feedback through temporary refresh failures.

  • Native instructor grading stops response-loss retries and late result notices after leaving or replacing the grading screen, while retaining exact retries for the active grading session.

  • Native quiz grade checks refresh the matching submitted result after instructor review without spending another attempt or replacing the learner’s answers.

  • Native course players explain the passing-grade requirement, disable completion until eligibility is confirmed, and let learners refresh instructor grading status.

  • Native quizzes explain when answers exceed device recovery storage, preserve editable answers without submitting, and reserve space for the result receipt.

  • Native quizzes save attempts before sending, restore unresolved answers and retry identities when reopened, and retain confirmed results or definite rejections in encrypted storage scoped to the account and community. Failed recovery reads or saves prevent a new submission.

  • Encrypted draft storage supports required reads that distinguish missing drafts from unavailable storage, corrupt manifests, and missing committed chunks, for durable retry recovery.

  • Native quizzes retain retry identities until the result is resolved and create a fresh identity for a deliberate new attempt.

  • Native quizzes stop resubmission after the attempt limit is reached and clear earlier scores when answers change or another attempt begins.

  • Native quizzes keep answers unchanged while submitting, preserve them after failure, and remove misleading retry actions when quiz access is refused.

  • Native quizzes prevent simultaneous duplicate submissions, ignore confirmations after departure, and clear answers and results when switching quizzes.

  • Native course players retain the selected lesson when a refresh reorders content and recover to a remaining lesson when the selected lesson is removed.

  • Native course video progress stays with its lesson, ignores delayed playback events from previous lessons, and resets when the video is replaced.

  • Native course players wait for initial lesson availability before showing content and keep lessons hidden until a failed first progress load is successfully retried.

  • Native course players remove lessons and completion actions when refreshed course or enrolment access is refused, and suppress delayed save feedback after access is lost.

  • Native course players no longer show a delayed progress-save error after the member leaves while the follow-up progress check is pending.

  • Native course enrolment discards confirmations when displayed price or availability changes; dismissed confirmation callbacks cannot submit, and older actions cannot close replacement dialogs.

  • Native course enrolment ignores an old confirmation after a refresh removes the course, and hides the obsolete confirmation dialog.

  • Native course details clear unavailable content after refused refreshes and prevent departed enrolment confirmations or delayed readback errors from affecting the member.

  • Native bundling shares one Sentry core implementation across import styles, removing duplicate code and restoring the Android bundle to its existing size budget.

  • Native Clubs shows complete descriptions instead of cutting off information with no way to read the remainder.

  • Native Clubs clears stale directory entries when access is refused or the directory is unavailable, with an explicit unavailable state instead of a retry loop.

  • Native Clubs loads directory pages beyond the first 20 entries and preserves loaded clubs while retrying a failed later page.

  • Native search stacks result details and saved-search controls at larger text sizes, keeping full result titles and saved queries readable.

  • Native search resets local state when the account or community changes, prevents stale delete confirmations from executing after unmount, and ignores late saved-search completions.

  • Native search retries failed later pages at the same cursor, retains earlier results, stops automatic retry loops, and removes results when refreshed access is unavailable.

  • Native saved-search lists expose all returned entries, distinguish loading failures from empty lists, retain entries during refresh, and clear them when access becomes unavailable.

  • Native saved searches capture the visible query, prevent duplicate keyboard submissions, preserve failed-save drafts, and no longer record an unrelated previous search's result count.

  • Failed authenticated mobile downloads remove their partial cached files while preserving the original transfer error if cleanup also fails.

  • Authenticated mobile downloads preserve readable filenames, including spaces and accented characters, instead of exposing internal cache timestamps in the share sheet.

  • Native Knowledge attachment downloads no longer open a share sheet after the article closes or loses navigation focus; returning does not revive an old pending share, and abandoned downloaded copies are removed from the cache.

  • Native HTML serialization imports its existing escaping functions directly, avoiding unused entity-decoding tables in the Android bundle while preserving Unicode and attribute escaping.

  • Native Knowledge feedback API handling reads the saved choice and current totals after submission, and propagates write or confirmation failures instead of inferring counts from a receipt.

  • Native Knowledge attachments recover after a successful article refresh; late download failures cannot hide files from the refreshed response.

  • Native article rendering imports only the Ramda utilities it uses, reducing Android bundle overhead while preserving reader behaviour.

  • Native Knowledge article bodies preserve HTML and Markdown structure, decoded entities, and link destinations through a shared native renderer; plain-text articles retain literal content and line breaks.

  • Native Knowledge article reads clear unavailable content and titles after access refusal, keep them hidden during recovery, reject malformed article links, and isolate reads when moving between articles.

  • Native Resources now reserves space for the keyboard so search feedback remains reachable by scrolling, including at large text sizes.

  • Native Knowledge search now offers Load More beyond the first 20 matches, preserves results when a page fails, retries that page, and discards late results after the search term changes.

  • Knowledge search supports cursor-paginated results while preserving the legacy array response, title-match priority, and popularity ordering. Tied results have a deterministic order, and cursors are scoped to the search term.

  • Knowledge article pagination now follows manual ordering and timestamp ties without repeating or skipping unchanged articles; existing numeric cursors remain supported while their anchor exists. Resource and Knowledge cursors reject impossible dates and times.

  • Native Resources and Knowledge discard retained results after access is refused, including while refreshing again, and show an unavailable state. Knowledge empty-state guidance now matches browsing or searching without suggesting a category control that is absent.

  • Refreshing native resources also rechecks the opened saved file, removing stale details when it is no longer available.

  • Native saved resources open by their exact ID regardless of browse page, with retry for failed loads and a clear unavailable state for removed files.

  • Native resource pagination retries the failed page without discarding already loaded files.

  • Native Files and knowledge-base browsing can load additional pages instead of stopping at the first page.

  • Resource pagination follows manual ordering and creation time consistently, avoiding skipped or repeated files and preserving continuation when the last file on a page is deleted.

  • Native resources show category-loading failures with a dedicated retry while keeping loaded files available.

  • Native knowledge-base refresh follows the current search or browse mode after entering or clearing a search term.

  • Native comment reloads preserve newer reaction changes when requests overlap while still accepting fresh comment text and later reaction counts.

  • Native connection actions remain responsive after React development lifecycle checks replay the screen's effects.

  • Native comment reactions block duplicate pending toggles and actions after closing the sheet, expose per-comment busy state, and suppress failure messages after departure.

  • Native comment reactions replace the member's previous reaction correctly and reconcile counts and selection with the server response.

  • Native blog navigation resets article-specific comment visibility and expires share actions from the previous article while preserving comment deep links.

  • Native blog articles handle share-dialog failures with translated feedback, keep dismissal silent, and suppress late errors after leaving the article.

  • Native comment sheets show a persistent retry notice when reloading existing comments fails; retrying reloads the list without resending the comment.

  • Native comment delete confirmations expire when the sheet closes or changes target, and cannot submit a deletion more than once.

  • Native comment sends reject duplicate or departed callbacks, preserve newer composer text during pending sends, and suppress completion effects after departure.

  • Native comment sheets isolate state by article or item and ignore reads completed after departure, allowing a new target to load immediately.

  • Native comment sheets stop automatically repeating failed initial loads and show a persistent error with an explicit retry.

  • Native blog refreshes preserve the article and open comment sheet, show recoverable refresh errors, and stop displaying retained articles when access is refused.

  • After an uncertain native appreciation reaction, the app reloads server state before allowing another toggle and provides a translated read-retry message if recovery fails.

  • A refused native appreciation reaction now preserves note content received by a concurrent refresh.

  • Native appreciation refreshes no longer overwrite reaction changes made while the read was in flight; later fresh reads still update counts.

  • Appreciation lists now return the current viewer’s selected reaction, preserving native reaction state after refresh while filtering reaction metadata by viewer and community.

  • Native appreciation walls preserve accumulated notes during refresh, retry failed pages without skipping them, reset when changing members, and prevent rapid load-more taps from skipping a page.

  • Native appreciation reactions prevent duplicate taps and submissions after leaving the screen, and keep each pending reaction independently disabled.

  • Native Activity waits for loaded totals and shows a persistent retry notice when refreshing existing activity fails.

  • Mobile bundle-budget failure reports include the export process status and signal to distinguish native tool crashes from bundling errors.

  • Native bundles share one Zod validation core between forms and API schemas, removing duplicate library code without dropping features or translations.

  • Native authenticated downloads recheck the session and community before opening the share sheet, discarding stale files after an account or community change.

  • Native linked-account actions stop at screen departure, including delayed removal confirmations; failed requests retain the email for retry.

  • Native wiki loading failures show a persistent retry state instead of incorrectly reporting that no pages exist.

  • Native wiki pages place editing actions below the heading so large text cannot collapse the title or push page content into a blank scrolling area.

  • Native form feedback appears at the top while the keyboard is open, preventing validation and error messages from being hidden behind it.

  • Native group forms keep focused fields above the on-screen keyboard and allow form actions to respond while it is open.

  • Native Android discussion replies keep the composer above the keyboard; group discussion guidance wraps fully at large text sizes.

  • Native group task counts no longer remain visible as current when both task and statistics refreshes fail.

  • Native group media and task filters clear previous results when changing filters and offer explicit retry states when loading fails.

  • Native group tasks remain available when their statistics request fails, with unavailable counts hidden and refresh feedback shown.

  • Native group media and task pagination now follows current cursors, avoids duplicate rows, and uses current filters when delayed actions refresh the list.

  • Native wiki creation respects page navigation while saving and clears obsolete page history when opening the newly created page.

  • Native wiki deletion confirmations expire when the page selection changes, and completed deletions preserve a newer page view.

  • Native wiki saves preserve a newer page selection, and stale revision requests no longer change its loading state or report errors for the previous page.

  • Native group votes and accepted answers distinguish saved actions from failed refreshes and cannot overwrite a newer question selection.

  • Native group Q&A ignores stale answer readbacks after changing the expanded question, preserving the newer view.

  • Native group actions now ignore stale confirmation callbacks after leaving the screen, covering membership and collaboration mutations.

  • Native group discussion replies now prevent repeated submission before rendering, preserve text edited while a reply is sending, and suppress delayed send feedback after leaving the screen.

  • Native discussions retain accepted replies on failed refresh, ignore superseded older-page requests, clear state when changing discussions, and hide retained content when refreshed access is refused.

  • Native offline status banners reserve physical space above screens, preventing overlap with Back controls on Android at large text sizes.

  • Shared test animation mock no longer remounts the tree it wraps. The framerMotionMock Proxy built a new component on every property access, so <motion.div> changed component type on every render and React destroyed and rebuilt the whole subtree beneath it each time. Queries resolved to nodes that were detached microseconds later, which surfaced as an intermittent "element could not be found in the document" in whichever suite lost the race — most recently GroupDetailPage, which blocked a production deploy. Components are now cached per tag, matching the real @/lib/motion shim. Test-harness only; no runtime behaviour changes.

  • Native attendance ignores delayed no-show confirmations after leaving the attendance screen.

  • Native ticket catalogues now distinguish unsupported-event refusals from retryable load errors and use the shared accessible retry layout for genuine failures.

  • Native ticket requests with an unknown outcome now have a translated recovery card that restores the original request, prevents changed-quantity duplicate claims, and remains reachable when ticket availability changes.

  • Native ticket requests now save account- and community-scoped retry keys before sending and retain them across screen departures or app restarts; failed storage prevents an unprotected submission.

  • Native ticket loading now ignores obsolete retry responses so older data or errors cannot replace the latest catalogue.

  • Native ticket claims and cancellations retain their request key when retrying the same request after a failed or lost response while the screen remains open.

  • Native event tickets now reset catalogue and cancellation state when the event, account or community changes, ignoring delayed results from the previous screen.

  • Native free-ticket claims and cancellations now prevent overlapping submissions and suppress late screen feedback after departure.

  • Native event details now replace local RSVP counts and participation state with the latest refreshed event snapshot.

  • Native wallet forms stop pending preparation when closed. Transfers and donations cannot start from a departed form; responses to already-sent requests finish their retry-record cleanup without showing stale success or refreshing the departed wallet. Changing account or community resets the wallet view and its transfer draft.

  • Native wallet recipient search ignores outdated responses. Changing the search clears old matches, and late results cannot replace the current query's recipients or interrupt its progress. A delayed recipient lookup from a wallet link no longer overwrites a recipient selected manually.

  • Native message attachments remain reachable when the keyboard is open. Opening the attachment menu dismisses the keyboard so it cannot cover the photo action, including at enlarged text sizes.

  • Native message photo selection stops when its conversation closes. Late permission and picker responses cannot open or attach media to a departed screen; managed copies completed after departure are cleaned up. Overlapping picker actions are blocked, and native errors preserve the draft and allow retry.

  • Native exchange actions prevent duplicate or competing submissions. Actions remain retryable after failure, and responses received after leaving the exchange or changing account, community or exchange cannot show stale notifications or navigate away from the current screen. Lost action responses trigger a fresh read of the exchange without automatically repeating the write. Actions pause while that read is pending or has failed, including confirmation sheets already open.

  • Native listing edits preserve text entered while a generated description is loading. A late suggestion no longer overwrites the member's newer draft.

  • Native hashtag discovery keeps loaded topics available after a failed refresh. Retry remains available, and older refresh responses cannot replace newer topics or surface obsolete errors after feed access changes.

  • Native biometric unlock prevents overlapping prompts and respects pending sign-out. An obsolete prompt cannot uncover the account while sign-out is running. Controls announce progress, and a failed sign-out remains recoverable.

  • Native sign-in and verification-email resend prevent duplicate actions. Resend controls show accessible progress, remain retryable after failure, and avoid updating departed screens.

  • Native MFA and password recovery handle keyboard, duplicate-tap and departure cases consistently. Incomplete MFA codes cannot be submitted from the keyboard, and verification received after departure cannot start sign-in. Password recovery prevents overlapping submissions while preserving retry after correctable failures.

  • Native registration prevents duplicate submissions and ignores responses received after the form closes. Refused submissions remain retryable with the entered details preserved. Session completion uses the shared sign-in provider, preventing an older registration from undoing sign-out or replacing a newer account while credentials are being saved.

  • Native sign-out completes when offline file cleanup fails. Encryption-key and index cleanup still run after a file-deletion error, which is reported for diagnosis. The session recovery screen prevents duplicate actions, shows sign-out progress, and keeps failed actions retryable.

  • Native session restoration preserves a replacement sign-in during old cleanup. Finishing cleanup for a rejected startup session no longer clears an account that signed in while that cleanup was pending.

  • Native community settings ignore obsolete refresh responses after a switch. A delayed response from the previous community can no longer replace the new community's settings or clear its remembered selection. Startup and departure checks also prevent stale loading and recovery work from changing current state. Overlapping switches roll back to the last working community when the latest choice fails, and loading stays active until that rollback finishes.

  • Native community selection prevents overlapping choices and shows progress. Closing the picker prevents a late response from redirecting or consuming a recovery link. Failed switches no longer claim the session is intact once sign-out has started.

  • Native Marketplace keeps the current catalogue during overlapping requests: stale filter/page responses are ignored, duplicate page requests and rows are prevented, and failed refreshes/paging retain listings with Retry. Repeated Save taps are guarded with an accessible busy indicator; older reads cannot undo a confirmed save, and a failed save no longer restores outdated listing details.

  • Native Connections, exchange requests and Jobs retain loaded rows when refresh or paging fails, with a visible retry action. Starting a Connections refresh no longer restores requests that the member has already accepted or removed.

  • Native course, podcast, blog and poll lists explain failed refreshes and paging without hiding loaded content. Members can retry from the list; course and podcast pull-to-refresh indicators now reflect the pending request.

  • Native listing recovery preserves the intended edits: removing every skill tag now clears the saved tags, including after a failed clear is retried. Partial-save retries ignore repeated taps and show a busy state while Continue is disabled. Leaving a pending creation prevents late results from starting follow-up writes or navigating back to the listing.

  • Native listing saves ignore repeated taps while a request is pending: create and edit forms synchronously guard submission before the disabled button rerenders, while allowing a new attempt after failure.

  • Native list recovery and hashtag navigation: Home, Messages, Groups, Listings and the member directory now expose retryable errors without hiding loaded rows. Hashtag routes no longer decode parameters twice, and discovery ignores stale search responses and clears old failures when returning to trending topics.

  • Native member counts now follow the current search: a slower directory response can no longer overwrite the result count after a newer member or message-recipient search has completed. Pagination exposes only its accepted response metadata, preserves it on failed refresh, and clears it when the query is replaced or disabled.

  • Native hashtag feeds retain the correct posts during navigation and recovery: late responses cannot replace a newer hashtag, and failed pagination or refresh keeps loaded posts visible with Retry. Hashtag feeds now use the shared guarded pagination path, including duplicate-row and overlapping-request protection.

  • Native Android/iOS search, refresh and shared-control reliability: mixed search results retain members and listings with the same numeric ID; refreshing during pagination no longer strands the loading indicator, and failed refreshes preserve the next-page position. Search and recipient lists expose retryable errors while retaining loaded results. Detail loaders clear the previous record when the requested identity changes. Retry buttons show progress, pending confirmations resist dismissal, and feed galleries fit their card with a missing-image fallback. Updated the Expo updates package to its SDK 55 recommended patch and aligned Apple version metadata. Metro now preserves Expo exclusions and ignores native build scratch folders that could crash its Windows watcher during Android compilation.

  • Every legal page on the accessible frontend showed the wrong name. communityName() in web-uk/src/routes/legal.js read res.locals.tenant, which the shell does not set — it sets tenantName, tenantSlug and communityName — so the legal hub, terms, privacy, cookies, community guidelines, acceptable use and the accessibility statement all captioned themselves "Project NEXUS Accessible" instead of the community's own name, on a site where eleven communities share the templates. It now prefers tenantName and falls back through the older reads, so nothing that does pass a tenant object regresses.

  • The accessible frontend has a changelog, rendered from CHANGELOG.md itself. /changelog lists every release with its date and /changelog/<version> shows that release's notes. It is a list-then-detail pair rather than one page because the file holds 18 releases and the largest single release is over 2,000 lines; the React page renders the whole ~7,900-line document in the browser, which is a poor trade for the members this frontend exists for. The footer's Changelog link, deliberately withheld while the page did not exist, is now live.

    • Markdown is rendered at BUILD time, not per request, by web-uk/scripts/build-changelog.js, and marked is a devDependency for that reason: the internet-facing app ships no markdown parser and parses nothing per request. The output is passed through the existing sanitizeCmsHtml allow-list even though the source is our own file, because marked passes raw HTML through by default — a test asserts no release contains a script tag, an iframe or an on* handler.
    • One generated file per release, plus a small index. The first version wrote a single 2.7 MB JSON; require then pulled the entire rendered history into memory at boot to serve a page needing one release, and every new release produced a 2.7 MB diff in a public repository. The index carries metadata only and is loaded at boot; a body is loaded on demand.
    • The release slug from the URL is matched against the index before it is used to build a path, so only a generated file can ever be read. Pinned by a test that feeds it ../index and ../../package.
  • The accessible frontend's /features page now shows the whole platform catalogue — 8 groups, 119 features — with working search and category filtering. It was a hand-written list of six bullets, which had drifted comprehensively from the React page's 119.

    • The catalogue is now shared. The group order, item order and each feature's maturity moved out of react-frontend/src/pages/public/FeaturesPage.tsx into react-frontend/src/data/featuresCatalogue.json, which both frontends read, so the two pages cannot drift again. The React page imports it directly; the accessible frontend keeps a generated copy (npm --prefix web-uk run build:features, verified by check:features) because its Docker build context is web-uk/, so neither the catalogue nor the React locale files are in its image.
    • Nothing was re-translated. All 119 features and their descriptions already existed in all eleven languages for the React page, so the generated copy carries that text rather than re-keying ~2,600 strings into lang/*.php and creating a second copy to keep in step.
    • 🔴 Search and category filtering run on the SERVER, as a plain GET form. The React page does the same job in JavaScript with a search box and filter chips; doing that here would leave the members this frontend exists for with no filtering at all. Search folds case and accents on both sides of the comparison, an unknown category in the query string is ignored rather than matching nothing, groups with no surviving items are dropped instead of rendering empty headings, and because it is a GET a filtered view is a shareable URL.
  • The accessible frontend's footer now carries the community's partner logo, the source repository and the powered-by badge, each under its own heading, in GOV.UK Design System style. The React footer has shown all three for a long time; the accessible frontend showed none of them, so a community's funder or council partner was invisible to exactly the members most likely to have been referred by one. The bottom of the footer is now a deliberate replica of the React footer's four bands — the community's copyright line with cookie settings opposite it, the release status line, the three brand marks, and the licence strip — which also removes the four stacked sentences it replaced, between them saying "Project NEXUS" three times. Details that are deliberate, each for a reason:

    • The powered-by badge sits in the second govuk-footer__meta-item, the slot GOV.UK's own footer uses for its origin mark, and deliberately does not reuse the link class GOV.UK applies there, because that class renders the royal crest through a ::before background image and this service is forbidden from displaying it. Pinned by web-uk/tests/footer-branding.test.js.
    • The badge uses the light artwork, not a choice between the light/dark pair the admin panel offers, because govuk-footer is light and this frontend has no dark mode — there is not one prefers-color-scheme rule in its stylesheet, the GOV.UK Design System being light-only. Both variants remain in use platform-wide: React picks between them by theme and this frontend's black header already uses each community's dark logo.
    • Every brand mark sits on an explicit white plate. Measured, not assumed: powered-by-nexus-light.png is PNG colour type 2 (RGB, no alpha channel) with a white background baked in, against a footer background of rgb(244, 248, 251) — so without a plate the badge rendered as a stray white rectangle. Community partner logos have the same problem more often than not, arriving as opaque JPEGs.
    • The source-repository link is not configurable per community and must not become so: AGPL-3.0 Section 7(b) requires the running application to link to its own source, and react-frontend/src/config/externalLinks.ts keeps the same URL in a single constant for that reason. The partner logo and the powered-by badge are per-community, reading the same general.* settings the React footer reads, so configuring them once covers both frontends.
    • The release line states the platform version and links Features and Documentation. It does not link Changelog, because the accessible frontend has no changelog page yet and a footer link to a 404 across eleven live community sites is worse than a missing link. It also makes no maturity claim about the accessible service itself, whose GOV.UK phase banner correctly still reads "Beta".
    • The copyright year is literal text in the translations, as the neighbouring attribution key has always carried it, and needs bumping once a year in eleven files. A :year placeholder was tried and reverted the same day; web-uk/src/lib/template-filters.js records the two ways it broke, the second of which failed 30+ test suites.
  • GET /api/v2/tenant/bootstrap now returns config.platform_version. The accessible frontend prints the platform version in its footer and had no source for it: its Docker build context is web-uk/, so the repository's VERSION file is not in its image. The value comes from config('app.version') and nowhere else, because scripts/check-version-consistency.mjs already pins that config value to the root VERSION file — so a release bump carries through and no downstream copy can drift. Note that data.config is now always present in the response, where previously it was omitted for a community with no footer settings configured; nothing read its absence as a signal, and both frontends already access it optionally. Regression tests: TenantBootstrapControllerTest::test_bootstrap_exposes_the_platform_version_for_frontends_without_the_version_file and ::test_bootstrap_platform_version_tracks_the_repository_version_file.

  • A nightly Sentry sweep now tells a human when errors need attention. Until now the only error monitoring ran for 30 minutes after a deploy, so anything that did not coincide with a deploy was invisible: a Redis name-resolution fault ran from 2026-09-06 to 2026-09-16 (1237 events) and ended only because a routine Docker upgrade restarted the daemon, and a daily safeguarding pager fired 17 times from 2026-08-30 naming three real members who could not be contacted. scripts/sentry-triage.mjs had been written as "the collection half of the nightly Sentry loop" and had never once run on a schedule. The new Sentry Nightly workflow runs it every morning at 09:00 UTC, just after the platform's own 08:05 pagers, and alerts over Telegram — the channel the owner already receives — only when the state changes, matching uptime-check.yml rather than nagging daily about a known issue. Because the repository is public, scripts/check-sentry-nightly.mjs prints counts only to the workflow log and writes the issue titles to a file that only the Telegram step reads. The run fails when the sweep or the alert cannot be delivered, never merely because issues exist. sentry-triage.mjs also accepts its credentials from the environment now, since .secrets.local/sentry.env does not exist in CI, and no longer runs its sweep when imported. Regression tests: scripts/test/sentry-nightly-gate.test.mjs (including that no title, culprit, short id or permalink can reach the public log) and scripts/test/sentry-triage-config.test.mjs.

  • The public sales order endpoint now accepts an enquiry with no quote attached. POST /api/v2/sales/orders required a complete quote object (plan name, capacity label, billing cycle, pricing mode and five price labels), so the sales site could only use it from the quote builder on its hidden /hosting page. A plain "tell us about your community" enquiry from a public page had nowhere to go, which left the sales site with mailto: links as its only route to the inbox — and those do nothing for a visitor with no mail app configured. quote is now optional and all-or-nothing: send it complete, or omit it entirely. A half-filled quote is still rejected, because accepting one would put a half-priced estimate in the enquiry email. Quote-less enquiries get their own subject line ("Project NEXUS enquiry" rather than "Project NEXUS order enquiry") and an email body with the pricing rows and line-item table left out instead of rendered as a column of dashes. Existing callers that send a full quote are unaffected. The development-only aspnet-backend/ mirror of this controller had the same hard requirement and has been changed in step, so the two do not drift apart before that migration finishes. Regression tests: SalesOrderApiTest::test_public_sales_order_accepts_a_general_enquiry_with_no_quote, ::test_public_sales_order_still_rejects_a_half_filled_quote, ::test_public_sales_order_still_requires_a_contact_name_without_a_quote, ::test_public_sales_order_honeypot_silently_accepts_without_sending, and SalesOrderControllerUnitTests.Submit_AcceptsGeneralEnquiryWithNoQuote / .Submit_StillRejectsHalfFilledQuote on the ASP.NET side.

  • Prerender jobs could be starved for ever, silently freezing a community's crawler pages. PrerenderService::claimNextJob() ordered strictly by priority, queued_at, which is only fair while the queue periodically empties. It stopped emptying: the 2-minute drift sweep enqueued a priority-3 job roughly every 2.3 minutes while each render takes 150–180s, so one processor tick could never catch up. Between 2026-09-16 and 2026-09-19, 1,420 priority-3 jobs were claimed and exactly 2 priority-5 jobs were — both in the last minute before the backlog closed over. Jobs #7125, #7126 (the platform master) and #8035 (hour-timebank) then sat queued and unclaimable for days. The damage was not the delay but what the delay caused: both freshness sweeps skip a tenant that has any queued/claimed/running job, so the starved row removed its own tenant from the only sweeps that would have displaced it. Those tenants stopped re-rendering entirely — app.project-nexus.ie served the same crawler snapshot across three deploys, and hour-timebank.ie joined it two days later. A job held past prerender.starvation_promote_seconds (default 30 minutes) is now claimed ahead of newer work whatever its priority, oldest first; priority still decides the order inside the window. Regression tests: PrerenderServiceTest::test_a_starved_job_is_claimed_ahead_of_newer_higher_priority_work, ::test_priority_still_decides_the_order_inside_the_starvation_window, ::test_starved_jobs_are_drained_oldest_first, ::test_promotion_can_be_switched_off.

  • A tenant blocked from refresh is now reported instead of skipped in silence. prerender:detect-drift and prerender:auto-recache both skip a tenant that already has work in flight — correct, but unbounded in time and, unlike the equivalent platform-wide guard, with no alert. A job that could never be claimed therefore excluded its tenant from every freshness path permanently while both sweeps reported success every two minutes for three days. PrerenderService::stuckTenantBlocks() applies the same time bound per tenant that blockingGlobalJob() already applied platform-wide: past prerender.tenant_block_alert_seconds (default 2 hours) the sweep names the tenant and the job, reports active_job_stuck, and exits non-zero so its scheduler-liveness stamp goes stale. A running job renewing its worker lease is exempt — it is doing the work, not blocking it. Regression tests: PrerenderAutoRecacheTest::test_a_long_blocked_tenant_is_reported_and_fails_the_sweep, ::test_a_recently_queued_blocking_job_is_still_skipped_quietly.

Changed

  • Group-exchange confirmation checks the terms the member actually reviewed. The detail response carries a terms_token; if a client submits one that no longer matches the current terms, confirmation is refused with HTTP 409 TERMS_CHANGED so the member reviews the change before confirming again. Native, React and accessible clients submit the displayed token. A client that submits no token — including the Play Store build live since 15 September — confirms exactly as before and is unaffected, so this is not a breaking change. Regression tests: GroupExchangeControllerTest::test_confirmation_without_a_terms_token_is_accepted_from_an_older_client, ::test_confirmation_with_a_blank_terms_token_is_treated_as_an_older_client, ::test_confirmation_with_a_wrong_terms_token_is_still_refused, ::test_confirmation_requires_the_terms_that_were_reviewed.

  • **Quiz lessons now require the learner to have a passing, fully graded attempt before lesson completion or certificate issuance/access through the course API. Failed or pending-review attempts cannot satisfy completion. Progress reads present historical unpassed quiz completions as unfinished without deleting stored history. Clients must handle QUIZ_PASS_REQUIRED (HTTP 422) and may use lesson completion_allowed (a new field) to disable completion until eligible. Not marked breaking: no field, endpoint or caller permission is removed, renamed or retyped, and QUIZ_PASS_REQUIRED reuses the HTTP 422 coded-error shape these endpoints already return for COURSE_NOT_AVAILABLE, PREREQUISITES_NOT_MET and INSUFFICIENT_CREDITS, so a conforming client needs no change.

  • The footer's "Powered by Project NEXUS" badge links to project-nexus.net. The marketing site moved from project-nexus.ie to project-nexus.net; the badge's destination is not a per-tenant setting but a constant in Footer.tsx, so one change covers every community that has not set general.powered_by_url of its own. Measured against the live bootstrap API on 2026-09-17: six of the thirteen public communities plus the platform's own master tenant have no powered-by settings at all, and stratford and partner-demo have a custom badge image but no custom link — all nine follow the constant. The five communities that point the badge at timebanking.org (awid, crewkerne-timebank, minehead-and-coast-timebank, ryde, timebanking-org) are untouched. No database change and no per-tenant admin work is involved. Regression tests: Footer.test.tsx — "links the default powered-by badge at the marketing site", "prefers the community's own powered-by link over the default".

  • project-nexus.net is an allowed CORS origin. The sales site's enquiry form posts to this API cross-origin, so moving the site to .net without allowlisting it would have failed every submission at preflight with no server-side error to find afterwards. Added to all three copies of the list — config/cors.php, App\Core\CorsHelper and App\Helpers\CorsHelper (the hot path) — because they have drifted apart before. The .ie pair stays allowed while the old domain still resolves. Regression tests: tests/Laravel/Unit/Helpers/CorsHelperTest.php::test_getAllowedOrigins_includes_the_sales_site_origins and the matching test in tests/Laravel/Unit/Core/CorsHelperTest.php.

  • Gamification milestone emails are paced to one per member per hour. Awards arrive in clusters: on 2026-09-17 one member received four emails in three seconds — a listing confirmation, two badges and a level-up. That is the pattern that earns a "report spam" click, and milestone mail earns no engagement to offset it. GamificationEmailService::sendMilestoneEmail() now claims a per-member, per-tenant slot with Cache::add (atomic, so two awards racing in the same second cannot both win it) before sending, and hands the slot back if the send fails. The award itself is untouched: the in-app notification and push still fire, and the monthly gamification digest still recaps every badge earned, so nothing is lost but the burst. The window is config('mail.gamification_milestone_min_interval_seconds'), default 3600; 0 disables it. If the cache is unreachable the email is sent — a cache outage must never silently stop milestone mail platform-wide. This is deliberately the smaller of the two options: folding milestones into the notification_digest queue would replace a branded milestone email with a plain digest line and would do nothing for members whose digest frequency is instant, so it is a product decision rather than a deliverability fix. Regression tests: GamificationEmailServiceTest::test_only_the_first_milestone_email_in_the_window_claims_the_slot, ::test_the_slot_is_per_member_and_per_tenant, ::test_a_failed_send_hands_the_slot_back, ::test_the_limit_can_be_switched_off, ::test_the_default_window_is_one_hour.

Fixed

  • Mobile analytics access errors now describe the unavailable analytics screen without incorrectly claiming that every event-management tool is unavailable; the message is translated in all seven app languages.

  • The mobile organiser's Event analytics action now opens its own screen instead of the top of event details. It supports refresh, visible stale-data errors and refusal states, and clears prior results when the event, account or community changes.

  • Mobile bundles now share one React Hook Form implementation and load only the QR matrix generator needed by native SVG codes, avoiding duplicate form code and unused browser QR renderers.

  • Android exports no longer include six unused Material Symbols font weights through the font-loader import, removing approximately 5.9 MB of asset data while preserving the default font and explicitly selected weights.

  • Erasing an account now also deletes the notifications still queued for that person. GDPR erasure deleted the bell notifications (notifications) but left the queued copies (notification_queue), which carry content_snippet — text describing what that member did — and email_body, a fully rendered email addressed to them. Because the digest and instant runners deliberately skip erased accounts, nothing ever consumed those rows: they sat for up to seven days as pending, were then flipped to failed by the stale-row expiry, and were only deleted thirty days after that. So a member's own content could survive roughly 37 days past the erasure they asked for. Both erasure paths (GdprService and UserService::deleteAccount) now delete the rows outright — nothing there was ever sent, so there is no delivery audit trail to preserve — and the daily cleanup carries a sweep for rows left by accounts erased before this change, using the same predicate the recipient queries use so the two cannot drift apart. The sweep runs in both the scheduled cleanup and the admin-triggered one. Regression tests: MemberSelfServiceTest::test_delete_account_removes_notifications_still_queued_for_the_member, CronDigestAnonymisedRecipientTest::test_cleanup_clears_queue_rows_left_by_an_already_erased_account.

  • Email is no longer sent to addresses that can never receive it. Six of the nine hard bounces on the Postmark transactional stream in the 30 days to 2026-09-17 were to structurally undeliverable addresses: four seeded demo members on @partner-demo.test and two erased users on @anonymized.local. Neither domain resolves — .test is reserved by RFC 2606 and .local by mDNS — so every one of those sends was a guaranteed hard bounce charged against the sending domain's reputation, and the reputation damage is what puts ordinary member mail in the spam folder. EmailDispatchService::send() now refuses a recipient under .test, .local, .invalid, .example or .localhost before it resolves a tenant or touches the mailer. It returns false (the existing "not sent" contract, so no caller is told a send succeeded) and logs at info, not warning, because this is expected behaviour rather than an incident. The guard sits in send(), which sendRaw(), sendWithOptions() and EmailService::send() all funnel into, so one check covers every caller. Seeded demo data is unchanged: @partner-demo.test remains correct for a fixture, it is simply no longer mailed. Regression tests: EmailDispatchServiceTest::test_reserved_suffixes_are_recognised_as_unroutable, ::test_ordinary_domains_are_not_treated_as_unroutable, ::test_send_to_reserved_domain_returns_false_without_reaching_the_mailer, ::test_send_to_anonymised_user_address_is_refused, ::test_guard_covers_every_public_entry_point, ::test_reserved_suffix_list_is_exactly_the_documented_set.

  • Erased accounts are no longer treated as notification recipients. GDPR erasure keeps the users row and rewrites email to deleted_<id>_<hash>@anonymized.local, stamping deleted_at and anonymized_at (GdprService, UserService::deleteAccount). The notification digest and instant-queue recipient queries joined users without testing either stamp, so erased people were selected as recipients, had their queued items claimed, and produced a hard bounce every run. deleted_at IS NULL AND anonymized_at IS NULL is now part of the digest roll-up query, both instant-queue fetches and the hot-match recipient query — the four recipient lists that had no account-state filter at all. The remaining recipient queries in CronJobRunner already filter on status = 'active', which an erased account never is. The send guard above remains the backstop; this is the real fix, because a blocked send still costs a round trip and still means the code believed that person was a recipient. Regression tests: CronDigestAnonymisedRecipientTest (all three cases, including a live-member control).

  • A sales enquiry no longer goes out looking like a community's own billing mail. The sales site's enquiry form posts to POST /api/v2/sales/orders, and the resulting email was indistinguishable from ordinary platform notification mail — a real enquiry from a prospective customer was missed because of it. It arrived as "hOUR TimeBank" <billing@project-nexus.net> with audit category billing. Two causes. The category: submit() passed 'billing', which Mailer::resolveFromPrefix() maps to the billing From-address bucket. There is now a ninth bucket, Mailer::CATEGORY_ENQUIRIES, and a new audit category sales_enquiry mapped to it — tested before the billing branch so the marketplace_ / billing test cannot swallow it. The From name: it was never tenant inference (the route already passes tenant_id => null with allow_missing_tenant, and that correctly resolves to no tenant); it was the platform-wide default From name, which production sets to a single community's name. send() now accepts an optional fromName in its options array, threaded to the new Mailer::withFromName(), and the sales route sets it explicitly. No existing caller passes one, so every other email's From address and name are unchanged, and the new bucket stays on the transactional Postmark stream. Regression tests: MailerTest::test_sales_enquiry_category_routes_to_its_own_from_bucket, ::test_sales_enquiry_bucket_does_not_disturb_the_existing_billing_bucket, ::test_sales_enquiry_stays_on_the_transactional_stream, ::test_withFromName_overrides_the_default_sender_name, ::test_withFromName_ignores_empty_values_and_strips_header_injection, SalesOrderApiTest::test_public_sales_order_sends_as_a_sales_enquiry_not_tenant_billing_mail.

  • The platform master's public front page (app.project-nexus.ie) is now actually prerendered for crawlers. The 2026-09-13 planner fix was not enough: PrerenderService::loadTenantTargets() and the render script's own get_tenants() / static page list still excluded tenant id 1, so every scheduled path (auto-recache, drift detection, the job processor) never planned master and a queued master job was claimed but never ran. Master is now a target everywhere, served at the app host root and never at its own tenants.domain (the sales site). Regression tests: PrerenderServiceTest::test_tenant_targets_include_the_platform_master_at_the_app_host_root, scripts/test/test-prerender-tenant-plan.sh.

  • A queued prerender job no longer kills a deploy's full render. prerender-tenants.sh treated every new invocation as a "newer deploy" and cancelled the in-flight run, so the once-a-minute prerender-job-processor.sh terminated each post-deploy render about 50 seconds in — after the master pages had rendered, before they were published. A targeted run (--tenant / --routes) that finds the lock held now exits 75 with a lock_takeover_refused event instead, and the processor skips its tick while the render lock is held. Regression tests: scripts/test/test-prerender-lock-takeover.sh (scenario 6), scripts/test/test-prerender-processor-render-lock.sh; all three prerender harnesses now run in CI's Migration Safety Gate.

  • The deploy's crawler delivery probe now runs after the post-deploy render has published, instead of seconds after the traffic switch. It measured the previous generation of snapshots and reported the master tenant's front page blank on two consecutive deploys where the render then succeeded minutes later. scripts/deploy.sh now starts the 30-minute error watch in the background at the switch (its window is timestamped from when it starts), waits for the server's detached render log to record its end via the new scripts/wait-for-prerender-publish.sh (bounded, never fails the deploy, never signals the server), then probes, then collects the watch result. The waiter is a child process and deploy.sh holds the SSH host and key as plain shell variables, so the invocation passes both explicitly — without that it refused to start and the probe fired early anyway. Regression test: scripts/test/test-deploy-probe-after-prerender.sh, run in CI; it pins the ordering, the waiter's exit codes, and the credential handover itself.


Back to all releases