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: 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.
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.
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.
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.
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.
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).
The screen had no mobile state at all: the clock scrolled away mid-paper, the
only navigation was save-and-next so a candidate could not go back, the palette
sat below the question where it pushed submit off-screen, and the nav row lost
three of its four links under 700px.
Sticky header carries the clock and a progress bar; previous/next are real
buttons; the palette is a drawer below 1024px opened by a floating button that
sits above the tab bar. Primary destinations move to a bottom tab bar on
phones, data tables scroll inside their own card, and every control meets the
44px touch minimum.
Verified with a headless browser across five viewports rather than by reading
the stylesheet: scripts/audit_mobile.py fails on horizontal overflow, tap
targets under 44px, and content hidden behind a fixed bar; scripts/shots.py
captures each page at both widths. Both caught bugs I would otherwise have
shipped, including a palette toggle that lived inside the drawer it opened and
so could never be pressed on a phone.
fmt_dur drops a leading zero hour, so a 24 minute paper reads 24m 00s.
Axum + SQLite full-stack copy of the ZenithPrep PYQ practice app.
Paper builder with unseen-question exclusion, timed CBT screen with
palette and autosave, +4/-1 grading with numerical tolerance, per-question
analysis, history, dashboard with weak-subtopic tracking and coach notes.