Changelog

What we shipped, when. Newest first.

Update — 2026-07-20

20 July 2026

20 July 2026

Fixed — Resending an event invite now works

  • Resending an event invite was failing every time. From the event's invitee list,

"Resend" (and "Resend selected" for a batch) returned a server error and no email went out. This affected every club, on every event, for every invite — there was no combination where it worked, and no error the club could act on.

  • The cause was internal: when preparing the reminder, the app looked up whether your club

had set up its own custom "event invited" email, and it asked for that setting using names that no longer matched the database. The lookup failed, and because sending an invite is treated as something that must never half-happen, the whole resend correctly stopped rather than sending a wrong or half-built email. That safety behaviour was right; the lookup underneath it was wrong.

  • Both resend and bulk resend now work, and a club that has built its own custom invite

email will now actually get that template used.

  • The same faulty lookup was also present when invites are first sent. There it failed

quietly rather than loudly: the invite still went out, but always using the standard wording, so a club that had designed its own invite email never saw it used. That is now corrected too.

  • No data was affected and no invite links were changed by this fix. Invites that were sent

successfully before are unaffected, and existing booking links keep working.

Note for the owner

The subject line still comes from the app's standard wording ("Reminder: You're invited to …") even when a custom template is used, because the sender has always taken the subject from the app rather than from the template. Changing that would alter the wording members receive, so it has been left exactly as-is and flagged for your decision rather than bundled into a fix.

Improved — Event invitations now use the template you chose for that event

  • When you create or edit an event you can pick an invitation template for it. Until

now the invitation and resend emails ignored that choice and only ever looked for a club-wide "event invited" automation rule — so the template set on the event itself was quietly never used.

  • Invitations now follow a clear order: the event's own invitation template first, then

your club-wide "event invited" rule, and finally the standard built-in invitation. The same order is used for first invitations, single resends and bulk resends, so all three now send the same thing.

  • If the template chosen on an event has since been archived or deleted, the invitation

quietly falls back to your club-wide rule (or the standard email) instead of failing.

Fixed — Invitations can no longer be sent using an SMS or Telegram template

  • An event invitation is an email. If your club had an "event invited" automation set up

for SMS, Telegram or Push only, that template could previously be picked up and sent as an email — producing an email built from message content never written for email.

  • Only email-capable templates are now eligible. Templates set to Email, to the legacy

"Email + Telegram" option, or to any combination that includes email, all still work exactly as before. Older rules saved before the channel setting existed are still treated as email, so nothing that worked stops working.

  • If more than one club-wide invitation rule is eligible, the most recently updated one is

now chosen consistently, rather than whichever the database happened to return first. Previously the same event could use a different template from one send to the next.

Fixed — "Invites sent" now tells you what actually happened

  • Sending invitations always reported "N invites sent successfully", where N was simply

the number of invitations created. It said the same thing whether every email went out, every email failed, or no email was ever attempted.

  • Creating an invitation and delivering it are now reported separately, and the message

reflects the real outcome — for example "12 invites created — 10 sent, 2 failed", or "5 invites created" when you chose not to email anyone.

  • Emails that are queued for retry are reported as queued rather than sent, and emails

deliberately not delivered (currently a demo club — see the note below on why invitations aren't affected by unsubscribes) are shown as not delivered instead of being counted as successes.

  • Members with no email address, and invitations that have already been used, are now

listed as skipped with the reason, rather than silently disappearing from the count.

  • Invitation links are never thrown away because an email failed. The booking link is

still valid and you can simply resend it.

Fixed — Resending to a blocked address no longer breaks the member's existing link

  • When you resend an invitation and the system deliberately declines to send —

currently a demo club — it was treating that as a successful send.

  • For an invitation that had already been used, that had a real consequence: the member's

existing, working booking link was replaced with a new one, and the email carrying the new link never went anywhere. The member was left holding a link that no longer worked, with no way to be told.

  • The old link is now left completely untouched unless a replacement email was genuinely

sent or durably queued for delivery. The resend reports honestly that nothing was delivered, and bulk resend counts that member as skipped rather than resent.

Fixed — Results are no longer shown in green when the emails failed

  • Invitation results were always presented as a success, whatever happened. Even after the

wording was made accurate, "12 invites created — 12 failed" was still shown in a green success banner and a success notification.

  • Results are now styled to match the outcome: green only when every email was sent or

durably queued, amber when some were skipped, blocked or failed, and red when nothing was delivered at all. Anything that isn't a clean success now also stays on screen longer so it can be read.

Fixed — Two more places now report what actually happened

  • Re-inviting a member whose booking was cancelled previously said "Re-invited <name>"

purely because the invitation had been created — it said the same thing when no email went out. It now reports the real outcome, and only says "Re-invited" when an email was genuinely sent or queued.

  • Inviting members from the assessment screen ignored the delivery result entirely.

It now reports the same honest outcome as everywhere else.

Improved — You're now told when an invitation template needs attention

  • If the template chosen for an event (or your club-wide invitation automation) can't be

used — it's been archived, deleted, or isn't set up for email — the invitation still goes out using the standard wording, but you're now told, so a silent fallback doesn't go unnoticed for months.

  • You're also told when more than one club-wide invitation automation is switched on, since

only one can be used.

  • These notices explain what to check and never expose template contents or anything

belonging to another club.

Under the hood

  • A database error while loading members could previously be reported as "no email

address" for everyone in the batch — an outage disguised as a data problem. Read failures while loading the event, the club or the members now stop the request cleanly instead of producing a misleading result.

  • A missing event and an event belonging to another club remain indistinguishable, so

nothing can be learned by probing.

Fixed — "Request a new booking link" no longer breaks a member's existing link

  • The booking page lets a member request a fresh link if theirs has already been used.

Where the system deliberately doesn't send — currently a demo club — it correctly declined, but still replaced their existing link. The member was left holding a link that no longer worked, on the one screen where there's no staff member to ask.

  • The existing link is now left completely untouched unless the replacement email was

genuinely sent or durably queued. The member is told simply that we couldn't send a new link and to contact their club.

  • That message is deliberately identical for every reason a new link couldn't be sent, so

the page can't be used to work out anything about a member's email address or about what another request happened to be doing at the same moment.

Fixed — "Resent 0 invites" is no longer shown as a success

  • Bulk resend and bulk revoke reported through a green success notification whatever

happened, so "Resent 0 invites · 12 skipped" — where nothing at all was resent — still looked like a success.

  • Bulk results are now coloured by what actually happened: green only when every row

completed, amber when some were skipped, and red when none completed. The compact summary of skip reasons is unchanged.

Improved — Template problems now show wherever you resend

  • The invitation, resend and bulk-resend screens all report when a configured template

couldn't be used, so the notice reaches you wherever you happen to be working — the event page, the calendar panel, and the grading booking screen.

  • On the grading screen the notice now persists while the list refreshes, instead of

disappearing before it could be read, and repeated resends no longer stack the same notice over and over.

  • These notices sit apart from the delivery result: a template notice never turns a

failed send green, and a successful send is never reported as a problem.

Fixed — Two people regenerating the same invite can no longer break the link

  • If the same invitation was regenerated twice at nearly the same moment — a member

clicking "request a new link" while a staff member resends, or two quick clicks — both emails went out and only one of the two links could work. The member could easily be looking at the dead one, and nothing in the system knew.

  • Replacing an invitation link and creating its replacement email now happen as a single

all-or-nothing step. The first request wins and its link is the live one; the second is told the invitation was just regenerated elsewhere, and sends nothing.

  • If the system loses power at exactly the wrong moment, the replacement email is already

recorded and still goes out — the link that was promised is the link that works.

  • A request that is refused — because another one won, or the club is in demo mode

— leaves the existing link completely untouched, rather than quietly reporting success while changing nothing.

  • If a connection drops at the exact moment the change is being made, we can no longer be

certain either way. Staff are now told plainly that the result couldn't be confirmed and to check the member's invitation, instead of being reassured that nothing changed — which might not be true.

  • When two people regenerate at once, the one who loses is now told that **another

regeneration completed first** and not to rely on the previous link. It no longer says the member's existing link is unchanged, because when someone else has just replaced it, that was the opposite of the truth.

What "sent" does and doesn't mean

  • Invitation emails are transactional, not marketing. They are deliberately not governed

by the unsubscribe and bounce suppression list that applies to bulk club-wide messages — a member who has unsubscribed from newsletters still receives the invitation they need in order to book.

  • The one case where the system deliberately doesn't send is a demo club, and that is

decided before anything is changed, so nothing is altered and nothing is claimed.

  • "Sent" means the email was accepted by our provider, or safely queued to be delivered.

Neither is a guarantee it reaches an inbox — a mailbox can still bounce it or file it as spam. Where we can't confirm delivery, we now say so rather than implying success.

Fixed — Demo clubs no longer claim an email was sent

  • On a demo club, "request a new booking link" replied as though a new link had been

emailed. Demo clubs deliberately don't send email, so nothing had been.

  • Demo stays exactly as read-only as before, but now says honestly that a new link

couldn't be sent.

Improved — Warnings and partial results now have their own amber

  • Notifications previously had only green, blue and red. A result that was neither a clean

success nor an outright failure — "8 resent, 2 skipped" — had to borrow one of those, and both misrepresent it.

  • There's now a proper amber warning style, so a partial result looks like a partial

result.

Correction to an earlier note

  • Earlier notes said a template problem meant "the standard invitation was used". That was

inaccurate in two ways: when an event's template can't be used the system falls back to your club-wide template rather than the standard one, and a template being chosen says nothing about whether the email then arrived.

  • Template notices now describe only which template was selected, and never claim an

email was sent. Whether it was delivered is reported separately, by the delivery counts.

Fixed — A booking and a link-regeneration can no longer race each other

  • Until now, "does this member already have a booking?" was checked just *before* an

invitation link was regenerated or revoked — so a booking created in that same instant could slip through, leaving an invitation re-opened for a member who had just booked.

  • Regenerating and revoking now make that decision *while holding the same per-event lock

the booking system itself uses*, so the two can genuinely no longer interleave: whichever starts first finishes first, and the other sees the result.

  • Staff "add booking" and the register's walk-in bookings now take that same lock too. Staff

can still deliberately book over capacity — that behaviour is unchanged — it simply happens under the lock now.

  • Re-adding a member whose booking was cancelled is refused exactly as before; a cancelled

booking is never silently brought back by re-adding.

For the owner — new database steps

Three new proposed migrations accompany this (regeneration v2, atomic revoke, locked booking creation), plus one closing cancelled-booking resurrection in grading. Apply them before deploying the app build that uses them — the app fails safe (a clear error, no fallback to the old behaviour) if they are missing. Details in the release checklist.

Fixed — The attendance monitor no longer cries wolf

  • Since the 16 July attendance system upgrade, the behind-the-scenes monitor that

checks attendance bookkeeping reported failure on every run — not because anything was failing, but because it counted the club's pre-upgrade historical records (which are simply awaiting a one-off review) as if they were live errors. A permanently-red monitor is worse than none: a real problem would have been invisible inside the noise.

  • The monitor now tells those two stories apart. Historical records show as an

amber "historical review outstanding" note with the exact counts, while the red alert is reserved for things that are genuinely wrong right now — and it also now confirms on every run that the old writing path is still switched off.

  • Nothing about attendance itself changed: registers, entitlements and the pending

review of historical records are exactly as they were. This is the monitor telling the truth, plus a prepared, read-only review pack so the historical tidy-up can be done deliberately rather than under a false alarm.

Improved — Invoice & VAT settings now have their own home

  • The club's invoice details (supplier name and address, VAT registration, VAT

rate, and whether prices include or exclude VAT) used to live inside Settings → "Your Plan" — the page about your Allsorted subscription, where nobody thought to look. They now have their own tab: Settings → Payments → Invoices & VAT.

  • The page now says plainly whether the club is ready to issue invoices, and if

not, exactly which details are missing — in plain words, not database names.

  • Turning VAT on now asks for everything invoices legally need (VAT number and an

explicit inclusive/exclusive choice, with a worked £10 example showing the difference). Turning VAT off requires none of it. These checks are also enforced server-side, so no route into the settings can save a half-configured VAT state.

  • VAT invoicing currently supports GBP only, and the settings now say so rather

than finding out at enrolment time: a club billing in another currency sees a clear note, can't switch VAT on, and the readiness summary lists the currency requirement alongside everything else. Previously the page could say "Ready to issue invoices" for a setup that every enrolment payment would then refuse.

Fixed — Enrolment no longer hangs at the payment step

  • When a club's invoice details were incomplete, a parent completing an enrolment

invite reached the payment step and the page sat on "Preparing payment..." forever, with no message and no way forward. No payment was being taken, but the page never said so.

  • The page now stops, explains calmly that the club's payment setup isn't finished,

confirms no payment has been taken, and offers a Try Again button. Every kind of failure at that step now exits the spinner — including network drops.

  • Staff now find out before a parent does: the Trials & Enrolments page shows

a warning when the club isn't ready to issue invoices, linking straight to the new Invoices & VAT tab to finish setup.

Update — 2026-07-19

19 July 2026

19 July 2026

Security — Database permissions for two sensitive operations (now applied)

Status: Applied to production on 19 July 2026 and verified. Both findings are closed.

  • Two database functions — the one that edits manual invoice lines, and the one

that permanently erases a member record — were reachable more broadly than intended. Both are only ever meant to be called by the server, through the normal app routes that already check your role and your club. The app itself was never the problem: every route in the product does the right checks. The gap was that the database was also willing to accept those two calls from outside those routes.

  • The corrective changes remove permission from every

general caller and leave only the server able to run these two operations. The functions themselves are untouched — the wording, retention and safeguarding behaviour of a member purge, and the penny-exact totals of a manual invoice edit, all stay exactly as they are today.

  • These changes are now live. They were applied on 19 July 2026 and the follow-up

check confirmed the intended result for both functions: no general caller can run them, and only the server can. Each change verified its own result before committing, so a change that quietly did nothing could not have been mistaken for a fix.

  • Applying them needed no downtime and no app deploy, and normal use was unaffected.
  • One operational note for the future: re-running an older database bundle can restore

the very permission that was removed from the member-erase function. The release checklist requires the corrective to be re-applied, and the permissions re-checked, after any such run.

Internal — Release safeguards for the permission fixes

  • The two correctives are now registered in the release system as independent,

apply-on-their-own units, so they can be shipped in an emergency without waiting for any other work.

  • They are also placed last in the generated deployment bundles. This matters: a

later database change can re-grant a permission that an earlier one removed, and re-running an older bundle would otherwise silently undo the fix. The release checklist now records exactly which older bundles have that effect and requires the corrective to be re-applied afterwards.

  • The two bundle formats (command-line and SQL-editor) now share one definition of the

permission contract, so they cannot drift apart, and every deployment mode ends by re-checking the permissions and stopping loudly if they have been re-opened.

  • A new automated guard pins the exact set of places in the codebase that grant these

permissions. The previous guard only counted them, so swapping one grant for a brand-new one went unnoticed; the replacement compares the full list and fails on any new file, signature, grant style or widened recipient. Its documented blind spots are written down alongside it rather than left implied.

Internal — Hardening the safeguards around the permission fixes

  • The automated guard that watches for anything re-granting these permissions now

reads SQL properly rather than pattern-matching it. It previously missed several perfectly legal ways to write the same grant — leaving the argument list off, naming the function part-way through a comma-separated list, quoting the schema or function name, or omitting the final semicolon — and it did not notice a function being dropped and recreated, which quietly restores the very permissions we removed. All of those are now caught.

  • Malformed SQL used to be treated as clean. An unterminated comment made the guard

ignore everything after it and report the file as fine; it now stops and names the file and line instead. It also now reads every tracked SQL file in the database folder, including ones sitting at the top level that were previously outside its sweep, and checks that the generated deployment files are up to date with their generator.

  • The guard's genuine blind spots — SQL that is assembled at run time rather than

written out — are listed explicitly and are deliberately not what we rely on. The real check remains reading the live database permissions after applying.

  • The owner's pre-apply check and the security test suites all touch the same two

database permissions. They now take turns via a single lock that records who holds it, so one can no longer capture or undo another's half-finished state. The lock is released even if a run fails or crashes part-way, and it cannot be removed by a process that no longer owns it.

  • On the production position: the two permission fixes described above were applied

on 19 July 2026 and verified. The safeguards in this section are about preventing a future regression — they are not themselves database changes, and none of them has been applied anywhere.

Update — 2026-07-18

18 July 2026

18 July 2026

Fixed: event invitations appear immediately

  • New invitations now appear as soon as they are sent. The event panel updates its

invite count and rows immediately, and the full event-management page now does the same. Both quietly check the server copy without flashing or requiring a browser refresh. If that follow-up check fails or briefly returns stale information, the confirmed rows stay visible instead of incorrectly saying that no invitations have been sent.

Improved: clearer Communications navigation

  • Communications is easier to move around. Messages, Comms Map, Automations,

Templates and Deliverability now sit in one centered icon menu using the same glass styling and responsive motion as member profiles. The menu remains swipeable on smaller screens, while existing routes and permissions are unchanged. The menu now stays above the page title throughout the Communications area, so switching sections no longer makes the header jump above and below it.

Fixed: invoice PDFs show their line items again

  • Invoice PDFs now always show the real line items. A fault in how the lines were

fetched meant every invoice PDF rendered a “line item details are temporarily unavailable” placeholder instead of its contents. The lines now load reliably in a stable order, and if they genuinely can’t be read the PDF is held back with a clear error rather than producing an incomplete financial document. Previously-generated placeholder PDFs regenerate automatically. Totals are unchanged.

New: VAT support for VAT-registered clubs

  • Clubs registered for VAT can now invoice with VAT end to end. With VAT enabled in

Settings → Billing, every invoice your club raises — memberships, joining fees, events, class packs, manual invoices and more — now carries the right VAT per line (inclusive or exclusive, at your default or a per-line rate), totals stay exact to the penny, and issued invoices are never altered afterwards — corrections flow through credit notes at the original rates. Clubs without VAT see no change at all.

  • A new VAT summary report. Reports gains a VAT tab for VAT-enabled clubs: net, VAT

and gross by period, code and rate, with credit notes netted at their original rates and a CSV export. It’s a bookkeeping aid — clearly labelled as not a VAT return.

  • Draft invoices can be recalculated. If you change VAT settings, existing drafts

keep their saved treatment unless you explicitly recalculate them (a logged action); issued history is never touched.

Improved: course and pathway reliability

  • Course building, pathway assignment and reordering are now crash-proof. Reordering

sections, lessons, stages, tasks and portal messages applies as a single change (no more half-reordered lists if two people edit at once — the later editor is asked to refresh); assigning a pathway creates the member’s full task list atomically; and lesson watch-time is recorded exactly once even from two devices. Where information can’t be read, screens now say so instead of appearing empty.

Improved: card-payment invoices and invoice line ordering

  • Card payments always produce a correct invoice for VAT clubs. When a card payment

settles an event booking, the resulting invoice now carries the club’s VAT treatment exactly like every other invoice — and if the VAT settings can’t be read at that moment, the payment is retried automatically rather than quietly recording an invoice without VAT. Clubs without VAT see no change.

  • Invoice lines keep the order you arranged them in. New invoices remember the order

their lines were created or edited in — on screen and on the PDF — instead of falling back to insertion order. Existing invoices keep their familiar order, and issued invoices remain locked exactly as issued.

Improved: every remaining report fails safe, and event invoices self-repair

  • The last six reports now say “couldn’t load” instead of zeros.

Financial, Insurance, Data Quality, Events, Trial Funnel and Class Pulse join the rest of Reports: a failed load shows a clear notice (never £0.00 revenue, an empty funnel or “no feedback” as if true), and a network failure can no longer leave the Financial report stuck on its loading screen.

  • An event invoice can no longer end up empty. In the rare case where an invoice was

created but its line items failed to save, the payment flow now repairs the invoice automatically on retry — and if it can’t, the problem is flagged on Platform Health rather than leaving an invoice that looks settled but has no contents. The daily reconciliation now actively re-repairs any such flagged invoice, and the card payment retry always reconnects to the exact invoice it needs to fix rather than creating a duplicate.

  • One reliable path for event-payment invoices. Whether the confirmation comes back

through the browser or the payment provider’s webhook, a paid booking’s invoice is now created, repaired and settled by a single authority — so two arriving at once can never produce a duplicate invoice or one without its line items, and the booking screen no longer raises the invoice separately. Paid bookings for another club’s event (a cross-club event that charges a different club) are declined at payment time rather than recorded under the wrong club’s VAT; free cross-club bookings are unaffected.

  • Rapidly changing a report’s dates or programme no longer shows stale numbers.

On the Insurance, Data Quality, Events and Trial Funnel reports, switching filters quickly could briefly let an older, slower result overwrite the newer one; the report now always shows the result for the filter you actually chose.

Behind the scenes

  • **The next database release now fails safely and runs in clearly separated

windows.** Blocking preflight checks return a real failure to the deployment runner, the card-payment invoice identity index is verified as usable rather than trusted by name alone, lock-taking audit maintenance runs only in its declared quiet window, and the post-code status-notification outbox no longer repeats unrelated database work. This is release hardening only; it does not enable a feature, send a communication or change live data.

Fixed: register saves for existing classes

  • **Attendance saves work again for classes created before Programmes were

introduced.** Those classes already had a valid programme through their original target-programme setting, but the new attendance authority compared only the newer programme field and rejected the mark. Existing classes are safely reconciled to the canonical programme, new timetable saves keep that relationship populated, and the server now takes the attendance programme from the class rather than the browser’s current filter.

Update — 2026-07-17

17 July 2026

17 July 2026

Fixed: the top menu now uses your club's member terminology

  • **The main navigation now uses the same member wording as the rest of the

app.** If your club calls people Members, Players, Gymnasts or another configured term, the top-bar link follows that setting instead of always saying Students. The mobile menu, command menu and Settings already used the configured wording; all of the app navigation now agrees.

Plan a sequence of scheduled member status changes

  • **You can now plan a sequence of member status changes — e.g. pause on one

date, reactivate on another.** From a member’s profile, setting a future “Effective from” date on a status change now opens a planner where you can add, remove and reorder up to ten stages, each a status and a date. Save it and each stage applies automatically on its date; until the first stage, the member keeps their current status. A single scheduled change still works exactly as before — it’s just a one-stage plan.

  • The profile shows the whole plan as a timeline. The scheduled banner lists every

stage in order, with the ones that have already applied ticked and the ones still to come marked as upcoming. “Edit” reopens the planner on the remaining stages, and “Cancel plan” drops the not-yet-applied stages while keeping anything that has already happened — applied history is never rewritten, and no automated messages fire for cancelled stages.

  • The club-wide Scheduled Status Changes page groups by plan. Each plan shows its

next change and, for multi-stage plans, expands to reveal every stage. Cancelling from here uses the same confirmation and the same “keep what’s applied” rule.

  • Plans are validated before they can be saved. Dates must be real calendar dates

in strictly increasing order with no repeats, two stages in a row can’t set the same status, the first stage can’t be in the past, and a plan can hold between one and ten stages — checked as you type and re-checked on our side. Replacing a plan is atomic: the previous pending plan is superseded and the new one saved together, so a member can never be left mid-change with a half-saved schedule.

Fixed: scheduled status plans — reliability and validation

  • Scheduled plans no longer disappear behind a long history. The club-wide

Scheduled Status Changes page, and the daily job that applies due changes, used to read the whole history in one go — so a club with a large back-catalogue of already-applied changes could push the current, still-pending plans out of view and, worse, leave the tail of a big backlog unapplied. Both now fetch the pending plans first and page through the work in full, so every current plan shows and every due change is applied no matter how much history has built up.

  • Cancelling a plan always leaves an audit trail. Cancelling a scheduled plan now

records the cancellation and drops the not-yet-applied stages together as a single step, so a cancel can never quietly succeed without a matching entry in the member’s activity history. Applied stages are still never touched.

  • A first stage that repeats the member’s current status is refused. Setting up

a plan whose opening stage matches where the member already is (for example Active → Active) was a confusing no-op; it’s now rejected as you type and on our side. The planner also measures “not in the past” against your club’s local calendar — the same clock our side uses — so a plan starting today is never wrongly refused around midnight.

Schedule portal messages to appear and expire on set dates

  • **You can now schedule a portal announcement to appear and expire on set

dates.** Each message in Settings → Member Portal has an optional “Show from” and “Show until” date. Set both to run a notice for a fixed window, set only one to open or close-ended it, or leave both blank to keep the old behaviour — shown whenever it’s enabled. A half-term closure or a “kit orders close Friday” reminder can go in ahead of time and clear itself, with no need to remember to turn it on and off. The dates are inclusive and use your club’s local calendar, so a message set to end today still shows all of today.

  • Members only ever see what’s live right now. A message scheduled for

the future, or one whose end date has passed, never reaches the member portal — the filtering happens on our side before anything is sent to a member’s device, not in their browser. The manual on/off switch still works as an override, and archiving still hides a message immediately whatever its dates say.

  • The Settings list shows each message’s status at a glance. Every

message carries a badge — Live, Scheduled, Expired, Off or Archived — worked out from its dates, its enabled switch and today’s club date, so you can see exactly what members are seeing without doing the mental arithmetic. Impossible dates and an end that falls before its start are refused rather than saved.

  • Editing just one date can no longer create an impossible window. When you

change only the “Show until” date on a message that already has a “Show from” date (or the other way round), the new date is now checked against the one already saved — so an end that would fall before the existing start, or a start after the existing end, is refused rather than quietly stored. Clearing a date still works as before.

Copy a member's insurance number with one tap

  • You can now copy a member's insurance/governing-body number with one tap. A

small copy button sits next to the membership number on a member's profile — tap it and the number is on your clipboard, ready to paste into an insurer's or governing body's system, with a quick tick and a confirmation to show it worked. If your browser blocks the clipboard for any reason, it tells you plainly rather than pretending it copied. Editing insurance details still works exactly as before — the copy button sits alongside the existing edit action, it doesn't replace it.

Safeguarding lead now works everywhere

  • Designating a coach as safeguarding lead now takes effect across the app.

Being named your club's safeguarding lead is meant to unlock the restricted safeguarding views a coach otherwise can't see — retained accident reports and their PDFs, the accident detail screens, and the retained-record counts on the dashboard. Previously the designation was only honoured in one place and was silently ignored everywhere else, so a designated coach was still refused that data. The designation is now read from the club's own membership record on every one of these checks, so it grants exactly what it's supposed to — no more, no less. Owners and admins are unaffected (they already have this access), and a coach who has not been designated still sees no change.

  • The designation is per club and reflects the current setting immediately. A

coach who is the safeguarding lead at one club gains nothing at another club, and removing the designation takes effect on the very next request — there is no cached access that outlives the change. If the membership record can't be read for any reason, access is refused rather than assumed, so this never opens a safeguarding record by accident.

Behind the scenes

  • Security hardening: tighter club boundaries on three internal actions. Sending

event invitations, regenerating a class lesson plan, and firing an automation now each re-confirm that the member, class, or plan being acted on belongs to your own club before anything is read, written, or sent. A request that references another club's record is refused instead of proceeding. This has no effect on normal use — everything you do with your own club's data behaves exactly as before.

  • Guessing an id no longer reveals whether it exists. A handful of endpoints

(a staff-access removal, an accident-report file link, a member data-erasure, and the member progress-report preview and send) used to answer "that belongs to another club" differently from "that doesn't exist". They now return the exact same *not found* response either way, so a stranger's id can't be confirmed by probing. Access was already correctly refused in every case; this only removes the tell.

  • A club can no longer attach another club's record to its own data. Several

save actions — the charges and membership-plan editors, the member CSV importer, a team's season, a class recording's lesson, and an enrolment invite — now check that a referenced belt system, programme, plan, season, lesson or token actually belongs to your club before storing the link, rejecting anything that doesn't.

  • Enrolment invites are now fully sealed to your own club. Sending an invite

used to trust a custom email template and an invite-token reference from the request without re-checking ownership: a crafted request could have rendered another club's template into the email or aimed delivery-tracking at a token it didn't own. An invite now only uses a template that belongs to your club (falling back to the standard wording otherwise) and refuses outright if the token it names isn't yours, and the later delivery-status stitch is likewise pinned to the originating club. Normal invites — sent from your own club's screens — are unaffected.

  • Removing group-club access no longer reveals demo tenants. Trying to revoke a

staff access row you don't administer already returned a plain *not found*, but a guessed id that happened to land in a demo club leaked a different response — a tell that the id was real. All unauthorised attempts now return the identical *not found* regardless of the target club, while a club's own administrator still sees the usual read-only notice on the demo tenant.

  • A momentary database problem is no longer reported as "not found". Those same

belonging-to-your-club checks (charges and membership-plan editors, a team's season, a class recording's lesson, and lesson-plan regeneration) previously treated a transient database error the same as a genuinely unknown id — quietly telling you the record didn't exist when the real problem was on our side. They now tell those two situations apart: a real database hiccup surfaces as a generic try-again error instead of a misleading "unknown" message, while a genuinely foreign id is still refused exactly as before.

  • Sturdier event invitations. Sending event invitations now validates the selected

member list up front — rejecting malformed ids and capping how many can be invited in one go — counts a member picked twice only once, and stops with a clear error if the invite links can't be written, rather than reporting a partial success. Inviting members now also requires the same permission already needed to resend or cancel an invite, so the whole invitation workflow is gated consistently.

Fixed: event rosters, certificates and calendars showing empty

  • Event and grading screens that had started coming up blank are working again.

After an earlier database update today, several places that list booked members could return nothing at all — event and grading day rosters, the register’s event roster, the printable attendance / scoring / certificate PDFs, the grading scores and analytics screens, the examiner scoring page, the member portal’s events list and its subscribed calendar feed, and the household lookup in support. Each of those reads has been corrected, so rosters, downloads and calendars are populated as normal.

  • These screens now fail loudly instead of looking empty. Where a booking list

can’t be read, the screen or download now reports a clear error rather than quietly showing an empty roster or a blank PDF — so “no one’s booked” always means exactly that, and a genuine problem is never mistaken for it.

Fixed: more screens that could show empty (staff leave, certificates, membership counts)

  • Staff leave no longer silently disappears. After the same database update, the staff-leave

panels on registers and calendars could come up blank because of an ambiguous lookup; leave now loads correctly, and if it genuinely can't be read the screen says so rather than pretending nobody's off.

  • Examiner scoring, member certificates and the settings membership-plan counts were affected by

the same class of ambiguous lookup and are fixed too. A guard now checks the whole codebase against the live database's relationships, so this class of "silently empty" bug can't quietly return.

Missing-in-Action now counts "Late" as attended

  • Late now counts as attended in Missing-in-Action reports and automations. A

member marked *Late* has clearly turned up, but the Attendance report's Missing-in-Action list and the automated absence reminders only recognised *Present*, so a member who was regularly late could be flagged as missing and even chased with a "we miss you" message. *Present* and *Late* are now treated the same everywhere attendance is judged — one shared rule drives the report, the automated reminders and the scheduled email summaries — so a Late mark keeps a member out of the missing list. *Late* is still shown as Late in the coach and staff breakdowns; it is only counted as attended, never relabelled.

  • The missing list is no longer skewed by the dates you're viewing. The

Attendance report's Missing-in-Action list now measures each member against their genuine most-recent attendance, independent of the date range shown on screen. Previously, narrowing the report to a recent window could make a member who attended just before that window appear to have gone missing; the list now uses the member's real last-attended date, so it means what it says whatever range you have selected.

  • A database hiccup can no longer fire absence reminders by mistake. If the

attendance lookup that powers the automated Missing-in-Action reminders can't be read, that check now stops for that run instead of assuming nobody attended — which would otherwise have flagged every member as missing and sent a wave of unwarranted messages. Every other automation (insurance, gradings, birthdays and the rest) is unaffected and continues as normal.

Missing-in-Action now respects the programme lens

  • **When you view the Attendance report for a single programme, Missing-in-Action

now measures attendance within that programme.** Previously the missing list used each member’s most-recent attendance across the whole club, so a member who had stopped coming to, say, Football but was still attending another programme was wrongly kept off the Football missing list. With a programme selected, the list now looks only at recent attendance at that programme’s classes — over a fixed recent window, independent of the date range on screen — so a member absent from this programme shows up even if they are active elsewhere. A Late mark still counts as attended, and the whole-club view (no programme selected) is unchanged.

  • A database hiccup can no longer send a misleading scheduled report. The

Missing-in-Action figure in the scheduled email report summaries now stops the report if its attendance lookup fails, instead of treating the failure as “nobody attended” and emailing a summary that flags every member as missing. The run is recorded as failed so it shows up in our health checks, and a correct report goes out on the next run once the read succeeds.

Kukkiwon is now a Taekwondo-scoped custom field

  • **Kukkiwon number is now a Taekwondo-scoped custom field, not shown to unrelated

programmes.** The Kukkiwon dan-certificate number used to be a fixed field baked into every member's record, so a football, dance, gymnastics or karate club saw an empty "Kukkiwon No." box that meant nothing to them. Kukkiwon is now one of the custom member fields you opt into from Settings → Custom Fields and scope to your Taekwondo programme(s), so only the members it applies to ever see it. Clubs that don't track it never see it at all.

  • Nothing you've already recorded is lost, and existing numbers keep working.

Any Kukkiwon numbers already on your members' records continue to show and remain editable exactly as before, whether or not you've adopted the new custom field — the record falls back to the original value so no data is hidden or dropped. Once a club adopts Kukkiwon as a custom field, the number is captured and edited alongside your other custom fields, and the existing values carry across untouched.

  • The member's Grading tab now shows the field the way you've set it up. The

Kukkiwon line on a member's Grading tab now reads your custom field — using the value you've captured there and the label you've given it, so a renamed field shows its new name — and still falls back to the member's original number when you haven't adopted the field. A member of a programme the field doesn't apply to sees nothing there, as expected.

  • Mixed clubs are never given a club-wide Taekwondo field by mistake. When

existing Kukkiwon numbers are migrated onto the custom-field model, the field is set up automatically only when every member who has one sits in a single, clear programme — in which case it's scoped to exactly that programme. If your members are spread across more than one programme, nothing club-wide is created; the original numbers are kept as-is and the clubs concerned are flagged so you can pick the right programme yourself, so a Taekwondo field can never appear on a football or dance member's record.

Fixed: MIA report count could be wrong for very busy clubs

  • **The scheduled attendance report's "Missing in Action" count is now accurate no matter how

busy the club is.** For clubs logging thousands of attendances a fortnight, the report could read more members as missing than really were, because it only looked at the first slice of recent attendance. It now reads all of it, so the count is right.

Fixed: Attendance report’s missing list no longer shows a false “everyone missing”

  • **The Attendance report’s Missing-in-Action list now says so plainly when it

can’t be worked out, instead of listing everyone.** When you view the report for a single programme, the missing list is built from a read of recent attendance. If that read failed, or was too large to load in full, the list used to fall back to treating it as “nobody attended” — flagging every member as missing on screen and in the export. It now shows a short “couldn’t load” notice for that one section instead, so a temporary hiccup or a very large club can never produce a false list of missing members. The rest of the report still loads as normal.

  • The 14-day cut-off now follows your club’s local calendar. Whether a member

counts as missing is measured against your club’s own “today” rather than the server’s clock, so a member who attended right on the edge of the window near midnight is judged by the correct day.

  • Every source the missing list depends on now has to load in full. As well as the

attendance read, the class list and the member roster behind the list are now read completely, and if any of them fails or is too large to load in full the section shows the same “couldn’t load” notice rather than risk a false list. The missing list is also now built from who is currently enrolled in the programme — so someone who has left the programme is never shown as missing (even when you’re looking at an older date range), while a current member who hasn’t attended still is.

  • Five more reports now say “couldn’t load” instead of showing zeros.

Membership, Households, Owner Overview, Accidents and Student Analytics now treat a failed or partial read as a problem to show you — the report (or just the affected section, such as Attendance Alerts or Revenue by program) displays an unavailable notice rather than “0 households”, “£0.00 revenue”, “no accident reports” or a false attendance alert. A genuinely empty result still shows as zero.

  • Very large clubs can no longer see quietly understated totals. The reads behind the

Households and Owner Overview figures (including invoices) are now read in full, and if a dataset is too large to read completely the report says so instead of showing a lower number as if it were the truth.

  • Student Analytics grades every member against the right ladder. In a club running more

than one grading system, each member’s belt or grade now resolves against their own programme’s ladder (and a programme filter uses that programme’s ladder), so nobody shows as ungraded or sorted against another discipline’s ranks.

Improved: reports fail safe instead of showing a misleading zero

  • **The Attendance report’s totals now say “couldn’t load” when a

read fails, instead of a believable zero.** Every dataset behind the headline numbers (attendance, classes, enrolments and staff) is now read completely; if a required read fails or is too large to load in full, the report — or just the affected section — shows a clear notice rather than an under-count. Nothing you see is stale from a slower, superseded request either.

  • Program-lens reports count the right people for the dates you picked. For

Membership, Progression, Student Analytics, Accidents, Households and Owner Overview, who counts under a program filter is now measured against your club’s own calendar days (correct across the March/October clock changes), so a member who joined late on the final day of the range is included; and if that lookup fails, the report shows an error instead of a false zero.

  • Scheduled email reports read your whole report list and flag failures. The list of

enabled reports is now read completely (a large list is never silently truncated), and a read failure is reported rather than quietly skipping due reports. A test send is reported honestly — if every recipient fails, it no longer says it succeeded.

  • Member profiles tell you when a scheduled-status check couldn’t run. A failed

check now shows a short “couldn’t check” notice with a retry button instead of looking like there are no scheduled changes, and it clears itself once it loads or you save a change.

Improved: scheduled status-change notifications are now recoverable

  • A scheduled status change never silently loses its follow-up notification. When a

scheduled change is applied, its notifications are now recorded durably at the same moment as the change itself, and a background repair step re-sends any that didn’t go out (for example if the system restarted at exactly the wrong moment) — without ever sending a duplicate.

  • The repair step now runs automatically and reports honestly. The recovery sweep is

scheduled every five minutes, and if recording a notification’s outcome fails, the run is flagged for attention instead of quietly counting it as done — nothing can look “delivered” unless it truly was.

  • **The scheduled email report’s “Upcoming Trials” section can no longer show

a false “None”.** If the trials list can’t be read in full, the report is held back and the failure flagged, rather than emailing an empty section as if no trials were booked. A genuinely empty week still shows “None” as before.

Update — 2026-07-16

16 July 2026

16 July 2026

Smoother navigation

  • Returning from What's new no longer flashes the public homepage. The

changelog now preserves an authenticated staff session, so using the browser Back button returns directly to the dashboard shell without a marketing-page flicker.

Honest calendars and team views

  • Subscribed family calendars now fail visibly when their source data is unavailable.

A temporary database problem can no longer publish a believable empty calendar that makes booked classes or events appear to have disappeared.

  • Calendar events now keep a valid duration. Events without a recorded end time

use a 90-minute fallback, while events that finish after midnight end on the next day rather than before they start.

  • Team Sports no longer turns read failures into empty teams, rosters or results.

Team lists, fixture details, report cards and player search show a retryable unavailable state instead of zero counts or blank selections.

  • Matchday updates remain honest after a successful write. If the optional

automation fan-out cannot load its recipients after a fixture is published or a result is recorded, the failure is reported for operators without telling staff the completed update failed and inviting a duplicate submission.

  • Modules & Programs preserves its last known-good configuration. Failed core

reads now show the shared Retry state; only the documented not-yet-migrated vertical column may continue in deploy-safe mode.

Safer enrolment and event configuration

  • Enrolment questions are checked before payment or member creation. Required

programme-specific questions can no longer fail late after an enrolment token has been consumed or a partial member record has been created.

  • Duplicate enrolment add-ons are rejected. Prices, invoice lines and onboarding

tasks now all use the same server-verified selection, preventing repeated items from producing mismatched totals.

  • Progression filters stay within their programme. Class and event-type minimum

and maximum levels must belong to the selected club-owned progression system, and reversed ranges are rejected.

  • Group-event booking policy comes from the host club. Required booking

checklists and demo protection now follow the club running the event while the invited member remains correctly linked to their own club.

  • Concurrent member transfers no longer clean up the wrong club. If another

request moves the member first, the stale request stops before removing class enrolments or changing invoices and asks staff to refresh.

  • Uncertain refund outcomes are held for reconciliation. A Stripe timeout or

connection failure no longer restores a stale balance and invites a second refund; staff receive an honest pending-check response instead. Refund amounts must also be exact, safe whole pence values.

Release groundwork

  • Member-portal sign-in is ready for atomic credential handling. Login-code

and magic-link issuance/consumption, trusted-device sign-in and session rotation now use one-time transactional authorities, preventing two concurrent clicks from creating two valid sessions. Sign-in sets the secure cookie directly, trusted devices are tied to the issued login attempt and session lineage, and removing an authorised contact revokes their pending and active credentials in the same transaction. Staff status and bulk-invite views now ignore preserved login history for contacts whose access was removed, while retaining those rows for audit. Trusted-device revoke retries converge on one rotated session after a lost response instead of revoking the retry underneath itself. The authority is applied and verified in persistent local Docker only, not live; production still requires the reviewed migration-before-code release step.

  • Attendance usage recovery is installed locally behind a compatibility bridge. The

transaction records the obligation to adjust class usage alongside the attendance change, so a temporary accounting failure can be retried instead of being lost. It snapshots the billing owner, rejects cross-club identities, and leaves impossible negative counters pending for repair instead of silently clamping them. It is applied and verified in persistent local Docker only and remains in compatibility: 23,066 historical rows are still unclassified, with zero approvals or classifications. It has no live effect, and switching to v2_only is not authorised.

  • The full trial lifecycle is ready for a migration-before-code release.

Public and staff booking, rescheduling, cancellation, pipeline status changes, and add-student conversion now share transaction authorities and expected-state locks. Exact replays return the recorded response; changed payloads and terminal source revival are rejected. Reminder and communication work is durably leased after commit; a delayed send carries an immutable trial-recipient snapshot and remains visibly queued until its worker records the real delivery result. Known failures remain repairable, while ambiguous provider outcomes make repair health non-green and require an explicit operator decision instead of a blind resend. An authorized delayed-send retry reopens its lifecycle and queue records together, and Admin Health shows a stale or failing repair heartbeat. Browser roles lose direct trial-booking and automation-queue writes while retaining tenant-scoped reads; staff cancellation/rescheduling requires the effective trials-management capability and demo guard, and public token routes remain available. Token-scoped, staff, and cron repair paths keep committed moves and cancellations recoverable. The authority is applied and readiness-verified in persistent local Docker only, not live; generated types still require final reconciliation after all reviewed supplemental authorities.

  • Class capacity and waiting-list enrolment now have one rehearsed authority.

Recurring rosters and dated bookable occurrences share an exact lock-and-count contract, including active/new member semantics, scheduled extra classes and ad-hoc pins. Offers are persisted before email or automation work, ambiguous provider outcomes remain visible for explicit resolution, and claiming the final place reserves it until the matching enrolment atomically creates the roster link. Browser roles no longer own queue, roster, occurrence-booking or capacity writes, and direct settings, import, member-profile, portal and cleanup paths are cut over to tenant-bound authorities. Rollback and real two-session disposable proofs now cover final-seat configuration, claim, consume/expiry and promotion races. Email scanners land on GET confirmation pages and only signed POST confirmation mutates. Dispatch evidence has a five-attempt/dead policy, cancellation automation is repairable, historical offered rows are retired safely, and staff/trial enrolment cannot reach the legacy class writer. The authority is applied, double-applied and readiness- verified in persistent local Docker only, not live; the pinned production- clone readiness/rollback gate remains mandatory before release.

  • Waitlist sibling identity and transfer-date rosters are now closed for review.

Guardian email is contact data rather than a child key: known student identity is preferred, anonymous joins use a child fingerprint, legacy duplicates are remediated explicitly, and converting one child does not remove their sibling. Staff cancellation accepts an exact secondary-club access grant behind the same route capability gate. Member transfer now waits for concurrent occurrence booking, transactionally cancels every transfer-date/future source booking with durable cancellation evidence before moving the member, preserves past source history, and rejects target live preassignment. The authorities are applied and verified in persistent local Docker only, not live.

  • Platform subscription checkout has a locally rehearsed concurrency design.

The proposed authority prevents a club and its group from opening competing subscriptions, reuses an exact checkout request safely, and rejects a changed Stripe-session replay. Ambiguous provider calls remain blocked for reconciliation, and a stale webhook cannot overwrite a newer canonical subscription. It remains unapplied and does not alter live checkout until the matching routes and webhook are reviewed and deliberately cut over.

  • Empty-household deletion is guarded against financial-history loss. The

proposed lifecycle authority now refuses deletion while a membership, course-package subscription, or GoCardless relationship remains, rather than allowing parent-table cascades to erase those records.

  • Cross-club member transfers have a rehearsed all-or-nothing authority. The

wired authority moves membership, class, invoice and activity state in one transaction, while refusing provider-backed memberships and shared invoices that require deliberate finance review. The proposal is verified only through rollback/concurrency proofs and persistent local Docker readiness; it is not live.

  • Online enrolment is ready for an atomic migration-before-code release. The

wizard now prepares one immutable server-priced quote before Stripe, binds the PaymentIntent and webhook to that operation, and finalizes token, household, contact, member, consent, formally issued invoice, classes, programme, onboarding and optional trial conversion in one exact-replay transaction. Shared guardian email never selects a student or household; staff must make an explicit trusted choice, and ambiguous household email fails safely. Provider success followed by a local failure enters reconciliation and tells the parent not to pay again. Rollback and disposable two-session proofs cover contention, changed payloads, faults and two siblings sharing one email. A five-minute repair worker reclaims pending or expired post-finalize effects, so a process crash cannot strand welcome automations. The SQL is applied and readiness- verified in persistent local Docker only; the dependency migration must still land immediately before this runtime cutover in production.

  • Enrolment owner alerts are retry-safe. Telegram notifications to club

owners now claim a durable, club-scoped delivery record before sending. A replay cannot notify the same owner twice after an uncertain completion, failed deliveries remain retryable, and stored delivery keys contain only a one-way hash of the Telegram chat identity.

  • Grading result writes and publishing now have a rehearsed causal authority.

Versioned authorities tie each award to its exact booking, prevent publish and result edits from racing, and refuse an undo if the member's level has since changed. The application routes are wired and the authority is applied and readiness-verified in persistent local Docker only; production still requires the data preflight and migration-before-code release.

  • Refund and external-entry races have rehearsed transaction designs. The

refund operation ledger now has its runtime cutover described below, while its SQL remains unapplied outside local Docker. The broader external-event creation authority is wired and applied/readiness-verified in persistent local Docker only; it creates the invoice, canonical entry and paired entry as one unit with database uniqueness under contention.

Safer payments and invoices

  • Opening a one-off payment page no longer creates a Stripe customer. Member

payments keep their receipt email and local identity without generating a new provider customer before any money moves. Long-lived Allsorted subscription customers remain unchanged and are now created with recoverable idempotency.

  • Stripe refund and dispute records now reflect what actually happened. Partial

refunds retain exact whole-pence totals and only the affected subject is updated; a later full refund can complete correctly. A lost dispute reopens the debt rather than pretending the invoice was void, and paid insurance renewals can repair a transient local-write failure on replay.

  • GoCardless settlement is more recoverable. Authoritative read/write failures

stay retryable, missing provider mappings create visible reconciliation work, package access is verified after settlement, and subscription amounts must be exact positive integer pence. Provider success is no longer hidden by a local write that quietly failed.

  • **Refunds, module purchases, vertical add-ons and platform subscription checkout

now have durable authorities ready for release.** Each operation is claimed locally before Stripe is called, replays use the same provider idempotency key, and ambiguous or pending outcomes remain blocked for reconciliation rather than being submitted twice. The proposal SQL is rehearsed and applied locally only, so it must still be applied remotely before these runtime cutovers are deployed.

  • Refund problems are now visible and repairable. A bounded background worker

and owner-operator queue cover pending, provider-unknown and provider-succeeded/ local-repair states. Event cancellations and failed paid waitlist claims use the same authority, so they cannot report a refund that Stripe has not confirmed.

  • Formal invoice issuance has a single frozen contract. The proposed authority

allocates one club-scoped number and freezes the customer, supplier, dates, currency, whole-pence totals and tax facts. Owners can configure supplier billing identity in Settings; issued invoices are immutable, corrections use separately numbered credit notes, and public invoice links are opaque, expiring and revocable.

  • Future payment providers now have a defined integration seam. Small capability

contracts cover connections, one-off payments, recurring collection, refunds, normalised events and reconciliation without adding provider-specific columns to every feature. Stripe and GoCardless adapters and provider-health reporting now exercise that seam; moving every historical call site behind it remains an incremental architecture task. No additional provider was enabled in this pass.