Skip to main content

Give feedback

Back to all releases
Changelog

1.5.5

Released 2026-07-08

Added

  • Volunteering module: completed the deferred cross-surface features from the module audit. (1) Admins can now open submitted expense receipts through a tenant-scoped, admin-gated download endpoint (previously the link pointed at a private storage path that 404'd). (2) A new Shift Swaps admin screen (Volunteering → Shift Swaps) resolves swap requests that require admin approval — such requests previously sat in admin_pending forever with no UI to approve or reject them. (3) Scanning a shift check-in QR now opens a proper check-in page that verifies the volunteer and offers checkout; the QR is generated on-device (via the qrcode package) instead of being sent to a third-party image service. (4) Approved volunteer hours now appear in the community feed as their own card type (a dedicated volunteer_hours feed source keyed to vol_logs — they were previously written to the feed but silently dropped as orphans). (5) Federation is now two-way for opportunities: inbound opportunities mirrored from partners appear in the opportunities listing, and deleting/closing a shared opportunity retracts it from partners (with matching receiver-side handling that deactivates the mirror). New user-facing and admin strings were added across all supported locales, with backend regression tests for the receipt download, org-wallet money paths, feed listener, and federation retraction.
  • Fresh installs now seed a usable master tenant and first-run platform admin. php artisan migrate --seed now creates tenant 1 as the neutral Master Tenant and a god-mode platform administrator, with local development credentials documented as admin@project-nexus.local / ChangeMe123! and overridable via NEXUS_BOOTSTRAP_ADMIN_EMAIL / NEXUS_BOOTSTRAP_ADMIN_PASSWORD. The default seeder no longer creates the private hOUR Timebank tenant or demo tenant-2 content, E2E fixture defaults use generic project-nexus.local addresses, public API examples now use the seeded master tenant header, and god-mode auth lookup now recognises is_god / role=god during cross-tenant login.
  • Added featured-image upload and preview to the admin blog builder. Content > Blog now lets admins upload a JPEG, PNG, GIF, or WebP featured image through the shared tenant-scoped upload pipeline, stores a portable /storage/... image path in the existing featured_image field, keeps the manual URL fallback, shows an inline preview through the configured API asset host, and provides a one-click remove action. New admin UI strings were added across all supported locales, with focused Vitest coverage for the upload-and-preview path.
  • Added the full GrapesJS webpage builder to admin custom Pages. Content > Pages now has the same four authoring modes as newsletters (Plain text, Rich text, HTML, Design), but Design uses the webpage-focused GrapesJS stack (grapesjs-preset-webpage, blocks-basic, forms, tabs, tooltip, custom-code) instead of the newsletter MJML builder. Pages now store content_format and design_json so the published HTML remains in content while visual designs reopen losslessly; public custom pages preserve builder CSS and render builder pages full-width instead of forcing them into the rich-text card layout. The builder uses the existing HeroUI/Tailwind admin chrome and NEXUS upload/image-library flow, exports script-free HTML, and deliberately avoids Joomla scripts/packages.
  • Hardened the custom Pages GrapesJS builder for production use. Public builder CSS is now parser-scoped to the custom-page container and unsafe/global CSS, app-shell escape declarations, scripts, handlers, and public inline styles are stripped so tenant-authored pages cannot restyle the React shell or attribution chrome. The admin save path now synchronously flushes the current GrapesJS canvas before submitting, so an immediate Save after editing cannot send stale HTML or design_json. The Pages API validates plausible GrapesJS project JSON, rejects invalid, malformed, wrong-shape, or oversized designs with translated errors, clears design_json when leaving Design mode, and has targeted frontend/backend regression coverage for the save, edit, sanitisation, invalid-project fallback, and validation paths.
  • Added Laravel route-file matching and a Laravel-only smoke manifest to the React backend-prep guardrails. The API-call inventory now checks Laravel routes/*.php when openapi.json does not cover a React call, records the matched Laravel method/path/source in the local matrix, and keeps ASP.NET status as not_checked. A new npm --prefix react-frontend run smoke:laravel-manifest command writes a local Laravel-mode smoke checklist under .local-docs-archive/react-laravel-smoke/latest/, and check:dual-backend-prep regenerates it with the other prep artifacts. This is preparation tooling only: it does not add real frontend adapters, does not run ASP.NET, does not certify ASP.NET readiness, and does not change Laravel production runtime behaviour.
  • Added local module certification worksheets for future ASP.NET backend parity. npm --prefix react-frontend run certification:worksheets now turns the Laravel React API-call matrix into local module worksheets under .local-docs-archive/react-api-certification/latest/, and check:dual-backend-prep regenerates them after guardrail and inventory checks. The worksheets group P0/P1/P2 rows by module, show Laravel OpenAPI match status, keep every ASP.NET row as not_checked, and list the proof required before any future compatibility claim; they are local handoff material only and do not change frontend runtime behaviour.
  • Added Laravel React dual-backend preparation guardrails and OpenAPI matching. The local preparation command npm --prefix react-frontend run check:dual-backend-prep now verifies Laravel remains the default backend, blocks obvious ASP.NET-specific conditionals in production page/component files, runs inventory fixture tests, and regenerates the local matrix. The API-call inventory now matches React calls against the Laravel openapi.json where possible and records ASP.NET status as not_checked for every row, preserving the rule that ASP.NET remains development-only until separately audited and smoke-tested.
  • Expanded the Laravel React API-call inventory into a module/prioritised contract matrix. The inventory tool now has a fixture-backed Node test (npm --prefix react-frontend run test:api-inventory) and records module grouping, P0/P1/P2 priority, auth/tenant hints, upload/download markers, upload field names, response type hints, dynamic path markers, raw-fetch markers, and first source locations. The regenerated local matrix still makes no ASP.NET readiness claim; it is a safer work-queue seed for making the development ASP.NET backend conform to the production Laravel React frontend contract later.
  • Added a safe Laravel React API-call inventory tool for future ASP.NET contract work. npm --prefix react-frontend run inventory:api-calls now scans the production Laravel React source without changing runtime behaviour and writes a local JSON/Markdown matrix under .local-docs-archive/react-api-inventory/latest/. The dual-backend documentation and frontend agent guide now clarify that this is preparation only: Laravel remains the production/default backend, ASP.NET is still development-only and not assumed ready, and generated inventory output is input for making ASP.NET match the Laravel React API contract later.
  • Started the safe React dual-backend switch scaffolding with Laravel protected as the default. The production React frontend now has explicit local backend-target scripts (dev:laravel, dev:dotnet, build:laravel, build:dotnet) plus a tiny tested backendTarget config helper that defaults missing or invalid values back to laravel; no existing API calls, auth flow, tenant bootstrap, upload flow, Pusher flow, deployment script, or Laravel production default was changed. Documentation was tightened across the public roadmap and agent/frontend guidance to state the current operating model clearly: the Laravel backend and Laravel React frontend are in production, the ASP.NET backend is development-only, ASP.NET must conform to the Laravel React API contract, and the React frontend should move to its own repository only after backend contract parity is mature enough.
  • Documented the React dual-backend portability guardrails before implementation. Added docs/REACT-DUAL-BACKEND.md and linked it from the public docs index to define the safe path for making the Laravel React frontend switchable between Laravel and ASP.NET while keeping Laravel as the protected default contract. The roadmap requires environment-only backend targeting, ASP.NET conformance to Laravel API contracts wherever possible, tiny adapter boundaries for unavoidable differences such as realtime transport, and Laravel-mode verification before any future ASP.NET portability change is accepted. A local implementation plan was also recorded under .local-docs-archive/plans/ for agent handoff; no runtime code, backend routes, frontend behaviour, or deployment configuration changed.
  • The newsletter builder now has an image library — browse and reuse images you've already uploaded instead of re-uploading each time. A new tenant-scoped GET /v2/upload/list endpoint (UploadController::indexImageUploadService::listImages) returns the tenant's previously-uploaded images (newest first, images only), and a new Image library toolbar button opens a HeroUI gallery modal (AssetLibraryModal) to browse them or upload a new one. Picking an image applies it to the current target — hero background / image src / a fresh mj-image — through the same absolute, email-safe URL pipeline. New newsletter_builder.library_* strings across all 11 locales (drift 0/0). Regression coverage: UploadControllerTest asserts GET /v2/upload/list is registered + auth-gated and returns the tenant's images newest-first while excluding non-images.
  • Third-party licence attribution + a dependency-licence audit gate (open-source compliance & sellability groundwork). Added THIRD_PARTY_NOTICES.md (curated attribution for the bundled open-source components — GrapesJS and MJML for the newsletter builder, plus React, HeroUI, Tailwind CSS, Laravel, etc.), a generated THIRD_PARTY_LICENSES.md full production inventory, and scripts/check-licenses.mjs (npm run check:licenses) which audits the npm + Composer production trees and fails if a new strong-copyleft (GPL/AGPL/SSPL) dependency is introduced. NOTICE and README now point to the third-party notices. The audit confirmed both trees are permissive (npm ~362 pkgs all MIT/Apache/ISC/BSD/0BSD; Composer 130 pkgs MIT-dominant) with documented licence elections (nette/*→BSD-3, james-heinrich/getid3→MPL-2.0, DOMPurify→Apache-2.0) and flagged the single transitive strong-copyleft dependency — joomla/string (GPL-2.0-or-later, pulled only by wamania/php-stemmer) — as a tracked KNOWN_EXCEPTIONS entry to remove before any fully-proprietary distribution. Purely repo/compliance tooling — no runtime or user-facing behaviour change.
  • A platform super admin in god mode can now permanently delete a tenant — safely, and for the first time. The super-admin panel already had a "Delete" action, but it was misleading and inert: it called deleteTenant(id) with no hard-delete flag, so it merely deactivated the tenant (identical to the adjacent "Deactivate" item); the backend's real hard-delete was never reached from the UI, was neutralised by a frontend/backend query-param mismatch (?hard=1 vs hard_delete), and even if invoked only cleared 3 of ~600 tenant-scoped tables, orphaning everything else and leaving Stripe still billing a deleted customer. Tenant deletion is now a deliberate two-stage lifecycle: Deactivate (is_active = 0) stays the default, reversible action, and a new god-only "Permanently delete" button (in the tenant detail Danger Zone, hidden from non-god users, disabled until the tenant is deactivated) runs a complete, audited purge. The engine (TenantPurgeService, the inverse of TenantProvisioningService) discovers every base table with a tenant_id column from INFORMATION_SCHEMA (so it can't rot as tables are added) and deletes the tenant's rows in bounded chunks; it deletes the tenant's own members while reassigning platform super-admins to the parent community (never deleting them); and it cleans up external state best-effort — cancels Stripe subscriptions + detaches the customer (StripeSubscriptionService::cancelAllForCustomer), removes the tenant's documents from the shared Meilisearch indices by tenant_id filter (SearchService::purgeTenant — never dropping a shared index), clears Redis tenant keys (RedisCache::clearTenant), and deletes the on-disk httpdocs/uploads/tenants/{slug}/ directory. Custom domain / Plesk vhost / DNS teardown remains a surfaced manual follow-up (reported in the UI and command output). Hard guards make an accidental purge impossible: never the Master tenant (id 1), only a deactivated tenant, only one with no child tenants, and the UI requires typing the tenant slug to confirm against a live dry-run preview of exactly what will be removed (row counts, member count, manual follow-ups). The real work runs as a queued job (PurgeTenantJob, $tries = 1) so a multi-million-row purge can't time out the request, and everything is audited (tenant_purged). The same engine is exposed as php artisan tenant:purge {id} [--dry-run] [--force]--dry-run doubles as a "how much data does this tenant hold" report. New endpoints: GET /v2/admin/super/tenants/{id}/purge-preview and POST /v2/admin/super/tenants/{id}/purge (both requireGod); the old "Delete" list item was removed (it duplicated Deactivate) and the ?hard=1 param path deleted. New api.super_purge_* strings across all 11 locales; new admin.super.purge_* UI strings, translated across all 11 locales (drift 0/0). Regression coverage: TenantPurgeTest (purge removes the tenant + its data + members, preserves platform super-admins, refuses Master / active / has-children, dry-run counts without deleting) plus updated adminApi tests for the new purge endpoints.
  • The newsletter module now sends genuinely designed emails — a MailChimp-grade authoring upgrade, delivered in two phases. Previously the only editor was a Lexical rich-text box whose DOM round-trip silently dropped or escaped any HTML it didn't understand (tables, inline styles, <style>, MSO comments, buttons), so a pasted designed email arrived in inboxes as literal escaped source. The content field is now a multi-mode editor (NewsletterContentEditor, shared by the newsletter and template forms) with a segmented switcher — Plain text · Rich text · HTML source · Design — a contextual hint per mode, and a collapsible live desktop/mobile preview rendered through the real send pipeline (new POST /v2/admin/newsletters/preview), shown inside a device frame (browser chrome / phone shell) so it reads as a real email, always in a sandboxed script-free iframe with a DOMPurify pass and an approximate client-only fallback if the endpoint is down. HTML mode is a lazy-loaded CodeMirror 6 editor that stores markup verbatim (structurally impossible to mangle) with an Insert-image button that uploads to our own domain (POST /v2/upload). Design mode is a full GrapesJS + MJML drag-and-drop visual builder (NewsletterBuilder) whose blocks are MJML components — so the exported HTML is inbox-safe/table-based by construction; the editable design is serialized to a new design_json column (reopens losslessly) while the compiled HTML is what gets sent, and its asset manager reuses the same image host. Switching from a designed format back to Rich text (which Lexical would strip) is gated by a confirm dialog. Authors can start from a gallery of six professionally-designed, email-safe starter templates (Announcement, Community Digest, Event Invite, Welcome, Re-engagement, Simple Letter) picked from a thumbnailed modal with live mini-previews, seeded per-tenant (idempotent migration). Backend rendering (NewsletterService::renderEmail()) became a format dispatcher on a new content_format column (plaintext|richtext|html|builder, default richtext so every existing newsletter renders exactly as before): richtext keeps the branded shell; html/builder are injected verbatim (a complete <html> document isn't re-wrapped, a fragment gets a minimal skeleton) with a compliance backstop that injects an unsubscribe footer + List-Unsubscribe when the author didn't include one; plaintext is escaped and also sent as a real text/plain alternative part (proper html→text via soundasleep/html2text, threaded through the Postmark, Gmail and SMTP transports — the SMTP path was HTML-only and now sends multipart/alternative). Every format runs through a CSS inliner (pelago/emogrifier, fail-open) so styles survive Outlook/Gmail, with @media/dark-mode preserved, and admin-authored HTML is cleaned by a new email-safe sanitizer (EmailHtmlSanitizer — strips <script>/<iframe>/<form>/on*/javascript: while preserving tables, inline styles, <style> and MSO conditionals; the generic HtmlSanitizer would have destroyed the email markup), gated on format with a 512 KB cap. New merge token {{unsubscribe_url}} (bare URL for href="") complements the existing {{unsubscribe_link}} (full <a>). New deps: @uiw/react-codemirror, @codemirror/lang-html, grapesjs, grapesjs-mjml (all lazy-loaded so they cost nothing until their mode is opened), plus Composer pelago/emogrifier + soundasleep/html2text. Every new UI string is translated across all 11 locales (the ~45-key newsletter_content_editor block); new api.* keys for the format/size validation across all 11. Regression coverage: NewsletterRenderFormatsTest, EmailHtmlSanitizerTest, EmailCssInlinerTest (PHP) + preview/content_format cases in the controller feature test; contentFormat, NewsletterContentEditor, TemplateGalleryModal, NewsletterBuilder (vitest), with the newsletter/template form suites updated for the new editor.

Security

  • Volunteering module: closed within-tenant authorization and safeguarding gaps. Recurring shift pattern endpoints (create/update/deactivate/delete future shifts) now require the caller to manage the owning opportunity via VolunteerService::userCanManageOpportunityById() — previously any authenticated tenant member could alter patterns and delete another opportunity's future shifts. Shift swaps now reject swapping across different opportunities (which bypassed per-opportunity application approval). Group-reservation addMember now validates the added user belongs to the tenant and tenant-scopes the roster join (previously accepted arbitrary user ids and could leak another tenant's names/avatars). Guardian-consent requests no longer return the consent token in the API response — it is delivered only to the guardian by email, so a minor can no longer self-approve their own safeguarding gate.

Fixed

  • Login and registration no longer download the full application route registry or app-only runtime providers. Auth-entry routes now load through a small AuthRoutes chunk, while the full AppRoutes registry and realtime/menu/presence/podcast provider stack are imported only after navigation into the non-auth application shell. Startup SEO metadata now imports TenantContext directly so the @/contexts barrel cannot pull realtime/presence/podcast code back into the entry chunk, and AuthContext now lazy-loads the WebAuthn helper only when a passkey login action actually runs. The bundle-budget gate now blocks the auth pages from drifting back into the full route registry, blocks TenantShell from statically importing app-only provider code, and blocks these startup leaks from returning.

  • Feed reaction popups now load only on interaction. The pure reaction emoji/label config now lives outside ReactionPicker, and the hover/long-press reaction menu, tooltip, and motion code lazy-load only when the picker opens, keeping ordinary feed-card rendering focused on visible core content.

  • Feed profile hover cards now load only after hover intent. Feed cards now render a tiny deferred wrapper around author links and lazy-load the full profile popover, connection actions, presence indicator, and hover-card API path only when a desktop user actually hovers an author. The first hover still opens the popover after the lazy chunk arrives, and touch devices keep the plain tap-to-profile behaviour.

  • The main Tailwind build no longer scans test-only React files. The production app stylesheet now explicitly excludes src/test and every __tests__ folder from Tailwind's source scan, with a bundle-budget guard so test fixtures cannot leak one-off utility classes into the shipped CSS.

  • Early error telemetry no longer pulls Sentry into startup. Production logger errors and i18n missing-key reports now route through the consent-gated post-paint telemetry queue instead of dynamically importing the Sentry wrapper immediately, and the bundle-budget guard blocks those startup files from reintroducing direct Sentry imports.

  • Core timebanking, volunteering, and organisation audit fixes. Volunteer user-review eligibility now constrains both application aliases to the active tenant, admin volunteer-hours audit responses include vol_org_transactions payment reconciliation again, organisation wallet payouts follow the canonical auto-credit reconciliation model even when the org balance goes negative, accessible organisation wallet/dashboard pages no longer expose the stale auto-pay toggle, wallet transfer errors/system counterparty labels now use API translation keys across all supported locales, and the React matches/reviews routes are feature-gated. Focused backend, accessible frontend, schema-contract, and React route-gate regression tests were added.

  • Admin legal document versions: restored the "Create New Version" button label. The versions page now looks up the button text through the existing legal_versions.create_new_version key in the split admin_enterprise namespace, so users no longer see enterprise.create_new_version.

  • Partner Time Banks federation production hardening. External partner time-credit transfers now use client/content idempotency before debit, carry partner idempotency evidence into the canonical transaction ledger, preserve opaque remote member IDs, surface pending reconciliation states safely, and include richer stale-transfer reconciliation audit context. Federation read/settings endpoints and direct external member/listing filters now honour operation gates and partner allow flags; internal cross-tenant message writes are atomic; Partner API user/listing exports follow federation consent and visibility; public volunteering/organisation aggregates apply small-N suppression; external partner admin defaults fail closed; and related backend/admin errors now use translations. External partner API response logging now handles list/numeric payloads without dropping successful member/listing results, federation member/listing/event React controls now use HeroUI Switch/semantic Button patterns instead of clickable chips/gradient actions, and mixed internal/external member/listing responses now expose explicit source counts plus internal cursor/load-more scope so first-page external enrichment cannot be mistaken for fully paginated partner data. Regression coverage was added for external transfer replay, allow-flag enforcement, Partner API privacy, aggregate suppression, translation context partitioning, external source metadata contracts, pagination-scope contracts, and React transfer/admin flows.

  • Volunteering credit conservation: auto-approved hours are always paid. The auto-pay path now debits the organisation wallet unconditionally (allowing a negative balance, as a reconciliation figure) and mints whole credits to the volunteer, mirroring the manual verify path. Previously, when auto-pay was on and the org wallet was short, the hours were committed approved but paid nothing and could never be paid later — silently stranding the volunteer's credits.

  • Volunteering gamification and rewards now fire on every approval path. Volunteer badges and personal-dashboard hour totals now query vol_logs on the real approved status (they filtered on a non-existent verified status, so badges were never awarded and dashboards always read 0 hours). Admin-panel hours approval and the auto-approve path now dispatch VolLogStatusChanged, so XP, badges, and feed activity are awarded consistently, not only on member self-verify.

  • Volunteering admin screens no longer crash or render blank on real data. Admin expenses and hours-audit tables now coerce MySQL DECIMAL columns (which serialize as JSON strings) to numbers before formatting/summing, fixing .toFixed crashes and string-concatenated totals. The training admin page now reads the actual API field names (user_name, completed_at, expires_at, certificate_reference) so volunteer name, dates, and certificate render, and the backend now returns real status-count stats across all records under pagination.

  • Volunteering correctness and i18n fixes. Editing an opportunity no longer wipes its latitude/longitude (the controller now forwards them). myShifts/myHours now forward cursor/per_page so entries beyond the first page are reachable. Waitlist spot-offer notifications are now delivered instantly by email and push (previously they reached nobody). The new-opportunity admin email now reads the poster from created_by. RecurringShiftService now returns translated, correctly-shaped error envelopes instead of hardcoded English strings; the hours-paid email no longer duplicates "time credits"; the applications empty-state now shows a translated status label instead of the raw enum. Accessible-frontend create-opportunity now sends valid federated_visibility values (listed/none), and the organisation "View job openings" link is gated behind the job_vacancies feature.

  • Pre-render volunteering and organisations freshness now follows the live React/API contract. Tenant prerender and sitemap planning now treat /organisations as part of the volunteering feature, source organisation URLs from vol_organizations, include active/open volunteer opportunities only when their organisation is approved/active, clear versioned tenant sitemap caches on content changes, purge stale snapshot sidecars for deleted/hidden dynamic routes, enqueue recache jobs for valid routes even when no prior snapshot exists, and restrict prerender admin read/monitoring endpoints to platform super-admins. Regression coverage pins sitemap generation, route ownership, stale inventory detection, queue reset, sidecar cleanup, and platform-only admin access.

  • Location and map route chunks now avoid the shared location barrel. Map, autocomplete, listing-detail, profile, settings, events, groups, members, volunteering, marketplace, compose, and admin analytics callers now import only the direct location component they render, and the bundle-budget gate blocks non-test source from reintroducing @/components/location barrel imports that could couple Google Maps/location exports into unrelated page chunks.

  • Register no longer loads Google Maps on first paint. The register page now renders a plain location input initially and lazy-loads the richer place autocomplete only after the user interacts with that field, keeping the initial auth/register network trace at zero Maps requests while preserving autocomplete when it is actually needed.

  • The newsletter design studio now lazy-loads the GrapesJS builder. The full-screen design route renders its chrome and loads newsletter metadata before fetching the heavy MJML/GrapesJS builder chunk, uses direct UI primitive imports, and the bundle-budget gate blocks the static builder import from returning.

  • Newsletter admin routes now use a split translation namespace. The newsletter dashboard, campaigns, subscribers, segments, templates, diagnostics, send-time tools, preview panes, and newsletter-specific editors now load the smaller admin_newsletters locale file instead of fetching the full admin translation monolith, with bundle-budget guardrails preventing those surfaces from drifting back to admin.json.

  • Advanced admin routes now use a split translation namespace. SEO, redirects, 404 tracking, AI/email/feed algorithm settings, match diagnostics, email deliverability, and prerender tooling now load the smaller admin_advanced namespace instead of the full admin translation monolith, with bundle-budget guardrails checking both the source usage and per-locale namespace files.

  • Configuration and system admin routes now use split translation namespaces. Module configuration, translation/infrastructure settings, activity logs, cron tooling, onboarding/image/native-app/registration/SSO/retention/verification/WebP system screens now load admin_config or admin_system instead of the full admin translation monolith, and the bundle-budget gate verifies those namespaces across all shipped locales.

  • Federation and enterprise admin routes now use split translation namespaces. Federation activity, API keys/docs, agreements, partners, analytics, settings, webhooks, GDPR/compliance, roles, logs, legal docs, system monitoring, and enterprise health screens now load admin_federation or admin_enterprise instead of the full admin translation monolith, with bundle-budget guardrails covering both modules.

  • Super-admin and volunteering admin routes now use split translation namespaces. Super admin tenants/users/audit/federation/pilot-inquiry screens and volunteering approvals, config, consents, expenses, giving days, hours audit, organizations, projects, safeguarding, training, and refund screens now load admin_super or admin_volunteering instead of the full admin translation monolith, with bundle-budget guardrails covering both modules.

  • Split admin locale groups are no longer duplicated in the monolithic admin namespace. The migrated newsletter, advanced, configuration, system, federation, enterprise, super-admin, and volunteering translation groups were pruned from every locale's admin.json, cutting the English admin monolith by roughly 395 KB and adding a bundle-budget guard so split groups stay out of the fallback admin payload.

  • More admin modules now avoid the monolithic admin locale payload. Timebanking, groups, gamification, reports, billing, analytics, matching, moderation, CRM, AI, marketplace, content, resources, blog, jobs, users, listings, caring-community, advertising, agents, API partners, categories, community, deliverability, diagnostics, events, goals, help, ideation, impact, legal, national, performance, podcasts, polls, premium, provisioning, regional analytics, safeguarding, support, and shared editor/legal admin screens now use module-level admin namespaces, with their migrated groups pruned from admin.json; the English monolithic admin locale is now roughly 2 KB instead of the original roughly 903 KB.

  • Global locale startup no longer preloads broker/admin panel payloads. The 77 KB broker namespace was removed from the universal i18n startup list, while the broker and partner panel shells now load only their own small shell namespace instead of prefetching the 903 KB admin namespace before a specific embedded admin page needs it. The bundle-budget gate now blocks admin/broker namespaces from returning to global startup and blocks broker/partner shell preloads of the admin monolith.

  • PWA install no longer precaches heavyweight lazy chunks. The generated service worker now excludes newsletter builder, asset-library, HTML editor, map, PDF/chart, markdown/editor, contextual-help, and large PNG fallback bundles from the precache, cutting the generated precache from roughly 18.6 MB to roughly 10.0 MB while keeping those hashed assets browser-cached on demand. The bundle-budget gate now enforces a 12 MiB service-worker precache ceiling so route-only bundles cannot quietly return to first-visit install cost.

  • Frontend CI installs recover after the startup-budget dependency update. The React lockfile now records the rxjs dependency shape expected by npm 10 npm ci, including HeroUI CLI's optional peer entry, the Lighthouse workflow runs on Node 22 to match the frontend engine requirement, and the React CI job has a bounded timeout so a stuck worker cannot leave main pending indefinitely.

  • Platform audit — Priority 1 fixes for feed pagination, notification locale, and hot-path performance. The feed's "load more" no longer sticks in a permanent loading state when a fresh load (new-posts banner, filter/mode switch, compose success, or pull-to-refresh) interrupts an in-flight append: the loading flag now resets by abort-controller identity rather than abortedness, so infinite scroll, the load-more button, and pagination skeletons recover instead of freezing until remount. Admin-initiated member notifications (credit grants, balance adjustments, event approvals, group approve/reject/remove, report resolve/dismiss, review hide/remove, deliverable assign/reassign — bell + device push) now render in the recipient's preferred_language via LocaleContext::withLocale, and the credit-grant/balance-adjust hour unit is pluralised through trans_choice (new wallet_admin.hours_unit key across all locales) instead of a hardcoded English ternary. Personalised marketplace browse no longer rebuilds the collaborative-filtering interaction matrix once per saved listing (now memoised per request and seeded from the 25 most-recent saves) and batches per-owner reciprocity lookups into one query, and the notification bell dropdown hydrates all grouped actors in a single users query instead of one query per collapsed group. Root Cause: the feed's finally guard skipped the reset for the deliberately-aborted append; admin notification blocks rendered __() under the acting admin's request locale; and the ranking/notification-grouping loops issued per-iteration queries. Prevention: loading flags reset by controller identity, all admin member-notification sends are wrapped in LocaleContext, and the batched-query paths are covered by the existing ListingRanking/CollaborativeFiltering and notification-grouping tests.

  • Saved-collection detail and the message composer handle failure and non-mouse input correctly. CollectionDetailPage now shows an error state with a retry action instead of rendering a blank page when the collection fails to load, and the message-composer attachment-remove button is now always visible (previously hover-only, invisible on touch) with a larger 24px target so it works for touch and keyboard users, not just mouse hover. Root Cause: the collection loader used try/finally with no catch, leaving data null so neither the loading, empty, nor content branch rendered; the remove button relied on group-hover opacity and a 16px hit area. Prevention: the loader now catches failures into an explicit error state, and the control is always-visible with an enlarged target.

  • Federation protocol privacy and ledger follow-up. Credit Commons completion now uses one balance/hash/ledger path for both POST commits and PATCH-to-completed transitions, records the canonical transactions ledger row exactly once, and respects settings-backed federation consent for account discovery, debits, credits, relay credits, and local payees. Komunitin account discovery/detail and transfer settlement now use the same federation_user_settings gates instead of legacy users.federation_optin, V1 member/listing federation APIs now honour profile/search/item visibility, and newly-created/imported users now seed as local-only until they explicitly opt into Federation. The accessible Federation onboarding parity test now exercises CSRF correctly, and the React Federation hub/connections polish pass removes the heavy gradient treatment from primary Federation navigation/action surfaces.

  • Federation internal module audit fixes. Internal Federation now consistently honours member profile visibility and item-level federated visibility across React API and accessible frontend browse/detail paths, clamps member travel radius, clears notification/review sharing flags on opt-out, stores message and transaction text unescaped, and localises recipient notifications plus backend activity/audit labels with the recipient language. Admin Federation settings now write the real tenant feature gates instead of legacy config-only values; partnership rows, analytics, balances, and exports count same-platform messages/transactions from the canonical internal ledgers without double-counting sender/receiver copies. Data Management exports the tenant-scoped internal Federation records as well as partnerships/external partners, clearly labels the limited import scope, and the missing admin locale keys for that page are now present across supported locales. Regression coverage was added for private profiles, listing/event visibility, message-copy visibility, and admin partnership statistics.

  • Federation follow-up audit fixes tightened setup access and member privacy. Super-admin-only Federation setup endpoints now enforce that boundary server-side for API keys, data import/export/purge, aggregate consent secrets, webhooks, external protocols, Credit Commons config, and settings mutation; member activity feeds are opt-in gated and scoped to the current member; partner listing stats count only visible federated listings from opted-in owners; group browsing respects the group owner's Federation privacy; accessible Federation transfers are idempotent and count received transactions correctly; quick opt-in keeps location sharing off by default; and failed optimistic message read updates roll back in React.

  • Marketplace checkout now completes the audited buyer paths instead of dropping critical order details. Accepted offers now expose a buyer payment action, listing checkout requires an explicit shipping or pickup choice when fulfilment options exist, selected shipping costs are sent to the order API and reflected in the Stripe total, coupon discounts update the displayed checkout total, pickup reservation failures stop payment-intent creation, and sellers now have a discoverable shipping-options management route from marketplace navigation. The order API validates shipping_cost, and SQL marketplace pagination now uses sort-aware cursors for price/popular ordering so equal-price listings are not skipped or duplicated across pages. Regression coverage pins accepted-offer checkout, shipping totals, coupon totals, pickup reservation failures, shipping manager discovery, shipping-cost order payloads, and price cursor pagination.

  • Marketplace checkout and seller onboarding now use registered, tested upload/shipping contracts. Buyer shipping selection can fetch a seller's active shipping options through the public /v2/marketplace/sellers/{id}/shipping-options route, merchant onboarding avatar/cover uploads now go through a dedicated /v2/merchant-onboarding/image endpoint instead of the text-only seller profile update path, and the marketplace form/filter UI no longer emits the audited React/a11y warnings. Regression coverage pins the buyer shipping route, onboarding image upload, and React upload endpoint wiring.

  • Production login and registration no longer do heavy unauthenticated startup work. React i18n now preloads only the startup namespaces needed by the shell/login flow and lets route components lazy-load their own translation files, removing the large all-namespace locale waterfall from unauthenticated page loads. The register route is lazy-loaded out of the main app bundle, Google Places setup is deferred until a Google-backed location field is focused or edited, and auth pages skip public menu fetching because they do not render the navbar/footer.

  • Image-heavy listing, marketplace, and branding surfaces now request cached thumbnails instead of full-size local uploads. A public Laravel thumbnail endpoint safely resizes same-origin /uploads and /storage media on demand with long-lived cache headers, listing cards/detail heroes, feed media, message context cards, group media grids, marketplace galleries, order dashboards, collections, map-search cards, profile listing cards, search and federation listing/event cards, story highlight previews, podcast mini-player artwork, author avatars, tenant/partner logos, course/ideation/caring covers, and shared avatar images all request bounded derivatives for local uploads. Built-in footer/partner-page artwork now ships WebP alternatives instead of forcing multi-megabyte PNGs on modern browsers. Shared avatars default to lazy/async loading, realtime startup code is deferred until it is actually needed, Sentry now loads through the consent-gated wrapper instead of a startup SDK import, and marketplace browse/search now hydrates only the primary card image plus an image count instead of eager-loading multiple images per listing.

  • Marketplace avatars now use the cached thumbnail pipeline by default. Local uploaded avatar URLs now resolve through /api/v2/media/thumbnail at a bounded 96x96 size, while external/federated avatar URLs remain untouched. Marketplace detail, buyer orders, seller orders, and seller profile pages now use the shared avatar resolver instead of direct raw avatar_url image sources, closing the live trace class where listing pages could download multi-megabyte avatar originals.

  • Partner-page artwork now avoids heavyweight original image fallbacks. The public partner page now points its banner, NEXUS logo, and West Cork partner artwork at the existing WebP assets even on the fallback img src, removing ambiguity around large PNG/JPG originals. The unused PNG/JPG originals for partner banners, powered-by badges, and the NEXUS logo were removed from public/images, and the bundle-budget guard now fails if those heavyweight originals or references quietly return.

  • Telemetry context updates no longer pull Sentry into returning-user startup. User, tenant, and breadcrumb context set during auth/tenant bootstrap is now queued inside the lightweight telemetry wrapper and flushed only after the post-paint/idle Sentry initialization has completed, so analytics-consented sessions do not load the Sentry SDK just because bootstrap finished before the idle window.

  • Routine startup telemetry no longer requests the Sentry wrapper chunk before idle. Auth user context, tenant context, API timing breadcrumbs, API stale-client messages, and service-worker reload breadcrumbs now pass through a tiny consent-gated telemetry queue that waits until after first paint/idle before importing the local Sentry facade. Crash boundaries, support reports, and missing-translation/error reports still load telemetry on demand, but ordinary login/register/bootstrap traffic no longer spends an early network request on the Sentry wrapper even for returning analytics-consented users. The bundle-budget guard now blocks AuthContext, TenantContext, and the API client from reintroducing direct Sentry-wrapper imports on those startup paths.

  • The production service worker no longer precaches every route bundle on install. The PWA precache now contains only the push handler, manifest, favicon/Open Graph image, and app icons instead of hundreds of JS/CSS route chunks, preventing a first-install background download of the full application. The bundle-budget guard now caps generated precache payloads at 256 KiB so route-only code stays demand-loaded.

  • Auth startup now preloads fewer translation namespaces. The global i18n bootstrap no longer fetches legal, public, or settings locale files on every app load; those route namespaces lazy-load when their pages or modals mount, while login/register keep only the auth-shell namespaces needed for first render.

  • Repeat visits now keep build-scoped translation caches warm for longer. Production i18n localStorage entries and the service-worker locale runtime cache now live for 30 days instead of expiring after an hour/day; the cache keys and URLs remain build-versioned, and stale build prefixes are purged, so new deployments still invalidate translations while repeat visits avoid unnecessary locale JSON refetches.

  • Optional analytics no longer load on auth-entry pages. Returning users who previously granted analytics consent no longer fetch Sentry or Ahrefs while sitting on login, register, password reset, email verification, identity verification, or OAuth callback routes; the consent provider starts optional telemetry only after navigation into a non-auth app route, while crash capture still lazy-loads the lightweight Sentry wrapper only if an actual root error occurs.

  • Consented Sentry startup waits until after first paint/idle. Non-authenticated route visits that already have analytics consent no longer fetch even the local Sentry facade chunk during the initial render; the consent provider now schedules that import after the post-paint idle window, while auth-entry routes still skip optional analytics entirely and crash capture remains lazy/on-demand.

  • Prerendered public pages no longer preload route-only app chunks. The static prerender pipelines now strip non-core Vite modulepreload hints from generated public HTML snapshots, preserving only vendor-react and vendor-i18n; public pages still hydrate through the normal import graph, but first paint no longer has to compete with eager vendor-heroui, route, map, chart, or editor preload fetches. The bundle-budget gate now scans every generated HTML file so heavy prerender preloads cannot silently return.

  • Authenticated layout chrome now lazy-loads rare global surfaces. The shared app layout now loads the podcast mini-player, caring-community emergency banner, Swiss FADP consent banner, and floating support reporter only behind their existing feature/auth gates, keeping ordinary app routes from bundling those conditional controls into the common layout chunk.

  • The cookie-consent banner no longer competes with first auth paint. The consent provider still mounts immediately and optional telemetry remains blocked until consent, but the visible banner chunk now waits until after the initial render/idle window before loading its modal, disclosure, switch, and icon dependencies. Local production-preview traces show login/register first-second startup at 15 total requests, 6 JS requests, zero Maps, zero Stripe, zero Sentry, zero admin locale requests, and zero cookie-banner chunk requests.

  • Authenticated cookie-consent restore no longer competes with first paint. The provider still saves explicit consent changes to the server immediately, but the cross-device GET /cookie-consent restore for already-authenticated users now waits until after the first render/idle window. A focused regression test verifies the restore request is not issued on mount and only runs after the deferred idle window, keeping login/register and first app-route rendering free of this non-critical API request.

  • Production service-worker registration no longer competes with auth startup. The controllerchange safety listener still attaches immediately so already-controlled tabs can reload safely after an update, but the actual /sw.js registration and periodic update polling now wait until after first paint/idle. The bundle-budget guard now fails if main.tsx regresses to immediate service-worker registration, preventing the PWA update check from adding another early request during login/register/bootstrap.

  • Map and Google Places code now load only when needed. Shared location cards, marketplace listing detail maps, marketplace map search, generic entity map views, and admin community geography analytics lazy-load the Google/OpenStreetMap implementation only after maps are enabled and coordinates are present, so profile, event, group, listing/detail, marketplace, and admin analytics routes no longer fetch map-provider code just because they include a possible map branch. The Google Places autocomplete branch also moved behind a focused/edited-field lazy import, keeping create/register/detail workflows from loading @vis.gl/react-google-maps before the user actually uses Google-backed location search. The bundle-budget guard now covers the location component folder and blocks LocationMapCard, PlaceAutocompleteInput, or direct production LocationMap imports from reintroducing static map/provider imports.

  • Authenticated layout chrome now avoids the shared UI barrel. Navbar, mobile drawer/tab bar, notification flyout, quick-create, theme picker, footer, tenant logo, and shared navigation menu items now import only the UI primitives they render instead of the full @/components/ui barrel. The bundle-budget guard now covers layout, branding, and navigation runtime folders so common app chrome cannot accidentally pull unrelated UI modules into repeated route chunks.

  • The React route registry no longer rides in the first app chunk. TenantShell now loads the tenant route registry from a dedicated route chunk, preserving slug-stripped tenant routing while moving the hundreds of route dynamic-import references out of App.tsx. The bundle-budget guard now blocks the route registry from drifting back into the startup file.

  • Route-island Tailwind utilities now load with their lazy apps. The main stylesheet no longer scans src/admin, src/broker, src/caring, src/partners, or src/super-admin; each lazy app entry imports its own focused Tailwind sheet instead, moving admin, broker, caring-community, partner-timebank, and super-admin utility CSS out of login/register/member-facing startup. The bundle-budget guard now verifies that the main Tailwind source excludes those route islands and that each lazy app keeps its CSS split wired in.

  • Shared HeroUI wrappers now avoid the package-root barrel. The local UI compatibility layer imports HeroUI primitives from focused @heroui/react/* subpaths instead of the root export wherever the installed package exposes one, leaving only the package-root-only disclosure hook on the root import. The bundle-budget guard now blocks root HeroUI imports from returning to production source, and wrapper tests mock the same subpath modules used by runtime code.

  • Auth pages no longer download the all-HeroUI vendor chunk. Vite now keeps React Aria/Stately/Types/Internationalized internals in a focused vendor-react-aria chunk while letting @heroui component packages split with the route/UI surfaces that use them. The built login static JS graph dropped from about 474 KiB gzip to 369 KiB gzip, and register from about 481 KiB gzip to 377 KiB gzip. The bundle-budget gate now computes the built LoginPage/RegisterPage static JS graphs and fails above 420 KiB gzip so the old all-HeroUI chunk cannot return.

  • The compose modal now loads only the active creation workflow. The shared compose shell no longer imports post, listing, poll, event, and goal tab implementations synchronously; each tab is now its own lazy boundary, so opening one compose workflow does not also download every other compose form. The bundle-budget guard now blocks both the shared UI barrel and synchronous tab imports from returning to ComposeHub.

  • Feed visits no longer download composer and desktop-sidebar code up front. The feed page now lazy-loads ComposeHub only when creating/editing content and lazy-loads the desktop sidebar behind the existing desktop media-query gate, so mobile and read-only feed visits stay focused on the actual feed list instead of fetching composer forms and sidebar widgets. The bundle-budget guard now blocks those static imports from returning to FeedPage.

  • Feed cards defer comment threads and analytics until opened. FeedCard now lazy-loads the threaded comments UI only when a card's comments are expanded and lazy-loads the post analytics modal only when the analytics action is selected, keeping ordinary feed scrolling focused on visible card content. Bundle-budget checks prevent those optional surfaces from returning to the first feed-card chunk.

  • Feed reaction summaries no longer preload the reactor-details modal. The visible reaction count row now keeps only the lightweight summary in the feed card and lazy-loads the tabbed reactor-details modal, user lookup, avatars, and loading state only after a user opens reaction details. Bundle-budget checks prevent the modal/API path from returning to ReactionSummary.

  • Feed bookmark controls no longer preload collection-management UI. The ordinary save/unsave button now keeps the long-press collection picker in its own lazy chunk and the feed social controls use focused UI primitive imports, so scrolling the feed does not download bookmark collection forms before the user asks for them. Bundle-budget checks prevent the picker or broad UI barrel from returning to the feed-card path.

  • Toast rendering no longer rides the global startup chunk. The toast context now keeps only the lightweight state/actions on the app shell and lazy-loads the animated live-region viewport, motion shim, and toast icons only when a toast is actually shown.

  • Production performance hardening now covers marketplace checkout, donation checkout, maps, editor CSS, contact security checks, and more image-heavy pages. Marketplace, donation, Verein dues, and optional identity-verification payment flows lazy-load Stripe only after a payment intent exists, the marketplace barrel no longer re-exports Stripe or map modules into ordinary browse/detail chunks, GrapesJS builder CSS moved out of the public app shell and into the lazy admin builder chunks, Contact now defers Cloudflare Turnstile until the form is used instead of loading the challenge script on first paint, Apache no longer overwrites the media-thumbnail endpoint's immutable cache headers, shared image uploads now create a persistent 640px WebP/JPEG derivative, and blog/event/group/explore/marketplace detail surfaces request bounded thumbnails instead of raw local uploads.

  • Hot-path database indexes now cover the remaining audited listing, marketplace, inbox, and notification scans. A guarded Laravel migration adds additive composite indexes for public marketplace browsing by newest, price, popularity, and promoted ordering; message inbox sender/receiver latest-message scans plus unread sender counts; notification cursor lists with unread/type filters; and public listings pagination with moderation-aware filtering. The message inbox query now scans sent and received messages as separate indexed branches before grouping conversations, avoiding the old OR/CASE access path that could collapse into a broad tenant scan. Public event lists now apply the same active/null status visibility rule as nearby events, keeping draft/cancelled/completed rows off the hot public list path and aligning the query with the existing tenant/status/start-time index. A new read-only php artisan performance:audit-hot-paths --strict command runs representative EXPLAIN checks for feed, listings, marketplace, SQL search fallback, messages, events, and notifications so the production DB pass can be repeated after deploy without ad hoc SQL. The migration checks tables, columns, and existing index names before altering schema so older or partially migrated environments can run it safely.

  • Search and member-directory hot paths now have deeper DB performance coverage. The read-only hot-path audit now checks unified search fallbacks for users/events/groups, autocomplete suggestions, trending terms, saved-search lists, and the member-directory fallback instead of treating "search" as listings-only. SQL event search and suggestions now apply the same active/null status visibility rule as public event browsing, and a guarded follow-up migration adds indexes for event search by tenant/start time, saved-search listing by user/created date, and privacy-aware member-directory scans. The audit now distinguishes tiny local table scans with viable candidate indexes from true larger-table scan risks, so local Docker evidence stays readable while production EXPLAIN runs can still flag load-bearing regressions.

  • Auth startup, public routes, and admin navigation now stay leaner under the performance budget. Returning analytics-consented users no longer start the Sentry SDK before first paint; the app shell now keeps a local crash boundary in main.tsx, schedules the Sentry wrapper from a post-mount React effect after first paint/idle, and otherwise lazy-loads telemetry only when a breadcrumb/error is actually needed. The budget gate blocks static Sentry imports or direct Sentry startup from returning to main.tsx. Login/register now use a separate auth layout and lazy-load the full navbar/footer/podcast/session shell only when a non-auth route needs it, while auth, identity, public, about, help, legal, listings, blog, explore, marketplace, feed, volunteering, matches, group-exchange, skills, activity, bookmarks, resources, knowledge-base, federation, onboarding, ideation, caring-community, clubs, organisations, advertising, settings, hashtag, endorsement, jobs, goals, polls, groups, exchanges, leaderboard, achievements, dashboard, messages, wallet, profile, search, notifications, members, events, and landing/listing components import UI pieces directly instead of pulling the broad shared UI barrel back into their chunk graphs, cutting the built main entry from roughly 233 KiB gzip to roughly 178 KiB gzip. Auth SSO/OAuth buttons, password-strength feedback, suspense/error fallback screens, cookie consent, premium gating, glass-card primitives, and the globally mounted toast provider now also import only their rendered UI primitives instead of the broad barrel; the crash-only support-report form is lazy-loaded from the error boundary instead of riding the normal app startup path. The register page also lazy-loads the Google-backed location autocomplete instead of statically importing the location/maps barrel on the auth startup path. The auth language switcher, source repository tooltip, legal acceptance modal, login page, and confirm-dialog surface now load on demand, and the Rollup vendor split keeps React's JSX runtime out of the HeroUI vendor chunk, so the HTML shell no longer modulepreloads vendor-heroui and the built main entry is roughly 96 KiB gzip. Admin breadcrumbs, headers, sidebars, page headers, stat cards, data tables, bulk-action toolbars, icon pickers, visibility-rule controls, and the contextual-help drawer now use direct UI primitive imports and/or the small admin_nav namespace, and the super-admin entry no longer preloads admin.json, so admin shells and dashboard-style pages do not fetch the monolithic admin locale just to render navigation/loading/help/table/action/icon-picker/visibility labels. Admin page headers also defer the 76 KB contextual-help registry until after render and lazy-load the help drawer only when opened, and the admin header defers its non-critical support-request count lookup until after paint/idle so every admin route does not compete with first render for that badge request. The bundle-budget gate now fails if the main JS entry regresses above 200 KiB gzip, if the HTML shell modulepreloads HeroUI/charts/maps/Sentry chunks, if startup/auth files reintroduce the broad UI barrel / location barrel / monolithic admin namespace / startup Sentry import, if shared admin chrome reintroduces the help registry/drawer, full UI barrel, or monolithic namespace on the initial/open-help/table/action/icon-picker/visibility-control path, or if performance-critical public and high-traffic route surfaces reintroduce the broad UI barrel.

  • Auth-entry routes now avoid the remaining post-idle startup leaks. Routine telemetry queued on login/register/password/verification/OAuth entry routes no longer flushes Sentry just because a returning user previously allowed analytics, tenant SSO/OAuth buttons render without the shared HeroUI button wrapper, auth pages import PageMeta and the page-title hook directly instead of the SEO/hooks barrels, and the login page defers SimpleWebAuthn/passkey autofill code until the user focuses the email field or starts passkey login. Bundle-budget checks now block static WebAuthn imports in LoginPage, auth-startup hooks/SEO-barrel imports, and missing auth-route telemetry suppression.

  • Added frontend loading regression budgets for the performance audit. Lighthouse CI now has explicit script/stylesheet/image/total resource budgets, a Playwright performance spec checks that login/register do not eagerly load Maps, Stripe, realtime, telemetry, or a large locale waterfall, browse pages avoid checkout/map bundles and oversized local image responses, and npm --prefix react-frontend run check:bundle-budget fails if the built main JS/CSS gzip payloads creep above the audited thresholds. npm --prefix react-frontend run analyze:bundle now emits a Rollup visualizer report for bundle investigations. Tenant bootstrap, public tenant lists, platform stats, category lists, and marketplace category templates now get public ETag/cache headers plus tenant-aware in-memory SPA caching, while role-aware menu responses stay uncached at the HTTP layer. The PHP Docker runtimes now install the webp command-line tools alongside GD WebP support so legacy and batch conversion paths have the expected converter available, and php artisan media:warm-thumbnails can safely dry-run or pre-generate the same cached derivatives used by /api/v2/media/thumbnail for existing uploads.

  • Production route performance audits are now repeatable after deployment. npm run audit:performance:routes runs a Playwright-based route sweep for login, register, listings, marketplace detail, feed, profile, search, messages, events, groups, and admin dashboard against PERF_AUDIT_BASE_URL/PERF_AUDIT_TENANT_SLUG, optionally using PERF_AUDIT_EMAIL/PERF_AUDIT_PASSWORD and admin credentials for protected workflows. It records request counts, transfer sizes, navigation timings, oversized local images, unexpected Maps/Stripe/Sentry/realtime loads, media-thumbnail cache headers, each route's largest transferred resources, and locale-waterfall warnings under .local-docs-archive/performance-traces/latest/; it explicitly flags too many locale JSON responses, admin locale payloads on non-admin routes, and oversized locale files so the original language-file regression is checked in every post-deploy trace. The audit also auto-discovers the first marketplace detail link from the marketplace browse page when PERF_AUDIT_MARKETPLACE_DETAIL_PATH is absent, warns if no real detail URL can be found, labels localhost/Vite runs as dev-mode evidence rather than production proof, and fails fast when the target host is unreachable so the post-deploy production trace requirement is a deterministic command instead of a manual checklist.

  • CDN/cache-header verification is now a repeatable post-deploy command. npm run audit:performance:cache probes the app HTML shell, service worker, manifest, and a real media-thumbnail URL against CACHE_AUDIT_BASE_URL, records Cache-Control, content type, validators, and CDN cache headers under .local-docs-archive/performance-traces/latest/, reuses the first thumbnail URL observed by audit:performance:routes when CACHE_AUDIT_THUMBNAIL_URL is not set, and fails/warns when the HTML shell is cacheable without revalidation, service-worker caching is too loose, or thumbnails lack long-lived immutable caching plus ETag/Last-Modified evidence.

  • Volunteering group reservations now consume shift capacity everywhere. Shift listings, applications, direct shift signup, admin approval, waitlist joins/promotions, and group reservation creation now share reservation-aware capacity accounting; group reservation creation locks the shift row before checking slots and inserting, direct signup re-checks opportunity/organisation visibility, failed expense submissions delete uploaded receipts, and the React volunteering/organisation pages hide disabled matching, expenses, QR check-in, and review surfaces instead of leaving dead UI.

  • Jobs module audit fixes closed pagination, visibility, i18n, and test-runner gaps. Public job browsing now uses structured cursor pagination that respects featured-first ordering plus deadline/salary sort keys, recommendations share the same open/deadline/moderation visibility filter as public listings, jobs AI/calendar/audit/offer strings now render through PHP translations, the timebank offer credit ledger description is translated, and the jobs page/component Vitest coverage no longer hangs on recursive/asynchronous mocks.

  • Volunteering shifts now enforce approval, capacity, and visibility rules consistently. Opportunity creation/update, group signup, waitlist, expenses, check-in, matching, and organisation application flows now share stricter approved-organisation/opportunity checks, translated validation messages, and frontend guardrails so pending or unavailable volunteering records cannot leak into public or applicant workflows.

  • Volunteering React workflows now expose the end-to-end module actions. Browse and organisation-profile apply flows recover into guardian consent, opportunity details can sign up, switch, cancel, or waitlist shifts, opportunity creation sends remote/geocoded data, expenses upload receipts, group leaders can reserve shift slots, organisation profiles accept reviews, and organisation application declines support policy-aware decision notes.

  • Volunteering organisation profiles no longer expose pending or suspended organisations. The public React/API and accessible frontend organisation detail paths now use the same approved/active visibility rule as the directory, while still returning the full public stats contract (opportunity_count, volunteer_count, total_hours, review_count, average_rating) for approved profiles. New organisation registration now returns users to their owner-scoped managed-organisations area on web and mobile instead of opening a public pending profile. The organisation detail page and accessible jobs placeholder no longer pass vol_organizations.id into the jobs module's separate organizations.id filter, avoiding unrelated job matches. Volunteer hour logging now refuses non-approved organisations, and the documented admin volunteering opportunities endpoint is registered. Regression coverage pins public API, service, accessible, React, and mobile behavior. Root Cause: the public detail path called the older British-spelling service method, which was tenant-scoped but not status-scoped or stats-complete, and client registration flows had come to rely on that leak. Prevention: both organisation detail service methods now hide non-approved organisations by default, and tests cover pending/suspended HTTP detail, accessible detail/jobs, registration redirects, and suspended-org hour logging.

  • Main-branch CI now passes npm 10 lockfile validation, PHP 8.2 config bootstrap, and the current OWASP scan. The React frontend lockfile now includes the nested rxjs@7.8.2 peer entry required by HeroUI/i18next tooling under GitHub Actions npm 10 resolver, the Docker/test database SSL option now uses a cache-serializable PHP 8.2-compatible PDO option map, the test translation preload limiter now reads through cached Laravel config, the OWASP-only extract-zip@2.0.1 finding is suppressed as dev/CI-only Puppeteer browser-download tooling with no patched npm release, and prerender/page/wallet regression tests were updated for tenant-prefixed inventory routes, full custom-page fillable fields, and collision-free CI fixture users.

  • Blog featured-image uploads now preview and render reliably across dev and production hosts. The blog builder no longer saves the environment-specific absolute URL returned by the upload endpoint, which could persist localhost or a development API hostname into posts.featured_image; uploaded blog images are saved as /storage/... paths and resolved at display time. The React asset resolver now honors VITE_API_URL as well as VITE_API_BASE, re-roots stale localhost /storage URLs, and the Vite dev/preview server proxies /storage alongside /uploads. The public Blog API also re-roots stale localhost storage URLs so posts created during the broken window can still render.

  • Custom Pages built with GrapesJS now respect light and dark mode. The NEXUS starter blocks no longer export hard-coded light backgrounds/text colours; they use the platform theme tokens (--background, --foreground, --surface-elevated, --border-default, --accent-color) with fallbacks. Public custom-page rendering now injects a scoped builder baseline so builder pages inherit the active React/HeroUI/Tailwind theme, and legacy saved .nexus-page-* starter CSS is tokenized at render time so pages created before this fix are not stuck in a light palette. The scoped NEXUS starter theme override is emitted after saved builder CSS, so old pale hero gradients cannot win over dark mode. The builder preview iframe now receives explicit light/dark token definitions instead of always rendering on a white canvas. Regression coverage pins tokenized starter CSS, legacy public rendering, and themed preview output.

  • Fresh install Super Admin now boots into a usable tenant-1 platform root. The default seeder now gives tenant 1 the required hierarchy root path (/1/) as well as hub/subtenant permissions, and the bootstrap regression now logs in with the seeded god-mode admin, verifies the Super Admin dashboard, tenant list, hierarchy endpoint, and creates a child tenant under the master tenant. True god-mode users now receive platform-global Super Admin access even when their current login token belongs to a non-hub tenant such as tenant 2; ordinary tenant superadmins still need master/hub scope. The React tenant bootstrap also clears tenant-bound auth tokens when URL/bootstrap resolution moves the app from one tenant to another, preventing stale tenant-2 sessions from producing misleading Super Admin 403s on tenant 1.

  • React error fallback no longer crashes outside the toast provider. The app-level error boundary now sits inside ToastProvider, and the fallback can suppress the provider-dependent report-problem action when needed. This prevents an original route/chunk failure from being hidden behind a secondary useToast must be used within a ToastProvider crash.

  • Custom Pages built with GrapesJS now respect light and dark mode. The NEXUS starter blocks no longer export hard-coded light backgrounds/text colours; they use the platform theme tokens (--background, --foreground, --surface-elevated, --border-default, --accent-color) with fallbacks. Public custom-page rendering now injects a scoped builder baseline so builder pages inherit the active React/HeroUI/Tailwind theme, and legacy saved .nexus-page-* starter CSS is tokenized at render time so pages created before this fix are not stuck in a light palette. The scoped NEXUS starter theme override is emitted after saved builder CSS, so old pale hero gradients cannot win over dark mode. The builder preview iframe now receives explicit light/dark token definitions instead of always rendering on a white canvas. Regression coverage pins tokenized starter CSS, legacy public rendering, and themed preview output.

  • Local Laravel API boot no longer stalls the Vite login flow on Docker Desktop. The custom translation cache now stores merged PHP + JSON translation groups, avoiding a full per-locale PHP translation preload during every Laravel bootstrap. This cuts local Docker API startup/request overhead that made the React Vite login screen feel stuck.

  • Docker fresh installs can load the committed Laravel schema dump without a MySQL SSL mismatch. The local/dev/test database config now disables MariaDB client SSL verification for Laravel's schema-loader path, matching the non-SSL Docker DB service used by php artisan migrate --seed.

  • Local development documentation is now Docker-first. Removed stale non-Docker local-server guidance and its hardcoded startup helper, standardized local API references on Docker PHP at 127.0.0.1:8090, and updated frontend/mobile/accessibility examples so fresh installs target the Docker stack.

  • Fresh prerender audit closed the remaining dynamic-route and all-tenant route-safety gaps. Manual enqueue, webhook/service invalidation, and unexpected-snapshot cleanup now all fail closed for tenant-owned dynamic routes: public detail pages must match a known sitemap shape and belong to the target tenant before they can be recached, while private/tool routes such as /listings/create and /profile/settings are rejected instead of slipping through broad prefixes. Explicit all-tenant route jobs are now limited to the always-public route floor, so feature-gated routes such as /jobs cannot bypass the tenant-aware planner and render across tenants with that module disabled. The cleanup sweep now removes wrong-tenant dynamic snapshots such as stale /blog/{slug} files, not just wrong-tenant CMS pages. The host queue worker now validates the exact five-key job export contract before eval, and the Inventory UI translates staleness chips consistently instead of showing raw API enum values.

  • Custom Pages admin list now stays lightweight for full GrapesJS pages. GET /v2/admin/pages no longer returns the full content, content_format, or potentially multi-megabyte design_json editor payload for every row; the list screen keeps using summary metadata, while the edit screen still loads the full page through GET /v2/admin/pages/{id}. A controller regression test pins the slim list contract so large designed pages cannot make Content > Pages sluggish again.

  • Fresh custom Pages builder audit pass closed the remaining edit/save race and UI polish gaps. Switching away from Design mode now flushes the live GrapesJS canvas before any destructive-mode warning, cancelled switches keep the fresh builder state instead of falling back to stale parent props, and mode tabs stay inert while saving. The Design canvas/palette/inspector now show a read-only saving layer so direct GrapesJS edits cannot mutate the page underneath an in-flight save. Public custom-page breadcrumbs now route through the tenant-aware home path, and shared builder chrome comments/tests were generalized from newsletter-specific wording to the shared page/newsletter builder surface.

  • Fresh custom Pages full-stack audit fixed schema bootstrap drift and the remaining outer-form save race. The committed MySQL schema dump now records the Pages builder migration in laravel_migrations, matching the columns already present in the dump so schema-loaded environments do not think the migration is still pending. The Content > Pages form now disables title, slug, meta, status, navigation settings, preview/back/cancel controls, and menu fields during an in-flight save, matching the editor lock so authors cannot change page metadata under the request.

  • Custom Pages HTML rendering now preserves safe inline styling from pasted or builder-authored HTML. Public custom pages still strip scripts, event handlers, unsafe URL schemes, app-shell/global CSS, fixed/sticky overlay declarations, zero-inset escapes, and z-index, but safe inline declarations such as colours, spacing, sizing, and border radii now survive the public sanitizer. This fixes a production-readiness gap where HTML pasted into Content > Pages could save correctly but render visually flattened on the public page. The unsafe CSS checks were also made deterministic by removing stateful global regex flags, with repeated-call regression coverage.

  • Hardened the custom Pages GrapesJS builder completion path. Creating a page now respects an explicitly entered slug while preserving title-derived and duplicate-suffix behaviour, public builder HTML no longer renders inside a duplicate builder wrapper, and the shared image-library/modal + HTML insert-image flows now use the builder asset API with explicit page/newsletter labels. Page Design uploads now register absolute uploaded image URLs with GrapesJS whether they come from the toolbar, the GrapesJS asset manager, or the image library, HTML-mode custom pages render through the scoped custom-page HTML path so pasted designed HTML keeps its layout/CSS, and regression tests cover manual slugs, wrapper scoping, toolbar uploads, selected-image replacement, asset-manager drops, and library upload/select behaviour.

  • The prerender engine no longer fans tenant-owned detail routes, including custom CMS pages, across every tenant. Explicit prerender jobs such as /page/foo now require a tenant_slug/tenant id at the API, service, and host-script layers, preventing a page that exists only on one community from being rendered as 404 snapshots under all other tenants. Auto-recache and sitemap-drift detection now normalize cached app-domain paths back to tenant-local routes before enqueueing, so /tenant-a/page/foo is recached as tenant A's /page/foo instead of being misattributed by shared host. The unexpected-snapshot purge now validates /page/{slug} against the owning tenant's published CMS pages, so stale cross-tenant custom-page snapshots can be cleaned up while legitimate tenant pages are preserved. Parent-domain sub-tenant host resolution was also aligned with the sitemap planner. The admin panel now includes plain-language operator workflows, a tenant safety inspector that explains why each saved snapshot exists, and a preview-first live purge flow with typed confirmation; the live-delete button now stays disabled until the matching preview, typed confirmation, and all-tenant acknowledgement are complete. The API enforces the same confirmation for live all-tenant purges, and summary counts now ignore the synthetic inventory-truncation marker instead of treating it as a snapshot. The audit history now uses plain-language action/outcome labels with formatted details instead of raw codes and dense JSON, and the large admin module has been split into focused operator-guide, tenant-safety, purge, and formatting components. Health/metrics observability now reports cache read/write status, tenant-aware route-planner readiness, queue age, stuck jobs, scheduler heartbeats, and the last successful/failed prerender pass with operator-readable labels. The sitemap explorer now parses the actual prerender:plan-routes JSON shape, so operators see tenant dynamic routes from the same plan the worker uses. Regression coverage covers global CMS-page enqueue rejection, tenant-scoped CMS/blog ownership rejection, all-tenant purge confirmation, tenant safety reporting, cross-tenant CMS-page snapshot cleanup, auto-recache/drift wrong-tenant canaries, health/metrics stability, and an authenticated-admin Playwright scenario for the tenant-safety/purge/enqueue workflow. The Docker test database bootstrap now disables unsupported MySQL client SSL verification only in the testing environment, so schema loading no longer fails before the prerender tests can run. Root Cause: tenant-local detail routes were accepted without a tenant scope, and cleanup treated every /page/* snapshot as intrinsically valid dynamic content. Prevention: tenant-owned detail routes are rejected unless tenant-scoped and owned by that tenant according to the sitemap eligibility rules, and purge validates CMS-page ownership before preserving /page/* snapshots.

  • The newsletter builder's block palette is now a clean 2-column grid of all 15 email blocks — matching the MJML newsletter demo. An earlier attempt to relabel/describe/group the blocks (customizeBlocks) never actually applied in the browser — it ran before grapesjs-mjml finished registering its blocks (that happens during the editor's onReady cycle, after init() returns), so it was silently overwritten — and a related CSS change had meanwhile left the palette as a cramped single column. The non-functional relabelling was removed (the palette now uses grapesjs-mjml's own default block labels, which match the demo), and the palette is a proper 2-column grid of rounded, themed, icon-on-top + label-below cards (verified against a rendered harness: 2 cards per row, 24px icons). All 15 blocks are surfaced (1/2/3 Column, Text, Button, Image, Divider, Group Social, Social Element, Spacer, Navbar, Navbar Link, Hero, Wrapper, Raw).

  • You can now set the hero (and section) background image in the newsletter builder — the "couldn't change the hero image" gap is closed. grapesjs-mjml registers mj-hero without a background-url trait, so a dropped hero had no UI to set its background picture at all. The Settings tab now surfaces Background image URL, Background colour and Height for a selected mj-hero (and background image/colour for mj-section), added on selection via a new builderTraits helper; and the image pipeline is now hero-aware — selecting a hero and using Insert image (or dropping an upload onto it) sets its background-url (an absolute, email-safe URL) instead of inserting a stray mj-image. New newsletter_builder.trait_* strings across all 11 locales (drift 0/0). Regression coverage: imageActionFor (vitest) pins hero → background, image → replace src, anything else → insert a new image.

  • The newsletter Design Studio's image feature now works end-to-end — the button, drag-and-drop, and (crucially) the delivered email — and the builder is properly labelled and more fully featured. Three separate defects made images unusable: (1) the "upload" affordance ran through GrapesJS's default AssetManager modal, whose file-picker depends on Font Awesome glyphs this app doesn't ship, so it rendered dead; (2) a dropped image showed in the editor preview but never rendered in the sent email, because on a canvas drop there is no asset-manager target, so GrapesJS's own blob:/base64 preview stayed in the saved model (a live reference in the browser, a dead one in an inbox) — and the insert code also fell back to a relative /storage path, which no email client can resolve; (3) inserting an image mangled the rest of the email, because it appended a raw MJML string with only quote-escaping, corrupting the surrounding MJML nesting. The image path is now a single, reliable pipeline: every entry point (toolbar Insert image, asset-manager drop, edit-existing) uploads to our own domain and applies the absolute URL only (never the relative path — extracted into a tested resolveUploadedUrl), insertion uses the GrapesJS component-definition object API (insertImageComponent — no string re-parse, no sibling corruption), a component:add sweep overwrites any stray blob:/data: image with the uploaded URL, and inserting an image selects it and opens Settings so alt text / link are immediately editable. A server-side send-path safety net (EmailHtmlSanitizer::normalizeEmailImageSources, wired into NewsletterService::renderFullHtmlEmail) guarantees a delivered email never carries a broken image regardless of what was saved: it absolutizes root-relative /storage & /uploads <img src> to APP_URL, rewrites protocol-relative //host to https://, and drops blob:/non-image-data: images. Alongside the fix, the builder was made clearer and more capable (kept in its full-screen studio, which stays): the block palette is now labelled — each block shows a lucide icon, a bold title and a one-line description, grouped under Layout / Content (GrapesJS's native drag-to-canvas is preserved; only the card rendering is ours, via customizeBlocks), and the collapsed side-rails now name themselves instead of being silent bars; a Send test to me action (wiring the existing POST /v2/admin/newsletters/{id}/send-test), a desktop/mobile live-preview modal, editable subject + preheader in the studio header, an autosave status chip, and a first-run "start from a template / blank" card were added. New newsletter_builder.* strings (block labels/descriptions, preview, send-test, empty-state) translated across all 11 locales (drift 0/0). Regression coverage: insertImageComponent/resolveUploadedUrl (vitest, GrapesJS-free), and EmailHtmlSanitizerTest + NewsletterRenderFormatsTest for the send-path image normalization (relative→absolute, blob/junk-data dropped, absolute untouched). Root Cause: the image flow relied on GrapesJS's Font-Awesome-dependent asset modal and left a client-only blob:/base64 (or a relative /storage) src in the saved model, and inserted images as raw MJML strings; nothing on the send path guaranteed reachable, absolute image URLs. Prevention: one shared upload→apply pipeline that inserts only absolute URLs via the object API, a defensive blob sweep, and a tested server-side normalizeEmailImageSources net on every html/builder send.

  • Image upload in the newsletter Design Studio (and every other uploader) works again — the upload endpoint was registered at the wrong path, so every upload 404'd. The whole newsletter image pipeline — the GrapesJS asset manager, the "Insert image" toolbar button, and InsertImageButton — uploads through adminNewsletters.uploadImage(), which POSTs to POST /api/v2/upload; the UploadController is written as a v2 endpoint ($isV2Api = true, docblocked POST /api/v2/upload) and returns exactly the { url, path, mime_type, size_bytes } shape the frontend and the uploadResponseSchema Zod validator expect. But the route was only ever registered as POST /upload (→ /api/upload), a path nothing in the app calls — so every real upload hit the non-existent /api/v2/upload and came back 404, and the image silently never appeared. Registered the canonical POST /v2/upload route (kept POST /upload as a harmless legacy alias, same controller + throttle). No frontend change was needed — the client was always correct; the server route simply never matched its own contract. Root Cause: routes/api.php registered the generic upload endpoint as /upload while the controller's $isV2Api contract and every caller targeted /v2/upload, so all uploads 404'd. Prevention: UploadControllerTest now asserts POST /api/v2/upload is registered and auth-gated (a missing route returns 404, so the 401/422 assertions pin the route's existence), alongside the retained /upload coverage. Requires a PHP-app deploy to take effect in production (route registration is server-side).

  • A tenant's custom header logo no longer spills over and bleeds past the header on mobile. Uploaded custom logos are curated per-tenant and can be large — a tall square/stacked mark renders at 64px inside the 56px mobile navbar row (and Layout then added a further --logo-extra: 1.75rem top offset), while a wide wordmark at max-w-[200px] crowds the hamburger and action icons on a ~360px phone — so the logo overflowed the header on small screens. On mobile (< sm, 640px) the navbar now shows the compact brand-mark icon — the exact initials Avatar fallback that tenants without a custom logo already get (initials on the tenant's brand colour, auto-contrast text) — and swaps back to the full custom logo at sm+, unchanged. Implemented as an opt-in collapseLogoOnMobile prop on the shared TenantLogo component (Navbar only — the mobile drawer and footer are untouched), using pure Tailwind responsive gates (sm:hidden / hidden sm:inline-flex) so there's no JS media query and it's prerender/SSR-safe; the light/dark logo swap is preserved. Layout now applies --logo-extra at sm+ only, so square-logo tenants no longer get a dead gap below the header on mobile. Also fixed a latent accessibility gap: the brand/home link now always carries aria-label={tenant name} (previously, when the logo image was hidden and the avatar is decorative, or below 480px with no logo, the link had no accessible name). Regression coverage: TenantLogo.test asserts that with a custom logo + collapseLogoOnMobile both the compact avatar and the full logo render (gated by the responsive tokens), that the swap is opt-in, and that the link exposes an accessible name. Root Cause: the header rendered the full-size custom logo at every breakpoint, so large logos exceeded the compact mobile navbar's height and width. Prevention: the Navbar opts into a mobile icon swap centralized in TenantLogo; jsdom can't evaluate media queries, so the test asserts on the presence of the responsive class tokens rather than computed visibility.

  • The community switcher now signs a logged-in member out before moving them to another community, instead of stranding them half-authenticated. Switching from a parent timebank (e.g. Timebank UK) into one of its sub-communities (Crew Kirk, Stratford) via the utility-bar "Switch community" dropdown kept the member's session token — but that token is scoped to the original tenant, and the API rejects it against any other community with 403 tenant_mismatch (app/Http/Middleware/Authenticate.php). The result was a logged-in-looking UI throwing errors on every tenant-scoped request. The switcher (Navbar.tsx handleTenantSwitch) now calls logout() first (clearing the access token and tenant identity, invalidating the token server-side) so the destination boots cleanly logged-out and the member re-authenticates against the new community. Platform super admins are exempt — they can legitimately cross tenants with one token, matching the server's cross-tenant rule exactly (a new isPlatformSuperAdminUser() predicate that, unlike isSuperAdminUser(), deliberately excludes tenant-scoped is_tenant_super_admin, since a tenant super-admin hits the same 403 elsewhere). Regression coverage: two Navbar.test cases assert a normal member is logged out before the switch and a platform super admin is not. Root Cause: the switcher performed a same-origin navigation to the sub-tenant while leaving the original tenant's token in localStorage, which the API then rejected as a tenant mismatch on every request. Prevention: the frontend exemption predicate is documented as MUST-mirror the server rule in Authenticate.php, and the two Navbar tests lock the logout/no-logout behaviour by role.

  • The moderation Reports page (broker & admin panels) now shows who and what each report is actually about — previously it was untriageable. Every row listed only the reporter, a bare content-type chip, and a "reason" that was printed twice (the API mapped both the reason and description columns to the single reports.reason value, so the two table columns were always identical), with no way to see the reported member or content. The reports table has always stored the target polymorphically (target_type + target_id), but the API returned only the raw target_id integer. A new ReportTargetResolver service now batch-resolves each page of reports into display metadata — the reported member's name/avatar, or the reported content's preview + title + author (resolving user, post, comment, listing, review, event in one query per type, no N+1, tenant-scoped) — and flags deleted targets as removed. The table's redundant "Description" column is replaced by a "Reported" column showing that target inline, and each row opens a self-contained detail drawer (reporter, full target with author, reason, timestamps, and the existing Resolve/Dismiss actions) so a moderator can triage without leaving the panel; links to a reported member stay inside the current (broker/admin) panel. Also fixed: the content-type filter silently ignored comment/review/event (the backend allow-list omitted them). New admin.moderation.* UI strings translated across all 11 locales (drift 0/0). Regression coverage: AdminReportsControllerTest asserts user/listing targets resolve to a label + author and that a deleted target is reported as target_exists=false. Root Cause: the reports API never resolved the polymorphic target and duplicated reason into description, so the UI had nothing meaningful to render. Prevention: target resolution lives in one tested service; the feature test locks the enriched response shape.

  • Admins can delete feed posts again — and so can everyone whose action goes through a confirm dialog. The shared confirmation dialog was silently answering "no" to every "Confirm". Clicking ⋯ → Delete post → Confirm on another member's feed post did nothing at all: no network request, no toast, no log — which is exactly why three prior fixes to the delete endpoint and permission gate never helped (the delete was never sent). The app-wide ConfirmDialog (useConfirm(), used by ~71 destructive actions) rendered its Confirm button with both slot="close" and onPress={() => resolveAndClose(true)}. In a controlled HeroUI v3 AlertDialog, slot="close" fires onOpenChange(false)resolveAndClose(false), which raced the button's own resolveAndClose(true); because resolveAndClose settles the promise on its first call, the close-driven false won in a real browser, so confirm() resolved false and every delete handler bailed at if (!ok) return; before calling the API. Removed slot="close" from both action buttons so each has a single deterministic resolver (Confirm → true; Cancel / ✕ / backdrop / ESC → false); the dialog still closes via the controlled open state (pending → null). This restores every confirm-gated destructive action platform-wide, not just feed deletion. Root Cause: the Confirm button combined two mutually-exclusive HeroUI v3 close mechanisms — slot="close" (auto-close) and a result-capturing onPress — whose false/true resolvers raced on a single click, and the false path won in real browsers. Prevention: ConfirmDialog.test now asserts confirm() resolves true on confirm (previously the boolean was deliberately left unasserted because the dual-resolver ordering "differed between jsdom and a real browser"); removing slot="close" eliminates the ordering dependency, so jsdom and the browser now agree. HeroUI v3 AlertDialog close semantics were verified via the heroui-react MCP docs (the "Close Methods" and "Controlled State" sections show slot="close" and a result-capturing onPress are mutually exclusive).

  • The notification bell dropdown no longer renders rows squashed and overlapping on top of each other. Each notification row in the flyout (desktop popover + mobile drawer) is a HeroUI v3 <Button>, and HeroUI's base button style hard-codes a fixed height (h-10/md:h-9 = 36px). The row markup added min-h-9, but a min-height cannot cancel a fixed height, so every row stayed pinned at 36px while its wrapped 2–3 line notification text (~60px) overflowed downward onto the next row — producing the cascading overlap. The row Button now sets h-auto (height follows content) with a min-h-14 tap-target floor, so each row grows to fit its text; the message text also gained line-clamp-2 so a single long notification can't dominate the compact dropdown (full text remains on the /notifications page) and the text→timestamp gap was widened mt-0.5mt-1. The HeroUI Button is retained for its focus-ring/keyboard/ARIA behaviour — only its fixed height is neutralised. Root Cause: a fixed-height HeroUI Button was used as the container for multi-line content; min-h-* does not override a fixed height. Prevention: multi-line clickable rows must use h-auto (not just min-h-*) when built on a component whose base style sets a fixed height. HeroUI Button styles were checked via the heroui-react MCP (get_component_source_stylesbutton.css) to confirm the fixed-height root cause.

  • The category dropdown on the Create Listing page no longer shows a raw [missing] listings:form.category_search marker. The category Autocomplete's search box called t('form.category_search'), but that key was never added to the listings namespace, so i18next fell back to rendering the literal [missing] namespace:key debug marker as the search placeholder. Added form.category_search ("Search categories") to the English source and translated it into all 10 non-English locales (drift 0/0). Root Cause: a translation key was referenced in CreateListingPage.tsx without ever being added to the locale files. Prevention: check-i18n-drift.mjs (CI-gated) reports 0/0; any future missing key would fail the gate.

  • The newsletter "Design" builder is usable again — rebuilt with its own always-visible block palette and a labelled toolbar. The GrapesJS drag-and-drop builder rendered its canvas but exposed no way to add content: the block palette and the Style/Settings/Layers panels are toggled by GrapesJS's default toolbar buttons, whose glyphs come from Font Awesome — which this app does not ship (it uses lucide-react) — so every toggle rendered as a blank square and the block palette was stranded behind an invisible "open-blocks" button. The builder now suppresses GrapesJS's default chrome (panels: { defaults: [] }) and pins each manager into our own React layout: a permanent left block palette (BuilderBlockPalette, hosting the MJML BlockManager), a labelled lucide toolbar (BuilderToolbar — undo/redo, desktop/tablet/mobile device switch, outline toggle, view-compiled-HTML modal, clear) with tooltips + aria-labels, and a tabbed Style / Settings / Layers inspector (BuilderInspector) whose manager nodes stay permanently mounted. The MJML engine (grapesjs 0.21.13 + grapesjs-mjml 1.0.8), design_json save/restore, image upload (POST /v2/upload) and the sent-newsletter read-only overlay are unchanged, as is the content/content_format='builder'/design_json save contract. 13 new strings translated into all 11 locales. Root Cause: GrapesJS 0.21's default panel buttons depend on Font Awesome glyphs the app doesn't load, so the block palette had no visible, labelled way to be reached. Prevention: the builder no longer relies on GrapesJS's default panels at all; NewsletterBuilder.test asserts panels:{defaults:[]} + the pinned manager appendTo targets and the always-visible palette, and BuilderToolbar.test asserts every control is labelled and its click reaches the editor handler.

  • The newsletter Design builder is now genuinely usable — a full-screen studio, working image insert, starter templates, and no more garbage renders. After the chrome rebuild the builder was still squeezed into the newsletter form's two-thirds-width content column and then spent a fixed 224 px palette + 288 px inspector, leaving roughly 250 px of actual canvas — so columns were unworkable, three-column layouts effectively unusable, and the whole thing looked unfinished. Design mode now opens a dedicated full-screen studio (NewsletterDesignStudio, route /admin/newsletters/edit/:id/design) that fills the viewport with its own header (Back · Save · Done) and background-autosaves through the existing partial update endpoint (only content/content_format/design_json, so targeting and scheduling set on the form are never clobbered); the inline Design tab becomes a launcher card that opens it (saving a draft first to obtain an id). The palette and inspector are now collapsible, oversized block cards shrink from 68 px to 44 px, and the canvas gains a neutral gutter so the email reads as a sheet. Image insert actually works and is discoverable: the asset-manager upload now applies the uploaded file to the target block and closes the modal (previously it only added the asset to the gallery and left the image blank), and a new Insert image toolbar button uploads via POST /v2/upload and drops an mj-image at the selection. A Templates picker in the builder loads starter designs straight into the canvas, design_json now round-trips through the gallery/API, and an idempotent migration seeds five curated MJML starter templates (Announcement, Newsletter digest, Event invite, Single call-to-action, Welcome) — stored as MJML in content so they parse into real mj-* components — for every tenant. Legacy design_json from the pre-MJML builder no longer renders as broken, unaligned junk: a restore is only accepted if it still compiles to valid table HTML, otherwise the canvas is reset and a dismissible "built in an older editor" notice is shown. 28 new newsletter_builder.* strings translated across all 11 locales (drift 0/0). Root Cause: the builder was mounted inside a narrow form column with fixed side panels (no room for the canvas), and the custom asset uploader added images to the gallery without ever assigning them to the target block. Prevention: the builder lives in its own full-screen route with collapsible panels; NewsletterBuilder.test asserts the restore-validity guard (a design that doesn't compile to MJML is dropped, not rendered), and the upload path applies the asset to the editor target.

  • GDPR admin: "Mark completed" no longer 500s, the compliance dashboard shows real numbers, and account erasure actually wipes 2FA secrets and exchange notes. Setting a data-subject request to completed (PUT /v2/admin/enterprise/gdpr/requests/{id}) returned a 500 ("Failed to update GDPR request") — the update wrote a completed_at column that does not exist on gdpr_requests (the real column is processed_at); only the completed branch appended it, which is exactly why Mark processing and Reject worked but Mark completed failed. The same phantom column silently zeroed three GDPR-dashboard analytics (average processing time, on-time compliance score, and month-over-month completed counts) behind swallowed try/catches, so the compliance figures had been wrong. Two further swallowed schema-drift bugs on the Article-17 erasure path meant personal data survived a right-to-erasure request: the 2FA step targeted non-existent users.totp_secret / totp_backup_codes columns (so the AES-encrypted secret in user_totp_settings and any "remember this device" trusted-device tokens were never deleted), and the exchange-notes step referenced a non-existent exchange_requests.provider_notes column (so the whole UPDATE threw and no notes were cleared). Completing a request now records processed_at / processed_by; erasure deletes the real TOTP store (user_totp_settings + user_trusted_devices) and clears the exchange free-text that actually exists (requester_notes, broker_notes, requester_feedback, provider_feedback, cancellation_reason, decline_reason); both completion writes are tenant-scoped; and if a critical erasure step ever fails again the request is left processing (logged at error) instead of being falsely reported complete. Root Cause: controller and service SQL referenced columns that do not exist in the schema, hidden behind broad try/catch swallows that turned hard failures into silent no-ops. Prevention: regression tests assert the endpoint returns 200 with processed_at/processed_by set (AdminEnterpriseControllerTest), that erasure removes user_totp_settings/user_trusted_devices (MemberSelfServiceTest), and a source guard (GdprServiceTest) locks the corrected TOTP/exchange column set so the drift cannot silently return.

  • Video embeds (YouTube and every other provider) render again — the SPA's own Content-Security-Policy was silently blocking them, not YouTube. After the "restore video embeds" work started emitting youtube-nocookie.com player iframes and img.youtube.com thumbnails, the React app's Content-Security-Policy response header (set by nginx) still listed only Stripe + Cloudflare under frame-src and no video hosts under img-src, so every browser refused to frame the player (Framing 'https://www.youtube-nocookie.com/' violates ... frame-src) or load the poster (Loading the image 'https://img.youtube.com/...' violates ... img-src) — before any request ever reached YouTube. The feed, the knowledge-base resource pages, and the job-vacancy employer videos were all affected. frame-src now allows the six supported providers (youtube-nocookie.com, youtube.com, player.vimeo.com, open.spotify.com, w.soundcloud.com, player.twitch.tv, clips.twitch.tv, www.tiktok.com) and img-src allows the YouTube thumbnail hosts (img.youtube.com, i.ytimg.com), across every vhost block in both react-frontend/nginx.bluegreen.conf (production) and react-frontend/nginx.conf (single-color fallback). Root Cause: the CSP allow-list was never updated to match the hosts the newly-restored embed feature emits; the browser, not YouTube, was blocking the frames and images. Prevention: the CSP frame-src/img-src must be extended whenever a new embedded third-party origin is introduced — the embed builder (LinkPreviewService::videoEmbedFor) and the CSP are now kept in sync on the same provider list. Requires a React-container rebuild/deploy to take effect (the header is baked into the nginx image).

  • Feed posts containing a YouTube/Vimeo/streaming URL embed an inline player again (regression). Video embeds had silently stopped working platform-wide: LinkPreviewService built the player from the video id in the URL alone, but only after a successful server-side scrape of the page — and providers like YouTube now block datacenter-IP requests, so the scrape failed on the production server and the whole preview (video id and all) was discarded. On production this produced zero video previews, ever. Known media providers are now detected from the URL and the embed is built before, and independently of, any outbound fetch, so the player renders even when the page can't be scraped. Coverage broadened from YouTube/Vimeo to also include Spotify, SoundCloud, Twitch and TikTok (the frontend player renders each provider's iframe; Twitch gets its required &parent=<host> appended client-side; Spotify/SoundCloud render as compact audio players). Root Cause: video-embed generation was gated behind an Open Graph scrape that streaming providers now reject from server IPs, even though the embed needs only the URL. Prevention: LinkPreviewVideoEmbedTest asserts every provider yields a content_type=video embed with Http::assertNothingSent().

  • Listings (offers and requests) now reliably appear in the community feed. Publishing a listing to feed_activity was a fragile side-effect that only fired on the exact create happy-path in ListingsController::store(); listings created any other way — or before that code existed — never reached the feed (e.g. Timebanking UK's active offers were absent from its feed while showing normally in Listings). Feed publish/unpublish is now driven by the Listing model lifecycle (ListingObserver on create/update/delete, keyed on visibility — status=active and not moderation-held), so every creation path publishes and later status changes (approve/suspend/reactivate) show or hide the feed card correctly. A new idempotent php artisan feed:backfill-listings [--tenant=] [--dry-run] command retro-publishes existing active listings that are missing a feed row. Root Cause: feed publishing lived only in one controller path with no model-level guarantee and no backfill for rows that missed it. Prevention: ListingFeedPublishTest covers observer-on-create and the backfill command.

  • Admin feed-post deletion: aligned the client permission gate with the backend and added the missing success-path test. The feed page computed its "is admin" flag from a narrower set of roles than the backend authorises — it omitted god, is_god and is_tenant_super_admin (all accepted by BaseApiController::callerIsAdminTier/requireBrokerOrAdmin), so a user elevated only via those would see no Delete option even though the API would allow the delete. The check is now a shared isAdminTier() helper kept in sync with the backend. The server AdminFeedController::destroy path was verified correct end-to-end and now has success-path coverage (test_destroy_successfully_deletes_another_members_post — asserts the feed_activity row is removed and a delete_feed_item audit-log row is written), which it previously lacked. Root Cause: the frontend admin-tier check had drifted narrower than the backend definition. Prevention: single shared isAdminTier helper + the new backend success-path test.

  • Feed moderation "Unauthorized": the broker-panel Delete/Hide buttons were blocked client-side and the request never reached the server. The shared FeedModeration screen (mounted at both /admin/moderation/feed and /broker/moderation/feed) re-validated the user's role inside handleAction with a hand-rolled check that accepted only admin/super_admin/moderator plus the two super-admin flags — a much narrower set than the backend requireBrokerOrAdmin() guard on /v2/admin/feed/posts/* (which also accepts tenant_admin, broker, coordinator, and the is_admin/is_god flags). A legitimate tenant_admin or broker therefore hit toast.error('Unauthorized') and an early return before any DELETE was sent — which is why the two prior investigations found the server path correct and zero delete_feed_item rows in the audit log: the request was dying in the browser, not the backend. The gate now uses a shared canModerateContent() helper (in src/lib/roles.ts) that mirrors requireBrokerOrAdmin(). Root Cause: a client-side authorization gate drifted stricter than the server it is supposed to mirror, silently blocking authorized users before the network call. Prevention: canModerateContent() is the single client mirror of requireBrokerOrAdmin() (documented as MUST-stay-in-sync), plus a FeedModeration regression test asserting a tenant_admin without super flags reaches deleteFeedPost and sees no unauthorized toast.

  • "Send to a specific group" (or county/town) silently emailed the entire membership. A newsletter's target_groups / target_counties / target_towns were saved by the admin UI but never read at send timesendNewsletter() didn't even select the columns — so a group-targeted newsletter went to everyone, and the recipient-count preview showed the full membership (masking the bug). The send path now reads the stored targeting and narrows every audience through a new filterUserIdsByTargeting() reusing the existing group-membership/location SQL builders (facets OR-combined, then intersected with the audience; unregistered subscribers excluded while targeting is active); sendNow() self-loads the targeting so scheduled/recurring sends inherit the fix; and the recipient-count endpoint + compose-form preview apply the same filter so the number always matches what will actually send. Related footgun fixed: target_audience = 'segment' with no segment selected used to fall through to all members — it now refuses with a validation error. Root Cause: the targeting columns were write-only; no code path between sendNewsletter() and recipient resolution ever consumed them. Prevention: NewsletterTargetingTest pins group/county/town filtering, OR-combination, the segment guard, non-member-subscriber exclusion, and sendNow() row self-loading, plus a controller feature test.

  • Deploy: made the newsletter multi-format migration pass the blue-green migration-safety gate. 2026_07_03_100000_add_content_format_to_newsletters declared ->default('richtext') on the source line after ->enum(...); the gate's line-based linter (check-migration-safety.sh) only inspects the column-definition line, so it read the addition as a non-nullable column with no default and aborted the deploy (which is correct to refuse — such an add would break the still-live color during a blue-green switch). The column was in fact always safe; moving the default onto the enum line satisfies the linter with no schema or logic change.

  • CI: brought the full test + security matrix green across every module. A manual full-pipeline run (which forces all jobs, bypassing change-detection) surfaced pre-existing failures the normal path-filtered pushes had skipped: (1) three unit tests — one legal (LegalDocumentServiceTest — the new tenant-scoped getAllForTenant conditional now uses a plain if instead of the query-builder ->when() so the DB-mock chain matches) and two newsletter model tests (Newsletter/NewsletterTemplate $fillable assertions updated for the new content_format/design_json multi-format fields); (2) a hardcoded "Space" keyboard-shortcut label on the podcast episode page, now t('player.key_space') and translated into all locales; (3) the OWASP Dependency-Check false positive that failed --failOnCVSS 7 — an incidental cygwin1.dll in the scanned workspace was fingerprinted as Red Hat Cygwin 1.3.22 (CVE-2008-3323 / CVE-2007-6181, both 2007-2008), now suppressed by exact CPE in owasp-suppressions.xml, mirroring the existing joomla/string wrong-CPE suppression.

  • Build: re-synced react-frontend/package-lock.json so CI's npm ci passes (recurring drift). A transitive rxjs@7.8.2 (pulled in via heroui-cli/i18next-cliposthog-node) keeps dropping out of the lockfile whenever it is regenerated under npm 11 — the local default, which tolerates the gap — but the npm 10.x used by CI (node 22) and the Docker images rejects it with Missing: rxjs@7.8.2 from lock file, breaking every npm ci-based job (CI Pipeline's React build, Security Scan, Lighthouse CI) while local npm ci reports "in sync". The newsletter drag-and-drop builder's dependency additions re-dropped the heroui-cli rxjs node. Regenerated with npm 10.9.3 --package-lock-only (adds only the one missing entry, no version churn, node_modules untouched so the live dev server is unaffected); verified npm ci exits 0 at npm 10.9.3. Prevention: always regenerate this lockfile with npm 10.x, never npm 11.

  • CI: resolved a HIGH underscore.js DoS (CVE-2026-27601 / GHSA-qpx9-hpmf-5gmw) that turned the Security Scan red. A freshly-disclosed underscore DoS (unlimited recursion in _.flatten/_.isEqual; vulnerable <=1.13.7, fix 1.13.8) landed in the scanners' databases and began failing the Security Vulnerability Scan on every push, independent of any code change — flagged by both Trivy and the blocking production npm audit --omit=dev --audit-level=high. underscore reaches the production bundle only via grapesjs (0.21.13 pinned its own nested copy to exactly 1.13.1; also pulled by backbone/backbone-undo) — the admin-only newsletter email builder. Fixed by adding "underscore": ">=1.13.8" to the react-frontend npm overrides block (the same mechanism already used there for basic-ftp/esbuild/postcss/serialize-javascript/tmp/uuid); the lockfile now dedupes every underscore instance to 1.13.8 and drops grapesjs's private 1.13.1 copy. This does not change grapesjs's own version (stays 0.21.13) — only its transitive underscore bumps 1.13.11.13.8, a patch-level security release with no API changes. npm audit --omit=dev --audit-level=high now reports 0 vulnerabilities; lockfile regenerated with npm 10.9.3 --package-lock-only (no version churn, rxjs@7.8.2 retained, node_modules untouched).

Changed

  • Polished the newsletter builder's Style / Settings / Layers inspector panels. The GrapesJS style, trait and layer managers are now themed to match the app — clean uppercase sector headers, app-styled inputs/selects/number-and-unit fields, segmented controls (e.g. text-align), a tidy Settings (trait) panel, and a cleaner Layers tree — instead of GrapesJS's default chrome, and they follow the light/dark theme via CSS tokens. Pure presentation (react-frontend/src/styles/newsletter-builder.css only): grapesjs-mjml already exposes the full MJML style set (typography, spacing, background including background image, border), and neither those properties nor their MJML mapping changed.

  • The admin Legal Documents area (Terms, Privacy, Cookie policy, etc.) was rebuilt so legal documents can actually be authored and saved. The module's versioning engine, public legal pages, and login re-acceptance gate were already sound, but the admin authoring UI on top of them was broken in ways that blocked entering any content. "Saved but didn't save" (the reported edit bug): the document create/edit form was built against fields that don't exist on the documents table (content, version, status live on versions, not the document), so a save silently persisted only the title while the API reported success — and the content textarea on the create page was thrown away entirely, never becoming a version. The form is now a focused Document settings form (title, type, and acceptance rules — require acceptance, when to require it, notify-on-update, active), the type is locked after creation (it anchors the unique key and every stored version/acceptance), and creating a document now drops you straight into the version editor to write the first version. The unscrollable modal (the "window that won't scroll and disappears when you click away"): the rich-text version editor lived in a modal whose internal markup broke the scroll container, so on a long document the page couldn't scroll and the Submit button was unreachable — and clicking the backdrop discarded everything typed. It is now a full-page editor with a metadata panel, the rich-text editor at full width, and a sticky action bar so Save is always reachable; unsaved-changes are guarded on navigation and on browser close. The remaining confirmation dialogs (Publish, Delete, Notify) no longer close on an accidental backdrop click. The document list now shows the real current version number, version count, and a derived status (Published / Draft only / No content) instead of a phantom always-"1.0"/broken-badge, with a primary "Manage content" action and a first-run empty state prompting new communities (e.g. Timebank UK) to set up their terms, privacy, and cookie policy. Backend hardening: the version-update service verified nothing before reporting success — a tenant-scoped existence check now guarantees a save that reports success actually persisted (cross-tenant edits return 404, published versions return 400); publishing a missing/cross-tenant version returns 404 instead of a generic 500; document creation validates the type and acceptance mode and returns a clean translated 422 on a duplicate type (previously an opaque 500); the admin list now includes deactivated documents; and version history is ordered newest-first by recency rather than by string-sorting version numbers ("10.0" no longer sorts before "9.0"). New backend regression tests cover version-update persistence and tenant isolation, publish flag/pointer updates, ordering, the full create→version→publish→public-endpoint lifecycle, and document-metadata CRUD (title/flags persist, type-change and duplicate-type rejected, cross-tenant 404, inactive documents listed). Frontend tests were rewritten for the new settings form, the new full-page editor, and the list. New admin UI strings added and translated into all 10 non-English locales (drift 0/0); two new backend api.* strings added across all 11 locales.

  • The Podcasts module is out of alpha — production-hardened end to end, with configurable media storage (local today, any S3-compatible provider when the platform grows). The full-module audit and upgrade covers storage, safety, creator tools, listener experience, admin operations, and i18n. Storage: podcast audio keeps landing on the persistent local disk by default, but the long-scaffolded cloud switch now actually works — the missing league/flysystem-aws-s3-v3 adapter is installed (flipping podcasts.media_storage_driver to cloud previously fataled at upload), the existing s3 disk serves every S3-compatible provider (AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO) via AWS_ENDPOINT, and the admin config API now validates storage settings atomically (unknown disk, uninstalled driver, or a non-http CDN base URL are rejected with translated 422s instead of breaking every subsequent upload). Two new operator tools de-risk the eventual migration: a storage doctor (POST /v2/admin/podcasts/storage/verify + php artisan podcasts:storage-doctor) proves credentials with a write/read/delete probe before the switch, and php artisan podcasts:migrate-media stream-copies existing audio to the target disk with sha256 verification, per-episode atomic repointing (no 404 window — the media proxy serves whichever disk the row names), resumability, and opt-in source deletion. The admin UI's dead "Azure Blob" disk choice (no such disk is configured) was removed. Media pipeline: the placeholder processing job now does real work — getID3 verifies uploads are genuinely audio (the upload path only trusts the client MIME type; imposter files are failed as not_audio instead of served forever), episode duration is auto-detected when the creator leaves it blank, and an optional ClamAV hook (CLAMAV_ADDRESS) quarantines infected uploads outright (object deleted, media endpoint refuses infected rows) while staying honest — without a scanner the status remains scan_unavailable, never clean, and uploads are never blocked. A new media_failure_reason column tells admins why media failed. Hardening: the audio proxy, transcripts, chapters, and RSS feeds gained rate limits (audio at 180/min per listener — generous for range-request scrubbing, bounded against bandwidth abuse); chapter payloads are capped at 200 and sorted by start time; visibility=private/members now 422s with a translated message when private shows are disabled (previously silently downgraded to public); listen recording serializes per listener instead of locking the whole episode row; and the release-due scheduler query got its missing index. Creators: the studio gained upload cancel (form preserved) and retry, client-side file size/type validation before a multi-hundred-MB upload starts (limits ride along in /v2/podcasts/mine meta), a "Scheduled for {date}" chip on scheduled episodes, a creator-facing RSS preflight (GET /v2/podcasts/{id}/validate-feed — the same checks admins run, now also reporting how many episodes the feed builder would silently skip), and a Listener stats panel backed by a new owner-scoped GET /v2/podcasts/{id}/stats (listens over time, unique listeners, completion rate, top episodes). Listeners: a global player context owns a single audio element, so playback survives navigation — with a persistent mini-player docked above the mobile tab bar (artwork, deep-link, play/pause, seek, close), per-episode resume positions (localStorage, cleared on completion) with a "Resume from X / Start over" affordance, a persisted playback-speed preference, keyboard shortcuts on the episode page (space play/pause, arrows seek/volume — never hijacking form fields), a Retry action on audio load errors, and a mobile-safe scrolling chapter list. Completion analytics moved into the player context so finishing an episode is recorded even after leaving the page. Admins: the dashboard's shows/episodes tables paginate server-side (previously silently truncated at 200 rows) and support bulk approve/reject with partial-failure reporting. i18n: the module is fully translated — all 24 backend API strings (previously English placeholders in every non-English locale) and all ~51 new UI strings across the 10 non-English locales; drift 0/0. The module registry drops stage: 'alpha'; the podcasts feature remains off by default (opt-in per tenant, unchanged). Regression coverage grew from 94 to 126 backend tests plus new player-context, mini-player, studio, and admin suites — including PodcastStudioPage.test.tsx, which was one of the ~20 test files hitting the known uiMock fork-worker hang and now runs.

  • Partner Timebanks panel, phase C: a plain-English and consistency polish across all 17 embedded pages. A six-way parallel audit of every page produced 90 copy rewrites (validated key-by-key against the EN source — key names unchanged, all {{interpolation}} tokens preserved) and 30 component fixes, applied one-agent-per-file. Copy: the highest-read strings — page descriptions, section headings, switch labels, empty states, destructive confirmations — now describe outcomes in coordinator language instead of protocol vocabulary (e.g. the master toggle "Federation Enabled → Partner with other timebanks", with its description now saying exactly what turning it on shares; the auto-approve toggle now spells out its risk and steers to the safe default; "Not Enabled for Tenant" → "Partnering is not switched on for this community"). Components: bare "no data" texts across the panel became BrokerEmptyStates (several with create-CTAs); ad-hoc status chips became the panel-wide BrokerStatusChip vocabulary (API keys, credit agreements, the settings master switch); rotating the aggregates signing secret now requires an explicit confirmation dialog (it invalidates partner verification and previously fired on a single click); the auto-approve switch disables while partnership requests are off; the partnership-limit input clamps typed values to its declared 1–100 range; Network settings shows an "unsaved changes" hint; the API-docs webhooks link became a tenant-aware router link (was a raw full-reload <a href> that broke inside the panel); the created-API-key display no longer renders a white box in dark mode. The sweep also surfaced and fixed four production bugs where the UI rendered raw i18n keys: 17 missing partnership level/permission/audit keys (Partnerships), 2 missing webhook status keys (an inactive or failing webhook showed federation.status_inactive literally), 12 missing activity event-type keys, and 9 mangled CSV export headers ("CSV Actor" → "Actor"); plus directory/neighbourhood counters that rendered label text where numbers belonged now interpolate the real counts. 56 new EN keys added; the 90 changed + 56 new keys were re-translated into all 10 non-English locales per-key (stale values deleted first — never --force, so human-reviewed translations elsewhere were untouched). Also fixed: scripts/translate-i18n-gaps.mjs silently skipped admin.json via a stale "admin is English-only" exclusion left over from the policy voided on 2026-06-06 — admin fills now translate like every other namespace.

  • Partner Timebanks panel: two page consolidations. (1) The partner directory and "our listing" editor — previously two separate pages — are now one tabbed page at /partner-timebanks/directory ("Browse partners" / "Our listing", deep-linkable via ?tab=profile, which the Overview setup checklist uses); the separate /directory/profile route was removed. (2) Creating a federation API key no longer navigates to a separate wizard page: the API Keys page's "Create key" buttons open the same CreateApiKey form in a modal, which closes back onto the refreshed list. CreateApiKey gained an optional onDone embedded mode (suppressing its standalone header/guidance chrome) and ApiKeys an optional onCreateClick/refreshToken pair; rendered standalone both behave exactly as before, and the deep-linkable /partner-timebanks/api-keys/create route is retained as a fallback outside the primary flow.

  • All partner-timebank / federation surfaces now live in a dedicated "Partner Timebanks" panel at /partner-timebanks, with the technical plumbing gated to super admins. The admin sidebar's technical 14-link "Partner Timebanks" section and the separate "Partner APIs & Integrations" section were replaced by a single overview-zone entry (alongside Dashboard / Broker Panel). Every admin can open the read-mostly panel — Overview, Partnerships, Partner directory, Neighbourhoods, Credit agreements, Activity feed, Analytics — while the setup/plumbing surfaces (External platforms, Credit Commons, Inbound API partners, Partner cooperatives, API keys, Event webhooks, Developer guide, Shared statistics, Data management, Network settings, and the Overview's setup checklist/settings shortcuts) are visible and routable only for super admins (super_admin role, is_super_admin, is_tenant_super_admin, or god — the same gate as the Communications section). The sidebar entry shows for all admins when federation is on; on tenants with only partner_api/caring_community (all-super-admin content) it shows for super admins alone. The panel clones the broker-panel architecture (own layout, sidebar, breadcrumbs, route guard, lazy code-split chunk) and reuses the broker's presentational primitives; every existing page is embedded unchanged as a thin wrapper (no page rewrites). Its sidebar organises the 17 destinations into plain-English sections: Partner network (Partnerships, Partner directory, Neighbourhoods, Credit agreements), External connections (External platforms, Credit Commons, Inbound API partners), Caring Community (Partner cooperatives — the section is gated on the caring_community module and disappears entirely when it is off), Access & security (API keys, Event webhooks, Developer guide), Activity & data (Activity feed, Analytics, Shared statistics, Data management), and Settings. A new native Overview dashboard answers "are we connected, is anything waiting on me, is anything broken" from the existing analytics/settings/profile endpoints, with a partnering-off banner, a recent-connection-errors banner, KPI stat cards, a 4-step setup checklist, and a most-active-partners card. Credit Commons was evaluated against the code and confirmed to be a general external protocol (one of the four federation_external_partners adapter types alongside Komunitin/TimeOverflow), not a Caring Community surface, so it stays under External connections. partner-timebanks was added to the reserved route prefixes in both RESERVED_PATHS (React) and TenantContext::getReservedPaths() (PHP) so path-based tenants resolve the panel correctly. New partners i18n namespace (~131 keys) machine-translated into all 10 non-English locales (drift 0/0). Backend endpoints are unchanged (still admin-gated); this is a navigation/visibility restructure — no federation table, protocol contract, or API behaviour changed.

  • Smart Matching 2.0 — the matching engine was rebuilt end-to-end so matches are close, relevant, reciprocal, and explainable. The headline fix: members were being matched with people on the other side of the country (e.g. for gardening) because distance was never a hard constraint — and for members without a saved location the distance filter was skipped entirely, matching them against the newest listings tenant-wide labelled "nearby". Matching now enforces hard gates in the candidate query: physical listings beyond the effective max distance (the member's preference, capped by the tenant setting) are excluded outright, remote/hybrid listings are distance-exempt, listings whose owner has been inactive for 90+ days are skipped, and members without a location get remote-friendly matches plus an in-app prompt to set their location (a per-tenant "match community-wide" escape hatch exists for genuinely nationwide communities). Scoring moved from a flat weighted sum to a three-pillar geometric mean (Relevance × Feasibility × Trust) so one terrible dimension can no longer be averaged away, with new signals: two-way directional skills matching (what you offer ↔ what they need, with proficiency weighting), availability overlap, member activity recency, review trust (Bayesian-smoothed), and completed-exchange history. The engine also learns: dismissals (with new reasons "too far" / "not my skills" / "not interested"), repeated owner dismissals, and past interactions adjust future results, and no member can monopolise a results page. Group matching is now real — powered by the full group recommendation engine (shared memberships, interests, location, activity) with its own cache, surfaced to members on the Matches page and as a "Recommended groups" strip on the Groups page. When a community has AI enabled, embeddings become a true semantic matching signal (including profile-to-profile similarity) and members see a short AI-written "why this match" explanation on their top matches, generated in the background within existing per-tenant AI cost limits — with the algorithmic reasons always present as fallback. Two long-standing score corruption bugs were fixed along the way: the semantic/KNN boosts collapsed every boosted match to a score of 1/100, and the cache warmer wrote every entry as exactly 100.00 (analytics and hot-match notifications were running on garbage). The member experience was rebuilt to match: the Matches page gained distance/remote chips, mutual-match highlighting, a "why this score" breakdown, save/message/dismiss-with-reason actions and honest empty states; a new Match Preferences page (pause matching, distance, quality threshold, categories, notification frequency) replaces the dead settings redirect; the dashboard "Suggested for You" widget is now powered by the real engine; hot/mutual match emails deep-link to the highlighted match; and the admin panel gained gate/AI controls plus location-data-readiness, feedback-reason, and pillar analytics. Match approvals, safeguarding gates, and broker workflows are unchanged. (Deployed to production 2026-07-02; the per-tenant match cache was cleared post-deploy so the corrupted scores regenerate under the new engine.) The committed schema dump (database/schema/mysql-schema.sql) was refreshed from production to include the three new matching migrations (match_cache v2 columns, group_match_cache, match_preferences.matching_paused).

  • The admin sidebar's "Communications" section (Email Settings, Email Deliverability, Deliverability) is now visible only to super admins. The section — and, when it is the only surviving section in the "Communications" zone, the zone header itself — is gated on isSuperAdmin in AdminSidebar's nav config (matching the existing advertising/module_configuration conditional-spread pattern), so ordinary admins no longer see the platform email/deliverability links. isSuperAdmin covers the super_admin role, is_super_admin, is_tenant_super_admin, and god. The sibling Marketing/Advertising sections in the same zone are unchanged; on tenants that expose those, the zone header still renders for non-super-admins. No routes or endpoints changed — this is sidebar visibility only.

  • Build hygiene: restored the SPDX header on the generated react-frontend/src/resources.d.ts. The i18next-cli types generator rewrites this tracked file without an SPDX header, which trips the blocking SPDX License Compliance CI gate; the standard header is re-added (as scripts/add-spdx-headers.mjs does). No runtime effect — regenerating this file must always be followed by re-adding the header.

  • Content moderation and safeguarding configuration are now broker duties, living in the broker panel. The broker panel gained a Moderation section — Content Queue, Feed Posts, Comments, Reviews, and Reports — plus a Safeguarding Options page, each reusing the existing admin module component unchanged inside the broker shell (the same thin-wrapper pattern as the broker Safeguarding dashboard; no page rewrites). To let genuine broker/coordinator users operate them, the underlying endpoints were re-gated from admin-only to broker-or-admin: the content moderation queue (/v2/admin/moderation/queue + review/stats), feed post moderation (/v2/admin/feed/posts* + stats), comments, reviews, member reports (/v2/admin/reports index/stats/show/resolve/dismiss), and safeguarding options CRUD. Each mutating controller adds a self-dealing guard mirroring the match-approval and balance guards — a broker cannot moderate content they are a party to (their own feed post/comment, a review they wrote or received, their own queued content, or a report they filed / that targets them; content-target report ownership is not resolved and is documented as such), while admins retain full latitude. Mutations are throttled 60/min. Deliberately kept admin-only: moderation settings (tenant policy), feed announcer grant/revoke (privilege management), and the safeguarding statement/member-activity export (legal). New api.broker_cannot_moderate_own_content string across all 11 locales; broker.moderation_*/safeguarding_options UI strings in the broker namespace. Regression test: tests/Laravel/Feature/Controllers/BrokerModerationAuthorizationTest.php (19 cases). The existing admin controller tests (member-403 assertions) continue to pass unchanged.

  • CI no longer stacks stale runs or runs the whole pipeline for unrelated changes. Two structural fixes to cut the "CI queued for hours after a small change" problem: (1) every push/PR workflow (ci.yml, e2e-tests.yml, security-scan.yml, lighthouse.yml, docs-lint.yml, pr-checks.yml) now has a concurrency group keyed on ${{ github.workflow }}-${{ github.ref }} with cancel-in-progress: true, so a new push cancels its own superseded in-flight run on that ref instead of leaving orphaned runs to saturate the runner pool during an iterative fix session. (2) ci.yml gained a changes preflight (dorny/paths-filter, mirroring the existing i18n-changed job) that gates the expensive stages: a frontend-only change skips the sharded PHP suite + static analysis, a backend-only change skips the React build + API contract + E2E smoke + accessibility cluster, and a docs/CI-only change skips both plus the post-merge Docker image build. The php-checks aggregate gate now treats a skipped suite (no PHP-relevant paths touched) as a pass, and docker-verify tolerates skipped frontend jobs so a backend-only push still rebuilds/verifies the PHP image. workflow_dispatch still forces a full run. No test, gate, or coverage was weakened — unrelated jobs are skipped, not removed.

Removed

  • Removed the rubix/ml dependency, eliminating the last strong-copyleft (GPL) package from the production dependency tree. rubix/ml was an optional recommendation-training accelerator, used only behind a class_exists(\Rubix\ML\Kernels\Distance\Euclidean::class) guard in scripts/train_recommendations.php with an existing pure-PHP distance-kernel fallback — so removing it is behaviour-preserving (training already ran the pure-PHP path whenever Rubix wasn't installed). Dropping it also removes its transitive chain rubix/ml → wamania/php-stemmer → joomla/string (GPL-2.0-or-later) plus the amphp/* and andrewdalpino/okbloomer packages (production Composer packages 130 → 117). The whole tree is now permissive (MIT/BSD/Apache/ISC, with electable james-heinrich/getid3 → MPL-2.0 and non-copyleft react-leaflet Hippocratic-2.1). npm run check:licenses now passes with zero known-copyleft exceptions (KNOWN_EXCEPTIONS = {}), and THIRD_PARTY_NOTICES.md / THIRD_PARTY_LICENSES.md are refreshed. This clears the one dependency that would have blocked distributing Project NEXUS under a separate proprietary/closed-source licence. No app/ code referenced Rubix; the app boots and tests pass unchanged.
  • Removed SendGrid entirely — Postmark is now the sole platform email provider (owner decision; the SendGrid account was cancelled). The SENDGRID_API_KEY was already removed from the production environment; this removes the code. Deleted: the SendGrid event-webhook controller (SendGridWebhookController) and its public /webhooks/sendgrid/events route; the sendgrid:sync-suppressions and emails:reconcile-transient-failures cron commands + their scheduler entries (both were SendGrid-only — email_suppression is now fed solely by the Postmark webhook); the sendViaSendGrid() and usePlatformSendGridFallback() Mailer methods; the config/mail.php sendgrid block; and the SendGrid option in the admin Email Settings UI. The Mailer fallback chain is now Postmark → SMTP (was Postmark → SendGrid → SMTP), the platform-provider default is postmark, and a tenant still stored with email_provider = 'sendgrid' now falls through to the platform provider instead of activating a dead driver. The From-address helpers Postmark reuses were renamed rather than deleted (resolveSendGridFromPrefixresolveFromPrefix, SENDGRID_DOMAINSENDING_DOMAIN), and the deliverability dashboard's delivery-confirmation check now watches provider='postmark'. The email_log.provider enum keeps its historical 'sendgrid' value (no migration); the now-unused sendgrid/sendgrid Composer package is left in place pending a separate lockfile regeneration. Tests updated (MailerTest, EmailMailerRoutingTest) and the obsolete SendGrid command/webhook tests removed. No user-facing behaviour change — platform email already sends via Postmark.
  • Removed all /admin/federation/* pages, /admin/api-partners, and the caring hub's "Partner Cooperatives" link (owner-approved, no redirects). With the Partner Timebanks panel now the sole home for external-partner setup, the 16 admin federation routes and the Inbound API Partners route were deleted outright — routes, lazy imports, and both sidebar sections — following the same retirement pattern as safeguarding/moderation (a stale bookmark lands on the admin not-found page). The caring hub (/caring) also lost its Partner Cooperatives entry, since hasFullCaringAccess admits regular admins who must no longer see external-partner surfaces; the page itself lives on at /partner-timebanks/caring/peers, and the legacy /admin/caring-community/federation-peers redirect was retargeted there. In-app links that pointed at the old admin pages were retargeted to the panel: the ~37 cross-links inside the shared PartnerTimebankGuidance sidebar, the API-keys create/cancel navigation, the Enterprise system-config related-page link, and the module registry's federation detail link (which had pointed at a nonexistent /admin/federation/settings path — a pre-existing dead link, now fixed). The admin module components stay on disk and render inside the new panel's thin wrappers, so no functionality was lost. Orphaned admin_nav federation label keys remain in the locale bundles per the established cosmetic-cleanup precedent. No API endpoints were removed or re-gated.
  • Removed the "Recent" pages list from the admin sidebar. The admin sidebar previously tracked the last five admin pages a user visited (persisted in localStorage under admin_recent_pages) and rendered them under a "RECENT" heading above the navigation zones. The section added clutter to an already-dense sidebar, so it has been removed in full: the rendered list, the localStorage read/write helpers, the visit-tracking wired onto every nav link, and the label-lookup map that only fed it. Navigation, the needs-attention strip, and synonym-aware search are unchanged; stale localStorage entries are simply ignored. No API or route changes.
  • Removed the admin Content Queue, Moderation section, and Safeguarding pages (owner-approved, no redirects). Following the match-approvals tidy-up, the remaining broker/safety duplicates were removed from the admin panel outright: the /admin/moderation/queue|feed|comments|reviews|reports routes, /admin/safeguarding, and /admin/safeguarding-options, along with the admin sidebar's entire Moderation section and the Safeguarding trio (Safeguarding, Member Safeguarding, Safeguarding Options). The now-single-item "Matching & Safety" section was dissolved — Smart Matching moved into the Intelligence & Diagnostics section — and the admin sidebar's safeguarding flag-count poll (/v2/admin/safeguarding/dashboard) was removed. The admin module components stay on disk and are reused by the new broker pages, so no admin coverage was lost; only the admin routes, nav entries, and breadcrumb mappings are gone (a stale bookmark lands on the admin not-found page). Three in-app links that pointed at the old admin pages (Enterprise system config, Onboarding settings, Municipal impact "trust pack") now target the broker equivalents. No API endpoints were removed.
  • Removed the duplicate admin Match Approvals pages and the legacy /admin/broker-controls/* redirects (owner-approved, no redirects). With the broker panel now the sole home for every broker duty, /admin/match-approvals and /admin/match-approvals/:id were deleted outright — routes, the MatchApprovals/MatchDetail components and their test coverage, the admin sidebar entry, and the breadcrumb mappings. The Smart Matching overview's "Broker approvals" shortcut now points at /broker/match-approvals (which every admin tier can open, since the endpoints are broker-or-admin). The old /admin/broker-controls/broker bookmark redirects were also removed as part of the same tidy-up; a stale bookmark now lands on the admin not-found page. No API endpoints changed — only the duplicate admin UI surfaces are gone.
  • Retired the separate experimental Next.js public frontend (next-public-frontend/). The inert, shadow-only Next.js SSR app for public pages has been removed in full, along with all of its wiring: the next_public_frontend blue-green compose service (the next-public-shadow profile), the admin readiness endpoint GET /v2/admin/config/next-public-frontend plus its AdminNextPublicFrontendController and NextPublicFrontendReadinessService, the admin "Next.js public frontend readiness" page and its adminApi client method/types, the NEXT_PUBLIC_FRONTEND_ROUTING_ENABLED (config/app.php) cutover flag, the check:next-public:inert / check:next-public:dry-run scripts and their tests, the Apache foundation-canary example, and the cross-app @nexus/public-shared Vite alias with its shared-component source. Public pages continue to be served by the React/Vite SPA with the existing bot-only prerender engine for SEO — no production routing, prerender behaviour, or live API payload changed (the app was never enabled). The opt-in public_contract API contract (attached only for explicit include=public_contract / X-Public-Contract callers) is retained as harmless inert code; the now-unused advanced.next_public.* admin translation keys remain in the locale bundles pending a separate cosmetic i18n cleanup.

Fixed

  • The blue/green deploy no longer aborts at the "Required Env Var Check" gate now that SendGrid is gone. scripts/deploy/phases/validate-env.sh still listed SENDGRID_API_KEY and SENDGRID_WEBHOOK_VERIFICATION_KEY as mandatory, but SendGrid was fully removed in the Postmark-only cutover and the production .env has SENDGRID_API_KEY commented out — so every deploy failed at env validation before any build or migration ran (and because the "missing var" message is written to stderr, the detached deploy log showed only the gate header followed by the cleanup/notify lines, reading as a silent failure). The gate now requires POSTMARK_SERVER_TOKEN — the provider actually in use — instead of the two dead SendGrid keys, so the env check tracks the live email stack. Deploy-tooling only; no application behaviour changed.
  • Gamification feed cards ("Reached Level 2!", badge announcements) are now real per-member posts that admins can actually moderate. Every badge and level-up card was written into the feed with a literal source_id = 0; combined with the feed table's uniqueness key and upsert, ALL such cards in a community collapsed into a single shared row per type whose author, text, and date were silently overwritten by every subsequent award. The visible symptoms: gamification posts with a missing/blank author, and admin "Delete (Admin)" / broker Feed Posts delete unable to get rid of them — the card was served with id 0, and even a successful delete came back the next time anyone levelled up. Cards are now recorded with source_id = user_id (one card per member per type — a member's card updates in place to their latest badge/level, other members' cards are untouched), so attribution is stable and moderation surfaces can target them like any other feed item. A migration purges the legacy collapsed source_id = 0 rows (and any stray engagement rows keyed to id 0) on every tenant — this is what removes the two orphaned cards on the Timebanking UK feed. Also hardened the author display path end-to-end: the public feed's author hydration now falls back to first + last name when the users.name column is an empty string (the admin moderation list already did this), and the React feed no longer accepts an empty string as a valid author name or avatar (it falls back to the localized unknown-member label instead of rendering a nameless card with an initials-less avatar). Regression tests: tests/Laravel/Feature/GamificationFeedCardsTest.php (write path, per-user upsert, admin delete, empty-name fallback) and two new getAuthor cases in react-frontend/src/components/feed/types.test.ts.
  • Repaired the seven auto-generated federation/api-partners admin test files that had been silently broken since creation (19 failing tests plus two files that hung the vitest run). The tests stubbed shared admin components by mocking the '../../components' barrel, but the components under test import those files directly (../../components/PageHeader, ConfirmModal, StatCard, DataTable, EmptyState), so the stubs never intercepted and the real components rendered without the expected test ids. Every mock now targets the direct module path. The two hanging files (PartnerDirectory.test.tsx, ApiPartnersAdminPage.test.tsx) had a second defect: mocking all of @/components/ui with the @/test/uiMock proxy while also importing @/test/test-utils at the top level crashes the vitest fork worker (IPC "Channel closed") and the run waits forever at collect — they now use the partial importOriginal mock pattern of their sibling tests, stubbing only the React-Aria components that misbehave in jsdom (Select, Switch, useConfirm). One stale assertion was also fixed: the PartnerDirectory refresh test clicked the Refresh button while it was still isLoading-disabled during the debounced initial load. All 87 tests across the seven files now pass. Test-only change — no runtime behaviour affected.
  • The Partner Timebanks panel chrome (sidebar title, "Full Admin Panel" link, Caring Community section, breadcrumbs, Overview title, and the top-partners "{{count}} exchanges" counter) now renders translated in all 10 non-English locales. These panel-layout keys were added after the phase C translation pass and gap-filled with English verbatim, which also turned the non-admin i18n gap-regression CI gate red on main (+12 gaps per locale). Each value is translated using the terminology already established inside the same locale file (e.g. de "Partner-Zeitbanken", fr "Banques de temps partenaires", ga "Bainc Ama Comhpháirtithe"; "Caring Community" follows each locale's existing admin translation). The three "Credit Commons" keys stay verbatim in every locale by design — it is the proper name of the accounting protocol — and are now recorded in .github/i18n-gap-baseline.json as accepted cognates. Drift stays 0/0.
  • The broker Members page now shows real community counts and paginates (was "Total members: 1" for a 256-member community, with no way past the first rows). Every broker surface that counts members read the collection total from the wrong place: the api client unwraps the backend's { data, meta } envelope into res.data (the row array) and res.meta (the pagination meta), but the Members page KPI cards, the members table, the Onboarding pending-members list, and the broker sidebar's pending-members badge all looked for meta inside res.data — which never exists — and fell back to counting the rows the page happened to receive. The KPI cards run limit=1 count queries, so "Total members" and "Active members" rendered as 1 (verified against production: hOUR Timebank has 256 members, 255 active); the table's total equalled the visible page size, so DataTable computed one page and never rendered pagination; the sidebar badge was capped at 1. All four sites now read res.meta.total (row count kept only as a defensive fallback), restoring correct cards, working pagination, correct tab count chips, and an honest sidebar badge. The page also got its finishing pass: a role filter (All roles / Member / Broker / Admin / Tenant admin / Organisation admin — the list endpoint already supported ?role=, it was just never exposed in the UI), deep-linked ?status= navigation from the stat cards and dashboard tiles now resets pagination and bulk selection so a stale page number can't land on an empty result set, and the Refresh button's spinner now tracks either in-flight request rather than requiring both. Three new broker.json keys translated across all 11 locales (drift 0/0). Regression test: react-frontend/src/broker/pages/__tests__/MembersPage.pagination.test.tsx.
  • Removed two orphaned translation keys (stats_source_types, source_job) from all 10 non-English matches.json locale bundles. Their English source keys were removed in the member Matches page revamp (c592e2699), leaving the translated copies behind as "extra keys" warned about by scripts/check-i18n-drift.mjs. The keys were unreferenced, so nothing visible changes; the drift check is back to 0 missing / 0 extra.
  • Group recommendations no longer crash on a non-existent connections.addressee_id column. GroupRecommendationEngine::getConnectionGroups joined and filtered on c.addressee_id, but the connections table has receiver_id (not addressee_id), so the query threw SQLSTATE[42S22] Unknown column 'c.addressee_id' every time connection-based group suggestions were computed — the platform's highest-volume production error (~121 events/day). Both references now use c.receiver_id. Regression test: tests/Laravel/Feature/Matching/DistanceKmAndConnectionsSchemaRegressionTest.php.
  • Proximity search and the Smart Matching cache warm-up no longer fail with "Column 'distance_km' in ORDER BY is ambiguous". Four content/reference tables (users, categories, attributes, listing_attributes) carried a stray, always-NULL distance_km varchar(255) column added in error. Every proximity query computes (haversine) AS distance_km and JOINs one of those tables (typically users for the member's location), so the physical column collided with the computed alias and made ORDER BY distance_km / HAVING distance_km ambiguous (SQLSTATE[23000] 1052) — breaking the matching cache warm-up cron and the Explore / Events / Listings / Marketplace / Caring / Volunteer proximity searches. Migration 2026_07_02_000010_drop_stray_distance_km_columns drops the four stray columns (verified 100% NULL on production, with nothing writing to them); the legitimate match_cache / match_approvals / match_history distance_km decimal(8,2) columns are retained, and the committed schema dump was updated to match. Because this is a DROP COLUMN migration, the blue-green deploy must be run with DEPLOY_ALLOW_DESTRUCTIVE_MIGRATION=1. Regression test: tests/Laravel/Feature/Matching/DistanceKmAndConnectionsSchemaRegressionTest.php.
  • The admin sidebar's "Donations & Support" link (support-tier configuration) reappeared for tenant super admins. The member_premium block in AdminSidebar — the Donations & Support page (/admin/member-premium, where a tenant configures its monthly/yearly support tiers and Stripe sync) and the Recurring Supporters list (/admin/member-premium/subscribers) — was gated on isGod && hasFeature('member_premium') since c2f230855 (2026-06-05), when it was bundled with the genuinely platform-level Plans & Pricing / Billing links. 5d64eba68 (2026-06-29) then reworked that module from a paid "Member Premium" tier into a tenant-level support-donations feature but left the isGod gate in place — so any tenant super admin who is not a platform "god" lost the sidebar link even though the routes themselves are only feature-gated (FeatureGatedElement feature="member_premium", reachable by any tenant admin). The link was strictly more restrictive than the page it pointed to. The block is now gated on hasFeature('member_premium') alone, matching the route, so the Donations & Support and Recurring Supporters links return for tenant super admins with the feature enabled; the god-only Plans & Pricing / Billing links are unchanged. Also corrected two stale sidebar test assertions that still expected the pre-5d64eba68 labels "Member Premium" / "Premium Subscribers" (the live labels are "Donations & Support" / "Recurring Supporters"). Regression test: react-frontend/src/admin/components/__tests__/AdminSidebar.test.tsx (hides god-only Plans & Billing but shows Donations & Support to non-god super admins).
  • The broker panel now renders in each broker's own language instead of English placeholders (~255 untranslated strings per locale). When the broker panel was built and ported (0b38c7da4, 2026-07-01/02), its new broker.json keys were gap-filled with the English source into all ten non-English locales so the keys existed — which satisfies the key-existence drift gate (scripts/check-i18n-drift.mjs, still 0/0) and is not a mechanical "stub" (scripts/check-i18n-stubs.mjs) — but the values were still English. ~255 translatable strings per language (~2,530 total) sat in .github/i18n-gap-baseline.json as accepted gaps, so no CI gate ever flagged them and a German / French / … / Arabic broker saw an entirely English console. All ten locales (ga de fr it pt es nl pl ja ar) have now been translated in-context, matching each file's established terminology (e.g. fr Courtier / Vérification / Appariement / Annonce, de Broker / Prüfung / Überprüfung, ja ブローカー / 審査 / マッチ), preserving every {{token}} interpolation and _one/_other plural variant exactly, and keeping bare acronyms/symbols verbatim (DBS, ID, AccessNI, 2FA, the em-dash , {{km}} km). The two intentionally language-neutral glyph hints (⌘K, esc) remain identical across locales by design. .github/i18n-gap-baseline.json was refreshed to lock in the win (non-admin English-fallback gaps 4009 → 1482; broker gaps ~2,530 → ~28 residual cognates). Value-only change — no key added, removed, or reordered; drift stays 0/0 with no new stubs. Each locale was then adversarially re-reviewed by a native-speaker pass, which corrected a further set of machine-translation defects — most notably a full Irish (ga) QA sweep (an invented non-word for "Follow up", a calqued/aircraft-"landing" empty-state, a meaning-inverted dashboard heading, and several pre-existing mistranslations such as dashboard.description and dashboard.view_all), German/Spanish bare day abbreviations ({{count}} T / {{count}} d{{count}} Tg. / {{count}} días, applied across the whole time-ago/expiry class), and French/Spanish "message copy" false-friends (copie de message / copia de mensaje).
  • The broker ⌘K / esc keyboard hints were hardcoded, turning the i18n literals CI gate red on main. The broker header search affordance (⌘K) and the command-palette close hint (esc) shipped in 0b38c7da4 as bare JSX text inside <Kbd>, which the non-admin hardcoded-string gate (scripts/check-i18n-literals.mjs) counts as untranslated literals. Both are now routed through t('broker') (header.search_shortcut, palette.close_key), with the language-neutral glyph value mirrored across all 11 locales and the i18n gap baseline refreshed for the two parity keys. No visible change — the glyphs render identically; this only satisfies the translation-coverage gate.
  • Match-approval statistics were always zero, and matches pending longer than 30 days vanished from every counter. Two latent defects in MatchApprovalWorkflowService::getStatistics(), caught during the broker-panel visual pass: (1) the service returned keys (pending/approved/rejected/avg_review_hours) that neither the admin nor the broker Match Approvals UI ever read — both consume the MatchApprovalStats shape (pending_count/approved_count/rejected_count/avg_approval_time), so the stat cards, tab chips, and the new broker sidebar badge rendered 0 forever; the service now returns both key sets. (2) The pending count was windowed by submitted_at >= N days, so a match awaiting review for more than 30 days silently disappeared from the queue counters (while still listed) — exactly the items most in need of attention; pending is now counted un-windowed as a live queue, while approved/rejected/rate remain period activity metrics. Regression test: BrokerMatchApprovalAuthorizationTest::test_approval_stats_count_old_pending_matches_and_use_ui_field_names.
  • Brokers cannot adjust their own time balance (self-dealing guard). Balance adjustment is now broker-accessible (from the member-detail modal), but AdminTimebankingController::adjustBalance rejects any attempt by a broker/coordinator to adjust their own account (403) so they can't mint themselves time credits; admins retain full latitude, and all adjustments remain audited. Covered by BrokerUserActionsAuthorizationTest.
  • Brokers can now edit an active monitoring record and act on an exchange from its detail page. Two gaps in the broker panel: (1) the User Monitoring page was add-or-remove only, so changing a monitored member's reason, messaging-disabled toggle, or expiry meant removing and re-adding the record; it now has an Edit action that reuses the upserting setMonitoring endpoint (frontend-only) and preserves the record's remaining expiry when the broker doesn't pick a new duration, so a reason-only edit never silently clears the expiry. (2) The Exchange Detail page was read-only, forcing brokers back to the list to approve/reject a pending_broker exchange; it now carries the same Approve/Reject action modal as the list and refreshes in place. The existing page test suites (ExchangeDetailPage.test.tsx, UserMonitoringPage.test.tsx) continue to pass and were updated for the new toast dependency.
  • The broker panel no longer locks brokers out of safeguarding/vetting/members on tenants without the exchange feature. BrokerRoute gated the entire /broker panel on the exchange_workflow tenant feature, so a broker (or coordinator) on a community that runs safeguarding, vetting, insurance, member-approval and monitoring but does not use the exchange-approval workflow was redirected away from every broker tool. Access is now role-based only (hasBrokerPanelAccess); the exchange_workflow feature gates just the exchange-specific surfaces — the Exchanges sidebar item (BrokerSidebar), the /broker/exchanges* routes (a new ExchangeFeatureRoute guard bounces deep-links back to the broker dashboard), and the Exchange-Workflow card on the broker Configuration page. Regression tests: react-frontend/src/broker/BrokerRoute.test.tsx, react-frontend/src/broker/__tests__/BrokerSidebar.test.tsx.
  • The "Delete My Account" button no longer stays permanently disabled (GDPR Article 17 erasure was unreachable for some users). Two independent defects blocked the account-deletion confirmation modal (react-frontend/src/pages/settings/SettingsPage.tsx). (1) The "Type DELETE to confirm" text field had no autofill suppression, so Chrome / password managers filled it with the account email — the field sits directly above the current-password input and browsers treat the nearest preceding text field as the username slot — and that value can never equal the required keyword, leaving the delete button greyed out forever. (2) The code required the literal English DELETE while the Spanish UI instructed "Escribe ELIMINAR para confirmar" and French showed the placeholder "SUPPRIMER", so members in those locales could never satisfy the gate by following the on-screen instruction. The confirmation input is now an autofill-proof DeleteConfirmationField component that renders read-only until the user actually focuses/clicks it — Chrome ignores autoComplete="off" for a text field next to a password input but never autofills read-only fields, so the account email can no longer be injected on open (with autoComplete="off" + a non-credential name as belt-and-braces); the modal auto-focuses its close button, so the read-only guard is only lifted on genuine user interaction. The keyword match is extracted into react-frontend/src/pages/settings/deleteConfirmation.ts (isDeleteConfirmed), which is case-insensitive, whitespace-trimmed, and accepts either the canonical DELETE or the localized keyword the UI actually showed the user (so a re-localized placeholder can never silently lock members out again); and the es/fr settings.json delete_modal strings were realigned to DELETE for consistency with the other nine locales. The backend password re-authentication remains the real safety gate; only the reachability of the confirm button changed. Regression tests: react-frontend/src/pages/settings/deleteConfirmation.test.ts, react-frontend/src/pages/settings/DeleteConfirmationField.test.tsx.
  • Postmark email delivery/open/bounce status is now recorded in email_log. The email_log.provider column was enum('sendgrid','gmail_api','smtp'); once platform mail routes through Postmark, Mailer::logEmail() writes provider='postmark', which MySQL/MariaDB non-strict mode silently coerced to '' — breaking PostmarkWebhookController row matching (WHERE provider='postmark') and therefore the delivery/open/bounce status updates on email_log. A migration adds 'postmark' to the enum. Sending itself and bounce-driven suppression were unaffected. The committed schema dump (database/schema/mysql-schema.sql) was refreshed from production to match.
  • The email-trigger audit matrix now registers the donation and GDPR admin notifications. EmailTriggerAuditService::eventMatrix() was missing rows for the donation_admin (tenant-admin "donation received" alert) and admin_gdpr_action (tenant-admin "member initiated a GDPR action" alert) categories, so the EmailTriggerAuditServiceTest::test_dispatcher_categories_are_represented_in_audit_matrix guard failed on main. Both categories are now covered, restoring a complete email-trigger audit surface. No user-facing behaviour change.
  • Container image builds no longer fail on transient pecl.php.net / package-mirror outages. CI's "Docker Build Verify" and production blue-green image builds repeatedly broke on an unchanged RUN pecl install redis line when pecl.php.net returned 504 Gateway Timeout or served truncated tarballs. Every pecl install (redis, pcov) in Dockerfile, Dockerfile.prod, and Dockerfile.bluegreen is now wrapped in a retry-with-backoff loop that clears the stale download dir between attempts and only fails the build after 5 genuine failures; composer install in the blue-green image retries similarly, and all apt-get update/upgrade/install calls now pass -o Acquire::Retries=3. Verified by an end-to-end build of the hardened layer (php -m confirms the redis extension loads). PHP ini parity between Dockerfile and Dockerfile.prod is unchanged (scripts/check-dockerfile-drift.sh still passes).
  • Online members no longer show as "offline" in messages and across the platform. The React app keeps presence alive solely through the 60-second /v2/presence/heartbeat, but the is_online/online_status shown in conversations, member profiles, group chats, and the feed sidebar were derived from users.last_active_at — a column only ever refreshed by the unused /auth/heartbeat endpoint. As a result last_active_at went stale ~5 minutes into every session and actively-browsing members read as offline everywhere that column is consulted, regardless of the status they set. PresenceService::heartbeat() now also refreshes users.last_active_at on its existing once-per-60s, tenant-scoped DB-write path (guarded so a users write can never abort the presence write), so every last_active_at-derived online indicator becomes accurate again in one place. The 1:1 conversation header dot/label additionally now read live canonical presence via PresenceContext (consistent with the conversation list) and fall back to the now-fresh is_online. Regression tests: tests/Laravel/Unit/Services/PresenceServiceTest.php (test_heartbeat_bridges_users_last_active_at, test_heartbeat_bridge_is_tenant_scoped).
  • The safeguarding restriction notice now appears as soon as a conversation opens — before the member types — without alerting staff on page load. When a member opens a conversation with someone they cannot contact directly (coordinator-mediated contact or required vetting), the conversation payload now carries a server-authoritative safeguarding preflight block, so the upgraded safeguarding panel renders immediately and the composer is replaced with an explanation. Opening a conversation no longer emails or notifies brokers/admins; only an actual blocked send attempt or the new explicit action does. A new "Request coordinator help" button in the panel calls POST /api/v2/messages/{id}/request-coordinator, which re-checks the restriction server-side, dispatches a dedicated SafeguardingCoordinationRequested event (email + bell alert to active admin/tenant_admin/broker/super_admin staff in each recipient's locale), writes an activity_log audit row, is idempotent within a 10-minute window, and confirms success to the sender. The existing blocked-send gate and its SafeguardingContactAttemptBlocked alert are unchanged; the gate logic is now shared (MessageService::evaluateSafeguardingContactGate) so the preflight notice, the send block, and the coordinator request can never diverge. Two safeguarding.php detail strings that previously claimed "the team has been alerted" / "your message has not been sent" were made context-neutral so they are accurate on page load too. Regression tests: tests/Laravel/Feature/Controllers/MessagesControllerTest.php, tests/Laravel/Unit/Listeners/NotifySafeguardingCoordinationRequestedTest.php, react-frontend/src/pages/messages/ConversationPage.test.tsx.
  • PHP CI shards now fail with actionable timing diagnostics instead of abrupt runner cancellations. The PHPUnit shard job timeout has been raised to 45 minutes while each PHPUnit invocation has a 40-minute guard, shard file list, elapsed-time summary, and explicit timeout error so slow-shard drift is visible before GitHub cancels the job.
  • Safeguarding message blocks now enforce coordinator-mediated contact and alert tenant staff. Provider-side safeguarding declarations that only record a required vetting type no longer over-block member messaging, while true vetted-contact and coordinator-mediated-contact blocks now return structured guidance, lock the message composer behind an in-thread safeguarding panel, and dispatch email plus bell alerts to active tenant admin, tenant_admin, and broker users for follow-up. Regression tests: tests/Laravel/Feature/Controllers/MessagesControllerTest.php, tests/Laravel/Unit/Listeners/NotifySafeguardingContactAttemptBlockedTest.php, tests/Laravel/Unit/Services/SafeguardingTriggerServiceTest.php, tests/Laravel/Feature/I18n/EmailLocaleIntegrationTest.php, react-frontend/src/pages/messages/ConversationPage.test.tsx.
  • Listings public contracts are now opt-in so live API payloads stay unchanged. The public_contract block on /api/v2/listings and /api/v2/listings/{id} is only attached for explicit Next public-frontend callers using include=public_contract or X-Public-Contract: 1; normal SPA, mobile, and prerender reads retain their prior response shape. Regression tests: tests/Laravel/Feature/Controllers/ListingsControllerTest.php, next-public-frontend/src/lib/__tests__/tenant-api.test.ts.
  • i18n: propagated 72 new advanced.next_public.* admin translation keys to all 10 non-English locales. The next-public workstream added these keys to en/admin.json without backfilling non-EN files, breaking the translation drift CI gate. English placeholder values have been added to ar, de, es, fr, ga, it, ja, nl, pl, and pt pending proper translation.
  • i18n: translated the safeguarding.errors.vetting_check_failed string into the 9 non-English locales where it was leaking verbatim English. The key existed in ar, de, es, fr, it, ja, nl, pl, and pt safeguarding.php but held the untranslated English source (only ga already had a real translation). Proper in-language translations were added, matching the tone of the adjacent safeguarding error strings (e.g. coordination_request_failed). Key parity across all locales is unchanged (62 keys each).

Added

  • The broker panel now has Match Approvals pages, and its frame was upgraded to the new broker design language. /broker/match-approvals (queue) and /broker/match-approvals/:id (detail) port the admin matching module into the broker panel restyled on the new broker primitives: severity-colored stat cards, a deep-linkable status filter (?status=pending|approved|rejected|all), a score gauge with quality label, party/listing cards, review-details card, and approve / reject-with-reason actions (the backend self-dealing guard's message surfaces in the toast when a broker tries to review their own match). Both routes and the new sidebar item are gated on the exchange_workflow feature exactly like Exchanges, and the sidebar item carries a live pending-count badge fed by the (now broker-accessible) approval-stats endpoint. Frame polish: sidebar gets an active-rail indicator, brand tile, and hover micro-interactions; the header gains a Help Center shortcut. All strings live in the broker.matching.* namespace. Regression tests: MatchApprovalsPage.test.tsx, MatchApprovalDetailPage.test.tsx, updated BrokerSidebar/BrokerLayout suites.
  • Brokers can now review smart-match approvals without the admin panel — with a self-dealing guard. The five match-approval endpoints (/v2/admin/matching/approvals list/detail/stats and the approve/reject mutations) moved from admin-only to broker-or-admin in routes/api.php, since reviewing proposed member↔listing matches is a core broker duty; matching configuration, cache, and analytics stay admin-only. A new AdminMatchingController::guardBrokerNotParty() check rejects (403) any broker/coordinator attempt to approve or reject a match they are a party to — either as the matched member or as the listing owner — mirroring the adjust-balance self-dealing guard; admins retain full latitude. Approve/reject decisions are now also written to the org_audit_log (match_approved / match_rejected) and throttled to 60/min like the other broker decision endpoints. A new api.broker_cannot_review_own_match string was added across all 11 locales. Regression test: tests/Laravel/Feature/Controllers/BrokerMatchApprovalAuthorizationTest.php (15 cases pinning what brokers can and cannot do).
  • The broker Onboarding page now surfaces at-a-glance KPI cards above the funnel. Four metric cards — total Registered, Overall conversion % (first stage → last), Pending approvals, and the biggest drop-off stage with its conversion rate — are derived from the existing funnel + pending data (no new endpoint). (A month-over-month registration trend chart was left out for now because the broker funnel endpoint returns stage counts only, not a time series.) New KPI labels added across the 11 locales.
  • Broker member notes now support the full CRM note workflow instead of being add-only. When adding a note a broker can pick a category (general / outreach / support / onboarding / concern / follow-up) rather than every note being silently filed as broker, and each existing note can be edited, deleted, or pinned (pinned notes sort to the top). Backed by the existing adminCrm.updateNote / deleteNote endpoints. New note labels added across the 11 locales.
  • Brokers get a full member-detail view with the operational actions inline, so they no longer need the admin User-edit page. A new "View details" action on each member opens a modal (react-frontend/src/broker/components/MemberDetailModal.tsx) showing the member's balance, join date, last-active, onboarding state, and vetting/insurance/consent status, plus the operational action set: approve / suspend / reactivate, resend verification email, send password-reset email, reset 2FA, adjust time balance (amount + reason, in a confirmation sub-modal), and a safe-edit form for name / phone / bio / tagline / location. Privileged actions (role/status change, ban, delete, impersonate) are deliberately absent from the UI and rejected server-side for brokers. Smoke-tested in MemberDetailModal.test.tsx (loads member, exposes the operational actions, hides privileged ones, and drives a balance adjustment). All strings added across the 11 locales.
  • The broker Members page gained the admin-Users triage tools brokers were missing: role & verification visibility, stuck-member filters, and bulk approval. It now shows a Role column and an Email verified / unverified column, adds "Never logged in" and "Onboarding incomplete" status tabs so brokers can triage members who are stuck, and supports multi-select bulk Approve / Suspend (wired to the now-broker-accessible bulk-approve / bulk-suspend endpoints) via a selection action bar. All new labels are translated across the 11 locales.
  • Brokers can now perform the operational member-management actions that previously required the full admin panel — with hard guards against privilege escalation. So a broker/coordinator never has to leave /broker to do their job, the member endpoints the broker panel needs were moved from the admin-only group into broker-or-admin in routes/api.php: bulk approve/suspend, 2FA reset, verification / password-reset / welcome emails, a read-only GDPR-consent view, safe profile edits (PUT /v2/admin/users/{id}), and (audited) time-balance adjustment (/v2/admin/timebanking/adjust-balance). Privilege escalation is blocked in depth: AdminUsersController@update now rejects any broker attempt to change role, status, email, profile_type, or organization_name (403); a new BaseApiController::callerIsAdminTier() distinguishes brokers from admins; securityTier() places brokers above ordinary members but below admins, so a broker can reset a member's 2FA/password but never touch another broker or an admin; and creating/importing users, banning, deleting, badge management, direct password set, impersonation, and super-admin promotion all remain admin-only or super-admin-only. Brokers stay tenant-scoped and every action is audited with the actor id. A new api.broker_cannot_edit_field string was added across all 11 locales. Regression test: tests/Laravel/Feature/Controllers/BrokerUserActionsAuthorizationTest.php (15 cases pinning what brokers can and cannot do).
  • Tenant admins are now notified the moment a member initiates a GDPR action. Previously, when a member used the Privacy settings to submit a data-rights request (access/erasure/rectification/restriction/objection/portability), delete their account, export their personal data, or change a consent preference, admins were told nothing — the request became a silent pending row surfaced only by a dashboard count nobody had to open, or by the overdue-request cron once it was already ~25 days old (near the Art.12(3) deadline). A new GdprActionOccurred event is now dispatched from the member entry points (GdprService::createRequest and updateUserConsent, UsersController::deleteAccount, MemberDataExportController::create), and a NotifyAdminOfGdprAction listener fans out a bell notification, a push, and an email — each rendered in the recipient admin's own preferred_language via LocaleContext — to every active super_admin/admin/tenant_admin, linking to the admin GDPR queue. Data-rights requests are flagged as action-needed within 30 days; deletion/export/consent are informational. Admin-created requests (a different code path) deliberately do not fire this. Every dispatch is failure-isolated so notification wiring can never fail the member's action, and the fan-out is idempotent against queue re-delivery. New emails_misc.admin_notify.gdpr_* strings and notifications.push_gdpr_* titles were added across all 11 locales. Regression test: tests/Laravel/Feature/Gdpr/GdprAdminNotificationTest.php.
  • Postmark delivery, bounce, and spam-complaint events are now ingested via a webhook. A new PostmarkWebhookController (POST /api/v2/webhooks/postmark) mirrors the SendGrid event handler for the Postmark send path: it authenticates via HTTP Basic auth (or an X-Postmark-Webhook-Secret header) against POSTMARK_WEBHOOK_SECRET, then updates email_log (delivered/bounced/opened/status, matched on Postmark's MessageID), maintains the email_suppression cache the Mailer consults before every send, records NewsletterBounce rows (hard/soft/complaint), and feeds EmailMonitorService. Tenant is resolved from the Postmark Metadata.tenant_id we attach at send time. Reuses existing api.* strings (no new translation keys). Inert until the webhook is configured in Postmark and POSTMARK_WEBHOOK_SECRET is set.
  • Platform email can now be routed through Postmark instead of SendGrid (opt-in send path). A new postmark driver in App\Core\Mailer sends via the Postmark Email API (raw HTTP, no SDK dependency) and is selected only when MAIL_PLATFORM_PROVIDER=postmark and POSTMARK_SERVER_TOKEN are set — otherwise sending behaviour is completely unchanged (SendGrid/SMTP). It reuses the existing category→From-prefix mapping (so every per-category sender address on the verified project-nexus.net domain carries over) and the existing per-tenant sender-name resolution, and it routes newsletter/digest categories to the Postmark broadcast message stream and everything else to the transactional stream (independent IP reputation). A Postmark send failure falls back automatically to the platform SendGrid account, then SMTP. Adds mail.platform_provider and mail.postmark.* config. No production behaviour changes until the env flag is flipped; the Postmark delivery/bounce/spam-complaint event webhook is tracked as a separate follow-up.
  • Tenant admins are now notified when a monetary donation is received. Previously a successful Stripe donation emailed only the donor a receipt — organisation admins got nothing, so donations could arrive completely unannounced. A new DonationAdminNotificationService (modelled on SupportReportNotificationService) now fires from the Stripe webhook success path (StripeDonationService::handlePaymentSucceeded), but only on a donation's first completion so re-delivered webhook events never re-notify. It sends every active tenant admin (admin/tenant_admin/super_admin/god or the is_*admin flags) an in-app bell notification plus an email — each rendered in that admin's own preferred_language via LocaleContext::withLocale — showing the amount, donor, fund, message, and date with a button to the admin donations area. Anonymous donations still show the donor's name to admins (needed for reconciliation/Gift Aid) but flag the anonymous intent so it isn't surfaced publicly. Every step is failure-isolated: a bounced admin email can never fail the webhook (which would make Stripe retry the event for days). New emails.donation_admin.* strings were added across all 11 locales. Regression test: tests/Laravel/Unit/Services/DonationAdminNotificationServiceTest.php.
  • The admin header now links to the Support Reports area with a live open-request indicator. A new support (life-buoy) icon button sits beside the notification bell in the admin top bar and navigates to /admin/support-reports. On mount it fetches GET /v2/admin/support-reports/stats and, when there are open requests, overlays a small red count badge (clamped to 99+); the badge is hidden at zero. The fetch is best-effort — if the stats endpoint is unavailable for the current admin the badge simply stays hidden — and the button's aria-label announces the open count for screen readers. Two new admin_nav translation keys (support_requests, support_requests_count) were added across all 11 locales. Regression tests: react-frontend/src/admin/components/AdminHeader.test.tsx.
  • The shadow Next.js public frontend now ports the React public visual system instead of approximating it. The inert Next app maps HeroUI v3 to the NEXUS brand tokens, loads the React-aligned Inter typography, carries React-style public chrome markers, renders listings/events/jobs/marketplace/organisations with dedicated SSR card/detail layouts, moves static/blog/CMS/legal content into the shared .legal-content prose container, and aligns home/jobs/about/help H1 copy with the live React baseline. The post-port live-vs-shadow screenshot matrix is recorded in next-public-frontend/visual-parity-checklist.md; production routing, React/Vite serving, Apache/Plesk config, prerender serving, the live sitemap, and cutover flags remain unchanged. Regression tests: next-public-frontend/src/lib/__tests__/design-system-foundation.test.ts, next-public-frontend/src/ui/__tests__/PublicPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicListingsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicEventsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicJobsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicMarketplacePage.test.tsx, next-public-frontend/src/ui/__tests__/PublicOrganisationsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx.
  • The shadow Next.js public frontend now has a committed visual parity and performance checklist. The readiness artifact records the live-vs-shadow comparison matrix for hour-timebank, captures the current H1 and visual-delta gaps, documents why the local React dev surface could not be used as the baseline during this pass, and records the image/caching posture required before a future cutover. This remains shadow-only and does not change production routing, React/Vite serving, Apache/Plesk routing, the live sitemap, or prerender serving.
  • The shadow Next.js public frontend now has locale-aware SEO metadata and intrinsic public media rendering. Public routes now emit all 11 locale hreflang alternates plus x-default, OpenGraph locale, tenant fallback OG/Twitter images, tenant-default <html lang> with RTL direction for Arabic, single-H1 no-JavaScript checks, and Next image rendering with intrinsic dimensions for logos, cards, hero media, and galleries. This remains shadow-only and does not change production routing, React/Vite serving, Apache/Plesk routing, the live sitemap, or prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/metadata.test.ts, next-public-frontend/src/lib/__tests__/listings-seo.test.ts, next-public-frontend/src/lib/__tests__/design-system-foundation.test.ts, next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx.
  • The shadow Next.js public frontend static, blog, CMS, and generic public content renderers now use HeroUI v3. Home support content, fallback static/legal pages, blog index/detail, CMS pages, and generic public collection/detail content now share the same SSR-safe HeroUI Card, Chip, and Link primitives as the rich route families, removing the remaining bespoke public-panel and post-list rendering from PublicPage.tsx. This remains shadow-only and does not change production routing, React/Vite serving, Apache/Plesk routing, the live sitemap, or prerender serving. Regression tests: next-public-frontend/src/ui/__tests__/PublicPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx.
  • The shadow Next.js public frontend rich dynamic route families now render through HeroUI v3. Listings, events, jobs, marketplace items, and organisations now share SSR-safe HeroUI Card, Chip, and Link route primitives for index cards, detail layouts, media galleries, breadcrumbs, facts, and empty states while preserving existing public contracts, module gating, SEO JSON-LD, and no-JavaScript rendering. This remains shadow-only and does not change production routing, React/Vite serving, Apache/Plesk routing, the live sitemap, or prerender serving. Regression tests: next-public-frontend/src/ui/__tests__/PublicListingsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicEventsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicJobsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicMarketplacePage.test.tsx, next-public-frontend/src/ui/__tests__/PublicOrganisationsPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx.
  • The shadow Next.js public frontend shared chrome now renders through HeroUI v3 components. The isolated SSR shell now uses HeroUI Surface, Card, Chip, and Link primitives with Tailwind v4 utility layout for the tenant header, public navigation, home hero, call-to-action links, and AGPL footer while preserving crawler-readable no-JavaScript HTML. This remains shadow-only and does not change production routing, React/Vite serving, Apache/Plesk routing, the live sitemap, or prerender serving. Regression tests: next-public-frontend/src/ui/__tests__/PublicPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx.
  • The shadow Next.js public frontend now has a HeroUI v3 and Tailwind CSS 4 design-system foundation. The isolated Next app imports HeroUI styles through Tailwind v4, pins the React-aligned HeroUI/Tailwind versions, mirrors the shared token/glass styling sources, and retires the bespoke route/card globals before the HeroUI component re-skin. This remains shadow-only and does not change production routing, React/Vite serving, Apache/Plesk routing, the live sitemap, or prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/design-system-foundation.test.ts.
  • The shadow Next.js public frontend now has React-aligned shared public chrome. The isolated SSR shell expands public navigation for listings, events, jobs, marketplace, organisations, resources, blog, help, and contact; renders a tenant-token home hero with a no-JavaScript brand panel; and upgrades the footer into platform/support/legal link groups while preserving AGPL Section 7(b) attribution. This remains shadow-only and does not change production routing or prerender serving. Regression tests: next-public-frontend/src/ui/__tests__/PublicPage.test.tsx, next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx, next-public-frontend/src/lib/__tests__/public-messages-validation.test.ts.
  • The shadow Next.js public frontend message catalog now covers all 11 platform locales. The isolated Next public app registers en, ga, de, fr, it, pt, es, nl, pl, ja, and ar, expands every public chrome key across those locale files, and adds a parity test so future route-family keys cannot drift before any canary or cutover. This remains shadow-only and does not change production routing or prerender serving. Regression test: next-public-frontend/src/lib/__tests__/public-messages-validation.test.ts.
  • The shadow Next.js listings route now has a locked rich public contract and real SSR renderer. /api/v2/listings and /api/v2/listings/{id} add a public-only public_contract object with card/detail fields, neutral location, provider, images/gallery, time-credit value, status, dates, and pagination metadata while preserving existing SPA fields. The inert Next public frontend now uses typed listings fetchers, tenant locale forwarding, module gating, no-JavaScript index/detail rendering, OpenGraph/Twitter metadata, and schema.org JSON-LD for listings only. This remains shadow-only and does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/ListingsControllerTest.php, next-public-frontend/src/lib/__tests__/tenant-api.test.ts, next-public-frontend/src/ui/__tests__/PublicListingsPage.test.tsx, next-public-frontend/src/lib/__tests__/listings-seo.test.ts, next-public-frontend/src/lib/__tests__/module-gates.test.ts.
  • The shadow Next.js events route now follows the listings rich-route contract pattern. /api/v2/events and /api/v2/events/{id} keep existing SPA/prerender payloads unchanged unless explicit Next callers opt in to public_contract, then expose public card/detail fields for image, category, neutral location, organiser, date range, status, and dates. The inert Next public frontend now has typed event fetchers, events module gating, no-JavaScript index/detail rendering, OpenGraph/Twitter metadata, schema.org/Event JSON-LD, and en/ga chrome keys for events only. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/EventsControllerTest.php, next-public-frontend/src/lib/__tests__/tenant-api.test.ts, next-public-frontend/src/ui/__tests__/PublicEventsPage.test.tsx, next-public-frontend/src/lib/__tests__/events-seo.test.ts, next-public-frontend/src/lib/__tests__/module-gates.test.ts.
  • The shadow Next.js jobs route now has an opt-in public contract and JobPosting renderer. /api/v2/jobs and /api/v2/jobs/{id} keep existing SPA/prerender payloads unchanged unless explicit Next callers opt in to public_contract, then expose public job fields for employer, logo/gallery, neutral location, compensation, skills, deadline, status, and dates without private contact/application data. The inert Next public frontend now has typed jobs fetchers, job_vacancies module gating, no-JavaScript index/detail rendering, OpenGraph/Twitter metadata, schema.org/JobPosting JSON-LD, and en/ga chrome keys for jobs only. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/JobVacanciesControllerTest.php, next-public-frontend/src/lib/__tests__/tenant-api.test.ts, next-public-frontend/src/ui/__tests__/PublicJobsPage.test.tsx, next-public-frontend/src/lib/__tests__/jobs-seo.test.ts, next-public-frontend/src/lib/__tests__/module-gates.test.ts.
  • The shadow Next.js marketplace route now has an opt-in Product/Offer public contract. /api/v2/marketplace/listings and /api/v2/marketplace/listings/{id} keep existing SPA/prerender/mobile payloads unchanged unless explicit Next callers opt in to public_contract, then expose public marketplace fields for item images/gallery, category, neutral location, price, seller display, delivery, condition, quantity, expiry, status, and dates without private contact/order data. The inert Next public frontend now has typed marketplace fetchers, marketplace module gating, no-JavaScript index/detail rendering, OpenGraph/Twitter metadata, schema.org/Product + Offer JSON-LD, and en/ga chrome keys for marketplace only. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/MarketplaceListingControllerTest.php, next-public-frontend/src/lib/__tests__/tenant-api.test.ts, next-public-frontend/src/ui/__tests__/PublicMarketplacePage.test.tsx, next-public-frontend/src/lib/__tests__/marketplace-seo.test.ts, next-public-frontend/src/lib/__tests__/module-gates.test.ts.
  • The shadow Next.js organisations route now has an opt-in Organization public contract. /api/v2/volunteering/organisations and /api/v2/volunteering/organisations/{id} keep existing SPA/prerender/mobile payloads unchanged unless explicit Next callers opt in to public_contract, then expose public organisation fields for logo, website, public contact email, owner display, aggregate stats, type, status, and dates while keeping wallet fields out of public contracts. The inert Next public frontend now has typed organisation fetchers, organisations module gating, no-JavaScript index/detail rendering, OpenGraph/Twitter metadata, schema.org/Organization JSON-LD, and en/ga chrome keys for organisations only. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/VolunteerControllerTest.php, next-public-frontend/src/lib/__tests__/tenant-api.test.ts, next-public-frontend/src/ui/__tests__/PublicOrganisationsPage.test.tsx, next-public-frontend/src/lib/__tests__/organisations-seo.test.ts, next-public-frontend/src/lib/__tests__/module-gates.test.ts.
  • The shadow Next.js app now has inert sitemap and robots metadata routes. next-public-frontend/app/sitemap.ts builds sitemap entries from the shadow route-ownership manifest plus opted-in tenant public content for listings, events, jobs, marketplace items, and organisations, while app/robots.ts points crawlers at the shadow sitemap. These routes exist only inside the isolated Next app and do not alter the live Laravel sitemap, Apache/Plesk serving, prerender engine, or any cutover flag. Regression test: next-public-frontend/src/lib/__tests__/public-sitemap.test.ts.
  • The former member-premium module is now presented as community donations and support. The member-facing page is labelled "Donate" with the subtitle "Support this community", adds a one-off donation CTA using the existing Stripe donation checkout, and reframes recurring monthly/yearly tiers as support levels with recognition rather than paid feature access. Admin navigation and module configuration now show "Donations & Support" and link directly to the support admin page. Donation Stripe routing can now be configured per tenant with Stripe Connect onboarding/status controls; when unset, payments continue through the platform Stripe account with tenant metadata for reporting. One-off donation rows and recurring support subscriptions store the original payment route and Stripe account for later refunds, cancellation, billing-portal access, admin reporting, and CSV export. Regression tests: react-frontend/src/pages/premium/PricingPage.test.tsx, react-frontend/src/admin/modules/premium/MemberPremiumAdminPage.test.tsx, react-frontend/src/admin/modules/volunteering/DonationRefunds.test.tsx, react-frontend/src/admin/modules/config/moduleRegistry.test.ts, tests/Laravel/Unit/Services/DonationStripeAccountServiceTest.php, tests/Laravel/Unit/Services/StripeDonationServiceTest.php, tests/Laravel/Unit/Services/MemberPremiumServiceTest.php, tests/Laravel/Unit/Models/VolDonationTest.php.
  • Admins can inspect the shadow-mode Next.js public frontend readiness without enabling it. A new read-only admin API endpoint and /admin/seo/next-public-frontend page report the Next app package, route ownership manifest, shadow runtime commands, retained Vite private-route ownership, unchanged prerender fallback, and remaining cutover blockers. This does not activate production routing, does not alter Apache/Plesk edge behavior, and does not change the current prerender serving path. Regression tests: tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx, react-frontend/src/admin/api/adminApi.test.ts.
  • The Next.js public frontend readiness page now validates the shadow route manifest. The read-only admin endpoint checks for the pinned lockfile, required package scripts, configured shadow Compose profile, duplicate public route patterns/keys, malformed route entries, and accidental collisions between Next public routes and Vite private prefixes. The admin page shows route counts and manifest validation status only; it still cannot enable production routing or change prerender behaviour. Regression tests: tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The shadow Next.js public frontend now owns the second public route batch in manifest-only mode. Listings, events, jobs, organisations, resources, knowledge-base, and marketplace index/detail routes render crawler-readable placeholder HTML inside the isolated Next app while create/edit/member workflows remain Vite-owned. This is still shadow-only and does not change production routing or prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/ui/__tests__/PublicPage.test.tsx, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js public frontend now hydrates second-batch public routes from Laravel APIs where safe. Listings, events, jobs, volunteering organisations, resources, knowledge-base, and marketplace discovery/detail pages can render crawler-readable titles, descriptions, canonical metadata, and tenant-aware links from existing public API responses while falling back safely when data is absent. This remains shadow-only and does not change production routing or prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/tenant-api.test.ts, next-public-frontend/src/ui/__tests__/PublicPage.test.tsx.
  • The Next.js public frontend readiness API now reports Laravel API-backed route coverage. Admin readiness data distinguishes manifest-only public routes from routes with shadow content sources, reports that Laravel public APIs remain the source of truth, and validates that API-backed route keys still exist in the shadow route manifest. This does not enable production routing or alter prerender serving. Regression test: tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The Next.js public frontend readiness page now shows API-backed public route coverage. The read-only admin page lists the Laravel public API methods/endpoints backing shadow public routes, giving administrators cutover visibility without adding any serving switch, edge route, or prerender change. Regression test: react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The shadow Next.js public frontend now uses a shared content-source manifest. next-public-frontend/content-sources.json is read by both the Next API client and the Laravel readiness service, reducing drift between SSR data fetching and admin cutover reporting while keeping production routing and prerender serving unchanged. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, next-public-frontend/src/lib/__tests__/tenant-api.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js public frontend now has a dedicated manifest consistency check. npm --prefix next-public-frontend run check:manifests validates that route ownership remains shadow-only, public route keys are unique, API-backed route keys exist in the route manifest, content sources use Laravel public APIs, and Next is not allowed to query the database. This is an inert verification command only and does not change production routing or prerender serving. Regression test: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts.
  • The shadow Next.js public frontend now runs manifest validation in its default check command. npm --prefix next-public-frontend run check now starts with check:manifests, and the admin readiness API reports that manifest validation script presence. This remains verification-only and does not change production routing or prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/package-scripts.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js public frontend now has a no-JavaScript public-route HTML check. npm --prefix next-public-frontend run check:no-js-html renders every Next-owned public route pattern plus tenant-slug home routes to static HTML and verifies tenant branding, canonical URL, JSON-LD, heading, and AGPL attribution are present without browser JavaScript. The default Next check command now includes this local verification; production routing and prerender serving remain unchanged. Regression tests: next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx, next-public-frontend/src/lib/__tests__/package-scripts.test.ts.
  • The Next.js public frontend readiness API now reports the no-JavaScript HTML check. The read-only admin readiness payload includes check_no_js_html alongside the existing shadow verification scripts, so administrators can see whether crawler-readable route coverage is wired before any canary or cutover work. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now shows the pre-cutover verification commands. The shadow-only admin panel lists the exact local commands for the isolated Next check, React build, React typecheck, and readiness API regression test so operators can prepare a future canary without adding an activation switch. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now includes the service-level readiness regression in its verification commands. The read-only admin payload tells operators to run both the readiness service unit suite and admin endpoint feature suite before any future canary, reducing the chance of backend manifest validation drift. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now shows per-route cutover gates. The read-only admin payload derives route-level gate rows from the shadow ownership and content-source manifests, showing each public route's source type and keeping every route blocked for live cutover until parity tests are explicitly completed. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now shows structured live-cutover gate blockers. The read-only admin payload and page list the manual verification commands, explicit approval requirement, missing edge-route configuration, and retained prerender fallback requirements for each future cutover step. This remains guidance only and does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The shadow Next.js tenant resolver now tolerates malformed encoded path segments. Bad crawler or proxy input such as an incomplete percent-encoded segment no longer throws during server-side tenant request normalization; the segment is kept as-is and the request remains in shadow-mode resolution. This does not enable production routing or alter prerender serving. Regression test: next-public-frontend/src/lib/__tests__/tenant-request.test.ts.
  • The shadow Next.js manifest gate now validates route/API parameter alignment. npm --prefix next-public-frontend run check:manifests now blocks API-backed routes whose endpoint placeholders drift from their public route pattern params, preventing mismatched SSR detail routes before any canary or cutover work. This does not enable production routing or alter prerender serving. Regression test: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts.
  • The shadow Next.js public frontend now validates route label translations. npm --prefix next-public-frontend run check:messages verifies every Next-owned public route label key resolves in the local public message catalog, and the default isolated Next check command now includes that gate. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-messages-validation.test.ts, next-public-frontend/src/lib/__tests__/package-scripts.test.ts.
  • The shadow Next.js manifest gate now blocks duplicate API-backed route keys. npm --prefix next-public-frontend run check:manifests catches duplicate content-sources.json route keys before they can make SSR data lookup ambiguous. This does not enable production routing or alter prerender serving. Regression test: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts.
  • The shadow Next.js manifest gate now enforces Laravel public API endpoints. npm --prefix next-public-frontend run check:manifests blocks API-backed content sources that are not relative /v2/... Laravel API paths, keeping shadow SSR data fetching on the approved backend contract. This does not enable production routing or alter prerender serving. Regression test: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts.
  • The Next.js public frontend readiness API now mirrors the Laravel API guards. The read-only admin readiness service blocks unsafe content-source metadata, malformed API-backed entries, duplicate API-backed route keys, endpoints outside relative /v2/... Laravel public API paths, non-GET methods, and public route/API parameter drift, matching the isolated Next manifest check before any canary or cutover work. This does not enable production routing or alter prerender serving. Regression test: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The shadow Next.js manifest checks now require critical Vite private ownership to stay with React. Both the isolated Next manifest checker and Laravel readiness API block removal of private/gated prefixes and create/edit mutation patterns from the route ownership manifest, reducing the chance of a future public canary accidentally absorbing logged-in routes. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The shadow Next.js manifest checks now block public routes that collide with Vite private mutation patterns. Public route entries such as /events/new are rejected if the route ownership manifest also marks them as Vite-owned create/edit flows, keeping future canary work fail-closed before route ownership can become ambiguous. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The shadow Next.js route manifest now reserves current Vite create/edit workbench paths. Current React action routes such as /events/create, /listings/edit/:id, /marketplace/sell, /organisations/register, and protected course/podcast/ideation/volunteering workbench paths are now explicitly Vite-owned before public detail patterns can catch them. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js public route manifest now separates marketplace discovery subroutes from item details. /marketplace/search, /marketplace/map, /marketplace/collections, and /marketplace/free are explicit public shadow routes, while job workbench routes such as /jobs/alerts, /jobs/my-applications, /jobs/talent-search, /jobs/bias-audit, and /jobs/employer-onboarding remain Vite-owned before /jobs/:id can catch them. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/lib/__tests__/public-messages-validation.test.ts, next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js tenant resolver now reserves remaining protected Vite workbench segments. Courses, caring-community member tools, federation onboarding, group-exchange creation, premium management, reviews creation, and volunteering organisation dashboards are explicitly Vite-owned so shared-host paths cannot be parsed as tenant slugs during future canary work. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/lib/__tests__/tenant-request.test.ts, next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js tenant resolver now reserves auth helper routes for Vite. Auth callback, password reset, email verification, identity verification, and optional identity verification prefixes are marked Vite-owned so shared-host paths cannot be parsed as tenant slugs during future canary work. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/lib/__tests__/tenant-request.test.ts, next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js public route manifest now covers developer, analytics, volunteering, ideation, and hOUR public pages. Developer docs, regional analytics marketing, explore, hOUR tenant information pages, municipality calendar, volunteering opportunities, and ideation challenges now have explicit shadow Next ownership, while pilot forms, newsletter unsubscribe, invite redemption, partner analytics dashboard, advertising tools, and guardian-consent token flows remain Vite-owned. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/lib/__tests__/tenant-request.test.ts, next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, next-public-frontend/src/lib/__tests__/content-sources.test.ts, next-public-frontend/src/lib/__tests__/public-messages-validation.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js manifest now has full concrete React route ownership coverage. A new regression test reads the current React route table and fails if any concrete route is neither Next-public nor explicitly Vite-owned. The manifest now adds shadow public ownership for groups, courses, podcasts, marketplace seller/category pages, coupons, clubs, and the caring-community hub, with Laravel public API content sources where safe; current coupon endpoints remain auth-only and are explicitly rejected as public SSR content sources. Protected caring-community tools, federation, exchanges, search, activity, chat, premium returns, donation receipts, seller coupons, and other member/workbench routes remain Vite-owned. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/react-route-coverage.test.ts, next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js manifest checks now block private Laravel API namespaces as public SSR content sources. API-backed public routes may still use approved /v2/... public content endpoints, but readiness now rejects admin, auth, dashboard, feed, messages, notifications, settings, super-admin, wallet, and broker namespaces before any future canary. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The shadow Next.js manifest checks now require API-backed endpoints to be plain paths. Content-source endpoints with query strings or fragments are rejected before any future canary, so SSR data fetching cannot smuggle private include flags through the manifest. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The shadow Next.js runtime now fails closed on unsafe content-source endpoints. Even if the manifest check is skipped, the isolated Next API resolver refuses non-/v2/ paths, private Laravel namespaces, query strings, and fragments before building SSR fetch URLs. This does not enable production routing or alter prerender serving. Regression test: next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The shadow Next.js public frontend now blocks traversal-style API content sources. Both manifest readiness checks and the runtime content-source resolver reject raw or encoded path traversal segments before any future canary can fetch Laravel public API data. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/shadow-manifest-validation.test.ts, next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The shadow Next.js runtime now shares the private Laravel API namespace guard with manifest validation. The runtime content-source resolver rejects auth-only coupon endpoints using the same Next-side private namespace helper as the manifest gate, preventing validator/runtime drift before any future public canary. This does not enable production routing or alter prerender serving. Regression test: next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The shadow Next.js runtime now fetches detail routes with named manifest parameters. API-backed shadow detail pages such as courses and podcast episodes now pass the route's actual parameter map into the Laravel endpoint template instead of only supporting id, so declared public content sources can render real SSR content before any future canary. This does not enable production routing or alter prerender serving. Regression test: next-public-frontend/src/lib/__tests__/tenant-api.test.ts.
  • The Next.js public frontend readiness API now verifies API-backed routes are public Laravel GET routes. The read-only admin checker fails closed when a shadow SSR content source points at an unregistered Laravel API endpoint or a registered endpoint that still requires authentication, preventing future canary work from depending on private or missing APIs. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadinessValidationIssues.test.tsx.
  • The Next.js public frontend readiness page now includes an operator playbook. The read-only admin payload and page show the future shadow verification, reviewed Apache config preparation, private Vite regression, public-route canary, and fallback-monitoring stages while keeping activation unavailable and requiring a separate explicit cutover instruction. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now documents the tenant-resolution contract. The read-only admin payload and page show that shadow Next resolves tenants through Laravel /v2/tenant/bootstrap, passes slug for shared-host path tenants, forwards Origin for custom-domain tenants, and does not query the database. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now verifies the tenant-bootstrap route status. The read-only admin payload checks Laravel's registered route collection and reports whether /v2/tenant/bootstrap is a public GET route before any future canary work depends on it. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now shows the future edge canary configuration status. The read-only admin payload and page document that Apache/Plesk remains the production edge, the NEXT_PUBLIC_FRONTEND_ROUTING_ENABLED flag is off, no route file is configured by this module, and no activation controls are available without a separate cutover instruction. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now groups future canary work into route batches. The read-only admin payload and page separate foundation public pages, API-backed public content, and retained Vite private routes with route counts, blockers, and verification commands, making the eventual canary sequence clearer without adding any activation switch. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now classifies remaining public route work. The read-only admin payload and page separate static/manual-review routes, auth-only backend routes, backend contract gaps, and unclassified manifest-only routes with aggregate public/API-backed/remaining counts, required next actions, and verification commands, while explicitly reporting no production effect and no activation controls. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadinessValidationIssues.test.tsx.
  • The Next.js public frontend readiness page now shows cutover eligibility. The read-only admin payload and page summarize that live cutover remains blocked, list concrete blockers such as remaining public route work, route parity, edge routes, and explicit cutover approval, and expose the next required actions without adding any activation controls. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadinessValidationIssues.test.tsx.
  • The Next.js public frontend readiness page now shows pre-cutover dry-run checks. The read-only admin payload and page list the exact shadow manifest/no-JavaScript HTML, platform legal content review, auth-only public contract review, private Vite regression, and inertness commands that must be run before any future canary, while still exposing no activation controls and no production routing effect. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The shadow Next.js content-source manifest now backs more foundation pages from a public Laravel API. About, features, contact, trust-and-safety, and the timebanking guide now resolve through a read-only translated /v2/public-page-content/{pageKey} contract, reducing static/manual-review route gaps while keeping production routing and prerender serving unchanged. Regression tests: tests/Laravel/Feature/Controllers/StaticPublicPageControllerTest.php, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The shadow Next.js content-source manifest now backs the legal hub from the translated public-page API. /legal resolves through /v2/public-page-content/legal with tenant-aware legal document summaries, reducing remaining route work while keeping production routing and prerender serving unchanged. Regression tests: tests/Laravel/Feature/Controllers/StaticPublicPageControllerTest.php, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The shadow Next.js content-source manifest now backs the changelog from a public Laravel API. /changelog resolves through /v2/public-changelog, which summarizes the public CHANGELOG.md file for crawler-readable shadow rendering while keeping production routing and prerender serving unchanged. Regression tests: tests/Laravel/Feature/Controllers/PublicChangelogControllerTest.php, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The shadow Next.js content-source manifest now backs locale-authored public static pages from a Laravel API. Developer docs, regional analytics, caring-community overview, and hOUR partnership/social-prescribing/impact/strategic-plan pages now resolve through /v2/public-static-route-content/{pageKey} using existing public locale assets, reducing remaining route work while keeping production routing and prerender serving unchanged. Regression tests: tests/Laravel/Feature/Controllers/PublicStaticRouteContentControllerTest.php, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The Next.js public frontend readiness API now distinguishes privacy-sensitive remaining routes. Ideation idea detail now stays blocked with auth-scope/privacy-review actions instead of being treated as a simple parameter mismatch, and static platform legal routes require an authoritative content source before any future public cutover. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now treats development status as feature-status content. /development-status is counted as API-backed through the existing translated /v2/public-page-content/features endpoint, matching the current React redirect target while keeping production routing and prerender serving unchanged. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The shadow Next.js content-source manifest now backs platform legal pages from public locale assets. /platform/terms, /platform/privacy, and /platform/disclaimer resolve through the allowlisted /v2/public-static-route-content/... contract, moving readiness coverage to 72 API-backed public routes while keeping production routing and prerender serving unchanged. Regression tests: tests/Laravel/Feature/Controllers/PublicStaticRouteContentControllerTest.php, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The shadow Next.js operator README now lists the remaining non-cutover blockers. The README names the remaining auth-scope routes that require public visibility and privacy review before any future public API. This does not enable production routing or alter prerender serving.
  • The shadow Next.js content-source manifest now marks the homepage as tenant-bootstrap backed. / is explicitly associated with the existing public /v2/tenant/bootstrap contract so readiness reporting recognises tenant branding as the Laravel source of truth, with no new endpoint and no production routing change. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, next-public-frontend/src/lib/__tests__/content-sources.test.ts.
  • The Next.js public frontend readiness page now links to an inert Apache/Plesk canary template. A new example-only scripts/deploy/apache/next-public-foundation-canary.conf.example file documents exact-match foundation public-page proxy rules for a future reviewed canary, and the read-only admin payload/page verifies the template exists while confirming it is not included by deploy or compose. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now exposes a cutover artifact inventory. The read-only admin payload and page list the route manifest, content-source manifest, inert Apache canary template, shadow runtime config, routing flag config, prerender fallback, and required future verification commands while confirming there are no activation controls or production effects. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness page now audits the inert Apache canary template against route ownership. The read-only admin payload/page expands the example RewriteRule paths, confirms they are exact public routes, and reports private-route collisions or unsupported rules as blockers before any future cutover. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The Next.js public frontend readiness checklist now flags accidental Apache canary inclusion. The read-only backend/admin safety checks expose a pass/blocker row confirming the inert Apache template is not referenced by deploy or compose, making future route-inclusion drift visible before any explicit cutover. This does not enable production routing or alter prerender serving. Regression tests: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The shadow Next.js module now has a local production-inertness verifier. npm run check:next-public:inert reads repo files only and fails if the cutover env flag is enabled, the Apache canary template is referenced by deploy/compose, the Next service leaves the opt-in next-public-shadow profile, or the prerender fallback disappears. The readiness payload lists this as a required pre-cutover command. This does not enable production routing or alter prerender serving. Regression tests: scripts/test/check-next-public-inert.test.mjs, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The shadow Next.js module now has an aggregate pre-cutover dry-run command. npm run check:next-public:dry-run runs the inertness guard, isolated Next check, backend/admin readiness contracts, React typecheck, and React production build in sequence, stopping on the first blocker while reporting no production routing effect. The readiness payload and operator instructions list it as a required future cutover command. Regression tests: scripts/test/check-next-public-dry-run.test.mjs, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php, react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadiness.test.tsx.
  • The shadow Next.js content-source manifest now backs Explore and Clubs from public Laravel APIs. The Next public frontend can render those shadow routes from /v2/explore and /v2/clubs, and the read-only readiness API reports them as API-backed route coverage. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now backs marketplace map and free-item pages from public Laravel APIs. The Next public frontend can render /marketplace/map from /v2/marketplace/listings/nearby and /marketplace/free from /v2/marketplace/listings/free, with readiness reporting both routes as API-backed coverage. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now backs marketplace search from the public listings API. The Next public frontend can render /marketplace/search from /v2/marketplace/listings, and the read-only readiness API reports that route as API-backed coverage. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now backs marketplace category pages from a public Laravel API. /marketplace/category/:slug can resolve category-filtered listings through the new read-only /v2/marketplace/categories/{slug}/listings endpoint, while production routing and prerender serving remain unchanged. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Feature/Controllers/MarketplaceListingControllerTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now backs the municipality calendar from a public Laravel API. /municipality-calendar can resolve the tenant's first consenting municipality through the new read-only /v2/municipality/events-calendar endpoint, returning an empty calendar when no municipality has opted in. Production routing and prerender serving remain unchanged. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Feature/Verein/MunicipalityCalendarTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now backs the FAQ page from the public help API. The Next public frontend can render /faq from /v2/help/faqs, and the read-only readiness API reports that foundation route as API-backed coverage. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now backs the Help page from the public help API. The Next public frontend can render /help from /v2/help/faqs, and the read-only readiness API reports that foundation route as API-backed coverage. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js content-source manifest now backs tenant legal document pages from public Legal APIs. Terms, privacy, cookies, accessibility, community guidelines, acceptable-use, and their version-history routes now point at existing public /v2/legal/... endpoints for readiness reporting. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/content-sources.test.ts, tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The Next.js public frontend readiness API now fails closed if the cutover env flag is enabled. The read-only admin safety checklist reports route_cutover_disabled as a blocker whenever NEXT_PUBLIC_FRONTEND_ROUTING_ENABLED=true, making accidental canary activation visible without changing production routing or prerender serving. Regression test: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • The Next.js public frontend readiness API now reads the cutover flag through cached Laravel config. The NEXT_PUBLIC_FRONTEND_ROUTING_ENABLED guard remains fail-closed while avoiding direct runtime env() reads outside config, so production config caching and PHPStan agree on the cutover safety check. Regression test: tests/Laravel/Unit/Services/NextPublicFrontendReadinessServiceTest.php.
  • Security scan dependencies now resolve patched Vite and WebSocket packages. The root and React frontend lockfiles use vite@7.3.6, and the React frontend lockfile resolves Lighthouse's direct ws dependency to 7.5.11, clearing the OWASP Dependency-Check findings for the vulnerable vite@7.3.2 and ws@7.5.10 resolutions.
  • The Next.js public frontend readiness page now translates all shadow validation blockers. The admin page has locale keys for the content-source and API-backed route validation issues surfaced by the read-only readiness API, so future blockers render as operator-readable labels instead of raw i18n keys. This does not enable production routing or alter prerender serving. Regression test: react-frontend/src/admin/modules/advanced/next-public/NextPublicFrontendReadinessValidationIssues.test.tsx.
  • The shadow Next.js public frontend now has operator instructions. next-public-frontend/README.md documents the shadow-only status, safe local commands, backend/admin readiness verification command, opt-in compose profile, route/content-source manifests, admin readiness endpoint, and explicit pre-cutover checklist. This does not enable production routing or alter prerender serving.
  • The shadow Next.js operator instructions now document the Apache canary audit workflow. next-public-frontend/README.md explains that scripts/deploy/apache/next-public-foundation-canary.conf.example is example-only, not included by deploy or compose, and audited read-only by the admin readiness API before any future explicit cutover. This does not enable production routing or alter prerender serving.
  • The shadow Next.js route manifest now explicitly keeps auth and onboarding in Vite. Login, registration, and onboarding prefixes are recorded as Vite-owned routes in next-public-frontend/route-ownership.json, preventing a future public canary from accidentally treating them as Next-owned public pages, and shared-host tenant resolution now has regression coverage proving those prefixes are not parsed as tenant slugs. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/lib/__tests__/tenant-request.test.ts, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • The shadow Next.js public frontend now covers the remaining public legal/static page set. Features, changelog, legal hub, legal version history pages, cookie/community-guidelines/trust-safety/acceptable-use pages, platform legal pages, and the timebanking guide now have explicit shadow route ownership and crawler-readable translated HTML. This does not enable production routing or alter prerender serving. Regression tests: next-public-frontend/src/lib/__tests__/public-routes.test.ts, next-public-frontend/src/lib/__tests__/public-messages-validation.test.ts, next-public-frontend/src/ui/__tests__/PublicRouteNoJsHtml.test.tsx, tests/Laravel/Feature/Controllers/AdminNextPublicFrontendControllerTest.php.
  • Donations & Support payment routing is hardened for production. Tenant Stripe Connect accounts are used for one-off and recurring donations only once onboarding is ready; otherwise payments keep using the platform fallback with tenant metadata. Recurring support Stripe Price IDs now record the Stripe account they belong to and checkout refreshes prices when the active route changes. Admin donation exports now include payment route, Stripe account, and PaymentIntent details. Regression tests: tests/Laravel/Unit/Services/DonationStripeAccountServiceTest.php, tests/Laravel/Unit/Services/MemberPremiumServiceTest.php, tests/Laravel/Unit/Services/VolunteerDonationServiceTest.php, react-frontend/src/admin/modules/premium/MemberPremiumAdminPage.test.tsx.
  • Donations & Support now has production finance operations coverage. The support admin page shows tenant-scoped completed volume, platform fallback liability, tenant Connect volume, Gift Aid-ready declarations, open disputes, active recurring supporters, and failed receipt follow-up. New admin APIs expose the same finance overview, tenant-scoped dispute list, Gift Aid CSV, and annual donor receipt CSV; Stripe dispute webhooks are recorded against donation tenants and one-off donations can store Gift Aid and fund-code data. The accessible support page now uses "Donate" / "Support this community" language instead of premium-member wording. Regression tests: tests/Laravel/Unit/Services/DonationOperationsServiceTest.php, tests/Laravel/Feature/Controllers/MemberPremiumAdminFinanceControllerTest.php, tests/Laravel/Feature/Controllers/DonationPaymentControllerTest.php, tests/Laravel/Feature/GovukAlpha/CommerceParityTest.php, tests/Laravel/Unit/Services/StripeDonationServiceTest.php, react-frontend/src/admin/modules/premium/MemberPremiumAdminPage.test.tsx, react-frontend/src/admin/api/adminApi.test.ts.
  • One-off donations now expose fund allocation and Gift Aid in the donor checkout. Donors can choose a community fund before paying by card, and GBP donations can collect a UK taxpayer Gift Aid declaration that is sent to the existing Stripe donation intent API for tenant-scoped reporting and export. Regression test: react-frontend/src/components/donations/DonationCheckout.test.tsx.
  • Donations & Support Stripe sync no longer shows a false success while a support level still needs sync. The admin page validates the returned support level before showing the “synced” toast, and the backend now verifies the persisted monthly/yearly Stripe Price IDs and active Stripe account scope before returning success. Regression tests: react-frontend/src/admin/modules/premium/MemberPremiumAdminPage.test.tsx, tests/Laravel/Unit/Services/MemberPremiumServiceTest.php.

Changed

  • The in-platform changelog now renders as a polished release-notes page. The footer changelog link still uses the same CHANGELOG.md source as GitHub, but the React page now has a wider layout, clearer section rhythm, styled release/category headings, and readable release-note rows instead of a cramped generic Markdown card. Regression tests: react-frontend/src/pages/public/ChangelogPage.test.tsx, react-frontend/src/components/content/MarkdownRenderer.test.tsx.
  • Imported Markdown knowledge-base articles now use a dedicated document presentation. Knowledge-base .md imports keep the same stored Markdown content, but the React article page now renders them with clearer heading hierarchy, readable paragraphs and lists, styled blockquotes, tables, code blocks, links, and images instead of the generic Markdown treatment. Regression test: react-frontend/src/components/content/MarkdownRenderer.test.tsx.

Fixed

  • Registration email verification notices now explicitly tell new members to check Junk or spam. The post-registration verification callout bolds the spam-folder instruction so first-time registrants know where to look if the message is missing. Regression test: react-frontend/src/pages/auth/RegisterPage.test.tsx.
  • Wallet credit transfers now allow the Idempotency-Key request header in CORS preflight responses. Custom-domain tenants such as Timebanking UK can submit transfer requests again instead of being stopped by the browser before the wallet API receives the POST. Regression test: tests/Laravel/Unit/Middleware/EnsureCorsHeadersTest.php.
  • Broker safeguarding now reuses the full admin safeguarding dashboard. The broker panel now renders the same flagged messages, guardian assignments, member-preference table, and assignment actions as /admin/safeguarding, while drill-down cards stay scoped to /broker/safeguarding. This fixes member preference rows rendering option objects instead of option labels. Regression tests: react-frontend/src/admin/modules/safeguarding/SafeguardingDashboard.test.tsx, react-frontend/src/broker/pages/SafeguardingPage.test.tsx.

Back to all releases