No description
Find a file
dev a0fcfc93b6 Title-case subtopic names via scripts/titlecase.py
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.
2026-10-16 15:20:14 +05:30
content Google sign-in with server-side token verification 2026-10-11 20:37:56 +05:30
deploy Backup script resolves symlinks, aborts without DB 2026-10-13 09:06:45 +05:30
examples Google sign-in with server-side token verification 2026-10-11 20:37:56 +05:30
scripts Title-case subtopic names via scripts/titlecase.py 2026-10-16 15:20:14 +05:30
src Paper preview in ZenithPrep style: stat cards, subject sections 2026-10-16 14:56:27 +05:30
static Paper preview in ZenithPrep style: stat cards, subject sections 2026-10-16 14:56:27 +05:30
tests PYQ clone: builder, CBT exam screen, grading, analytics dashboard 2026-10-11 20:06:26 +05:30
.gitignore Student sign-in UX, gzip, landing stats cache 2026-10-12 23:33:33 +05:30
Cargo.lock Rust-native optimizations: transactional writes, O(1) history, SQLite PRAGMAs, and native bench tool 2026-10-13 10:05:27 +05:30
Cargo.toml Rust-native optimizations: transactional writes, O(1) history, SQLite PRAGMAs, and native bench tool 2026-10-13 10:05:27 +05:30
DATA.md Degree-symbol fix (4 questions), document database search findings 2026-10-15 10:33:18 +05:30
README.md Deploy artifacts: Termux service scripts plus tunnel config 2026-10-12 22:26:33 +05:30
run_server.sh Rename zenith-pyq-clone to whaleprep 2026-10-12 08:17:40 +05:30
SECURITY.md SECURITY.md: audit result, rsa advisory assessment, session/data notes 2026-10-13 00:12:08 +05:30

WhalePrep

JEE Main PYQ practice: build a paper from 9,813 real previous-year questions (2002–2025, Physics/Chemistry/Maths), take it on a timed CBT screen with +4 / −1 marking and per-subject clocks, tag why you lost each question, then read the analysis and progress dashboard.

Rust + Axum + SQLite. Server-rendered HTML, KaTeX math, vanilla JS, no frontend framework.

Run

cargo run --release

Then open http://127.0.0.1:8080 and sign in with any name and email.

Environment:

var default meaning
PYQ_ADDR 127.0.0.1:8080 listen address
PYQ_DB pyq.db SQLite file, created if absent
PYQ_BASE empty URL prefix when mounted in a subpath
GOOGLE_CLIENT_ID empty enables Google sign-in; unset hides the button
GOOGLE_CLIENT_SECRET empty needed only by the full-page fallback

Question bank

A fresh database seeds 24 hand-written questions so the app works offline. For the real bank:

python3 scripts/import_examgoal.py pyq.db

This pulls the MIT-licensed 14,973-question pickle and imports the 9,813 fully-text MCQs with verified single answers. Idempotent — safe to re-run. See DATA.md for the source, the keep rule, and the schema mapping.

Google sign-in

Set a client id and the button appears on /student. Without it the page says Google is off and offers the email form instead.

GOOGLE_CLIENT_ID=1234-ab.apps.googleusercontent.com \
GOOGLE_CLIENT_SECRET=... \
cargo run

In the Google Cloud console add an OAuth 2.0 Client ID / Web application, then list every origin this runs on, for example http://127.0.0.1:8080 and http://localhost:8080. The callback URL is derived from the incoming request, so it follows the port you are on and follows x-forwarded-proto behind a proxy. Register each of those origins rather than trying to predict one URL.

Two paths lead to the same session:

  • Popup. Google Identity Services returns an ID token to the page, which posts it to POST /student/google.
  • Full page. Where a popup is refused, the link beside the button starts an authorization-code flow with PKCE. The code is redeemed server side at GET /student/google/callback, so the client secret never reaches the browser.

Either way the ID token is verified here, not in the browser. The signature is checked against the keys Google publishes at https://www.googleapis.com/oauth2/v3/certs, and the audience must be this client, the issuer must be a Google issuer, the expiry must hold, and the email must be verified. A token failing any of that is rejected and no session is created. The state parameter is single use and expires after ten minutes.

To confirm the rejection path without a real Google account:

cargo run --example forge_google_token -- YOUR_CLIENT_ID attacker@example.com

That prints a correctly signed token from a key the server has never seen. Posting it to /student/google returns 401 and creates no user.

Routes

route what it does
GET / landing page with live bank counts
GET /student, POST /student email sign-in, sets an HttpOnly session cookie
POST /student/google verifies a Google ID token, signs in
GET /student/google/callback redeems the authorization code for the full-page flow
GET /logout clears the session
GET /build, POST /build chapter/subtopic picker and paper generation
POST /attempt/:paper starts or resumes an attempt
GET /attempt/:paper the CBT screen
POST /attempt/:id/answer JSON autosave of one answer
POST /attempt/:id/submit grades and closes the paper
GET /result/:attempt score, accuracy and per-question analysis
POST /result/:attempt/tag/:qid toggles a why-lost tag on one question
GET /paper/:id owner shortcut to the attempt screen
GET /history every submitted paper
GET /dashboard trend, weak subtopics, mistake rollup, coach notes
GET /s/:token public paper view with take-this-paper CTA
GET /api/attempt/:id/state autosaved answers as JSON

Marking

+4 for a correct answer, −1 for a wrong one, 0 for a blank. Numerical answers compare against a 0.1 percent tolerance so 20.01 is accepted for 20.

Question selection

A paper is drawn from the chosen chapters or subtopics, shuffled with a seed derived from the request, and trimmed to the requested length. With "unseen questions only" the query excludes anything already answered by that user; once a subtopic is exhausted, the longest-ago-seen questions recycle first instead of erroring. JEE mode sets 60 minutes per subject with independent countdown strips that auto-advance.

Measured performance (9,837-question bank, local loopback)

metric value
release binary 17 MB
populated DB 12.8 MB
resident memory ~24 MB
/build latency p50 3 ms, p95 8 ms
/build throughput ~940 req/s at 20 concurrent
autosave writes ~470/s, p95 23 ms
import time ~10 s first run, ~3 s re-run

Recommended floor: 1 vCPU / 512 MB RAM / 5 GB disk. Comfortable headroom: 1 vCPU / 1 GB RAM / 10 GB SSD. No Postgres, no Redis, no object storage — one static binary plus one SQLite file.

Deploy (Termux + Cloudflare Tunnel)

The live site runs on an ARM tablet via Termux behind a dedicated tunnel:

# on the host
pkg install rust sqlite python git
git clone https://git.oiupoyt.space/oiupoyt/whaleprep.git
cd whaleprep && cargo build --release
./deploy/termux-start.sh
python3 scripts/import_examgoal.py whaleprep.db   # 9,813 real PYQs

# tunnel (once): cloudflared tunnel create whaleprep, point the CNAME at it
cloudflared tunnel --config ~/.cloudflared/whaleprep.yml \
  run ffc0df8c-0978-4b54-93a1-57c64fa1464c

See deploy/ for the service scripts and tunnel config. Live at: https://whaleprep.oiupoyt.space

Tests

cargo test

Covers the marking rules, numerical tolerance, and the urlencoded form decoding that shapes the build endpoint.