Questify

3.3.0

Released

Features

  • The dashboard opens with Today: this cycle of each daily (or weekday, or weekly-and-active-today) Quest, its tasks as inline status controls, "N of M done", and the streak with its unit — "6 days", "3 weeks" — so the daily loop is one page: open the app, tick the list, watch the streak extend (#859). The controls are the task row's cycle through the same PATCH /api/tasks/:id as Due this week, now shared as TaskStatusCycleButton; after a tick the page refreshes, and because finishing the last task completes the cycle in the database, the refreshed read brings back "Done for today" and the extended streak together. A cycle with no tasks points at its quest; a weekly cycle is labelled with its window; more than five templates link to /quests. On a phone Today is the first thing under the greeting and each tick is a 44px target; from xl it heads the main column, with the rail now spanning two rows and the main column auto-placed so an absent Today leaves no blank row. "Up next" skips every cycle of a template Today shows, so tomorrow's daily cycle no longer occupies the card meant for the next other scheduled quest; "Recently updated" collapses a template's cycles to its latest one, labelled "Cycle N", so a daily Quest is one card rather than a new card every morning — the read pages in batches of three times the preview until it holds enough distinct entries, bounded at five pages, since a template's completed history is "recently updated" too. The rail's "Cycle streaks" now count in the cadence's unit: days, weekdays, weeks, or cycles for any other interval. A failed Today read is an error line, not an empty day. Everything is behind NEXT_PUBLIC_FEATURE_QUEST_RECURRENCE.

  • The AI Quest Coach drafts routines as recurring Quests (#858). Its draft carries a repeat — none, daily, weekdays or weekly, with an optional session length, start time and, for a weekly routine, the day it falls on — in both the prompted JSON and the structured-output schema every provider is sent (COACH_QUEST_DRAFT_SCHEMA, still valid under OpenAI strict mode). The prompt tells the coach to set it when the user describes a routine, habit or practice ("every day", "each morning", "keep up"), to write the tasks as the 2–4 steps of ONE session with no frequency in their titles, to leave a project with an end as none, to ask "once, or a routine you repeat?" only when the answers leave it unclear, and to treat "make it weekdays only" as a change to repeat alone. The parser reads anything missing or unrecognised as none, so a project-shaped draft and every reply from before this behave exactly as they did; drops a session length longer than one cadence or a start that is not a 24-hour time; and strips frequency words ("daily", "every day", "each morning", "weekly"…) from a routine's task titles when a model writes them anyway. The draft preview states the cadence before Continue — "Repeats daily · 2 tasks each day · ~15 min · from 07:00" — and asking for a change redrafts it like any other. On the New Quest page, Continue on a routine swaps the form itself for the recurring-Quest form, pre-filled with the title, goal, context, constraints, cadence, session length, the next occurrence of the start time (on the named day for a weekly routine — "every Monday at 07:00" starts on the next Monday — rolled past the weekend for weekdays, and past a day whose clocks skip that time for daylight saving) and the session's tasks, under a "Recurring Quest" banner with "Create it once instead" — switching between the recurring and one-off forms by hand keeps everything typed, the shared fields carrying across and each form's own fields — reference links and a picked Quest Pack included — coming back when it does; the coach column stays beside it, so "Ask for changes" still works, and a redraft that comes back as a one-off swaps the ordinary form back in. Submitting creates the template — carrying the persona chosen on the coach's chip, so every cycle's investigations run on that model, refused with the usual upgrade message if the plan does not cover it — and like every recurring cycle now, each one counts toward the owner's Year Theme for its year (the banner names this year's) — and lands on its first cycle, as the recurring page does. With recurring Quests switched off the ordinary form is shown, with one line saying the repeat was dropped. The recurring-Quest form gains a Constraints field on /quests/recurring/new too. A routine draft costs the same coach turn as any other.

  • A recurring Quest can start every cycle with the same checklist (#857). The recurring-Quest form gains "Each cycle starts with these tasks": a one-line add row in the style of the task page's quick-add, with details behind a toggle and Enter adding the task rather than submitting the form, then each task editable in place with move up, move down and remove, up to 20; a task still typed in the add row when the form is submitted is saved with the rest rather than dropped. Adding the first task sets "Complete each cycle when" to All tasks are done, and removing the last sets it back, unless the person has already chosen a mode. A template with tasks is created switched off, its tasks are saved, and then it is switched on and minted, so a dashboard read in another tab cannot mint a cycle without the checklist; if the tasks cannot be saved the template is removed and the form says so. On a minted cycle, the owner's Details card has a Repeats row linking to "Edit tasks for future cycles" (/quests/recurring/[templateId]/tasks), which edits the template's list and returns to the cycle it was opened from; the page says that cycles already created, including the next one, keep the tasks they have. Applying an edit to the current cycle, and editing a template's schedule, are not part of this.

  • A recurring Quest can repeat daily, on weekdays, weekly or on a custom cadence (#856). The recurring-Quest form gains a Repeats control — Daily · Weekdays · Weekly · Custom (every N days or weeks, up to 90 days) — in place of the "Cadence: Weekly" statement; Weekly stays the default, so nothing changes for anyone who ignores it. Duration presets follow the cadence (15 min · 30 min · 1 h · All day for a daily or weekdays template, today's set otherwise, each list trimmed to what fits inside one cycle, and the custom-minutes field capped the same way), a preset that no longer fits after a cadence change falls back to the first one that does rather than submitting a value the validator refuses, and the validator caps the duration at one cadence instead of one week. The live summary names the cadence ("Repeats: Weekdays", "Repeats: Every 3 days"), previews the pair the mint will create from the chosen cadence — so a weekdays template started on a Friday shows Friday and Monday — and, for a one-day cadence, lists the next three cycles under "Then" so the shape of the week is visible before submit; the "created immediately" line says today and tomorrow, today and the next weekday, this week and next, or this cycle and the next. The create action writes the cadence and skipped weekdays from that choice instead of a literal '7 days'. The page header no longer calls it a weekly rhythm and the two "Repeat weekly" entry points read "Repeat on a schedule". Editing an existing template's cadence is not part of this: there is no template edit form yet.

  • The AI Quest Coach on a phone asks one question at a time (#854). In the full-screen panel below lg (#853) the interview was still a chat transcript: every turn stayed in the thread, each coach reply was a bubble carrying the preamble and the question, the user's answers were echoed back as bubbles, and the thread pinned its newest line to the bottom of a scroll box — so with the keyboard up the question or the answer field was off screen, and "Question 2 of ~5" sat under the greeting and question 1. The panel now shows the current step: the coach's latest question as a heading, taken from the bolded question of the canned openings (a bold run counts only when it is a whole sentence, so a model bolding one word of its question keeps the question whole) or the final question-mark sentence of a model turn (splitCoachQuestion() in lib/ai/coaching.ts, which also drops a "Let's start:" lead-in), its preamble as muted copy under it, and the answer field directly below. When an answer is sent, that question and answer move behind an "Earlier (N)" disclosure above the step, collapsed by default and collapsed again on every send, so the next question arrives with the field under it and nothing to scroll; opening it lists each answered turn as the question over the answer, with an Edit that puts that answer back in the field, focused; sent, it goes to the coach as a correction that names the question it re-answers (correctionMessage()) rather than as a reply to the question now showing, the row shows the new text marked "edited", and the progress count does not move — a "Keep original" control drops the change; anything already typed for the current question when Edit took the field comes back to it on "Keep original", and once the correction has been answered is offered back beside the field ("You were typing … · Restore / Discard") rather than put in it, since the coach may have moved on; the offer is dropped on the next send, on Start over, and when the correction's reply is the draft (Edit is not offered mid-turn or while a draft is waiting; editing the answer to the current question is just that answer again). While a reply is on its way the typing indicator stands in for the step; once a draft is ready the step is the draft summary with Continue / Start over below it in the field's place, and "Ask for changes" reopens on that summary. A failed send puts the text back in the field — still marked as a correction if it was one — rather than leaving the attempt in the history, where a retry would have gone out as a second answer to the same question (in the transcript at lg too); the substitution notice (#841) sits under the question it is about. At lg and up the transcript is unchanged. Both views are rendered from the same messages and share the header and the answer field, so the window crossing lg mid-interview changes only the middle and the field keeps its focus and its text (CoachingInterview picks the view from its sheet prop).

Technical changes· 13 entries for database, tooling, CI, tests and docs

Database

  • 20261110120000_release_330_review_fixes.sql makes the dashboard's Today section judge a cycle's window by instants (#859, PR #871 review). get_my_today_cycles() converted the cycle's start to local wall time and added scheduled_duration there, while the cycle card and get_quest_cycle_streaks() treat the duration as elapsed time, so across a daylight-saving change the two disagreed by an hour: a 24-hour Toronto cycle from midnight on spring-forward Sunday runs until 01:00 Monday but left Monday's Today at midnight, and on fall-back Sunday a cycle stayed in Monday's Today an hour after it ended. The rule now lives in quest_window_overlaps_local_day(_start, _duration, _timezone, _at), which compares the window's start and end instants with the local day's bounds converted back to timestamptz; the read calls it with now(), and its signature, columns and grant are unchanged. The helper is not a client RPC — revoked from public, anon and authenticated — and is in the generated types. supabase/tests/dashboard_today_cycles.sql gains nine assertions on the 2026 Toronto transitions (both directions, a 23- and a 25-hour day, a day-valued duration) and the two grants, and rls.sql asserts the helper is closed to anon.

  • 20261109120000_dashboard_today_cycles.sql adds the read behind the dashboard's Today section and puts the cadence beside every streak (#859). get_my_today_cycles(_limit) returns one row per recurring Quest cycle whose window overlaps today — today being the template's local day, so tomorrow's pre-minted cycle stays out and a weekly cycle whose window covers today comes in — for the cycles user_can_access_quest() admits (owned, or a party seat whose guild membership is still accepted). Each row carries the quest, its status (a completed cycle stays in, so "Done for today" and the streak it extended can render after the last tick), the template's cadence in days and skipped weekdays, the window, the streak from get_quest_cycle_streaks(), and the tasks as JSON in sort order — only those user_can_write_quest_task() admits, so an owner sees every task and a party member only their own seat's, and no tick the section offers can fail on permission. The limit is clamped to 1–20 and the default is 5. get_quest_cycle_streaks() is dropped and recreated with cadence_days and skip_days so the rail can say "7 days" or "3 weeks" rather than "cycles"; its body is otherwise the live one from 20260910130000, and its grants are re-applied. Both are SECURITY DEFINER, pinned to public, and callable only by authenticated.

  • 20261108140000_recurrence_cycle_year_theme.sql makes every newly minted recurring cycle count toward its owner's Year Theme (#858). mint_quest_recurrence_cycles() sets quests.year_theme_id on each cycle it creates to the owner's theme for the year that cycle starts in, in the template's timezone, when the owner has one and it is not archived; a year without a theme leaves its cycles untagged. A user has at most one theme per year, so the template stores nothing: a routine that runs across New Year counts toward each year's theme in turn, December's cycles toward this year's and January's toward next year's once it is written. This applies to every recurring Quest, not only those the AI Quest Coach drafted; cycles minted before this migration are left as they are, and a cycle keeps its tag if the theme is archived later. The theme is always the template owner's own, which quests_validate_year_theme_owner also checks on the insert. Covered by supabase/tests/quest_recurrence_cycle_year_theme.sql (12 assertions: no theme column on templates, December cycles tagged to the 2026 theme and New Year's Day to 2027's, another owner's cycles tagged only to their own theme, an owner without a theme minting untagged cycles, and an archived theme tagging nothing new while earlier cycles keep theirs).

  • 20261108130000_recurrence_template_persona.sql gives a recurrence template the model persona its cycles run on (#858). quest_recurrence_templates.model_persona_id is a nullable reference to model_personas, cleared if the persona row is deleted, and carries the same plan-tier guard as quests.model_persona_id — guard_quest_model_persona() is attached to the template table as-is — so a signed-in owner cannot put a persona above their plan on a template, an edit that leaves the persona alone is grandfathered, and trusted callers pass. mint_quest_recurrence_cycles() copies the persona into each cycle it creates when the persona is active and the OWNER's plan, read from their profile, still covers it, and mints the cycle with no persona otherwise: a Pro template keeps minting after a downgrade, its new cycles on the default model, instead of the quest guard's 42501 aborting the mint for every template the owner has, and the pg_cron path and ensure_quest_recurrence_cycles() decide the same way. Templates without a persona, which is every template created before this, mint as before. Covered by supabase/tests/quest_recurrence_template_persona.sql (15 assertions: the column and foreign key, the guard on insert and update, the copy from both the signed-in and the cron mint, a downgrade degrading only later cycles without breaking the mint, and a retired persona not copied) and two assertions in rls.sql.

  • 20261108120000_recurrence_template_constraints.sql gives a recurrence template a constraints column — time limits, budget, scope, things to avoid — that every newly minted cycle copies into quests.constraints, beside the goal and context it already copied (#858). It is nullable text of up to 2000 characters, the same bound as description and as the quest form's constraints, and rides on the template's existing owner policies and table grant. mint_quest_recurrence_cycles() — the #857 body otherwise unchanged — adds it to the cycle insert; a template without constraints, which is every template created before this, mints exactly as before, and changing a template's constraints leaves cycles already minted alone. Covered by supabase/tests/quest_recurrence_template_constraints.sql (eight assertions: the column, the 2000-character check, the copy into each cycle, a null staying null, and minted cycles untouched by a later edit) and four assertions in rls.sql (the owner can set it; another user can neither read nor change it).

  • 20261107120000_recurrence_template_tasks.sql gives a recurrence template an ordered task list that every newly minted cycle starts with (#857). quest_recurrence_template_tasks holds title (1–200 characters), optional details (up to 2000) and a zero-based sort_order, cascades with its template, and is readable only by the template's owner; clients get no insert, update or delete grant. The one write path is set_quest_recurrence_template_tasks(_template_id, _tasks jsonb), a security definer RPC that locks the template, refuses anyone but its owner with 42501 (a missing template reads the same), trims titles and details, rejects more than 20 tasks or a blank or over-long title with 22023, and replaces the whole list in one statement so a reorder never half-saves. mint_quest_recurrence_cycles() — the #856 body otherwise unchanged — copies the list into tasks for each cycle it creates, keeping the positions, behind the same returning id that gates the party seeding, so re-running the mint for a cycle that exists adds nothing. The copies are ordinary origin = 'user' tasks the owner can reword, remove or add to, and nothing done to a cycle reaches the template. An edit therefore reaches cycles minted after it only — and since the next cycle is minted ahead of time, it keeps the list it was minted with. Templates with no task rows, which is every template created before this, keep minting task-less cycles. Covered by supabase/tests/quest_recurrence_template_tasks.sql (38 assertions: the copy, the order and origin, the re-mint, edits reaching only later cycles, cycle edits leaving the template alone, the cap and every refusal, owner-only, clearing, and deletion) and nine assertions in rls.sql.

  • 20261106120000_recurrence_cadences.sql lets a recurrence template repeat daily, on weekdays, weekly or every N days (#856). cadence was always a free interval that only the app pinned to '7 days'; the one shape the schema could not hold was "every day except the weekend", which is now a one-day cadence plus quest_recurrence_templates.skip_days smallint[] (weekdays 0 = Sunday … 6 = Saturday on which no cycle is minted), checked to 0–6, fewer than seven of them, and empty unless cadence = interval '1 day'. mint_quest_recurrence_cycles(_owner_id, _now) — the body behind ensure_quest_recurrence_cycles() and the reminder cron — walks cycle indices from the current one, passes over a skipped weekday, and stops once two eligible indices have been considered (capped at 14 indices), so on a Friday a weekdays template mints Friday and Monday and a Mondays-only one mints the next two Mondays; for a template with no skip_days the walk is exactly the old (current, next) pair, so existing weekly templates behave as before and nothing is backfilled. An eligible cycle whose window has already closed is still passed over without reaching further ahead, as before. get_quest_cycle_streaks() needs no change: it ranks minted cycles by index, so a skipped weekend has no row and cannot break a weekday streak. Covered by supabase/tests/quest_recurrence_cadences.sql (daily, weekdays, six skipped days, every three days, daily across the November DST change, the weekly regression, the three constraints and the weekday streak) and four assertions in rls.sql.

Tests

  • supabase/tests/dashboard_today_cycles.sql (25 assertions) pins get_my_today_cycles() and the recreated get_quest_cycle_streaks() (#859): grants, the mint and the read agreeing on today's cycle with tomorrow's absent, a closed, an archived and a soft-deleted cycle out, a Tokyo template's last hour counting whatever the date is in Toronto, tasks as JSON in template order, the owner seeing every party task and a member only their seat's, a guild-backed seat seeing the cycle while its guild membership is accepted and nothing once departed, a completed cycle kept with its status, cadence, skipped weekdays and streak, the empty task list, the limit clamp, a stranger seeing nothing, and the streak list's new columns. rls.sql adds the two anonymous-caller refusals. On the app side today-cycles.test.ts, streaks.test.ts, today-cycles.test.tsx, task-status-cycle-button.test.tsx and additions to body.test.ts, queries.test.ts, quest-card.test.tsx, quest-cycle-streaks.test.tsx and the dashboard page.test.tsx cover the read's parsing and overflow flag, the unit words, the section's states and ticks, the shared button, the template ids flowing into Up next, the grouped preview's paging (fills from a second page, stops when full, gives up at the page cap), the "Cycle N" line, a completed cycle's ticks disabled, and Today rendered before the rail and never read with the flag off.

  • coaching.test.ts covers routine drafts on fixtures (#858): three routine payloads with "daily", "every day", "each morning" and "weekly" left in their task titles keep their cadence and come out with clean session titles and untouched details; two project payloads — one with a stray session length and a "daily" mid-title — stay none with their titles as written; a missing, malformed or unknown repeat reads as none; session lengths are bounded to one cadence and starts to 24-hour times; a title that is only a frequency word is kept; the preview wording for each cadence; a weekly routine's day read from "monday", "Mondays", "tue" or 0–6, dropped on any other cadence, named in the preview ("Repeats weekly on Mondays") and stripped from task titles ("every Monday"); the prompt's routine, ambiguity and change-request guidance; the prompt's own example payload parsing as a one-off; and the schema pinned to the parser, including repeat's keys and cadences. coaching-interview.test.tsx covers the cadence line and per-cycle task count in the preview, the recurring footnote, a one-off draft without either, and "Change it to weekdays" redrafting the line. coaching-quest-creator.test.tsx covers the form swap, the bounded task copy, "Create it once instead" and back, a one-off redraft swapping the ordinary form back, recurring Quests off, and a project draft unchanged. quest-recurrence-form.test.tsx, validators.test.ts, actions.test.ts and preview.test.ts cover the pre-filled form, a session length no preset offers, the start time's next occurrence (on a named weekday, at the default hour when no time was given, past a Friday's weekend for weekdays, past the spring-forward gap in America/New_York, on a fall-back day, and past the repeated fall-back hour once its first occurrence is over — the occurrence the server stores — with a sweep checking every offered start is stored in the future), the stacked layout, the constraints field and its storage, and the persona field — stored when chosen, refused above the plan or when unknown, none when empty or with personas off, and a tier-guard refusal reported as a persona problem rather than a guild one; coaching-quest-creator.test.tsx checks the chip's persona reaches the recurring form and follows a new pick, and that edits survive a round trip between the recurring and one-off forms while a new draft starts both over; form-carryover.test.ts covers reading each form's submitted values, including the one-off schedule and reference links as QuestForm defaults, the creator test keeps links and a picked Quest Pack across a round trip and names this year's theme on the recurring form whatever the toggle says (and none without one), and the recurring form takes a carried custom cadence, completion mode and guild. e2e/quest-coach-recurring.spec.ts drives a routine draft through Continue to the pre-filled recurring form and a one-off redraft back to the ordinary form.

  • supabase/tests/tasks_quest_list.sql no longer fails intermittently on its two "newest first" assertions. Its fixture pinned each quest's updated_at with a plain update, but quests_set_updated_at re-stamps now() on every update, so the four quests tied and their order came down to physical row placement; the pin now runs with that trigger disabled for the one statement (#857).

  • validators.test.ts, actions.test.ts and queries.test.ts under lib/quest-recurrences/ cover the task list (#857): the hidden JSON field parsed, trimmed and capped at 20, unreadable JSON reported rather than dropped, the return path accepted only as /quests/<uuid>, the create order (inactive insert, tasks, activate, mint) with the template removed when the tasks or the activation fail, and the edit action's save, clear, cap, refused-template and flag-off paths. recurrence-task-editor.test.tsx, recurrence-tasks-form.test.tsx, quest-recurrence-form.test.tsx, quest-details-card.test.tsx, the new edit page's test and quests/[id]/page.render.test.tsx cover adding with Enter inside a form, details, reorder, edit, remove, the 20-task stop, the completion-mode default and a chosen mode being kept, the future-only copy, and the link shown to the owner only. e2e/quest-recurrence-tasks.spec.ts creates a daily Quest with two tasks, checks both minted cycles start with them, edits the list from the cycle and checks the cycle keeps its tasks while the template changes. The apps/web statement and line floors move up to 94.7, measured at 95.01 / 91.85 / 95.03 / 95.01 across 4940 tests.

  • validators.test.ts, actions.test.ts and preview.test.ts under lib/quest-recurrences/ pin the repeat choices (#856): the interval and skip_days each one stores, cadence_days read only for Custom and bounded to whole days from 1 to 90, the duration capped at one cadence with a daily-specific message, and a preview that mirrors the mint's walk — today and tomorrow plus the next three for Daily, Friday and Monday then Tuesday to Thursday for Weekdays, two Mondays for six skipped days, a closed window dropped without a third cycle pulled forward, three-day spacing for Custom, the local hour kept across the November DST change, and nothing shown for a duration longer than the cadence. quest-recurrence-form.test.tsx covers the control's options and name, the daily presets with the selected one kept when it fits and replaced when it does not, the weekend skipped in the preview, the custom-minutes cap, the days/weeks custom cadence submitted as a day count, and the empty custom cadence previewing nothing. e2e/quests.spec.ts asserts the Repeats control defaults to Weekly before the existing flow runs.

  • coaching.test.ts pins splitCoachQuestion() on the two canned openings (the bolded question promoted, the theme title's bold left in the preamble, the lead-in dropped), a model bolding part of its question or a whole sentence, a model turn (the last sentence), decimals and times that must not split a sentence, abbreviations inside the question ("6 a.m. work?", "e.g.", "U.S.") and before a capitalised word, a lower-case word after a full stop, a colon mid-sentence, a question-only reply, a reply with no question, and the empty string (#854). coaching-interview.test.tsx covers the one-question view in panel mode: the heading over its preamble with nothing earlier, the next question replacing the previous one with the answered turn behind "Earlier (1)", the disclosure collapsing again on send, Edit refilling and focusing the field, a changed earlier answer posted as a correction naming its question with the row marked edited and the progress unchanged, Keep original clearing it and restoring a half-typed answer, the same draft offered back with Restore / Discard once the correction is answered and dropped on the next send or when the reply is the draft, Start over forgetting it, a failed send and a failed correction both returned to the field with the retry posted as one turn, the typing indicator in the step with Edit disabled mid-turn, the draft taking the step with Continue focused and no Edit, the draft kept after Continue for "Ask for changes", the substitution notice under the question, the transcript kept at lg, and the field surviving the switch across lg. e2e/quest-coach.mobile.spec.ts asserts the current question's heading, rather than the newest bubble, is inside the visual viewport with the keyboard up, and adds the step flow — only the current question showing, the answered turn filed and reopened, Edit sending a correction that names its question, the draft as the step — and a check that the transcript stays at lg. The apps/web coverage floors move up to 94.6 / 91.3 / 94.8 / 94.6 (statements / branches / functions / lines), measured at 94.94 / 91.83 / 95.13 / 94.94 across 4862 tests.