Questify

2.0.0

Released

Features

  • #267 — Test infrastructure (Playwright E2E + Vitest unit tests). Set up the full testing infrastructure for Questify 2.0. Added e2e/helpers/auth.ts with Supabase Admin API helpers (createTestUser, deleteTestUser, loginAs, signUpViaUI) that provision/teardown real test users using E2E_SUPABASE_SERVICE_ROLE_KEY; emails use the .invalid TLD so no real mail is ever sent. Added e2e/fixtures.ts with custom Playwright fixtures (testUser, authenticatedPage) that auto-skip when the service role key is absent, keeping the smoke run clean in environments without live Supabase creds. Added e2e/global-setup.ts wired into playwright.config.ts via globalSetup — logs whether live or smoke mode is active and validates required env vars before the suite starts. Expanded e2e/auth.spec.ts with live login and signup round-trip tests (gated on E2E_SUPABASE_SERVICE_ROLE_KEY) alongside login-form validation smoke tests. Added e2e/quests.spec.ts covering create-quest, add-task, and complete-task flows for authenticated users. Added src/lib/auth/validators.edge.test.ts with Vitest boundary-condition tests: exact-minimum password length, one-below-minimum rejection, name 120-char limit, whitespace-only name rejection, email trimming, and case-sensitive confirm-password mismatch. Added src/lib/tasks/types.edge.test.ts with edge-case coverage for nextTaskStatus (blocked→done shortcut), sortTasksByOrder (tie-breaking by createdAt, immutability), reorderTaskIds (at-boundary no-ops, unknown id), and applyTaskOrder (omitted ids excluded, empty id list). Added test:coverage script to apps/web/package.json and @vitest/coverage-v8 devDependency (run pnpm install to regenerate the lockfile). Updated ci.yml: the test job now runs pnpm --filter web test:coverage to generate lcov/html/json-summary artifacts; a new coverage-comment job uses davelosert/vitest-coverage-report-action@v2 to post a coverage summary comment on every PR from the same repo; ci-pass updated to include coverage-comment in its needs graph (skipped is treated as OK for push events and forks).

  • #266 — Location-Based Task Check-ins (proximity-triggered check-in prompts). Allows tasks to have an optional physical location anchor (name + WGS-84 lat/lng + configurable radius, default 100 m). Gated behind NEXT_PUBLIC_FEATURE_LOCATION_CHECKINS. New Supabase migration adds location_name, location_lat, location_lng, location_radius_m columns to tasks (with check constraints enforcing lat/lng co-presence and a positive radius), and a user_location_consent table (one row per user, RLS-restricted to the owning user) that persists the user's explicit opt-in before any geolocation API call is made. Updated apps/web/src/types/supabase.ts with the new task columns and the full user_location_consent table shape. Added apps/web/src/lib/location/: types.ts (TaskLocation, GeolocationState, ConsentState, ProximityHit, TaskWithLocation), haversine.ts (great-circle distance in metres), use-geolocation.ts (watchPosition wrapper with idle/pending/granted/denied/unavailable state machine, cleans up on unmount), use-proximity.ts (computes nearby tasks from current position using haversine, fires onProximityEnter on leading edge only — no repeat callbacks per task per entry), use-location-consent.ts (loads/upserts consent from user_location_consent via the browser Supabase client; exposes grantConsent() and revokeConsent()). Added four new components under apps/web/src/components/location/: TaskLocationPicker (expandable inline form with place-name input, lat/lng fields, and a radius slider from 25 m to 5 km), LocationConsentDialog (opt-in prompt rendered before the geolocation API is ever called; privacy copy notes position is never stored), LocationCheckinBanner (proximity alert card with "Check in" — sets task to in_progress — and "Dismiss" per-session), NearMeFilterButton (toggle with pulse animation when active). Task manager updates: locationCheckinsEnabled + userId props forwarded from both quest page server components; useGeolocation / useProximity / useLocationConsent wired to location state; Near Me toggle in the header gates on consent and requests geolocation; LocationConsentDialog shown inline when consent is unknown; check-in banners rendered for all undismissed proximity hits; "Near me" active state filters displayed tasks to those whose anchor falls within the user's radius; TaskLocationPicker embedded in both create and edit forms; MapPin badge shown on task cards that have a location. Updated QuestTask with a location: TaskLocation | null field and mapTaskRow to derive it from the new DB columns. Extended createTaskPayloadSchema and rawUpdateTaskPayloadSchema in server.ts with locationName, locationLat, locationLng, locationRadiusM Zod fields; UpdateTaskPayload type extended to match; parseUpdateTaskPayload passes all four through; POST /api/tasks and PATCH /api/tasks/[id] persist the fields to Supabase. Updated test fixtures to add location: null. Verified with tsc --noEmit (0 errors) and eslint --max-warnings 0 (0 warnings).

  • #265 — Annual 'Well Played' Recap (year-end summary). Added a personalised Spotify-Wrapped-style year-end recap accessible from the profile page year-round. New Supabase migration adds well_played_recaps (one cached row per user/year, unique constraint) and a compute_well_played_recap(p_user_id, p_year) PL/pgSQL function that aggregates quests completed, tasks marked done, AI investigations run, total active days (distinct calendar days with a completed task), longest consecutive-day streak, achievements unlocked, top quest-pack category, and favourite quest pack — then upserts the result so subsequent calls refresh rather than duplicate. The function is SECURITY DEFINER and grants EXECUTE only to the authenticated role; RLS policies restrict reads and writes to the owning user. Added apps/web/src/lib/recap/types.ts (WellPlayedRecap, WellPlayedRecapRow, mapRecapRow()) and apps/web/src/lib/recap/queries.ts (computeRecap(), getRecap(), listRecapYears()), following the same (supabase as any).rpc(...) cast pattern used elsewhere for functions not yet reflected in the generated types. Added apps/web/src/components/recap/well-played-card.tsx: a celebratory WellPlayedCard component with a violet-fuchsia-pink gradient hero banner, machine-generated narrative sentence, a 6-stat grid (quests, tasks, investigations, active days, streak, badges), a highlights row for top category and favourite pack, and a Share button using the Web Share API with a clipboard-copy fallback. Added /profile/recap/[year] server page that computes stats on load (with a Refresh button), sets Open Graph metadata, and shows the computed-at timestamp. Added /api/recap/[year]/og edge route using Next.js ImageResponse to produce a 1200×630 OG card for social sharing. The profile page now includes a "Well Played recaps" section at the bottom with link buttons for each year that has a cached recap plus always the current year, so users can generate their first recap immediately. Verified with tsc --noEmit (0 errors) and eslint --max-warnings 0 (0 warnings).

  • #264 — Admin panel (users, roles, privileges, quest packs, personas, audit log). Ported the full admin surface to Next.js. A shared app/admin/layout.tsx provides sidebar navigation (desktop) and a top-bar (mobile) linking all eight admin sections, with a server-side isAdminOrSuperAdmin() gate that returns notFound() for authenticated non-admins; unauthenticated visitors are already bounced to /auth/login by the existing PROTECTED_PATH_PREFIXES middleware. Added /admin (dashboard with user/quest/pack/role stat cards and a recent-activity feed from the audit log), /admin/users (searchable user list with inline role assign/revoke via UserAdminPanel), /admin/roles (create/rename/delete non-system roles and manage per-role privilege assignments via RoleAdminPanel, SuperAdmin-only), /admin/privileges (read-only privilege catalog), /admin/personas (create/edit/toggle model personas via PersonaAdminPanel, requires model_personas.write privilege), and /admin/audit-log (paginated, multi-filter append-only audit log — action, target type, actor ID, date range). The existing /admin/quest-packs and /admin/quest-jams pages are linked into the new shared nav without changes. Added lib/admin/{users,roles,personas,audit}.ts for typed Supabase queries and lib/admin/{user,role,persona}-actions.ts for 'use server' mutations; every mutating action writes an audit row via the service-role client (bypassing RLS) and calls revalidatePath. Added POST /api/admin/recover-superadmin: a token-gated (Bearer ADMIN_RECOVERY_TOKEN, min 32 chars) bootstrap endpoint to grant SuperAdmin to any existing user — returns 501 when the env var is unset, idempotent via upsert, records a superadmin.recover audit entry. Added isAdminPath() helper to lib/auth/redirects.ts. Extended the server env schema in lib/env.ts with the optional ADMIN_RECOVERY_TOKEN field. Verified with tsc --noEmit (0 errors) and eslint --max-warnings 0 (0 warnings).

  • #263 — Skill Track Quest Packs (progressive, stage-unlocked learning paths). Added a new skill_track Quest Pack type alongside the existing template type. New Supabase migration adds a type column (template | skill_track) to quest_packs, creates skill_track_stages (ordered stages per track with unique quest_pack_id, stage_number constraint), skill_track_stage_tasks (task templates per stage), and user_skill_track_progress (one row per user/track recording current_stage, started_at, and completed_at) — all with RLS policies matching the existing pack security model. Added advance_skill_track_stage(_quest_pack_id uuid) PL/pgSQL function (security definer) that atomically increments the caller's current_stage or marks the track complete when the final stage is reached. Added lib/quests/skill-tracks.ts with listSkillTracks, getSkillTrackBySlug, getUserTrackProgress, getActiveUserTrackProgress, startSkillTrack, advanceSkillTrackStage, getUserSkillTracksWithProgress, and resumeTrackHref helpers. Extended lib/quests/packs.ts to expose the type field on QuestPack. Updated types/supabase.ts with QuestPackType, all three new tables, the advance_skill_track_stage RPC, and the quest_pack_type enum entry. Quest Pack browser now renders skill track cards with a distinct tinted border/background, a Layers badge, and a "View Track" CTA in place of the template "Start this Quest" flow. Added /quest-packs/[slug]/track detail page showing the ordered stage map with a progress bar, stage-unlock state (completed ✓ / current / locked), per-stage collapsible task lists, and a Resume Track button for users mid-track. SkillTrackStageMap component handles the locked/current/done stage states with appropriate CTAs per state. Admin panel extended with a type toggle to switch between creating a Quest Pack or a Skill Track; the skill track form supports dynamic stage addition with per-stage title, description, and tasks (one per line, title | description format). Added skill-track-actions.ts server actions (createSkillTrackAction, updateSkillTrackMetaAction). SkillTrackResumeWidget on the dashboard surfaces active tracks with a per-track progress bar and "Resume Stage N" shortcut, and completed tracks with a revisit link. Seeded two starter skill tracks: "Productivity Fundamentals" (capture → prioritise daily → reflect and improve, 4 tasks each) and "Creative Projects" (spark → make → share, 4 tasks each). Verified with tsc --noEmit (0 errors).

  • #262 — Stall Detection (healthy loop check-ins for stuck quests). Detects active quests that have gone quiet — no task updates or AI investigations for N days — and surfaces a gentle, supportive check-in prompt. New Supabase migration adds quest_stall_notifications (one active record per quest, unique constraint) and quest_stall_events (append-only analytics log of stall_detected, checked_in, dismissed, snoozed, resolved events). Added check_stalled_quests(threshold_days int = 7) intended for a daily pg_cron job: it upserts notifications for idle active quests (last activity = latest of task updated_at and investigation created_at) and auto-resolves notifications whose quests have since seen new activity. Added dismiss_stall_notification(), snooze_stall_notification(days 1–30), log_stall_check_in(note text), and get_active_stall_notifications() DB functions with ownership enforcement and appropriate RLS policies. Gated behind NEXT_PUBLIC_FEATURE_STALL_DETECTION feature flag following the existing flag pattern. Added StallNotification type and getStallNotificationsForUser() query helper; extended QuestSummary with an optional stall_notification field; updated listQuests() to accept a pre-fetched stall map so no extra round-trip is needed. Dashboard fetches stall notifications in parallel with other data and passes them into listQuests. Added three API routes (POST /api/quests/[id]/stall/check-in, .../dismiss, .../snooze) each delegating to their DB function with server-side auth. StallBanner component renders an amber banner inside the quest card: shows idle-day count, optimistically reflects saved check-in notes, and offers an inline snooze dropdown (3 / 7 / 14 days) and a dismiss × button. CheckInModal is a full-page-overlay dialog with a resizable textarea (max 2000 chars), saves the note back via the check-in route, and passes it to the banner for immediate display without a reload. QuestCard accepts a showStallBanner prop and renders the banner when the flag is on and stall data is present. All components built with available UI primitives and Tailwind only — no additional dependencies. Verified with tsc --noEmit (0 errors).

  • #261 — AI Coaching Interview Mode (conversational quest planning). Added a guided AI interview flow for quest creation, gated behind NEXT_PUBLIC_FEATURE_COACHING_MODE. When the flag is on, the /quests/new page shows a "Start with AI Coach" card above the standard form. Clicking it opens a chat interface where the AI coach asks 3–5 focused questions (goal, context, constraints, tasks, title) one at a time, then emits a structured JSON quest draft that auto-fills the form fields (title, goal, context, constraints) and surfaces a suggested-tasks panel for manual reference. After pre-fill the user reviews and edits the form normally before saving — no new database columns or migrations required. Added src/lib/ai/coaching.ts with the coaching system prompt (COACHING_SYSTEM_PROMPT), CoachMessage / CoachQuestDraft types, buildCoachMessages(), and parseQuestDraft() (extracts the JSON block the AI embeds in its closing turn). Added POST /api/coach/interview route: accepts the full conversation history (capped at 20 turns), enforces auth and feature-flag gates, and calls Anthropic → OpenAI → DeepSeek in priority order using the same fetch-based helpers as the narrative route. Added three client components: coaching-interview.tsx (chat UI with message bubbles, bold-markdown renderer, typing indicator, progress bar, auto-scroll, Enter-to-send), coaching-entry.tsx ("Start with AI Coach" gradient card), and coaching-quest-creator.tsx (composes entry + QuestForm, remounts the form with AI-extracted defaultValues when the draft arrives). Updated quests/new/page.tsx to render CoachingQuestCreator instead of QuestForm directly, passing the coachingEnabled flag. Updated env.ts and .env.example with the new flag and JSDoc. Verified with tsc --noEmit (0 errors).

  • #260 — Quest Narrative Mode (AI-written completion story). Added an AI-generated epic/heroic completion story for finished quests, gated behind NEXT_PUBLIC_FEATURE_NARRATIVE_MODE. New Supabase migration adds narrative TEXT, narrative_model_type TEXT, and narrative_generated_at TIMESTAMPTZ columns to quests. POST /api/quests/[id]/narrative is owner-only and completed-quest-only: it loads all tasks with their investigation summaries, resolves the quest's model persona (falls back to the default active persona), and generates narrative prose inline via the quest's selected AI provider (Anthropic / OpenAI / DeepSeek) using the same fetch-based approach as the background worker. The new buildQuestNarrativePrompt() helper in src/lib/ai/narrative.ts assembles an epic/heroic storytelling prompt from quest goal, context, completed vs. incomplete tasks, and per-task investigation summaries, targeting 250–500 words of flowing prose with no bullet points. Results are persisted directly to quests.narrative and narrative_generated_at. The NarrativePanel client component renders below the task section on completed quest detail pages: it shows a Generate Story / Regenerate Story button (with spinner), an animated skeleton while generating, a styled amber-tinted prose card with per-paragraph rendering, generation date, and a Copy link share button. Non-owners see the narrative read-only with an empty state if none has been generated yet. Updated src/types/supabase.ts, env.ts, env.test.ts, and .env.example. Verified with tsc --noEmit (0 errors).

  • #259 — Random Quest Challenge (Balatro-style daily/weekly challenges). Added seed-backed Random Quest Challenges with quest_challenge_templates, quest_challenge_tasks, and quest_challenge_attempts, plus daily/weekly cadence/source enums, RLS policies, admin RBAC privileges, immutable attempt snapshots, and a quest-completion sync trigger that derives completion timestamps and XP bonuses from linked quests. Seeded an initial curated pool of short daily and weekly challenge prompts with 1–3 task templates. Added typed challenge period selection helpers, deterministic daily/weekly rotation, challenge query/action helpers, and an opt-in server action that creates an active quest prefilled with challenge prompt, constraints, reward context, and copied tasks while preventing duplicate attempts for the same period. Dashboard now surfaces daily and weekly challenge cards, /challenges shows open challenges plus attempt history, quest detail pages show linked challenge reward/completion state, and challenge completion unlocks a new Challenge Runner badge through the existing achievement pipeline. Updated Supabase types and focused period-selection tests. Verified with pnpm --filter web test -- src/lib/quest-challenges/queries.test.ts src/lib/achievements/server.test.ts, pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web build with local public Supabase env placeholders after allowing Google Fonts network access, git diff --check, and browser smoke checks for /, /challenges, and /dashboard auth redirects.

  • #258 — Party Quests (collaborative multi-user quests). Added Party Quest membership with quest_party_members, invitation audit rows, owner/member roles, owner backfill for existing quests, and RLS helpers so party members can read/write shared quest, task, and investigation rows while public viewers remain read-only. Added optional task assignees with database validation that assignees belong to the quest party, member progress RPCs, and party completion syncing for assigned-task or all-task completion modes. Dashboard quest listing now includes invited/member quests with party metadata and assigned-task progress. Quest detail pages now show a Party Quest panel for inviting users by email/display name, removing members, leaving a party, and viewing who has completed assigned work. Task management now supports assigning tasks to party members during create/edit, and task APIs accept shared-party access for list/create/update/reorder/delete/investigation flows. Updated Supabase/web/worker types and focused task validation/UI tests. Verified with pnpm --filter web typecheck, pnpm --filter web test -- src/lib/tasks/server.test.ts src/lib/tasks/types.test.ts src/components/tasks/task-manager.test.tsx, pnpm --filter web lint, pnpm --filter worker typecheck, and pnpm --filter worker lint; local Supabase migration application could not run because the supabase_db_questify container was not running.

  • #257 — Quest Jams (time-boxed sprint challenges). Added admin-created Quest Jams with quest_jams and quest_jam_entries tables, RLS policies, RBAC privileges, completion syncing from immutable quest completion timestamps, and RPCs for active/upcoming jam summaries, detail views, and leaderboard rankings by elapsed time from jam start. Added typed Quest Jam query/action/validation helpers, Supabase type coverage, and focused validator tests. Shipped /quest-jams browse, /quest-jams/[id] detail with join flow and leaderboard, and /admin/quest-jams management for creating, updating, activating, and pausing jams. Users can join with an existing quest or create a new sprint quest in-place; completed linked quests count only when completion falls inside the jam window. Added dashboard/home navigation and jam revalidation after quest updates. Verified with pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web test, git diff --check, and a local browser smoke check for /quest-jams with local Supabase env placeholders.

  • #256 — Year Theme guiding principle layer. Added user-owned Year Themes as a soft intention layer above quests. New Supabase schema adds year_themes with one theme per user/year, muted hex accent colors, archive metadata, owner-only RLS, quests.year_theme_id, and a trigger that prevents quests from referencing another user's theme. Added typed Year Theme validators, queries, and server actions, plus route-handler/server-action support so quest create/edit flows can optionally tag quests to the caller's own theme. The profile page now lets users create or edit the current calendar year's theme and review archived previous-year themes; the dashboard shows the active theme as a restrained banner with tagged-quest completion progress; quest cards and owner quest detail pages show the associated theme accent. Updated Supabase types, API/schema docs, quest validation coverage, and pgTAP RLS coverage for theme ownership and cross-user tagging denial. Verified with pnpm --filter web test, pnpm --filter web typecheck, and pnpm --filter web lint; pnpm supabase:test could not run locally because the Supabase Postgres container was not running on 127.0.0.1:54322.

  • #255 — Achievements & collectible badges. Added the v2 achievement system with a new Supabase achievement_rarity enum, user-owned achievements table, quests.quest_pack_id provenance for pack-completion unlocks, immutable quests.completed_at timestamps for streak derivation, RLS policies for private owner access plus public badge display on public quest owners, and pgTAP coverage for achievement visibility/write rules. Added 14 starter achievement definitions spanning first quest, 5/10/25/50 quest milestones, 3/7/30-day streaks, Quest Pack completion, public quest completion, all-tasks-complete, AI Scout, Party Quest, and Quest Jam winner. Server-side unlock evaluation now runs after quest completion, task completion, bulk task completion, API-created completed quests, and the first AI task investigation, returning newly unlocked achievements to the UI without failing the primary mutation if badge evaluation errors. Achievement writes use the service-role client rather than authenticated RLS inserts, so clients cannot mint arbitrary badges. Added reusable achievement badge/icon/toast components, in-app unlock notifications in the task manager, a new authenticated /profile badge gallery with locked mystery silhouettes, dashboard profile navigation, and compact badge strips on public owner cards and shared quest headers. Verified with pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web test, pnpm --filter web build, and browser smoke checks for /quests/public plus /profile auth redirect; pnpm supabase:test could not run locally because the Supabase Postgres container was not running on 127.0.0.1:54322.

  • #254 — Model persona selector (AI model choice per quest). Ported the model persona selection experience into the Next.js 2.0 quest flow behind NEXT_PUBLIC_FEATURE_MODEL_PERSONAS. Added quests.model_persona_id with a foreign key to the admin-managed model_personas catalog, backfilled existing quests to the recommended active persona, indexed persona joins, and updated get_my_active_quests() to return the selected persona id. The create/edit quest form now shows a persona picker modal with name, tagline, speed/cost/context badges, and recommended flags; selections persist by persona key and emit the model_persona_selected analytics event through the app's browser analytics bridge. Dashboard quest cards can show the selected persona inline, and task investigations now use the quest's selected persona by default while still accepting an explicit modelPersonaKey for API callers. Updated Supabase types, env parsing, API/schema docs, and focused Vitest coverage for validation, env parsing, the picker UI, and analytics dispatch. Verified with pnpm --filter web test, pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web build after allowing the build to fetch Google Fonts, git diff --check, and a browser smoke check that /quests/new redirects cleanly through auth with the feature flag enabled.

  • #253 — Public quests (shareable quest pages + public browse). Added the public quest discovery route at /quests/public, backed by new public quest query helpers that return public quest cards with owner attribution, task counts, completion counts, and status badges while preserving the existing owner-only dashboard query. Updated /quests/[id] so public quests render for unauthenticated visitors as read-only shared pages, with owner attribution, public-share actions in the header, and Open Graph/Twitter metadata derived from the quest title, goal/context, canonical URL, and attached images. Replaced the previous one-click share affordance with a share dialog that guards private quests, shows a copyable public link, supports native sharing when available, and renders a QR code via the new qrcode dependency. Added a narrow public-owner attribution RPC that returns only id, display_name, and avatar_url for owners of non-deleted public quests, plus pgTAP coverage confirming anonymous users cannot read full profile rows. Revalidated /quests/public after create/update/archive/delete quest actions so visibility changes appear promptly. Verified with pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web test, git diff --check, and a browser smoke check for /quests/public; pnpm supabase:test could not run locally because Supabase Postgres was not listening on 127.0.0.1:54322.

  • #252 — Quest Packs (browser, picker, template-based quest creation). Ported the Quest Packs experience to the Next.js 2.0 app. Added the public /quest-packs browser with active-pack search, category filtering, pack cards, task-count summaries, preview modal, and "Start this Quest" links into /quests/new?pack=<slug>. Added public Route Handlers for GET /api/quest-packs and GET /api/quest-packs/[slug], backed by shared quest-pack helpers that return ordered packs with ordered template tasks. Upgraded the quest creation flow so a selected pack pre-fills the title/goal, preserves the pack through auth redirects, shows the exact template task preview, and still clones pack tasks into the created quest through the existing server action/API path. Added /admin/quest-packs, guarded by the existing quest_packs.write privilege, with admin create/edit forms, active toggles, and reorder controls. Migrated the v1 starter catalog into supabase/seed.sql with ten idempotent Quest Packs and task lists matching docs/seed-data-summary.md. Added focused parser/slug utility coverage. Verified with pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web test, pnpm --filter web build with local public Supabase env values, and a browser smoke check for /quest-packs plus /quests/new?pack=fitness-foundations auth redirect preservation.

  • #251 — AI Task Investigation (background worker + investigation history). Chose the standalone worker architecture for v2: apps/worker now claims pending task_investigations rows from Supabase, processes them without Redis/BullMQ, and writes running → succeeded/failed status updates back to the investigation history. Added claim_next_task_investigation() with FOR UPDATE SKIP LOCKED plus service-role-only execute grants so multiple Railway/Fly worker replicas can claim work safely. POST /api/tasks/:id/investigations now validates the selected model persona, composes the final prompt from quest context, task details, custom prompt text, and image URLs, stores persona metadata/prompt/images in task_investigations, and returns 202 Accepted with the queued run. Added a provider abstraction for OpenAI, Anthropic, and DeepSeek, carrying forward AI_MAX_RESPONSE_TOKENS; OpenAI and Anthropic receive image attachments through their multimodal request schemas, while DeepSeek receives image URLs in the prompt context. The task UI now loads active model_personas, exposes a model persona selector, disables investigate actions when no persona catalog is available, displays model type and prompt history, and polls active pending/running investigations every three seconds so status changes appear near-real-time until Supabase Realtime is wired in. Documented the architecture decision in docs/ai-investigations.md, updated API/schema docs, added worker env vars (DEEPSEEK_API_KEY, AI_MAX_RESPONSE_TOKENS, AI_WORKER_POLL_INTERVAL_MS), and added focused worker coverage for provider parsing/summary helpers. Verified with pnpm --filter worker test, pnpm --filter worker typecheck, pnpm --filter worker lint, pnpm --filter worker build, pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web test, and pnpm --filter web build after allowing the build to fetch Google Fonts.

  • #250 — Task management (create, edit, reorder, status transitions). Added the authenticated task-management vertical slice for v2 quests. New API route handlers cover GET /api/tasks?questId=..., POST /api/tasks, PATCH /api/tasks/:id, DELETE /api/tasks/:id, PATCH /api/tasks/reorder, and POST /api/tasks/:id/investigations; every route checks the caller owns the parent quest/task before mutating RLS-protected rows. The task DTO helpers normalize Supabase snake_case rows into UI-friendly camelCase, validate payloads with zod, support both sortOrder and issue-note order, normalize in-progress to the database enum in_progress, and preserve the schema's blocked status while keeping the primary UI cycle to todo → in progress → done. Added an authenticated task workspace backed by get_my_active_quests() and embedded the same manager into owner-only quest detail pages: inline task creation, title/details editing, status toggles, up/down reorder controls, delete, compact/expanded view mode, progress summary, bulk completion, and per-task investigation history display. Investigation triggering currently creates a pending task_investigations row with the composed prompt so the future AI worker can pick it up. Added lucide-react to the web workspace, documented the task endpoints in docs/api-endpoints.md, and made public env parsing lazy so Next.js can collect API route modules during build without requiring Supabase runtime env at import time. Added focused Vitest coverage for task status/order helpers, payload validation, env parsing, and the task manager expanded/compact investigation rendering.

  • #249 — Quest management (create, list, edit, archive, delete). Ported core quest management to the Next.js app on top of Supabase. Added authenticated pages for /dashboard (quest list with all, active, draft, completed, failed, and archived filters), /quests/new (create form with title, goal, context, constraints, image URLs, visibility, and Quest Pack picker), and /quests/[id] (detail page with task list, status badge, images, share action, disabled AI investigate affordances for #251, inline edit, archive, and permanent delete controls). Added Route Handlers for GET/POST /api/quests, GET/PATCH/DELETE /api/quests/[id], and PATCH /api/quests/[id]/archive, all using the server Supabase client and authenticated ownership checks. Added quest server actions for form submissions with dashboard/detail revalidation, optimistic archive UI feedback, confirmation before permanent delete, and rollback if Quest Pack task cloning fails during creation. Added apps/web/src/lib/quests/{queries,actions,validators,packs}.ts, quest UI components, and validator coverage for lifecycle statuses, image URLs, and dashboard filters. Added a Supabase migration and type update for the required failed quest status so the lifecycle now supports draft → active → completed/failed → archived. Verified with pnpm --filter web typecheck, pnpm --filter web lint, pnpm --filter web test, and a local browser smoke check that anonymous access to /quests/new redirects through /auth/login?next=%2Fquests%2Fnew.

  • #247 — Authentication (email/password signup, login, logout). Shipped the full email/password auth surface on @supabase/ssr using the createServerClient + createBrowserClient pattern (no custom JWT handling). New routes under apps/web/src/app/auth/: /auth/login (email + password sign-in, supports ?next= for post-login redirects), /auth/signup (name + email + password — captures full_name into user_metadata so the trigger from #246 mirrors it into profiles), /auth/logout (POST/GET route handler that calls supabase.auth.signOut() and 303s to /auth/login), /auth/forgot-password (request reset email — deliberately vague success copy to avoid leaking account existence), /auth/reset-password (set new password after the email link — explicitly NOT in the auth-only redirect list, since the recovery email arrives with a live recovery session and bouncing the user off the page would make the flow impossible to complete), /auth/confirm ("check your inbox" landing for the email-confirmation flow), and /auth/callback (exchanges the Supabase PKCE code for a session cookie and bounces to the safe next path, rejecting open redirects). Added a placeholder /dashboard page so the auth round-trip has a destination — real implementation lands with #249. The middleware (apps/web/src/lib/supabase/middleware.ts) was upgraded from a pure session-refresh into the route gate: anonymous visitors hitting /dashboard or /quests/* are bounced to /auth/login?next=<original-path>, and authenticated visitors hitting /auth/{login,signup,forgot-password,confirm} are bounced to /dashboard — /auth/callback, /auth/logout, and /auth/reset-password are intentionally excluded so signed-in users can still complete those flows. Middleware redirects copy any Set-Cookie headers supabase.auth.getUser() wrote onto the response so a rotated refresh token isn't silently dropped on the redirect hop. Server actions live in apps/web/src/lib/auth/actions.ts with a discriminated AuthActionState (idle/error/info) consumed via useFormState, zod schemas in validators.ts (8-char minimum to match Supabase's auth.minimum_password_length), friendly Supabase-error-code mapping in errors.ts (invalid_credentials, user_already_exists, weak_password, email_not_confirmed, HTTP 429), and centralized redirect targets in redirects.ts so middleware / actions / links stay in sync. The shared safeInternalPath() helper rejects empty strings, off-site URLs, protocol-relative URLs (//attacker.example), and backslash variants — every user-controllable next target (login action, login page, callback handler) routes through it so a crafted login link can't open-redirect the user post-authentication. Added NEXT_PUBLIC_AUTH_EMAIL_CONFIRMATION feature flag to apps/web/src/lib/env.ts (with a boolean coercer) and .env.example — mirrors [auth.email].enable_confirmations in supabase/config.toml; when true the signup action redirects to /auth/confirm instead of /dashboard. New UI primitives Label and Alert (with destructive / success variants) added to @questify/ui. The legacy /login placeholder is now a server redirect to /auth/login so existing bookmarks and the home CTA still resolve. Home page CTAs updated to point at /auth/login and /auth/signup. Vitest coverage in apps/web/src/lib/auth/{validators,errors,redirects}.test.ts (the redirects suite asserts safeInternalPath blocks protocol-relative URLs and that /auth/reset-password is NOT auth-only); Playwright smoke spec in e2e/auth.spec.ts (every auth route renders the right form, /dashboard redirects anonymous users with next=%2Fdashboard, legacy /login redirects to /auth/login). Live signup/login round-trip e2e requires a real Supabase project and is gated behind #267 (test infrastructure).

  • #248 — Authentication (Google + Facebook OAuth via Supabase). Added social sign-in on top of the email/password surface shipped in #247. New signInWithOAuthAction(provider, next) server action in apps/web/src/lib/auth/actions.ts calls supabase.auth.signInWithOAuth with skipBrowserRedirect: true (required for SSR — Supabase returns the authorization URL rather than trying to call window.location) then issues a redirect() to the provider. The redirect target includes ?next=<safeInternalPath> so the post-login destination survives the OAuth round-trip. New OAuthButtons client component (apps/web/src/components/auth/oauth-buttons.tsx) renders a Google button and a Facebook button, each backed by useTransition so the button shows "Redirecting…" and disables while the server action is in flight. Inline SVG icons for both providers — no new icon-library dependency. A position prop places the buttons above (signup, OAuth-first convention) or below (login, email-first convention) the email/password fields, separated by an "or" divider. Wired into LoginForm and SignupForm. The existing /auth/callback handler already exchanged PKCE codes for session cookies — it now also checks for ?error= params set by providers when the user denies access or an upstream error occurs, bouncing to /auth/login?error=oauth_denied without leaking provider details. User profile data (full_name, avatar_url) is auto-populated from the provider's userinfo response by Supabase into auth.users.raw_user_meta_data — the trigger from #246 mirrors this into profiles with no extra handling required. Existing email/password accounts are linked automatically by Supabase when the same verified email is presented via OAuth. .env.example updated: the four OAuth vars (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_SECRET) are now active entries (previously commented out) with the Supabase dashboard path, provider setup guide links, and the callback URL pattern to register in each provider's developer console.

  • #246 — Supabase schema (full data model + RLS). Designed and shipped the complete v2 data model as six versioned migrations under supabase/migrations/. Migration 1 (init_core) adds the pgcrypto + citext extensions, the shared set_updated_at() trigger, three status enums (quest_status, task_status, investigation_status), and the profiles table — auto-populated by an on_auth_user_created trigger that mirrors auth.users 1:1. Migration 2 (quests_tasks_investigations) lands quests (with soft-delete via deleted_at, is_public for sharing), tasks (with sort_order indexed), and task_investigations (always-private AI run history), plus user_owns_quest() and user_owns_task() SECURITY DEFINER helpers used by the RLS policies. Migration 3 (packs_personas) adds the read-only reference catalog: quest_packs, quest_pack_tasks, model_personas — anon can read active quest packs (public catalog), model_personas is authenticated-only. Migration 4 (rbac) wires up roles, privileges, user_roles, role_privileges plus has_role(role_key) / has_privilege(privilege_key) predicates used across every RLS policy, and back-fills admin-write policies on the migration-3 tables. Migration 5 (audit_beta) adds admin_audit_log (append-only — no INSERT policy means only the service role can write) and beta_signups (anon can INSERT, admin reads via beta_signups.read). Migration 6 (functions) adds three Postgres helpers: get_quest_with_tasks(quest_id) (returns { quest, tasks } JSON, RLS-respecting), get_my_active_quests() (dashboard query with task-completion counts), and claim_admin_role() (one-shot SuperAdmin bootstrap that refuses if any SuperAdmin already exists, recorded in the audit log). Wrote supabase/seed.sql with the three system roles (SuperAdmin / Admin / Support), ten granular privileges, four starter quest packs with task lists (learn-react, write-a-novel, couch-to-5k, ship-a-side-project), and six model personas across Anthropic / OpenAI / DeepSeek — all idempotent via ON CONFLICT DO NOTHING. Added supabase/tests/rls.sql (18 pgTAP assertions: owner can RW own, anon can read public quests but not private, tasks inherit ownership, task_investigations are never publicly readable, RBAC predicates work, admins can write reference tables). Re-added .github/workflows/supabase.yml with db lint + supabase test db against a real local stack on every PR touching supabase/**. Updated apps/web/src/types/supabase.ts with a hand-authored Database type matching the schema until pnpm supabase:typegen runs against a real project. Documented everything in docs/schema.md — table inventory, RLS rules, RBAC model, helper functions, index strategy, and typegen workflow. Re-added the supabase/ CLI scaffold (config.toml, .gitignore, seed.sql) and the nine pnpm supabase:* scripts (start, stop, status, reset, test, diff, push, migration:new, typegen, typegen:local) since they were missing from develop. Validated all eight SQL files through libpg_query — every statement parses cleanly.

  • Hardened release.yml post-deploy healthcheck in response to Codex review on PR #245. The previous version required /api/healthz to return 200, but that route only ships with #244 — every production deploy that landed before #244 merged would have failed the workflow on a 404. Now the workflow makes the root URL probe the required check (a deployment that can't render / is broken) and treats /api/healthz as soft: 200 logs the body, 404 is treated as "route not deployed yet, skip", and only 5xx fails the workflow. Replays of the request use the ${DEPLOY_URL} env var to satisfy shellcheck and Turbo's env-vars rule.

  • #245 — CI/CD pipeline (Vercel + GitHub Actions). Extended release.yml to trigger on push to main (in addition to v*.*.* tags and manual dispatch) so merges to main auto-deploy to Vercel production; added a post-deploy /api/healthz probe with retries so a failed deploy fails the workflow. Added a final aggregate ci-pass job in ci.yml that gates on lint, test, build, and (where applicable) e2e — this is the single required check for branch protection on develop and main. Added .github/workflows/lighthouse.yml to run Lighthouse CI against successful Vercel preview deploys (triggered by Vercel's deployment_status events) and post a sticky PR comment with Performance / Accessibility / Best Practices / SEO scores; Lighthouse config in .github/lighthouserc.json warns under 80% perf and fails under 90% a11y. Added .github/workflows/deployment-comment.yml — a sticky-per-environment PR comment that updates as deploys move through pending → success / failure, with healthcheck URLs. Added .github/PULL_REQUEST_TEMPLATE.md, .github/ISSUE_TEMPLATE/{bug,feature,config}.yml, and .github/CODEOWNERS (default owner plus stricter ownership on .github/, supabase/, packages/ui/, and shared tooling). New docs/cicd.md is the operational source of truth: full deploy-flow diagram, workflow inventory, Vercel project setup, required secrets / variables / environments, exact branch protection rules for develop + main, and rollback procedure. Linked from the README under a new "CI / CD" section.

  • Fixed Supabase workflow validation step. Replaced supabase config get project_id (the config get subcommand was removed from the CLI — it now only accepts config push) with an inline Python tomllib parse that asserts required keys exist (project_id, api, db, auth), auth.enabled = true, and auth.email.enable_signup = true. Added a guard to the db-lint job so it skips when no migrations exist yet (the schema lands in #246).

  • #244 — Configure Supabase project (Auth, Postgres, RLS). Scaffolded the Supabase CLI directory at supabase/ with config.toml (project_id, ports 54321–54327, email/password auth enabled, scaffolded-but-disabled Google + Facebook OAuth blocks for #248), seed.sql, migrations/.gitkeep, and .gitignore. Added apps/web/src/types/supabase.ts (typed Database stub — overwritten by pnpm supabase:typegen once #246 lands a real schema) and threaded the Database generic through the browser, server, and middleware Supabase clients. Added a createServiceRoleClient() helper for trusted server-side work that bypasses RLS. Split apps/web/src/lib/env.ts into public/server schemas so SUPABASE_SERVICE_ROLE_KEY is validated server-side only and never inlined into the client bundle. Added a runtime /api/healthz route that pings supabase.auth.getSession() and returns { ok, supabase, timestamp, version } for Vercel probes and e2e smoke tests, with a Vitest test for the response shape and cache headers. Wired the supabase CLI as a dev dependency plus nine pnpm supabase:* scripts (start, stop, status, reset, diff, push, migration:new, typegen, typegen:local). Added .github/workflows/supabase.yml — path-filtered validation that parses config.toml, enforces the <utc-timestamp>_<slug>.sql migration naming convention, and runs supabase db lint against a real local stack. Threaded SUPABASE_SERVICE_ROLE_KEY (with a placeholder fallback) through ci.yml so server-side code can be exercised in CI. Added VERCEL_GIT_COMMIT_SHA to turbo.json#globalEnv for the healthcheck's version stamp. Extended vercel.json with a Cache-Control: no-store header on /api/healthz. Expanded the README with a Supabase section: command table, local-stack URLs, hosted-project linking flow, and a curl snippet for the healthcheck.

  • #243 — Initialize Next.js 14 + Turborepo monorepo scaffold. Bootstrapped the v2 platform on the portfolio-standard stack (Next.js 14 · TypeScript · Tailwind CSS · Supabase · Turborepo · Vercel). Removed the v1 Nuxt app, BullMQ worker, Prisma package, and shared package to clear the way for the rebuild — v1 remains accessible on develop and prior release branches during the migration window (#268).

  • Added apps/web: Next.js 14 App Router with TypeScript, Tailwind, Supabase SSR client (src/lib/supabase/{client,server,middleware}.ts), Zod-validated env loader (src/lib/env.ts), session-refresh middleware, and placeholder /, /login, /quests routes consuming the shared UI library.

  • Added packages/ui: shadcn/ui-style component library with cn() helper, Button (CVA variants), Input, Card, design tokens via HSL CSS variables (light + dark), and a Tailwind preset consumed by apps.

  • Added packages/typescript-config: shared base.json, nextjs.json, and react-library.json presets.

  • Added packages/eslint-config: flat-config base.js, react.js, and next.js presets wired with @next/eslint-plugin-next, eslint-plugin-react-hooks, typescript-eslint, eslint-plugin-turbo, and eslint-config-prettier.

  • Added root turbo.json with build, dev, lint, typecheck, test, test:e2e, and clean pipelines, including dependsOn topology and Supabase env declarations for build caching.

  • Updated root package.json to drive Turborepo (pnpm dev/build/lint/test), pinned pnpm@9.12.0 via packageManager, set engines.node >= 20.0.0, and added Prettier + Tailwind plugin tooling.

  • Replaced .env.example with the v2 surface: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_DB_URL, NEXT_PUBLIC_SITE_URL, AI provider keys, and documented OAuth/observability placeholders.

  • Rewrote README.md for the new stack: v1 → v2 layer comparison, repo layout, Turborepo command table, env-var reference, and links into epic #269's follow-up issues.

  • Updated vercel.json to build the web workspace via pnpm turbo run build --filter=web with framework: "nextjs"; added .nvmrc (Node 20), .npmrc (auto-install-peers), Prettier config + ignore file, and gitignore entries for .next/, .turbo/, and .v1-archive/.

  • GitHub Actions overhaul: rewrote ci.yml to run on Node 20 + pnpm 9 with split lint / test / build / e2e jobs driven by Turborepo, including pnpm-store and .turbo caching, optional TURBO_TOKEN remote cache hooks, and a Playwright browser cache. Refreshed codeql.yml with the security-and-quality query suite, a weekly schedule, and pnpm/Node setup via pnpm/action-setup@v4. Replaced release.yml to deploy to Vercel via the Vercel CLI (vercel pull → build → deploy --prebuilt --prod) on v*.*.* tags or manual dispatch, with a GitHub Environment URL output. Replaced staging.yml with a Vercel preview deploy on pushes to staging. Removed the Prisma-specific seed-staging.yml — the Supabase-driven replacement lands with issue #246. Restructured .github/dependabot.yml to cover root tooling, apps/web, packages/ui, packages/eslint-config, and GitHub Actions, with grouped updates per ecosystem.

  • Wired up Playwright e2e for apps/web: added playwright.config.ts scoped to apps/web/e2e/ with a chromium project, GitHub reporter in CI, trace/screenshot/video on failure, and an auto-booted Next.js webServer that seeds placeholder Supabase env vars. Added e2e/home.spec.ts covering the home-page hero copy, CTA targets, and the /login navigation flow. The previous CI failure happened because Playwright (with no config) was globbing the whole src/ tree and picking up the Vitest *.test.tsx files, which then require()'d the ESM-only vitest package and crashed. Vitest now stays in src/**, Playwright stays in e2e/**. Extended apps/web/tsconfig.json to include e2e/, playwright.config.ts, and vitest.config.ts so tsc --noEmit covers the test surface. Added CI, PORT, and E2E_BASE_URL to turbo.json so turbo/no-undeclared-env-vars stays satisfied and Turborepo invalidates the cache when these change.

  • Wired up Vitest in apps/web: added vitest.config.ts (jsdom env, automatic JSX runtime, @/ path alias, passWithNoTests: true, V8 coverage), a shared src/test/setup.ts that loads @testing-library/jest-dom/vitest and seeds Supabase env defaults, plus two smoke tests — src/lib/env.test.ts (Zod env validator) and src/app/page.test.tsx (home-page render + nav links). Full test infrastructure (Playwright + cross-workspace coverage gates) lands with #267.

  • Fixed CI lockfile + ESLint 9 compatibility: regenerated pnpm-lock.yaml against the v2 workspace (old lockfile retained the stale magicast override, breaking pnpm install --frozen-lockfile). Dropped eslint-config-next from apps/web (it requires ESLint ≤ 8) in favor of the shared flat config; the web lint script is now eslint . --max-warnings 0 and reads apps/web/eslint.config.mjs. Disabled three rules in @questify/eslint-config/next (@next/next/no-duplicate-head, no-html-link-for-pages, no-page-custom-font) that call context.getAncestors() — an ESLint 8 API removed in 9 and not patched in @next/eslint-plugin-next@14.2.x; the rules can be re-enabled once we move to Next 15. Disabled react/prop-types globally — TypeScript handles prop validation.

Design

  • Introduced the Questify Design System v1.0: parchment/indigo/gold colour palette replacing the previous blue theme, with full light and dark mode support.

  • Added apps/nuxt/app/assets/css/tokens.css exposing CSS custom properties for typography (--font-sans, --font-serif, --font-mono), spacing scale (--sp-1 through --sp-10), border radius (--r-1 through --r-full), elevation (--shadow-1/2/3), status soft fills, and motion tokens (--ease-out, --ease-spring, --dur-fast/base/slow). Includes .t-serif, .t-mono, and .t-display utility classes.

  • Updated vuetify.config.ts with the new brand palette (primary indigo #4d3b9a, secondary gold #caa44b, parchment background #faf7f0) and matching dark-mode values; added component defaults for VBtn, VCard, VTextField, VSelect, and VChip to align radius and density with the design spec.

  • Loaded Geist, Instrument Serif, and JetBrains Mono via Google Fonts in nuxt.config.ts.

  • Rebuilt the dashboard welcome section as a full-bleed dark hero panel with an italic serif greeting, monospace eyebrow label, decorative compass icon, and gold primary CTA — per design spec §10.

  • Updated QuestCard.vue to render quest titles in Instrument Serif (weight 400, 1.25rem) with tonal status chips using semantic colours; replaced hardcoded grey-darken-1 on archived quests with secondary.

  • Removed the animated radial-gradient / gradientShift loop from app.vue; background is now a clean theme-colour transition.