Questify

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/tasks and PATCH /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 through sendNotification() under a new "Task assignment emails" preference with its own profile toggle and unsubscribe copy, behind NEXT_PUBLIC_FEATURE_TASK_ASSIGNMENT_EMAILS (registered in .env.example, env.ts, the Dockerfile, Compose, turbo.json and 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-packs and /quest-jams are now <h2>s. The shared CardTitle always rendered an <h3>, so a card straight under a page's <h1> skipped a level and failed axe's heading-order rule — the one audit holding those four routes at Lighthouse accessibility 98, on the live site as well as in the pull request gate. CardTitle and AlertTitle take an as prop for the heading level (AlertTitle also accepts p); their defaults are unchanged, so no other card or alert moves. The issue had attributed /pricing's 98 to AlertTitle'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.sql adds notification_preferences.task_assignment_emails (default on) and teaches prepare_notification_email(), update_my_notification_preferences() and set_notification_category_by_token() the task_assignments category, so the unsubscribe link turns off only task assignment emails. It also adds task_assignment_recipient(task_id), a definer function for signed-in callers that resolves a task's seat to the account behind it with task_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 in task_assignment_emails.sql, and rls.sql checks the column is function-only and the lookup is signed-in only. (#673)

  • 20260928120000_party_invitation_accepted_emails.sql adds quest_party_invitations.owner_notified_at and claim_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 existing party category. pgTAP covers the accept, deny and claim cases in party_join_owner_emails.sql, and rls.sql checks the claim is function-only. (#672)

CI/CD

  • The pull request Lighthouse job's ~90 performance on /quests/public, /quest-packs and /challenges is explained rather than chased: those three read tables with GET, and @supabase/postgrest-js retries a failed GET with a 1 s + 2 s + 4 s backoff before giving up, so against the unresolvable placeholder https://example.supabase.co each document takes ~7.1 s of server-response-time — the only metric on those routes that is not perfect — while /quest-jams reads 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.yml documents the cause beside the placeholder and in the pull request comment, and names the CI_SUPABASE_URL repository 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_assigned and formatDueLocal(). (#673)

  • CardTitle and AlertTitle have tests for their default levels and the as prop, 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 in ai/credits.server.ts, the database half of achievements/server.ts and 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 in src/test/fake-supabase.ts rather 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 in apps/web/vitest.config.ts moves 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/feedback and the SuperAdmin recovery endpoint — plus the single-quest and public reads in quests/queries.ts, the admin half of the Quest Jam actions and getTasksWithInvestigations. 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 and resolveOwnedYearThemeId; the remaining challenge reads; POST /api/feedback, GET /api/tasks, and the guard and error paths of PATCH/DELETE /api/tasks/[id] and PATCH /api/admin/feedback/[id]; and both Open Graph image routes, tested with ImageResponse stubbed so the data paths are pinned without rasterising. 100 new tests. (#679)

Documentation

  • docs/notifications.md gains 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.md gains 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)