OmniTutor
▸ P2 LLD first user-facing surface · discovery + 4 subjects

P2 · discovery + 4 subjects

First demoable thing. The 4 design mockups (discovery · physics · math · history · english) become real rendered pages backed by the v2 subjects + topics tables. Every tile clickable, every route real. Search bar functional. Click anything → /setup?topic=X (which 404s until P3 lights up).
▸ phaseP2 · discovery + subjects
▸ statusdraft · pending sign-off
▸ depends onP1 signed off · v2 schema
▸ unblocksP3 (setup modal) · P4 (plan)

1.Scope

Five user-facing routes go live: / (discovery) · /physics · /math · /history · /english. Each renders from a Jinja2 template against the seeded subjects + topics tables. The search bar accepts free-form text and routes to /setup?topic=<encoded>. Every clickable tile (subject card · topic chip · curiosity question · recent row · trending pop) is a real <a> with the right query string. The setup route returns 404 — P3 lights it up.

Acceptance demo: visit https://omnitutor.ai/ · see the discovery page rendered against real DB · click "Physics" tile · land on /physics · click "Classical mechanics" topic · land on /setup?topic=classical-mechanics&subject=physics&source=tile · 404 page is friendly and explains "P3 not yet built."

2.Before / after

▸ end of P1

  • FastAPI with /v1/healthz · /v1/session · /v1/hello
  • Middleware stack live (session · log · rate · moderation)
  • Anthropic + ElevenLabs clients wired
  • Frontend tokens.css · owl SVG · KaTeX loaded · static test page
  • CI deploys on push · backups run nightly
  • No public route at / yet · only _design/ static review
  • subjects + topics tables exist · empty

▸ end of P2

  • omnitutor.ai/ renders the discovery page
  • /physics · /math · /history · /english render real subject pages
  • 4 subjects + ~32 canonical topics seeded
  • Search bar routes to /setup?topic=…&source=search
  • Every tile + chip + recent + curiosity card is a real link
  • Recents section reads from DB (empty for new sessions, populated for returning)
  • Trending section ranks topics by 7-day lesson count
  • Friendly 404 on /setup until P3

3.Work plan

Eleven ordered steps. Templates first, then routes, then queries, then events.

▸ ordered work

  1. Template scaffold: web/templates/_base.html with topnav · brand · sign-in stub. _partials/owl.html · _partials/topnav.html. Jinja2 inheritance. Tokens.css imported once.
  2. Subjects seed: seeds/subjects.sql populates 4 rows (physics · math · history · english) with archetype + display name. Idempotent (ON CONFLICT DO NOTHING).
  3. Topics seed: seeds/topics.sql populates ~32 canonical topics (8 per subject) lifted from the 4 design mockups. canonical=TRUE. Idempotent.
  4. Repos: app/db/subjects_repo.py (list_subjects · get_subject_by_code) · app/db/topics_repo.py (list_topics_for_subject · get_or_create_topic for free-form). Pure asyncpg + pydantic.
  5. Discovery service: app/services/discovery.py with get_recents(user_id) (top 4 lessons by recency) · get_trending(limit=6) (topics by 7-day lesson count from events) · get_curiosity_cards() (hand-curated for M1).
  6. Discovery route: GET / renders discovery.html with subjects · recents · trending · curiosity_cards · tagline (locked at "Learn anything, on your terms.").
  7. Subject route: GET /<code> renders subject.html (or subject_language.html for English archetype). 404 if code not in subjects table. Template gets subject · topics · libraries (for STEM) · levels.
  8. Search handler: GET /search?q=<query> · POST /search from form · sanitizes input · 302 to /setup?topic=<urlencoded>&source=search.
  9. Tile-link generation: Jinja macro {% link_to_setup(subject_code, topic_code, source) %} · single source of truth for setup route URLs. All tiles use it.
  10. Event ingestion: client-side beacon on every page load (session.start if first hit) and every tile click (lesson.invoke with {subject, topic?, source}) · uses POST /v1/event.
  11. 404 page: friendly 404.html with owl + lead "I haven't learned that route yet · here's home →". Used for /setup until P3.

▸ file layout · what lands in the repo

omnitutor-app/
├── app/
│   ├── routes/
│   │   ├── discovery.py        # GET /
│   │   ├── subjects.py         # GET /
│   │   ├── search.py           # GET /search
│   │   └── errors.py           # 404 handler
│   ├── services/
│   │   └── discovery.py        # recents · trending · curiosity
│   ├── db/
│   │   ├── subjects_repo.py
│   │   ├── topics_repo.py
│   │   └── lessons_repo.py     # for recents query
│   └── templates/
│       ├── _base.html
│       ├── _partials/{owl, topnav, footer}.html
│       ├── discovery.html
│       ├── subject.html        # physics + math + history
│       ├── subject_language.html  # english
│       └── 404.html
├── seeds/
│   ├── subjects.sql
│   └── topics.sql
└── web/static/
    ├── tokens.css
    └── (existing P1 assets)

▸ canonical topic seed (excerpt)

-- seeds/topics.sql · idempotent · ~32 rows
INSERT INTO topics (id, subject_id, name, canonical) VALUES
  ('01HZS-PHY-001', 'physics', 'Classical mechanics', TRUE),
  ('01HZS-PHY-002', 'physics', 'Electricity & magnetism', TRUE),
  ('01HZS-PHY-003', 'physics', 'Quantum mechanics', TRUE),
  -- … 5 more physics
  ('01HZS-MAT-001', 'math', 'Algebra', TRUE),
  ('01HZS-MAT-002', 'math', 'Calculus & analysis', TRUE),
  -- … 6 more math
  ('01HZS-HIS-001', 'history', 'Antiquity', TRUE),
  -- … 7 more history (5 eras + 3 themes)
  ('01HZS-ENG-001', 'english', 'Travel English', TRUE)
  -- … 7 more english
ON CONFLICT (id) DO NOTHING;

▸ route → URL mapping

RouteMethodRendersNotes
/GETdiscovery.htmlmain landing
/physicsGETsubject.htmlscience archetype
/mathGETsubject.htmlscience archetype
/historyGETsubject.htmlhumanities archetype (palette tweak)
/englishGETsubject_language.htmllanguage archetype (CEFR ladder)
/search?q=…GET302 → /setupfree-form topic
/setup?…GET404.html (M1)P3 lights this up
/v1/eventPOSTJSON OKanalytics ingestion

4.Test plan

▸ acceptance tests

  1. Seed integrity: after fresh DB + seeds, SELECT COUNT(*) FROM subjects = 4 · SELECT COUNT(*) FROM topics WHERE canonical ≥ 30
  2. Discovery renders: GET / returns 200 · HTML contains 4 subject tiles · search bar · "Learn anything, on your terms." headline
  3. 4 subjects render: GET /physics · /math · /history · /english all return 200 with canonical topics from DB
  4. Unknown subject 404s: GET /chemistry returns 404 with friendly owl page (no canonical chem subject yet · M1 ships only the 4)
  5. Subject content matches mockup: Playwright asserts each subject page contains the expected topic count + library count + level cards (per the mockups in _design/)
  6. Tile click routes correctly: click any topic tile on /physics → URL ends with /setup?topic=<slug>&subject=physics&source=tile
  7. Search routes correctly: type "Bohr atom" in search · submit · URL becomes /setup?topic=Bohr%20atom&source=search
  8. Try-chip routes correctly: click any try-chip on discovery · URL routes to /setup?topic=<chip-text>&source=trychip
  9. Recents query: seed a fake user with 2 lessons · GET / · response contains the 2 lessons in recents section
  10. Trending query: seed events for last 7 days · GET / · trending section shows the seeded topics in count order
  11. Event ingestion: tile click fires POST /v1/event with type=lesson.invoke, payload={subject,topic,source} · row visible in events
  12. 404 page: GET /setup?topic=foo returns 404 with owl + "→ home" link · clicking link returns to /
  13. Mobile responsive: Playwright at 375px width · all 5 routes render readable · no horizontal scroll · tap targets ≥ 44px
  14. Logging: every request logs trace_id · path · status · latency · all 13 dictionary fields populated
  15. Idempotency on /v1/event: repeat same event with same Idempotency-Key · second response matches first · only 1 row in events

5.Acceptance gate

P2 ships when every line below is true. P3 cannot start until then.

▸ P2 done · sign-off list

  1. All 11 work-plan steps (§3) green · code in main
  2. All 15 acceptance tests (§4) pass · CI green
  3. Mukesh navigates all 5 routes manually · clicks 4+ tiles per page · everything routes
  4. Search bar verified with: 1 canonical topic match · 1 free-form unmatched query · 1 emoji query (graceful)
  5. Lighthouse score ≥ 90 on / for performance + accessibility
  6. No spinners on any of the 5 routes (rendered server-side · zero perceptible load time)
  7. Mukesh signs off · git tag v1.2-p2-shipped