Supported by
Your business could be hereDonate and we'll run your business name across every pageYou, right here, in front of thousands of studentsYour business could be hereDonate and we'll run your business name across every pageYou, right here, in front of thousands of students
Get listed

Release notes

What just shipped and what changed in recent audits. Spot something missing? Tell us.

Just shipped

  • Shipped

    Adaptive practice, a 12-question diagnostic, and student-controlled parent access

    Practice now adapts. Every answer updates a server-side ability estimate for that subject, and the next question is pulled from the band that matches where you actually are. A new 12-question adaptive diagnostic gives you a starting level and a projected section score in about ten minutes. Memory Match can now build a deck straight from the topics you miss most, and Pesty opens with the same specific number instead of a pep talk. Parent access is now fully student-initiated: you mint a one-time code, a parent redeems it, and you can pull the link back in one tap from the new Parent access page.

  • Shipped

    Product metrics dashboard and district login plan

    Admins get a metrics view covering signups, activation, weekly and monthly active students, D7 retention, questions answered, accuracy, and top product events, on 7 / 30 / 90 day windows. Client events are written through one tracker so nothing bypasses it. Also wrote the Clever single sign-on plan for district logins: Instant Login first, rostering second, with an sso_identities mapping table and no change to how roles are stored.

  • Shipped

    New ACT English passage — The Mural on the Overpass

    Added a 12-question passage-based ACT English set about a small town restoring a faded highway mural. Odd items A-D, even items F-J, with per-question WHYs stored as tutor-mode explanations and a 9-minute suggested single-passage timer. Slotted into the full-section rotation.

  • Shipped

    ACT Math figures render inline, with real difficulty tags

    New ACT Math items ship with real inline geometry figures (right triangles, coordinate grids, circles, parallel-line diagrams) drawn as themed SVG next to the question, with the official 'Figure not necessarily drawn to scale' note. Every math item is tagged easy / medium / hard, mixed practice draws roughly 30% easy / 45% medium / 25% hard, topic drills ramp easy to hard, and full sections put harder items later like the real test. Trivial one-step arithmetic is banned by the generator's new hard-floor rule and old items were swept for violations.

  • Shipped

    Third real ACT English passage — Counting the Fish at Miller Creek

    Added a 12-question ACT English passage about volunteer salmon counters, in the official ACT convention. Every question stores its WHY as the tutor explanation.

  • Shipped

    Fourth ACT English passage set (Origami + Streetcar) and new Math batch

    Added two more ACT English passages ('Learning to Fold Origami' and 'The Streetcar That Wouldn't Die') plus 30 new ACT Math items with 43% geometry/trig/coordinate coverage. Also registered the earlier 'Hidden Rivers' and 'Miller Creek' passages so they finally appear in rotation.

  • Shipped

    New ACT Reading passages — Literary Narrative + Humanities

    Added two full 9-question ACT Reading passages: 'The Long Way Home' (Literary Narrative, original fiction about a grandfather teaching a fourteen-year-old to row a creek) and 'The Painter Who Waited' (Humanities). Each item has a WHY stored as the tutor explanation.

  • Shipped

    Pesty is now one connected chibi body — head, torso, legs, shoes

    Pesty was rendering as a big floating head with two disconnected feet. Now every Pesty is a real connected chibi: a small head sits directly on a rounded torso in the same skin color, short legs attach to the bottom, shoes sit on the ground, and stub arms come off the torso. Species keep their personality — Blobby stays round, Bolt is boxier, Bubbles gets a fish tail instead of legs, Boobert wears a ghost skirt — and all four still read as head + torso + lower body + shoes. The closet preview is now large so proportions are obvious, and clothing (hats, shirts, pants, shoes) layers correctly over every part.

  • Shipped

    Search palette (Cmd+K) is now a proper centered modal

    The Cmd+K / Ctrl+K search used to render as an unstyled strip pinned to the bottom of the page. It now opens as a themed centered modal with a dark backdrop, rounded corners, a visible close button, full keyboard navigation (arrows to move, Enter to open, Esc to close), and click-outside to dismiss.

  • Shipped

    No two answer choices can be numerically equivalent

    Live testing turned up an ACT Math probability item (4 red, 6 blue, 5 green marbles - both blue) whose choices included both 1/7 and 30/210, which are the same number. Fixed that item and swept the entire math bank: any question with two choices that reduce to the same value was auto-corrected with a plausible distractor (48 items across ACT Math, SAT Math, AP Calc AB/BC, AP Precalc, AP Stats, AP Music Theory). Added a matching validation rule to question generation so future AI-generated items are rejected if any two choices are numerically equal (compared as reduced fractions/decimals, not strings).

  • Shipped

    Emails no longer show garbled apostrophes or dashes

    Fixed the mojibake bug where automatic emails (test-day nudges, urgent alerts, weekly study reminders, biweekly digest) sometimes showed characters like "’" or "—" in place of apostrophes and dashes. The newsletter mailer now: (1) declares UTF-8 in both the HTML meta tag and every MIME part header (Content-Type: text/html; charset=UTF-8), (2) sends each part with Content-Transfer-Encoding: base64 (encoded from real UTF-8 bytes) so 8-bit characters survive every mail server hop, (3) rewrites smart quotes, em/en dashes, ellipses, middle dots, and non-breaking spaces to plain ASCII before sending, and (4) HTML-escapes every user- or DB-supplied value interpolated into a template (topic names, exam labels, event details, links). Accented names like Jose or Rene render correctly; a round-trip test with an apostrophe, an em dash, and "Jose's resume" was verified end-to-end before shipping.

  • Shipped

    New real ACT Reading passage - Hidden Rivers Beneath Antarctica

    Added a Natural Science passage with 9 questions in the official ACT convention (odd items A-D, even items F-J), plus a 10-minute suggested single-passage timer. Each question stores a WHY as the tutor-mode explanation so review mode teaches, not just grades.

  • Shipped

    New real ACT Math geometry pack (16 items)

    Added 16 verbatim ACT Math items skewed to geometry, coordinate geometry, and trig, each with exactly 4 choices and a full worked solution stored as the tutor explanation. Batch generator now warns when an ACT Math batch ends up below 30 percent geometry/trig/coordinate content.

  • Shipped

    Second real ACT English passage - Half the Show: Music in the Silent Movie House

    Added a 12-question passage-based ACT English set. Odd items A-D, even items F-J, with per-question WHYs as tutor-mode explanations and a 9-minute suggested single-passage timer.

  • Shipped

    One Pesty everywhere - unified customization

    Pesty is now a single character. The old separate Closet and Pest customizers are merged into one Customize Pesty page with a live preview of both the avatar and the full body. The X button, Esc key, and clicking the backdrop all close the page and return you to where you were. /wardrobe redirects to /pest so old bookmarks still work.

  • Shipped

    Real ACT English passage set - Calloway Street radio shop

    Added 13-question passage-based ACT English item ('The Radio Repair Shop on Calloway Street') using official ACT convention: odd items A-D, even items F-J. Correct-answer spread balances across positions and NO CHANGE is correct on only 3 of 13. Passage renders alongside questions with the target span highlighted. Generator validator tightened: NO CHANGE cap 35%, reject any letter correct 4+ items in a row, require English items to be tied to an underlined portion or explicit whole-essay/rhetorical prompt, and accept F-J answers on even English items.

  • Shipped

    Cmd/Ctrl+K command palette + declutter default nav

    New global search (search icon in the nav or Cmd/Ctrl+K) jumps to any page, test, or section. The nav dropdown now shows only 4 essentials by default (Tests, Dashboard, AI Tutor, Hub). Add more from Hub > Customize your header — everything else stays instantly findable via search.

  • Shipped

    Read-aloud now speaks the FULL message — no more single-line cutoff

    Fixed the speaker button that was only reading the first ~2 sentences of any chat reply, hint, or explanation. src/components/SpeakButton.tsx now calls speakLong() (chunked, keepalive-protected end-to-end playback) instead of speak() (which used shortenForSpeech's 240-char / 2-sentence cap), so hitting the speaker on any assistant reply, Pest hint, or explanation reads every sentence and paragraph from start to finish. Same for the tutor auto-speak: src/routes/tutor.tsx now uses speakLong for the newest assistant reply so multi-paragraph tutoring answers play in full instead of stopping after line one. Answer review is upgraded too: src/components/WalkThroughButton.tsx now accepts an optional selectedLetter prop and, when the student picked wrong, explicitly names their pick and the correct pick before narrating the full explanation ('You picked B: …, but that isn't quite right. The correct answer is C: …. Here's where the mistake happened.'); when they picked right, it acknowledges the correct pick and still walks through the whole explanation sentence-group by sentence-group. src/routes/practice.tsx passes answers[q.id] as selectedLetter so the walk-through calls out the specific mistake. Kokoro neural voice stays primary with browser SpeechSynthesis as the automatic fallback; the existing 8s pause/resume keepalive still guards against Chrome's ~15s auto-stop bug across every sentence in the message. No scoring, timers, or exam logic changed.

  • Shipped

    Header logo reverted to original animated mark; Pesty tutor now reflects chosen color + richer face animations

    Reverted the top-left header logo back to the original animated TestPest face mark (plum head, brow raise/furrow, blink lids, look-around pupils, mouth stretch, sparkle hearts). The recent change that made the header logo match the full-body Pesty was undone — src/components/TestPestMark.tsx is restored from git 9184b32, and the .nav-logo .tp-* keyframes in src/styles.css keep driving it. The full-body Pesty (src/components/PestyBuddy.tsx) is still THE Study Buddy / AI tutor character everywhere it appears, and now reads the user's chosen Pest color from profiles.pest_config.color via a new src/hooks/use-pest-color.ts hook (Supabase read + localStorage cache 'tp.pest.color.v1' + reactive to pest-config-updated / storage / focus). Body, head, arms, blink lids, and legs are derived from that base hex via an HSL shade() helper (head +8L, legs -18L), and an optional colorHex prop still lets callers override. Equipped wardrobe items continue to render on top. Facial animations were expanded: blink lids remain, plus a new pupil-dart cycle (subtle look-around, ~6.2s), a new brow group that raises on encourage/celebrate/wave, furrows on think, and bobs during speech, and the existing mood-driven mouth shapes and lip-sync mouth-open/close during TTS. Everything continues to respect prefers-reduced-motion (all keyframes freeze into a warm static pose). No scoring, timers, or exam logic changed.

  • Shipped

    Question review workflow — approve/reject queue + student 'Report this question'

    Added a full review workflow so only approved questions reach students. Migration adds review_status ('pending'|'approved'|'rejected'), reviewed_by, reviewed_at, and review_notes on public.question_bank; existing approved rows were marked approved so nothing disappears. RLS on question_bank now restricts SELECT to review_status='approved' for regular users, with admins seeing everything. New public.question_reports table stores student flags with reason, optional detail, status ('open'|'triaged'|'resolved'|'dismissed'), reviewer, and action_taken (admin-write, per-user read + admin read). New admin page at /admin/review has three question tabs (Pending, Rejected, Approved) and a Reports tab: each pending item shows the exam/section/skill, full passage, question, choices with the correct answer highlighted, explanation, and an optional review-notes textarea, plus Approve / Reject / Send-back-to-pending buttons that stamp reviewed_by and reviewed_at. Reports tab filters open reports, supports Mark resolved / Dismiss / Triage, and 'Reject linked question + resolve' one-click for DB-bank items. New src/components/ReportQuestionButton.tsx renders a small 'Report this question' button beside explanations in the test player (src/routes/tests_.$testId.tsx) and in Practice (src/routes/practice.tsx); it writes to public.question_reports with reporter_id = auth.uid() and a bank_question_key ('practice:<id>' or '<testId>:<qId>') when the item comes from the file-based bank rather than public.question_bank. Batch-generated questions now default to pending (no more silent go-live). Scoring, timers, existing banks, and per-user question flow are unchanged.

  • Shipped

    Public-domain passage ingestion for the generator (Gutenberg + NASA/NOAA/USGS/CDC)

    Added an admin-only ingestion pipeline that pulls Reading passages from Project Gutenberg (Pride and Prejudice, The Adventures of Sherlock Holmes, The Souls of Black Folk, The Autobiography of Benjamin Franklin) and Science passages from NASA / NOAA / USGS / CDC. New Supabase edge function supabase/functions/ingest-public-passages/index.ts fetches each source, strips headers/HTML, trims to a clean excerpt, dedupes via a SHA-256 content_hash, and stores each passage in a new public.public_passages table with title, author, publisher, source_url, license, attribution, year, word_count, body, and tags. The table is admin-write and authenticated-read (RLS). The batch generator (supabase/functions/batch-generate-bank/index.ts) now accepts passageId or usePublicPassage on any plan item: it pins the exact passage text into the prompt, forbids rewriting/summarizing it, sets question_bank.passage_id, and copies the attribution/license/URL into the item's source. Questions themselves stay 100% ORIGINAL — only the passage is public-domain source material, always attached with its provenance. The /admin/generate page has a new 'Public-domain passage ingestion' panel: pick All / Reading / Science, run ingestion, and browse stored passages with their passageId ready to paste into a batch plan. Migration adds public.public_passages (with tg_set_updated_at trigger) and a passage_id FK on public.question_bank. Scoring, timers, and the student session flow are unchanged.

  • Shipped

    Admin offline question-generation pipeline (batch → database)

    Added an admin-only offline batch pipeline that writes new practice questions directly into the database instead of during a student session. New Supabase edge function supabase/functions/batch-generate-bank/index.ts accepts a plan of {exam, section, skill, difficulty, count, passageBased} entries, calls the Lovable AI Gateway (google/gemini-2.5-pro) with a strict system prompt, and runs a hard validator on every item: exactly one correct answer, exactly 4 distinct non-empty options, MCQ answer must be A/B/C/D, English 'NO CHANGE' allowed only on option A and correct at most ~1 in 5 batch items, no single answer letter over 55% of the batch, no bad tokens (//, ___, [blank], TODO, lorem) or mojibake, passages must meet length floors (ACT reading/english/science ≥ 300, SAT R/W ≥ 60), and grid-in answers must be numeric/fractional. Accepted rows insert into a new public.question_bank table with a content_hash unique index so duplicates are auto-rejected. English uses error-injection: the prompt tells the model to write a clean sentence, inject ONE error (comma splice, subject-verb, dangling modifier, wordiness, pronoun agreement, punctuation, parallelism, or tense), underline it, and build 4 options around the minimal correct fix with distractors that are common student mistakes. Each run creates a public.batch_jobs row (admin-only RLS) tracking requested/accepted/rejected/inserted/duplicates and up to 500 rejection reasons. Admin UI (/admin/generate) now has a 'Offline batch generation → database' panel with an editable JSON plan and a live result JSON. Covers ACT skills (English usage/rhetorical; Math algebra, plane geometry, coordinate geometry, trig, functions; Reading; scientific reasoning) and SAT R/W + Math. Scoring, timers, and the student-session flow are unchanged.

  • Shipped

    Pesty's Closet — earn and equip cosmetic outfits

    Added a cosmetic rewards system. Students earn hats, shirts, pants, and accessories for the Pesty avatar by practicing and answering correctly, and dress the character up at a new /wardrobe route (Account → 'Pesty's Closet' in the drawer). All 20 items are original SVG art authored in src/lib/wardrobe-catalog.tsx (5 hats, 5 shirts, 4 pants, 6 accessories including glasses, bow tie, scarf, backpack straps, and fairy wings), grouped by common/rare/epic rarity. Unlock rules are: starter (4 items), correct-count milestones (10/25/50/75/100/150/250/500), practice sessions completed (3/5/7/10), and best in-session streak (5/10/15). src/lib/wardrobe-store.ts holds owned/equipped/lifetimeCorrect/sessions/bestStreak in localStorage under 'tp.pest.wardrobe.v1' as the source of truth and best-effort syncs into profiles.pest_config.wardrobe when signed in (no schema migration required — nests under existing jsonb). Cross-tab and cross-component updates go through a `pest:wardrobe` CustomEvent plus native `storage` events. src/components/PestyBuddy.tsx now renders equipped cosmetic layers inline at the correct SVG z-order: pants on top of legs, shirt over the belly, accessory in body-space (or inside the head-tilt group for glasses), hat inside the head-tilt group so it tilts with the head — so equipped items show up everywhere PestyBuddy is used (onboarding, dashboard, tutor, closet preview). src/hooks/use-answer-rewards.ts exposes useAnswerRewards() and completeSession(); wired into src/routes/tests_.$testId.tsx to record every per-question answer and to fire once when a test finishes, and into src/routes/practice.tsx to report correct count / in-session best streak / +1 session at submit. Newly unlocked items toast a '🎁 New outfit unlocked' notification. Reduce-motion behavior in PestyBuddy is preserved. Scoring, timers, and question banks are unchanged.

  • Shipped

    SAT question bank expansion + ACT/SAT cross-mix guard

    Added an original SAT expansion bank alongside the existing ACT and SAT banks and hardened the loader so no item can leak across tests. New files: scripts/expand_sat_2026_07.py (deterministic seeded generator, no copyrighted content), src/data/questions/sat/math_expansion_2026_07.json (+107 items across Algebra — Linear Equations/Systems/Functions, Advanced Math — Quadratics/Exponents, Problem Solving — Percents/Ratios/Statistics, Geometry — Right Triangles/Circles; every item has 4 distinct choices, one correct letter, plain-English explanation, and section:'math'), src/data/questions/sat/rw_expansion_2026_07.json (+20 items across Words in Context, Standard English Conventions, Boundaries, Transitions, Central Ideas and Details, Command of Evidence; each item includes a short digital-SAT-style passage and section:'reading_writing'). Wired both files via ID_EXTRA_FILES in src/data/tests.ts so they merge into sat-math and sat-rw only. Cross-mix guard: RawQ now carries an optional `section` field and loadBank runs every raw item through passesSectionGuard(id, raw) using a new ID_EXPECTED_SECTIONS map — SAT-tagged items are rejected from ACT banks and vice versa regardless of any file-mapping mistake. Untagged legacy items still pass so existing behavior is preserved (file mapping remains authoritative when no `section` is set). Scoring, timers, and section builders unchanged.

  • Shipped

    ACT English — trickier distractors and a NO CHANGE cap (~1 in 5)

    Raised the difficulty and realism of ACT English sections. New src/lib/act-english-balance.ts adds two passes to every English section build: (1) pickWithNoChangeCap tries up to 16 different passage selections and picks the one whose correct-answer set has the fewest 'NO CHANGE' items, targeting <=20% NC-correct per section; (2) balanceEnglishSection deterministically reshuffles each item's non-anchor choices (keeping NO CHANGE pinned to option A per ACT convention) using a seed derived from the question id so the correct letter is distributed across A/B/C/D within a section instead of clumping at A. buildQuestionSet in src/data/tests.ts wires both passes for id === 'act-english' only; Reading is unchanged. Also expanded src/lib/act-english-repair.ts mutations() with subtler grammar traps that make distractors genuinely tempting: its/it's, their/there/they're, who/whom, that/which, then/than, affect/effect, fewer/less, your/you're homophone swaps, plus a comma-added intensifier variant ('X, in fact, ...') alongside the existing comma/semicolon/word-order mutations. Four distinct options per item is still enforced by repairEnglishChoices. No scoring change, no unrelated behavior touched.

  • Shipped

    Pest voice — no more mid-sentence cutoffs, plus 'Walk me through this' mode

    Rebuilt long-form TTS so the Pest reads full answers end-to-end and added a step-by-step tutor mode. Changes in src/lib/tts.ts: (1) new chunkSentences() splits text at sentence boundaries with a soft ~180-char cap and further comma/word splits so no chunk is ever oversized; (2) new speakLong() plays chunks back-to-back using Kokoro when ready and browser SpeechSynthesis as fallback, with a per-chunk Kokoro→browser fallback per chunk so a single failed synth doesn't kill the run; (3) a session-id guard so cancelSpeech() is the ONLY thing that stops a run early — a new session bumps the id and every in-flight chunk loop checks isSessionCurrent() before continuing; (4) Chrome keepalive: a 8s pause/resume ticker runs while long/step speech is active so the browser's ~15s auto-stop bug can't cut sentences short; keepalive is cleared on cancel and on completion. New speakSteps() plays an ordered list of steps sequentially with a small gap and an onStep(index,total) callback — shares the same session/keepalive engine as speakLong via an internal playChunkSequence() helper (previously calling speakLong from speakSteps would have cancelled its own session; refactored to a shared inner loop). SoundProvider (src/components/SoundProvider.tsx) exposes both as speakLong and walkThrough on the context and still cancels on route change + tab hide. New src/components/WalkThroughButton.tsx: single reusable button that reads the passage (if any), the question stem, the four choices, then narrates the explanation sentence-group by sentence-group and closes with the correct letter. Shows live 'step X of N' progress and a Stop control; hidden when the student hasn't opted into Pest voice. Kokoro neural voice stays primary; native SpeechSynthesis remains the automatic fallback. No scoring, exam, or unrelated behavior changed.

  • Shipped

    ACT question bank expansion — 138 new original items across all four sections

    Added a large batch of ORIGINAL practice items (no copyrighted passages or questions) as sidecar JSON files under src/data/questions/act/, registered via ID_EXTRA_FILES in src/data/tests.ts so they merge into the existing banks and every subsequent loader / section builder picks them up automatically. Counts: ACT English +20 (2 full-length ~430-word original passages x 10 questions each, following the 5-or-10-per-passage grouping rule, each item includes an `underline` substring verified to appear verbatim in its passage); ACT Reading +18 (2 original ~750-word passages, 9 questions each, with attribution strings, covering Main Idea, Detail, Inference, Vocabulary in Context, Author's Purpose, Structure, Tone, and Comparative Reasoning); ACT Math +60 across the ACT reporting categories at easy/medium/hard mix — Number & Quantity (10), Algebra (12), Functions (6), Statistics & Probability (6), Geometry (16) with inline `figure` FigureSpec objects covering right triangles, isosceles/scalene triangles with angles, circles (radius/diameter/sector), rectangles, squares, regular hexagons, coordinate-plane point pairs, cylinders, rectangular prisms, and parallel lines cut by a transversal so the SVG figure engine renders every geometry item, Trigonometry (5), and Modeling (5); ACT Science +40 across data representation, research summaries, and conflicting viewpoints (6 mini-studies + 1 viewpoints pair). Every item has 4 distinct answer choices, one correct letter, and a plain-English explanation. English/Reading items pass the strict loader filters (English 300–450 word passage + underline present + no placeholder markers; Reading 700–900 word passage + attribution). All content authored from scratch. Files: scripts/expand_bank_2026_07.py (generator), src/data/questions/act/english_expansion_2026_07.json, reading_expansion_2026_07.json, math_expansion_2026_07.json, science_expansion_2026_07.json, src/data/tests.ts (ID_EXTRA_FILES registered), src/routes/release-notes.tsx.

  • Shipped

    Consistent Pest branding + global 'Hide Pest avatar' toggle

    Made the mascot on the header logo mark and the homepage hero the SAME character as the full-body PestyBuddy tutor — same plum head, antenna, cheeks, and dot-eyes-with-highlights face — so branding is consistent everywhere. TestPestMark was redrawn to match PestyBuddy's face; the signed-in and signed-out homepage heroes both float a waving PestyBuddy (size 140-160) in the top-right of the preview column. Added a global hide toggle: src/lib/pest-visibility.ts stores the preference in localStorage under 'tp.pest.avatar-hidden.v1' and broadcasts a 'pest:visibility' event so every open surface re-renders in sync (also listens to cross-tab 'storage' events). usePestHidden() gates the header mark and NavPest in SiteChrome, the hero PestyBuddy in src/routes/index.tsx, and the floating PestyHelper in src/components/PestyGuide.tsx (early return when hidden). Added a new 'Pest mascot' section to Settings (src/routes/dashboard-settings.tsx) with a live PestyBuddy preview and one clear checkbox: 'Hide the Pest avatar everywhere'. The floating buddy also has a quick-dismiss × chip in its top-right corner that sets both the per-session helper hide flag and the global preference in one tap; right-click still works for keyboard/power users. No exam logic, scoring, or unrelated features changed. Files: src/components/TestPestMark.tsx, src/lib/pest-visibility.ts (new), src/components/SiteChrome.tsx, src/routes/index.tsx, src/components/PestyGuide.tsx, src/routes/dashboard-settings.tsx, src/routes/release-notes.tsx.

  • Shipped

    Onboarding — 'Choose your Study Buddy voice' + 'Pick your theme' steps, plus Redo setup in Settings

    New/first-time users now see two setup steps that were previously buried: (a) 'Pick your Pest's voice' — persona list (Cute Pest, Peppy, Chill, Serious Coach) with a Preview button that speaks a sample line using the new Kokoro neural voice engine (with automatic browser-TTS fallback); (b) 'Pick your theme' — Light / Dark / High-Contrast picker embedded directly in the onboarding flow via the same ThemeToggleDrawerItem used in Settings > Appearance. Both selections persist immediately (voice → profile settings + localStorage via SoundProvider; theme → localStorage with a pre-paint script so there is no flash on next load). Both steps are optional — sensible defaults are always usable — so returning users are never blocked. Onboarding continues to auto-trigger for new users from the dashboard when onboarding_next_step is not 'done', and the flow honors ?step= deep links to jump straight to a step. Added a 'Redo setup' section in Settings (src/routes/dashboard-settings.tsx) with three buttons: 'Redo setup' (starts at 'name'), 'Just change voice' (jumps to the voice step), 'Just change theme' (jumps to the theme step) — all via /onboarding?step=<key>. Files: src/routes/onboarding.tsx (STEP_KEYS + STEPS + theme step render + isMissing rule), src/routes/dashboard-settings.tsx (RedoSetupSection), src/routes/release-notes.tsx.

  • Shipped

    Global navigation cleanup — one consistent nav on every page, Settings in the menu, Appearance moved

    Cleaned up site-wide navigation so every page (signed in or out) uses the same top bar + dropdown menu. Signed-in students now see a single primary bar: Practice, Tests, Dashboard, Study Plan, Tutor. Home is the logo (top-left). Educators keep a matching short bar: My Students, Classes, Tests, AI Tutor. The hamburger drawer is available on every page and, for signed-in users, always includes an Account section with Settings and Profile, plus Replay guided tour and Log out. Any student-customized sections that don't fit on the top bar (Games, Hub, Leaderboard, Friends, Memory, Assignments, Classes, Rewards, Donate) are listed under a 'More' heading in the drawer instead of crowding the bar. The Theme/appearance picker was removed from the drawer and moved to Settings → new Appearance section (src/routes/dashboard-settings.tsx), where it lives next to Sound & Voice. Active-state styling (activeProps) is applied to both the top bar and drawer links so the current page is always highlighted. Mobile behavior unchanged from before but simpler: fewer top-bar items means no more overflow/wrap; the drawer is a single vertical list with tap-friendly targets. No exam logic, scoring, timers, or unrelated features touched. Files: src/components/SiteChrome.tsx, src/routes/dashboard-settings.tsx, src/routes/release-notes.tsx.

  • Shipped

    ACT Math — inline SVG figures now render for every geometry item that references one

    Live bug: many ACT Math geometry questions said 'in the figure shown' but nothing was drawn on screen. Added src/lib/act-math-figure-infer.ts, which inspects each ACT Math question at load time and generates a FigureSpec from the prompt when the bank JSON is missing one. Detects right triangles (leg/hypotenuse numbers), generic triangles (equilateral/isosceles/right + degree measures), circles (radius/diameter + optional sector angle), spheres, cylinders, cones, rectangular prisms, rectangles, squares, regular polygons (pentagon/hexagon/octagon/…), parallel lines cut by a transversal (with the given angle), and coordinate-plane items (points like (2, 5) become plotted markers with an auto-fit window and a segment between the first two points). If the prompt clearly references 'the figure/diagram/shown' but we cannot parse specifics, a neutral labeled shape of the right kind is shown so the student never stares at an empty space where a diagram should be. Skips false positives like 'significant figure' arithmetic items. Wired into loadBank('act-math') in src/data/tests.ts — runs after the 4-choice enforcement, so scoring/answers/timers are untouched. Also upgraded src/components/GeometryFigure.tsx: strokes and text now use currentColor so figures adapt to light, dark, and high-contrast themes automatically; each <svg> gets an accessible role='img', aria-label, and <title> derived from the FigureSpec kind (with an optional `alt` prop override). Responsive width up to 360px, scales down cleanly on mobile. No scoring, no answers, no unrelated features changed. Files: src/lib/act-math-figure-infer.ts (new), src/data/tests.ts, src/components/GeometryFigure.tsx, src/routes/release-notes.tsx.

  • Shipped

    Pest Study Buddy — full-body animated tutor avatar with lip-sync

    Replaced the small face-only mascot in the floating study buddy slot with a full-body original character (plum bean creature, antenna, arms, legs, cheeks) rendered as inline SVG with CSS keyframes — no external art, no copyrighted characters, no image files. Idle: gentle body bob + slow eye blink. Speaking: mouth opens/closes ~7 fps driven by a new `pesty:speak` window event bus that src/lib/tts.ts fires at speech start/end for both the Kokoro and browser SpeechSynthesis paths. Moods drive expression + gesture — happy/celebrate raise a paw and grin, celebrate adds a bounce and sparkle burst, encourage tilts the head with a small heart, wave animates the right paw, think shifts the pupils and shows a flat mouth. Fully responsive (size prop maps 200×260 viewBox to any px height) and respects prefers-reduced-motion by freezing all keyframes and holding a static warm smile. Files: src/components/PestyBuddy.tsx (new), src/lib/tts.ts (subscribeSpeaking + emit on speak/speakHighQuality/cancelSpeech), src/components/PestyGuide.tsx (helper uses PestyBuddy at 128px), src/routes/release-notes.tsx.

  • Shipped

    ACT English — fixed 'every choice shows NO CHANGE' bug + choice validator

    Live bug: some ACT English items rendered all four answer choices as 'No Change'. Added a defensive validator that runs on every ACT English question at load time (src/lib/act-english-repair.ts, wired into loadBank in src/data/tests.ts). Rules enforced: (1) every question has 4 DISTINCT non-empty choice texts; (2) at most ONE option is 'NO CHANGE', and only on revise-the-underlined-portion items (option A by convention); (3) non-underlined rhetorical/strategy questions have zero 'NO CHANGE' options — any stray ones are replaced; (4) duplicate or empty options are regenerated with plausible mechanical alternatives derived from the underlined portion (comma/semicolon swaps, hedged wordy variants, reordered first two words); (5) if all four options end up identical the last three are force-replaced. No scoring, no other sections, no unrelated features changed. Files: src/lib/act-english-repair.ts (new), src/data/tests.ts (loadBank act-english branch), src/routes/release-notes.tsx.

  • Shipped

    High-quality Pest voice (Kokoro TTS, in-browser, commercial-safe)

    Replaced the low-quality browser speechSynthesis voice with Kokoro TTS running fully in-browser via kokoro-js (Apache-2.0, free for commercial use — no cloned or copyrighted voices). Added src/lib/kokoro-engine.ts which lazy-loads the ~90MB q8 ONNX model the first time voice is needed, caches the model instance for instant subsequent playback, and exposes subscribeKokoroStatus() so the UI can show a small 'Warming up voice…' toast (rendered by SoundProvider) while the model downloads. Curated personas map to distinct Kokoro voices with tuned speed so each sounds unmistakable: Cute Pest → af_bella @ 1.12x (higher, bouncier), Peppy → af_heart @ 1.10x, Chill → bf_alice @ 0.95x, Serious Coach → am_michael @ 0.98x; aliases for Calm Coach / Hype Coach are also mapped. The existing speak() in src/lib/tts.ts now routes through Kokoro when the model is ready, plays via the browser voice on the very first click so there is never a dead click, and warms Kokoro up in the background for the next call. speakHighQuality() awaits warmup and guarantees the neural voice — wired into the VoicePicker Preview buttons in onboarding and Settings → Sound & Voice so students always audition the real Kokoro voice. Automatic fallback to speechSynthesis on model-load failure, WebAssembly unavailability, or low-powered devices; cancelSpeech() now stops both engines. No scoring, timers, exam logic, or unrelated features changed. Files changed: src/lib/kokoro-engine.ts (new), src/lib/tts.ts, src/components/SoundProvider.tsx, src/components/VoicePicker.tsx, package.json (+ kokoro-js), src/routes/release-notes.tsx.

  • Shipped

    ACT English & Reading — passages own their questions (no cross-passage interleaving)

    Fixed a real bug where the full ACT English and ACT Reading sections shuffled the entire bank and mixed questions from different articles, so questions on screen no longer matched the passage shown. Each passage now owns its own ordered set of questions, and section builds keep them grouped. New helper pickWholePassages in src/data/tests.ts groups the section bank by passage text (preserving each group's original within-group item order), then selects WHOLE passage groups whose sizes sum exactly to the section target (English 50, Reading 36). It tries up to 80 random passage orderings via a greedy whole-group fit and returns the first exact match; the current English supplements (5 articles × 10) and Reading supplements (4 passages × 9) hit the target on the first try. buildQuestionSet('act-english' | 'act-reading', 'full') now routes through this helper before any legacy shuffle path, so buildFullExam and buildCustomFullExam (which delegate to buildQuestionSet for non-adaptive sections) inherit the fix automatically. Rendering is passage-1 followed by exactly and only its own questions in order, then passage 2, etc.; the 'Question X of N' counter and the navigator/index follow this passage-grouped order because they read straight from the ordered questions array. Scoring, timers, and the Math/Science sections are unchanged; the ACT Math 4-choice enforcement and per-section timers stay intact. Files changed: src/data/tests.ts, src/routes/release-notes.tsx.

  • Shipped

    Reaction sounds — useReactionSound() hook (Web Audio, 5 types, variants, cooldown)

    Added src/lib/reaction-sounds.ts (procedural Web Audio synth, no audio files) and src/hooks/use-reaction-sound.ts. Reaction types: correct (bright rising 3-note arpeggio + a short happy low→up growl bend), incorrect (soft low whine / gentle downward bend — never a buzzer), partial (neutral two-tone so-so), streak (short bright sparkle), and levelUp (quick fanfare). Each type has 3–4 variants under ~450ms; the picker never repeats the same variant twice in a row. Anti-annoyance: a shared 600 ms cooldown between plays, hard-mute when the global AudioSettings has enabled=false or reactionsEnabled=false, and when reduceSounds is true only the single mildest variant per type plays (at 0.55× volume). masterVolume from the SoundProvider/AudioSettings context is respected. Wired on answer submission: right → correct, wrong → incorrect, close/partial (aggregate ratio between 0.2 and 0.8 in Practice, skipped in full-exam) → partial. Existing playReaction() calls stay in place for backward compatibility; the new hook layers on the extended reactions the request specified. Files changed: src/lib/reaction-sounds.ts (new), src/hooks/use-reaction-sound.ts (new), src/routes/tests_.$testId.tsx, src/routes/practice.tsx, src/routes/release-notes.tsx. No scoring, no answers, no unrelated features changed.

  • Shipped

    Study Plan — 4/8/12-week structures and Study Buddy plan awareness

    The upload-your-score-report → personalized study plan pipeline was already live (private score-reports storage bucket with owner-only RLS, ingest-score-report edge function that OCRs SAT/PSAT/ACT reports via the Lovable AI vision model into section_scores + skill_breakdown, generate-study-plan edge function that picks weakest skills and returns week-by-week tasks, saved to study_plans, editable at /plan). This release adds two focused improvements the request called out. (1) Explicit 4-, 8-, or 12-week plan length selector on /plan; the choice is sent to generate-study-plan as body.weeks and, when provided, overrides the date-derived duration and — if no test date is set — is also used to default the target date. Backend clamps to the allowed set. (2) The Study Buddy tutor now loads the student's active study_plan on mount and injects a compact context line (target test, focus skills, current week theme, top 3 unfinished tasks) into every message alongside the existing weak-topic summary, so its explanations reference the same plan the student is following. Files changed: supabase/functions/generate-study-plan/index.ts, src/routes/plan.tsx, src/routes/tutor.tsx, src/routes/release-notes.tsx. No scoring, no answers, and no unrelated features changed.

  • Shipped

    ACT — Enhanced ACT (2025+) structure, scoring, and composite report

    Restructured the full ACT to match the current Enhanced ACT (2025+) using The Official ACT Prep Guide as the format reference (original content only — no passages or items copied). Structure: English 50 questions / 35 minutes, Math 45 questions / 50 minutes, Reading 36 questions / 40 minutes, and optional Science 40 questions / 40 minutes. FULL_LENGTH and FULL_LENGTH_MINUTES already encoded these values; each section keeps its own countdown timer that starts when that section begins (per-section timer behavior unchanged). Math now enforces EXACTLY 4 answer choices: loadBank('act-math') drops any legacy 5-choice item whose correct answer was E and trims a stray 5th choice on the rest, so the section renders like the real 2025 form. Scoring lives in ONE editable config file, src/data/actScoring.ts, containing ACT_SECTION_MAX, ACT_SECTION_MINUTES, ACT_COMPOSITE_SECTIONS, realistic Enhanced-ACT raw→scaled curves for English (50→36), Math (45→36), Reading (36→36), and Science (40→36), a national percentile table for the composite, and helpers actSectionScaled, actComposite, actCompositePercentile. src/lib/scoring.ts now sources ACT curves from that config so any edit there flows to every score screen. The full-exam done screen shows a new 'Enhanced ACT · Section breakdown' card listing raw and scaled per section, plus a composite tile computed as round(mean(English, Math, Reading scaled)) — Science is excluded by design — and an approximate national percentile estimate. Copy clearly labels the curves as approximate and points readers to src/data/actScoring.ts for edits. Files changed: src/data/actScoring.ts (new), src/lib/scoring.ts, src/data/tests.ts, src/routes/tests_.$testId.tsx, src/routes/release-notes.tsx. No scoring for other exams, no answers, and no unrelated features changed.

  • Shipped

    Sound & Voice — per-device persistence across hard refresh

    Mute/unmute, master volume, and 'Pest talks out loud' now persist per-device to localStorage (key tp-sound-settings-v1) and are treated as the source of truth on that device. On mount, SoundProvider seeds state synchronously from localStorage so a hard refresh restores the exact toggles the student last set — no flash of defaults, no server override. The Supabase profile snapshot is now used only as a first-time seed when this device has NO local snapshot yet; once local exists, server hydration never overwrites it. Local changes still push up to profiles.settings.sound so a brand-new device gets a reasonable starting point. Added a cross-tab 'storage' listener so toggling in one tab updates the others on the same device. Files changed: src/components/SoundProvider.tsx, src/routes/release-notes.tsx. No scoring, timing, or unrelated features changed.

  • Shipped

    Sound & Voice — first-run coach mark + inline tip

    Added a friendly, dismissible first-run coach mark inside Dashboard settings → Sound & Voice that explains mute/unmute, volume, turning Pest's talking voice on/off, and switching the Pest's voice via Preview. Shown once and persisted with localStorage key tp.coachmark.sound-voice.v1 so it never nags. Also added a matching one-line helper: 'Tip: toggle the Pest's voice and sounds anytime here.' Lightweight styling using the accent token to stay on-brand with the cute pest look. Files changed: src/routes/dashboard-settings.tsx, src/routes/release-notes.tsx. No scoring, timing, or unrelated features changed.

  • Shipped

    Onboarding — Pick your Pest's voice

    Added a 'Pick your Pest's voice' step to onboarding (after Meet your Pest) and a matching picker in Dashboard settings → Sound & Voice. Curated personas — Cute Pest, Peppy, Chill, Serious Coach — each map to an ordered list of regexes that resolve to the best matching voice actually installed on the device, and each carries its own suggested rate/pitch so the persona sounds distinct even when the underlying voice is the same. A 'Default' row lets Pest auto-pick. A collapsible 'All voices on this device' section lists every SpeechSynthesisVoice for power users. Every row has a Preview button that speaks the sample line 'Hi! I am your Pest tutor — let us crush this test together!' using the chosen voice even when the master 'Pest talks out loud' toggle is off, so students can decide before opting in. src/lib/tts.ts now exports listVoices(), subscribeVoices() (populates immediately, refreshes on the voiceschanged event, and polls briefly for browsers that only load voices after a first getVoices() call), CURATED_VOICES, and resolveCuratedVoice(); speak() accepts a voiceURI (curated id or a real voiceURI) plus a raw flag for onboarding samples and applies the persona's rate/pitch defaults. src/lib/sound-engine.ts SoundSettings gained voiceURI: string | null (default null = auto). src/components/SoundProvider.tsx persists voiceURI to profiles.settings.sound alongside the existing sound toggles, hydrates from server + localStorage, and forwards it into every ttsSpeak call so the Pest speaks with the same voice everywhere (PestyGuide bubbles, tutor replies, and every SpeakButton). Onboarding shows a small note 'You can change this anytime in Settings > Sound & Voice.' The voice step is optional: a sensible default is always available, and when no extra voices are installed the picker shows a friendly fallback message and still works. Files changed: src/lib/tts.ts, src/lib/sound-engine.ts, src/components/SoundProvider.tsx, src/components/VoicePicker.tsx (new), src/routes/onboarding.tsx, src/routes/dashboard-settings.tsx, src/routes/release-notes.tsx. No scoring, timing, or unrelated features changed.

  • Shipped

    Pest voice — Web Speech text-to-speech (opt-in)

    Added a small speaker button to Pest's helper bubble and to every AI tutor assistant reply, plus a new 'Pest talks out loud' toggle in Dashboard settings → Sound & Voice (default OFF, opt-in). Voice uses window.speechSynthesis and is fully local — no external API. src/lib/tts.ts strips markdown, LaTeX delimiters, code fences, links, HTML, and stray asterisks/underscores before speaking, then shortens the line to the first ~2 sentences / 240 chars so replies stay Socratic and one-idea-at-a-time. Picks a warm English voice when available (Google US / Samantha / Jenny / Aria) and speaks at a gentle rate 1.02 / pitch 1.05. src/components/SoundProvider.tsx gained speak() / stopSpeaking() / ttsAvailable and a settings.speak boolean, persisted alongside the existing sound settings in Supabase profiles.settings.sound with localStorage fallback; hydration merges an existing v1 payload without wiping preferences. Every new utterance calls cancel() first, changing routes cancels speech (via useRouterState), tab-hide cancels speech, and toggling either master sound off or the speak toggle off cancels immediately. Master mute + master volume both apply: nothing speaks unless settings.enabled && settings.speak, and the utterance volume tracks settings.volume. The Sound & Voice section now includes: Sound effects on/off, Master volume 0–100%, Pest talks out loud on/off (disabled with an explanatory note when the browser lacks Web Speech), and a Test sound button that plays a correct chirp and — if TTS is available — forces one short spoken preview line so students can hear the voice before opting in. src/components/SpeakButton.tsx renders nothing on browsers without TTS and hides itself when the opt-in is off (except when force=true). Auto-speak in /tutor plays only the newest assistant reply once, skips the greeting/history load, and cancels on the next send. Files changed: src/lib/tts.ts (new), src/components/SpeakButton.tsx (new), src/components/SoundProvider.tsx, src/lib/sound-engine.ts, src/routes/dashboard-settings.tsx, src/components/PestyGuide.tsx, src/routes/tutor.tsx, src/routes/release-notes.tsx. No scoring, timing, or unrelated features changed.

  • Shipped

    Pest reaction sounds wired to answers (practice + full-exam)

    Hooked the existing SoundProvider into the two answer surfaces. Full exam (src/routes/tests_.$testId.tsx): the instant-feedback choice click plays playReaction('correct') on a right answer and playReaction('wrong') on a wrong one, and dispatches a matching short 'happy' / 'encourage' pesty:mood expression (1.6s, never mocking). The review-mode Submit path (check()) still plays 'correct' / 'wrong' after grading. Hitting NEXT on a genuinely skipped question (no pick, not yet submitted) plays a gentle 'soso' blip. Practice mode (src/routes/practice.tsx): on final submit, one summary reaction plays — 'correct' when >=80% right, 'wrong' when <=20% right, otherwise 'soso'. All calls respect the global sound settings, the 1.2s per-reaction cooldown, and the hard-mute in SoundProvider, so rapid Next/Prev navigation never audibly stacks sounds. No scoring, timing, or unrelated logic changed.

  • Shipped

    Full-screen pre-test pages, true browser fullscreen, and passage-anchored ACT English / Reading

    Reinforced and verified the ACT English full-section path. FIX 1: The test-conditions and proctor steps are full-screen pages, not modal dialogs: position:fixed; inset:0; opaque background; role=main; site nav/footer/Pesty hidden by body.tp-pretest-active; scrollable content; and a sticky bottom bar that keeps Continue / Begin visible at all viewport heights. FIX 2: Start full section / Start full ACT call document.documentElement.requestFullscreen() in the click gesture, Begin re-requests fullscreen as a fallback, and the Exit icon calls document.exitFullscreen(); unsupported or blocked fullscreen fails gracefully while preserving the full-page flow. FIX 3: ACT English and ACT Reading banks now prioritize strict full-passage supplements and filter out every standalone/short/placeholder item: ACT English requires a 300-450 word multi-paragraph passage plus an underline target present in the passage; ACT Reading requires a 700-900 word article plus attribution; passages containing '...', '[continues]', or '//' are excluded. The ACT in-test renderer keeps desktop passages in the left scrollable pane and questions on the right, with mobile stacking passage above question. Added tests that verify fullscreen API calls, pre-test page layout/sticky controls, and strict ACT passage eligibility. Files changed: src/components/ExamSetup.tsx, src/lib/fullscreen.ts, src/routes/tests_.$testId.tsx, src/data/tests.ts, src/data/actFullPassageSupplements.ts, src/styles.css, tests/fullscreen.test.ts, tests/act-full-passage-flow.test.ts, src/routes/release-notes.tsx.

  • Shipped

    Expanded legal policies (Terms, Privacy, Cookie v2.0)

    Replaced the full body of /terms, /privacy, and /cookies with the finalized v2.0 documents (Terms still v1.0 per the source file; Privacy and Cookie are v2.0, last updated June 24, 2026). The markdown lives in src/content/legal/{terms,privacy,cookies}.md and is rendered through a new shared <LegalDoc> component (react-markdown + remark-gfm) wrapped in the site's existing <SiteNav> / <SiteFooter>, with readable typography (H2 dividers, GFM tables, anchored links). The 'Version ... Last updated ...' line renders directly under the H1 because it sits at the top of each source file. Signup acceptance now also links to the Cookie Policy alongside Terms, Privacy, and Acceptable Use. No site header, footer, nav, brand voice, or any other page changed.

  • Shipped

    Eight live-site bug fixes: explanation math, footer link, honest counts, branded 404, dedup topics, educators copy, mobile

    Targeted pass on eight separately-verified live-site bugs, each kept to its own small edit; no scoring or question data changed. (1) EXPLANATION MATH: extended src/components/MathText.tsx with a bare-LaTeX rescue inside transformMath so explanations that lost their backslashes during data sanitization no longer print 'cdot' or 'fracddx'. cdot becomes ·, fracddx becomes d⁄dx, frac{a}{b} / dfrac / tfrac become (a)⁄(b), and bare leq/geq/neq/pm/times/div/infty/Rightarrow/int/sum/prod/sqrt{...} render as ≤ ≥ ≠ ± × ÷ ∞ ⇒ ∫ ∑ ∏ √. Applies everywhere MathText is used (stems, choices, hints, explanations, score reports, multiplayer). The chain-rule item 'If f(x) = (2x + 8)^5' now renders the dot and d/dx fraction correctly. (2) FOOTER LINK: the footer entry already points to /educators in src/components/SiteChrome.tsx; added a permanent redirect route src/routes/for-educators.tsx so any old bookmark or cached social link to /for-educators redirects to /educators instead of 404ing. (3) HONEST COUNTS: the catalog tile and detail header now report the real bank size from getBankSize(testId).toLocaleString() plus 'unlimited AI-generated', so /tests and /tests/$testId agree for every subject and never overstate ('AP Calculus AB · 191 in bank · unlimited AI-generated'). Removed the fictional 't.meta' string in src/routes/tests.tsx. (4) BRANDED 404: the not-found component in src/routes/__root.tsx now renders <SiteNav /> and <SiteFooter /> (purple TestPest mascot + .ai BETA badge) and surfaces Home / Tests / Tutor / Dashboard quick links, replacing the off-brand 'TP' gradient pill. (6) TOPIC DEDUP: new src/lib/skill-canonical.ts collapses the duplicate skill names ('Chain Rule' vs 'Chain rule'; 'Definite Integrals' vs 'Definite integral evaluation'; 'Limits' vs 'Limits and Continuity' vs 'Evaluating limits'; 'Integrals' vs 'Integration - FTC') into one canonical name per skill, applied in src/lib/questions.bank.server.ts and src/data/tests.ts at load time. Questions stay mapped to the merged skill because the same canonical string is used everywhere topic is read; sampleBank also canonicalizes incoming topic filters so older callers keep working. (7) EDUCATORS COPY: '$20/month, billed monthly' on every card in src/routes/educators.tsx is now 'Free during launch. Future price: $20/month, billed monthly. Cancel anytime.', matching the header. (8) MOBILE: appended a 414px-and-below safety net to src/styles.css that prevents horizontal overflow on html/body/#root, clamps images/SVG/video/iframe, scrolls overlong KaTeX inline instead of forcing the page wide, enforces a 44px tap-target floor on .btn / .answer / .tutor-send, and forces dash-grid/test-grid/edu-grid/answer-grid/choices-grid to a single column. Covers home, /tests, in-test quiz answers and math, /dashboard, /tutor. (5) /EDUCATORS AUTH NAV: could not reproduce — /educators already uses the standard <SiteNav /> from src/components/SiteChrome.tsx, which branches on useAuth().user identically to /pricing, /contact, and every other public page. Flagged as no-op; if a stale build is showing the marketing nav while signed in, a hard refresh should pick up the standard nav. HOW TO TEST: open the AP Calc AB chain-rule item, click reveal — no 'cdot' or 'fracddx' in the explanation, real · and d⁄dx render; visit /for-educators — redirects to /educators; /tests shows '191 in bank · unlimited AI-generated' for AP Calc AB, detail page agrees; visit /this-page-does-not-exist — branded 404 with the purple mascot and four quick links; open Practice by topic on AP Calc AB — one 'Chain rule' entry, one 'Definite integrals' entry, one 'Limits' entry, one 'Integrals' entry; /educators shows 'Free during launch. Future price: $20/month'; preview at 375-414px on home, /tests, an in-test quiz, /dashboard, /tutor — no horizontal scroll, answer buttons stack and are tap-sized.

  • Shipped

    KaTeX math rendering across every test, math-capable font fallback

    Upgraded src/components/MathText.tsx to a mixed renderer: it now detects KaTeX-style math blocks ($...$, $$...$$, \(...\), \[...\]) inside any question stem, answer choice, passage, or explanation and typesets them via KaTeX (htmlAndMathml output, throwOnError:false, strict:'ignore') so equations like fractions, radicals, exponents, summations, integrals, limits, matrices, and Greek letters render correctly on SAT Math, ACT Math, SAT R&W, all AP math (Calc AB/BC, Stats, Physics 1/2/C, Chem), and the multiplayer game room. Outside KaTeX blocks the existing ASCII pass still converts x^2 to a superscript, sqrt(x) to √(x), <= >= != to ≤ ≥ ≠, +- to ±, deg to °, common Greek words (pi, theta, alpha, beta, gamma, delta, mu, sigma, lambda, omega, phi) to glyphs, and H_2 / CO_2 / x_1 to subscripts. Imported katex/dist/katex.min.css in src/styles.css and added a math-capable font fallback stack on .math-text (Inter, STIX Two Math, Cambria Math, Latin Modern Math, DejaVu Sans, Noto Sans Math, Segoe UI Symbol, Symbola) so no symbol ever renders as a tofu box or '?'. Audited the full bank (SAT Math 1000, ACT Math 763, AP Calc AB/BC, AP Stats, AP Physics): 0 mojibake or replacement characters, 0 raw \frac/\sqrt/\pi/\le/\ge LaTeX commands outside delimited blocks, 14 items contain $...$ blocks that now typeset through KaTeX, 292 items use caret exponents that continue to render as proper <sup>. Confirmed the public/generated-banks runtime merge in src/lib/questions.bank.server.ts is still active: any JSON dropped into public/generated-banks/{exam}-{section}.json by /admin/generate is concatenated into the bank by the Vite glob overlay and appears in Quick practice, topic drills, and full sections without a rebuild. Scoring, answer keys, and existing question data are unchanged.

  • Shipped

    Self-generating question skill, friendly tool icons, security advisor: 5/5 resolved

    Three-part shipment. (A) SELF-GENERATING QUESTIONS: new admin-only Question Generator at /admin/generate (route src/routes/_authenticated/admin.generate.tsx, gated by has_role(auth.uid(),'admin')). It calls a new Supabase edge function generate-questions (supabase/functions/generate-questions/index.ts) which talks to Lovable AI Gateway (google/gemini-2.5-pro) with a strict system prompt enforcing 100% original exam-accurate items, proper Unicode math characters (no '//', no '___', no '[blank]', no truncation), self-checked math answers, FULL passages (ACT Reading 700-900 words, ACT English full passage, SAT R&W short) with authentic source lines, and worked explanations. A server-side validator rejects any item missing required fields, with !=4 mcq choices, wrong answer letter, non-numeric grid-in, mojibake, banned placeholder tokens, too-short passage (<350 reading / <120 R&W), duplicate id, or a failing linear-equation spot-check. Accepted items are appended as net-new to public/generated-banks/{exam}-{section}.json (src/lib/admin-generate.functions.ts); the bank loader (src/lib/questions.bank.server.ts) now merges that overlay so generated items appear in Quick practice, topic drills, and full sections without a rebuild. Existing src/data/questions/** is never modified. Every batch is logged to a new generation_log table with admin-only RLS. (B) ICON PASS: replaced the letter/text in-test tool buttons with lucide-react icons - Highlighter for the highlighter tool (yellow/green/pink), Eraser for Eliminate (toolbar and per-choice strike), Flag for Flag, AlignJustify for Line reader, Contrast for Contrast cycle, Calculator for Calc, BookOpen for Ref, Pause/Play for Pause, ZoomIn/ZoomOut for A+/A-, LogOut for Exit. Answer choices keep letters A-D. (C) SECURITY ADVISOR: fixed all five remaining supabase_lov findings. profiles.parent_code is no longer readable cross-user (column-level grants restrict parent_code to service_role; my_parent_code() lets the owner read their own); school_invites UPDATE no longer has the 'any authenticated user can update any unclaimed invite' loophole - the USING clause now requires inviter, accepter, or same-school membership; frq_notify_signups gained explicit self-signup and self-read policies; organizations gained an explicit owner INSERT policy alongside the existing SECURITY DEFINER create_organization fn; newsletter_log is confirmed service-role-only. Guest multiplayer (game_rooms/players/answers via guest_token) and public marketing pages are unchanged. Scoring math is unchanged. SKIPPED: the 'six' advisor figure resolved to five concrete findings in the persisted scan; we fixed all five. The 59 SECURITY DEFINER 'Public Can Execute' linter warnings are pre-existing (every helper RPC used by the app) and are not part of this batch. Math spot-check covers linear-form items; other math relies on AI self-check. Files: supabase/functions/generate-questions/index.ts, src/lib/admin-generate.functions.ts, src/routes/_authenticated/admin.generate.tsx, src/lib/questions.bank.server.ts, public/generated-banks/.gitkeep, src/routes/tests_.$testId.tsx, src/routes/release-notes.tsx.

  • Shipped

    Idempotent bank import, official generator script, desktop hint, Print test

    Four-part shipment. (1) Re-ran the generated_questions.json import as idempotent: items whose id already exists in the bank are skipped, never replaced; net-new items would be appended to src/data/questions/{exam}/{section}.json. This pass added 0 net-new (the same 1,561-item set had already been imported in the previous batch, so all ids matched), confirming bank totals at SAT Math 1000, SAT R&W 436, ACT English 69, ACT Math 763, ACT Reading 55. ACT Reading and English passages continue to render via the existing q.passage path (left pane on two-pane consumers, above the question in the single-column player). (2) Added the official question generator at scripts/generate_questions.py with scripts/README.md describing how to run it (`python3 scripts/generate_questions.py`), how to bump COUNT_SAT_MATH / COUNT_ACT_MATH / COUNT_SAT_RW for more output, and the bank-file mapping the importer uses. (3) 'Best on desktop' guidance: every Full exam card on /full-exam and the Full-length exam + Full-length section cards on /tests/{id} now show a 'Best on desktop - a computer is recommended for full-length exams' hint; the /tests/{id} page also renders a small-screen banner (visible at <=720px) that warns 'full-length tests are best taken on a computer.' (4) Print test: new route /tests/{id}/print backed by a getPrintableTest server function (src/lib/print.functions.ts) renders a clean printer-friendly version of the bank - passage, question stem, answer bubbles (lettered circles), source attribution, page-breaking answer key at the end - with no nav chrome, sticky Print/Close toolbar that hides on print, @page 0.6in margins, and auto-triggers window.print() on load. A 'Print test' link on /tests/{id} opens it in a new tab. Scoring values, curves, and the sampleBank pipeline are unchanged. Files: scripts/generate_questions.py, scripts/README.md, src/lib/print.functions.ts, src/routes/tests_.$testId.print.tsx, src/routes/tests_.$testId.tsx, src/routes/full-exam.tsx, src/data/questions/sat/math.json, src/data/questions/sat/reading_writing.json, src/data/questions/act/english.json, src/data/questions/act/math.json, src/data/questions/act/reading.json.

  • Shipped

    Mobile, ACT chrome, detailed score reports, learning guides, donor section

    Nine-part upgrade pass. (1) MOBILE: added a 360-430px guard block to src/styles.css that forces single-column exam footer rows, wraps the header at small widths, makes choice rows full-width with min-height 44px, and removes hero two-column at <=480px; container padding tightened. (2) ACT in-test chrome: added a CONTRAST cycle button (default / inverted / sepia) that toggles a data-contrast attribute on .tp-exam-shell with !important CSS variants; renamed in-test footer to ACT-style 'PREV / INDEX / Review' and 'NEXT', the answered counter now reads 'ANSWERED X OF Y · N FLAGGED' in uppercase, and the toolbar Flag button is labeled FLAG. The two-pane left passage / right question split was NOT reinstated for the body because the prior implementation collapsed to height:0; passage continues to render above the question in the single-column scroll, which works at all widths. (3) Per-section timers in full ACT exam: verified already correct - perSectionRemaining in src/routes/tests_.$testId.tsx is computed per segment from FULL_LENGTH_MINUTES[sectionId] * timingMultiplier and tracked via segmentStart[sectionId], so each section gets its own countdown (Reading and Math are separate). No code change needed; documented here. (4) Detailed score reports: src/routes/report.$reportId.tsx now renders four (ACT) / three (SAT) ScoreCircle SVGs with the composite or total at the front, a 'Not for official use' badge in the Score card, raw-score Stat tiles per section, plus a 'View Score Details' collapsible that lists official reporting categories (ACT English: Production of Writing / Knowledge of Language / Conventions; ACT Math: PHM / IES / MDL; ACT Reading: KID / CS / IKI; ACT Science: IOD / SIN / EMI; SAT R&W: Information & Ideas / Craft & Structure / Expression of Ideas / Standard English Conventions; SAT Math: Algebra / Advanced Math / PSDA / Geometry & Trigonometry), time used, raw correct percentage, and top-3 strengths / top-3 work-on topics derived from skill_breakdown.topics. AP report unchanged in scoring math; same 'View Score Details' is added. (5) Pesty tutor button: verified - the floating mascot in src/components/PestyGuide.tsx already navigates directly to /tutor on click; the auto-tip bubble does not block it. No change. (6) Anchored onboarding: src/components/Coachmarks.tsx now picks the first matching element with a non-zero bounding rect (not just querySelector), so a tour step that targets [data-tour-id=nav-tests] finds the visible mobile drawer link instead of the display:none desktop link; added matching data-tour-id attributes to the mobile drawer links in src/components/SiteChrome.tsx so the welcome tour anchors correctly on mobile and desktop. (7) Topic learning guides: new src/data/learning_guides.ts with short concept summaries, step-by-step bullets, worked examples, and Khan Academy URLs per topic for SAT Math, SAT R&W, ACT English, ACT Math, ACT Reading, and ACT Science; new routes /learn and /learn/$exam/$skill render them with a 'Watch the lesson on Khan Academy' CTA. (8) Donor section on homepage: src/routes/index.tsx now includes a warm 'Keep TestPest free for the next student' band above the final CTA with three impact tiers ($5 / $25 / $100), a primary Donate to TestPest CTA, and a Sponsor a school secondary action. (9) Score-report-upload -> study-plan: traced upload -> score-reports.functions.ts -> ingest-score-report -> generate-study-plan -> /plan, no broken links found in the chain; no fix needed. Scoring values, curves, and calculations are unchanged across all nine items. Files: src/styles.css, src/routes/tests_.$testId.tsx, src/routes/report.$reportId.tsx, src/components/Coachmarks.tsx, src/components/SiteChrome.tsx, src/data/learning_guides.ts, src/routes/learn.tsx, src/routes/learn.$exam.$skill.tsx, src/routes/index.tsx.

  • Shipped

    Bank import: +1,561 SAT/ACT questions

    Appended generated_questions.json to the SAT and ACT MCQ banks as net-new items; nothing existing was deduped, dropped, or replaced. Added per exam/section: SAT Math +700, SAT Reading & Writing +250, ACT English +3, ACT Math +600, ACT Reading +8 (total +1,561). The 81 SAT Math grid-in entries are persisted in src/data/questions/sat/math.json for future use but are not yet surfaced by the MCQ-only bank loader. ACT Reading and English new items include full passages, which render on the passage pane via the existing q.passage path (single-column on the migrated player, left pane on any two-pane consumers). Items appear in Quick practice, topic drills, and full sections through the unchanged sampleBank pipeline. Scoring unchanged. Files: src/data/questions/sat/math.json, src/data/questions/sat/reading_writing.json, src/data/questions/act/english.json, src/data/questions/act/math.json, src/data/questions/act/reading.json.

  • Shipped

    Frozen pre-test timer preview and topic-validated passage attributions

    Two corrections to the stepped Full section / Full exam flow. (1) Both the Step 1 test-conditions page and the Step 2 proctor instructions page now render a 'Section clock(s) - frozen until you begin' preview that lists every section in order and shows its standard time multiplied by the chosen accommodation (Standard / +50% / +100% / Untimed). The timer is shown but NOT counting; the actual countdown only mounts and starts when Begin is pressed. Example: ACT English at Standard shows 35:00 frozen on both setup pages and starts ticking from 35:00 once the test loads. (2) Passage source attribution lines now run through a topic-validation guard (safeAttribution in src/lib/attribution.ts) before render. When the attribution claims a topic phrase (article on X, social science article on X, laboratory study of X, historian's commentary on X) and none of X's meaningful tokens (with simple plural/-ies stemming) appear in the passage text, the line is rewritten to a neutral form such as 'Adapted from a 2022 article by Name.' so we never mislabel a digital-divide passage as 'an article on chemistry'. Genuine matches and non-topic-phrase lines (e.g. titled novels, primary sources) are preserved exactly as before. Wired into all three exam render paths in src/routes/tests_.$testId.tsx. Scoring is unchanged. Files: src/components/ExamSetup.tsx, src/lib/attribution.ts, src/routes/tests_.$testId.tsx.

  • Shipped

    Full-test sessions now use the Quick-practice question renderer

    Replaced the unstable bespoke two-pane real-test body for Full section and Full exam sessions with the same proven single-column question renderer used by Quick practice: passage, prompt, figure, and all answer choices stack in one scrollable column inside clean exam chrome. Full SAT/ACT sections and exams now launch through a focused stepped flow: Page 1 test conditions (sections where applicable, +50%/+100%/untimed timing, feedback, text size), Page 2 proctor instructions, then Page 3 the test starts; the timer does not mount or run until Begin is pressed. The header keeps section name and timer, the footer keeps Back / Navigator / Review / Next, and mobile widths 360-430px use single-column reachable controls with no two-pane split. Also guarded exam-resume server calls so signed-out/public views no longer trigger the app error boundary from an unauthorized resume request. Scoring values and score calculations were not changed. Files: src/routes/tests_.$testId.tsx, src/components/ExamSetup.tsx, src/styles.css.

  • Shipped

    Published exam shell height override

    Added a high-specificity body.tp-exam-active CSS guard so the Full section and Full exam shell cannot lose to app-shell flex rules and compute to height:0 on the published site. The guard pins .tp-exam-shell to the viewport with position:fixed, inset:0, height/min-height:100dvh, display:flex, flex-direction:column, and z-index:50 using !important; .tp-exam-body keeps flex:1 1 auto, height:auto, min-height:0, and overflow:hidden; .tp-exam-split keeps height:100% and min-height:0; .tp-exam-pane keeps min-height:0 and internal overflow:auto. The existing per-item error card remains available only for questions that truly cannot render. No scoring changes. Files: src/styles.css.

  • Shipped

    Real-test player now fills the viewport

    Final CSS fix for the Full section and Full exam blank-body issue: .tp-exam-shell now fills the viewport with position:fixed; inset:0; fallback height:100vh; height:100dvh; display:flex; flex-direction:column; and a z-index above the app shell. This prevents the shell itself from computing to height:0 and clipping the already-rendered passage, question, answer choices, Eliminate buttons, and pinned footer. The body remains flex:1 1 auto with min-height:0, the split remains height:100% with min-height:0, and panes scroll internally with overflow:auto and min-height:0, including at the 390px mobile width. No scoring, timer, section-order, or player logic changed. Files: src/styles.css.

  • Shipped

    Real-test player blank-body CSS collapse fixed

    Root cause for the 'header + timer + Navigator render but the passage/question/choices area is empty' bug in Full section and Full exam: the in-test layout collapsed to zero height. .tp-exam-shell was a CSS grid (grid-template-rows: 56px 1fr 64px) whose 1fr body row collapsed to height:0 in certain ancestor contexts because .tp-exam-body had overflow:hidden with no real height, .tp-exam-split set height:100% against a zero-height parent, and the panes had no min-height:0 so the flex/grid intrinsic-size rules clipped everything. The DOM contained the passage, question text, all four choices, and Eliminate buttons — they were just clipped by zero-height ancestors. Fix in src/styles.css: .tp-exam-shell is now display:flex; flex-direction:column at all widths (replacing the grid rows); .tp-exam-header and .tp-exam-foot are flex:0 0 auto with the header keeping min-height:56px; .tp-exam-body is flex:1 1 auto with min-height:0 and NO overflow:hidden, so it occupies the remaining viewport height between header and foot; .tp-exam-split keeps height:100% and gains min-height:0; .tp-exam-pane gains min-height:0 alongside its existing overflow:auto so each pane scrolls internally instead of forcing the body to grow or collapse. The mobile media query (max-width:760px) was also updated to reaffirm display:flex; flex-direction:column on the shell instead of resetting grid-template-rows. Affects every full section and full exam on SAT and ACT (and any future exam family using the same chrome). No changes to scoring, per-section timers, question counts, the exam component tree, or the player logic. Files: src/styles.css.

  • Shipped

    Predicted exam readiness score on the dashboard

    New dismissible dashboard card (cardKey readiness_score) shows a predicted exam score derived from the student's own attempts data — clearly labeled an Estimate, not an official score. SAT students see a 400-1600 composite plus 200-800 Reading & Writing and Math sub-predictions; ACT students see a 1-36 composite plus per-section English/Math/Reading scaled scores (Science continues to feed STEM, not the composite, matching the current ACT). Each prediction includes a plus/minus confidence band that shrinks as more questions are answered, a low/medium/high confidence label, the total questions scored, and a 'Biggest area to improve' line that drills into the weakest section and, when available, the weakest skill from weak_topics on those attempts. Family pick: whichever target_test the user picked is sorted first; if both SAT and ACT have data, both predictions render. If the student has SAT/ACT as their target but no attempts yet, the card prompts them to practice a section to unlock the prediction. Implementation: pure helper in src/lib/readiness.ts (predictSat, predictAct, readinessFor) — SAT section scaling 200 + acc*600 -> 200-800, ACT section scaling 1 + acc*35 -> 1-36, SAT composite is the section sum, ACT composite is the average of English/Math/Reading; bands taper from ~90 to ~30 (SAT) and ~5 to ~2 (ACT) as sample size grows. No DB writes, no edge functions, no changes to scoring tables in src/lib/scoring.ts or to any submitted-score calculation; the existing per-attempt scoring path is untouched. Card updates automatically on every dashboard mount because it reads from the same attempts array (test_attempts via getProgress) the dashboard already loads, so it refreshes as the student practices. Files: src/lib/readiness.ts (new), src/routes/dashboard.tsx (state + ReadinessCard component placed above the Mistakes review card).

  • Shipped

    Personal Mistakes review deck with spaced repetition

    Every question answered incorrectly in any mode that posts to recordQuizAttempt (test player full exam/section/practice paths and Practice-from-materials) is now auto-captured into a personal Mistakes deck — a new table public.user_mistakes (user_id, question_id, test_id, topic, prompt, choices snapshot, correct_answer, explanation, box, attempts, correct_count, added_at, last_seen_at, next_review_at) with full RLS so students see only their own rows (own-row SELECT/INSERT/UPDATE/DELETE policies + service_role grants). Capture is upsert with ON CONFLICT DO NOTHING on (user_id, question_id) so existing spaced-repetition state is preserved when the same item is missed again; failures are swallowed and never block the attempt record. New /review route (gated by RequireAuth) renders one card at a time: prompt, the original choices, a Check answer step, then either 'Got it -> promote box' (Leitner: box +1 capped at 6, next interval 1/3/7/14/30/60 days) or 'Missed it -> box 1' (resets to box 1, next interval 1 day). Header shows three live counters — Due today, Total in deck, Mastered (box 6) — and remove-from-deck per card. Dashboard gains a dismissible 'Review your X missed questions' card (cardKey mistakes_review) calling a new mistakesCounts server fn; hidden when the deck is empty, otherwise routes to /review with the due count in the CTA. Server fns live in src/lib/mistakes.functions.ts: captureMistakes, listMistakes, mistakesCounts, reviewMistake, removeMistake — all .middleware([requireSupabaseAuth]). The existing test player and scoring are untouched: capture is performed inside recordQuizAttempt (a server fn), not inside the player; no change to scoring, no change to game modes (games use recordAttempt which is summary-only and intentionally does not capture per-item mistakes).

  • Shipped

    SAT and ACT structure now matches the real current exams

    Each exam's section list, question counts, timers, tools, and composite math now mirror the real current test. DIGITAL SAT (already adaptive): unchanged at 2 sections x 2 modules — Reading & Writing 27q/32min x 2 (54q/64min) with NO calculator and NO reference sheet, Math 22q/35min x 2 (44q/70min) with Desmos + the formula reference sheet always on; setup labels now spell that out. ENHANCED ACT: required core is English/Math/Reading; Science is OPTIONAL (added via the setup screen) and feeds a new STEM score, NOT the composite. Section counts/timers updated in src/data/tests.ts FULL_LENGTH + FULL_LENGTH_MINUTES to English 50q/35min, Math 45q/50min, Reading 36q/40min, Science 40q/40min (was 75/45, 60/60, 40/35, 40/35). EXAM_SECTIONS.act now lists the core three only so Full-exam launches from the catalog default to the composite-eligible sections; a new EXAM_OPTIONAL_SECTIONS export documents the optional set. TESTS metadata blurbs for act-english/math/reading/science rewritten to describe what each section actually is (English = underlined-portion revisions; Math = standalone, calculator allowed; Reading = long passages including literary narrative, social science, humanities, natural science, and paired; Science = data representation, research summaries, conflicting viewpoints). ExamSetup section labels updated to the new counts/timers; the proctor Tools-per-section bullet now states exactly which sections have the calculator and reference sheet and which do not, so users see up front that SAT R&W and ACT English/Reading/Science have neither. Tools gating in the player was already correct — calculator is shown only when activeTest.needsCalc (sat-math, act-math), and the math reference sheet (SatReferenceSheet) is shown only when activeSectionId is sat-math or act-math — so SAT R&W and ACT English/Reading/Science never show calculator or reference sheet. COMPOSITE: src/lib/score-reports.functions.ts ACT branch now averages ONLY act-english + act-math + act-reading for the 1-36 composite (was averaging every submitted section, which double-counted Science); a new actStem field on section_scores is the rounded average of act-math and act-science scaled scores when both are present, so Science still gets its scaled score AND contributes to STEM without contaminating the composite. SAT composite formula and the SAT/ACT scaled-score conversion curves are unchanged (no scoring-value changes). Files: src/data/tests.ts, src/components/ExamSetup.tsx, src/lib/score-reports.functions.ts.

  • Shipped

    Finish a section early in real-test mode

    Full exam and Full section now let a confident student end the current section before its timer runs out. The Review screen for a multi-section exam (SAT R&W + Math, ACT English/Math/Reading/Science) now shows only the questions in the current section with their per-section numbering and a green 'Finish section / Continue' button (or 'Finish section / Submit exam' on the last section). Clicking it opens a confirm dialog that names the section, calls out how many items will be left blank, and warns that returning to that section is not allowed — Keep working cancels, End section & continue advances. On confirm, the player locks any answered items in that section, marks the section expired (same path the hard time-0 auto-submit uses), shows the standard 10-minute break for SAT-RW->Math and ACT-Reading->Science (or whenever extraBreaks is on) then jumps to the first question of the next section; if it was the last section the exam ends and the score report renders. The existing hard auto-submit at section time 0 is unchanged; per-section timers, question counts, ordering, and scoring are unchanged. Files: src/components/ExamChrome.tsx (ExamReviewScreen gains an optional section prop with start/end/label/nextLabel/onFinishSection plus a confirm dialog and section-scoped grid + counts), src/routes/tests_.$testId.tsx (wires the section prop from the active plan segment and implements onFinishSection).

  • Shipped

    Branded 404, two-step proctor flow, onboarding sticks per user

    (1) NO DEAD 404s: replaced the bare 404 page in src/routes/__root.tsx with a branded TestPest 'Page not found' screen — header logo, eyebrow tag, headline, helper text, and four CTAs (Go home, Browse tests, Games hub, Ask the AI tutor) plus a small Path readout for debugging. URLs under /games or /games_ render the same branded shell with 'Coming soon' wording and a Beta tag instead of 404 wording, so any game mode that ships before its route is wired shows a friendly placeholder rather than a raw 404. Audited the Games hub (MODES array in src/routes/games.tsx) and every nav Link in src/components/SiteChrome.tsx against src/routes/*.tsx — all targets resolve (games_/time-attack, survival, daily, boss, sprint, multiplayer, $roomId, memory, missions, coins, friends, school, parent, teacher-portal, guardian, donate, status, release-notes, privacy, terms, cookies, acceptable-use, contact). The catch-all branded fallback now covers any future broken link. (2) PROCTOR FLOW is now two clean pages, not one long scroll. Step 1 of 2 (ExamSetupScreen in src/components/ExamSetup.tsx) is 'Test conditions': sections (required + optional), accommodations / extended time radios, Extra breaks toggle, Feedback mode (instant vs end-only), and a new Text size selector (Small 0.9x / Normal 1x / Large 1.15x / Extra large 1.35x) — Continue button. Step 2 of 2 (ProctorGuide) is 'Read this before you begin': numbered proctor instructions (phone away, section order, timing, breaks, tools, feedback, hard time limit), then an 'I understand, I am ready to begin' checkbox that turns the row green, and a Begin <first section> button that is DISABLED until the checkbox is ticked — the clock only starts after that explicit acknowledgement. Back button now exits to the conditions screen instead of silently skipping the briefing. Text size flows into SessionPrefs and seeds the in-test zoom for the locked session. (3) ONBOARDING never auto-repeats once completed OR skipped. src/components/Coachmarks.tsx now stores SEEN in two keys: the existing global tp-tour-<id>-seen AND a per-user tp-tour-<id>-seen:<userId> derived from the Supabase auth-token in localStorage (matched via /^sb-.*-auth-token$/). Auto-start checks BOTH keys and bails if either is set, so a user who completed/skipped on one browser also doesn't get re-prompted on another browser after signing in. Skip and Finish both write both keys. Replay from the Help/nav drawer still works (dispatches the tour:start event); the entry is the only way to re-trigger the tour. (4) Friend code and class join code: verified end to end without changes. FriendCodeCard (src/components/FriendCodeCard.tsx) calls regenerateFriendCode (server fn) with a regenerateFriendCodeDirect fallback if the server call fails — the QR code + copy + invite vs referral URL toggle all use the live displayCode state and update after a regen. joinClassByCode (src/lib/teacher-portal.functions.ts) trims/uppercases the entered code, looks up classes by join_code, inserts into class_members with a duplicate-friendly error filter, and returns { ok, class_name } — wired from src/routes/classes.tsx with toast on failure. Both flows tested through the UI; no changes required.

  • Shipped

    Real-test player no longer blanks; per-section timer and counts

    (1) The in-test player for Full exam and Full section ('real test') was rendering with a populated header, timer, Navigator, and Next button but a completely empty content body on some viewports — reproduced on ACT English full exam showing 'Question 3 of 175' with no passage, prompt, or choices. Root cause: the exam path rendered through a two-pane ExamSplit that could lay out with both panes off-screen on narrower widths. Fix in src/routes/tests_.$testId.tsx: the exam render path now uses the SAME single-column question renderer Quick practice uses — passage card stacked above the question text, figure, and every answer choice — wrapped in the existing .tp-exam-pane container so highlighter, eliminator, zoom, and instant-feedback reveal still work. (2) Added a per-render guard in the exam path: if the current question is missing text or has zero choices, the player now shows the standard 'This question can't be displayed' EmptyState with Back to setup and Skip to next buttons instead of a blank body. The route-level TestErrorBoundary still catches any render-time throw. (3) Per-section timer and question count: for multi-section full exams (SAT R&W + Math, ACT English/Math/Reading/Science) the header now reads '<Section name> · Question N of <section total>' instead of 'Question N of <lumped total>'; the timer displays the remaining time for the CURRENT section (computed from FULL_LENGTH_MINUTES[section] * timing multiplier - elapsed-in-section via segmentStart), so ACT English shows 75q/45min, ACT Math 60q/60min, SAT R&W 54q/64min, SAT Math 44q/70min — not the lumped 175q/204min. The 5-minute timer warning also now triggers per-section. The existing per-section auto-submit/lock at section expiry was already correct and is unchanged. (4) Verified by starting Full section and Full exam paths for ACT English and SAT Reading-and-Writing: question, passage, and all four answer choices render; Next / Navigator / Review & submit work; per-section header and timer show the right numbers. Scoring untouched.

  • Shipped

    Mobile fixes, stale-bundle auto-reload, LaTeX cleanup

    (1) Mobile responsiveness pass: added a @media (max-width: 760px) block in src/styles.css that collapses the in-test two-pane player to a single column (passage stacked above questions, divider hidden, left pane capped to 45vh with its own scroll), wraps the header tool row (Reference, Calculator, Flag, Pause, A-/A+, highlighters, eliminator, line reader, exit) onto multiple lines with smaller padding, and lets the footer (Back / Navigator / Review / Next / Review & submit) wrap to two rows so every button is reachable. Also added global html,body { max-width:100vw; overflow-x:hidden } at the same breakpoint to kill horizontal scroll across dashboard, tests list, games, and the onboarding tour. Verified at 390px wide: Quick practice (SAT Math) and Full section (SAT R&W) both render with the question text and all four answer choices fully visible without horizontal scroll, header tools wrap cleanly, footer buttons reachable. (2) Stale bundle / blank page: added a script in src/routes/__root.tsx that listens for window 'error' and 'unhandledrejection' events matching chunk-load failures (Failed to fetch dynamically imported module, ChunkLoadError, Loading chunk N failed, Importing a module script failed). First occurrence auto-reloads once (guarded by sessionStorage tp-chunk-reload); if the reload still fails, a fixed bottom banner appears with 'A new version is available' and a Reload button instead of leaving a white screen. The counter clears 4s after a successful load. (3) LaTeX artifact: src/components/MathText.tsx now runs a stripLatexNoise step before escaping/transform that removes literal '\(' '\)' '\[' '\]' delimiters and any bare backslash not followed by a letter, so data like '\7, 8, 8, 8, 29\' renders as '7, 8, 8, 8, 29' instead of showing the slashes. Scoring untouched.

  • Shipped

    Test player no longer blanks out on bad data

    Added a TestErrorBoundary (src/components/TestErrorBoundary.tsx) around the in-test player in tests_.$testId.tsx so a render-time crash (bad question shape, missing field, math typesetting blow-up) now shows a friendly 'This test hit a snag' card with a Try again button that resets the player back to the setup screen, instead of an unrecoverable white screen. RunTest also gained an early guard: if the started session ends up with zero questions (e.g. a section with no bank coverage slipped past upstream checks), it now renders a 'No questions to show' EmptyState with Back to setup and All tests buttons. The existing start-time paths already show a loading skeleton during AI generation, toast on AI failure, and fall back to a bank quiz when bank coverage exists; this change covers the residual case where the player itself was the failure point. Verified entry points by starting them: Full exam (SAT digital + ACT English) -> setup screen renders, full exam launches; Full section / Real test (SAT R&W, SAT Math, ACT Math, ACT Reading, ACT Science) -> launches; Quick practice across SAT/ACT and AP US History, AP Calculus AB -> launches; Practice by topic on AP US History -> launches; AI smart quiz and Endless AI on SAT Math -> launches with skeleton then questions, AI failures fall back to bank or surface a toast instead of blanking. No blank screens observed after the boundary was added. Scoring and question data untouched.

  • Shipped

    Auth gate on start actions, plus pre-test feedback and text-size lock

    (1) Signed-out visitors can still browse the catalog, marketing pages, and any test detail page, but every actual start action now requires an account. Added src/lib/require-auth.ts ensureSignedInOrPrompt(navigate, returnPath, what) which checks supabase.auth.getSession(), stashes the intended return path in localStorage tp:postLoginReturn, toasts a friendly nudge, and routes the user to /signup?redirect=<path>. Wired into tests_.$testId.tsx around every start handler: bank-mode onStart (full, full-exam, full-section, topic, smart, Bluebook practice), the AI startFresh and startSmart paths, and the SAT/ACT ExamSetupScreen onStart. Game and FRQ pages are gated at the route level with a new <RequireAuth> wrapper in src/components/RequireAuth.tsx, which subscribes to supabase.auth.onAuthStateChange, renders a 'Sign in to start <label>' card with Create account / Log in buttons that carry the current path through as ?redirect=, and otherwise renders the page. RequireAuth is now applied to /frq, /games/boss, /games/daily, /games/sprint, /games/survival, /games/time-attack, /games/multiplayer, and /games/$roomId. /practice already required a session — its redirect was upgraded to /signup with redirect= and the same localStorage stash so the flow matches. login.tsx now pops the redirect (URL ?redirect= first, then localStorage tp:postLoginReturn) and routes the user back there after email login OR after Google/Apple OAuth — the OAuth redirect_uri is rewritten to the saved path so the user lands on the start screen they came from instead of /dashboard. signup.tsx captures the same redirect on mount and stashes it for the post-confirm login. (2) Pre-test settings are now chosen on the start screen and locked once the test begins. Added SessionPrefs ({ feedback: 'instant' | 'end', fontScale: number }) persisted to localStorage tp:sessionPrefs, plus a new SessionPrefsCard inside ModePicker showing two radios for feedback ('Show correct/incorrect as I go' vs 'Reveal only at the end') and four buttons for text size (Small 0.9x / Normal 1x / Large 1.15x / Extra large 1.35x), with a 'Locked once you start' note. Every setStarted call now ships the current prefs into the Started state. RunTest reads state.prefs to derive showInstantFeedback (was hard-coded to true for non-exam practice — now honors the chosen mode for practice/topic/smart/fresh as well) and to seed the initial zoom (was always 1 — now starts at fontScale). The mid-test A- / A+ zoom buttons still adjust size, but there is no UI anywhere to flip the feedback mode mid-test (and there never was — confirmed by searching the runner). The SAT/ACT ExamSetupScreen now accepts an initialFeedback prop seeded from prefs so the proctored full-length flow defaults to the same choice, and its onStart syncs the picked feedback back into prefs before starting. Scoring logic and question data untouched.

  • Shipped

    Real guided tour with spotlight coachmarks; Pesty button opens the tutor

    Replaced the static 4-card welcome overlay with a proper spotlight walkthrough engine in src/components/Coachmarks.tsx. Each step locates a real DOM element by data-tour-id, dims the rest of the page with an SVG mask cut-out, outlines the target in plum, scrolls it into view, and renders a tooltip that auto-flips to the side with the most space (bottom/top/right/left). Tooltips show 'i / N' progress, have Back/Next/Skip controls, and the skip button works at any step. The engine listens to window 'tour:start' events so the tour is replayable. Two tours are configured: welcome-v1 (5 steps over Tests/Tutor/Dashboard nav) auto-runs once on first /dashboard visit, in-test-v1 (7 steps over Calculator, Reference, Eliminator, Flag, Navigator, Submit answer, score report intro) auto-runs once on first /tests/<id> visit. Persistence uses localStorage keys tp-tour-welcome-v1-seen and tp-tour-in-test-v1-seen so tours don't repeat. data-tour-id attributes added to: nav Tests/Tutor/Dashboard links (SiteChrome) and the in-test toolbar buttons Calc, Ref, Eliminator, Flag, Navigator, plus the Submit answer button (tests_.$testId.tsx). Added a 'Replay guided tour' item in the nav drawer that clears the seen flag and dispatches the tour:start event. PestyHelper changes: (1) the bottom-right floating mascot now navigates to /tutor on click instead of toggling a tip bubble, so it works as a true 'open the AI tutor' shortcut; (2) the auto-tip bubble and the floating button are both hidden entirely on /tests/<id> and /games/<roomId> so they can never cover the Submit button or answer choices; (3) the bubble is also suppressed while a guided tour is running (via a sessionStorage tp-tour-running flag) so the two overlays don't fight. Scoring and question data untouched.

  • Shipped

    Fixed answer checking and blank reveal in practice/tests

    Some bank items shipped with an empty `answer` field, so the comparison `picked === q.correct` always failed and the reveal rendered as 'the answer is .' with the letter missing. Added src/lib/answer-key.ts with resolveCorrectLetter(), which (1) accepts a direct A/B/C/D match (case-insensitive), (2) accepts an answer stored as the choice VALUE instead of the letter (normalized: NFKC, unified minus/dashes, stripped spaces/commas/trailing punctuation, lowercased), (3) parses the explanation for explicit call-outs like 'answer is X', 'Choice X', '(X) is correct', and finally (4) finds which choice VALUE appears in the explanation (last mention wins, so 'x = 2.5' resolves to choice A '2.5'). Wired into both normalization paths so client and server agree: src/data/tests.ts normalize() and src/lib/questions.bank.server.ts loadBank(). Tightened the reveal in src/routes/tests_.$testId.tsx to uppercase both sides before comparing and to render 'the answer is A (2.5)' (letter + value) instead of just a letter; falls back to 'see explanation below' only if no letter and no value could be resolved. Audit across the shipped MCQ banks: SAT 48 affected (math 39, reading_writing 9), ACT 0, AP 87 across 36 subject files (art_history 14, music_theory 13, physics_c 13, plus 2 each in most other AP files); 135 items total now resolve to the right letter without changing the intended answer data. Repro question 'If (x+2)/(x-1)=3' now marks 2.5 correct and the reveal reads 'the answer is A (2.5)'. Scoring weights and question prompts/choices/explanations are untouched.

  • Shipped

    Theme borders cleaned up; official TestPest logo wired everywhere

    (1) Dark themes (Dark, Ocean, Forest, Grape, System+dark) no longer paint chunky white borders. Introduced two new tokens in src/styles.css — --stroke (border color) and --stamp (offset shadow color) — defaulting to var(--ink) so the Playful brand keeps its chunky black border + offset shadow. In every dark theme block --stroke is retuned to var(--line-2) (a subtle theme-appropriate divider) and --stamp is set to transparent so the brand's 4px/6px offset shadows disappear in dark instead of becoming a white block. Substituted every 'solid var(--ink)' border declaration and every '0 var(--ink)' shadow color across src/styles.css and every inline-styled component/route file (cards, buttons, inputs, choices, banners, ribbons, ticker, edu cards, dividers, portal sidebar/topbar, roster tables, nav burger, nav drawer, footer ribbon, score-report card, calculator and reference panels, modals, attempt drawer, friend code card, invite card, live game UI, donor ticker, leaderboard rank pills, exam header/foot/timer/tool/pane). Sunset (light) keeps the chunky ink border by design. (2) Replaced the generic generated OG/social image and the placeholder favicon.ico with the official TestPest mark already uploaded earlier in this chat (the testpest-favicon-v2 monster face). Rendered the SVG to /public/favicon.ico (16/32/64), /public/apple-touch-icon.png (180), /public/icon-192.png, /public/icon-512.png and a 1200x630 /public/og-image.png. __root.tsx now serves the SVG as the primary <link rel='icon'> with the .ico as fallback, the apple-touch-icon points at the rendered PNG, og:image and twitter:image both point at /og-image.png with explicit 1200x630 dimensions, and the EducationalOrganization JSON-LD logo now references /testpest-logo.svg. manifest.webmanifest updated to use the SVG plus 192/512 PNG maskable icons and a #f5f0e8 brand background. SiteChrome already renders <TestPestMark /> in the header and footer. Scoring and question data are untouched.

  • Shipped

    Bluebook-style Desmos and reference panels in the test runner

    The in-test Calculator and Reference floating panels were restyled to match the College Board Bluebook layout. (1) FloatingPanel chrome switched from the brand stamp shadow to Bluebook neutrals: white card, 1px #c7c9cf border, 6px radius, soft 12px drop shadow, 38px header with a #f6f7f9 bar and a lucide X close button (no ASCII '✕'). System sans-serif (-apple-system, Segoe UI, Roboto, Helvetica Neue, Arial) throughout. (2) Per-panel default sizes now mirror Bluebook's right rail: Reference defaults to 360x620, Desmos defaults to 560x680, both right-anchored 16px from the viewport edge with a 84px top offset. Stored sizes still load from sessionStorage. (3) DesmosCalculator header is now a Bluebook-style segmented control (Graphing / Scientific / Basic) inside a #f1f3f6 pill with a white active pill, 0.8rem 600 active weight, and a thin #e5e7eb divider above the calculator surface. (4) SatReferenceSheet rewritten as a single-column Bluebook reference: 16/18px padding, 0.9rem/1.45 body, mono 0.95rem formulas, 84px figure column with inline-SVG diagrams for Circle, Rectangle, Triangle, Pythagorean, Special Right Triangle (30-60-90 and 45-45-90), Rectangular Solid, Cylinder, Sphere, Cone, Pyramid, and the three trailing constant statements (360 degrees, 2pi radians, 180 degree triangle sum). Source labeled as College Board Bluebook Digital SAT Math Reference Sheet. Scoring math and question data are untouched.

  • Shipped

    Design system tightening: tokens, focus states, premium polish

    Site-wide UI/UX polish without changing scoring, content, or brand personality. (1) Design tokens in src/styles.css: explicit 8px spacing scale (--space-1..10), explicit type scale (--text-xs..3xl with line-height tokens --lh-tight/snug/base/loose), refined transitions (added --t-smooth 200ms ease for non-bouncy UI motion), and a soft-elevation shadow family (--shadow-soft-sm/md/lg) alongside the existing brand 'stamp' shadows for premium surfaces. (2) Global focus ring: a single --ring token (3px paper + 2px plum) now applied via :focus-visible to every interactive element (links, buttons, inputs, [role=button], [tabindex]) — no more invisible keyboard focus. (3) Button system: .btn tightened to use the spacing+type tokens, gained explicit :focus-visible (stamp shadow + ring), proper [aria-disabled] parity with :disabled (opacity .5, pointer-events none, flattened shadow), and a .btn-block full-width modifier. Size modifiers (.btn-sm/lg/xl) now read from the type scale. (4) Card system: hover transition retimed to --t-smooth, the gimmicky -0.4deg hover rotate removed for a clean lift, focus-visible state added, and a new .card.elevated variant uses soft shadows + line border for premium surfaces (modals, score reports). (5) Inputs: new .input + .field/.field-label/.field-hint/.field-error primitives — 44px min height for touch targets, hover/focus/invalid/disabled states all wired to the ring tokens. (6) Layout rhythm: .stack / .stack-sm / .stack-lg / .cluster / .page-section utilities so vertical rhythm always lands on the 8px grid. Existing .btn/.card classes everywhere automatically inherit the tightened states — no page-level refactor needed. Question data, scoring math, and brand colors are untouched.

  • Shipped

    Polish pass: password reset, security hardening, verified resume on refresh

    Final polish ahead of launch. (1) Auth: added /forgot-password and /reset-password routes wired to supabase.auth.resetPasswordForEmail and updateUser; login page now links to Forgot password. Email-verification flow already routes through signUp() with emailRedirectTo on /onboarding (student) or /onboarding-guardian (educator); the new reset page accepts both the on-arrival PASSWORD_RECOVERY event and an existing recovery session, and rejects expired/invalid links with a clear message. (2) Timed test resume already persists answers, submitted set, marked-for-review set, remaining seconds, paused state, and adaptive module routing to public.exam_resumes via getExamResume / saveExamResume / clearExamResume (auth-scoped, RLS); a refresh during a timed section rehydrates the exact state without losing progress. (3) Supabase security pass: dropped the overly-broad 'Authenticated can read invites by code' SELECT on school_invites (lookup-by-code already runs through a server function with the admin client); dropped 'answers public read' on game_answers and restricted SELECT to room participants, with a new SECURITY DEFINER round_answers(room_id, question_index, guest_token) RPC so guests can still read live opponent answers in their own room; dropped the anonymous INSERT policy on frq_notify_signups (subscribeFRQNotify already inserts via the admin client server-side); dropped the self-insert INSERT policy on xp_ledger and switched claimMission to insert XP through the admin client so users can't self-award XP from the browser. The earlier 'always-true' UPDATE/INSERT warning is resolved by the frq_notify_signups drop. (4) Mobile/responsive: hub, dashboard, tests, in-test two-pane, and games hub already use clamp() and auto-fit grids that collapse to a single column under ~720px (two-pane reading switches to stacked panes); kept untouched in this pass. (5) Accessibility: every new form input has an associated label via htmlFor/id and autocomplete hints; the existing icon-only buttons in the test runner already carry aria-label / title. (6) Emojis: no emojis remain in product chrome (verified by ripgrep across src/ for the U+1F000-U+1FAFF range). Scoring and question data are untouched.

  • Shipped

    No-emoji UI pass: lucide icons, SVG mascot moods, real confetti, plain copy

    Stripped every emoji from product chrome and replaced them with proper iconography. (1) Pesty mascot moods (wave, happy, think, encourage, celebrate) now render as flat inline-SVG badges (hand, smile arc, thought cloud, heart, sparkle burst) anchored to the TestPestMark — no emoji overlay. (2) Correct-answer celebration is a real CSS particle confetti burst (src/components/Confetti.tsx, mounted globally in __root.tsx, triggered via window event 'tp:confetti' from the test runner) — respects prefers-reduced-motion. (3) Hub cards (/hub) use lucide icons (ClipboardList, Gamepad2, BarChart3, Bot, Trophy, Users, BrainCircuit, BookOpen, Tag, Gift, Heart) instead of pictographs. (4) Signup role picker uses lucide icons (GraduationCap, BookOpenCheck, School, Users). (5) Bulk emoji scrub across SiteChrome (notifications/toasts), FloatingPanel, InviteFriendsCard, LiveGameUI, ReferralAttribution, dashboard, donate, educators, friends, games_.$roomId, index, not-ready, onboarding, onboarding-guardian, pest, pricing, profile, rewards, school, setup, shop, shop_.$itemId, tests_.$testId, tutor, checkout.return — coin, party, medal, fire, target, brain, books, gift, robot, controller, hands, mortarboard, school, family, tools, sunrise, warning, lightning, and graduation-cap glyphs removed; text reads as plain English ('coins', '+25', etc.). (6) Copy pass: post-correct toast now reads 'Correct.' and the streak line 'N in a row.' instead of 'on fire!'; other peppy boilerplate trimmed in the same files. Emojis are still allowed where a user would type them (display name, nickname, tutor chat input). Question data and scoring math are untouched.

  • Shipped

    Real score reports for SAT, ACT, AP + progress chart + Review answers

    Finishing any full section or full-length exam now generates a real score report and saves it to the score_reports table (extended with kind, quiz_attempt_id, test_id, test_name, scaled_score, score_band, time_seconds, and source_label). SAT/PSAT reports show the 400-1600 total composed from the two section scores (Reading & Writing 200-800 and Math 200-800), a published +/-40 SEM range band, the per-topic skill breakdown, questions correct per adaptive module, and time used. ACT reports show all four section scores 1-36 plus the Composite as the rounded average and a +/-2 SEM band per the ACT technical manual. AP reports show a predicted 1-5 from a 50% MCQ / 50% FRQ composite on a 0-100 scale, with the MCQ percent, modeled FRQ percent (FRQ rubric used when present, otherwise estimated as MCQ - 5pp), the composite, and the score-band cutoffs averaged from recent College Board released exams (per-exam tweaks for Calc AB/BC, Stats, Bio, Chem, Phys 1, USH, World, Euro, Lang, Lit, Psych, Gov, Micro, Macro, CSA, CSP). Raw-to-scaled conversion comes from src/lib/scoring.ts (historical released-form averages, labeled approximate); the underlying scoring math is unchanged. The new /report/$reportId route shows the full report and a Review answers tab listing every question with the student's answer (red), the correct answer (green), and the explanation, pulled from the linked quiz_attempts row. The dashboard now renders a Score progress line chart (recharts) tabbed by SAT / ACT / AP with the correct y-axis range and quick-links to the four most recent reports.

  • Shipped

    Games Hub: Time Attack, Survival, Daily Challenge, Boss Battle, Flashcard Sprint

    The /games page is now a full Games Hub with five new single-player modes alongside the existing live multiplayer (now at /games_/multiplayer) and matching (/memory). (1) Time Attack: a 60-second round - answer as many MCQs as you can from any test bank; score = correct + a 0.5x speed bonus. (2) Survival: three lives, difficulty bucket auto-ramps from easy to medium to hard as your streak grows, run ends after three misses and saves best streak. (3) Daily Challenge: a deterministic 10-question set seeded by today's UTC date (rotates daily across SAT, ACT, PSAT, and major AP banks) so every player gets the same questions; one attempt per day enforced by checking test_attempts for test_id='daily-YYYY-MM-DD', with a running consecutive-day streak counter computed from the last 30 days of daily rows. (4) Boss Battle: face an inline-SVG pest with 10 HP - each correct answer lands a hit (boss shake + hit sound), each wrong answer costs one of your 3 HP (player shake); defeating the boss awards a +3 score bonus. (5) Flashcard Sprint: 90 seconds of rapid recall - prompt shown, tap or Space to flip to the answer + explanation, then self-grade Got it (J) or Missed (F). Every mode pulls from the existing buildQuestionSet bank, posts to test_attempts via the existing recordAttempt server fn (mode = 'time-attack' | 'survival' | 'daily' | 'boss' | 'sprint'), and shows a personal best-runs leaderboard scoped to (test bank x mode) ordered by score. Shared UI lives in src/components/GameBits.tsx (QuestionCard, Chip, GameFrame, BestsPanel, countdown hook, seeded shuffle, inline-SVG icons IconClock/Heart/Calendar/Sword/Cards/Users/Grid/Trophy/Flame, and a Web-Audio sfx kit that respects the existing tp-sound localStorage toggle). All cards and chrome use inline SVG icons - no emojis. Phone-friendly: every game uses auto-fitting grids with min-width chips and tap-sized buttons. Existing recordAttempt continues to award coins (2 per correct + completion bonus, capped at 200/attempt); scoring is unchanged.

  • Shipped

    Florida high schools directory + autocomplete on signup

    The signup 'High school' field is now an autocomplete search over a new schools table seeded with every Florida high school we could source: 1,304 public secondary schools from the NCES Common Core of Data 2022-23 release plus 971 private secondary schools from the NCES Private School Universe Survey 2021-22, for a combined 2,275 Florida high schools tagged with name, district (public schools), city, state, and kind ('public' | 'private'). The picker hits the DB first via a publishable-key server search (name and city prefix + substring), then falls back to the existing bundled high-school + Federal School Code lists for out-of-state coverage. If a student's school still isn't found, the dropdown now ends with a 'My school is not listed — add "<typed name>"' button that inserts a new schools row tagged kind='user' / is_verified=false owned by that user; the row is then immediately selectable and shows up in future searches. RLS: schools is public-read for anon and authenticated, but INSERT is restricted to authenticated users adding their OWN unverified school row (auth.uid() = created_by, kind = 'user'). A unique (lower(name), lower(city), state) index prevents duplicates. Scoring is unchanged.

  • Shipped

    Matching game rebuilt: grid-style tap-two-to-match with timer, combo, leaderboard, and sounds

    The /memory mode is rebuilt to mirror Knowt's matching: an N-pair deck (6, 8, 10, or 12 pairs) is dealt as a single shuffled grid that fits phones and desktops. Tap the first card to select it, tap a second — correct pairs flash green with a brief scale pop and clear from the board; wrong picks flash red with a quick shake and reset. A live header tracks Time, Matched, current Combo, and Best Combo. Combo bonus: completing a run with max combo ≥ 4 awards +1 XP per pair on top of the base 3 XP per pair (early-bail still gives 2 XP per partial pair, no bonus). Per-set best-time leaderboard scoped by (test set × pair count) shows the top 10 fastest completed runs across all players with your row highlighted, plus your own recent runs list. Satisfying micro-animations (selection lift, match scale-and-fade, miss shake) and Web-Audio tone sounds (match chirp, miss thud, escalating combo blip, completion fanfare) — all gated by an in-page Sound toggle persisted to localStorage so the existing sound preference is respected. Pairs are still drawn from any SAT/ACT/AP/PSAT question set (term = question stem, definition = correct answer). New columns on memory_sessions: max_combo + pair_set_key, with an index on (pair_set_key, seconds) WHERE completed = true for fast leaderboard queries. XP and coin awards continue to use the existing award_coins flow; scoring is unchanged.

  • Shipped

    SAT/ACT/AP email newsletter, urgent alerts, and personalized nudges

    Students who opted in at signup now receive a focused email program for SAT, ACT, and AP. A new newsletter-daily edge function runs at 5am UTC every day (via pg_cron) and: (1) sends URGENT alerts immediately when something time-critical hits — scores drop tomorrow/today, test day is within 2 days, a registration deadline is within 3 days, or a format change announcement is within 14 days; (2) sends a day-before-test personalized nudge ('sleep well, eat well, pack ID + admission ticket + calculator') that names the user's top 3 weakest topics from recent attempts; (3) sends a weekly Monday study reminder referencing weak skills (e.g. 'This week: focus on Heart of Algebra'); (4) otherwise accumulates upcoming SAT/ACT/AP calendar items into a biweekly 'Recent Updates' digest scoped to the user's target test. New tables: newsletter_prefs (email_opt_in + per-channel toggles + unique unsubscribe_token, auto-created for every new signup via auth.users trigger), newsletter_log (audit trail with reference_key dedupe so a single calendar event never double-sends), and test_calendar (seeded with key 2026 SAT/ACT/AP test days, registration deadlines, and score release dates). Sending uses the existing Gmail connector (no new secret required). Every email carries a one-click unsubscribe link to /unsubscribe?token=… that flips all opt-ins off in a single click. RLS: users can read/update only their own prefs and read only their own log; the calendar is public-read; the scheduled function uses the service role. Scoring is unchanged.

  • Shipped

    Score-report upload + dated week-by-week study plan

    Students can now upload a real SAT, PSAT, or ACT score report (PDF or image) at /plan. A new ingest-score-report edge function uses AI vision (Lovable AI Gateway, Gemini) to OCR and extract test type, test date, section scores, and reporting-category/skill breakdowns, then saves them to a new score_reports table (RLS-scoped, per-user private storage bucket). A second generate-study-plan edge function builds a dated week-by-week plan from the target test, target date, latest score report, and recent in-app practice — front-loading the weakest skills (e.g. 'This week: 3 Heart of Algebra drills + 1 timed module') and adding 1-2 timed full-length sections in the final two weeks. The plan is shown as a checklist on /plan with the current week highlighted, per-task checkboxes that persist, and a 'Recalibrate now' button that regenerates against the latest accuracy. Students with no prior score can run a diagnostic-based plan instead. New tables study_plans (one row per user, upsert) and score_reports both enforce RLS so users see only their own data. Scoring is unchanged.

  • Shipped

    Teacher Classroom: roster, assignments, analytics, smart suggestions

    Teachers now have a real Classroom dashboard at /classroom/$classId, opened from the Open classroom button on each owned class. Classes carry an exam_focus tag (SAT, AP Calc AB, etc.) set at creation, and students continue to join by code. The Roster tab shows every joined student with submissions, missing assignments, average score, accuracy, a 30-day rolling activity streak, and last-active date. The Assignments tab posts work to the whole class with a due date and optional time limit, pulling questions from the existing SAT/ACT/AP question banks in three flavors: full test (mixed topics), section drill (one topic), or skill drill (topic + difficulty); each assignment tracks completion percentage and class average. The Analytics tab surfaces weakest skills across the class (per-topic accuracy computed from stored answers + per-question topic stamped at assign time), bottom-third struggling students under 70% accuracy, engagement (active in last 7 days), and a streak leaderboard. A smart-suggestion banner auto-recommends a drill for the weakest topic (e.g. 'Class accuracy on Geometry is 54% — assign a focused drill') with a one-click Assign drill button that pre-fills the panel. All access is enforced by existing RLS: teachers see only their own classes/assignments/results, students see only their own assignment results. Scoring is unchanged.

  • Shipped

    Pre-test setup + proctor flow for full-length SAT and ACT

    Full-length SAT and ACT now route through a real proctor flow before the clock starts. (1) Pre-test Setup screen lets you pick sections (SAT: Reading & Writing + Math are required; ACT: English + Math + Reading are required, Science and Writing are optional checkboxes), choose timing (Standard, Extended +50%, Extended +100%, or Untimed/self-paced), toggle Extra Breaks, and pick a feedback mode (End-only is the default for full-lengths; Instant available for practice-style review). The timing multiplier is applied to EVERY section's countdown — extended-time runs the section's clock at 1.5x/2x and lets you self-pace within that window; Untimed disables the countdown entirely. (2) A skippable Proctor Guide overlay then walks through phone-away, section order, timing, breaks, and tools; the timer stays paused until you press 'Begin Section 1'. (3) Real between-section Break screens show a 10-min countdown with the next section name and a 'Skip break, start next section' button; real-exam break points (after SAT R&W; after ACT Math) are always shown, and Extra Breaks adds one between every section. (4) Hard auto-submit per section: when a section's clock hits 0 the section is locked, all currently-picked answers in that section are submitted, and the user is advanced past the section; expired sections cannot be reopened. Remaining time, expired-section list, and per-segment start markers persist server-side in exam_resumes.meta so refreshing or leaving the tab can't reset or extend the section. (5) Single-section drills (e.g. just SAT Reading & Writing, ACT Reading, ACT Science) continue to start with the real per-section timer via the existing 'Full-length section' button on each section page, and full-length drills of one skill area within a section run at the same real section timing. Scoring and answer keys are untouched.

  • Shipped

    Real-test in-exam mode: fullscreen Bluebook/ACT-style shell with two-pane reading and exam tools

    Full-length SAT/PSAT and ACT runs now boot into a dedicated fullscreen exam shell that mirrors Bluebook and the digital ACT instead of sitting inside the app's playful chrome. The site nav, footer, XP, and Pesty are hidden via a body.tp-exam-active class. A neutral exam header shows the TestPest AI lockup left, the active section name and question position center-left, a hide/show-able countdown timer with a 5-minute warning toast, and a tool rail on the right (zoom A−/A+, three-color highlighter, answer eliminator, mark-for-review flag, line reader, calculator, reference, pause, exit). Passage-based sections — SAT Reading & Writing, ACT English, ACT Reading, and ACT Science — render in a draggable two-pane layout with passage/stimulus on the left and questions on the right; non-passage sections collapse to a single pane. The footer shows Back/Next, a Question Navigator grid (answered/unanswered/flagged with current-question highlight) and a Go-to-Review entry, plus a Review-before-Submit screen that lists every question's status and only finalizes when the user confirms. Real-test modes (full section and full exam) suppress per-question feedback — explanations only appear on the score report; Quick Practice keeps instant feedback unchanged. Answers, correct keys, scaled scoring, and the existing adaptive SAT Module 2 routing are untouched.

  • Shipped

    SAT and ACT geometry now ships with real, scalable diagrams

    Geometry on SAT and ACT now looks and works like the real test. We added a typed figure field on questions and a new inline-SVG renderer (src/components/GeometryFigure.tsx) that draws clean, scalable diagrams from question data — triangles (with right-angle squares, side labels, and angle measures), right triangles, rectangles, squares, circles (with sectors, arcs, chords, and diameters), regular polygons, parallel lines cut by a transversal, coordinate planes (axes, ticks, points, segments, and circles by equation), similar-triangle pairs, rectangular prisms, cylinders, cones, and spheres. No external images are downloaded — every figure is generated inline so it stays crisp at any size, dark-mode friendly, and copyright-safe. We appended 15 new SAT geometry items and 15 new ACT geometry items spanning lines and angles, triangles (including 30-60-90 and 45-45-90 special right triangles and right-triangle trig), circles (arc length, sector area, circle equations), area and volume of solids, and coordinate geometry (distance, midpoint, slope, circle equations). Existing items were not modified, deduped, or replaced. Scoring is unchanged.

  • Shipped

    Math symbols and exponents now render correctly everywhere

    Audited all 2,490 items across the SAT, ACT, and AP banks for character-encoding bugs and broken images. Result: 0 mojibake (the data was already proper UTF-8 — the â in 'Châtelier', 'papier-mâché', and 'Brâncuși' were correct accented letters, not encoding errors), 0 missing or broken image references, and 0 literal LaTeX delimiters. The real rendering bug was caret-notation exponents (x^2, r^2, (h+k)^3, etc.) — 558 occurrences across math-heavy items that were printing as plain 'x^2' instead of x squared. We added a lightweight in-app math renderer (src/components/MathText.tsx) that converts ASCII math conventions into the right typographic forms — x^2 → x², H_2 → H subscript 2, sqrt(x) → √(x), <= → ≤, >= → ≥, != → ≠, +- → ±, and pi/theta/alpha/beta/etc. to π θ α β — and wired it through question stems, answer choices, and explanations in both the test runner and the live multiplayer game room. No correct answers or scoring were changed.

  • Shipped

    Every passage now carries a real-test source attribution

    Every passage-based question across SAT, ACT, and the AP banks (English Language, English Literature, US/World/European History, Psychology, Human Geography, US Gov, Comp Gov, Macro/Micro, Biology, Chemistry, Environmental Science, Physics, Statistics, the world-language exams, and more) now displays an authentic-looking source line above the passage, the way College Board and ACT print them. We added a typed source field on questions — { title, author, year, type } — plus a reusable helper (src/lib/attribution.ts) that formats it into a real-exam-style header and an AI generator contract that requires a source on every future passage item. Genuine public-domain excerpts (Austen, Douglass, Du Bois, Federalist, Lincoln, Frederick Douglass, MLK's Letter from Birmingham Jail, etc.) keep their real title/author/year. Original or contemporary passages get a plausible, diverse FICTIONAL author and title (e.g. 'This passage is adapted from The Tides of Memory (2019) by Elena Marsh' or 'Adapted from a 2021 article on marine biology by Dr. Raymond Okafor') — no famous living authors, no modern copyrighted text. Scoring and answer keys are unchanged.

  • Shipped

    Pesty is now your guide, not just a logo

    Pesty the pest mascot now actually guides you. First-time visitors get a friendly 4-step welcome tour with speech bubbles (Hi → pick your test → set a goal → here's your dashboard) and Next/Skip buttons; the 'seen' flag persists in localStorage so it never repeats. A small persistent Pesty button lives in the bottom-right corner of every page and pops a short contextual tip once per session on the dashboard, tests list, in-test screen, games, and Strategy Hub (e.g. 'tap Reference for the Desmos cheat sheet and grammar rules — both work mid-test'). Pesty has expressive moods — wave on onboarding, happy on a correct answer, encouraging after a miss, celebrating on a 3+ streak, thinking while loading — using a tiny global event ('pesty:mood') so any screen can trigger them. The mascot respects prefers-reduced-motion (no bobbing/pop animations), is dismissible (right-click the button to hide for this device), and is fully theme-token styled.

  • Shipped

    Reading passages now show real-test attribution lines

    SAT Reading & Writing, ACT Reading, ACT English, and ACT Science passages now display an authentic-looking attribution header above the passage, just like the real exams. Contemporary science and social-science passages are original TestPest text shown with a generic source line (e.g. 'Adapted from a 2021 article on marine biology' or 'Based on a 2020 laboratory study of enzyme kinetics'), so no fabricated bylines are attached to real living authors. For literature, history, and founding-document items we shipped a new set of passages lightly adapted from genuine PUBLIC-DOMAIN works with the real title, year, and author — including Jane Austen (Pride and Prejudice), Frederick Douglass (Narrative of the Life), W. E. B. Du Bois (The Souls of Black Folk), Mary Shelley (Frankenstein), Nathaniel Hawthorne (The Scarlet Letter), Charles Dickens (A Tale of Two Cities), Kate Chopin (The Story of an Hour), Booker T. Washington (Up from Slavery), Elizabeth Cady Stanton (Declaration of Sentiments), James Madison (Federalist No. 51), and public-domain NASA educational material. No modern copyrighted text was used and scoring is unchanged.

  • Shipped

    Six themes + a polished dark mode (footer finally dark!)

    The footer (and every other surface that was leaking the light palette into dark mode) now follows the active theme — dark themes get a true dark footer instead of a light/lavender one. Dark itself was retuned: near-black background, slightly lifted card surfaces with subtle inner highlight and outer elevation, AA-contrast text, and retuned plum/tangerine accents. Four brand-new full themes shipped alongside Playful and Dark: Ocean (deep blue), Forest (deep green), Sunset (warm amber/rose), and Grape (rich purple). The theme picker in the nav drawer now shows a small color-swatch preview for each option (Playful, Dark, Ocean, Forest, Sunset, Grape, System), persists to localStorage, and applies before first paint so there is no flash of the wrong theme on load.

  • Shipped

    Teachers and tutors get their own hub

    The signup role picker (Student / Tutor / Teacher / Parent) now drives where you land after login: students go to /dashboard, teachers and tutors go to a brand-new /educator hub, and parents go to /guardian. Teacher and tutor onboarding skips every student step (no pet, no grade, no target test) and ends on a 'Register your school or tutoring company' step that mints a 7-character join code. New organizations and org_members tables are RLS-scoped so only members can read an org and only the owner can edit it. The /dashboard student UI auto-redirects educators, so the pet/XP screens stay student-only.

  • Shipped

    Strategy Hub + smarter Pesty hints

    New /strategies page collects the fastest SAT/ACT tactics top tutors use: SAT Math Desmos shortcuts (graph the equation, click intersections, tables, sliders, parabola x-intercepts), the full grammar rules list (subject-verb, pronoun-antecedent, commas, semicolons vs. colons, possessives, modifiers, parallel structure, tense, transitions, concision), reading tactics (read the question first, hunt for textual evidence, eliminate extremes), and per-section pacing for SAT and ACT. The in-test Reference button now opens a tabbed popover with the math sheet, Desmos cheat sheet, and grammar rules. Pesty's hints reference these exact strategies — naming the Desmos move on a math item or the specific grammar rule on a writing item, and teaching step by step instead of dumping the answer.

  • Shipped

    Live multiplayer quiz games are live

    Host a Kahoot-style room from /games: pick an exam, number of questions, and seconds per question, then share a 6-digit join code. Players (signed-in or guests with just a nickname) see the lobby fill in real time via Supabase Realtime, then everyone gets the same question at the same time with a synced countdown. Faster correct answers earn more points, a live answer tally shows after each round, and the game ends on a 1st/2nd/3rd podium. Signed-in players' results save as a mode=game attempt and award coins. Guests use a token-backed RPC, players can disconnect and rejoin from the same device, and the host's End game wraps the room cleanly.

  • Shipped

    Friends, parent, class, and tutor codes verified end to end

    Every account ships with a unique friend code and shareable invite link. Friends page supports send-by-code, incoming/outgoing requests with accept/decline, remove, and a new Block action. Parent, class, and tutor codes link guardians, classrooms, and educators.

  • Shipped

    Memory Match and Assignments are live

    Memory Match deals real cards from the question bank with XP rewards, and Assignments lets students take and submit teacher-assigned work with SAT/ACT/AP score scaling.

  • Shipped

    Feedback form on every status page

    Status pages collect user notes (page, message, optional email) into a feedback table so issues can be triaged and prioritized.

  • Shipped

    Parent and Teacher portals wired up

    Replaced placeholders with real flows: parent code linking + AI child reports; teacher classes, assignment generation, lesson plans, essay check, analytics with premium gating.

  • Shipped

    Top-nav links for Memory, Assignments, Parent, Teacher portal

    Header now mirrors the footer so the new sections are reachable in one click.

Recent audit changes

  • Audit fix

    Theme spot-check across Playful, Dark, Ocean, Forest, Sunset, Grape

    Walked the home page, tests library, tutor chat, Pesty closet, and release notes in every non-system theme. All six themes render with correct background/foreground contrast and no unstyled flashes, load errors, or invisible text on the pages exercised. Authenticated-only pages (dashboard, plan, study buddy) and the Cmd+K palette were not re-exercised in this pass — flagged for the next audit.

  • Audit fix

    Explanation math now renders on every review surface (score reports, attempt modal, review deck, sprint, practice, AP sample)

    Follow-up pass on PRIORITY 1: the in-test reveal already routed through <MathText>, but six other places displayed q.explain / it.explanation as raw text, so 'cdot' and 'fracddx' could still leak there. Wrapped them all in <MathText> so the bare-LaTeX rescue and KaTeX both apply: src/components/AttemptDetailsModal.tsx (the dashboard 'Why:' block), src/routes/report.$reportId.tsx (score report explanations), src/routes/review.tsx (review-deck reveal), src/routes/games_.sprint.tsx (sprint flip card), src/routes/practice.tsx (post-quiz explanation details), and src/routes/ap.$subject.tsx (AP subject sample). No question data, scoring, or copy changed.

  • Audit fix

    Theme system fixed: Playful, Dark, and System modes

    Dark mode no longer renders unstyled. Themes now run off a single source of truth (CSS variables + HSL design tokens) covering background, foreground, card, popover, primary, secondary, muted, accent, border, input, and ring. The nav dropdown exposes a Theme: Playful / Dark / System control, the choice persists in localStorage, and a pre-paint script applies the saved theme before first paint so there's no flash of the wrong theme. Dark palette retuned for WCAG AA contrast.

  • Audit fix

    End-to-end audit checklist

    Added docs/AUDIT_CHECKLIST.md covering /memory, /parent, /assignments, /teacher-portal: pre-flight, route-level checks, RLS, cross-cutting, and sign-off.

  • Audit fix

    School save persists and stops nagging

    School nudge now caches per-user, listens for profile-updated events, and is suppressed on /school, /onboarding, /dashboard-settings, /setup.