300 of 483 subtopics normalized ('Addition And Subtraction Formula'
-> 'Addition and Subtraction Formula'). Acronym FIX map preserves
AC/AM/GM/HM/AP/GP/EM/EMF/RMS/SHM/GOC/IUPAC/pH; article-A and PN
Junction fixed; VSEPR and Raoult's Law proper nouns corrected.
Chapter names were already title case. Applied to live tablet DB.
shared() now renders: stat cards (questions, duration, max marks,
+4/-1), per-subject sections with q-ranges and chapter lists, a
'take this paper' card with the save-and-next explainer and privacy
note, then the full question list. New .kv/.grid2/.subj-sec/.ch-list
CSS with mobile single-column fallback.
Sanitizer root cause: a '<' followed by space/digit (e.g. '\(a < 0\)')
was parsed as a tag, failed allowlist, and got DELETED along with its
content — unbalancing KaTeX delimiters and killing all math after it
on the page. Now only '<' followed by letter, /, ! or ? starts a tag;
everything else becomes <. 4 regression tests added.
CSS: .zp-chk-wrap and .zp-subtopic-row labels no longer inherit the
generic uppercase rule, so chapter names render in DB title case
('Quadratic Equations', not 'QUADRATIC EQUATIONS').
- Logo: modulate 78 brightness / 88 saturation on the v3 whale source.
Verified clean (no line artifacts on black-flatten). Regenerated
logo.png, favicon.png, apple-touch-icon.png, and embed.png from it.
- Username input placeholder: 'e.g. Aarav Sharma' -> 'e.g. Nikhil The Great'.
Cloudflare edge cached the old placeholder bytes on the bare
/static/favicon.png and /static/apple-touch-icon.png URLs. The ?v=2
query string forces a fresh origin fetch with zero behavior change.
- logo.png: user's whale (1024px JPEG) with background flood-filled to
transparent at fuzz 3 (preserves white belly details), alpha
thresholded to kill JPEG-noise halos, trimmed and centered on
720px canvas. v3 verified clean: no streaks, no box lines, no fringe.
- favicon.png (32px) and apple-touch-icon.png (180px) regenerated
from the same source.
- embed.png (1200x630) rebuilt: whale at 440px left, no overlap with
the WhalePrep title text.
- views.rs: nav brand now references logo.png instead of logo.svg.
Logo: hand-crafted SVG whale silhouette in blue gradient (#3b82f6 to
#1d4ed8), DeepSeek-inspired minimalist style with water spout, eye,
and wave arc. Renders at 28px in the nav header.
Embeds: Open Graph and Twitter Card meta tags on every page. og:image
is a 1200x630 embed card with the whale logo, WhalePrep title, tagline,
and stats. Favicon.png (32x32) and apple-touch-icon.png (180x180)
generated from the same SVG. theme-color #0a0f1c for mobile browsers.
º (masculine ordinal U+00BA) -> ° (degree U+00B0) in Q35, Q963, Q5359,
Q9014. DATA.md now records the better-database audit: the
Indian_Exams_PYQ repo's 223 shift PDFs are LFS-locked and image-only;
examgoal CDN returns 403; our examgoal-derived bank stays the best
structured source. Also explains the Hard-difficulty reality (user-vote
metadata, ~2-3 hard questions per shift is authentic) and confirms all
9,813 questions carry exam='jee-main' source metadata.
The chapter structure now matches the NTA 2026 Paper 1 syllabus exactly
(verified against the official PDF at cdnbbsr.s3waas.gov.in):
- Physics: 20 units (Units and Measurements, Kinematics, Laws of Motion,
... Electronic Devices, Experimental Skills)
- Chemistry: 21 units (Some Basic Concepts, Atomic Structure, ...
Principles Related to Practical Chemistry)
- Maths: 14 units (Sets Relations and Functions, Complex Numbers, ...
Trigonometry)
scripts/reorg_nta_units.py maps upstream subtopics by keyword; 73
stragglers placed manually. Removed the 24 hand-written demo questions
(Q1-Q24) from the live bank — trivial one-liners that diluted the
difficulty pool. Bank is now 9,813 pure PYQs (2002-2025, all
exam='jee-main' verified from source metadata).
Dollar fix: 5,634 questions had a lone $ before \( or $$ from the
source data. fix_single_dollar now strips any orphan single $ while
preserving $$..$$ display pairs; the live tablet DB was patched in
place and the importer inherits the fix.
UI: removed the non-functional 'previous year paper' mode card, the
complete-syllabus button, the tick glyph inside filter pills, and the
'2025 Syllabus' badge beside the header title. Question count pills
replaced by a plain number input (5-75). Checked chapters now show a
blue left-edge highlight so a second click visibly deselects; checked
subtopics tint blue the same way.
Replicates zenithprepare.com/PYQ topic-selection UX:
- Subject switcher pills (Maths/Physics/Chemistry) with live selection
counters; only the active subject's chapters are shown.
- Searchable chapter rows with independent subtopic expand arrows
(checking a chapter no longer toggles the tray).
- Complete-syllabus one-click button (selects all 15 units, 75 Qs,
JEE mode).
- Difficulty filter pills (Easy/Medium/Hard, default all) and year
filter pills (2018-2025, default 2020-2025) wired through new
difficulties/years form fields; build_paper filters both, including
the FIFO recycle path. Verified end-to-end: Hard-only paper returns
only Hard questions; Medium+Hard across 2022-2025 leaks zero Easy.
- Settings bar with count pills, JEE-mode toggle and live pool
estimate.
Also fixes: brand header badge, raw-string delimiter clash with SVG
hex fill, Maths subject-key mismatch between pills and DB.
1. Batch write transactions in build_paper, start_attempt, save_answers,
and finalize (turned 75+ disk operations into 1 commit per action).
2. SQLite PRAGMAs: synchronous=NORMAL, cache_size=64MB, temp_store=MEMORY,
and mmap_size=256MB for zero-copy memory reads.
3. O(1) history: record right_count/wrong_count on attempts at finalize time;
history() loads in a single SELECT query without re-grading loops.
4. Native Rust load testing tool (src/bin/bench.rs) replacing Python scripts:
multi-threaded concurrent reads & simulated full student lifecycles with
sub-microsecond timing and statistical percentiles.
The UA edit broke indentation (script never ran since). Question ids
now come from /api/attempt/:id/state so the benchmark works against
any deployment. Verified live: 10 students, 45 writes/s through tunnel.
Invoking via ./backup.sh symlink made dirname escape the repo and sqlite
created an empty DB in the parent dir. Now uses readlink -f with a
location check plus a guard that refuses to run without ./whaleprep.db.
Exam/solution pages print clean via browser print-to-PDF: interactive
chrome hidden, questions unhidden, ink-friendly colors. Options printed
as black bars without the surface override — .opt/.chk/inputs/palette
now forced white. Verified with print-media emulation screenshot.
coach_notes takes the tag rollup the dashboard already fetches: a
dominant mistake type (>=half of >=2 tags) yields a targeted fix, and
chronic wrong-over-blank across history yields a guessing warning. Same
Vec<String> output, no view changes. Three unit tests cover fire/quiet
paths.
Sessions never expired server-side despite the 30-day cookie Max-Age;
repeat sign-ins orphaned rows. user_id now rejects tokens older than 30
days and deletes them on sight. Verified live: fresh 200, backdated 302
with row removed.
cargo audit finds one advisory (RUSTSEC-2023-0071, no fix). Assessed
not exploitable here: server only verifies RSA signatures, holds no
private key. Session, Google verification, rate limit, backup posture
documented alongside.
Live user data had only a stale pre-migration backup. First snapshot
verified: 9,837 questions, 6 users. Termux has no cron; script is manual
or Tasker-driven.
Integer explanations end with the answer but phrasing varies enough
that regex extraction would mis-key questions. A wrong key is worse
than no question. NUM stays hand-added until NTA keys transcribed.
rust:1.99-slim build, debian-slim runtime as uid 10001. Only static/
and content/ shipped alongside the binary; DB lives on /data volume.
Could not build-verify locally (no Docker daemon); paths checked
against ServeDir/include_str usage instead.
The exam screen autosaves on every answer; a stuck client retrying in a
tight loop could hammer SQLite's single writer. Ctx carries a small
in-memory per-user windowed counter with opportunistic cleanup — no
Redis needed. Verified live: 30x200 then 429s.
Sign-in page rewritten for non-technical students: Google-first when
configured, plain-language email path with honest 30-day scope, recovery
note, placeholders, full-width CTA. deploy/termux-start.sh sources .env
for GOOGLE_* credentials; deploy/env.example documents the 5-minute
Cloud Console setup; .gitignore covers secrets, DBs, pid/log files.
bench scripts send a browser UA so Cloudflare bot checks stop 403ing
local load tests. Landing bank counts cached 60s (five aggregate
subselects no longer run per hit). gzip compression on all responses.
Source data wraps every math fragment in display-mode $$ delimiters,
which KaTeX rendered as centered blocks mid-sentence. scripts/fix_math.py
normalizes the corpus: $$/$ pairs become inline \\(...\\) (block
derivations with newlines or environments stay display), legacy
\\matrix blocks become begin/end matrix, _\\limits/^\\limits,
\\raise/\\lower boxes, \\root/\\of, \\tag and \\, are rewritten
to KaTeX-supported forms. Batch-validated 60k inline spans: 99.98% render
clean; remaining handful are source pathologies that degrade to text.
import_examgoal.py runs the same normalization at write time.
deploy/termux-{start,stop,status}.sh manage the release binary on the
tablet; deploy/cloudflared-whaleprep.yml is the dedicated tunnel config
for whaleprep.oiupoyt.space. README gains a deploy section.
import_examgoal.py required /tmp/pyqeval helper modules that were never
committed, so it failed on any fresh machine. The two upstream model
files now live in scripts/examgoal_model/ (relative import fixed) and
the default pickle path is scripts/examgoal_db.pkl.
GET /paper/:id redirects the owning user to their attempt screen (404
otherwise, login wall otherwise). Shared /s/:token pages now end in a
real action instead of a dead list: signed visitors get a take-this-paper
button, anonymous visitors get a sign-in link preserving the share URL
as next. Verified live: 1/302/1 across the three states.
seen_questions gains seen_at (with ALTER TABLE migration for existing
DBs, index created after the migrate step). Submit stamps seen_at with
an upsert so repeats refresh recency. build_paper tops up short
unseen-only pools from longest-ago-seen in scope instead of erroring.
Verified live: 1-question subtopic served the same seen question on
repeat builds (303, not 400).
Result page shows a why-lost row (concept gap / calculation slip /
misread / out of time / blind guess) on every wrong or blank question.
POST /result/:attempt/tag/:qid toggles one tag; ownership checked,
vocabulary enforced server-side. Dashboard gains a 'why you lose marks'
section aggregating tags across all submitted attempts. Verified live:
tag persists, rollup renders.
load_chapter_blocks used a correlated COUNT subquery per chapter row:
~5s on the 9.8k-question bank. Rewritten as one GROUP BY joined once
(16ms p50 on /build). Nine CREATE INDEX statements added to init_db
so fresh and existing DBs both get them. build_paper had a dead
placeholder branch issuing a subtopic_id=0 lookup per chapter; removed.
scripts/import_examgoal.py pulls the MIT-licensed examgoal-derived
14,973-question pickle and keeps the 9,813 fully-text MCQs with single
correct answers (2002-2025, all subjects). chapterGroup becomes our
chapter, chapter slug becomes our subtopic. Idempotent: skips stems
already present.
views.rs gains rich() — a read-side sanitizer matching the importer's
allowlist — and all six stem/option/solution render sites use it.
KaTeX auto-render (trust:false) added to head() for $$..$$ math.
db.rs gains nine CREATE INDEX statements; the chapter loader query
drops from ~5s to 6ms on the 9.8k-question bank. DATA.md rewritten
around the real pipeline with attribution.
scripts/expand_seed.py generates synthetic questions across the full JEE Main
syllabus taxonomy (15 chapters, 45 subtopics, correct MCQ/NUM split). Stems
are deliberately sparse so no-one mistakes them for NTA content.
DATA.md documents why there is no real scraper: NTA PDFs render questions as
images; pdftotext extracts only IDs. Every aggregator with structured data
built it through manual entry pipelines. The document lists realistic paths
to a real bank and the SQL schema for loading one.
The builder previously exposed only chapters. Now each chapter expands to show
its subtopics as individual checkboxes: selecting a chapter checks all of its
subtopics, selecting the subject-level box checks everything, and selecting
individual subtopics sets the chapter to indeterminate. The exam query filters
by subtopic_id when any are given; the SQL path deduplicates so a question
matching two subtopics only appears once.
JEE mode sets duration to 60 × unique-subject-count (180 min for a full paper).
On the exam screen this renders per-subject countdown strips: Physics 60:00,
Maths 60:00, Chemistry 60:00. Each strip counts down independently; when one
hits zero the screen auto-jumps to the first unanswered question of the next
subject. The last strip exhausting time submits the form.
Single-subject papers keep the flat timer and show no strips.
Also: all audit targets now genuinely 44px (the .opt labels were 42px).