2.24.0
Released
Features
A party member is emailed when a quest owner assigns or reassigns a task to their seat, naming who assigned it, the task and the quest, giving the due date in the member's own timezone when there is one, and linking to the quest. Before this, nobody told them; they found the task by opening the quest, or a week before its deadline through the reminders. The email fires from
POST /api/tasksandPATCH /api/tasks/[id]only when the seat actually changes to a seat: nothing on unassign, nothing when the payload echoes the current seat, nothing for assigning a task to oneself, and nothing for a seat with no account. It goes throughsendNotification()under a new "Task assignment emails" preference with its own profile toggle and unsubscribe copy, behindNEXT_PUBLIC_FEATURE_TASK_ASSIGNMENT_EMAILS(registered in.env.example,env.ts, the Dockerfile, Compose,turbo.jsonand the Playwright web server, so the dev TUI offers it). A failed send never fails the write. (#673)A Party Quest owner is emailed when someone accepts their invitation, naming who joined and the quest and linking to its party panel. Before this, nobody told the owner; they found out that the invitee had arrived — and could now be assigned tasks — by reopening the roster. The email goes through the same path as the other Party Quest email and under the same "Party Quest emails" preference, so an unconfirmed or opted-out owner is never emailed and the unsubscribe link turns off only that category; the profile and unsubscribe copy now say the category covers both. It rides
NEXT_PUBLIC_FEATURE_PARTY_EMAILS, the owner opt-in for party mail. A failed send never fails the acceptance, other members are not told, and neither is the owner about declines or expiries. (#672)
Fixes
Card titles on
/pricing,/quests/public,/quest-packsand/quest-jamsare now<h2>s. The sharedCardTitlealways rendered an<h3>, so a card straight under a page's<h1>skipped a level and failed axe'sheading-orderrule — the one audit holding those four routes at Lighthouse accessibility 98, on the live site as well as in the pull request gate.CardTitleandAlertTitletake anasprop for the heading level (AlertTitlealso acceptsp); their defaults are unchanged, so no other card or alert moves. The issue had attributed/pricing's 98 toAlertTitle's<h5>, but that page renders no alert — the failing node in the report was the plan card's<h3>. (#664)
Technical changes· 10 entries for database, tooling, CI, tests and docs
Database
20260929120000_task_assignment_emails.sqladdsnotification_preferences.task_assignment_emails(default on) and teachesprepare_notification_email(),update_my_notification_preferences()andset_notification_category_by_token()thetask_assignmentscategory, so the unsubscribe link turns off only task assignment emails. It also addstask_assignment_recipient(task_id), a definer function for signed-in callers that resolves a task's seat to the account behind it withtask_deadline_recipient()'s rule minus the owner fallback — no row for an accountless seat, a seat whose guild membership is no longer accepted, an unassigned task, or a quest the caller cannot open — and returns the due date as wall-clock text in the recipient's timezone (preference, else the quest's schedule timezone, else UTC). pgTAP covers the category and the lookup intask_assignment_emails.sql, andrls.sqlchecks the column is function-only and the lookup is signed-in only. (#673)20260928120000_party_invitation_accepted_emails.sqladdsquest_party_invitations.owner_notified_atandclaim_party_join_notification(token), one conditional UPDATE by which the accepting account claims the single owner email for that acceptance — the same shape as the guild claim from #671, so a pressed-again link or two concurrent accepts email the owner once. The claim also returns the quest's owner and title, so the web app never reads the quest through the member's client and the email does not depend on the party-member read policy. No new preference: the email is under the existingpartycategory. pgTAP covers the accept, deny and claim cases inparty_join_owner_emails.sql, andrls.sqlchecks the claim is function-only. (#672)
CI/CD
The pull request Lighthouse job's ~90 performance on
/quests/public,/quest-packsand/challengesis explained rather than chased: those three read tables with GET, and@supabase/postgrest-jsretries a failed GET with a 1 s + 2 s + 4 s backoff before giving up, so against the unresolvable placeholderhttps://example.supabase.coeach document takes ~7.1 s of server-response-time — the only metric on those routes that is not perfect — while/quest-jamsreads through.rpc()(POST, never retried) and fails in milliseconds. The live-site audit measures all three at 100. The retries stay: they ride out a PostgREST schema-cache reload in production, and a gate artifact is not worth an empty page for a real visitor.lighthouse.ymldocuments the cause beside the placeholder and in the pull request comment, and names theCI_SUPABASE_URLrepository variable as the thing that closes the gap. (#664)
Tests
Route tests for the assignment email on both task routes (create with a seat, reassign, hand out an unassigned task, unassign, echoed seat, a refused write, and a throwing send that still answers 201/200), notifier tests for the flag, the self-assignment guard, an empty lookup, unreadable quest and assigner names and failed sends, and template tests for
task_assignedandformatDueLocal(). (#673)CardTitleandAlertTitlehave tests for their default levels and theasprop, and the public quest card test asserts its title is an<h2>. (#664)First deliberate coverage batch toward 75% lines. The zero-covered server-action and query modules under
src/lib— admin role, user and persona actions,admin/users.ts, quest-pack actions and reads, the service-role credit wrappers inai/credits.server.ts, the database half ofachievements/server.tsand the Quest Jam reads — now have behaviour tests (122 of them) that assert what is written and to which client, driven by a shared queued fake insrc/test/fake-supabase.tsrather than another per-file copy of the chainable builder. Measured 73.23 / 86.40 / 83.64 / 73.23 across 2506 tests against develop's 69.13 / 85.89 / 79.94 / 69.13; the ratchet inapps/web/vitest.config.tsmoves to 71 statements / 84 branches / 81 functions / 71 lines with the usual margin. (#679)Second coverage batch: the API route handlers that had no tests at all —
GET/POST /api/quests,GET/PATCH/DELETE /api/quests/[id], the three stall routes,archive,GET /api/admin/feedbackand the SuperAdmin recovery endpoint — plus the single-quest and public reads inquests/queries.ts, the admin half of the Quest Jam actions andgetTasksWithInvestigations. The route tests pin the status for every branch (401 before the body is read, 400 on bad JSON, 422 with field names, 403 vs 500 from the stall functions' "Not authorised", 204 with no body on delete), the owner scoping on every write and the quest rollback when a pack cannot be applied. 88 new tests; measured 75.86 / 87.01 / 85.56 / 75.86 across 2594. The ratchet moves to 74 statements / 85 branches / 83 functions / 74 lines. (#679)Third coverage batch, landing the floors #679 asked for: 76 statements / 87 branches / 85 functions / 76 lines (measured 78.58 / 88.44 / 87.20 / 78.58 across 2694 tests). Covered this time: the model persona, Year Theme, Quest Pack and coach-task choices on quest create and update (with both rollbacks), the task-count read and archive action; the guild roster actions (rename, accountless member, decline, leave, remove, delete) and their feature-flag refusals; the account-lookup party invitation, removing a party member and leaving;
admin/audit.ts, the recap reads, the year-theme progress arithmetic andresolveOwnedYearThemeId; the remaining challenge reads;POST /api/feedback,GET /api/tasks, and the guard and error paths ofPATCH/DELETE /api/tasks/[id]andPATCH /api/admin/feedback/[id]; and both Open Graph image routes, tested withImageResponsestubbed so the data paths are pinned without rasterising. 100 new tests. (#679)
Documentation
docs/notifications.mdgains a "Task assignment" section: the route-handler trigger site and why there is no delivery table, the database-side seat resolution and timezone rule, and what the email deliberately does not cover. (#673)docs/notifications.mdgains a "Telling the owner who accepted" section under Party Quest invitations, covering the shared category, the repeat-accept guard and what the email deliberately does not cover. (#672)