3.1.0
Released 2026-10-09
Added
Admin — importing members now checks the whole file first, brings across time-credit balances and towns, and imports in timed batches with a progress screen. Nothing is imported until the entire file passes: if any row has a problem the import stops, nothing is created, and every problem is listed in plain words by spreadsheet row and column, with the list downloadable. A wrong file (an Excel workbook, a file that is not UTF-8, tab-separated, an old template, missing, extra or empty columns, or headings not on the first line) is refused with a pop-up and a "Download the correct template" button. Semicolon-separated files and decimal commas from European Excel are accepted, and the apostrophe the platform's own export puts in front of values that start with
=,+,-or@is undone. The template hasfirst_name,last_name,email,phone,location(the member's town, optional) andbalance(optional), with headings only and no example rows. A balance is carried over as an opening balance wallet entry (new transaction typeopening_balance, sender 0) that the member sees as "Opening balance carried over from your previous timebank" (the import reference is stored on the entry and shown to the admin on the results screen); the most that can be carried over is 100,000 hours, a negative balance starts at 0 with the original recorded and listed in the results, and an imported member never also receives welcome credits (including when an admin approves them later). The import runs in batches the browser paces itself (10 to 200 rows, each stopped by the server after about 8 seconds), with a progress screen showing the bar, "batch n of about m", counts, hours and time left; it cannot be closed mid-run, can be stopped after the current batch, never duplicates a batch that is sent twice, and finishes an interrupted import. The results list balances set to zero, members whose identity step did not complete, and rows not imported (downloadable). Limits and safeguards: a fresh second-factor confirmation (step-up) is needed for the check and every batch, admins only, 2 MB and 5,000 rows per file, one held import per admin, the check is limited to 10 a minute per admin, and the member data the server holds for a checked file is discarded as soon as the import completes or is stopped (pressing Stop tells the server, throughstop: trueon the batch endpoint); otherwise it expires within two hours, for example when the connection is lost mid-import. Each imported member gets amember_importedaudit entry, the completed import getsmember_import_completed, and the activity-feed entry is kept. Results are announced to screen readers, focus moves to them, and progress is announced every 10%. New routes:POST /v2/admin/members/import/check,POST /v2/admin/members/import/{importId}/batchandGET /v2/admin/members/import/template. The import window also offers, ticked by default, to email each new member a link to set their password: one invitation per created member is queued in the same database transaction as the member (members held for an identity check are not emailed and can be invited from the member list once approved), the emails go out in the background at about one a second, the window gives the estimate before and after, and the batch endpoint takessend_invitationson the first batch only. Not part of this change: exporting a file ready to re-import, and asking members for their town at onboarding.Admin — an administrator can mark a member's email as confirmed. On Admin → Users → a member, the Email activation card now has "Mark as confirmed" beside "Resend verification email", after an "are you sure?" step that names the address. It unblocks a member who is stuck at sign-in because a community requires email confirmation. It applies exactly the same rule as the member clicking their own verification link (so on a self-serve community the account is also activated, and on a community that screens new members only an already-approved one is), is recorded in the activity and audit logs, and never releases an identity-check hold. Admins who outrank the member only; brokers are refused. Sent as
email_verified: trueon the existingPUT /v2/admin/users/{id}; there is no "unconfirm".Volunteering — organisations can remove an approved volunteer, and the people who run an organisation can no longer volunteer for it (website, accessible site and server). An organisation's owner and its team owners/admins can now take an approved volunteer off one of its opportunities: on the website from the organisation dashboard's Applications tab and from the applicants panel on the opportunity page, and on the accessible site from the organisation's Manage page, each after an "are you sure?" step. The volunteer is told, any shift place is offered to the waiting list, the removal is recorded in the organisation's audit log, and they can apply again later. This also lets someone remove their own old application. Until now only the person who created an opportunity was stopped from applying to it; anyone else who runs the organisation could apply, and then nobody could decide the application because nobody decides their own. Now the opportunity page shows them a short "you run this organisation" note with a link to manage its applications instead of an Apply button, and the server refuses the application with a plain message (shown as such on the accessible site). Community admins are not affected unless they run the organisation. People who run an organisation can still log hours and claim expenses with it; another admin decides those (owner decision, keeping the 2 Oct design). On the accessible site a refused expense claim now shows the server's own reason instead of a generic failure. New endpoint
POST /v2/volunteering/applications/{id}/remove;GET /v2/volunteering/opportunities/{id}now returnsruns_organisation. Group sign-ups (switched off everywhere) can still add someone who runs the organisation to a shift.Volunteering — admins can remove an approved volunteer from an opportunity. Until now nothing anywhere let an admin take a volunteer off an opportunity once their application was approved. On Admin → Volunteering → Applications an approved row's menu now has "Remove from opportunity"; after an "are you sure?" step the volunteer's place is ended, they are told by bell and phone notification in their own language, the removal is recorded in the organisation's audit log (who removed whom, from which opportunity), and a place they held on a shift that has not started is offered to the next person on its waiting list. They can apply again later. A pending application is still declined rather than removed. New endpoint
POST /v2/admin/volunteering/approvals/{id}/remove(admins only; refuses anything but an approved application with 422, and another community's with 404).Volunteering — group sign-ups now work the way the wording promised (website, server and admin; still switched off everywhere until a community admin opts in). A group leader who reserves places on a shift and names a member now creates a real sign-up for that member: the member appears in My shifts, can check in, is told by bell and email in their own language that the leader signed them up, and can take themselves off the shift. Removing a member or cancelling the booking tells the people affected straight away rather than waiting for a digest. A named member's place is counted once, through the group's reserved places, so a shift no longer reads as full twice over. On the website the Group sign-ups tab lets a leader remove a member or cancel the whole booking and lets a member leave, each after an "are you sure?" step, and no longer shows a Pending or Declined state that could never happen. Admins get a new page, Admin → Volunteering → Group bookings, listing every booking in the community with search and a status filter kept in the address, and can cancel one, which releases its places and every member's sign-up and tells the group (
GET /v2/admin/volunteering/group-reservations,DELETE …/group-reservations/{id}). New notification typesvol_group_signup_added,vol_group_signup_removed,vol_group_signup_cancelledare sent immediately (gaps A8, C7).Phone app — organisers can now run their shifts, expenses and fundraising from the phone (needs an app release). Until now the app could only show shifts to volunteers; nothing on a phone could create, change or remove one. From the organisation dashboard an organiser can now: see every opportunity the organisation has posted, including closed and cancelled ones; add, change or remove a one-off shift and set up repeating weekly or fortnightly shifts, with the same rules and the same server refusals as the website; open a shift's roster to see who is signed up, who is on the waiting list, who came from a group booking and who has checked in; review volunteer expense claims (approve, reject with a reason, mark as paid, never on their own claim); and run fundraising campaigns, confirm hand-overs and see gifts and history, as on the website. An opportunity can now be deleted from the phone, after confirmation. New translation namespace
volunteeringOrganiserin all seven app languages (gap M2).Phone app — volunteers get the rest of their tools (needs an app release). A new My tools list on the volunteering screen opens, each only where the community has it switched on: waiting lists (a full shift now offers Join waiting list instead of a sign-up the server refuses, shows the member's position, and lets them leave or claim a freed place); wellbeing check-ins with the same "Let someone get in touch with me" choice as the website; urgent shift requests to accept or decline; the qualifications register (a record, never an upload); accessibility needs (a private note); group sign-ups (alpha, off unless the community opted in); the member's own check-in code as a QR code and as text; and reporting a safeguarding concern with the same form and the plain-English status of earlier reports. Expense claims can carry a receipt photo from the phone, and giving by card opens the website in the browser. New translation namespace
volunteeringVolunteerin all seven app languages (gap M3).Accessible site — group sign-ups can now be used end to end (only where a community has switched the feature on). Group leaders can reserve places on a volunteer shift for their group, choosing the opportunity, shift, group and number of places on ordinary web pages. Adding someone works by searching their name and choosing them, instead of typing a member number that was shown nowhere. Each reservation shows its members; removing a member or cancelling asks "are you sure?" first, with no JavaScript needed. The wording now says what really happens: a member you add is signed up to the shift, told about it, and can take themselves off it. The feature stays switched off on every community until an admin turns it on.
Volunteering — a volunteer who says they are struggling is now heard. Until now a wellbeing check-in was saved and shown back to the volunteer, and used for nothing else: the wellbeing score looked only at shifts and logged hours, and nobody was ever told (on hOUR Timebank six "Struggling" check-ins in three days left the score at 80 and raised no alert). Now a Low or Struggling check-in in the last 14 days lowers the score (Struggling alone gives "high" risk) and shows a warning, and a later better check-in clears it. When a volunteer chooses Low or Struggling, the check-in form (website and accessible site) asks "Let someone get in touch with me", ticked by default and explained in plain words; if they leave it ticked, a wellbeing alert is raised and the community's safeguarding staff (admins, brokers, coordinators) and the owners, admins and liaison people of every organisation they volunteered with in the last 180 days get a bell notification and an email in their own language. Staff can read the volunteer's note after signing in; it is never put in an email, and organisations are told the name and how the volunteer feels, never the note. Nobody is told about themselves, and people are told at most once a day per volunteer. A client that does not send the choice shares nothing. Brokers and coordinators can now list and act on wellbeing alerts (
GET/PUT /v2/admin/volunteering/wellbeing/alertsmoved to the broker-or-admin group), which now carryreasonandlatest_checkin, and the alerts panel also appears on the broker volunteering safeguarding page.POST /v2/volunteering/wellbeing/checkinacceptsshare_with_teamand returnssharedandteam_notified; recent check-ins carryshared. New columnsvol_mood_checkins.share_with_teamandteam_notified_at(appended; instant). The accessible wellbeing page no longer says "Only you can see this" without qualification.Accessible site — volunteers can record their own qualifications; document uploads are gone. The owner decided that qualifications are recorded, never uploaded, and the website switched to a no-upload register on 5 Oct. The accessible site now has the same register at
/volunteering/qualifications: list (grouped as needs attention, awaiting confirmation, confirmed, withdrawn), add, edit (with a warning that editing a confirmed record clears its confirmation), and withdraw with a reason, in the website's own wording in all 11 languages. It is listed on the volunteering page and qualification reminders (?tab=credentials) now open it. The old document-upload page/volunteering/credentialsforwards to the register (its old form posts do nothing but forward), its download address is gone, and the upload endpointPOST /v2/volunteering/credentialshas been removed from the API (no client used it any more). Documents uploaded before today can still be listed, downloaded and deleted through the API; nothing stored was deleted (gap B15).Volunteering — an organisation's team can now be managed. Until now nobody could add or remove a volunteer organisation's members or change their roles after it was created. Community admins (Admin → Volunteering → Organisations → Members) and the organisation's owners (new Team tab on the organisation dashboard, and a Team page on the accessible site) can add someone from the community as owner, admin or member, change a role, or remove someone; the organisation's admins can see the team but not change it. Safeguards are enforced by the server: the person who registered the organisation and your own place cannot be changed, the last owner cannot be removed, and only active members of the same community can be added. Each change is recorded in the organisation audit log and the person is told in their own language. New routes:
GET/POST /v2/volunteering/organisations/{id}/members,PUT/DELETE …/members/{memberId}, and the same writes under/v2/admin/volunteering/organizations/{id}/members(gap D7).Volunteering — admins can see and revoke volunteer certificates. A new admin page (Volunteering → Certificates) lists every certificate issued in the community, with search by name, email or code, and lets staff download the printable copy (without marking it downloaded) or revoke one issued in error with a recorded reason. A revoked certificate fails the public check exactly as an unknown code does, its printable copy is refused, the holder is told in their own language, and the website and accessible site both show it as revoked with no download. New nullable columns
vol_certificates.revoked_at,revoked_by,revoke_reason(appended; instant). Guessed admin addresses such as/admin/volunteering/applications,/incidents,/settings,/organisations,/fundraising,/shift-swaps,/shiftsand/wellbeingnow redirect to the real page instead of "page not found".check:db-columnsnow also recognises columns added bySchema::tablemigrations (gap D8).Admin → Volunteering → Expenses: admins can now add an expense policy for an expense type that has none (maximum per claim, monthly maximum, receipt threshold, whether approval is needed). Until now a community with no policies saw "No expense policies configured" and had no way to create one: the API could only update a policy that already existed. Blank limits mean "no limit". An organisation from another community is refused.
Volunteering → My Hours (website): members can now see each hour entry they have logged, with its date, where it was for and whether it is waiting, approved or declined, and the shifts they hold, upcoming and past, each linking to its opportunity. Until now the tab showed only totals. Both lists load ten at a time with Load more. Entries cannot be changed or withdrawn from here, because the platform has no way to do that once logged.
Volunteering → My expenses (accessible site): a volunteer can attach a receipt to an expense claim (PDF, JPG, PNG or WebP, up to 10 MB), as on the website. A file of the wrong type or over 10 MB is refused with a message beside the field, and nothing is sent. A claim without a receipt is sent exactly as before. Website: the expense form's "Receipt" label was still in English in eight languages and is now translated.
Volunteering → certificates: anyone holding a volunteer's certificate (an employer, a college) can now check it is genuine without an account, at
/verify-certificateon the community's website. They enter the code printed on the certificate and the volunteer's name as printed. They are told only whether the two match and, if they do, the details already on the certificate: hours, period, organisations and issue date. A wrong name gets exactly the same answer as a code that does not exist, so a code on its own reveals nothing about who holds it, and the check is limited to 20 tries a minute per network address. Names match regardless of capital letters, spacing and accents. The printed certificate now gives the page's address, and the Verify button on a volunteer's certificate opens it. New public routePOST /v2/volunteering/certificates/check(code,name); the signed-inGET /v2/volunteering/certificates/verify/{code}is unchanged and still refuses anonymous callers. Owner decision, 6 Oct 2026: a narrow exception to the July rule that certificate lookups need a sign-in, because it confirms only what the person already holds.Volunteering → certificates (accessible site): the same certificate check now exists on the accessible site, at
/verify-certificateon every community's accessible address, built with the standard GOV.UK form, error summary and notification banner. A code in the address only fills in the box; nothing is looked up until a name is entered, and the name is sent in the form body, never in the address. A community with volunteering switched off answers "page not found". Each certificate on a volunteer's Volunteer certificates page now has a Check this certificate link to it, with the code filled in. New strings in all eleven languages (govuk_alpha_volunteering.certificate_check.*).Volunteering → an opportunity's page (accessible site): the people who run an opportunity can now change its details, close it to new volunteers, reopen it, and cancel it, as they can on the website since 6 Oct 2026. Before this, an opportunity posted on the accessible site could never be changed or withdrawn there. Edit opens the posting form filled in (the organisation is shown, not asked; a changed place drops the old map pin), with field-by-field error messages. Cancelling goes through a confirmation page that warns it cannot be undone, says approved volunteers will be told, and offers closing instead. The page shows Closed to new volunteers or Cancelled, volunteers can no longer apply to either, and a cancelled opportunity offers nothing more to change. Posting an opportunity now ends on its page with "Your opportunity has been posted. Add shifts so volunteers can sign up." — the confirmation the create form had always asked for but the page never showed. New strings in all eleven languages (
govuk_alpha_volunteering.opp_manage.*).Volunteering → an opportunity's page (accessible site): a volunteer who holds an upcoming shift now sees Check in at your shift, with the same QR code the website and phone app show, whether they have checked in yet, and when they arrived and left. Until now the accessible site had no check-in at all. The QR is drawn on the server, so it works without JavaScript; it encodes the same check-in address as the other clients, so whoever scans it lands on one confirmation page. A community with QR check-in switched off shows nothing. The venue member pass now draws its QR with the same shared helper, which also stops an XML declaration being written into the middle of the page.
Volunteering → organisation dashboard (accessible site): Expense claims, where an organisation's owner and administrators review the expense claims its volunteers have made. A volunteer could already claim an expense on the accessible site, but the organisation had nowhere there to see the claim. The page shows claims waiting for review by default, with tabs for approved, paid, rejected and all; the organisation's totals; and, on each claim, only the next step that is allowed: Approve or Reject (with an optional reason) for a waiting claim, Mark as paid (with an optional payment reference) for an approved one. A reviewer's own claim offers no action, only a note that another administrator must review it. Receipts download and are never opened inside the site. The totals always cover every claim; the website's Expenses tab shows totals for the current filter only (so "Approved" reads 0 on its Pending view), which is unchanged. New strings in all eleven languages (
govuk_alpha_volunteering.org_expenses.*);:name — :amountis added toscripts/php-lang-invariant-allowlist.jsonbecause it has nothing to translate.Volunteering → organisation dashboard (accessible site): Fundraising, where an organisation's owner and administrators run campaigns for their own organisation, as on the website since 5 Oct 2026. Until now the accessible site had no fundraising page for an organisation at all. They can start a campaign (title, description, dates and a goal, checked field by field), change it, pause and resume it, and end it after a warning page that offers a pause instead. Each campaign has a page with what has been raised, passed on and is still held; each hand-over from the community, with Confirm received while it waits; the gifts (donor contact and Gift Aid details are never shown); and the full history. A part of that page that fails to load says so without hiding the rest. New strings in all eleven languages (
govuk_alpha_volunteering.org_fundraising.*), reusing the website's wording;Stripe(a brand name, every language) andCheque(Spanish, Dutch, Portuguese) andPlatform(Dutch) are added toscripts/php-lang-invariant-allowlist.jsonas words a translator would leave alone.Volunteering → organisation dashboard (accessible site): Volunteer qualifications, where an organisation's owner and administrators confirm their volunteers' qualifications, as on the website since 5 Oct 2026. Until now nobody could confirm a qualification on the accessible site. (A volunteer still records their own qualifications on the website: the accessible site has no page for that yet, only the older document-upload page, which is no longer linked.) The register shows what needs attention first (awaiting confirmation or expiring soon), with counts, a search, and Confirmed, Expired and All views. Confirm asks how it was checked (saw the original, checked an online register, or the issuer confirmed it); nothing is uploaded or stored. Withdraw asks for a reason. No one can confirm their own qualification, and an expired one must have its expiry date updated by the volunteer first; both refusals come back in plain words. New strings in all eleven languages (
govuk_alpha_volunteering.org_qualifications.*), reusing the website's wording, rewritten to the formal "you" in Spanish and Dutch as the rest of the accessible site is.Volunteering → organisation dashboard: a new Opportunities tab lists everything the organisation has posted: open, closed to new volunteers, and cancelled (the public list shows only open ones). Each shows how many applications are waiting for a decision, how many volunteers are approved and how many shifts are coming up, with Manage, Edit and Post opportunity links. Before, an organiser had to browse the public list to find their own opportunities, and could not find closed or cancelled ones at all. New route
GET /v2/volunteering/organisations/{id}/opportunities, open to the same people as the rest of the dashboard.Volunteering → an opportunity's Shifts panel (organisers) and Admin → Volunteering → Opportunities & shifts: an Ask for help button on every upcoming shift sends an urgent request to the volunteers already approved for that opportunity who are not on the shift (up to 50; optionally only those listing a named skill). The organiser chooses how urgent it is and how long to keep asking, and is told how many volunteers were asked. Active requests are listed above the shifts, with how many were asked, accepted and declined, and can be withdrawn. Before, nothing on any screen could send one, so the volunteers' Alerts tab could never fill. New route
GET /v2/volunteering/opportunities/{id}/emergency-alerts, for the people who may manage the opportunity.POST /v2/volunteering/emergency-alertsnow returnsnotified.Volunteering → an opportunity's page: the people who manage it (its creator, the organisation's owner and admins, community admins) now get Edit, Close to new volunteers / Reopen and Cancel opportunity, and Edit opens a pre-filled form at
/volunteering/opportunities/{id}/edit. Before, the website could not edit, close or cancel an opportunity at all. Closing takes it off the volunteering list and refuses new applications, while the volunteers already on it keep their places. Cancelling asks first, removes it for good and tells approved volunteers. The page's badge now reads Cancelled for a cancelled opportunity, where it used to say "Closed".PUT /v2/volunteering/opportunities/{id}acceptsstatus(openorclosed); any other value gets 422.Volunteering → an opportunity's Shifts panel (organisers) and Admin → Volunteering → Opportunities & shifts: every shift now has a Who's coming button listing the volunteers on it by name, each marked as coming, checked in (with the time), checked out or did not come; the groups that booked places, with who booked and which members are confirmed; and the waitlist in order. Before, an organiser saw only "1 of 2 places taken", and attendance appeared on no screen. New route
GET /v2/volunteering/shifts/{id}/roster, open only to people who may manage the opportunity (its creator, the organisation's owner and admins, community admins).Volunteering → Organisations and admins can now create, change and remove shifts. Until today nothing on the platform could create a shift: the only server route was a repeating-pattern endpoint that no screen called, and a one-off shift had no route at all, so every feature built on shifts (sign-up, waitlists, group sign-ups, check-in, reminders, swaps) was unreachable for a real organisation. An opportunity's page now carries a Shifts panel for whoever may manage it (the person who posted it, the owning organisation's owner or admins, and community admins), with Add a shift (date, start, end, optional number of places), Add repeating shifts (every day, week, two weeks or month, on chosen weekdays, with an optional last date or maximum count), Edit and Remove on any shift that has not started, and Stop repeating on a pattern. Repeating shifts are created 14 days ahead the moment the pattern is saved, and the existing nightly job keeps topping them up. Removing a shift keeps every volunteer's application to the opportunity, clears their place on that shift, and tells them by bell notification in their own language. A shift cannot be removed or changed once it has started, cannot end before it starts, and cannot be shrunk below the number of volunteers already on it. The admin panel gains Volunteering → Opportunities & shifts, listing every opportunity in the community with the same panel under each. New API:
POST /v2/volunteering/opportunities/{id}/shifts,PUT /v2/volunteering/shifts/{id},DELETE /v2/volunteering/shifts/{id}; the shift list now includesrecurring_pattern_id; creating a repeating pattern returnsshifts_generated. All new wording is in all eleven languages. The accessible site does not offer shift management yet.Volunteering → Volunteers can now ask to swap a shift on the website. Since 24 August the phone app could ask for a swap, but the website could only answer one, and the Swap Requests tab's own empty state pointed members at a "shift details page" control that had never been built. On My Applications, every confirmed shift that has not started yet now carries an Ask to swap button. It opens a picker of the other future shifts on the same opportunity that have someone on them (the number signed up per shift was already public), and the member picks the shift they would rather do. The platform asks whoever holds that shift without revealing their name, exactly as the app does; the other volunteer accepts or rejects from their own Swap Requests tab, and where the community requires it an admin approves last. A request that gets no response is retried once under the same key, so a dropped connection cannot create two requests. The tab's empty-state wording now points at My Applications, in all eleven languages. The accessible site's swap form, which still asks for a typed shift number and the other person's user ID, is unchanged and remains unusable in practice.
Volunteering → Fundraising alerts. Four things that used to happen silently now send a bell notification and an email, each in the recipient's own language: (1) when an organisation starts a fundraising campaign from its own dashboard, the community's admins are told it is already live, with a Review or pause this campaign button that opens the admin page with that campaign's pause confirmation (nothing is paused until the admin confirms); (2) when a community admin pauses or ends an organisation's campaign, the organisation's owners and admins are told; (3) when an organisation confirms it received money the community passed on, the community's admins are told; (4) when a gift to an organisation's campaign is received (a card payment succeeds, or an admin marks a pledge as paid), the organisation's owners and admins are told, with the donor's name or "a donor who asked to stay anonymous", never an email address. The person who made the change is not emailed about it. A failed alert is logged and never undoes the change or fails a Stripe payment confirmation.
Volunteer expenses — volunteers now hear about every decision in the app as well as by email. Approval, rejection and payment of a claim each send an in-app notification and a phone push (
vol_expense_reviewed,vol_expense_paid) alongside the existing email, in the volunteer's own language.Volunteer expenses — a claim can name the opportunity it was for. The claim forms on the website and the accessible site offer an optional "Opportunity" choice listing only the opportunities the volunteer was accepted onto at the chosen organisation (the API already accepted
opportunity_id; no form offered it). On the accessible site, which works without JavaScript, it is one list grouped by organisation, and choosing an opportunity from a different organisation is refused with a message that names the field. The phone app does not offer it yet.Footer "Powered by" badge for a whole network of communities. Admin → Settings → Powered-by branding (platform owner only) now has a second box, "Badge for communities under this one". Fill it in once on a hub community (for example Timebanking UK) and every community under it, at any depth and including communities created later, shows that badge — label, link and image — instead of the Project NEXUS one. The hub's own footer keeps its own badge (the Project NEXUS default unless set). A community that sets its own badge uses only its own: the image, label and link are never mixed from two sources. Both badges also get a wording choice: "Powered by" (the default), "Provided by", or custom text. The first two are shown in each member's own language; custom text is shown as typed and is not translated. Applies to the web app and the accessible site. New settings
general.network_powered_by_{wording,label,url,image_light,image_dark}andgeneral.powered_by_wording; new APIPOST /v2/admin/settings/network-powered-by-image-light|dark. Saving or uploading a network badge refreshes every community under the hub at once. All new wording is in all eleven languages.Help & support → Jira: every ticket the platform raises in the help desk can now also be watched by other support staff, so Jira emails them about it as well as the person it is assigned to. The help desk's own notification setting only emails a ticket's assignee, watchers and reporter, so a second agent heard nothing. New server setting
SUPPORT_JIRA_WATCHER_ACCOUNT_IDS(comma-separated Atlassian account ids; empty = no watchers). A failed watcher keeps the ticket and is recorded on the report, like a failed assignment. Covers tickets raised through the platform only, not ones sent to the help desk by email or through its portal.Every volunteering safeguarding incident now has a case record — a permanent history (who did what and when, which nobody can edit or delete; the database refuses it) and a page for each kind of person involved. Staff (admin panel
/admin/volunteering/safeguarding/:id, broker panel/broker/safeguarding/volunteering/:id) get a case file: the report, the status with a required reason for resolving, escalating or closing, the handler, how it is filed, the authorities told, team notes, messages to the reporter or the organisation, sharing the full report with the organisation's safeguarding lead, and the whole history with filters. The incident list opens each case, can show only incidents nobody is handling yet, and its figure tiles filter by status; the old update dialog and its "Recent actions" card (which listed incidents, not actions) are gone. The person who reported it gets a page in the web app and on the accessible site (/volunteering/incidents/:id) with its reference, a plain-words status ("Being looked into", "Passed to a specialist"…), what has happened since, messages from the team, and a box to add information until it is closed; the report form now ends with the reference and a link to that page. The organisation it is linked to — its owner, administrators and safeguarding lead/deputy — gets a Safeguarding reports page (/volunteering/org/:orgId/safeguarding, a tab on the organisation dashboard, and/volunteering/organisations/:id/safeguardingon the accessible site) showing a summary of each report, the team's messages and a box to send the team an update; the lead sees the full report only while staff share it, and nobody in the organisation is ever shown who made a report. Each person is told by bell and email what they need to know, in their own language, with no written text in any email; nobody is told about an incident about themselves. New API:GET /v2/admin/volunteering/incidents/{id},POST …/notes,POST …/messages,POST|DELETE …/share;GET /v2/volunteering/incidents/{id}(the reporter's view) andPOST …/additions;GET /v2/volunteering/organisations/{orgId}/incidents,GET …/incidents/{id},POST …/incidents/{id}/updates. Existing incidents keep their old notes as the first entries of their history. All new wording is in all eleven languages.Group exchanges explain themselves, and two new kinds (web app, accessible site and phone app). Starting a group exchange now asks "What kind of group exchange is this?" and offers five kinds, each with a one-sentence explanation and a worked example: Workshop or class (the new default — the person running it earns the hours they put in, everyone attending pays the hours they attended, and any hours left over go to the community's time fund), A team helping someone (each helper earns the hours they spent and the person helped pays for all of it), Share equally, Share by amount of effort and Type each person's hours (the existing three, renamed and explained). The page states the rule "One hour of time is one time credit. People giving time earn credits; people receiving time pay them." and says "Giving time" / "Receiving time" instead of "providers" / "receivers". Before anything is saved, the form shows one plain line per person ("Mary earns 2 hours", "Tom pays 2 hours"), the hours going to the community time fund and the totals, worked out by the server; a split that cannot go ahead is explained in plain words (for example, a workshop whose organisers would earn more than the attendees pay is pointed at "A team helping someone"). Each participant sees "You will earn…" or "You will pay…" before confirming. Completing a workshop credits the leftover hours to the community fund in the same transaction as every balance, conserved to the cent; the fund records them as
group_exchangeentries and each attendee's wallet shows their share as a payment to the community time fund. API: newPOST /v2/group-exchanges/preview;split_typeacceptsworkshopandteam;GET /v2/group-exchanges/{id}returnscommunity_fund_hours. All new wording is in every language.Volunteering safeguarding incidents now reach the organisation they are about. When a report is linked to an organisation — by the reporter, or later by staff — its owner, administrators and designated liaison person (and deputy) get a bell and an email saying a report has been linked to it, what kind of report it is, how serious, when it happened and which opportunity. They are deliberately not told who made the report, who it is about or what it says, because a report can be about the organisation itself; the community's safeguarding team has the full record. The reporter, anyone the report is about, and anyone already alerted as community staff are not sent it. Admin → Volunteering → Safeguarding & incidents (and the broker panel's copy) has a new How this incident is filed section in each incident: staff can link, change or remove its organisation and opportunity, correct its type, severity and date, and record that the police or another authority was told, with their reference. Both report forms — the web app and the accessible site — now open with a warning to call the local emergency number if someone is in immediate danger, and say who will be told about the report. The staff alert email now names the organisation, opportunity and date, and gives the kind of incident in words. All new wording is in all eleven languages. API:
PUT /v2/admin/volunteering/incidents/{id}acceptsorganization_id,opportunity_id,incident_type,incident_date,authority_notifiedandauthority_reference, and now validatesseverity.Volunteering → Fundraising campaigns can raise money for an organisation. When an administrator creates or edits a campaign (Admin → Volunteering → Fundraising campaigns) they can choose which approved volunteering organisation it is for, or leave it for the whole community; the campaign list has an Organisation column. Every donation to that campaign records the organisation on the donation itself (decided by the server from the campaign, never by the donor's browser), so it stays correct even if the campaign is edited later. Card payments now tell Stripe who the gift is for: the payment description reads "Donation to {organisation} via {community} - {campaign}" and the payment carries
nexus_beneficiary(organizationorcommunity),nexus_organization_id,nexus_organization_name,nexus_giving_day_idandnexus_giving_day_title, so the finance team can reconcile a payout in Stripe without opening the platform. The money still goes to the community's own Stripe account — this labels the gift, it does not send money to the organisation. Members see "For {organisation}" on the campaign card, in My donations and on the receipt, and the card checkout says which organisation the donation supports. The admin donation list and CSV export include the organisation and can be filtered by it. A gift made against a volunteering opportunity is credited to that opportunity's organisation.Volunteering → Safeguarding → Report an incident (web app): the form now asks what kind of incident it is (a welfare concern, an allegation, something someone disclosed, a near miss, or something else), when it happened, which organisation and volunteering opportunity it was with, and which member it is about. Every new question is optional except the kind, which starts on "a concern". Before this, every report reached staff with no organisation, opportunity or person, as type "other" and dated the day it was sent. New route
GET /v2/volunteering/incidents/report-optionslists the community's active organisations and their opportunities, by name only;POST /v2/volunteering/incidentsnow refuses anincident_datein the future, and a blank date still means today. Admin → Volunteering → Safeguarding & incidents, and the broker panel's copy of it, now show each incident's title, organisation, opportunity and date, plus a Who is handling this? picker. Staff can hand an incident to any active administrator, broker or coordinator, but never to the person it is about, and the new handler gets a notification and an email. The staff list response now includeshandlers.Volunteering → Fundraising campaigns keep a permanent history. Every campaign change (created, edited — with the before and after values — paused, resumed, ended, organisation set) and every step of every gift (started, paid, failed, could not start, refund requested by an admin, partly refunded, refunded, pledge marked received) is written to a new append-only
vol_fundraising_eventstable in the same database transaction as the change itself, with who did it and when. Database triggers refuse any UPDATE or DELETE of that table, and the retention job never prunes it. Community admins see the history in the campaign window (Admin → Volunteering → Fundraising campaigns → View donors → History), with Stripe references; organisations see their own campaigns' history without donor identities.Volunteering → Hand-overs: a record of money passed on to an organisation. Card gifts are held in the community's Stripe account, so a community admin now records each transfer to the campaign's organisation (amount, date, method, reference, optional note) in the campaign window's Hand-overs tab, which shows raised / passed on / still held. A hand-over can never exceed what the campaign still holds; it cannot be edited or deleted (database-enforced), only cancelled with a reason. The organisation's owners and admins get an email, bell and push notification in their own language and confirm receipt from their dashboard. New
vol_fundraising_handoverstable.Volunteering → Organisations run their own fundraising campaigns. The organisation dashboard has a new Fundraising tab (also reached from the hand-over email) where an approved organisation's owners and admins create, edit, pause, resume and end campaigns for their own organisation — live straight away, no approval step — and see each campaign's gifts (name or "Anonymous"; never email or Gift Aid details), hand-overs and history. Seven new
/v2/volunteering/organisations/{id}/...endpoints; the organisation always comes from the URL, never the request body.Donations → Stripe carries the platform donation number. Card checkout now creates the pending donation before the PaymentIntent, so the PaymentIntent metadata includes
nexus_donation_id; a webhook that arrives before the intent id is saved is matched by that number. Platform refunds carrynexus_donation_id,nexus_giving_day_id,nexus_organization_idandnexus_refunded_by_user_id. A checkout Stripe refuses to start is kept as a failed donation with a history entry.Accessible site → Volunteering → Safeguarding → Report an incident: the same questions now appear on the accessible site. A member chooses what kind of report it is (if they choose nothing it is filed as a concern about someone’s welfare, which the page says; it was always filed as "other" before), can give the date it happened as day, month and year (a half-typed, unreal or future date is refused with a message rather than silently filed as today), and can pick the organisation and the volunteering opportunity it was with. Without JavaScript the opportunity list cannot be narrowed to the chosen organisation, so each opportunity is shown with its organisation’s name and a mismatched pair is refused with its own message. If the list of organisations cannot be loaded, the form says so and the report can still be sent. The member’s list of their own reports gains an Organisation column, and the form is no longer shown filled in again after a report is sent successfully. All new wording is in all eleven languages.
Volunteering → Qualifications: the certificate-upload "Credentials" tab is replaced by a qualifications register, after a review of what the law allows and what other volunteer platforms do. A volunteer records a qualification (type, issuer, date obtained, expiry date, certificate or registration number, notes) and nothing is uploaded or stored; the tab tells them to keep the certificate and show, email or post it to the organisation directly. Police checks (DBS, AccessNI, PVG, Garda vetting) stay out and point to the community's safeguarding team. The organisations a volunteer is placed with see those records on a new Qualifications tab of their dashboard and can confirm one after seeing the original, checking an online register or hearing from the issuer; community admins, brokers and coordinators can confirm from a new Admin → Volunteering → Qualifications page. Nobody can confirm their own. Editing a confirmed record removes the confirmation. A nightly job marks records expired on the date and sends an "expiring soon" reminder (window from the existing admin reminder setting, now called Qualification expiry, default 30 days) and an "expired" notice to the volunteer by email and bell, plus a digest to the owner and admins of each organisation the volunteer is linked to. Qualification types follow the community's jurisdiction (Children First and First Aid Response for Ireland; safeguarding adults, Emergency First Aid at Work and First Aid at Work for the UK; first aid, safeguarding training, manual handling, food hygiene, driving licence, professional registration and other everywhere). Every change is kept in an append-only history. API:
GET/POST /v2/volunteering/qualifications,PUT /v2/volunteering/qualifications/{id},POST …/{id}/withdraw,POST …/{id}/confirm,GET /v2/volunteering/organizations/{id}/qualifications,GET /v2/admin/volunteering/qualifications; commandvolunteering:qualification-expiry. The old upload endpoints and table are untouched for now (no production uploads existed); their removal is a separate step.Accessible site → Volunteering → Safeguarding → Report an incident also asks who the report is about. The member types a name and presses Find member; the page comes back with the matching members of the community to choose from, and everything already typed is kept. It works without JavaScript, the name searched for is never put in the page address (so it does not end up in browser history or server logs), the reporter is never offered as the person, and a name typed but never looked up is refused with a message rather than silently dropped. Pressing Enter in any box still sends the report rather than running a search. Staff see the chosen member under "Subject", exactly as for reports from the web app. All new wording is in all eleven languages.
Admin → CRM → CRM overview (the CRM front page): it now opens with a Needs a coordinator strip listing what is waiting right now — overdue tasks, tasks due today and members waiting for approval — each a link into the list with that filter applied, or a green "Nothing is waiting on you" when all three are zero. The eight figures (members, active in the last 30 days, new this month, waiting for approval, open tasks, overdue tasks, member notes, tags in use) each carry a plain-English hint (never signed in, share of members active, assigned to you, added in the last 30 days, members with a tag) and each links to the page it comes from. Two new cards show Your next tasks — the open tasks assigned to you, overdue first, with an Overdue / Due today / due-date marker and the member each is about — and Recent notes, the latest notes coordinators have written, with the member (linking to their notes), category, pin and author; concern notes the reader may not see are left out, exactly as on the notes page. The five CRM tools plus the member list are a row of cards, each with a one-line description. One Export menu replaces three buttons and offers the overview figures, notes, tasks, tags and activity CSVs; the overview CSV now carries every figure on the page (it had five of thirteen). A line says when the figures were loaded, a failed load shows an error with Retry, refreshing keeps the figures on screen instead of blanking them, and the page keeps to the screen on a phone. API:
GET /v2/admin/crm/dashboardadditionally returnstasks_due_today,my_tasks,next_tasks,notes_last_30_days,recent_notes,tags_in_useandtagged_members.Volunteering → Donations tab rebuilt. Fundraising campaigns are now grouped into Live campaigns (with the donate buttons), Coming up and Past campaigns (closed, no buttons), and each card says how much has been raised of the goal in the community's currency, the percentage funded, the number of donors and when it ends ("Ends 23 October 2026 · in 18 days"). Three summary tiles show live campaigns, the money raised across them, and what you have given (completed gifts, with a note of any pledge still pending) — replacing a "Total donors" figure that double-counted anyone who gave to two campaigns. My donations names the campaign on each gift, says "Card", "Bank transfer", "PayPal" or "Cash", explains on a pending pledge that an administrator confirms it when the money arrives, and offers View receipt on completed card gifts. The pledge form picks the campaign (or general support), shows the currency next to the amount, offers bank transfer, PayPal or cash only (card payments have the real checkout), and takes an optional payment reference so the administrator can match the transfer. A failed load shows an error with Try again, and refreshing keeps the figures on screen instead of blanking them. API:
GET /v2/volunteering/donationsreturnsgiving_day_titleon each row.Admin → CRM → Onboarding funnel: the page can now be narrowed to a cohort — members who joined in the last 30 days, 90 days or 12 months, or everyone — so a coordinator can see whether recent recruits are getting through; the cohort and the step whose list is open live in the address (
?joined=90&step=email_verified), so a reload, the back button or a shared link lands on the same view. A fourth tile, Could use a nudge, counts the members who have not reached the final step; the "new in the last 30 days" tile stays community-wide and says so when a cohort is chosen. Each waiting member now shows how long ago they joined ("3 weeks ago") beside the date. An Export who's waiting button downloads a CSV of every member who has not finished the journey — step, next step, name, email, joined date and days since joining, longest-waiting first within each step — and each step's open list has an Export this step button for just that step (newGET /v2/admin/crm/export/funnel, admin only, same cohort, capped at 5,000 rows). The monthly sign-ups chart has a matching table for screen readers, a failed load shows an error with Retry, an empty cohort offers Show everyone, and changing the cohort or refreshing keeps the figures on screen instead of blanking the page. API:GET /v2/admin/crm/funnelacceptsjoined_days(0, 30, 90 or 365) and returns it.Admin → CRM → Activity timeline: entries are now grouped under one heading per day (Today, Yesterday, then the date) with the time of day on each row, a coloured icon for the kind of activity, and a link to the member; a results line says how many activities the view holds ("Showing 1–25 of 106 activities"). The member, activity type, date range and page live in the address (
?user_id=&type=&days=&page=), so a reload, the back button or a link from a member's page lands on the same view. The date range is a row of buttons (7 / 30 / 90 days / all time), the type filter has an explicit All types choice, each row has a show only this member button, the empty view offers Show all time or Clear filters, a failed load shows an error with Retry instead of "No activity found", and refreshing or changing a filter keeps the list on screen instead of blanking the page. An Export activity button downloads a CSV of the current view (newGET /v2/admin/crm/export/timeline, admin only, same filters, capped at 5,000 rows).Admin → CRM → Member tags: each tag card now shows how many members carry it (the old card said only "Members", with no number) and when the tag was last added to anyone; the list can be sorted by most members or A to Z, the search box filters it, and a results line says how many tags match. Opening a tag lists its members newest first with who added the tag and when, a link to each member, and a Remove from all members button; the tag and search live in the address (
?tag=&q=&sort=), so the back button returns to the list and a link can point straight at one tag's members. Adding a tag offers the existing tags first (with their counts) so one idea does not end up spelled three ways, says when a member already has the tag instead of a generic failure, and pre-fills the tag when adding from a tag's members view. Removing asks for confirmation naming the tag and the member (or the number of members), and an Export tags button downloads a CSV of every tag assignment grouped by tag. API:GET /v2/admin/crm/tagsreturnslast_added_atper tag andcreated_by_nameper member; newGET /v2/admin/crm/export/tags(admin only).Admin → CRM → Coordinator tasks: the page opens on the tasks that still need doing (pending and in progress) instead of everything ever created, with Overdue, each status, and All one click away; a filter by assignee (anyone, you, or a colleague); an Export tasks CSV button; and a results line that says how many tasks match. The filters live in the address (
?status=&priority=&assigned_to=&q=&page=), so a reload, the back button or a shared link keeps them, and the CRM dashboard's Open tasks and Overdue tasks figures now open the list on that view. Each task card shows an Overdue marker only for open tasks whose date has passed (a task due today is highlighted but not overdue, matching the dashboard's count), says Assigned to you when it is yours, names the related member with a link, and shows when a completed task was completed. The actions menu only offers changes that make sense for the task's state, Cancel is now Cancel task so it cannot be mistaken for closing the menu, and deleting asks for confirmation naming the task. Due dates are shown as the calendar date that was entered, in every time zone. API:GET /v2/admin/crm/tasksacceptsstatus=openandstatus=overduealongside the four stored statuses.A script,
scripts/cloudflare-strip-x-powered-by.mjs, that ensures every Cloudflare zone on the account removes theX-Powered-Byresponse header at the edge (the Plesk/Apache origin addsX-Powered-By: PleskLinand has no clean server-wide switch on this setup). Idempotent: run it after adding a domain and zones that already have the rule are left alone. Needs a Cloudflare token with Zone:Read + Transform Rules:Edit inCLOUDFLARE_API_TOKENor.secrets.local/cloudflare-transform-token. Removes the low-severity header-disclosure item O-181 (security register E-089).Admin → Users: an Export all members button next to Import CSV. It downloads the same CSV as Reports → Member reports (every member of the community: name, email, phone, location, role, status, approval, join date, last login, balance, XP, level; spreadsheet formulas neutralised), so the export no longer has to be found under Reports.
Admin dashboard (Mission Control): a Needs your attention strip at the top lists every queue waiting on a coordinator — members and organisations awaiting approval, listings awaiting review, messages to review, exchanges needing a broker, fraud alerts (wallet communities) and data requests (platform accounts) — each a link into the list with the filter already applied, and says All clear when nothing is waiting. Four headline cards (Members, Active listings, Exchanges this month, Hours exchanged this month) carry the change against last month; a second row shows members active in the last 30 days, new members this month, total listings and all-time hours. An Exchange activity chart replaces the text rows: hours exchanged and number of exchanges by month for the last twelve months. API:
GET /v2/admin/dashboard/statsreturns this-month / last-month pairs with a*_delta_pct,active_users_window_days,approved_users, and_partial/_failed_metrics;/trendsdefaults to twelve whole calendar months and reports failed series inmeta.Listings: a My listings page (
/listings/mine) for a member's own offers and requests. It groups them by what other members can see — Live, Waiting for review, Not approved (with the administrator's reason), Expired and Closed — with a count on each tab, an offers/requests filter, and Edit, Renew (or Extend within a week of expiry) and Delete on each listing. The tab and filter are in the address (?tab=expired&type=offer). Until now every list hid anything not live and approved, so a member could not find their own listing once it expired, was waiting for review, or was refused. Reached from the dashboard, a new My listings entry in the Timebanking menu, and a button on the Listings page. Backed by a newGET /v2/listings/mine(owner only, tenant-scoped,status/type/limit/cursor, per-group counts inmeta.counts).Broker panel: every member's name, on every page, now opens the member window (the same window the Members page had), and the window can be reached by link (
?member=<id>), so it survives a reload and keeps the page's filters when closed. The window gains Exchanges, Message copies, Monitoring (with Add to monitoring / Extend), Risk tags and Support needs tabs. Rows on the Members list open it with a click or Enter.Broker panel: Export CSV on Members, Messages, Review Archive, Exchanges, Vetting, Insurance and Risk tags. The export pages the same list the screen shows, so a broker can never export a row they could not see, stops honestly at 5,000 rows and says so, and names the file after the filter. The Archive export carries every column of the record.
Broker Messages: keyboard shortcuts (j/k to move, Enter to open, r to mark reviewed, f to flag; r/f/a/n on a message), listed under
?; Mark reviewed for several routine copies at once (flagged copies are never bulk-reviewed); a date-range filter; quick actions on a message to Add sender to monitoring and Tag the listing. Review Archive: date-range filter and an "Open record" action. API:GET /v2/admin/broker/messagesand/exchangesacceptfrom/to(Y-m-d, inclusive; 422 when malformed); exchanges returnbroker_nameandbroker_decided_at; risk tags returnowner_id.Broker Exchanges: date-range filter; Vetting: status tabs with live counts replace the dropdown. Monitoring: a search box, All / Messaging off / Expiring soon tabs, an Extend action and a Refresh button. Risk tags:
?listing=<id>opens the tag form for that listing; listing titles link to the listing. Configuration: a sticky save bar with Discard, a warning before leaving with unsaved changes, number fields that show their unit, and section jump links.Broker panel: detail pages (exchange, match, message) name the record in the breadcrumb trail.
Shared admin table: optional row click (keyboard-reachable; clicks on buttons or links inside the row are left alone), a controlled search value, and server-side sorting hooks.
Broker dashboard: every queue tile now says how long its oldest item has been waiting and shows a fortnight of arrivals with the change against the previous fortnight. A new My week panel counts the exchanges, messages, matches and vetting re-checks the broker decided in the last seven days (with the average time to review a match on communities that use the exchange workflow). The activity list covers match decisions, dispute settlements, reversals, balance adjustments and member, report and moderation actions; a row opens the record it is about, the "5 minutes ago" carries the exact time on hover, and See all opens the last 100 actions. Waiting for you now also lists members' support needs nobody has seen, pending vetting re-checks and matches awaiting approval. API:
GET /v2/admin/broker/dashboardreturnsoldest_waiting,trendsandmy_weekand acceptsactivity_limitandonly=activity; the broker exchanges and messages lists acceptsort=oldest.Broker panel: the header bell shows the unread count and opens the notifications inside the panel (latest 15, Mark all as read, a link to the full list) instead of leaving it; a light/dark theme switch; Admin panel (when allowed) and Keyboard shortcuts in the user menu, with
?opening the shortcuts list. The search box (⌘K / Ctrl K) now finds members, help articles and recent pages, and offers actions such as Next unreviewed message. An Insurance sidebar badge counts certificates expiring soon or awaiting review. A detail page can name its record in the breadcrumb trail; until it does, the record number is shown.Members can attach up to three screenshots (PNG, JPEG or WebP, up to 10 MB each) to a Help & support request on the web app, by choosing files or pasting a screenshot into the form (support ticket HELP-11). Each image is checked to be a real image and re-encoded, which removes hidden data such as a phone photo's location, and is stored privately rather than at a public address. The screenshots are attached to the Jira help desk ticket, where the member can also see them; if attaching fails, the platform retries for about 17 minutes without raising a second ticket. Admins see them on the report in Support reports. New table
support_report_attachments.On the accessible site, members can attach up to three screenshots (PNG, JPEG or WebP, up to 10 MB each) to a Help & support request too. The form uses the standard GOV.UK file upload, which works without JavaScript and adds a drag-and-drop area when JavaScript is on. Too many files, a file that is too large, or a file that is not a real image is refused with an error next to the upload field, and the rest of what the member typed is kept. A request without screenshots is sent exactly as before. Translated into all eleven languages.
The phone app's Help & support form can attach up to three screenshots or photos too, for any kind of request, each shown as a thumbnail that can be removed before sending. The app shrinks large pictures and converts iPhone HEIC photos to JPEG on the phone, and says why when a picture is left out. A request without pictures is sent exactly as before. Translated into all seven app languages. No new native code, so it can ship as an over-the-air update — but only after a store build that includes the earlier expo-updates 55.0.33 patch, which already blocks an over-the-air update of
main.Staff can tick Remember this device at the two-factor step. Administrators, brokers and coordinators were asked for an authenticator code at every sign-in; on a device they choose to remember they now skip it for up to 30 days. Staff have their own settings under Admin → Module configuration → Two-factor authentication (on by default, 30 days, never more than 30); the members' setting is separate and unchanged. A remembered device stops counting when the setting is switched off or shortened, and as soon as the account's role or an admin flag changes — a device remembered as a member never carries into a promotion (new nullable column
user_trusted_devices.role_fingerprint). Members whose community requires two-factor for everyone still cannot remember a device. Security register E-085.Brokers can now hear voice messages in a conversation they are reviewing (owner decision: a safety feature). Before, the broker's copy showed only "Voice message (N s)" and a transcript when there was one, so a voice message without a transcript could not be reviewed at all. Each voice message in the review thread has a Play recording button; the recording is fetched only when it is pressed, and every play is recorded in the audit log under the broker's name. The same limits as reading the thread apply: only the conversation under review, never a message deleted for everyone, and never a conversation the broker is part of. Available in all eleven languages.
Broker dashboard: a "Waiting for you" section lists the first few items of each queue — new members, unreviewed messages, exchanges needing action, member reports — with the quick decisions right there: Approve a new member (after the same confirmation the Members page asks) and Mark reviewed a routine message. Flagged messages, exchanges and reports only offer Open, because they need reading first. Each list is read through its page's own endpoint, so the same rules apply (a broker never sees their own conversations or reports about themselves), and it refreshes itself after any action.
Broker Messages: on the Unreviewed tab, several messages can be selected and marked reviewed at once. New route
POST /v2/admin/broker/messages/review-bulk(broker or admin, at most 50, 30 per minute). Each message keeps the single-review rules — another community's copy is not found, the broker's own conversation is refused — and is audited on its own, marked as bulk. Flagged messages are never bulk-reviewed: they are skipped and the broker is told to open them one at a time; a message someone already reviewed is left as it was.Broker panel: after a decision, the next item opens straight away. Marking a message reviewed (or approving and archiving it), approving or rejecting an exchange, settling or closing a dispute, and approving or rejecting a match now open the next one waiting in the same queue — a message opened from the Urgent tab moves on to the next urgent one — and say so when nothing is left, returning to the list. Message, exchange and match pages show "3 more waiting · Next". Flagging a message keeps the broker on it, since a flag usually needs follow-up. On the Messages list the message preview now opens the message too (only the sender's name did).
Broker panel: a disputed exchange can now be settled from its page. Brokers were shown disputes as "needs action" but had no button for them, and the guide told them to adjust both members' balances by hand. The exchange page now shows the hours agreed when it was requested and the hours each member says, and offers Settle dispute (choose the hours to pay, within the range set in Configuration, with a note both members see) or Close with no hours (for work that never happened: the exchange is cancelled, nothing moves, both members are told). The settle range on the screen comes from the same calculation the server applies (
ExchangeWorkflowService::disputeHoursWindow()), so the form never offers a figure the server would change. New routePOST /v2/admin/broker/exchanges/{id}/cancel-dispute(broker or admin, refuses a party to the exchange, reason required). The broker Help Centre guide "What can I do when an exchange goes wrong?" is rewritten in all 11 languages.Broker panel: a completed exchange whose credits moved can be reversed from its page with Reverse exchange and a reason. This uses the existing reversal (a correcting entry beside the original; the server's party and rank checks are unchanged). A reversed exchange says so.
Broker dashboard: two new cards, New members to approve and Member reports to check, and "What needs you now" lists every waiting queue instead of the first four. Each card's number is counted by the same rule as the list it opens, with a test that compares the two. The broker sidebar gains a count on Reports, and Members now takes its count from the dashboard.
Broker panel: counts refresh themselves. The sidebar and the dashboard update about two seconds after any broker action anywhere on the page, when the browser tab comes back into view, and once a minute. The shared API client now announces each successful change (
API_WRITE_EVENT, method and path only); the broker panel listens for changes under/v2/admin/.Mobile app: members can now ask for help from inside the app, the same way the website's "Help & support" button works. A new Help & support screen lets a member choose what they need ("Something isn't working", "How do I…?", an account or sign-in problem, or a suggestion), write a short summary and the details, and send it. "Something isn't working" also asks how badly it affects them, and can include the app version, phone system and language, shown to the member before sending and easy to untick. The request goes to
POST /api/v2/support/reports; the screen shows the reference number and says a receipt email is on its way. Reaching the daily limit of five requests, sending too quickly and losing the connection each get their own plain message, and what the member typed is kept. Members reach it from the top of Support & legal, from More → Account, from "Still need help?" at the end of the help answers and guides, and from "Report this problem" on any screen that has crashed. Every string is translated into all seven app languages. This reaches members only in a new app build.Explore's "Recommended for you" now shows offers from other members that match what a member said they need help with (in the welcome wizard or Settings → Skills), labelled "Offers help with something you need". An offer matches when its category or a skill tag has the same name, or the skill appears in its title. Before this, needs played no part in a member's own recommendations, and a new member with no listings of their own got only the general popular-listings fallback.
Organisation admins can now review the expense claims made to their organisation, from a new Expenses tab on the organisation dashboard (
/volunteering/org/{id}/dashboard?tab=expenses). They approve or reject pending claims, mark approved claims as paid with an optional payment reference, and open receipts. Only the organisation's creator and its active owners and admins can do this. Nobody can review, or mark as paid, their own claim, and claims made to other organisations or in other communities are never shown. New endpoints:GET /v2/volunteering/organisations/{id}/expenses,PUT /v2/volunteering/organisations/{id}/expenses/{expenseId}andGET /v2/volunteering/organisations/{id}/expenses/{expenseId}/receipt.When a volunteer submits an expense claim, the organisation's creator, owners and admins now get a notification and an email, each in their own language. The volunteer is not notified about their own claim.
Support reports can now be copied to the Jira Service Management help desk, so they can be answered there. The connection is switched off by default (
SUPPORT_JIRA_ENABLED). It connects as an Atlassian service account, which does not use a help-desk agent seat. Its scoped API token works only through the Atlassian platform gateway, soSUPPORT_JIRA_CLOUD_IDsends API calls there, whileSUPPORT_JIRA_SITE_URLis used for the admins' "Open in Jira" links. All settings are documented in.env.example. A new command,php artisan support:jira-check, proves the connection works while it is still switched off. It checks the key, the help desk and the four request types, and changes nothing. With--create-test-ticketit raises one ticket marked TEST, using the same steps as a real report, and stores nothing on the platform. When the connection is on, every saved report creates a ticket in the background. A Jira outage never loses a report or slows the member down. The ticket carries the reference, community, platform user id, page path, app version, Sentry link and impact (as a priority), and the technical details come as an attached file. It never includes the member's IP hash. By default the ticket is raised under the platform's service account with no member name or email. A second switch,SUPPORT_JIRA_SEND_MEMBER_EMAIL(also off), raises it in the member's name so Jira can email them replies. The admin support-reports view shows the Jira ticket and any sync error. Every 15 minutes the platform reads each ticket's status back from Jira (support:jira-sync-status; read only, it never writes to Jira and needs no Atlassian setting changed). A ticket that is done in Jira marks the report resolved, and a reopened one moves it back to triaged. A report linked to Jira shows its Jira status. Its status can't be changed by hand any more (422 SUPPORT_REPORT_HANDLED_IN_JIRA), but it can still be closed and given notes. On the website, the "Report a problem" button is now Help & support. Members first choose what they need: something isn't working, how do I…?, an account or sign-in problem, or a suggestion. Only "something isn't working" asks how badly it affects them and offers to attach technical details. After sending, the form shows just the confirmation and its reference, and the member is emailed a receipt in their own language. The receipt gives the reference, the type of request and the summary, and never includes technical details. The platform sends it itself, whatever the Jira switches say. If the receipt fails, the request still goes through (SupportReportNotificationService::sendReceipt). The platform receipt is always sent, even when Jira will email the member too: it goes out through the platform's own authenticated domain and reaches the inbox, whereas Jira's first message to a new customer (a "confirm your email address" request from a*.atlassian.netaddress) was seen landing in junk, and Jira holds every later notification until it is clicked. When Jira will also write, the receipt names Jira's sender address (SUPPORT_JIRA_NOTIFICATION_SENDER, defaultjira@<site>) and tells the member to look in their junk folder for that confirmation. With that switch on, the ticket is raised in the member's name and its description also names them and gives their email address. The ticket is raised directly in the member's email address and Jira creates the help-desk customer itself (Atlassian's separate "create customer" call needs a Jira administrator, which the platform's account deliberately is not). If Jira refuses to raise it in the member's name, the ticket is raised under the platform's account with the member's details in it, the refusal is shown on the admin page, and the member gets the platform's own receipt email instead. Every failed attempt is now shown on the admin page, not only the ones Jira answered. A new setting,SUPPORT_JIRA_ASSIGNEE_ACCOUNT_ID, assigns every new ticket to the person who answers the help desk, so Jira emails them. The admin support-reports page shows each report's type, its Jira ticket and status, an "Open in Jira" link and any Jira error. A report handled in Jira can only keep its status or be closed there. The member and admin Help Centre guides are updated to match, in all 11 languages. Reports now carry a request type (broken,how_to,account,suggestion; defaultbroken): onlybrokenasks for an impact and keeps diagnostics. Each member may send at most 5 reports in any 24 hours (429 SUPPORT_REPORT_DAILY_LIMIT).The Onboarding Funnel admin page (
/admin/crm/funnel) now shows who is waiting at each step of a member's journey. Each step explains in plain words what it means ("Confirmed email — clicked the link in their welcome email"), a box at the top names the step where most members are stuck, and "Show who" lists the newest members waiting there with a link to each profile. The page replaces five panels that repeated the same four numbers, three "how to read this page" cards, and terms such as "conversion", "handoff" and "entry stage". The funnel API (GET /api/v2/admin/crm/funnel) addstotal_members,new_last_30_days, and per-stagewaiting/waiting_members.Publishing a new policy or policy version automatically queues a service email for every active community account, including administrators and newsletter opt-outs. Emails include the author's change summary and policy link. A durable recipient ledger prevents repeat notifications and shows queued, submitted, failed, suppressed and uncertain outcomes; delivery blocks remain respected.
Newsletter administration now shows a separate policy email activity section with recipient, queue, submission, provider delivery, bounce, and exception counts for each published version.
Policy emails now record per-recipient opens and policy-link clicks, with a filterable recipient activity view in newsletter administration. Each recipient counts at most once as having opened and once as having clicked; nothing is counted before the email has actually been sent, or in the first two minutes after, when mail-security scanners fetch messages automatically.
An automatic check now confirms the web server will actually start on its configuration files before anything is built. A change made earlier the same day would have stopped the website's web server starting, and was only caught late in testing.
A new automatic check now catches a kind of mistake that has twice put live pages out of action: code calling something with the wrong kind of value, or the wrong number of values. It runs on every change alongside the existing checks, and it already found one real fault (
npm run check:arg-types).Group types now do something for members. The Groups page has a "Group type" filter (also in the phone filter sheet, and kept in the page link so a filtered view can be shared), and each group card shows its type, marked in the colour chosen for it. The filter appears once a community has at least one active group type set up under Admin → Groups → Group types; a type that is switched off is neither offered nor shown. Choosing a type when creating or editing a group already worked and is unchanged (
GET /api/v2/groups/types; group list and detail responses now carrytype).Members can now link their community's own sign-in provider from Settings → Connected accounts while signed in, and then sign in with it. Someone who already has an account and tries that provider first is told to sign in the usual way and link it from their settings (completes F-244).
The 15-minute uptime check (
.github/workflows/uptime-check.yml) can now also post its "site down" and "recovered" messages to Slack, as a second channel beside Telegram. It uses a Slack incoming webhook stored in the newSLACK_UPTIME_WEBHOOKrepository secret and sends the same message at the same moments. Telegram is unchanged. Without the secret the Slack step only logs a warning, so the run does not fail for that reason. With the secret set, a message Slack refuses fails the run, as a refused Telegram message already does. The Slack step also runs when the Telegram step has failed. The repository is public, so the webhook address and Slack's reply are never written to the log; only the HTTP status code is.Brokers can mark a member's support needs as seen, from the new Members' support needs page in the broker panel (
POST /api/v2/admin/safeguarding/member-preferences/{userId}/seen). Each member's row now names, in plain words, the protections their answers switch on (for example "Vetted members only"), with search, a "Not yet seen / Everyone" filter, and a link to open the member. A member who changes their answers shows as not yet seen again. Nobody can mark their own record. The safeguarding dashboard endpoint also returnssupport_needs_unseenandpending_support_actions, which drive new sidebar counts.Every page of the broker panel now shows a clear notice while the community has not chosen its safeguarding jurisdiction. It says what stops working until it is set: brokers cannot record vetting confirmations, and members who asked to be contacted only by vetted people cannot be contacted or matched. Only an admin can set it, so an admin gets a button to the setting and brokers and coordinators are told to ask an admin. The notice disappears as soon as the jurisdiction is saved.
GET /api/v2/admin/broker/dashboardgainssafeguarding_jurisdiction_configured(true/false, ornullwhen it cannot be known), because coordinators cannot read the vetting policy itself. The admin panel shows the same notice on every page, with the button, because only admins can set the jurisdiction and some never open the broker panel.
Changed
Admin — hours that came in through a member import are no longer counted as hours exchanged. An imported opening balance is a carried-over starting point, not a swap between two members, so it is now left out of these figures: the public "hours exchanged" (
GET /v2/platform/stats) and Explore figures, impact and social value, the CRM funnel, the admin timebanking and analytics statistics, municipal impact, the national KISS dashboard's hours, member reports and report exports, the feed sidebar, the AI newsletter figures, the member directory and profile "hours given / received", a member's activity page (hours given and received, the monthly chart and net balance, as the member and their admins see it), the NexusScore, and pending reviews. A member's balance, wallet totals and statements still include it. A few counts that treat every wallet entry as activity, and already count astarting_balancethe same way, still count an import as one entry: the community-impact "exchanges" total, the federation trust score, monthly engagement recognition, and the exchange counts used by matching and shown on job applications. The admin's per-member insights also add the imported hours to the member's earned totals, as they do for a starting balance. The existingstarting_balancehandling is unchanged.Admin — the member import has moved to new endpoints and a new template. It now uses
POST /v2/admin/members/import/checkthenPOST /v2/admin/members/import/{importId}/batch(second-factor confirmed), and the template atGET /v2/admin/members/import/templatehaslocationandbalancecolumns and norolecolumn. A file that still has arolecolumn is refused as the old template; imported members are always ordinary members.Account invitations now last 7 days and say "Set your password" (owner decision, 8 Oct 2026). The email a new member gets when an administrator creates their account — and the same email from assisted onboarding and to a new community's first administrator — used to carry a "Reset Password" button that stopped working after one hour, which few people act on for an account they did not ask for. The button now reads "Set your password" and the link works for 7 days, once. Forgot-password and "Send Password Reset" links are unchanged at one hour.
password_resetsgains a nullableexpires_at(migration2026_10_08_200000, metadata-only); a row without one is a one-hour reset link, so existing links behave as before. Every expiry check and the three scheduled clean-ups now go throughApp\Services\Auth\PasswordResetTokens, so a clean-up can no longer delete a live invitation after an hour, and the email-trigger audit no longer reports an invitation (sent as a welcome email) as a password reset that was never emailed. New email stringsemails.account_invitation.cta/.expiryin all eleven languages.Accounts an administrator creates no longer have to confirm their email (owner decision, 8 Oct 2026). An administrator creating an account vouches for its address. Created with an administrator-set password, or by CSV import, the email is now recorded as confirmed straight away, so the member is no longer stopped at sign-in with "please verify your email" on communities that require it; the create form says so when the welcome email is switched off, so the address gets checked. Created with the welcome email, it is confirmed when the member uses the emailed set-password link, which proves the inbox is theirs — so a mistyped address is never marked confirmed. More generally, completing any password reset now confirms the email, because reset links are only ever delivered by email; until now an invited member set their password and was then asked to verify their email as well. Identity-check holds (F-278) are unchanged. The confirmation rule now lives in one place,
App\Services\Auth\EmailConfirmationService, used by the verification link, password reset and the admin action.Volunteering — when a community admin posts an opportunity, their own organisations come first and are labelled "Yours". Since admins were allowed to post for any approved organisation (7 Oct), the organisation picker mixed the admin's own organisations with everyone else's in one alphabetical list. Their own now sit at the top with a "Yours" tag; members who are not admins see only their own organisations, as before. New key
volunteering.form_org_yoursin all eleven languages.Phone app — version 1.9.0 (Android build 21) prepared for the Play Store. It carries the organiser and volunteer tools listed under Added and an update to the app's update library, so it needs a store release rather than an over-the-air update. The server's record of the newest phone version (
MOBILE_EXPO_CURRENT_VERSIONdefault inconfig/mobile.php) moves from 1.8.1 to 1.9.0; the minimum version is unchanged, so no existing copy is refused.Agent guidance (
AGENTS.md): every open question for the Data Protection Officer is now kept in one private register (.local-docs-archive/dpo/DPO-REGISTER.md, not in the public repository). Agents must read it before changing what personal data is shared, kept or erased, and add an item in the same session when a change raises a new question. No platform behaviour changes.Volunteering → Group Sign-ups is hidden on every community (owner decision, 6 October 2026). The tab let the leader of a community group block out places on a shift and name the people filling them, but those people were never told, were not real shift sign-ups, could not check in and earned no hours, and the add-member screen promised an invitation that was never sent. The platform default is now off; a data migration clears every community's saved "on" value (the admin form re-saves every setting, so most communities held one) so the default actually applies; and the switch under Admin → Module configuration → Volunteering → Tab Visibility stays, badged Alpha, for a community that wants to opt in deliberately. A saved
?tab=group-signupslink now opens Opportunities instead. The accessible site's Group sign-ups link is removed from its volunteering page; the page itself still answers at its address, since that site cannot read per-community tab settings yet. The phone app never had it. Nothing that was booked is deleted.Super-admin tenant form: the Location tab now asks for a place, not coordinates. When creating or editing a community, the "Location Name" box is the same place search members already use for groups and events (
PlaceAutocompleteInput, so it follows the acting tenant'sgeocoding_provider: Google by default, OpenStreetMap or OS Places when configured). Picking a place fills the location name, latitude, longitude and country in one go; the latitude and longitude boxes stay visible and editable as a manual fallback, and a geocoded country outside the curated dropdown list is kept selectable rather than shown as blank. Nothing changes in the API or thetenantstable. Newtenant_form.location_name_helpstring in all eleven admin locales. Regression test:TenantForm.test.tsx("location tab geocoding").Header "More" menu never shows a blank column. A column whose sections are all collapsible (for example only "Tools", when a community has the Engage and Progress modules off) now opens its first section by default, so the heading is not left sitting over empty space. The chevron stays and the section can still be collapsed. The More button itself is hidden when every section in it is gated away.
New Project NEXUS "Powered by" badge artwork in the footer of every community that has not set its own badge, on the web app (light and dark mode) and the accessible site. The wording ("Powered by") and the link (https://project-nexus.net) are unchanged. The new image has a transparent background with a thin white outline, so the same artwork is used in both modes.
Volunteering safeguarding API, for the incident case record.
PUT /v2/admin/volunteering/incidents/{id}no longer acceptsaction_takenorresolution_notes(staff write notes on the case file instead; the old text was carried into each incident's history), requires areasonto resolve, escalate or close, and answers an invalid value with 422 naming the field instead of a generic failure.GET /v2/volunteering/incidentsnow returns only the caller's own reports for everyone, staff included (staff use the admin list), andGET /v2/volunteering/incidents/{id}answers 404 rather than 403 to anyone but the reporter, so a stranger cannot learn that an incident exists. An organisation cannot post an update to a closed incident (409, as for the reporter), and withdrawing a share that is already withdrawn answers 200. Not marked breaking: the only clients of these fields are this repository's web app and accessible site, which change in the same release.Admin → Volunteering: Giving days is now called Fundraising campaigns everywhere an administrator sees it — the sidebar entry (under a Fundraising group), the breadcrumb, the page title, the create/edit buttons and the confirmation messages — because the member page calls the same things campaigns and nobody could find where to set one up. The page description now says where members see a live campaign. The member help article on donations is rewritten for the rebuilt Donations tab (no currency choice, Record a pledge, live / coming up / past campaigns), and the "Donations and partner venues" article no longer tells members to choose a currency.
Admin → CRM → Member notes polish. The note's actions menu sits at the top right of the card again (it had dropped under the member's name); pinned notes carry a Pinned chip as well as the amber edge; the "General" category chip is visible instead of rendering as plain text; the filters are one row (search, member, Clear filters) with the categories as a toggle row below; a results line says how many notes there are and whether a filter is applied; the empty state offers Add note or Clear filters; the edit dialog names the member the note is about instead of asking for one, and pinning is a switch with a plain explanation. The search waits for a pause in typing instead of asking the server on every keystroke, and the filters live in the address (
?q=&category=&user_id=&page=) so a reload, the back button or a shared link keeps them. An Export notes button downloads the existing CSV (admin only; the export applies the same concern-note rules as the list). Pinning from the menu now sends only the pin flag. English wording is sentence case and the page description says what the notes are ("Private notes coordinators keep about members. Members never see them."); the new strings are translated in all eleven languages.Volunteering → Fundraising campaigns: a campaign switched off before its end date now shows as Paused (and can be resumed) instead of Ended, and its organisation can no longer be changed once it has received a gift — the admin picker explains why, and the server refuses it. The admin campaign update endpoint now answers 422 with the reason for invalid input instead of a 500. The member Donations tab links a campaign's organisation to its page, and a receipt for a gift to an organisation says it is held by the community on the organisation's behalf.
Any active member can now be an organisation's Designated Liaison Person (DLP) — owner decision.
PUT /v2/admin/volunteering/organizations/{id}/dlpused to accept only a broker, coordinator or admin, so every organisation that registered would have needed someone given a platform staff role just to name its safeguarding contact. The organisation DLP is a record shown on the safeguarding pages: it grants no access to safeguarding data and receives no notifications, so naming an ordinary member gives them nothing beyond the label (pinned by a test that such a member is still refused the incident list). Still refused: someone outside the community, and a suspended or inactive account. Still admin-only: who may assign it. Unchanged: assigning a DLP to an individual incident still needs a broker-tier role, because that person is sent the incident details and a link into the broker panel. The dialog's help text now says so in all eleven languages; the "must be a broker, coordinator or admin" message is removed from all eleven.Broker panel: the safeguarding jurisdiction is now set on the Configuration page, as the first row of the Compliance & Safeguarding card, beside the other admin-only settings — it was a dropdown with its own Save button at the top of the Vetting confirmations page, a work-queue page brokers use and cannot change it on. It shares the Configuration page's Save bar, Discard and unsaved-changes guard (one Save for the page; it is still sent to
PUT /v2/admin/vetting/policy, and a refused jurisdiction never loses the other settings). Brokers read the value, marked Admin only, with the confirmation it requires. The Vetting page keeps the policy summary and gives admins a link to the new location; the "Safeguarding jurisdiction not set" notice and the dashboard's compact version of it now point there too (the dashboard button pointed at Safeguarding Options, which has no such control). A link to a Configuration card now scrolls to it and focuses it once the page has loaded. Who may set it is unchanged; the API is unchanged. Broker strings in all eleven languages.SECURITY.mdstates that member display images are served publicly by design (F-563; E-089, Cyphere pen test). Profile photos and listing/event images on the public uploads tree can be fetched by anyone holding their 128-bit random address; the scope section now says so, with the reasons, so a future tester gets the answer up front rather than as a Low finding.End-to-end tests: Playwright now blocks the app's service worker in every project except the PWA one. Once the worker controls a page, Playwright's request routing forwards each API request twice, so the single-use session refresh was presented twice and refused as superseded, which showed the test member a "Something went wrong" screen mid-journey on slow runners. Test-rig only; the app and its service worker are unchanged.
Session refresh rate limit (
POST /api/auth/refresh-token, 10 per 60 seconds per client address) is now read fromconfig/auth.php(AUTH_REFRESH_RATE_LIMIT_MAX_ATTEMPTS/AUTH_REFRESH_RATE_LIMIT_WINDOW_SECONDS, floored at 1) instead of being hard-coded. Defaults are unchanged in every deployed environment; only the CI end-to-end stack raises the ceiling incompose.ci.yml, because it drives every browser from one address and the production ceiling was signing test actors out mid-journey. Pinned by three tests inAuthControllerTest.CI: the mobile API route snapshot is refreshed for the two admin download routes added on 4 October, and the deploy-probe test gives the render waiter a three-second grace so one slow poll cannot end it early. No user-visible change.
Frontend test suite: the two shrink-only test baselines (test-file type errors and dead barrel mocks) were regenerated downward after the admin Settings, dashboard and download work, nine download tests read their recorded calls safely, and the Settings smoke test renders inside the confirmation-dialog provider. No user-visible change.
Admin → Settings redesigned as one page with one Save. Section jump links at the top (each one shows an amber dot while that section has unsaved changes), a card per section in the same style as the dashboard and the broker panel, and a Save bar that sticks to the bottom and reads Unsaved changes or All changes saved. Save is disabled until something changes, Discard puts the loaded values back, and leaving the page with unsaved changes asks first. The lower Additional configuration (localisation, onboarding, wallet, moderation, notifications, limits) no longer has its own Save, Reload and Reset buttons: one Save writes both halves, says plainly if one half failed and keeps that half marked unsaved, and Reload from server / Reset additional configuration live in a small menu on the Save bar (Reset asks for confirmation). Who can join is now a three-way choice, Open / Invite only / Closed, with a line explaining the chosen mode. Every logo slot (partner, header light and dark, powered-by) previews the image on both a light and a dark background, replaces a broken image with a notice rather than an empty box, and says whether uploads and removals apply at once or on Save. Header colours use the platform colour picker with a hex field and Use default. The code is split into one component per section under
react-frontend/src/admin/modules/system/settings/, with a sharedAdminSaveBaranduseUnsavedChangesGuardinsrc/admin/components/;SystemConfigis now presentational overuseSystemConfigForm.Admin dashboard: every number card is now a link into the page that explains it (Users, Listings with the matching status tab, Timebanking, Hours report, Member reports). "Active users" means members seen in the last 30 days rather than approved accounts, which was nearly everyone. Exchange figures and the chart count only hours one member gave another: opening balances, admin adjustments, donations and reversals are left out, so a 3,000-hour opening-balance import no longer flattens every other month. Quick actions are a single row of links. On a first load the page shows a placeholder in its final shape; if the figures cannot be loaded it says so with a Try again button instead of a row of dashes, and a figure that could not be computed is marked Could not load on its own tile rather than shown as zero.
Broker panel lists (Members, Messages, Archive, Exchanges, Match approvals, Vetting, Insurance, Risk tags, Monitoring) refresh quietly after any broker action and when the tab regains focus, keeping the page and filters, with no flash. Insurance status tabs go from eight to five (Rejected / Revoked merged; older status links still work and are named in a chip). The ten admin pages embedded in the broker panel (content queue, moderation, safeguarding options, support needs, guardians, support actions, volunteering incidents) now use the broker tiles, empty states and loading placeholders, put their Refresh button in the page header instead of an otherwise empty card, and refresh themselves too; the admin panel itself is unchanged. The nine-section "How safeguarding works here" guide is one collapsed panel that remembers whether you left it open.
Broker panel code: the Members page, member window, Messages, Message detail, Review Archive, Exchanges, Vetting, Insurance, Risk tags, Monitoring and Configuration pages were split into shared components (
src/broker/components/{member,messages,exchanges,vetting,insurance,risk-tags,monitoring,configuration}); duplicated dialogs (flag message, approve/reject exchange, reject match, insurance form, member notes) exist once; the three hand-built member pickers use the sharedMemberSearchPicker; Configuration rows render from a settings schema. Behaviour pinned by the existing tests.Broker dashboard layout: the quick links are one compact row instead of seven cards, Waiting for you sits beside My week on wide screens, a tile whose figure could not be computed is marked on the tile itself, a compact card points at the safeguarding-jurisdiction setting while it is unset (the panel-wide notice no longer doubles up with it on the dashboard), loading placeholders match the real tile shapes, and a settings save that changed nothing no longer prints "0 settings changed".
Broker panel shell: the sidebar remembers whether it was collapsed; collapsed mode shows the count on each icon instead of a red dot; badges are coloured by meaning (red for queues to clear, amber for attention, neutral for standing counts such as monitored members); Guardians and Configuration have their own icons; the shortcut hint names the key for the broker's computer (⌘K or Ctrl K); page changes show a placeholder inside the content area instead of a full-screen loader; "Safeguarding" and "Moderation" breadcrumbs are no longer links to pages that only redirect.
Broker panel lists: the Exchanges header gains Needs action and Disputed cards fed by the same counts as the tabs; the Messages and Review Archive stat cards count the whole queue or archive (new labels "Flagged messages", "Approved records" and so on) rather than the rows on screen, and the Urgent tab shows its count; the Members page keeps its search and role filters in the address bar so a filtered view can be linked to; exchange hours, match listing types, dates and the quick-view copy reason are translated or server-formatted; the raw "↔" is an icon; Reject uses the panel-wide soft red style; stats-failure notices on Vetting and Insurance use the shared warning alert; the notes dialog closes with Close and shows a spinner while loading.
The technical details attached to a "something isn't working" report no longer fill up with the checks the web app repeats every minute (online count, notification and message counts, presence). A successful check is now left out and only counted; a failed one is still kept. Before this, those checks pushed the member's own actions out of the 50-entry log within about ten minutes — on support ticket HELP-10, the poll the member had created half an hour earlier was no longer in it.
The Safeguarding Support actions page is rewritten in plain words, in all eleven languages. Its first list is now titled "Waiting for a member's yes" and explains that a supporter set these up, nothing happens until the member agrees, and staff can record the member's yes if the member gave it by phone, in person or on paper (the button reads "Record the member's yes"). The second list, previously "Authority records" with "act-alone power" and "Authority sighted", is now "Proof that a supporter may act alone": a note that staff have seen the legal document behind it, which gives nobody any power. The broker sidebar description and the brokers' Help Centre line for the page say the same. On a phone, both lists on this page are now shown as cards with their button on screen, instead of tables that had to be scrolled sideways. What the page does is unchanged. The page now opens with a "What this page is for" box that explains linked accounts and the three levels a member can give a supporter (Nothing, Prepare only, They can act on their own, including that an unanswered item lapses after 14 days), and both list introductions say in plain words when a broker needs to act.
Broker panel lists fit the screen. On a phone, each row of the ten broker list pages (Members, Onboarding, Exchanges, Match Approvals, Messages, Vetting, Monitoring, Risk Tags, Insurance, Review Archive) is shown as a card with its buttons at the bottom, instead of a table that had to be scrolled sideways; count boxes sit two to a row. On a laptop the buttons column stays pinned to the right edge, and the least useful columns (Members' role, last active and joined dates; Messages' "Flagged"; Vetting's requirement, already shown above the table) appear only on very wide screens, so Members, Messages and Vetting no longer hide their buttons off the edge. Pending members have a visible Approve button instead of only the last item of the "⋮" menu. The moderation pages (Feed Posts, Comments, Reviews, Reports, Content Queue) show cards on phones too, with Hide / Delete / Resolve / Dismiss / Approve / Reject along the bottom of each card, and their count boxes two to a row; they use a new
ModerationCardscomponent built from the same cells as the table, so these pages change on phones in the admin panel as well (tables on larger screens are unchanged). The broker list pages use opt-in options on the shared adminDataTable(mobileCards,stickyActions, columnhideBelow/hideInCard/isActions), so admin tables there are unaffected.Help & support is now easy to find on every screen size for signed-in members. On phones and tablets (below 1024px) a round life-ring button sits just above the bottom navigation bar, in the bottom-right corner; it is announced as "Help & support" by screen readers, clears the podcast mini-player and the phone's home-indicator area, steps aside while the on-screen keyboard or a bottom sheet is open, and is not shown in a message conversation or during onboarding, where the bottom bar is hidden too. Before this the button was hidden on phones and the only way in was a small link at the bottom of Menu. Menu now has a full-width Help & support row near the top, under the account summary, and the profile menu (the avatar at the top right) has a Help & support item on every screen size. On computers the bottom-right button keeps its label and has clearer contrast in light and dark mode. The Help & support form shows the four kinds of request as cards with icons (two columns on wider screens), a short introduction, full-width buttons on phones, and a clearer confirmation with the reference in a box and a note that an email receipt follows. The back-to-top button now stacks above the round button up to 1024px. The members' Help Centre guide "How do I report a problem?" says where to find the button on a phone or tablet, in all eleven languages. The request itself, and what is sent to the server, are unchanged.
The accessible site's "Report a problem with this page" form is now Help & support, matching the main site. Signed-in members choose one of the same four kinds of request (Something isn't working, How do I…?, Account or sign-in problem, Suggest an improvement), and "How much did this affect you?" now appears only under "Something isn't working", as a standard GOV.UK question that opens when that option is chosen (it is always shown when JavaScript is off). The form now sends
request_type, and sendsimpactonly for "Something isn't working"; until now every request from the accessible site was recorded as a problem. Signed-in members also get a Help & support link in the main navigation at the top of every page, which records the page they came from; the footer link stays where it was, now also called Help & support. The page's wording is updated in all eleven languages.The broker panel's Vetting page shows everyone the community's safeguarding jurisdiction (owner decision, 3 October 2026). Brokers and coordinators see it as plain text, marked Admin only, with a line telling them to ask an admin if it needs changing; only admins get the picker. The setting is now called "Safeguarding jurisdiction" on that page instead of "Safeguarding policy package", in all eleven languages, and the admin Help Centre guide's safeguarding set-up articles name the new labels. The server enforces the same rule: only an admin can save the jurisdiction (
PUT /api/v2/admin/vetting/policy), and a broker or coordinator is refused even if their account carries an admin flag. This reverses the 2 October change that opened the save to brokers (F-546), which was never released. The broker and admin Help Centre guides now say "jurisdiction" instead of "policy package" throughout their safeguarding articles, in all eleven languages, name the Admin only mark and the new notice, and quote the current on-screen labels (several French, Spanish, Italian and Portuguese labels there were already out of date).The broker panel's Safeguarding page is now separate pages, each with its own sidebar link under a new Safeguarding heading: Members' support needs (first), Guardians and Support actions, plus Volunteering incidents where volunteering is switched on. Until now they were tabs on one page, and brokers said things were hidden. Its Flagged messages tab listed the same message copies as the Messages page, so it is merged into Messages: a new Urgent tab there shows flagged messages nobody has reviewed yet, and the dashboard's Safeguarding Alerts card opens it. Messages also gains a search across the message text and both people's names (
GET /api/v2/admin/broker/messages?filter=urgent&q=…), which searches the whole queue, not just the page on screen. Old links (/broker/safeguarding?tab=…) still land on the right page. Alerts and emails about volunteering incidents now open Volunteering incidents, and alerts about one member's answers open that member on Members' support needs. Each page loads only its own data. The broker guide's Safeguarding article is rewritten to match, in all eleven languages; it also no longer says the page needs special access, which stopped being true on 2 October.The broker dashboard's "Onboarding Flags" card is renamed "Support needs not yet seen" and now counts exactly the members the Members' support needs page lists as not yet seen.
Community admins are now emailed about every Help & support request, whatever its impact. Until now only "I cannot continue" and "A core task is broken" reports sent an email; the rest raised only the bell notification, which was being missed. Each member is still limited to 5 requests a day.
The broker panel has been tidied up for brokers who are not technical. Every page now has a "How this page works" link straight to the matching article in the plain-English broker guide. The redundant "Back" button has gone from the header of each list page (Exchanges, Messages, Vetting, Risk Tags, Insurance, Review Archive, Settings), since the sidebar is always there; detail pages keep theirs. The figures on the Messages and Review Archive pages now say what they count ("Flagged on this page", "Matching this filter") instead of "Flagged in view" and "In current filter", the Vetting page no longer speaks of "attestations" or "UK-wide" checks, and the one-line descriptions under each page title say in everyday words what the page is for. The dashboard's Recent Activity list shows what happened in words ("3 settings changed", "Message #87 · with a note", "Listing #164 · was High risk") instead of the raw data it used to print. The Safeguarding page's flagged-messages table shows the reason in words ("Random sample", "First contact") instead of codes. All eleven languages are updated.
The welcome wizard's skills step now saves into the member's real skills (Settings → Skills), the list that matching, Explore and the personalised feed use. Members tap one of their community's categories or type their own, under "I can offer" and "I need help with", and the step starts with any skills they already have. Before this, the step saved listing categories into a separate store that nothing outside the wizard ever read.
POST /v2/onboarding/completenow takesskills: {offer: string[], need: string[], replace?: bool}. App versions still on the stores keep working: theiroffers/needscategory ids become skills named after the category, and nothing is cleared. The response no longer carrieslistings_created/listing_ids, andGET /v2/onboarding/statusnow returns an emptyinterestslist.The link pinned at the bottom of the admin sidebar now opens the platform's own Help Centre guide for community admins (
/help/admins) instead of the developer documentation site, which was too technical for most admins. It opens in the same tab like every other sidebar link. The separate "Help Centre" entry under System, which duplicated it, is removed; the Caring Community panel keeps its own link to the Caring admin help page.Community admins can no longer mark a volunteering expense claim as paid: the organisation that pays the volunteer records payment from its own dashboard. Community admins still see every claim and can approve or reject pending claims on the admin Expenses page, which now shows approved claims with a note instead of a "mark as paid" action.
PUT /v2/admin/volunteering/expenses/{id}withstatus: paidnow answers403.Guests now count towards an event's limit, so an event shows fewer places available and can refuse a booking it would previously have accepted — the stated limit is now actually enforced, which matters where it is a fire limit. Cancelling a booking now also cancels the guests brought with it and frees their places for someone else (F-356).
A podcast episode or show that someone has reported can no longer be deleted by its creator while the report is still open — they are asked to archive it instead, or wait for a moderator. The reports themselves are now never deleted, so a creator can no longer clear their record by deleting and re-uploading the same audio (F-338).
Creating, editing, publishing, closing and exporting community surveys now requires a community administrator. The "official notices" switch previously granted this as well, which was never intended and let someone download every survey answer with the respondent's identity attached (F-347).
The roles and permissions screen now lists only the permissions the platform actually enforces. It previously offered 66, of which one worked — ticking any of the others told an administrator they had delegated something when they had not (F-363).
The "Roles & Permissions" link under Enterprise in the admin sidebar is now shown only to platform god users. Community administrators no longer see it in the sidebar; the page and its API are unchanged.
The Communications section of the admin panel (Email Settings, Email Deliverability, Deliverability) is now shown only to god accounts; super admins previously saw it too. Opening one of those pages by its address now returns anyone else to the admin dashboard. The underlying API permissions are unchanged.
The Regional Analytics screen (Analytics & Reporting → Regional Analytics) is now available only to god accounts, until it is reworked. Its figures are drawn almost entirely from volunteering logs and Caring Community help requests, so a timebank without those modules saw a page of zeros. The link is hidden from everyone else, opening it by its address returns them to the admin dashboard, and its API now refuses anyone who is not a god — including platform super admins.
The Performance page (Analytics & Reporting → Performance,
/admin/performance) is now shown only to god accounts. The link is hidden from everyone else, and opening the page by its address returns them to the admin dashboard. Its API permissions are unchanged.The Impact Report link (Analytics & Reporting → Impact Report,
/admin/impact-report) — the social impact / social value report — is now shown in the admin sidebar only to god accounts. Other administrators no longer see it in the sidebar; the page and its API are unchanged.The Monitoring link (Enterprise → Monitoring,
/admin/enterprise/monitoring) is now shown in the admin sidebar only to god accounts. Other administrators no longer see it in the sidebar; the page, the Enterprise dashboard shortcut to it, and its API are unchanged.The Prerender Engine page (Growth & Discovery → Prerender Engine,
/admin/seo/prerender) is now shown only to god accounts; platform super admins previously saw it too. The link is hidden from everyone else, and opening the page by its address returns them to the admin dashboard. Its API permissions are unchanged.The whole Growth & Discovery section of the admin panel is now shown only to god accounts: SEO Overview (
/admin/seo), SEO audit, URL redirects, Search Analytics, Prerender Engine and 404 Error Tracking. The section is hidden from every other administrator — platform super admins included — its pages no longer appear in sidebar search, and opening any of them by its address returns them to the admin dashboard. The underlying API permissions are unchanged.The Enterprise Dashboard (
/admin/enterprise) is now shown only to god accounts, because much of it is unfinished. Its link is gone from the Enterprise section of the sidebar and from the admin home page's "Advanced Controls" shortcut for every other administrator, platform super admins included. Opening it, or the Roles & Permissions and Permissions pages (already hidden from the sidebar), by address now returns them to the admin dashboard. GDPR Dashboard, Legal Documents and Compliance Dashboard are unchanged. The underlying API permissions are unchanged.Cron jobs are now shown only to god accounts. The four Platform Operations links (Cron Jobs, Cron Logs, Cron Setup, Cron Settings) are hidden from everyone else — platform super admins included — and opening one of those pages by its address returns them to the admin dashboard. The Background Jobs list on the Operations page, which is the same cron jobs, is shown to god accounts only; other administrators see the cache panel alone. The underlying API permissions are unchanged.
Volunteering now has its own section in the admin sidebar, listing every volunteering page: Overview, Applications, Hours to verify, Shift swaps, Expenses, Community projects, Organisations, Training, Safeguarding & incidents, Giving days, Donation refunds and Settings. Previously the sidebar linked only to the volunteering overview, and the other pages could be reached only through shortcut cards at the bottom of it. Donation refunds moved here from Financial. The Organisations link shows how many organisations are waiting for approval and appears under "Needs attention" when any are. Each page can also be found by everyday words in the sidebar search, such as "charity", "receipts" or "timesheets".
Admin pages that worked but were linked from nowhere now have sidebar entries: group join requests and group moderation (Community), Courses, audience segments (Marketing), SEO audit and URL redirects (Growth & Discovery), marketplace reports & disputes and coupons, badge settings (Engagement), registration policy and the admin Help Centre (Platform Operations), and Swiss data protection (FADP) under Enterprise. Each link appears only when its module is switched on. The permissions list (beside Roles & Permissions) and subscriptions (beside Plans & Pricing) are shown to god accounts only, like their neighbours. Page and API permissions are unchanged.
The Volunteer organisations admin page (
/admin/volunteering/organizations) has been reworked so decisions cannot be missed. Organisations waiting for approval are now listed in a "Waiting for your approval" panel at the top of the page, each with its contact details, registration date, a short description and full-size Approve and Decline buttons. Previously those buttons sat in the last column of an eight-column table and could only be reached by scrolling sideways. The table now has five columns (organisation, status, activity, wallet balance, actions), so it fits on screen. Edit, members, wallet and suspend actions are grouped in one Manage menu per organisation instead of a row of buttons. A row of status tabs with counts (All, Waiting for approval, Active, Suspended, Declined) replaces the cramped status dropdown, and dates now read "8 Jan 2026" rather than the ambiguous "8/1/2026". Organisations stored as "approved" now count as Active.
Removed
Admin — the old member import endpoints are gone.
POST /v2/admin/users/importandGET /v2/admin/users/import/templateare replaced by/v2/admin/members/import/*(see Added). Only the admin pages of the website called them (nothing in the phone app or the accessible site does), and the admin page and the new endpoints ship together, so nothing needs changing for anyone. Their old tests moved with the new endpoints; a test now confirms the old addresses no longer exist.Volunteering — the retired guardian-consent code is gone. Guardian consent was switched off when the platform became adults-only (25 Sep 2026), leaving code that could no longer run:
GuardianConsentService(requesting, granting and withdrawing a guardian's consent, and the email it sent to the guardian), its container binding and email-audit entry, and the volunteering consent check behind seven apply / shift sign-up / waitlist / group-booking paths, which was hard-wired off. All removed, with their strings in 11 languages and three test files. Unchanged: the six volunteering guardian-consent addresses still answer 410 "retired" so an old app version gets a clear message; thevol_guardian_consentstable and its history stay for data export and retention; and the nightly job that marks lapsed consents as expired still runs (its query moved into the job runner, with its tests).Admin — the "Hours review" tab switch is gone. The volunteering setting
volunteering.tab_hours_reviewappeared in Module configuration but nothing read it: the volunteer's page has no hours-review section, and the organisation dashboard's Hours review tab never checked it. Removed from the settings list, the server defaults, the website's types and defaults, and its labels in all 11 languages (with an unused duplicate inadmin_contentand an unused top-leveltab_hours_reviewlabel). Any stored value is simply ignored.Volunteering — unused code and API routes removed. Nothing called any of these: the website's orphan hours-review screen (
HoursReviewTab, superseded by the organisation dashboard's) and itsGET /v2/volunteering/hours/pending-review;GET /v2/admin/volunteering/expenses/export(the admin page builds its CSV itself) and the unusedadminVolunteering.exportExpenses/getApplicationshelpers;GET /v2/admin/volunteering/applications(the page uses/approvals);GET /v2/volunteering/wellbeing/my-status;GET /v2/volunteering/giving-days/{id}/stats; the signed-inGET /v2/volunteering/certificates/verify/{code}(the public check replaces it, and its tenancy test now pins the check); the legacyGET /vol_opportunities; andVolunteerController::show(). Their service methods and tests went with them;openapi.json, the volunteering module guide (whose certificate paragraph predated the public check) and the mobile route inventory are updated. Two doc comments that named non-existent/applications/{id}/approve|declineroutes now name the real/approvals/...ones.The "Currency" box on the volunteer expense claim forms (website and accessible site) is gone. The server has always recorded a claim in the community's own currency and ignored the box, so it could only mislead. The API still accepts and ignores a
currencyfield, so older app versions keep working.The welcome wizard's "What are you interested in?" step is gone. Nothing on the platform ever used its answers. Interests sent by older app versions are accepted but no longer stored. The admin switches for the step (
onboarding.step_interests_*) are gone and any saved values are ignored.The admin "Listing creation" setting for onboarding (
onboarding.listing_creation_mode/listing_max_auto) is retired, and onboarding never creates listings. The setting turned the skills step's answers into listings; it was off in every community on the live platform, so no member sees a difference.Three admin pages that could no longer be reached have been deleted: the "group geocoding" placeholder page (it only said the screen had not been rebuilt yet), "Smart Match Users" (replaced by the broker match approvals page) and "Smart Match Monitoring" (replaced by the matching analytics page). Since the F-533 change their old addresses already forward to those current pages, and those forwards stay in place, so nothing changes for anyone using the admin area. Their tests and the translations only they used were removed with them, including the whole
admin_communitytranslation file in all eleven languages.
Fixed
Members — the background map lookup for members' towns can no longer get stuck on towns it cannot find. Every 30 minutes the platform looked up the map position of up to 50 members per community who had a town but no position, with no ordering and no record of having tried. A community with 50 towns the map service could not find was asked about the same 50 on every run, and nobody after them was ever looked up; a large member import would have made this much worse. A new command,
members:geocode-pending(every minute, all communities, up to 10 members and about 12 seconds per run so it does not hold up the other scheduled jobs, one request a second as the map service requires), marks each member as attempted before asking, takes never-tried members first and then the longest-ago attempts, and asks about an unfound town again only after 7 days, or at once if the member (or an admin) changes their town. The 30-minute job still looks up listings but no longer members; the manual/cron/geocode-batchpage uses the same safe lookup, capped at 20 members and about 20 seconds because it runs inside a web request. A map-service failure (too many requests, a server error, a timeout) is treated differently from "town not found": those members are asked again in about an hour rather than a week, and the run stops after three such failures in a row, so a short outage cannot park hundreds of members. Uses the newusers.geocode_attempted_atcolumn (migration2026_10_09_100100). Becauseusershas a full-text search index, adding it is not instant: MariaDB rebuildsusers, and writes to it wait until that finishes (about 2.5 seconds for 63,000 rows in a local test), so check the production row count before deploying.Infrastructure — a full Redis no longer silently deletes queued jobs and locks. Production Redis (
nexus-php-redis), which holds the queue including delayed jobs, Horizon's records, the cache and every lock, ranmaxmemory-policy allkeys-lru. When full, that deletes any key without an error. The policy had been changed tovolatile-lruin the oldcompose.prod.ymlon 2026-05-02, but the container (created 2026-05-01) was never recreated, and that file was deleted on 2026-08-09. That left production Redis with no definition in the repository. It is now defined incompose.redis.ymlwithnoeviction, so a full Redis refuses writes loudly instead. The 400 MB data limit is unchanged, and the container memory ceiling rises from 512M to 768M to leave room for Redis rewriting its save file. Localcompose.ymlandcompose.ci.ymluse the same policy. Every deploy now re-asserts the policy and logs usage and evictions (secure_redis_eviction_policy), and stops only if the policy cannot be set. Measured before the change on 2026-10-09: 3.8 MB peak in 7.9 days, 0 keys evicted. The policy was switched live on production the same day, which does not survive a Redis restart until the container is recreated from the new file. Regression test:scripts/test/test-redis-eviction-policy.sh.Registration — the welcome email no longer splits the surrounding database transaction.
SendWelcomeNotificationranCREATE TABLE IF NOT EXISTS email_verification_tokensevery time someone registered. MariaDB commits any open transaction before a table-creation statement, even when the table already exists, so work that should have been all-or-nothing around a registration could be half-saved. In tests it ended each test's automatic rollback, so one registration test's community settings stayed in the test database and made another test fail when the two ran side by side. The listener now checksSchema::hasTable()first and only creates the table if it is genuinely missing (it is part of the normal schema). Regression test:tests/Laravel/Feature/Listeners/SendWelcomeNotificationTransactionTest.php. The recovery commandemails:resend-stuck-activations(ResendStuckVerificationEmails) had the same unconditional create and now has the same check; its own tests had been leaving their seeded test communities, members and verification links in the test database.Admin — the member import results screen showed labels without numbers. The result boxes ("Imported", "Skipped" and so on) appeared with no counts. The rebuilt import shows every count, and the phone numbers and negative numbers that the platform's own export writes with a leading apostrophe now come back in without it.
"Resend Welcome Email" now gives a member who has never signed in a way to set their password. On Admin → Users → a member, it used to send only a "sign in" button, which led nowhere for someone with no password — an invited member whose link had lapsed, or an imported member. For a member who has never signed in it now sends a fresh 7-day set-password link instead; for anyone who has signed in it is unchanged. The link is issued only when the administrator outranks the member, the same rule as "Send Password Reset". A community's own full-page welcome template is replaced by the standard wording for that member, because it has no place for the link.
Admin → Volunteering → Applications — tidied up, and every row now has options. A declined application showed as "Unknown", because the shared status label had no "Declined" (it now reads Declined, in red, everywhere the admin area shows that status). The status tabs, search box and opportunity filter were squeezed into the table's header slot beside an empty grey block and a second Refresh button, and the filter's label wrapped mid-word; they now sit in their own bar above the table, with an "All opportunities" choice, and wrap neatly on smaller screens. On narrower screens the applied date moves under the opportunity name instead of making the table scroll sideways, and on a phone each application is a card. Approved and declined rows used to offer nothing; every row now has a menu to open the member's admin page, open the opportunity or email the applicant.
Phone app — after voting in a poll, your choice was not marked. While a poll is open the server hides the vote counts from everyone but its creator, and in that state the phone app kept drawing every option as an untouched button; the only sign of a vote was a small "You voted" chip. Once you have voted (or the poll has closed) the options now become a read-out with your choice ticked in a solid circle, outlined and labelled "Your vote", as the website does — with no bar or percentage while the counts are still hidden. "Your vote" is translated in all seven app languages. Reaches members with the next phone app update.
PollCard.test.tsxcovers it.Volunteering — nobody was emailed about a new application, and the applicant got no confirmation. The "new application" notice went only to the member who posted the opportunity, never to the organisation's owner or its admins, and because it was not one of the notice types sent immediately it was never emailed at all to anyone on the default (off) digest setting — only a bell. Applying also sent the applicant nothing. Now everyone who manages the organisation (the organisation's own account, its active owners and admins, and whoever posted the opportunity) gets a bell, a push and an email straight away, once each; the applicant gets a "your application was sent" email (new type
vol_application_submitted), or the acceptance email at once when the community approves applications automatically. Both are rendered in each recipient's own language, and a failure to send is now logged as an error instead of being silently ignored. These emails also have their own subject lines instead of "Notification from Project NEXUS". Reported by the owner;VolunteerApplicationNotificationTestcovers it.Volunteering — a second owner of an organisation could not be removed or stepped down. The person who registered an organisation always runs it and can never be removed, but the "an organisation must keep at least one owner" check counted only team records, and most organisations have no team record for the person who registered them. So once someone else was made an owner, neither the organisation's owner nor a community admin could remove them or change their role: both were refused with "make someone else an owner first". The registering member now counts as an owner while their account is active, and both team lists (the organisation's Team tab and the admin Members window) show them as the owner, first, whatever their team record says. Found by walking the Team tab in a browser;
VolunteerOrgMembersTestcovers both cases.Volunteering — a volunteer could be checked out before their shift had started. Check-in opens 30 minutes early, so an organiser could close a volunteer's attendance with no time on the clock (seen 24 minutes early). Checking out is now refused until the shift's start time, with the start time in the message (gap C13).
Volunteering organisation team screens (website and admin) now name an organisation account by its organisation name rather than its contact person, and the Arabic and Irish "usually valid for 1 year" hints now show the number.
Accessible site: the new organisation Team table now scrolls sideways inside its own focusable region on narrow screens, and the status tags on the Team, qualifications and revoked-certificate pages use the standard GOV.UK tag component. Four accessible-site tests that still described the removed document-upload page were updated to the qualifications register.
Developer tooling: the disposable web-uk test environment (
compose.webuk-e2e.yml) now uses its own Redis database (REDIS_DB=1). Its background-job queue was shared with the ordinary local stack, so a queue worker started in either ran the other's jobs against the wrong database.scripts/webuk-e2e-env.sh resetnow empties that database.Volunteering — shift-swap notifications are no longer dropped when two arrive within a minute. Every swap notification pointed at the same link, so the platform's one-minute duplicate guard (which deliberately folds a burst of chat messages into one alert) dropped, for example, "approved by an admin" right after "your swap was accepted", or a second swap request to the same person. Each swap, and the admin's decision, now has its own link; the duplicate guard itself is unchanged.
Volunteering — changing a repeating shift's days now updates its shifts straight away. Editing the weekdays, times or dates of a repeating pattern used to change only the pattern: shifts on dropped days stayed listed and the new days appeared only when the nightly job next ran. Future shifts that no longer fit are now removed and the next two weeks are created at once. A shift somebody is booked on (or that has any check-in, swap, expense or incident attached) is kept, never taken from them, and the response says how many were kept.
Phone app — a sent shift-swap request no longer reads "Sent to" with a blank name. Since the server hides who you asked until they agree, the card now says the request went to the volunteer on that shift and that they will be named once they agree (as the website does). The organisations tab's empty message no longer offers to "join" an organisation, which the app cannot do; it says to register one or ask its owner to add you. Unused check-in wording for a QR screen the app does not have was removed. Takes effect in the next app release.
Volunteering admin — the Applications page now shows every application, one page at a time. It used to load at most 150 applications and search, filter, count and export only those, so in a busy community the oldest decided applications were missing from every tab, count, search and export. Search, the status tabs, the opportunity filter and the counts now run on the server; the export includes every matching application; the filters are kept in the page address. Pending applications still come first.
GET /api/v2/admin/volunteering/approvalsnow takesstatus,q,opportunity_id,pageandper_pageand returns{ items, total, page, per_page, counts, opportunities }instead of a bare list (its only caller is this page).Admin breadcrumbs: the volunteering Certificates page showed a warning marker instead of its name. The trail now reads Admin / Volunteering / Certificates in all eleven languages.
Volunteering — "Usually valid for 1 years" now reads "1 year". The qualification form's typical-validity hint on the website and the accessible site now uses the right singular and plural forms in all 11 languages (including Arabic, Irish and Polish, which have more than two).
Data export — a revoked volunteer certificate now shows as revoked, with the reason. A member's personal-data export listed their volunteer certificates but not whether a community admin had revoked one or the reason the admin recorded; both are personal data about the member and now included.
Developer tooling: the pre-commit hook now runs the staged PHPUnit files (gate B) and the native push producer audit (gate C) in the app container as
www-datainstead of root. Run as root,Storage::fake('local')left root-owned directories understorage/framework/testing/disks/local(e.g.volunteer-credentials,volunteer-expenses) and a root-owned.phpunit.cache, so every laterdocker exec -u www-dataPHPUnit run failed those tests with "Failed to open directory: Permission denied" (three times on 2026-10-07). Before running PHPUnit the hook now also hands any root-owned files in those two test-only locations back towww-data(ownership change only, nothing deleted). Pinned byscripts/test/test-precommit-runs-php-as-www-data.sh(stubbed docker), run in the Migration Safety Gate job.Volunteering → post an opportunity (website): a community admin can now choose any approved organisation in the community, not only ones they belong to, as the platform already allowed; and "Create opportunity" from an admin's Community projects page now starts the form with the project's title, which it used to drop.
Admin → Volunteering overview: the quick-action buttons and the alert banner went to addresses without the community's own prefix, so on a community reached by its address (for example /hour-timebank/admin) they left that community. They now stay in it, as every other admin link does.
Volunteering emails: the email telling someone their community project proposal was approved, declined or is under review linked to a page that does not exist; it now opens the Community projects section. The safeguarding-training expiry reminder linked to a section name the website does not have and opened the list of opportunities; it now opens Safeguarding (on the accessible site, the training page).
Volunteering → Safeguarding (website): Report Incident now shows on the section's default view, beside Add Training. Until now a member had to switch to Incident Reports before the button appeared, so someone wanting to report a concern might never find it. An empty incident list also offers it.
Volunteering → Expenses (website): someone with no organisation to claim from opened the claim form and found only a notice and a disabled button. The notice now offers Find opportunities and Check my applications, as the Hours tab does.
Volunteering (accessible site): empty pages that named an action now link to it: an empty waitlist links to the opportunities; no shift suggestions links to Your skills; no organisations (and the create-opportunity page with none) link to Register an organisation; no opportunities links to the community Feed where the community has one. An organisation's review page no longer says "No one has applied yet" once it has volunteers.
Volunteering (website): five places described an action without offering it. An empty waitlist now links to the opportunities (a waitlist is joined from a full shift); an empty swaps list links to My Applications, where a swap is asked for; an empty expenses list has the Submit Expense button its text refers to; an empty community projects list has Propose a Project; and a declined organisation, which says to contact the community, has a Contact Us link.
Volunteering → Wellbeing (website): when a member's wellbeing score is low, the section suggests reaching out to their community for support but gave no way to do it. It now has a Contact Us button beside the self-care tips, opening the community's own contact form.
Volunteering → For You (website): with no recommendations, the section told members to "add skills to your profile" and offered no way to do it. It now has an Add your own skills button that opens Settings on the Skills section (shown when the community has Settings switched on, as on the Skills page).
Volunteering (accessible site): someone who runs an organisation and logs hours for it was offered Approve and Decline on their own entry in the organisation's review list, and pressing either failed, because nobody may approve their own hours. Their own entry now shows a note that someone else who runs the organisation must approve it, as the expense review already did for a reviewer's own claim.
Volunteering (accessible site): an organisation's review page showed only the first 20 applications and hours waiting for a decision, with no way to see more or to look back. Applications can now be filtered by status (waiting, approved, declined or all), each list has Load more, and only an application still waiting shows the approve and decline form; the others show what was decided. (Seeing one opportunity's applications on its own needs an API change and is not done.)
Volunteering (accessible site): an opportunity's page listed shifts that had already happened as "available", and the application form let a volunteer choose a past or full shift. Volunteers now see upcoming shifts only, and the form offers only upcoming shifts with a place, as the website does. The people who run the opportunity still see past shifts, with the roster of who came.
Volunteering (accessible site): a volunteer who already holds a shift on an opportunity was offered "Sign up for this shift" on every other shift, and pressing it silently moved them off the shift they held (a volunteer holds one shift per opportunity, and the freed place goes to that shift's waitlist). The button now reads Switch to this shift, with a line naming the shift they would give up, and the switch is sent with the shift the page showed them holding: if that has changed since the page loaded, nothing is moved and they are told why. Website: the "Switch shift" label was a wrong machine translation in four languages (French read "change gear"; Arabic, Japanese and Dutch did not mean a work shift) and is corrected.
Volunteering (accessible site): a notification about hours, a shift swap, an expense claim, a waitlist place, a certificate or safeguarding training now opens that page. The links carry the website's section names (
/volunteering?tab=hoursand so on), and the accessible volunteering page understood only its own three tabs, so every other link landed on the list of opportunities. A section the community has switched off still shows opportunities, and the website'scommunity-projectsspelling now opens the community projects tab. A qualification reminder (?tab=credentials) still shows opportunities on the accessible site, because it has no qualifications page for volunteers yet.Volunteering (accessible site): three volunteer pages that worked had no way in. Urgent shift requests, My wellbeing and Safeguarding (training records and incident reports) are now linked from the volunteering page with its other tools. The list now also follows each community's own volunteering section switches, the same rule the website uses: a section the community has turned off (for example wellbeing, or expenses) is no longer offered, and a switched-off tab named in the address shows opportunities instead. Every section is on unless a community turns it off, so nothing changes for a community that has not. The older document-upload page
/volunteering/credentialsis deliberately not linked: since 5 Oct 2026 the website records qualifications without uploads, and the accessible site has no qualifications page for volunteers yet. Rule inweb-uk/src/lib/volunteering-sections.js.Accessible site tooling: the API consumer ledger (
web-uk/scripts/generate-api-consumer-ledger.js) read onlyroutes/api.php, so a call to any API route declared in another file Laravel serves under/api(todayroutes/api-volunteering-qualifications.php) was reported as having no Laravel route. It now reads every fileRouteServiceProviderregisters under the/apiprefix, and lists them in the ledger aslaravelApiRouteFiles.Admin → Volunteering → Expenses: an administrator who tries to approve or reject their own expense claim now sees "You cannot approve or reject your own expense claim. Ask another administrator to review it." instead of the generic "Failed to update expense". The refusal itself is unchanged; it now answers with its own code,
SELF_REVIEW_FORBIDDEN(still HTTP 403). New wording is in all eleven languages.Super-admin tenant form: place search imported directly. The 6 Oct place-search change imported it through the location component barrel, which the bundle-budget check forbids because it can pull Google Maps into unrelated screens; CI failed on that check. Same behaviour, direct import. The super-modules test now also mocks the tenant context at its direct path, which the dead-mock check required once the import was direct.
Checking a certificate by its code works on every community address (accessible site). The main app reserved the new
verify-certificatepage address on 6 Oct 2026 but the accessible site's copy of that list was not updated, so its routing test failed in CI. The two lists match again.The phone app opens the right screen from a cancelled-shift alert. The alert points volunteers at their Applications tab; the phone app's route contract did not list that destination yet and its test failed in CI. Added.
Urgent shift request: the error message shown when the request cannot be sent now goes through the same server-message helper the shift manager uses, so the admin text check passes and the server's own validation message is still shown when there is one.
Three React test files corrected after the 6 Oct volunteering commits: the Help Centre FAQ text test now expects a link's address to be part of the visible words (F-562 made it so), the module-configuration test's sample module no longer carries fields its type does not have, and the volunteering page test casts its mocked tenant through
unknown. No app behaviour changed.Volunteering → an opportunity's page: an approved volunteer's Shift Check-in QR code was shown below the whole shift list, so with many shifts it was easy to miss, and under it the page said "Check-in QR codes are not yet available for some shifts", which was untrue: the page had asked for a code for every shift and been refused for each one the volunteer was not on (16 refusals on a 17-shift opportunity). It now asks only for the volunteer's own shift and shows the code just above the shift list.
Volunteering → My Hours: a volunteer with no organisation to log hours for saw a greyed-out Log Hours button and the sentence "You can log hours after an organisation approves your application or adds you as a member", with nothing to press. The note now has Find opportunities and Check my applications, which open those tabs.
Volunteering → Shift swaps (accessible site): the form for asking to swap a shift asked for "the shift you want instead (shift number)" and "the volunteer to swap with (member number)". Neither number is shown anywhere on the accessible site, so no volunteer could ask for a swap there. The page now lists the volunteer's own upcoming shifts, each with Ask to swap, which opens a page listing the same opportunity's other upcoming shifts that have someone on them. The volunteer picks a shift, never a person, as on the website: whoever is on it is asked, and their name appears on the volunteer's request only once they agree (until then it reads "To: the volunteer on that shift (named once they agree)"). On a request card the volunteer's own shift is now labelled Your shift whichever way the request went; a request they had sent used to call their own shift "Their shift". The page also showed only the newest 20 of a volunteer's shifts, because it asked the API for
limit=50where the API readsper_page.Volunteering → an opportunity's page (accessible site): a full shift showed only a Full tag, while the Shift waitlist page told volunteers "When a shift is full you can join its waitlist". A volunteer whose application is approved can now press Join the waitlist on a full, upcoming shift; a shift they already wait for says On the waitlist: place N with a link to their waitlists instead. Being already on the list, a place having come free, the shift having started, and a safeguarding refusal each get their own message. Each entry on the Shift waitlist page now links to its opportunity, where a freed place is claimed; the page used to say "Sign up from the opportunity page" without a link.
Volunteering → certificates: the certificate is ready email's button led to
/volunteering/certificates/{id}, a page that does not exist; it now opens the Certificates tab. A certificate'sverification_urlwas the API address, which needs a sign-in, so nobody the certificate was shown to could use it; it is now the public check page.Volunteering: the owner of an organisation still awaiting approval could not reach its dashboard. The "awaiting approval" card on the volunteering page and the pending row on My organisations were plain text, although the dashboard already works for a pending organisation. The card now has Set it up while you wait (or See your organisations when several are pending), and each pending row has Open dashboard.
Volunteering: a community administrator whose admin rights were given by the admin switches on their account (rather than by the old role text, which the platform no longer writes) was refused when editing, closing or cancelling an opportunity, when adding an opportunity to an organisation they did not own, when opening an organisation's dashboard (only
is_adminandis_godwere missed there), and when listing or updating volunteer wellbeing alerts. All four checks now use the platform's single admin rule (AdminTier), so every community administrator is recognised. Brokers and coordinators are still refused, including ones left with an old admin switch on. Covered bytests/Laravel/Feature/Volunteering/VolunteerCommunityAdminFlagsTest.php.Volunteering → urgent shift requests: only the organisation's owner or a community admin recognised by role text could send one; its admins and the opportunity's creator were refused. All of them can now, and so can any community admin. A request can now be withdrawn by any of those people, not only by whoever sent it. A request for a shift that has already started is refused.
Volunteering → an opportunity's page: an organisation admin who had not created the opportunity could not see its Applications panel, because the website showed it only to the creator. The server had the opposite gap: it refused the creator if they were not an organisation admin. Both now allow the same people: the creator, the organisation's owner and admins, and community admins.
Volunteering → repeating shifts: a weekly or fortnightly pattern saved without any weekdays was accepted and created a shift every day (15 in the two weeks ahead), and weekdays sent as text ("2") were stored but never matched, so they created no shifts at all. The server now requires at least one weekday (1 = Monday to 7 = Sunday) for weekly and fortnightly patterns and answers 400 otherwise, including when an edit would remove them. Weekdays sent as text are understood. A pattern stored without weekdays before this fix repeats on its start date's weekday instead of every day. The website's form also says "Choose at least one day" before it sends anything.
Volunteering: an opportunity's creator who is not an admin of its organisation was refused when editing or cancelling it, although they could already change its shifts, and was not shown its shift panel (the page tested
can_manage ?? is_owner, so a falsecan_managehid the creator's controls).can_managenow includes the creator, and update and cancel use the same rule. Volunteers with a pending or approved application could no longer open an opportunity once it was closed; they now still can.Volunteering → Shift swaps: a volunteer who asked to swap was shown the other person's name straight away, on their own "Sent" card, on the website, the phone app and the accessible site alike. That contradicted the promise made when asking ("you will not see their name unless they agree"), and the whole point of asking for a shift rather than a person. The swap list now withholds the recipient's name, picture and member number from the requester until the recipient has accepted (including when an admin still has to approve, or later refuses). The recipient always sees who is asking. The website's Sent card now reads "To: the volunteer on that shift (named once they agree)" until then; the phone app shows the line without a name until its next release.
Footer "Powered by" badge: returning visitors kept seeing the old Project NEXUS artwork after the new one was deployed, because it was published under the old file name and browsers keep that image for a year without checking again. The new artwork now has its own file name (
powered-by-project-nexus-light.png/-dark.png) on the web app and the accessible site, so every browser fetches it. The old file names still answer, with the new artwork, for any page that still points at them.Signed-out visitors are no longer offered Explore in the header or the mobile drawer. The Explore page requires sign-in, so the link only sent visitors to the login page.
Signed-out visitors are no longer offered Skills, Matches or AI Chat in the header "More" menu or the mobile drawer. Those pages require sign-in, so the links only bounced visitors to the login page; on a community with the Engage and Progress modules off they also left the More menu showing an apparently empty "Tools" column. The menu now collapses to one column for visitors there.
Volunteer expenses — a new claim no longer sits unseen. When nobody at the organisation can be told about a claim (the person claiming is its only admin, or every other admin's account is closed), the community's admins are now told instead, by email and in the app, with a note saying why and a link to Admin → Volunteering → Expenses. Community admins are not told when an organisation admin is (owner decision: fallback only).
Volunteer expenses — suspended or closed accounts are no longer emailed about new claims as organisation admins.
Volunteer expenses — the approval email now includes the reviewer's note. Only the rejection email carried it before.
Volunteer expenses — the website's claim form now lists every organisation a volunteer can claim from. It listed only organisations the volunteer belongs to as a member, but being accepted onto an opportunity does not make someone a member, so most volunteers saw "no organisation" and could not claim at all. It now matches the server's rule and the Hours tab: organisations that accepted the volunteer, plus ones they belong to. The accessible site had the same gap and is fixed too; the phone app already did this.
Volunteering → Qualifications wording: counts now read correctly for one record ("1 needs attention", "1 qualification" — they said "1 need attention" and "1 qualifications"), in every language with its own plural forms. The organisation's bell reads "Volunteer qualifications needing attention at : " instead of "1 volunteer qualifications … need attention". On the admin Qualifications page a screen reader no longer hears "Expires: Expires …", and a record confirmed by an organisation names that organisation once instead of twice.
Footer "Powered by" badge in dark mode: removing the dark-mode image in Admin → Settings → Powered-by branding now takes it out of the dark footer again. For part of 6 October the dark slot fell back to the light-mode upload, so a removed image kept showing. Each mode has its own slot and its own built-in Project NEXUS badge, which is what the admin label says; a badge that should look the same in both modes is uploaded to both slots (the Timebanking UK network badge is).
Volunteering safeguarding: an administrator, broker or coordinator who reports an incident themselves now gets the staff alert ("a safeguarding incident has been reported", with the link to the safeguarding page) that every other staff member gets, as well as the "we have received your report" confirmation. They used to get only the confirmation, so a staff member reporting on behalf of someone else had no staff alert in their inbox. Someone the incident is about is still never told.
Volunteering → Opportunities (desktop and tablet): the search box and "Near me" button sat flush against the first opportunity card with no gap. They now have the same spacing below them as the rest of the page.
Mobile app: when a member's login had already expired, setting up push notifications reported the expected "session expired" reply to the error tracker as a fault. It no longer does; other push-registration failures are still reported. Ships with the next app build or over-the-air update. (Sentry NEXUS-MOBILE-5, NEXUS-PHP-6F)
Volunteering → Qualifications: the date in qualification expiry messages is now formatted with the platform's regional formatting setting rather than the bare language code, as every other date is; the dates members see are unchanged (day first, month spelled out). The phone app's list of web pages now records the three new safeguarding report pages (a reporter's own report, and an organisation's reports list and case page) as not yet built in the app.
Volunteering safeguarding: when a high or critical incident changed status, the person who reported it was sent a second email meant for staff, linking into the broker panel they cannot open. They now get only their own update, with a button to their report's page, and their bell links there too. A staff member who reports an incident gets the reporter's confirmation, not the staff alert about their own report.
Volunteering incidents (the staff timeline new in this release): the database snapshot that fresh installations and the test suite are built from had the timeline table but not the two rules that stop anyone editing or deleting a timeline entry, so on a freshly built database the timeline was not actually permanent. The snapshot now carries both rules. The mobile app's list of server addresses is refreshed for the new staff incident pages.
Group exchanges (the new kinds in this release): completing an exchange whose community time fund record had gone missing stopped with an internal "class not found" error instead of the intended refusal. On the accessible site, the five kinds no longer hide each kind's number box until it is chosen: GOV.UK's reveal script marked the radio buttons in a way screen-reader rules do not allow, so every box is now shown under its kind and each is labelled with its kind for screen readers. The "earns / pays / goes to the community time fund" lines read correctly for one hour and for any other number of hours in all eleven languages (several had dropped the number). The organisation Fundraising tab loads its interface pieces the way other fast-loading pages do; nothing visible changes.
Phone app — group exchange links in a community with Groups switched off: a link to a group exchange (from a notification or a shared web address) was refused when the community had Groups off, even though group exchanges have their own switch and do not depend on Groups. The app now checks only the group exchanges switch, matching the server and the website (HELP-12 follow-up).
Group exchanges: the create page said the people giving time "will transfer hours to" the people receiving it — the opposite of what happens (the people receiving time pay). The web app's "Custom hours" preview also worked the split out differently from what the server actually charged; every client now shows the server's own figures. An unrecognised kind sent to the API used to be saved as a blank kind that no settlement could handle; it is now refused with "Choose one of the five kinds of group exchange." (422
SPLIT_TYPE_INVALID). A participant with no hours in a workshop, team or custom exchange is now refused with a message instead of being silently left out at settlement.Accessible site → Help & support: when a member attached two or three screenshots to a request, they could reach the support team in a different order from the one they were attached in, because the upload parser records each file when it finishes saving to disk rather than when it arrives. Files sent in one field now keep the order they were sent in, on every accessible-site upload form.
Help Centre → Setting up safeguarding for your community (admin guide): the steps for choosing the safeguarding jurisdiction still sent admins to Vetting confirmations and a Save jurisdiction button, neither of which has held the setting since it moved to the broker Configuration page. The steps now read Configuration → Compliance & Safeguarding → Safeguarding jurisdiction → Save Changes, in all eleven languages. Keeping notes, tasks and tags about members names the Add note and Add tag buttons as the screen shows them.
Help Centre → Donations and Donating to your community articles: in every language except English, several bold button names did not match what the screen shows (for example "Donner par carte" for a button that reads "Faire un don par carte"), which turned the CI help-label check red. Where the article was right and the button wording was stale or mistranslated, the button was corrected instead: the pledge button still said "Confirm donation" in seven languages (in Arabic, in English), though in English it has said "Record pledge" since 5 October; "My donations" was in Title Case in Spanish, Italian and Portuguese; the Dutch receipt button read "view reception" and the Dutch continue button had a stray capital; the Polish receipt button said "till receipt".
Volunteering safeguarding incidents: every save on the staff screen told the reporter their incident had been "marked as opened" even when its status had not changed — the reporter is now told only when the status really changes, and that notification links to the Safeguarding tab of their Volunteering page (it linked to an address that did not exist). The "Action taken" box on the staff screen was labelled "Taken" in English and mistranslated in every other language (in Japanese it read "photographed").
Group exchanges in a community with Groups switched off: members could open the group exchange form, but every save failed with "Failed to create exchange — Service unavailable", and their list of group exchanges never loaded. The server checked the community's Groups switch, while the web app, the accessible site and the mobile app all show these screens on the separate Group exchanges switch. Every
/v2/group-exchangesroute now followsgroup_exchangesonly (HELP-12, Feminist Marketplace).Admin → Volunteering → Safeguarding & incidents, and the broker panel's copy of it, showed only the 20 newest incidents, with no way to see older ones — including ones still open — while the figures above counted every incident. The list now pages through every incident, its search box (which did nothing) searches titles, descriptions, organisations and the names of the reporter and the person the incident is about, and a new status filter shows open, under investigation, resolved, escalated or closed incidents. A search or filter that finds nothing says so and offers to clear it. API:
GET /v2/admin/volunteering/incidentsacceptssearch; an unknownstatusis ignored rather than returning an empty list.Volunteering → Donation receipt (
/donations/{id}/receipt): the Community line was always blank and the payment reference never appeared, because the page read field names the server does not send (community_name,reference); it now readstenant_nameandpayment_reference, shows the payment method as "Card" rather than the stored keystripe, and names the organisation and campaign the gift went to. A card payment for a fundraising campaign from another community, or one that does not exist, is now refused before Stripe is contacted; the campaign id used to be stored unchecked. Admin → Fundraising campaigns: the target amount box showed a "$" sign whatever the community's currency; it now shows the community's own currency code.A log file the web server cannot write to no longer breaks the platform. If the day's
laravel-YYYY-MM-DD.loghad been created by a script run as root (adocker exec … php artisanon the first run of the day, which is exactly how production runs migrations), the web server user could not append to it, and every request that merely tried to log something — including the error-handling blocks meant to swallow failures — answered 500. Seen on 5 October when reporting a safeguarding incident: the incident was saved, the staff emails could not be sent on the local stack, logging that fact threw, and the member saw "An unexpected server error occurred". Thedailyandsinglechannels now ignore a failing log sink (the failure is written to PHP's own error log once per process instead), and thestackchannel setsignore_exceptions. Regression test:tests/Laravel/Feature/Logging/LoggingFailureDoesNotBreakRequestsTest.php.Volunteering → Donations: every card gift in My donations showed the raw text
[missing] volunteering:donations.payment_methods.stripe— the server records card payments asstripeand the screen only knewcard. Amounts had no currency at all ("340.00", "340 / 1200.00"); they are now formatted in the community's currency. An ended campaign was listed under "Active" with working donate buttons and counted in "Active campaigns". Donate by card offered a choice of EUR, GBP or USD, but the server accepts only the community's own currency, so choosing another ended in a generic "Failed to initialize payment"; the checkout now shows the community currency and offers Gift Aid only when that currency is GBP. On a phone the header's three buttons stacked as a ragged left-aligned column; they are now equal-width rows. "1 donors" is now "1 donor" (and the singular form is corrected in French, Italian, Portuguese, Spanish and Dutch).Admin → CRM → CRM overview: when the figures could not be loaded, the page showed every figure as 0 (an empty-looking community rather than a broken page) and only a brief toast said anything was wrong; it now shows an error with Retry. On the server, a failed task or note count was being turned into a zero as well; those queries now fail visibly instead of reporting "nothing to do".
Admin → CRM → Onboarding funnel: the final step ("Regular member") looked up a "waiting" sentence that does not exist, which logged a missing-translation error on every load; and the CSV "Days since joined" figure is now whole calendar days (someone who joined 40 days ago at 9am read 39).
Admin → CRM → Activity timeline: choosing All time never worked — the page sent no day limit and the server then applied its 30-day default, so the view silently stayed at 30 days; it now asks for all time explicitly. Relative times read "1 hours ago"; rows now show the clock time under a day heading instead. The "Login" rows are one row per member at their most recent visit, not one per sign-in, so they are now labelled Last active / "Last seen on the platform".
Admin member search box (CRM notes, Coordinator tasks and every other admin form that searches for a member): the magnifying-glass icon sat outside the field's border, most visibly on a phone. The shared combo box now floats a start icon inside the field, the same way the dropdown arrow floats at its end, and makes room for it.
CI: two translation gates that were red on
main. The Irish-locale audit now has the six values that are deliberately identical to English recorded as reviewed inscripts/irish-react-reviewed-invariants.json(the{{queue}}: {{count}}attention-strip label, the Ctrl and K shortcut keys, the example phone number, and the two placeholder-only{{sender}} → {{receiver}}/{{a}} ↔ {{b}}labels); and the admin-UI literal check accepts the Assign DLP dialog showing the server's own refusal reason, which the server already translates, with the same documented marker the other admin pages use.Volunteering → Safeguarding → Assign DLP (the Designated Liaison Person for an organisation) asked an admin to type a numeric user ID, and answered every refusal with the same "Failed to assign DLP". The dialog now has the shared member search (find the person by name or email), and
PUT /v2/admin/volunteering/organizations/{id}/dlpsays why it refused, in the community's language: the organisation does not exist (404), the person is not in this community, or their account is not active (422, fielddlp_user_id).SafeguardingService::tryAssignOrganizationDlp()returns the reason;assignOrganizationDlp()still returns a boolean. Three newapi.vol_dlp_*messages and sixadmin_volunteeringstrings, in all eleven languages.Admin → Settings → Date and number format never saved. The picker sent
regiontoPUT /v2/admin/settings, but the controller that serves that route (AdminConfigController) did not list the key: a save with only the region changed was refused as "no recognised settings", a save with other changes dropped it silently, and the page never read it back, so it always showed Ireland. The key is now accepted (two letters, stored uppercase, anything else is a 422), returned by GET, and the formatting cache is cleared on save. The unroutedAdminSettingsController, which did accept it, was never what the page called.Admin → Settings: a community set to invite only showed the registration switch as OFF, and flipping it back wrote
closed; the mode is now shown and saved as it is (the legacy valueinviteloads as Invite only).Admin → Settings → Reset to defaults left the sign-up gates on their old values: it deleted
general.email_verification/general.admin_approvalbut not the bare keys the registration and login gates read, and did not clear the settings cache, so the page showed the defaults while sign-up kept the old rule for up to five minutes. Both rows are deleted and the cache is cleared.More downloads that failed for signed-in users. Jobs → Analytics → Export CSV, Volunteering → Certificates → Download and the interview card's Add to calendar (.ics) opened API addresses in a new tab or as a plain link, which send no sign-in token, so they answered
auth_required; they now download through the signed-in API client (the certificate is saved as an.htmlfile to open and print, rather than rendered inside the app). Legal documents → Compliance → Export requested the server's CSV as JSON, found nothing to parse and saved no file; it now downloads the CSV. The Member, Hours, Inactive members, Impact, Municipal impact and Municipal ROI report exports, the poll results export and resource downloads fetched files by hand with the access token current at that moment, so they failed once that short-lived token expired; they now useapi.download, which renews the sign-in and retries.Admin downloads that never downloaded. GDPR audit log → Export CSV, Consent types → Export CSV, Pilot enquiries → Export CSV and the Log files download button opened an address in a new browser tab, which sends no sign-in token (or, for two of them, pointed at a page that does not exist), so they showed
auth_requiredor nothing. They now download through the signed-in API client and show a translated message if the download fails. GDPR request → Generate export built the member's ZIP and said "Export available", but nothing could fetch it: a new route (GET /v2/admin/enterprise/gdpr/requests/{id}/export/download) serves it only to an admin of that community, only while the export is unexpired, only from the export directory, and records every download in the GDPR audit log; the page now shows a Download export button. Log files gained a matching download route (…/log-files/{filename}/download), limited to platform super admins like the viewer. The mobile API route inventory is refreshed for the two new routes.Admin → Users → Import: a filled-in copy of the downloaded template could not be imported. The template starts with a UTF-8 byte-order mark (so Excel shows accented names correctly), and so does any file Excel saves as "CSV UTF-8"; the importer read the mark as part of the first column name and answered "Missing required columns: first_name". It now strips the mark before reading the column names.
Admin dashboard: the "Review" link for listings awaiting moderation opened the Listings page on All because that page ignored the status in the address; it now reads and writes
?status=(all|pending|active|inactive). Structured activity-log rows (for example blog post events) showed a name followed by nothing on the dashboard; they are now put into words with the same helper the Activity log page uses. The trends window started mid-month and under-counted the oldest month; it is now bound from the first of the month.Admin → Users → Import: Download Template opened the template address in a new browser tab, which sends no sign-in token, so it showed
{"errors":[{"code":"auth_required"…}]}instead of downloading. It now downloads through the signed-in API client (as the other admin exports do) and savesuser_import_template.csv; if the download fails, the admin sees a translated error message.Startup download: the staff step-up prompt (E-085) was mounted eagerly at the app root, which pulled the passkey client and the security-confirmation modal into the entry bundle every visitor downloads and pushed it to 213 KiB gzip, over the 200 KiB budget. It is now lazy-loaded after startup and registers its handler when its chunk arrives; the entry bundle is back to about 101 KiB gzip.
Warning, error and success colours were missing from 88 screens: 235 class names such as
bg-warning-50,text-danger-700andborder-success-200refer to numbered colour shades the HeroUI v3 theme does not define, so they compiled to nothing and the boxes, badges and messages rendered with no colour at all (every one was confirmed colourless in the running app). They now use the theme's real tokens —bg-*/10tints,border-*/30borders andtext-*-soft-foregroundtext, which measures at least 4.5:1 contrast in light and dark mode — and the separate dark-mode shade overrides are gone because the new tokens adapt on their own. The newsletter send-time heat map uses gradedbg-successtints. A new ESLint rule (no-restricted-syntaxinreact-frontend/eslint.config.js) fails the lint step on any numbered status shade, so they cannot come back.Broker panel: the command palette's member results and the inbox's vetting re-check rows now name a member with the shared display-name helper, so an organisation account shows its organisation name rather than its contact person. The activity timeline's exact-time tooltip test now compares against the same locale-aware formatter the component uses, so it no longer fails on a CI runner whose default locale differs. Also fixes the CI gates the 3–4 Oct commits tripped:
listings/mineis recorded as a mobile parity gap, the mobile API route inventory is refreshed, three test-file type errors are fixed and two fixed baseline entries locked in, a now-fixed Spanish Help Centre label is removed from its baseline, andListingServicedeclares the item type of its own-listings collection.Dashboard: the Active Listings card and the View all link under the member's listings opened the community-wide Listings page, showing everyone's listings. Both now open My listings. The card's number and the list now come from the same source as that page's Live tab, so the two always agree; the section is retitled Your listings.
Admin and broker tables: clicking a sortable column header marked it sorted but never reordered the rows (the column carried no id, so the sort reported a generated key).
Broker Insurance: the list asked the server for 25 rows per page but paged by 20, silently skipping five certificates on every page. Broker Configuration: a number field snapped back to its default while being cleared.
Broker panel: Bulk Approve, Bulk Suspend, Reactivate and deleting a note (Members page and member window) now ask for confirmation before acting — reactivation emails the member and the bulk actions change access for several people at once. A member window that fails to load shows an error with Retry instead of a skeleton that never ends.
Broker panel: a failed approve or reject on Exchanges, Match approvals or Insurance keeps the dialog and the typed reason open instead of discarding them. Match approvals no longer spin for ever or show "0" after a failed request: first load shows a placeholder, failures show an error with Retry, failed stats show "—". Messages and Review Archive treat a failed reply as an error with Retry, not an empty list, and the Archive search waits 300 ms between keystrokes.
Broker panel: Check Vetting on a member now opens the Vetting page filtered to that member (the link was previously ignored), with a banner and a Clear filter button; the Insurance page shows the same banner for its member links. Insurance coverage amounts use the community's own currency instead of a hard-coded €.
Broker panel: the exchange lifecycle strip now includes Pending provider, In progress and Pending confirmation, so every status highlights a stage, and an expired exchange shows as a closed outcome. Message-flag severities (info, warning, concern, urgent) and the archive "flagged" decision have proper labels and one shared colour, so the Archive record no longer reads "Unknown" and an archived message shows the decision actually recorded. Back on an exchange or message detail returns to the list tab it was opened from.
Broker panel: column headers that only sorted the visible page (Members "Joined", Insurance "Member", Onboarding queue) are no longer offered as sortable; the four moderation pages and Safeguarding options set their own browser-tab title instead of sharing one; vetting detail dates, insurance expiry dates and onboarding avatars go through the shared formatters.
The local pre-commit check for the phone app's notification inventory no longer blocks a commit because of somebody else's uncommitted edit to an unrelated file. The inventory scan reads the whole working folder, so with two sessions working side by side, one session's unsaved change blocked the other's commit. The check now names the files that are out of date and blocks only when one of them is part of the commit (or when it cannot tell). CI is unchanged and still fails on any out-of-date entry.
On the Polls page, a member who had already voted in an open poll was shown the vote buttons again, because the results stay hidden until the poll closes. Since 12 September, clicking the option they had already chosen said "Vote recorded!" instead of telling them they had voted. They now see the option they chose, ticked, with "Results revealed when poll closes", and a repeat vote says "You've already voted on this poll." in all eleven languages. Voting for a different option from the feed now gives the same message instead of an empty error.
A poll's description lost its paragraphs and numbered lists once the poll was published: the Polls page and the accessible site ran the text together into one paragraph, and the feed did not show the description at all. The description now appears on the feed's poll card, and every place that shows it keeps the line breaks the author typed (support ticket HELP-10).
On the Reports page (admin and broker), reports made from the listing report form or the phone app showed the raw code as their reason, for example
safety_concern. They now show words such as "Safety concern" or "Not a timebank service", in all eleven languages, in the table, the phone cards and the details panel. Reasons a member typed are still shown exactly as written.In a member's Linked accounts settings, the account-support choice "Do it on their own" was badly machine-translated in all ten other languages (for example German "Mach es alleine", Japanese 「自分たちでやれよ」). It is now "They can act on their own" and is translated by hand, along with "Prepare only — you approve each one" and the sentence explaining both.
On a phone, the four count boxes at the top of the Reports page broke words in the middle ("Pendin g", "Resolve d") since they were put two to a row. The icon now sits above the number on phones.
On the accessible site's Report a problem form, the follow-up question "How much did this affect you?" was revealed by choosing "Something isn't working". The GOV.UK reveal script marks that radio button as expanded or collapsed, which radio buttons are not allowed to carry, and the accessibility check failed. The question is now always shown, with the hint "Only answer this if something isn't working", in all eleven languages; it is still only required for that choice.
The accessible site's Create event form had the same accessibility fault as the report form: choosing how a repeating event ends ("After a number of occurrences" or "On a specific date") revealed a follow-up field using the GOV.UK reveal script, which marks the radio button in a way radio buttons are not allowed to carry. Both follow-up fields are now always shown, each with a note saying which choice it belongs to, in all eleven languages. The tick boxes on the event forms use the same script but are allowed that marking, so they are unchanged.
The phone app's copy of the server's route list is refreshed for the two new broker routes (settle a dispute, review several messages at once), so its release check can verify the app's calls again.
On a community reached through a shared address (
app.project-nexus.ie/hour-timebank/..., andlocalhostin development), clicking a tab or filter briefly dropped the community name from the address, and a safety guard put it back about 130 ms later behind the router's back, so the address jumped and the page could redraw. Address changes made by a page now keep the community name from the start. The same fix covers sub-communities reached through their parent's own domain (for exampleuk.timebank.global/stratford), where the safety guard did not run at all and the name could be lost from the address for good. Community domains such as hour-timebank.ie were never affected.A coordinator's broker panel could not open Vetting: the page, its counts and the vetting part of a member's details failed to load, because the server refused coordinators even for viewing. Coordinators can now see vetting records, as documented; only a broker or an administrator can confirm, renew, revoke or resolve them, the page hides those buttons from a coordinator and says why (in all eleven languages). (F-550)
The Help Centre label check listed three already-fixed entries (one English, two Dutch), which failed its "nothing fixed is still listed" test; the list is regenerated.
Broker dashboard: "What needs you now" counted a flagged unreviewed message twice — once under Safeguarding Alerts and once under Unreviewed Messages. The total now counts it once; both chips still show.
Broker message review: a stray "0" no longer appears beside every message in a conversation (the server sends "edited" as 0/1 and the page printed the 0). Voice messages now say "Voice message (12 s)" with their transcript when one exists, instead of an empty bubble; the recording itself stays private to the two members.
Broker member window: the reason box for resetting two-factor sign-in showed a raw "[missing] admin_users:…" label. It now shows its proper wording in every language.
Moderation pages (Feed Posts, Comments, Reviews, Reports): the count line said only "Showing"; it again says "Showing 20 of 78". Feed Posts shows posts written in the web composer as text instead of their web code, and shows the title of polls and goals, which have no body. Reviews shows who was reviewed instead of "?" (the page read a field name the server never sends). The Reports page opens pre-filtered when a link asks for it (
?status=pending).On screens from 768px to 1023px wide (tablets, and phones turned sideways) the floating Help & support button sat behind the bottom navigation bar, so members could not see or press it. It now sits above the bar.
The count boxes at the top of the broker Risk Tags page (Critical, High, Medium, Low) now always count every risk tag. They were counted from whichever level was open, so after following a link to one level, every other box showed 0.
Brokers and coordinators can save the broker panel's Configuration page (F-547). Every save they made was refused, even with nothing changed. The page sent back every setting it had loaded, including two kinds that only an admin may change: a switch the page never shows (
exchange_workflow_enabled), and a random-sample rate an admin had set to 100%. The server refuses the whole save when it sees either. A broker's save now sends only the settings the broker changed. Admins still save the whole page as before.The broker dashboard's safeguarding card could only ever go up. It left out members a broker had "reviewed", but nothing on the platform recorded that a broker had reviewed anyone. It also counted members whose answers switched nothing on. "Mark as seen" now records it, and the card counts with the same code as the list it opens.
The mobile course-builder test "does not consider an unsaved lesson title saved when a quiz question is added" now waits for the builder to pass the lesson's final state on to the route guard before checking it. The lesson reports its state from one effect and the builder relays it from another; on the slower CI runner the check sometimes landed between the two and saw the earlier "question being added" report. The CI log showed only that report, with no lesson save started, so the component itself was correct.
The broker dashboard's "Pending Exchanges" card counts exchanges waiting for a broker's approval and exchanges in dispute, but clicking it opened a list of the first kind only. A community whose queue held only disputes saw a number on the card and an empty page behind it. The card now opens a new "Needs action" tab on the Exchanges page that shows both, so the number and the list always match. The tab shows its own count and is translated into all eleven languages.
More broker dashboard cards now open a list that matches their number. "High Risk Listings" counts high and critical listings but opened the high ones only, so critical listings were missing from the list; it now opens a new "High and critical" tab on the Risk Tags page. "Safeguarding Alerts" counted the transfer-fraud detector's alerts, which brokers cannot see anywhere (they are in the admin panel's Fraud alerts), while the card opened the flagged-messages list; it now counts flagged messages nobody has reviewed yet, which is what it opens. "Unreviewed Messages", and the count on the Messages page itself, included conversations the broker is part of, which the review list deliberately hides from them; they no longer do. The vetting re-check card counted requests made under an earlier vetting policy and requests from deleted members, which the Vetting page does not list; it now counts what the page lists.
The broker Help Centre guide on revoking a vetting decision now names the counter by its current label ("Re-checks requested"), in all eleven languages. The broker panel renamed it but the guide still said "Review requests".
The safeguarding dashboard showed an untranslated, machine-made label for a flag reason it had no wording for; it now shows the translated "not recorded" reason.
The mobile app's record of the platform's API routes is refreshed to include the routes the broker-panel work added, so the mobile checks can verify every endpoint the app calls again.
Squashed and broken text across the broker panel. The number tiles on every broker page, and on the admin pages the broker panel reuses (Safeguarding, Content Queue), laid the icon, label, figure and arrow on one row, so in a four- or five-across grid a label like "Unreviewed Flags" broke in the middle of a word and the big numbers sat at different heights. The tiles now stack the label above the figure, the label never breaks inside a word, and grids go to four or five across only on wide screens. Status chips ("Visible", "Pending"), table headings ("Severity") and dates ("5/9/2026") no longer wrap mid-word in any table on the platform. The blank card that appeared above the figures on the Content Queue, Feed Posts, Comments, Reviews, Reports and Safeguarding pages is gone: it was the admin module's own header with its title hidden by a styling trick, and that trick also hid the module's Refresh and "New assignment" buttons from brokers, who now get them back. The Content Queue's two filter menus showed their internal names (
moderation.pending,moderation.all_types) instead of "Pending" and "All Types".The broker panel's Safeguarding page works for brokers and coordinators again (F-542). Since 8 August every one of its views — member safeguarding preferences from the welcome wizard, flagged messages, guardian assignments, support actions and authority records — refused them, because the page required an individual
safeguarding.viewpermission that no screen can grant. The page then showed the refusal as "No member preferences". Brokers and coordinators now have the page in full (owner decision); they still cannot review a monitored copy of their own messages or read the safeguarding trail kept about themselves. If the member list cannot be loaded, the page now says so instead of reporting no members.docs/ROLES-AND-PERMISSIONS.mdnow states the rule: every broker-panel function works for brokers.The broker panel's Admin only label and the Configuration page's admin-only notice were hard to read. The label was small grey text with no visible background, and the notice was amber text on pale amber, below the minimum contrast for readable text (WCAG AA, 4.5:1). The label is now a solid amber badge with dark text (8.7:1 in light mode, 10:1 in dark), and the notice is dark text on the card with an amber edge (14.6:1). Its wording now says plainly to ask an admin in your community.
Every page of the broker panel now meets the WCAG AA contrast minimum for text, in light and dark mode. Measured page by page, text in the shared status colours sat just under the line: purple links 4.47:1 (4.18 in dark), grey table headings and the search box 4.36, amber status chips and buttons 4.49, white on red Delete buttons 3.48 (4.35 in dark), small red figures 3.57 (4.19 in dark), the volunteering "Not assigned" label 1.95 and faded labels there 3.15. While the broker panel is open it now uses slightly stronger text colours (darker in light mode, lighter in dark) and a deeper red behind white button text; button fills, a member's chosen accent colour and the rest of the site are unchanged, and the high-contrast accessibility setting still takes precedence. All 25 broker pages, the sidebar, the header and the pop-up windows were re-measured afterwards: no text below the minimum in either mode.
The broker dashboard's "some figures couldn't be loaded" notice is readable and says what to do in plain words ("press Refresh… if a dash is still there, tell an admin"); it used to tell brokers to check the server logs, which they cannot see. Same design as the panel's other notices, in all eleven languages.
German broker-panel wording added on 3 October addressed people as "du"; the rest of the German broker panel uses "Sie", so those strings now do too.
Brokers and coordinators can edit, pin and delete member notes from the broker panel's Members page (F-545). They could add notes, but every edit, pin and delete was refused. Concern notes about themselves, or about anyone they do not outrank, stay out of their reach.
Brokers and coordinators can read and change the moderation settings shown on the broker panel's Content Queue page (F-543). The switches were admin-only, so in the broker panel they showed blank and saving them was refused.
Explore's "Recommended for you" could show a member their own listings. One of its sources falls back to the community's most popular listings, and nothing removed the member's own from that list; they are now excluded once, for every source.
The Help Centre guide "Setting up your profile for the first time" no longer describes the retired Interests step or says the platform creates listings from your skill choices, in all eleven languages.
The personalised listings and feed ordering now really favours listings that match a member's skills. Its "interest match" compared the category id stored on a member's skill (
user_skills.category_id, askill_categoriesid) with a listing's category id (listings.category_id, acategoriesid). Those are two unrelated numbering systems, so a listing was boosted only when the two numbers happened to be equal, whatever the categories were called. Skills saved by the welcome wizard have no category id at all, so they never counted. The match now compares the member's skill names, ignoring case, with the listing's category name and its skill tags, every lookup scoped to the community (PersonalisedFeedService). Listings shared into the feed are matched the same way.Explore's "Recommended for you" never used the skills a member offers to find listings tagged with them. The lookup named a column the skill-tags table does not have, and the error was silently swallowed, so that part of the recommendations returned nothing. It now works.
The local pre-commit check (
scripts/git-hooks/pre-commit) could hang for good on a commit that included a test which sends email, such asRegistrationOnboardingTest: the platform mailer still connects to the container's mail host (mailhog), which does not exist on a developer machine. The check now points mail at the container itself, so the same tests finish in seconds. CI is unaffected.Settings → Skills: the search box showed blank suggestions, because it expected a different reply from the server than it receives. It now lists the skill names other members of the community use. Typing a new skill and pressing "Add Skill" now adds it; before, nothing happened unless "Use …" was pressed first. Members can also now mark a skill as one they want help with ("Wants"), not only one they offer, and matching reads both.
New members in a community with no safeguarding questions set up no longer see an empty "Safeguarding" step in the welcome wizard (web and mobile app). The step was switched on by default and showed "No safeguarding options have been configured for this community".
GET /v2/onboarding/confignow leaves the step out until the community has at least one active safeguarding option, and a "required" safeguarding step no longer blocks completion when there is nothing to answer.Brokers and coordinators can now open and handle volunteering safeguarding incidents from the broker Safeguarding page (F-536). They were already alerted to new incidents, linked to that page and could be assigned an incident, but the page did not show volunteering incidents and the list and update endpoints (
GET /v2/admin/volunteering/incidents,PUT /v2/admin/volunteering/incidents/{id}) refused them, so a reported concern could go unread in a community where brokers run safeguarding. Nobody sees or handles an incident about themselves (F-507), and assigning an organisation's designated liaison person stays with administrators.Admin → Settings → Help Centre opened a page whose every article is about the Caring Community module, even in communities that have Caring Community switched off. Without the module, the menu entry now opens the full admin guide (
/help/admins) instead, and typing/admin/helpshows the admin "not found" page. The Help FAQs editor (/admin/help/faqs) is general and is unchanged.The French admin help guide told administrators to open "GRC" in the admin menu, but the menu item is labelled "CRM". The guide now uses the word the menu shows. (Two automatic checks that count design exceptions and progress bars were also brought up to date after the Onboarding Funnel page was rebuilt.)
Onboarding Funnel numbers were wrong. Each step was counted on its own, so a later step could hold more people than an earlier one and the page reported step rates of 500% and 150%. Each member is now counted at the furthest step they have reached, so numbers only fall down the list and every percentage is out of all members. Banned, suspended, rejected and deleted accounts are no longer counted; starting balances and admin credits no longer count as exchanges; and system credits with no member on the other side were themselves being counted as a "repeat user". The monthly chart now shows months with no sign-ups instead of skipping them, and no longer compares the first days of the current month against a whole previous month (which showed "-92%" on 2 October). The broker panel's onboarding card uses the same figures and is corrected too.
Volunteer expenses (Admin → Volunteering → Expenses): the review window always opened on "Approve", even for an expense that was already approved, and approving it again failed with only "Failed to update expense". The window now offers just the next valid step — approve or reject a pending claim, mark an approved one as paid — and paid or rejected claims no longer show a Review button. If another admin has dealt with the claim in the meantime, the message now says so in plain words in every language ("This expense has already been reviewed…" / "Only an approved expense can be marked as paid."), and the list refreshes to show the claim as it now stands. A claim in the wrong state now answers 409 rather than a misleading 404 "not found" (
PUT /api/v2/admin/volunteering/expenses/{id}).Corrected wrong translations on the volunteering admin pages: the Decline button read "decrease" in Arabic and "decay" in Irish; Arabic also showed "energetic" for Active and "decreased" for Declined; the Irish page title said "willing organisations"; Japanese used "withdraw" for Decline. Hour amounts no longer run into the unit ("12horas") in most languages.
Dropdowns no longer let the selected text run underneath the arrow — first reported on the Priority filter in CRM → Coordinator Tasks. The shared dropdown applied even padding on both sides, which overrode the extra room HeroUI keeps on the right for the arrow; that room is now always reserved while the arrow is shown, at every size.
Coordinator Tasks (CRM → Coordinator Tasks): the button that marks a task complete can now be seen. It was a small white tick box on a white card; it is now a round button with a visible border and a tooltip, filled green once the task is done, and the task menu offers "Reopen task" for a completed task instead of "Mark Complete" again.
Searching for a member inside an admin pop-up now shows its results. The member search used by Coordinator Tasks, Member Notes, Member Tags, the Activity Timeline, partner venues and the caring-community admin pages drew its result list inside the pop-up, which cut it off, so the search appeared to do nothing. It is now a HeroUI ComboBox whose list floats above the pop-up. Members found by email address are also shown now; the list previously re-filtered the server's results by name and hid them.
The Create Task form now explains what a task is for, labels the member field "Member this task is about" with a hint, explains who "Assign to" means, and fills "Assign to" with you instead of an empty "Select an item" (a blank choice already meant you).
The CRM Activity Timeline (CRM → Activity Timeline) no longer says a member "Updated their profile" when the system changed their record. The entry was taken from the last time anything at all changed the member's account, so automatic changes — the leaderboard awarding season bonus points at midnight, an admin edit — appeared as the member editing their profile, including members who had not signed in. The entry is now recorded at the moment a member saves a change to their own profile or photo, and shows that time. Profile edits made before this release were never recorded, so they no longer appear.
Newsletter analytics (Analytics & Reporting → Newsletters) now shows each community's real figures. It reported "No analytics found" and zero campaigns on every community, including ones that had sent thousands of newsletter emails: its summary query asked the database for two columns that do not exist, and the failure was silently turned into an empty result. Its "Subscribers" figure now counts active newsletter subscribers rather than every active member, "Campaigns sent" no longer counts drafts, and if the figures cannot be loaded the page now says so instead of claiming nothing has been sent.
Signing in no longer runs a database schema statement. The login rate limiter issued
CREATE TABLE IF NOT EXISTS login_attemptson the first sign-in of every server process, and MariaDB silently commits any open transaction before such a statement, even when the table already exists. That split any transaction the sign-in ran inside. In the test suite it permanently saved the fixture data of whichever test signed in first, which made unrelated login tests fail depending on run order. The table is part of the committed database schema, so the statement has been removed (app/Core/RateLimiter.php);tests/Laravel/Feature/Core/RateLimiterTransactionSafetyTest.phppins it.
Security
F-577 (Low): per-member rate limits on signed-in routes counted by network address instead. On every authenticated API route the
throttle:middleware runs beforeApp\Http\Middleware\Authenticate(that class is not in Laravel's middleware priority list), so when a named limiter ran,$request->user()was always null for a bearer-token caller and the limiter fell back to its per-address key. Affected: everynexus-route-*tier on a signed-in route (about 194 routes), thegroups-*family,bulk-export,events-people-bulk,podcast-media, and the signed-in branch ofmfa-challenge. Members sharing one address (an office, a care home, a mobile carrier's shared address) shared each other's allowance, and one member changing address got a fresh one. The group-wideapilimiter was already correct (F-036). Every limiter now takes the member from a verified access token, the way F-036 does, through one helper,RouteServiceProvider::verifiedActorId(); unverified, expired or revoked bearer text still counts by address.RouteThrottleIdentityTestsweeps the live router so a new limiter on a signed-in route is covered automatically. The member-import limiters stay per-address on purpose; their per-administrator limit is enforced in the controller.F-572 (Low): confirming an email could make an account held for its identity check look live. An account an administrator creates on a community that requires an identity check is stored approved but pending until the check passes (F-278). Confirming the email activated any approved, pending account, so a held member who asked the public "resend verification" form for a link and clicked it became
active— visible to member search, digests and other "live member" lists — while the check was still outstanding. They still could not sign in. Confirming an email now never activates an account whose identity check is unfinished or refused, on every path. Regression test:EmailVerificationControllerTest::test_verify_email_does_not_release_an_account_held_for_its_identity_check.Volunteering — nobody can confirm their own shift check-in or check-out. A volunteer who was also on the organisation's team (or a community admin) could scan-verify their own check-in code and check themselves out, attesting their own attendance. The server now refuses that with a clear message; another organiser has to do it.
Feed posts and group discussions now keep only the formatting their editors can make (F-569, follow-up to Cyphere's stored-HTML-injection finding). The feed post editor makes bold, italic, underline, strike, lists and links; the group discussion editor also makes two heading sizes and quotes. Anything else — a large heading, a picture loaded from another site, a table — could only reach a post by being sent straight to the API, and it then showed on the page. It is now removed both before saving and when showing the post; the words inside it are kept. Links show only their real address as the clickable part, and this is now also enforced before saving: a member's own link words are stored as plain text followed by the full address as the only link (fake "user@" prefixes removed, look-alike international domain names stored in their encoded form), so the saved post and every API response match what the page shows. No existing post, discussion or help answer on production used any of the removed formatting, so nothing anyone can see today changes.
Typed HTML in a comment or description is now shown as plain text, never rendered (F-568; root cause behind Cyphere's F-562 stored-HTML-injection retest). Comments and the description boxes for listings, events, groups, ideas and job adverts are plain typing boxes with no formatting buttons, yet whatever was typed into them used to be treated as web code when shown back: a member could type a heading, a table, an image pulled from any website, or a link, and it rendered. The earlier F-562 fix only stopped the link text being clickable, so every other tag still rendered. Now these boxes store and show exactly what was typed, as words — any HTML tag appears as plain text, the same way the accessible site and the phone app already behave. This covers comments everywhere (feed, events, polls, listings), the listing/event/group/idea/job descriptions, and group announcements, questions and answers, and challenges. Genuinely formatted areas that are meant to have formatting (feed posts, group discussions) and all staff-written content (blog, knowledge base, legal, custom pages) are unchanged. Server side, the same text-only rule is applied before storage, so the protection does not depend on the browser.
Comments that contain only markup (which now becomes empty text) are rejected with the normal "comment text is required" message rather than saving a blank comment, and an edit that would empty a comment this way leaves the existing comment untouched.
Member rich text: a linked image is now unwrapped even when the link's visible text happens to match its address, so an image can no longer stay clickable through the "the text already is the address" exception.
Phone app build tooling:
shell-quotepinned to 1.12.0 (CRITICAL GHSA-pqg4-j6r4-53mv, command injection inquote(); pulled in byreact-devtools-core, developer tooling that does not ship in the app). The dependency audit that blocks the Security Scan passes again; nothing in the installed app changes.A member's own words can no longer be a link (F-562, Cyphere retest of 6 Oct 2026). The 4 Oct fix kept a member's link clickable and showed its destination beside it; Cyphere retested and failed it, because the words the member chose were still the link. Now, in anything a member writes (listing, event, group and organisation descriptions, posts, comments), every link is unwrapped: the member's words stay as plain text and are followed by one link whose text is its own address, written out in full with any fake user-name part removed and look-alike international names shown in punycode. A link whose text already is its address (a pasted URL) is left alone. Administrator content (blog, knowledge base, legal, custom pages) is unchanged. The accessible site and the phone app never rendered member links as live. The address link is shown as a link (underlined, in the platform colour) and wraps inside a narrow comment box instead of overflowing it; and if the page ever has no document to work with, the link tags are dropped and the words kept rather than letting a labelled link through.
Volunteering: a person allowed to decide an opportunity's applications (for example an organisation admin) could approve their own application to it. Nobody can decide their own application now, whatever their role;
PUT /v2/volunteering/applications/{id}answers 403.Dependency security fixes (accessible site and phone app).
proxy-addr2.0.7 → 2.0.8 in the accessible site (CVE-2026-90711, rated critical; Express uses it to work out each visitor's address behind the proxy) andsource-map-js1.2.1 → 1.2.2 in the accessible site and the phone app's build tools (CVE-2026-93749 / GHSA-68fv-2mgg-jv7q, a denial of service from a malformed source map). Version bumps only, within the ranges Express, PostCSS, Sass and Tailwind already accept; nothing else changes.The footer "Powered by" badge settings are now platform-owner only on the server, not just in the admin screen. The badge's label and link could be changed by any community admin with a direct
PUT /v2/admin/settingsrequest; the admin screen hid the fields but the server did not check. Every badge setting now needsusers.is_god, the same rule the badge image uploads already had. Badge links must be fullhttp://orhttps://addresses, and a badge image can only be set by uploading a file (the settings request may only clear one).Volunteering safeguarding: when an incident was reported about a member of community staff, that staff member was sent the same alert as everyone else — bell and email, with the title and the reporter's name. Nobody is now told about an incident that is about them, matching the existing rule that nobody sees or handles such an incident (F-507).
The member wallet statement CSV uses the shared formula guard (F-565, Low; E-089).
GET /v2/wallet/statement(also behind the accessible frontend's wallet export) neutralised spreadsheet formulas with its own rule that looked only at the first character, so a trigger behind leading whitespace or a control character (" =1+1","\x01+1+1") passed through. Every CSV the platform writes now goes throughCsvExportSanitizer. Found while re-auditing Cyphere's wallet CSV finding (F-561); not reported by Cyphere and not shown to be exploitable in Excel.Members can no longer hide transfers from their own wallet history (F-564, Informational; E-089, Cyphere pen test).
DELETE /v2/wallet/transactions/{id}set a per-member hide flag on a transfer. The balance and the other party's record were never touched and no client offered the action, but a member must not be able to edit their own view of the time-credit ledger. The route, controller action and service method are removed; hide flags already set are still honoured on read, and nothing can set new ones.Links in member-written text now show where they really go (F-562, Medium; E-089, Cyphere pen test). A member could put
<a href="https://evil.example/login">Click here to re-authenticate</a>into a listing, event or poll comment and it rendered as an ordinary platform link — safe from script, but a ready-made phishing lure in other members' feeds.sanitizeMemberRichTextnow appends the destination host (or mailbox) in brackets to every link that leaves the platform's origin unless the visible text already names it, and a link with no text shows its full address. Applied to the sanitiser's output rather than inside a DOMPurify hook, so content removed later cannot satisfy the check. Relative and same-origin links, and administrator-authored content, are unchanged.Spreadsheet exports built in the browser now neutralise formulas, and so does the platform billing export (F-561, Low; E-088). Seven admin and member exports wrote other people's text straight into CSV: Wallet → Export (other members' names and transfer descriptions), Newsletter subscribers (names typed into public sign-up forms), Federation activity, the FADP processing-register export, the Federation and platform audit logs, and Cron job logs. A value such as
=HYPERLINK(…)became a live formula when the file was opened in Excel or LibreOffice. A sharedreact-frontend/src/lib/csv.tsnow quotes and neutralises every cell with the same rule as the server'sCsvExportSanitizer, and all seven use it. The super-admin billing CSV (TenantBillingService::exportCsv) wrote community and plan names unneutralised — a community's own admin sets its name — and now usesCsvExportSanitizer. Also fixed in passing: the cron log export never doubled inner quotes (a"broke the file), and the platform audit log export put the target and target-type columns under each other's headings.Office uploads must be real Office documents (F-559, Low; E-088). Message attachments, resources and job CVs accepted any ZIP archive as a
.docx/.xlsx(a plain archive renamed.docxwas accepted at all three). A sharedOfficeDocumentInspectornow requires a genuine package ([Content_Types].xmlplus the main word/, xl/ or ppt/ parts) at all four Office upload points including group files, and refuses packages carrying a macro project (vbaProject.binor a macro-enabled content type). Group files previously inspected only files the type detector reported as a bare ZIP, so a macro-carrying.docxthat it recognised as Word was accepted; it is now inspected too. Legacy.doc/.xls(OLE) files are not inspected.Paper onboarding stores scans under an extension that matches their content (F-560, Low; E-088).
PaperOnboardingIntakeService::createFromUpload()kept the extension the uploader sent, so JPEG bytes namedx.phpwere stored as<uuid>.php(private disk, no serving route, admin-only — harmless today, but the exact precondition F-554 guards against). The extension now comes from the detected type (PDF, JPEG, PNG, WebP) and anything else is refused.Admin member CSV import checks the file itself and has limits (F-558, Low; E-088).
POST /v2/admin/users/importdecided "is this a CSV?" from the type the browser declared — PNG bytes declaredtext/csvwere accepted — and had no size or row limit (a 50,000-row file was processed in one request). The type is now detected from the file's content (text only, no NUL bytes), files over 2 MB are refused, and a file with more than 5,000 rows is refused whole before any account is created. Both new messages are translated in all eleven languages.The admin per-member statement download is a real CSV with formulas neutralised (F-557, Low; E-088). Admin → Timebanking → User report → download built its CSV by joining strings, so a transfer description such as
=HYPERLINK(…)was written as a live spreadsheet formula, and it returned the CSV inside a JSON envelope, so the saved.csvfile actually contained JSON. It now streamstext/csvas an attachment, every cell goes throughCsvExportSanitizer, and amounts are signed numbers. The "Balance After" column, which repeated today's balance on every row, is replaced by each transaction's status: balances also change through grants and adjustments that are not in this list, so a per-row balance cannot be computed from it.Knowledge-base attachments are stored privately (F-556, Low; E-088). They were written to the public disk, so a draft article's attachment answered an anonymous request at its
/storage/tenant_X/kb_attachments/…address even though the API download route correctly refused members. New uploads (andkb:import) now go to the private disk and are read only through the API route; files already on the public disk still download through the API, and both Apache layers (httpdocs/.htaccessfor dev,docker/apache/storage-alias.conffor production) refuse the statickb_attachmentspath. Retested locally and on the production image.Private file downloads keep their own locked-down Content-Security-Policy (F-555, Low; E-088). Message attachments, voice messages and admin support screenshots send
default-src 'none'(withsandboxfor message media) so that a stored file opened directly can run nothing, but the platformSecurityHeadersmiddleware replaced it with the page policy on every API response. It now keeps an endpoint's policy when that policy grants nothing at all (default-src 'none', every other directive'none'or a baresandbox) and still replaces anything looser. Verified live on the attachment and screenshot routes.Public storage (
/storage) in the production images now gets the same protection as/uploads(F-554, Medium; found by internal review E-088). Production maps/storagetostorage/app/publicwith an ApacheAliasoutside the web root, sohttpdocs/.htaccess— its script-extension guard and its sandbox CSP — never applied there; on the image built fromDockerfile.bluegreena.phpfile placed on the public disk ran, and every other file was served with noX-Content-Type-Optionsand no CSP. No upload path was found that lets a member choose the stored extension, so this was a missing second line of defence rather than a demonstrated exploit. Both images now install one shareddocker/apache/storage-alias.confthat refuses script-like files, switches the PHP engine off, and sendsnosniffplus the same sandbox CSP as/uploads; retested on the production image (PHP-family files 403, everything else sandboxed)..dockerignoreand.gitignoregained the exceptions the new file needs, and a test pins both images, the config and the CSP match.The accessible frontend's error pages named the web framework (F-553, Low; found by an internal review while re-testing F-552). Helmet removes Express's default
X-Powered-Byheader on normal pages, but when the community lookup fails with anything other than "not found" the error is rethrown out oftenantRouting, Express skips every later middleware, and the error page keptX-Powered-By: Express(seen live on/wp-login.phpand unknown paths on the accessible hosts).app.disable('x-powered-by')now switches it off at the application level so no response path can send it; a test covers both the normal and the error-page path.Uploaded PDFs could carry JavaScript that ran when another member opened them (F-551, High; reported by Cyphere during the independent penetration test). Cyphere uploaded a PDF with an embedded script to a group's files and confirmed it ran when the file was opened in a browser's PDF viewer. No upload feature looked inside a PDF. A new inspector (
app/Support/Uploads/PdfActiveContentInspector.php) reads the PDF's structure, including compressed object streams, objects hidden inside other objects' stream data (a viewer reaches those through the cross-reference table; everyobjandtrailerkeyword is read, so comments or odd spacing in an object's header cannot hide it) and #-escaped names, and refuses scripts, launch actions, form submission, XFA forms and rich media; a file it cannot fully read (an unsupported encoding inside an object stream, a truncated object stream, an encrypted file with object streams, a decompression bomb, or more than 2 seconds of work) is refused rather than passed. Because the check runs before sign-in, one request may cause at most 3 seconds of inspection across all its files. It runs once for every API upload through the newRejectActivePdfUploadsmiddleware in theapigroup, so messages, group files, team documents, resources, help-centre attachments, job CVs, expense receipts, volunteer credentials and the safeguarding statement are all covered, andGroupFileServicerepeats the check. The member sees a translated message asking them to print or export the document to a new PDF: from the API (api.pdf_active_content_refused,api.pdf_not_inspectable), on the React group files page (groups:files.upload_pdf_*, which previously showed only "Failed to upload file") and on the accessible group files page (govuk_alpha_groups.states.file-pdf-*). Other upload pages still show their existing generic error. Restricted-but-openable PDFs saved with compressed object streams (some bank statements, payslips and official forms) are refused as uncheckable, and fillable forms with a submit button are refused. Checked against 400 real PDFs with no false refusals. Pinned bytests/Laravel/Feature/Security/E086/F551ActivePdfUploadRefusedTest.phpandF551PdfActiveContentInspectorTest.php. Files uploaded before this change are not re-scanned.The React web server ran an out-of-date nginx (1.27.5) and printed its version on its own error pages (F-552, Medium; reported by Cyphere during the independent penetration test). The production image was built from
nginx:alpine3.21, a tag that freezes nginx at 1.27.5, and nginx's built-in pages (for example the 410 returned for/wp-login.php) ended withnginx/1.27.5, on production and on the pen-test server. Both React Dockerfiles now build fromnginx:1.30-alpine(the supported stable line, 1.30.5 today), both nginx configs setserver_tokens off, and the four nginx test scripts use the same image so CI tests what ships. The deploy's Dockerfile sanity check (scripts/deploy/phases/validate-env.sh), which looked for the literalFROM nginx:alpineand would have stopped the next deploy, now accepts any Alpine-based nginx image, and a test pins the check and the Dockerfiles together. The deploy builds with--no-cachebut not--pull, so later 1.30.x patch releases arrive only when the base image is pulled on the server. Pinned bytests/Laravel/Feature/Security/E086/F552ReactNginxVersionDisclosureTest.php.High-risk staff actions now ask for a fresh authenticator code (E-085). Because staff can now remember a device, being signed in no longer proves the code was entered recently. Changing a role or sign-in email, granting super-admin, banning or deleting a member, setting someone's password, resetting their two-factor, signing in as a member, adjusting balances or granting credits, reversing an exchange, erasing via a GDPR request, managing enterprise roles, changing sign-in/SSO/API-key/secret settings and moving or purging communities are refused with
AUTH_STEP_UP_REQUIREDunless a code (or recovery code, passkey, or identity-provider sign-in) was entered in the last 15 minutes (5 for resetting someone's two-factor). The app asks for the code and repeats the action; one confirmation covers further high-risk actions for the next few minutes. A password alone, or a remembered device, is not accepted. New middlewarestep-up(App\Http\Middleware\RequireRecentSecondFactor); teststests/Laravel/Feature/Security/TwoFactor/StaffStepUpTest.phpandStaffRememberedDeviceTest.php.A broker's report queue told them how many complaints were about them (F-549, Low). Reports a broker is part of are withheld from their queue (F-454), but the queue's total was only corrected for the page being shown, and the page's count boxes counted every report in the community. Either number, minus the reports the broker could actually open, was the number of complaints about them or their content. The rule now lives in one place (
App\Services\ReportQueueVisibility) and the queue, its count boxes and the new dashboard card all count only what the broker can open. No report content or reporter identity was exposed. Regression testtests/Laravel/Feature/Security/E084/F549ReportQueueTotalTest.php.A broker or coordinator whose account also carried an admin flag could change the broker panel's admin-only settings (F-548). The save for the broker Configuration page decided "admin" by its own check, which counted the flag whatever the role. Everywhere else the platform's single rule (
AdminTier) treats brokers and coordinators as operational staff, never admins, even with the flag. The save now uses that rule, and the Configuration page locks the same settings for those accounts.The code scanner (Semgrep) began blocking on two places that insert HTML into a page, the feed post renderer and the shared
SafeHtmlcomponent, after an update to its online rules on 2026-10-03; neither file had changed. Both already pass the HTML through the platform's DOMPurify profile (sanitizeMemberRichText), and both findings had previously been reviewed and dismissed as false positives on GitHub. The reasoning is now recorded on the lines themselves (nosemgrep), which is what the blocking scan reads.A new high-severity advisory in
braces(CVE-2026-93687 / GHSA-vfj7-8cjw-p6xm, a stack-overflow denial of service from deeply nested patterns), published with no fixed version, is recorded as a time-limited exception for the mobile app and the accessible frontend (expires 2026-11-03). In both it is only ever run on the repository's own file patterns: in the mobile app by build and pre-commit tooling that does not ship in the app, and in the accessible frontend by a template file-watcher that is switched on only in development.The person a concern note is about can no longer rewrite it (F-544). They could already neither read nor delete it, but they could overwrite its text or move it out of the concern category through the member-notes edit route. The edit is now refused, as the read and delete routes are, for the subject and for anyone they do not outrank.
The production settings file is now owner-only, and every deploy keeps it that way (F-541).
/opt/nexus-php/.envholds every platform secret. It, and seven.env.bak-*copies holding older secrets, could be read by every account on the production server (mode 664/644). They were set to 600 by hand on 2 October 2026. A new deploy step,secure_env_file_permissionsinscripts/deploy/phases/validate-env.sh, now runs before anything is built. It re-applies owner-only access to.envand every.env.bak-*copy, logs any file it had to tighten (never its contents), and stops the deploy if it cannot. This matters because a hand edit or acpbackup quietly reopens the file. A new CI test,scripts/test/test-env-file-permissions.sh, covers it in the Migration Safety Gate.The monthly restore drill now proves the encrypted offsite copy, and reports every result.
scripts/restore-drill.shused to restore only the copy kept on the server and told nobody when it failed. It now downloads the newest encrypted database backup from the offsite remote, refuses one that is missing, more than three days old or not really encrypted, decrypts it with the drill's own key, restores it into a throwaway database and checks row counts and that the database's triggers and views came back. It also unlocks the uploaded-files, storage and server-configuration backups from the same night, checks each is a complete archive that actually holds files, and checks the configuration still carries the master key. It ignores sub-folders in the Drive folder, fails if it cannot read the live database to compare against, then deletes everything it downloaded. The monthly message lists the start of each key the backups are locked to, marked as owner key or server drill key, so the owner can see their own keys are still on the list. Pass or fail it sends a Telegram message; a drill that cannot send its message counts as failed. A lost or wrong key is therefore found within a month rather than during a disaster.DRILL_SOURCE=localkeeps the old local-only drill for development. It also now reads database settings safely from an.envthat has Windows line endings or both spellings of a setting (F-123, E-080).Every deploy now installs the monthly restore drill's schedule. The drill's 04:00-on-the-1st schedule was only a comment in the script, and no deploy step installed it, so nothing guaranteed it ever ran.
scripts/deploy/phases/install-backup-cron.shnow writes/etc/cron.d/nexus-restore-drillnext to the nightly backup's cron file, re-asserting it on every deploy and correcting hand edits (F-123, E-080).The Google Drive sign-in used for offsite backups is now limited to the backups it creates.
scripts/setup-rclone-local.shasked Google for access to the whole Drive; it now asks fordrive.file(only files the backup tool itself created), refuses to continue with a wider token, and sends the server only that one connection instead of the whole local rclone configuration.scripts/setup-rclone-gdrive.shinstalls rclone and age from Ubuntu's packages instead of piping a script from the internet into a root shell. It also no longer adds its own nightly schedule next to the one every deploy installs, which made two backups run at once; it removes that old entry, finishes by running one real backup and then the restore drill, and its printed restore help now describes the encrypted files. On Windows it now also finds rclone right after installing it through winget, which puts it in a versioned sub-folder the script did not look in. Takes effect only when the owner re-runs the sign-in (F-123, E-080).New server step to set up backup encryption:
scripts/setup-backup-encryption.sh. Run once as root on the server with the owner's two public keys. It generates the restore drill's own key (once, never replaced), writes the root-only list of public keys the backups are encrypted to, and stores the Telegram details for backup alerts. It refuses fewer than two owner keys, the same key twice, anything that is not an age public key, and any private key.scripts/setup-rclone-gdrive.shnow calls it before its test backup (F-123, E-080).Two tools for the owner's own computer to make and test backup keys:
scripts/backup-keygen.shandscripts/backup-decrypt.sh. The first makes a key and shows the secret half, also in groups of four for a paper copy, plus the public half for the server; it can save a private copy (readable only by the owner). The second opens any downloaded backup file, or the small key-check file, with a key that is pasted, typed back in from paper (spaces and any case accepted) or read from a file. It says PASS or FAIL, never stores the key and never leaves a partial file. It only says the key does not open a file when that is what age reports; a damaged download or a failure to fetch the tool is reported as such, with age's own message, so a good key is never mistaken for a lost one. On Windows both run age in a throwaway Docker container, so nothing needs installing (F-123, E-080).The backup encryption tests now run in CI. The Migration Safety Gate installs
ageand runs the six backup tests (nightly encryption, restore drill, cron install, server setup, owner key tools, Drive setup checks). The Drive setup check also had a flaw that could have let a "this must not be present" check pass when the forbidden text was present; fixed and confirmed against the old scripts (F-123, E-080).The extra database backup taken during deploys no longer goes to Google Drive unencrypted, and no longer deletes other backups there. A deploy that changes the database makes its own backup first (
scripts/deploy/lib/db-backup.sh). Unless an older encryption setting happened to be configured, that copy was uploaded to the same Drive folder as a readable file, and its clean-up deleted every file in the folder older than seven days, the nightly backups included. It now encrypts to the same keys as the nightly backup, skips the offsite copy rather than upload it readable, does not let a logging step hide a failed upload, and cleans up only its own files. The monthly restore drill now counts every unencrypted file still in the folder, whatever wrote it (F-123, E-080).The nightly backup now also protects the server's master key, and no longer fails quietly. Some database fields (two-factor secrets, encrypted identity details, tokens) can only be read with the key in the server's
.env, and no backup held it. The nightly job now uploads that file encrypted, asnexus_config_DATE.tar.gz.age, and keeps no unencrypted copy of it in the backups folder. It also: alerts on any failure anywhere in the job, not only the steps that were checked; gives files their real name only after they are verified, so a crash cannot leave a broken file that looks healthy; treats a missing Google Drive sign-in on the server as a failure rather than quietly skipping the offsite copy; refuses to back up a file store that does not exist or holds no files; and requires at least two of the owner's keys, no longer counting the server's own drill key, which would be lost with the server (F-123, E-080).The nightly offsite backup copies are now encrypted before they leave the server.
scripts/server-nightly-backup.shencrypts the database, uploads and storage backups withageto every public key in a root-only recipients file on the server (at least two, so losing one key does not lose the backups) and uploads only the encrypted*.agefiles, plus a small key-check file the owner can open to prove a stored key still works. Missing keys, a single key, a missingageor a failed upload now stop the upload, exit with an error and send a Telegram alert (newscripts/backup-alert.sh, which reads its settings in a way that cannot silently lose them and treats a message Telegram rejects as not sent); there is no unencrypted fallback. The nightly job now also refuses a database dump that was cut short (no completion line) instead of keeping and uploading it, and any failure of the nightly job, not only the upload, sends a Telegram alert. Copies kept on the server stay unencrypted for fast restores.rclone syncof the whole backup folder is replaced byrclone copyof the encrypted files only, with offsite clean-up limited to those files. Needs the keys installed on the server before it is deployed, or the offsite copy stops (F-123, E-080).Four platform-maintenance pages are now limited to god accounts. The test runner, sample-data generator, image converter and blog restore pages opened for any community administrator who typed their address, although they act on or report about the whole installation and the sample-data tool's own server check already required a platform administrator. They now use the same god-accounts-only guard as the cron jobs and performance pages (F-534).
Six old admin page addresses now forward to the current pages instead of bypassing their module checks. Old addresses for group types, group ranking, group locations, group geocoding and two smart-matching screens still opened pages directly, and because the admin area checks module switches by the start of the address, these older spellings skipped those checks. They now forward to the current page for each (group types, group ranking, the groups list, the broker match approvals, the matching analytics), which carries the normal check. None of them was linked from any menu (F-533).
The AI-agents admin pages now close when the AI-agents switch is off. The menu already hid them and the server already refused their requests, but typing a page address still opened an empty page whose every request failed. The pages now show the usual "not available" screen instead (F-532).
The older AI-agents admin tools now need both the AI-agents switch and the Caring Community switch. The older KI-Agenten admin page and its ten actions disagreed about which switch turned them on: the page followed the AI-agents switch, three actions followed the Caring Community switch, and the other seven, including approving the agents' suggestions one at a time or all at once, followed no switch at all. A community with both switched off could still apply agent suggestions. The page and every action now require both switches (F-531).
The Swiss data-protection (FADP) admin pages now stay closed unless the community has switched that module on. The module is off by default and the admin menu already hid it, but typing the page address still opened it, and the nine admin actions behind it (retention settings, the processing register, the consent ledger export and the disclosure pack) only checked that the caller was an administrator. The page and those actions now require the module to be switched on. Members' own consent records are unchanged (F-530).
Switching off the marketplace now also switches off coupon administration. The admin page for merchant coupons, and the three admin actions behind it (list, pause, delete), checked only the coupon switch and ignored the marketplace switch above it, so a community with the marketplace turned off could still manage coupons. Both the page and the actions now need both switches on, the same rule members and sellers already follow (F-529).
An account record can no longer leak private fields if it is ever turned into a response by mistake. The safety net that hides private account fields only covered the current password field. It now also hides the old password column still filled in for some older accounts, the password-reset token, and contact, location, payment and role details, so an accidental output elsewhere in the code cannot expose them (F-417).
The phone app's check that every outside link goes through its safe-link helper now looks at the whole app. The automated check only searched two folders, so four places that open web pages, in the payment and link-handling code, were outside its view. It now searches every folder except build output, and the places that legitimately open links themselves are named with the reason (F-480).
Links that open the phone app can no longer slip past its own safety checks. When the app could not match a link, or deliberately refused it, it still handed the original link to the app's screen router, so any web page, email or other app could open screens the checks were meant to keep closed. Refused and unrecognised links are now stopped, apart from the payment-return links members need. The check that keeps the in-app image viewer private could also be dodged by writing its name in capitals or with an encoded slash; it now compares names case-blind and after decoding (F-496, F-497).
A refusal from the platform's background-jobs button now carries a readable message instead of the number 400. Platform administrators have a page that lets them start one of the platform's routine background tasks by hand. If the task named does not exist, the refusal came back with the text the administrator had typed sitting in the slot reserved for the platform's own error name, and the number
400sitting in the slot reserved for the message a person reads. Nothing unsafe happened — only platform administrators can reach the page, the refusal still refused, and nothing was displayed as a web page — but any tool reading the answer had no reliable way to tell one kind of refusal from another, and a person reading it was shown a number. The refusal now carries a proper error name and a readable message, matching the equivalent refusal on the scheduled-tasks page beside it. Starting a task that does exist is unchanged (F-470).A sign-up addressed to a community the platform cannot identify is now refused instead of quietly creating the account in the main community. Apps and websites can tell the platform which community a visitor is in by sending its number. If that number arrived as something that is not a number at all — a community's short name typed into the wrong box, for example — the platform ignored it completely and fell back to the main community. Somebody signing up for their local timebank was therefore told "registration successful" while their account had actually been created somewhere else entirely. The same mistake made with an unrecognised number has always been refused; the unreadable value now gets the same refusal. Real community numbers, short names and community web addresses all still work exactly as before, and pages that name no community at all — the home page, sign-in, about — are untouched (F-479).
A community number with a decimal point or similar no longer gets quietly rounded down to a different community. The fix above refused values that are not numbers at all, but a value such as
1.9or1e0still counts as a number to the software, which then chopped it down to a whole number —1, the main community — so a sign-up sent that way was still created in the wrong place. Only a plain whole number is now accepted as a community number; anything else gets the same refusal as an unknown community. Ordinary community numbers, short names and web addresses work as before (F-504).The "who has read the new policy" figures can no longer be pushed up by reloading a link, and no longer count reads that cannot have happened. Each policy email carries a tracking image and a tracking link. Every time either was fetched it was counted again, so one person reloading eight times showed as eight opens; reads were counted for emails that had not even been sent yet; and the automatic scan that many email security systems do the moment a message arrives was counted as the member reading it — which wrongly took that member off the list of people an administrator still needs to chase. Each recipient now counts once, only after their email has been sent, and not during the first two minutes when those automatic scans happen. Tampered links still record nothing. Separately, the "read the policy" button in these emails pointed at an address the server did not answer, so it led to an error page; it now opens the policy (F-469).
A broker can no longer close the safeguarding re-check about an administrator. When a community's safeguarding rules change, the platform raises a "please re-check this person's clearance" request for each affected account. Brokers could already not change an administrator's clearance decision itself, but they could still mark the re-check request about an administrator as done, so nobody was prompted to look again. Closing a re-check now follows the same rule as the decision: a broker may close one only for an ordinary member, administrators keep their full say, and nobody may close the one about themselves (F-502).
Removing a listing from a marketplace collection now says so when there was nothing to remove. Asking to take a listing out of one of your collections always answered "removed", even when that listing had never been in the collection — including a listing from another community. Nothing was ever changed or revealed, but anything relying on the answer was misled. It now answers "not found" in that case, the same way whether the listing exists elsewhere or not at all. Removing a listing that really is in your collection works as before (F-503).
Publishing a policy in a large community no longer holds up the administrator, or the policy itself, while every member's email is lined up. Publishing wrote one email record per member there and then, inside the administrator's own request and while holding a lock on the policy — in a big community that is thousands of rows before the page could answer. Now only the first 250 are written at that moment, saved together with the publication itself, and the rest are added in the background in batches. If that background step is ever lost, the regular email task notices and finishes it, so nobody is missed. Policies published before policy emails existed are never picked up by that catch-up, so they will not suddenly send emails (F-471).
Safeguarding alerts about a member now stop going to their old community once they have moved. When a platform administrator moved a member to a different community, the yearly safeguarding check-in and the safeguarding-clearance renewal alerts kept going to the old community's staff — naming the member and the kind of record — while the member's new community heard nothing. These alerts now only go out while the member still belongs to the community that holds the record (F-424).
The older partner time-credit transfer now re-checks everything at the moment the credits move. The original partner connection (switched off by default, and off on the live platform) checked the people, the partnership and the credit agreement first and then moved the credits a moment later without looking again. A change made in between — a member suspended or withdrawing consent, a partnership suspended — did not stop the transfer. It now re-checks all of these at the moment the credits move, exactly as the newer partner connection already does, and never credits an account that is no longer active (F-396).
The automatic check that stops published passwords from being built into setup scripts now checks every part of a script, not just the file as a whole. This check exists because a password published in the code once ended up on real accounts. It accepted a script as safe if a safety guard appeared anywhere in the file, so a script could guard one step and create an administrator account from a published password in another step that could be run on its own. Each step that creates a password must now be protected itself, or only be reachable through a protected step (F-494).
Three exceptions in the automatic security scan of the software the platform depends on now only cover the versions they were judged safe for. The scan stops a release if a dependency has a serious known weakness, and keeps a list of reviewed exceptions. Three of those exceptions applied to every version of their package, so if a later update had brought back a vulnerable version, the scan would have stayed silent. One now applies only to the fixed version and later, one only to the two versions actually reviewed, and one — for a package no longer used anywhere — has been removed, so it will be reported if it ever returns. A new automatic test keeps them this way (F-501).
Pages built with the page designer can no longer draw over the rest of the site. Community administrators can design public pages, and the designer already stripped the usual ways of making something cover the whole screen — but one more way still worked, and the app's own built-in styles offered another. Instead of chasing each one, a designed page now sits inside a frame that nothing inside it can draw outside of, so a page can never cover the site's header, menus or footer. Screen-size and feature conditions in a page's styles are also now checked before use, like the rest of its styles already were. Normal page designs look the same (F-499, F-500).
The security-scanning guide now shows exceptions limited to the versions that were actually checked. Its own example of how to write a dependency-scan exception used the "every version, for ever" form that F-501 found to be unsafe; the example and a short explanation now show the bounded form instead (F-501).
Caring-support hours can no longer pay a supporter whose account is suspended or banned. When a coordinator logs hours for a support relationship they can be approved and paid straight away. The platform already refused this if the organisation behind it was suspended, but it never checked the supporter, so a suspended or banned supporter was still paid. Logging such hours is now refused with a clear message, and nothing is recorded as approved. Supporters in good standing are paid exactly as before (F-505).
Settling a member's time-credit estate now pays only a beneficiary who can receive it, and can no longer make hours vanish. When an estate was settled in favour of a named person, the platform never checked that person's account, so a suspended or banned beneficiary was paid. Worse, if the beneficiary had since moved to another community, the hours were taken from the estate, paid to nobody, and the estate was still marked as settled. Settlement now checks the beneficiary first; if they cannot receive the hours, it is refused and the estate keeps them. Normal settlements are unchanged (F-506).
An administrator can no longer see or handle a volunteering safeguarding incident that is about them. The platform's other safeguarding records already keep people out of records about themselves, but the volunteering incident records did not: an administrator who was the subject of one could read the investigators' notes, rewrite them, assign the case to themselves and close it for good. Such incidents are now left out of their lists and treated as not found, they cannot be changed by that person, and nobody can assign an incident to the person it is about. Every other administrator handles these incidents as before (F-507).
Staff alerts now reach every administrator, however their access was granted. Administrators are now normally set up with an administrator setting rather than an "administrator" job title, but five kinds of alert still looked only at the job title: reported content, exchanges waiting for approval and disputes, new groups, new members waiting to be approved, and completed identity checks. In a community whose administrators were all set up the newer way, those alerts reached nobody. They now use the platform's single shared definition of who should be told, so administrators and brokers receive them as intended, and ordinary members still do not (F-508).
A member's personal-data download no longer includes the safeguarding officer's private notes about their vetting. Everywhere else, the platform deliberately shows a member only the outcome of their safeguarding clearance — the decision and its dates — and keeps the officer's private notes, internal summary, the reason given and who decided out of view. The personal-data download did not follow that rule and included all of them. It now matches: members still receive their own clearance decision, its dates and which clearance it is (F-509).
Someone signed in to a community that has since been renamed is no longer shut out of every page. When a community's short name changes, apps and the accessible website can go on sending the old one for a while. Since the refusal above and its sister fix, sending a name the platform no longer recognises refused the request outright — even for a member who was signed in and whose own sign-in clearly said which community they belong to. Those members are now simply served their own community. Nothing is relaxed for a visitor who is not signed in: an unrecognised community name from someone with no account is refused exactly as before, and a signed-in member naming a different real community is still refused (F-481).
The "Maximum transfer" limit an administrator sets now actually applies. The System Config page offers a cap on the largest number of hours one member can send another in a single transfer. It saved, it showed the saved value back, and it did nothing: the platform read a different setting, so the real ceiling stayed at the built-in 1,000 hours. An administrator who capped transfers at five hours would still have seen a member move two hundred in one go. The page now writes the setting the platform reads. Transfers below the cap are unaffected, and a community that has set no cap keeps the same built-in ceiling it had before (F-492).
Switching a part of the platform off for a whole community is now recorded. Administrators can switch off messages, the wallet, listings and five other areas for everyone in their community. Neither of the two pages that can do it kept any record of who did it or when — the matching record for the other kind of switch on the very same page was added earlier and did not cover these. Both pages now write the same entry, naming the administrator, the area and whether it was switched on or off. What administrators are allowed to do has not changed (F-493).
Reordering a community's safeguarding questions is now recorded. Staff can change the order the safeguarding questions appear in. Creating, editing, deleting and importing those questions have all recorded who did it since an earlier fix; reordering was missed and left no trace at all. It now leaves the same kind of entry. Reordering only changes what order things are shown in and cannot add or remove any protection — this is about being able to answer "who changed this, and when" (F-495).
A transfer from an outside organisation is no longer reported back to them as having failed when it actually went through. Time credits arriving from another platform are added to the member's balance first, and the member is then emailed about it. If that email could not be sent, the platform told the sending organisation the whole request had failed — using the exact answer it reserves for "try this again". A sender that did try again, with a new reference, would have paid the member twice. One everyday reason the email could not be sent was the member's own choice to receive no federation email, which was then recorded as an email system fault rather than as the member's decision. The sender is now told the transfer was accepted, with the notification problem reported alongside it. The failure is still recorded on the transfer and still shows on the email-delivery page administrators use, and a repeat of the same transfer still re-sends the missed email without paying anyone twice. Connections to outside platforms are switched off by default and none is connected today (F-422).
When an outside organisation takes back time credits it sent you, your wallet now shows it. Hours delivered from another platform were listed in the wallet while they stood, but the moment the sender cancelled them the entry was hidden — so the balance dropped by five hours and there was nothing anywhere on the member's history to explain where they had gone. No local record of either the arrival or the removal was ever kept. A cancelled delivery now stays on the wallet and reads as hours leaving the account, matching the drop. A delivery the platform could not take back, because the hours had already been spent, also stays visible and still reads as hours received, because the member kept them. Ordinary deliveries are shown exactly as before. Connections to outside platforms are switched off by default and none is connected today (F-491, first half; the second half — how much a cancellation is allowed to take when the hours have been spent — is unfinished and needs a decision from the platform owner).
An outside organisation can no longer leave a review on a member who never agreed to federated reviews. A member chooses whether to take part in federation at all, and separately whether reviews from other communities may appear on their profile. The older partner interface has always checked both before accepting a review from outside; the webhook route checked neither, so a partner could put a review on the profile of someone who had never opted in, and it counted towards their public rating. The webhook now asks the same two questions. A member who has opted in is reviewed as before, and the rule added earlier — that a review must describe a real exchange with that member — still applies on top. Note what is still not possible: a member cannot refuse one particular reviewer on another platform, because the platform holds no reliable identity for a person it does not host. The choice is all-or-nothing for the member, and suspending the partner for an administrator. Connections to outside platforms are switched off by default and none is connected today (F-406, remaining half).
An outside organisation can no longer pay time credits into the wallet of a member who turned federation off. Every member has two switches: whether they take part in federation at all, and whether they accept time credits from other communities. Both are off until the member turns them on. Every other way an outside platform can send time credits reads those switches first and refuses — the webhook route, the one most likely to be used in practice, never looked at them, so credits from an outside organisation landed in the wallet of someone who had refused federation, and a community using those switches as a safeguarding control did not have one. The webhook now asks the same question the other routes ask. A member who has opted in is paid exactly as before, and a repeat of a transfer that was already accepted still gets the same "already recorded" answer rather than a false failure. Connections to outside platforms are switched off by default and none is connected today (F-484).
A copy of your data no longer includes the staff notes about a safeguarding concern you raised. When someone asks for a copy of everything the platform holds about them, the file they got back included, for any safeguarding concern they had reported, what staff had decided to do about it and the notes they wrote when closing it. Those notes are about the person the concern was about, not about the person who raised it, and the website itself has always kept them hidden from the reporter. The download now shows exactly what the website shows: your own account of what you reported, when, how serious you said it was and what state it is in. Staff looking into a concern still see their full record, and the rest of the data copy is unchanged (F-488).
A refused safeguarding-training decision now stays on the community's record after the member deletes their account. If staff turned down someone's safeguarding training certificate and that person then deleted their account, the refusal disappeared with them — who refused it, when, and why. The community was left with no record that it had ever happened. The same thing was fixed earlier for refused background checks, for the same reason, and this closes the matching gap. The refusal itself is kept; everything the member submitted alongside it — the course name, the provider, the certificate reference and any document link — is still erased, and training the member submitted that nobody refused is still deleted in full (F-489).
The accessible version of the site now works out who is an administrator the same way the rest of the platform does. It had a staff-only door that decided by reading a single text label on the account. Most administrator ranks are not stored in that label — they are stored as separate yes/no settings — and the information it was reading did not include the label at all, so the door would have turned away everyone, including genuine administrators. Nothing was affected in practice: the door is not attached to any page today. It now asks the platform for the account's actual rank, admits community, network and platform administrators, and still turns away brokers, coordinators and ordinary members (F-498).
Switching off a safeguarding requirement across listings is now recorded. A maintenance command clears the old "background check required" marker from listings, and with one option it does that for every community at once. It left no record at all — nobody could afterwards be told who ran it, when, which communities it covered, or how many listings lost the requirement. The command now asks whoever runs it to name themselves, refuses to run if the record cannot be written, and writes two entries: one before it changes anything, so a run that stops half way still leaves proof it happened, and one afterwards with the numbers. This matches the change made to the related clean-up command, so both show up in the same place. The checks that already stopped an accidental run are unchanged (F-427).
A safeguarding clearance brought over from the old records now says which check it is for. Communities can import historic background-check decisions into the new safeguarding system. Those imported clearances were stored without recording the type of check behind them, so a broker looking at the vetting list saw a confirmed clearance with nothing beside it saying what had actually been checked. Every clearance a member of staff records by hand has always carried that detail. Imported ones now carry it too, taken from the community's own configured checking scheme, and the import refuses to create a clearance it cannot name rather than creating a blank one (F-426).
A first-aid certificate is no longer relabelled as a criminal-record check when deleting its file fails. If a member deleted a volunteering credential and the stored file could not be removed, the platform kept the record as a marker so the leftover file is not forgotten about — but that marker alone made the record show up in the member's own list as retired criminal-record evidence, with the wrong name against it. A first-aid or food-hygiene certificate is not a criminal-record check, whatever went wrong with the file. Those records now keep their own name and appear in the "needs review" group instead, so the unfinished clean-up is still visible. The file itself is still withheld, because the member asked for it to be deleted, and genuine criminal-record records are still handled exactly as before (F-425).
A broker who has been reviewed can no longer remove other members' comments on that review. Brokers help moderate comments, and the only thing stopping them clearing up criticism of themselves was a rule about comments they had written. A review is written by one member about another, so a broker the review was about could delete or hide every comment other members had left on it — and a hidden comment disappears for everyone. The equivalent buttons on the community feed were corrected earlier; the comment moderation screen was missed, on both the delete and the hide. Both now refuse a broker who is the subject of the review. Moderating comments on anyone else's review, and everything administrators can do, is unchanged (F-420).
A broker can no longer cancel an administrator's safeguarding clearance. Brokers are trusted to record and withdraw the safeguarding checks of ordinary members, and the only thing stopping them was a rule that said they could not do it to themselves. Nothing stopped a broker doing it to an administrator. In a community that only lets vetted people contact a safeguarded member, a broker could therefore take an administrator's clearance away and cut them off from the very members that protection exists for, with the administrator recorded as having been withdrawn by the broker. A broker may now only record or withdraw a clearance for someone they are senior to, which is the same rule already used when staff adjust another person's time balance or suspend an account. Administrators are unaffected and can still do this for each other, brokers can still do it for ordinary members, and the existing refusal to decide your own clearance is unchanged (F-421).
Approving volunteered hours no longer pays time credits into a suspended or banned account. Approving hours creates brand-new time credits — nobody's wallet is reduced to pay for them — so the credits were new money in the community, and they were landing on accounts an administrator had deliberately barred. All four places that approve hours were affected: the organisation's own approval screen, the automatic approval that some communities use, the community administrator's approval page, and the caring-community review decision. Each of them already refused to pay out on behalf of a suspended organisation but none of them looked at the member. All four now apply the same platform-wide rule every other way of paying a member already applies. The hours stay waiting rather than being marked approved and unpaid, so they can be approved normally the moment the suspension is lifted — nothing is lost. Approving an active member's hours, and declining anyone's, are unchanged (F-475).
Settling a group exchange now checks everyone's account again, and can no longer make time credits disappear. A group exchange checked that a member's account was in good standing when they joined and never looked again, even though settlement can happen days later. Two things went wrong because of that. A member suspended or barred in the meantime was still paid. And if a member had been moved to a different community in the meantime, the hours were taken off the people receiving the service but never reached the person who provided it — the exchange was marked finished, a wallet record was written for a payment that had not happened, and four hours simply vanished from the community's total. Settlement now re-reads every participant's account at the moment the hours move, and refuses outright if the credit cannot land, so either everything moves or nothing does. A refused settlement changes no balance and leaves the exchange ready to finish as soon as the account is in good standing again. Ordinary settlements are unchanged (F-476).
The caring-community review decision no longer pays out on behalf of an organisation that is suspended or was never approved. Three other places that approve volunteered hours already refused to create time credits for an organisation a community had suspended or had not yet approved. This fourth one — the caring-community coordinator's review decision — never looked at the organisation at all. Approving there created brand-new time credits for the member and pushed the organisation's own balance into the red, with a payment record to match, for an organisation with no standing to pay anyone. It now applies the same rule as the other three, both before the decision is made and again at the moment the credits would move. Declining hours is still allowed while an organisation is suspended, so a coordinator can clear the queue, and an approved organisation is unaffected (F-478).
Taking back a network-administrator privilege now really takes it back, whichever button granted it. Two of the four ways to appoint a network administrator — "move and promote", and a bulk move with the promote option ticked — recorded the appointment in two places at once, while removing it only cleared one of them. The account kept full administrator powers in a community that never appointed it, including reading the member list with everyone's email addresses, while the panel, the button and the audit entry all said the privilege had been removed. The other two ways were corrected earlier; these two now match them. Appointing a network administrator still works exactly as before (F-464).
A data-protection request from a member now always reaches somebody. When a member asks to see, move or erase their data, the platform alerts the community's administrators. The list of who to alert was built the wrong way, so in a community whose administrators were appointed the normal way it could reach nobody at all — no alert on the bell, no push, no email — and the request would only surface weeks later through a nightly overdue check, close to the legal deadline. The same fault silently skipped the alerts for new listings, new community events, new volunteering opportunities, and the message telling a community that another community had accepted its partnership request — so those went unseen too. Eight places that pick administrators were corrected to match the one that was already right. Nobody who should not be alerted now is (F-490).
A safeguarding case can now be given to a network administrator, and they appear on the administrator list. Someone who runs a community and the communities under it could not be named as the person responsible for a safeguarding incident or for a volunteering organisation. The request was simply refused and the incident was left with nobody responsible for it. The same account was also missing from the administrator list the member-records screen shows, so no work could be routed to them there. Ordinary members still cannot be given a safeguarding case, and the administrator list still does not include brokers (F-467).
An administrator can no longer delete the concern note recorded about themselves. Staff can record a confidential concern about a member or about another member of staff. A recent fix stopped the person a concern is about from reading it — but the delete button was never checked, so they could still destroy it without ever seeing it. Deleting a concern note about yourself, or about someone at or above your own level, is now refused, and the note survives. Anyone senior enough to see the note can still remove it, and ordinary notes are unaffected (F-462).
Network administrators can now use three controls they were being refused. The platform records an administrator's authority in two ways, and three checks only looked at one of them. A network administrator — someone who runs a community and the communities under it — could not send or clear a community emergency alert, could not open a job applicant's CV, and could not read the view and like figures for a post. The emergency-alert case was the worst: the account was let through the admin door and then turned away by the page itself. All three now use the platform's single, canonical test for who is an administrator. Ordinary members and brokers are still refused all three (F-466).
An outside platform can no longer create unlimited time credit in a community. The older partner interface capped a single transfer at 100 hours and then only checked that a credit agreement existed with the other community — it never read the monthly limit written on that agreement. Because the sender is on the other platform there is nothing to take the hours out of locally, so the credit was made from nothing: against a published limit of 10 hours a month, ten calls put 1,000 hours into the community. The month's running total is now checked against the agreement before any credit is added, and where the agreement states no figure, 200 hours a month applies. Cancelling a transfer does not hand the budget back. The same transfer also used to be recorded as if a named local member had sent it, so that member's wallet showed hours they had never spent; a transfer from an outside platform is now recorded with no local sender at all. Connections to outside platforms are switched off by default and none is connected today (F-440).
An outside platform can no longer publish reviews on a member it has had no exchange with. The older partner interface accepted a review with no checks whatsoever — no exchange behind it, no limit on how many, no check that the two people named had ever dealt with each other — and wrote it straight in as approved and publicly counted, in the name of a real member of the community who had not written it. Five identical one-star reviews could be planted on any member, and they moved that member's public rating. A review arriving this way must now name a real exchange in that community that both named people took part in, and one exchange can earn only one review. This brings the partner route in line with the rule the platform's own review screen has always applied. Connections to outside platforms are switched off by default and none is connected today (F-441).
An outside platform can no longer read private messages it was not part of. The message-reading route for partner platforms filtered only on the community the credential belonged to — which, for an outside platform, is the whole community — so any partner credential could read the full subject and body of every message any other partner had delivered, a hundred at a time. It was reading more than the member it belonged to can: a member's own inbox does not show these messages at all. A partner may now read only the messages it delivered itself. A partner reading back its own correspondence is unaffected (F-446).
The older partner interface now honours the two privacy switches a member sets on their federation profile. "Share my location" and "Share my skills" are both off unless a member turns them on, and every other part of the platform respected that — but the partner routes returned a member's home address and skills regardless, for the whole membership, 100 at a time. All three affected routes (the member list, a member's profile, and a listing's owner details) now withhold both fields when the member has them switched off, and a member can no longer be found by searching on a field they have hidden. A member who chooses to share still shares (F-447).
An outside platform can no longer send a message in the name of a real member of your community. The older partner interface deliberately skips checking the sender, because on a genuine partner transfer the sender is on the other platform — but it then stored whichever local member the partner named as the author. The message landed in the member's inbox, was recorded twice under that name, and the platform emailed it out with that member's real name in the subject line, the preview and the "From" panel, carrying whatever text the partner had written. A message arriving from an outside platform is now stored with no local author; the sender's name as the partner supplied it is what identifies them, which is what the alert and the federation inbox already showed. The same check on the internal route, which does refuse this, is unchanged (F-448).
One partner platform can no longer block another's transfers. Transfers from partner platforms are de-duplicated by a reference the sending platform chooses, and every partner of a community was sharing one pool of those references — so the first platform to use a given reference owned it permanently, and a second platform's genuine transfer was turned away as a duplicate before any of the money checks ran. The member it was meant for was never credited, and because "duplicate" is the one answer a well-behaved sender does not retry, the transfer simply vanished. Each partner platform now has its own pool. A platform repeating its own reference is still correctly refused and the member is still credited exactly once (F-450).
A key issued for an outside organisation is now recorded as belonging to one. When a community creates a partner key it can link that key to the outside organisation it is for. The link was saved, but the key was still filed as if it belonged to another community on this platform — and the partner routes read that to decide what to serve. The result was that a key made for an outside organisation was answered with other communities' members instead of the issuing community's own, and several protections written specially for outside organisations never ran on any key the platform could actually make. A key linked to an outside organisation is now recorded as one. Keys made without that link are unchanged, and the check that the organisation belongs to your own community still applies. Note this fixes what new keys are recorded as; keys already in the database are unchanged and need a decision from the platform owner. Connections to outside platforms are switched off by default and none is connected today (F-463).
A partner key can no longer be handed your own members' private federated correspondence. The message-reading route for partner platforms has two paths. One was corrected earlier so an outside organisation reads only the messages it delivered itself. The other path — the one every key the platform could actually make was taking — simply returned every federated message involving anyone in the community that issued the key, subject and full text, including their exchanges with a third community. It was showing more than the members themselves can see: these messages do not appear in a member's own inbox. A key that names no outside organisation is party to nothing, so it now reads nothing. A key linked to an outside organisation still reads back the messages that organisation delivered (F-468).
The partner member directory no longer publishes barred, suspended and refused accounts. The rule excluding accounts that are not in good standing sat on one of the two paths only, and it was not the path any real key took. So a partner key could list — 100 at a time, and look up by name — the profiles of people a community had banned or suspended, people whose registration an administrator had refused, and people who never confirmed their email address. The same gap meant such an account could still be sent a message and still be credited with time credits through the partner route. All four places now apply the same rule the rest of the platform applies. Active members are listed, reachable and payable exactly as before (F-483).
A member's hidden skills can no longer be confirmed by guessing at them. A member can switch off sharing their skills with partner communities, and the partner directory correctly leaves the field blank. But the search box still matched against it: searching for the exact hidden wording brought the member back and the result count said "1", confirming the guess without ever showing the value. The search box now ignores a skills entry the member has switched off, which is what the platform's own federated search already did and what the matching filters were corrected to do earlier. A member who chooses to share is still found by their skills and still has them shown (F-465).
Taking back a network-administrator or platform-administrator grant now really takes it back. Granting either of those did two things at once: it set the permission, and it also changed the person's listed role to "administrator". Taking the grant back only undid the first, so an ordinary member who had been granted and then un-granted was left as a full community administrator — able to manage every member — while the panel, the button and the audit entry all said the permission had been removed. Granting now sets the permission alone, which is all it ever needed to do and is exactly what the super-admin panel's own version of the same grant already did, so granting and un-granting now cancel each other out exactly. Someone who was already an administrator before the grant stays one, as they should (F-431).
Editing a network administrator's phone number no longer takes away their permissions. A recent fix made the two member-edit screens switch off administrator permissions whenever the role was set to something ordinary. That fired too widely: for an account listed as an ordinary member who had been given network-administrator permission — the normal result of granting it from the super-admin panel — every save switched the permission off, including a save that only changed a phone number. The community could lose its only network administrator through routine admin work, silently, and the account could not restore itself. Permissions are now only switched off when the operator actually changes the role to a lower one. Genuinely demoting someone still removes every permission, on both screens (F-452).
Approving people in bulk no longer demotes them. The bulk approval button set everyone it approved to "member", whatever they already were, so approving a pending broker or administrator quietly stripped their role. On an account that also held a separate administrator permission, the effect was worse: the record was written down to "member" while the permission stayed on, so the person still had powers the screen said they had lost. Bulk approval now leaves the role alone, exactly as approving one person at a time already did (F-429).
Moving someone to another community no longer carries their administrator powers with them. Moving an account only changed which community it belonged to. An administrator of community A, moved into community B, arrived administering B — appointed by nobody there — and a network administrator moved between two networks kept that permission, which silently re-pointed their reach at the new network's communities. Community-level authority (the administrator role and both community permissions) is now cleared on every move, including the bulk move, and the record of the move now says what was removed instead of only where the person went. Platform-wide access is left alone, because it is not a permission over the community the person happens to sit in. The separate "move and promote" action still promotes, as intended (F-453).
A member of staff can no longer read the monitoring record kept about their own messages. Messages are copied for review when someone is being watched, and the broker screens let the person a copy was about open it — so they could read that they were being watched and why, how serious it had been judged, the reviewing administrator's private note and that administrator's name, and the decision later filed about them. Those screens now refuse when the record is about the person asking, and the review queue and the archive list leave their own rows out, so the record cannot be reached by paging through a list instead. The same refusal now covers the exchange detail screen, which handed a member of staff who was one of the two people in the exchange the other member's email address and the internal risk note an administrator had written about their own listing. Reading records about other members is completely unchanged (F-436).
A member of staff can no longer read a complaint made about them, or about something they posted. The moderation screens showed the name of the member who complained, their picture, and the complaint in their own words — to the very person being complained about, who as a broker holds moderation powers over them. The complaint screen now refuses, and the moderation queue leaves those complaints out, so one cannot be found by paging through the list instead. This brings the moderation screens in line with what the platform already does elsewhere: a seller told their marketplace listing has been reported is never shown who reported it or what they wrote. Closing or dismissing such a complaint was already refused. Administrators are unaffected, and staff reading complaints about other members see exactly what they did before (F-454).
Nobody can now read the private safeguarding notes written about their own vetting record. Brokers are allowed into the vetting screens as part of their job, and those screens accepted any member's identifier — including the reader's own. The person the record was about could therefore read the safeguarding officer's private note and the private summary of what they had been cleared for (both of which are deliberately stored scrambled precisely because they are sensitive), the internal code giving the reason a clearance had been withdrawn, and the name of the officer who decided their case. Those four details are now left out when the record is about the person reading it. They can still see their own clearance status and dates, which is what the member's own "my vetting status" page has always shown. Opening another member's record is completely unchanged (F-437).
The private notes staff keep about a person's conduct are no longer shown to that person, whatever their role. These "concern" notes were already hidden from brokers, but the rule stopped short of administrators — so a community administrator was served the full text of a concern that a senior platform administrator had recorded about them, along with the name of the colleague who wrote it. The rule now applies at every level: you are never shown a concern note about yourself or about someone at your own level, while anyone senior to the person the note is about still sees it in full. It also now covers three more places the same notes were being read: the activity timeline, the spreadsheet download, and the response returned after editing a note. Ordinary CRM work is unaffected — staff still read concern notes about the members they support (F-457).
The safeguarding review queue no longer shows a member of staff the monitoring record kept about their own messages. Messages are copied for review when someone is being watched, and the queue listed every copy in the community with no exception for the person reading it — so the subject of the monitoring could see, in their own work screen, that they were being watched, the reason recorded, how serious it had been judged, the reviewer's private note and that reviewer's name. Their own copies are now left out of the queue, and out of its count, so they cannot be reached by turning to another page. Copies about other members are unchanged, and colleagues still see the record about the person in question — it is hidden only from that person (F-455).
Nobody can now open the safeguarding history kept about themselves. The per-member safeguarding history screen, and its spreadsheet download, accepted any member's identifier including the reader's own. Because every opening of that screen is itself recorded, and the record names who opened it, an administrator could read the list of colleagues who had looked at their own safeguarding history and when — and see that their own messages were being copied for review, and why. Oversight stops working when the person being overseen can watch it happening. Both the screen and the download now refuse, and refusing leaves no entry in the history. Reading another member's history, on either route, is unchanged, and a supported member can still see the notes recorded on their own support arrangement (F-456).
Changing a safeguarding question to "information only" can no longer quietly cancel the protection members chose. Each safeguarding question a community asks has a type: a tick box, a drop-down, or a plain information note. Changing a live tick box into an information note left every member's answer on record, left the question switched on and left its protections in place — but the platform stopped counting those answers. A member who had said "I will only deal with vetted people" became contactable by anyone, was not told, and stopped seeing their own choice in their settings. Brokers could do this as well as administrators. The platform already refused to switch such a question off or to strip its protections; it now refuses a change of type as well, whenever that change would stop any member's live answer counting. Changing the type when nobody's answer would stop counting, or when nobody has a live answer, still works exactly as before (F-433).
Changes to a community's safeguarding questions now record who made them. Creating, editing or removing a safeguarding question, and applying a country's safeguarding preset, were each recorded with the time, the network address and the browser used — but with no account against them. "Who changed this safeguarding setting?" could not be answered from the platform's own records, which is also what would have made the problem above hard to trace after the event. All four now name the member of staff who acted. Members' own actions were already recorded correctly and are unchanged (F-434).
Dependencies: the markdown renderer (
league/commonmark) is updated to 2.10.3, which closes a way of slipping disallowed raw HTML past its sanitiser when a disallowed tag name ends the literal (GHSA-97jj-33gv-5xf9). The platform renders member-written text through this library, so the fix matters beyond the dependency graph. Lockfile-only; no code change.A safeguarding alert raised when the deadline sweep runs in a community with the module switched off now opens a single grouped report rather than two, and a per-community failure is reported the same way. A partner review is also now checked against the safeguarding rules before anything else, so if those rules cannot be read the partner is told to try again rather than being given a refusal that looks final (follow-ups to F-406 and F-408).
Demoting an administrator now actually removes their powers. Changing someone's role back to "member" in the admin panel recorded the demotion and showed them as an ordinary member, but left their administrator permissions switched on underneath — so they could still view any member's account as that member, and at platform level still see every community on the installation. Both admin screens are fixed, including the one the super-admin panel actually uses. Removing "god" access still has no control anywhere on the platform and is deliberately unchanged (F-399, F-400).
A member of staff who is also someone's supporter can no longer confirm their own request on that person's behalf. Where a supporter prepares an action and the supported member approves it, a staff account that was itself the supporter could record that approval as having been given "offline" — moving the supported member's time credits with no act of consent by them anywhere in the record. Staff who are not involved can still confirm by phone or in person for someone who cannot use the platform themselves, and members can still confirm their own (F-405).
A community administrator can no longer switch off the automatic safeguarding deadline alert for concerns that are already open. Turning off the caring-community module, or marking the community inactive, used to stop the alert immediately and silently: a concern stayed open, sat past its review deadline, and nobody was told. The check is now driven by the overdue concerns themselves, so neither switch can reach it, and if it ever does escalate a concern in a community with the module switched off, that is raised as an alert naming the community. Changing any community feature is also now recorded in the administrator audit log, which it previously was not (F-408).
Destroying legacy vetting evidence now leaves a permanent record. The maintenance command that deletes old criminal-record-check files and clears the details from the records wrote nothing anywhere — there was no way afterwards to say who ran it, when, for which community, under whose data-protection approval, or how much was destroyed, and the approval reference the command insisted on was thrown away. Every run now writes two entries to the data-protection audit log, one before anything is touched and one with the results, both naming the operator and the approval reference. The operator must now name themselves, and if the record cannot be written the command refuses to destroy anything. This command is run by hand on the server and is not reachable from the website (F-409).
The one-off import of old vetting decisions no longer grants a safeguarding clearance to someone whose later check did not pass. It looked only at the old approved record and never at the rest of that member's history, so a member whose most recent criminal-record check had been refused or withdrawn was given the platform's confirmation that they were cleared to contact members who had asked for safeguarding protection. Any contradicting record now sends the member to the broker review queue instead, which grants nothing — the route the import was always meant to use for unclear cases. This import is run by hand on the server and is not reachable from the website (F-410).
A partner platform in another community can no longer leave reviews on a member it has had no exchange with. A review arriving from a partner must now name a completed exchange that partner recorded with that member, one exchange can earn only one review, and a partner is limited to five reviews of the same member a day. Previously a partner could publish any number of approved one-star reviews, with its own wording, on any member of the community, and they counted towards that member's public rating. Connections to outside platforms are switched off by default and none is connected today (F-406).
Time credit arriving from an outside platform is now limited to what the community has agreed to accept. Each transfer was already capped at 24 hours, but nothing limited the total, so the same request repeated under a new reference could create unlimited credit. A community must now have an active credit agreement, and the month's total is checked against it before any credit is added — the same rule the other three partner protocols already follow. Where the agreement states no figure, 200 hours a month applies. Connections to outside platforms are switched off by default and none is connected today (F-407).
An outside platform can no longer hand itself back the monthly credit limit it has just used up. The limit above counted only transfers still marked "completed" — but the same outside platform decides when a transfer stops being completed. It can cancel one at any time, and if the member has already spent the credit nothing is taken back: the transfer is simply marked as disputed and the member keeps the hours. Either way the hours stopped counting, so the month's allowance was free again and the same trick could be repeated without end. In testing, 72 hours were delivered into a community whose agreement allowed 30, with the running total reading zero at the finish. Every hour that arrives from outside now counts towards the month whatever happens to it afterwards, which is what the other partner routes already do. The trade-off is deliberate and worth stating: a partner that cancels a transfer in good faith does not get that part of its allowance back until the following month. Cancelling a transfer still takes the credit back from the member when they still hold it. Connections to outside platforms are switched off by default and none is connected today (F-428).
A broker can no longer change or delete their own insurance certificate. Two ways of doing that were missed when the rest of this was fixed: moving the expiry date out (or clearing it) made an out-of-date certificate count as current again, so the insurance requirement an administrator had placed on the broker's own listing stopped applying; deleting the record erased an administrator's refusal of that insurance. Administrators are unaffected, and a broker can still manage other members' certificates (F-401).
A member of staff can no longer close the safeguarding re-check about themselves. A community asks for these re-checks automatically when it changes its safeguarding policy, and until now the person the re-check was about could mark it handled — so nobody was prompted to look at their clearance again, and the record named them as the person who dealt with it. Confirming and withdrawing a clearance already refused this. Closing somebody else's re-check is unchanged (F-402).
A member of staff can no longer sign off the monitoring record of their own message. Messages are copied for review because someone is being watched; marking a copy reviewed, approving and filing it, or flagging it all took it out of the staff review queues and made it eligible for automatic deletion — and the person doing it could be the sender or the recipient. All three actions now refuse the people the message is between, at every level of staff. Reviewing other members' copies is unchanged (F-403).
The same refusal now applies on the safeguarding screen, which reaches the same message copies by a different route and so was not covered by the change above (F-404).
The demo-data seeding commands no longer carry a password in their source, and refuse to run outside a development environment. A command that creates sample accounts held its shared password as plain text in this repository and had no environment check at all — it would seed those accounts into whichever community it was pointed at. The password has been removed from every seeder, the value now comes from an environment variable or is generated for the run, and all of the seeding entry points now refuse to run unless the environment is explicitly one of
local,developmentortesting. The previous checks were written the other way round — they refused only an environment named exactlyproduction, so an unset or differently-named environment passed straight through. The affected accounts on the live platform had their credentials rotated at the same time; none of them had ever been signed into. A new blocking check,npm run check:seed-credentials, fails the build if a credential literal or an unguarded seeder reappears (F-398).Dependencies: the accessible frontend (
web-uk) now usesmorgan1.12.1, which escapes a double quote in logged request fields, so a visitor can no longer forge extra fields in the access log through their User-Agent or Referer header (GHSA-9f6g-j8ch-79g4, Dependabot #139). Developer-tool copies ofbrace-expansionin the root,react-frontendandweb-uklockfiles are updated to patched releases (GHSA-q2hr-2g5m-vwhr, Dependabot #145/#150/#151/#158–#160); none ships in a production image. Lockfile-only; no code change.A guest can no longer be added into an event seat that has already been offered to a member on the waiting list. An organiser also can no longer lower an event's capacity below the number of people already seated, guests included (F-397).
A cross-community time-credit transfer already in progress is now refused if the sender's community switches off cross-community transfers, or the sender opts out of cross-community sharing, before the credits move. The same re-check already applied to the receiving side (F-395).
If a member withdraws or lowers a carer's permission to spend their time credits while a payment by that carer is already in progress, the payment is now refused instead of completing (F-392).
Submitting the same caring-support visit twice at the same moment (for example a double-click) can no longer record and pay it twice (F-391).
A broker who has been reviewed can no longer hide or delete that review from the community feed. Deleting it that way also deleted other members' comments on the review; the review-moderation page already refused this (F-390).
A broker can no longer clear the compliance controls an administrator placed on the broker's own listing: they cannot record or verify their own insurance certificate, or change or remove the risk tag on their own listing. An insurance certificate recorded as already verified now also records who verified it (F-389).
A group marked private or secret is no longer sent to external partner platforms, and neither is who joins it. A group made private after being shared is withdrawn from them without its details. External partner connections are switched off by default and none is connected today (F-386).
An administrator of one community is no longer treated as an administrator of every other community when member profiles are shown. This could not be reached through the platform today, but would have exposed members' exact locations and surnames to other communities' administrators if a profile page were ever opened to signed-out visitors (F-394).
Story highlights now respect blocking. A member on either side of a block could still see the other's highlighted stories, although their ordinary stories were already hidden (F-393).
A member who has been blocked can no longer notify the person who blocked them by @mentioning them in a group they both belong to (F-387).
A member who has been blocked can no longer find out who blocked them by checking another member's block status. The block-status answer now only tells you whom you have blocked (F-388).
When a community approves volunteering hours automatically, an organisation that is suspended while a member's hours are being submitted can no longer pay them out. The same check already applied when hours are approved by hand (F-385).
Volunteering hours logged by a coordinator for a caring-support relationship can no longer be paid by an organisation that has been suspended, or has not yet been approved. This was the third way round the organisation freeze (F-384).
When a podcast creator deletes their own account, the complaints other members made about their episodes, and the moderators' decisions on them, are now kept. Previously the account deletion removed them along with the episodes (F-383).
A club organiser importing members from a spreadsheet can no longer create accounts that skip the community's identity check. Imported accounts now follow the same joining rule as accounts an administrator creates: where the community requires an identity check, the account waits for it (F-382).
Blocking a member of a partner community no longer reveals the name and photo of someone who has not chosen to be visible to other communities. Only members who have joined cross-community sharing, in a partnership that is active, can be blocked across communities, and the block list names them only while they stay visible (F-381).
A web address typed into the platform — an interview link, an evidence link, an order tracking link, a venue website — can no longer be written in a form that sends whoever clicks it to an unrelated website. One shared safety check covers all of these, and it had a gap (F-380).
A web address typed into a support report can no longer send an administrator to an outside website when they open it from the support console (F-358).
When a member deletes their own account, the community's safeguarding vetting decision about them, its history and any open review are now kept, in the same way safeguarding reports already were. Blocks that other members placed on them are also kept, so those members do not silently lose their protection. The account itself is still erased and the email address is still freed (F-339).
If a supported member revokes their carer while a payment by that carer is already in progress, the payment is now refused instead of completing (F-342).
A group marked private is no longer listed to partner communities. Its name, description, cover image and member count were previously published to them, which is the rule events have followed since an earlier fix (F-377).
A member who has opted out of cross-community sharing can no longer use any of the cross-community connection features — sending, accepting, rejecting or removing a connection, or checking its status (F-373).
Turning off cross-community sharing now ends the connections you already have with people in other communities, instead of leaving them live. Erasing your account does the same. Previously those connections stayed, the other community kept reading your name and photo, and turning the setting off also removed your ability to delete them (F-352).
Switching off "visible to other communities" now also stops people in other communities sending you a connection request, and hides your name and photo in their connection lists. It previously only affected search and profile pages (F-351).
Permanently purging a community now also deletes the private group chat messages, AI chat conversations and consent records — including the IP addresses in them — that it previously left behind for ever. The routine's description now states exactly what it does and does not remove, and says so in its report on every run (F-354).
Permanently purging a community is now recorded twice — once when it is ordered and once when it completes — and both entries name the administrator who ordered it. The record previously said the action was taken by "System". The administrator must also type the community's short name to confirm, which the server now checks rather than trusting the screen (F-353).
Deleting a job advert now also deletes the CVs applicants uploaded to it. They were previously left on the server with nothing pointing at them, so no later deletion or data-erasure request could find them (F-361).
A maintenance command that deletes accounts with dead email addresses can no longer delete them during a name-lookup outage — it now tells the difference between "this address does not exist" and "the lookup service is not answering", and leaves the second alone. It also now protects the platform's most senior administrator accounts, which it previously missed (F-375).
Removing the block that stops the platform emailing someone is now recorded, with who did it. If the block is there because the person asked not to be emailed, removing it is refused unless the administrator explicitly confirms they are overriding that request (F-374).
The platform's main framework (Laravel) and two supporting packages (Flysystem for file storage, phpseclib for encryption) are updated to versions that fix newly published weaknesses: a display issue on the debug error page (never switched on in production), mishandling of unusual characters in file paths, and a timing weakness in one encryption method.
Deleting a group no longer destroys the abuse reports filed about it, the moderator's decisions on those reports, or the group's own record of who did what — and the deletion itself is now recorded with the name of whoever ordered it. The files members uploaded to the group are now removed with it instead of being left on the server (F-337, F-360).
Clearing an application log from the admin screen is now recorded, with who did it, before the log is cleared — and is refused if that record cannot be written (F-355).
Removing a permission from a role in the admin screen now actually removes it. Previously, for any role created through that screen, the screen reported success and the permission stayed live (F-350).
A reviewer whose permission has expired can no longer auto-approve caring hours, which creates time credits (F-364).
Permission to view the cross-community comparison dashboard now only works in the community where it was granted, instead of in any community (F-371).
A grant that gives someone the "official notices" badge now stops working when it expires. Previously an expired grant kept working indefinitely (F-370).
A club-management permission recorded against no community can no longer be used to act inside a community. One of the two places that checks this had been fixed; its duplicate had not (F-362).
An ordinary member can no longer be given access to every club's dues register — including other members' email addresses — dues waivers, reminder emails sent from the community's address, or federation consent, through the roles screen (F-349).
A community administrator can no longer attach a permission the roles screen does not offer, and a permission recorded against no specific club now grants nothing rather than everything. Together these closed a route by which one save could let an ordinary member create accounts in every club (F-348).
When a partner community scrubs a cross-community transaction, the record of it is now kept with its text removed, rather than deleted outright — so there is still evidence the transaction happened, and who scrubbed it (F-366).
Asking a partner-federation address for "an account" without naming one no longer returns community-wide totals; it now asks which account (F-365).
The summaries a partner community can read about a member now cover only their cross-community activity, not their whole exchange history inside their own community (F-346).
A partner community can no longer add time credits to this community's balances without limit. Inbound credit now requires an active credit agreement and stops at a monthly ceiling. Partner federation is switched off and no partner is connected (F-345).
The currency named in a partner community's web address is now checked against the currency this community actually issues, instead of being ignored (F-357).
An outside partner community can no longer switch off a member's account unless that member has agreed to cross-community sharing (F-331).
An outside partner community can no longer read a member's balance and credit limit by using the edit address instead of the view address. The two now apply the same rule about who has agreed to be visible (F-330).
An outside partner community can no longer read transactions it is not party to. It previously received the whole community's exchange history, including private exchanges between members who never joined cross-community sharing. Partner federation is switched off and no partner is connected, so nothing was exposed (F-329).
A cross-community transfer now stops if the person receiving it withdraws from cross-community sharing, or their account is deactivated, while the transfer is in progress (F-376).
If a community link is suspended, or a partner community switches its cross-community transfers off, a transfer already in progress now stops instead of completing (F-344).
A community administrator can no longer approve hours for a suspended volunteering organisation from the admin page, which was creating time credits for an organisation the community had switched off. Declining those hours still works, so the queue can still be cleared (F-379).
A volunteering organisation that a community suspends can no longer create time credits through an approval that was already in progress, and neither can an approver whose rights were withdrawn at the same moment (F-343).
A community administrator who is the buyer or the seller on a marketplace order can no longer decide that order's dispute themselves (F-341).
Time credits can no longer move on an exchange that one of the two members cancelled while a broker was settling its dispute — a case the platform's own reversal tool could not undo afterwards (F-340).
Blocking is now enforced in one place rather than being added by hand to each feature, so a blocked member is refused on any path that goes through the platform's contact rules — including ones nobody has checked individually. Two deliberate exceptions: posts to a whole group are unchanged, and blocked members now stop appearing in each other's match suggestions and match emails (F-336).
A blocked volunteer can no longer send a shift swap request to the volunteer who blocked them (F-369).
A blocked member can no longer make an offer, or send the message attached to it, on a listing belonging to the person who blocked them (F-368).
The ranked-poll voting service now refuses a blocked pair in its own right, not only on the web address that reaches it (F-367).
Regional point transfers now respect blocking, so a blocked member can no longer write a message into the blocker's points history (F-335).
Goal buddy requests, acceptances and notes now respect blocking, so a blocked member can no longer use them to notify the person who blocked them (F-334).
A blocked member can no longer send an hours gift, or the message attached to it, to the person who blocked them (F-333).
A member who has been blocked can no longer send time credits, or a message attached to them, to the person who blocked them — through either a transfer or a donation (F-332).
The mobile app's third-party "brace-expansion" package is updated to a version that fixes a newly published weakness that could make it hang on certain input. It reaches phones with the next app release.
The website now hangs up on a request that stops sending part-way through, instead of holding the connection open indefinitely. Normal uploads, including large videos on a slow connection, are unaffected (F-359).
When a member accepts all of a community's policies in one go, the record of how they accepted now says so honestly. It was being filled in from the community's own number, which the database read as a position in a list — so community 1 recorded "registration", community 2 recorded "login prompt", and so on. The values looked reasonable, which is why this went unnoticed. Existing records are unchanged; new ones are correct (F-378).
The accessible site's HTML cleaner now has its own tests, so a change that weakened it would be caught (F-316).
The accessible site now has an automated check that stops any page from showing member text as raw HTML without cleaning it first (F-315).
A learner can now submit a course quiz at most 20 times an hour (F-313).
Achievement campaigns can now award at most 1,000 XP, the same limit as challenges (F-311).
Awarding a badge that doesn't exist is now refused instead of reporting success, badge counts only include badges actually given, and members get one badge notification in their own language instead of two (F-310).
On the accessible site, opening the two-step sign-in set-up page no longer replaces a code you may already have scanned; you now press "Set up" first (F-307).
The accessible site's automated check that page addresses are built safely now covers every part of every address (F-306).
In the mobile app, links on an event's agenda now go through the app's link safety check before they open (F-301).
In the mobile app, the picture viewer now shows only pictures from the web and can no longer be opened by a link from outside the app (F-300).
A printable course certificate is now cleaned of any hidden code before it opens (F-299).
Links that members or admins type in (event video links, order tracking, websites, report links, menu items and more) now go through one shared safety check before the website shows or opens them (F-298).
A job interview's video-call link must now be an ordinary web address, and interview location notes can no longer hold a script or local-file link (F-297).
A safeguarding report's evidence link must now be an ordinary web address; anything else is refused (F-296).
A paid community plan is now matched to the community using the platform's own payment records, not details carried in the payment message (F-289).
Corrected an internal note about how far the event check-in reward limit can be exceeded when many people check in at once. No behaviour changed (F-295).
Unused code that could have created volunteering credits without any checks has been removed (F-293).
A donation refund can no longer be started twice at the same time (F-292).
An administrator's grant of time credits can no longer be applied twice by pressing the button twice (F-294).
A marketplace payment, refund or payout can no longer be sent twice if the platform loses Stripe's reply. Every money movement is now recorded before Stripe is contacted, and before anything is sent again the platform checks with Stripe whether it already went through. Anything that can't be confirmed is flagged for staff, in its own error-tracker alert, instead of being retried blindly (F-283).
A skill category can no longer be placed under another community's category (F-312).
A broker or coordinator whose account still carries an old admin flag can no longer read a job vacancy's applicants (F-309).
A date of birth entered from a paper sign-up form is now saved on the member's account, so the adults-only check applies to these members like everyone else (F-320).
Accounts an administrator creates, including from a spreadsheet, are approved straight away but still have to pass the community's ID check if it requires one, unless the administrator confirms they checked the person's identity themselves, which is recorded. A spreadsheet can now only create ordinary members, not administrators or brokers (F-278).
Several admin screens (creating an account, sign-in provider settings, webhooks) and a partner connection now refuse values that are too long, instead of silently cutting them short and leaving a record that doesn't work (F-327).
Accounts created on an administrator's behalf are no longer made live by default: the code that creates them now has to say whether each account is approved (F-319).
"Clear all communities' cache" now clears every community. It used to clear only the first five, while telling the admin it had cleared them all (F-287).
The admin fraud-alert count now shows the open alerts on the Fraud Alerts screen. It was always zero before (F-318).
Members who switch notification emails off no longer get emails when someone likes, comments on, replies to or shares their content (F-317).
If a message's community has been removed before the safeguarding review check runs, the check is now skipped instead of running against another community's records (F-288).
Members can now block someone from a partner community. The block stops that person's messages, connection requests and credit transfers, and the emails and notifications they would send, in both directions, and removes any connection between the two. Blocked partner-community members appear in your blocked list, and their profile page has a Block button. Blocks made in the past that never took effect across communities now do (F-284).
Opening an unsubscribe link in an email no longer switches your emails off by itself: it now asks you to confirm with a button. Email security scanners that open links automatically were turning people's notifications off without them knowing (F-321).
Reviews of an employer now follow the employer's "who can see my profile" setting, as other reviews already do (F-286).
The public version page no longer shows the server's PHP version or the text of the latest change, which named the security fix it contained. It still shows the version identifier the deploy tools rely on (F-314).
Thumbnails can no longer be made from files in private upload folders, such as message attachments and voice messages, that the web server already refuses to serve (F-290).
Asking for your account to be deleted through the data-protection request form now needs your password and signs you out everywhere, the same as the other two ways of deleting an account (F-308).
Rejecting or cancelling a data-protection request now records who did it, when and why, and adds an entry to the audit log, as completing one already did (F-325).
A visitor filling in the sales enquiry form can no longer slip a second email address into the reply-to line of the email the sales team receives (F-323).
Two parts of the standard email layout, the statistic-card icon and the badge colour, are now checked before they go into an email. Nothing uses them unsafely today; this closes the door before anything does (F-322).
A record that does not say which community it belongs to is now refused by the database instead of being filed under the platform's main community (or under none). On 29 tables — including reviews, comments, likes, badges, group posts and memberships, connections, event replies, polls and volunteering — a missing community used to be filled in silently, which is how some TimeBank Ireland reviews, a group post, a volunteer application and badges ended up in the wrong community earlier this year. Records that name their community are unaffected (F-328, O-087).
Joining a community through its own sign-in provider (for example a council's staff login) now follows that community's joining rules. An invitation-only or closed community no longer admits anyone the provider knows, and a community that requires approval or an ID check now holds the new account for that step instead of letting the person straight in (F-270).
A broker can no longer approve or reactivate a member whose identity check is still outstanding or has failed; only an administrator can release those accounts. Brokers still approve ordinary sign-ups and lift ordinary suspensions (F-277).
"Report a problem" no longer records the part of the web address after
?or#, where sign-in and password-reset links carry their codes, so those codes can no longer end up in the staff support queue or in error reporting. The platform also strips them from reports sent by older versions of the app (F-281).A custom page made with the page builder can no longer contain a form that sends what visitors type to another website; a form on a community page can only send to that community's own site. Pages saved earlier with such a form are made safe when they are shown (F-282).
Text a member types when declining or cancelling an exchange, and members' names, can no longer put a working link or other hidden code into the email the other member receives; they now appear as plain text (F-273).
A community's email sender name or address can no longer be used to add hidden recipients, such as a secret Bcc, to the emails that community sends, including password resets. The email settings page now refuses such values with a clear message (F-280).
A blocked member can no longer peer-endorse the person who blocked them. Peer endorsements now also follow the safeguarding contact rules and the member's "who can see my profile" setting (F-271).
Employer reviews now follow the same rules as other reviews: a blocked member cannot leave one, the safeguarding contact rules apply, and a member can leave one review per employer instead of an unlimited number (F-272).
Blocking someone now also stops them rating your exchanges, marketplace orders or volunteering, commenting on your ideas, and reacting to your podcast episodes. None of these can email the person who blocked them any more (F-279).
A member who has been moved to a different community can block people again. Before, the screen said the block worked but nothing was stored, and the blocked person's messages still arrived (F-285).
Deleting a member's account now also takes them off the community's newsletter list and removes any newsletters still waiting to go to them. It no longer deletes the record that stops email reaching an address that bounced or opted out, so that protection survives the deletion (F-274).
Deleting a member's account now also removes their two-factor backup codes, which used to keep working, and their linked sign-in accounts, which held their real email address from the sign-in provider (F-275).
A data-protection request (such as a request to delete an account) can now only be set to one of its real statuses. Before, an unknown status was accepted and silently stored as blank, which took the request off the overdue alarm while the person's data was never deleted (F-324).
A legal document (such as terms of use) that has been published or agreed to by members can no longer be deleted, because deleting it wiped every member's record of having agreed. The admin screen now says, in the admin's own language, to switch the document off instead. Draft documents can still be deleted, and each deletion is now recorded in the audit log (F-276).
Switching a community's maintenance mode on or off now refreshes only that community's search-engine pages. It used to restart page rendering for every community on the platform (F-245).
The volunteering accessibility page now says truthfully that these notes are private and are not shared with organisations or coordinators, and a save that fails is reported as an error instead of "saved" (F-227).
"Add someone who can help" in linked-account settings now does what it says: the person you name is asked to help you, and nothing changes until they accept. It used to make you their helper instead (F-224).
The Caring Community "Verified" and "Coordinator" trust levels now require a real ID check. Email verification or a failed ID attempt no longer counts, and community settings can no longer remove the requirement (F-269).
Sending regional points now asks you to pick the recipient by name and confirm "Send … to …?" before anything is sent, and points can only go to active members of your community. Spending regional points in the marketplace is switched off until a checkout that honours the discount exists, so points can no longer be spent on a discount that never applied (F-225).
Every GitHub Actions step used by the build and release pipeline is now pinned to an exact, reviewed version, so a changed or hijacked tag upstream cannot alter what runs (F-008).
The Warmth Pass now says "Identity verified" only for members who have passed an ID check, no longer describes itself as proof a member is verified, and "Areas I help with" now lists help the member gives rather than help they receive (F-228).
A member's linked community sign-in now works only through the same sign-in system it was linked under. If a community admin later points the provider somewhere else, linked members have to link it again from their settings, so the change cannot be used to sign in as them (F-268).
Added an optional safeguard so that visitor addresses forwarded by Cloudflare are trusted only when the request comes through this platform's own Cloudflare account. It is off until the operator configures it, and changes nothing until then (F-248).
A job candidate's data export still includes the employer's notes about their application and interviews, but no longer identifies which staff member wrote the decision or proposed the interview (F-261).
A sign-in provider set up by a community admin can no longer sign someone into an existing account just because the email address matches. New members can still join through it, and anyone already linked keeps signing in. Providers approved by the platform operator are unchanged (F-244).
A community's donation and premium payments can now only go to the Stripe account created through that community's own Stripe setup on this platform. Another account entered by hand is refused, and one saved earlier is no longer used; payments fall back to the platform account until the community completes its own setup (F-263).
Brokers and coordinators no longer see staff concern notes written about themselves, about fellow brokers or about admins; they still see concern notes about ordinary members. Admins are unchanged (F-252).
Switching a module on or off from the System Config page is now recorded, like it already was from the other settings page. There are two admin pages that switch the same modules, and only one of them wrote a line in the admin record saying who had made the change and when. So an administrator could switch off the Caring Community module — the one that carries safeguarding — and nothing showed who had done it. That matters, because switching that module off hides every open safeguarding concern from the staff responsible for them. Both pages now write the same record, in the same form, so they cannot be told apart afterwards. Nothing about who is allowed to make the change has altered (F-460).
Switching Federation off on the System Config page now really switches it off. Federation is how a community shares things with other communities. Switching it off on that page only changed what members saw: the community looked un-federated, while it carried on sharing its listings, messages, transactions, member profile updates, groups, connections, reviews and volunteering opportunities outward, and carried on accepting other communities as partners. The switch on the other settings page had always done both halves; this one now does too, and switching Federation back on from the same page restores it properly. In practice the sharing that was continuing is between communities on this installation — sharing with outside platforms has been switched off since July and no outside platform is connected — and that is exactly the sharing an administrator would be switching off (F-461).
An outside platform's access key can no longer be swapped for a pass that skips the second check it was meant to require. A community can mark a partner platform's key as one that must sign every request it sends, using a second secret the partner keeps separately — so having the key alone is deliberately not enough to get in, and a key on its own is refused. But the same key could be handed to the sign-in endpoint and traded for an hour-long pass that needed no signature at all, carrying everything the key was allowed to do. A key that had leaked on its own was therefore as good as the full credential. That trade is now refused, and a pass already issued stops working the moment a community turns request signing on. Keys that do not require signing are unaffected and work exactly as before. Nothing had this setting switched on, so nothing was exposed by it in practice — the protection would simply have failed silently the first time it was used (F-449).
"Require member approval" and "require email verification" now actually do what the switch says. These two decide whether someone who signs up is held for a person to check, or is let straight in with the community's starting time credits. The settings page saved them under one name while the sign-up and sign-in checks read another, and most communities had a value stored under the name the checks read — so an administrator could turn member approval back on, see the page confirm it, reload and see it on, and the next stranger to sign up would still be let in and credited automatically. The same split made the switch inert in the other direction too: turning the requirement off did nothing. Both names are now written together every time either is saved, so the page and the platform can never disagree again. For communities where the two names were left saying different things in the past, the platform now takes the cautious reading and screens the new member; one save from the settings page sets it to whatever the community actually wants (F-458).
Finishing an exchange no longer pays time credits into a suspended or banned account. Every other way of moving credits to a member — a transfer, a donation, a grant from the community fund, a marketplace sale — already refuses to pay an account the community has suspended or banned. Finishing an exchange was the one way that did not, so a member an administrator had suspended for abuse carried on earning from exchanges, and the community's obligation to honour those hours carried on growing. It did not even need the suspended member to sign in: the other member confirming the hours had the same effect. Confirmation is now refused with a clear message, no credits move in either direction, and the exchange stays where it was — so the moment the suspension is lifted, either member can confirm it and it finishes normally (F-442).
The public list of categories no longer publishes every column the platform stores against them. The address that hands a website or app a community's list of categories — used by the listings, events, volunteering and search pages, and open to anyone without signing in — returned each category exactly as it is stored, whatever that happened to be. The categories table carries four columns that do not belong to it, evidently left behind by an old change applied to the wrong table, and one of them is named
reset_token. All four are empty on every community, so nothing has been given away; the problem is that the address published whatever the table held, so anything added to that table in future would go out to the internet without anyone changing any code. It now sends a named list of details only — the category's identifier, name, web-address name, kind, colour, parent, position and whether it is switched on — which is everything the website, the accessible site and the phone app actually read (F-438).The check that is meant to stop a password ever being published again can no longer be switched off by accident. Yesterday a shared password sat in this public repository for 153 days, and 22 accounts on the live platform accepted it. The automatic check added in response asked whether a piece of code mentioned the safety guard — not whether it actually used it. So a single unused line naming the guard, which does nothing at all when the code runs, was enough to make the check pass a seeding script that created a full platform-administrator account from a published password. The check also only looked in two folders, and not at the database update scripts, which are the ones that run against the live platform on every release. It now looks for the guard being genuinely used, treats a guard whose refusal is thrown away as no guard, and reads the database update scripts, the older update scripts and the start-up code as well. It still passes quietly on the honest code already in the repository, which was measured rather than assumed, and a new set of tests keeps both halves of that honest (F-430).
The phone app now checks every web address it hands to the phone, with no exceptions left. The app has one place that decides whether an address is safe to open — it refuses anything that could run code or pretend to be a file — and five screens were opening addresses themselves instead: the two marketplace payment screens, seller payment set-up, identity verification, and the "you must update" screen. Nothing unsafe could reach them (the payment addresses come from Stripe and the update address from the platform's own settings), so nothing was at risk; this closes the gap before it can become one, and each screen keeps the specific message it already showed when an address would not open (F-445).
Someone the community has formally refused can no longer be paid time credits. The platform keeps one list of account states that must never receive credits, and every way of paying a member is supposed to read it. The list was wrong in two ways: it named a state an account can never actually be in, so that entry protected nothing, and it left out "refused" — the state an account is put in when a community turns down someone's application to join. A refused applicant could therefore still be paid by a transfer, a donation, a grant from the community fund, finishing an exchange, a marketplace sale or a community delivery, and the credits were then stranded, because that account cannot sign in. Three of those six places had also written out their own copy of the list instead of reading the shared one, so the same two mistakes were repeated in each; all three now read the shared list, so it cannot drift again. Paying an ordinary member, and paying a newly registered member who has not yet confirmed their email address, are both unchanged (F-477).
Suspending a member now stops a payment that is already under way. A transfer and a donation both checked whether the recipient was allowed to receive credits, but they checked it a moment before the money actually moved and never looked again. If an administrator suspended or banned that account in the gap — a fraction of a second, but a real one, and we have reproduced it — the hours were still paid in. That is the opposite of what suspending someone is for, and the sender's hours were lost with it, because nobody can spend from a suspended account. Both now re-check the recipient at the moment the money moves, holding the account open so the answer cannot change underneath them, which is how the community fund and the marketplace delivery payout have always done it. Nothing is paid and nothing is taken from the sender when the recipient is refused. Ordinary transfers and donations between members in good standing are unchanged, including when something else about the recipient's account is edited at the same moment (F-411).
Blocking someone now stops a payment they have already started sending you. A transfer or a donation carries a note the sender writes, and that note is delivered to the recipient as an alert, a phone notification and an email — which is why the platform refuses one from someone you have blocked. That refusal was decided a moment before the credits moved and never looked at again, so a block made in the gap did not stop the delivery: the hours arrived and the sender's text arrived with them. We had suspected this and had not proved it; we have now reproduced it, on both the transfer and the donation. Both now re-check the block, and the community's safeguarding contact rules, at the moment the money moves. Blocking a different member, and ordinary transfers and donations between members who have not blocked each other, are unchanged (F-412).
A new member's welcome time credits can no longer be taken away by another member. Communities can give every new member a starting balance of time credits — five hours by default. To make sure nobody is paid twice, the platform checked whether a credit had "already been given" partly by looking at the wording on past entries in the member's wallet history. But the wording on an ordinary transfer between members is free text, written by whoever sent it. So any signed-in member could send a new member one hundredth of a credit, type the right words in the note, and the new member's welcome credits were never paid — permanently, silently, and repeatable against every new member. The administrator's screen simply reported nothing awarded. The platform now identifies a genuine welcome grant by something only the platform itself writes, so no member can imitate one. Members who were given their welcome credits under the old system are still correctly recognised and are not paid twice (F-474).
Removing a story from a highlight now tells you whether it was actually removed. A member can group their stories into highlights on their profile. Asking to take a story out of a highlight always answered "removed", even when that story was not in the highlight at all — someone else's story, a story from another community, or an identifier belonging to nothing. Nothing was ever taken from anyone else: the platform only ever looks inside the member's own highlight. What was wrong was the answer, so neither the member nor anything keeping a record could tell a real removal from one that did nothing. The answer is now "not found" when nothing was removed, and that answer is deliberately word-for-word the same whatever the identifier really was, so it still cannot be used to work out whether somebody else's story exists. Removing a story that really is in your own highlight is unchanged (F-418).
Deleting a community's single sign-on provider no longer records a deletion that did not happen. An administrator asking to delete a sign-on provider was told "deleted" even when their community had no provider by that name — including when the name belonged to another community — and the platform wrote that deletion into the administrator audit log, naming the provider. Nothing was ever deleted from another community; the problem was the record, and this platform's security assurance rests on that record being true. The request is now refused when there is nothing to delete, nothing is written to the audit log, and the refusal reads the same whether the name belongs to another community or to nobody. A real deletion still works and is still recorded exactly once (F-472).
Removing a member's verification badge now refuses a member of another community. An administrator of one community could ask to remove a verification badge from a member of a different community and was answered with an ordinary success. Nothing was ever removed — the platform only ever touches badges in the administrator's own community — but an operator, or any automated process driving this address, was told the removal had succeeded. The equivalent "read this member's badges" screen next to it was already refusing the same request outright; the two now agree. Granting and removing badges for your own community's members is unchanged (F-473).
Eight administrator actions no longer report success for something belonging to another community. Editing a newsletter template, deleting a role, setting which groups are in a collection, deleting a community-partner link, deleting a course category, deleting a skill category, switching off an emergency alert, and saving an internal note on a pilot enquiry each answered with an ordinary success when the thing named belonged to a different community. In every case nothing was changed — the platform's own checks stop at the community boundary — but the screen, and any automated process driving these addresses, was told the change had been made. The group-collection one was the worst: it also wrote an entry into the administrative audit log describing a change to the other community's collection that never occurred. All eight now answer "not found", and the group-collection entry is no longer written. One sibling action in the same file, deleting a newsletter template, already did this correctly and is what the others now copy. Administrators acting on their own community's records are unchanged (F-482).
Turning federation off now stops your listings being sent to outside platforms. Every way an outside platform can ask to see a member's listings already checked whether that member had agreed to federation at all, and refused if they had not. The outbound half — where the platform volunteers a listing to each connected platform as it is created or edited — never asked. It also left the per-listing "share with other platforms" marker switched on when a member turned federation off, so the next time they edited an old listing its title, description and their member number went out to every connected platform, after they had withdrawn consent. The outbound half now asks the same question as everything else, and turning federation off switches those markers off at the same moment. A member who has agreed to federation still has their listings shared exactly as before (F-413).
Un-sharing, closing or deleting a listing now tells outside platforms to drop it. A listing already sent to another platform stayed on display there for ever: stopping sharing, closing the listing or deleting it sent nothing at all, so the member saw it disappear from their own community while it carried on being shown elsewhere. The platform now sends a withdrawal message, using the same approach already used for volunteering opportunities. The withdrawal carries only enough to identify the listing — never the title or description again. A listing that was never shared still sends nothing (F-414).
A member who turned federation off is no longer alerted by an outside platform. Turning federation off already stopped the emails. The bell alert and the phone notification for the same two events — someone on another platform asking to connect, and a review arriving from another platform — were decided by a different switch that turning federation off never touches, so those members went on being alerted. Both now ask exactly the same question the email already asked. Members who have federation switched on are alerted exactly as before (F-419).
A request to erase someone's data from an outside platform is no longer recorded as done when it failed. When a member deletes their account or withdraws from federation, the platform tells every connected outside platform to erase the copy it holds. If that platform was down or refused, the instruction was dropped in silence: the retries the platform promises — after five minutes, half an hour, and two hours — never ran, and the alert that asks an operator to do it by hand never fired. A refusal now does fail properly, so the retries happen and an operator is alerted if they all fail. A refusal caused by federation being switched off, or by the outside platform having been suspended here, is recorded loudly instead of retried, because retrying could not reach anyone (F-485).
A completed transfer to an outside platform is now actually sent to them. When a member sent time credits to someone on another platform, the step that notifies that platform looked for the partner's identity in a place the record does not keep it, found nothing, and quietly did nothing — on every transfer, ever. The member's hours moved locally, and the receiving platform was never told, so the two sets of books drifted apart with no warning. The platform now reads the partner from where it genuinely records it. A transfer between communities on this platform is still never sent outside, even where the two use the same internal numbering (F-486). Connections to outside platforms are switched off by default and none is connected today.
A community can no longer be shown another community's transfer because two reference numbers happened to match. When a community sends time credits to an outside platform, the record keeps the outside platform's reference number in the same box that an ordinary community-to-community transfer uses for the receiving community's number. Both numbering schemes start at one and count up, so they overlap. A community whose own number happened to match an outside platform's number was served the other community's transfer when it asked for it by reference: the number of hours, the sending member's own written note, and that member's name. The platform now only treats that box as a receiving community when the transfer is demonstrably not one sent outside. Where it cannot tell, it refuses — a deliberate choice, because the alternative is showing one community another community's private transfer. A community reading its own transfer, whether it sent it or received it, is unaffected (F-487). Connections to outside platforms are switched off by default and none is connected today.
An outside platform that has been suspended can no longer switch itself back on, or wipe the record of why it was suspended. Suspending a connection is the only lever an operator has against an outside platform that is misbehaving. The step that handles a "partnership activated" message would have lifted that suspension on the outside platform's own say-so, and cleared the error count and the recorded reason along with it. Nothing could reach that step in practice — the two doors into it already turn away a suspended platform — but the step itself is available to any future code that forgets to check, so it now refuses. The error count and the reason are also no longer cleared by any message from the outside platform: only an operator, or a successful sync, clears them. Activating a connection that is genuinely waiting to be set up still works, and both doors still turn a suspended platform away (F-423).
Fixed
The admin GDPR dashboard now shows a real compliance score instead of 0. Since the page was rebuilt in April 2026 the server never sent the score, the consent coverage figure, the completed-requests count or the requests-by-type breakdown, and the page showed each missing figure as zero — so every community saw a compliance score of 0 and consent coverage of 0% whatever its actual position. The score is now calculated from three parts: data requests answered within 30 days (40 points; a closed request with no recorded closing date is not counted as on time), the share of members who have accepted every required consent at its current version (30 points; administrators are left out, as in the legal-document figures), and the share of recorded breaches that are resolved (30 points). "Completed this month" now shows this month's figure rather than nothing, a figure that fails to load shows "N/A" rather than 0, and the separate back-up calculation the server fell back to on error — which used different field names the page could not read — has been removed. A test now checks that the server sends every field the page reads.
The admin Group Recommendations page now shows the recommendations members are actually given. Since it was built in February 2026 the page read from a database table that has never existed; the error was silently hidden, so it always showed 0 recommendations, an average score of 0.00, a 0% join rate and "No recommendations found". It now reads the store the platform really fills when it works out which groups to suggest to each member (hour-timebank had 2,679 on 1 October), shows each one's score, whether the member has since joined, and the overall join rate, and reports a real error if that read fails. The average match score is now shown as a percentage, like the per-row scores. A regression test seeds real recommendations and checks the figures, including that one community never sees another's.
Chrome no longer types the admin's saved email into the admin sidebar search, or their saved password into secret fields. Admin pages with a hidden-text field — an AI provider API key, SMTP password, OAuth client secret, map API key, or a new password set for another member — looked like a sign-in form to the browser's password manager, so it filled the admin's own saved password into that field and their email into the nearest text box above it, which is the sidebar search. Pressing Save on such a page could then store the admin's login password as the API key or secret. Two earlier fixes only marked the search box, which Chrome ignores for saved sign-ins; all 20 such fields now use one shared component that tells Chrome, 1Password, LastPass and Bitwarden not to fill them, and a test fails if a new one is added without it.
The admin Cron Jobs page now shows whether each job's last run worked, and a failed run is no longer hidden. Every job showed "Last Status: Unknown", because the status label had no entry for the word the system records for a successful run ("success"). Worse, the page counted a run as failed only when it was recorded as "failed" — but the system records a failure as "error" — so a broken job showed no red mark and the "Last Failed" total stayed at 0. Both words now have translated labels in all eleven languages, and a job whose last run errored is marked and counted as a failure.
The pre-deploy check no longer counts a test suite as passed when part of it never ran. The PHP and React test suites are split into shards that run side by side. If a newer push cancelled some shards part-way, the check that decides whether a commit may be deployed still treated the suite as passed as long as the other shards had succeeded — on 30 September it accepted a run where two of the ten PHP shards had never run. Such a run is now not counted at all, so the check looks further back for a complete run, or refuses. A failed shard still refuses as before, including when it caused its siblings to be cancelled. A regression test now runs in CI on every push.
The new policy email screens and messages are now available in all eleven languages, not English only, and the policy email activity page shows each delivery state (queued, submitted, blocked, and so on) as a translated label instead of the raw system word.
Policy publication now rejects a missing change summary in the service itself, queues recipients in batches, and orders the email audit by publication time while preserving the title used when each notice was sent. The policy activity page also sets a proper browser title.
Legal document creation and settings now show the API's translated error explanation instead of a generic "Save failed" message, including when a document of the selected type already exists for the community. The automatic admin translation check now records, at both places, that these explanations are already translated by the server, which it had refused to accept without that note.
Network administrators can now use six screens that were wrongly refusing them. A network administrator is someone given authority over a community and the communities under it. The super-admin panel has always granted that as a permission on its own, leaving the person's listed role as "member" — and six places checked the listed role and nothing else, so they turned a real network administrator away. The six: seeing unpublished knowledge-base articles in the list, opening an unpublished article by link or by name, editing a shared resource someone else uploaded, deleting one, and running volunteer check-in for a shift. All six now ask the platform's single authority check, the same one the rest of the admin screens use. Nobody gains anything: an ordinary member is still refused at all six, and brokers and coordinators are still refused exactly where they were before. This was found while fixing F-431, which makes every network-administrator grant look this way (F-431 follow-up).
Saved-search alerts now count only the listings you can actually open, arrive in your own language, and stop for a suspended account. The hourly "new listings match your saved search" alert counted every listing, including ones a moderator had rejected, ones still waiting to be reviewed, and ones that had been erased. So a member could be told four listings matched and find one when they followed the link, and the platform was announcing content it had decided that member may not see. The alert now uses the same visibility rule as every other place listings are shown, and the number recorded against the saved search matches it. The wording of the alert was also written into the code in English, so it reached everyone in English whatever language they had chosen; it now uses the platform's translations and is written in the recipient's own language, in all eleven. And the alert is no longer sent to an account the community has suspended, banned or deleted. A member in good standing with a genuinely new listing still gets the alert exactly as before. While fixing this, the hourly task was also made to put the community setting back the way it found it when it finishes (F-435).
Six of the permissions a partner platform can be given now actually exist to give. When a community creates an access key for a partner platform it chooses what that key may do. The list of choices had fallen behind the platform: six permissions the partner routes check for — reading the list of communities, reading listings, reading and sending messages, and reading and writing reviews — could not be picked at all, so six documented partner features were unusable with any key the platform itself could create. The list of choices and the permissions the routes enforce now come from one place, and an automatic check fails if the two ever drift apart again. Of the six, sending messages, writing reviews and reading messages can only be granted by a platform administrator, not by a community's own administrator, in line with how moving time credits is already treated. Making up a permission that does not exist is still refused (F-451).
Tooling:
js-yamlis now declared in the rootdevDependencies.scripts/preflight.mjsand the deploy check gatescripts/predeploy-ci-verify.mjsbothrequire()it, but it was only named inoverrides, so it resolved only while a stray copy sat innode_modules; anynpm update/npm ciat the root removed it and both scripts crashed with "Cannot find module js-yaml" — for the deploy gate, a refusal indistinguishable from "not checked". Same class of fault as the undeclaredminimatchfixed in9794618e1.Three places where an action was stopped because of a block now say "not allowed" rather than "bad request". The action was refused either way; only the explanation was wrong (F-336 follow-up).
Submitting a community survey response, or an emergency alert, with something missing now returns a clear message about what is missing instead of a server error (F-372).
Pausing or resuming an achievement campaign no longer shows an error; the pause or resume always worked, but the screen said it had failed.
Uploading a photo or file larger than 1 MB works again on every community's website. Since 24 September the web server in front of the platform refused any upload over 1 MB before it reached the platform, so an ordinary phone photo could not become a profile picture — which also stopped new members finishing the welcome steps. The web server now accepts uploads up to 105 MB, the same limit the platform itself uses, and each kind of upload keeps its own size limit.
"Was this article helpful?" answers in the Help Centre are now kept with the community whose article was rated. Every community's answers were being filed under the platform's main community.
The "Restrict matching" and "Require coordinator approval" safeguarding options now apply to matches. A match involving a member who chose either option, or who is under monitoring, is no longer shown or emailed to anyone until a coordinator approves it on the Match Approvals page. That page was always empty until now, because nothing ever sent matches to it. When a coordinator approves a match, both members are told. A rejected match stays hidden, and neither member is told. The matching screens and Help Centre articles are updated in all languages.
The post-deploy check of what search engines see no longer reports freshly re-rendered pages as frozen: it now judges a page by when it was last rendered, not by how old the code is. Deploys also no longer re-render dozens of existing pages at random because of a faulty "already exists" check.
Completing an exchange now earns experience points (XP) for both members. Until now a completed exchange earned nothing, because the XP award only ran for direct wallet transfers. Each member now receives the existing "complete a transaction" award once per exchange; no XP amounts or level thresholds were changed.
The mobile navigation menu no longer answers with a server error. The address that gives a phone app a community's simplified menu failed for every caller, signed in or not. The platform's built-in fallback menus are written into the code rather than stored in the database, so their entries have no database identifier — and the menu code insisted on reading one, which turned into an error the moment it looked. It now copes with an entry that has no identifier, and the address returns the menu it was always meant to return. As well as leaving apps without a menu, the constant failure was noise in the platform's error reports that could hide a real fault (F-439).
Signing up to a community that no longer exists no longer creates the account in the platform's main community. The phone app and the accessible website both name the community they are asking about by its short name. When that name matched nothing — because the community had been renamed or removed — the platform quietly fell back to the main community instead of saying so: the person was told "registration successful", was placed in a community they had not chosen, and was invisible to the community they meant to join. Reading anything that way was answered with the main community's details too. An unrecognised community name is now refused. The two other ways of naming a community, by web address and by the
?slug=setting, already refused it. Naming a community that does exist is unchanged, and a request that names no community at all — the platform's own home, sign-in and about pages — still reaches the main community exactly as before (F-443).A refusal to serve a community now reaches the browser as a refusal, with its own reason and the usual protective headers. When the platform turned a request away because the community was unknown, was switched off, or did not match the sign-in being used, it wrote the refusal straight out and stopped the request there — before the step that adds the security headers, and before the step that lets a browser read an error coming from another address. A browser therefore reported an unexplained connection failure instead of the real reason, and the "community unavailable" page went out with none of the usual protections against being framed or mis-read. Each of those refusals is now an ordinary response, so it carries those headers and keeps its own status and reason instead of all three collapsing into one generic message. No member information was exposed by this (F-444).