Quiz runner — session rail
The desktop sidebar was `display: none` in the player theme, so the only way to
move between questions was a dropdown that showed numbers and nothing else. It
is now the left rail a Qbank session has: one row per question with its number,
an excerpt of the stem, its difficulty, and a mark for bookmarked ones. The
active row is marked with aria-current, answered rows turn green, and the rail
sticks while the question scrolls. Below 1150px there is no room for both, so
the topbar dropdown takes over as before.
Article pages — read straight through
Each section opened in a modal overlay, which meant a topic could not be read
end to end: you opened a section, read it, closed it, opened the next. Sections
now render inline as one page under a sticky contents rail, with the last-edited
date, matching how the reference reads. Deep links still work — `?section=` now
scrolls to and highlights the section instead of trapping the reader in a
dialog, and the rest of the article stays visible around it.
`scrollIntoView` is called defensively: it does not exist in every environment.
Tests: 2 new runner tests (rail lists number, excerpt and difficulty; navigates
and follows the active row; marks answered after a study response is submitted).
Stem assertions are now scoped to the question card, since the rail repeats each
stem as an excerpt. Article tests assert sections are inline and no dialog
opens. Full suites green: 96 backend, 135 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Question bank filters
The panel was a dialog inside a panel inside an aside, with raw checkbox lists
truncated at `.slice(0, 40)` — so most of the vocabulary was simply unreachable.
Replaced with the same facet rows the test builder uses: Status, Difficulty,
Systems, Disciplines, Diseases, Symptoms, Articles, each opening a search +
checklist panel and summarising as "Name +N". Tag lists show the most-used first
and reach the long tail by search instead of hiding it.
Category management page (/categories)
Renaming, reparenting and delete-with-move used to live inside that filter
panel. They now have their own page: a searchable tree with question and
subcategory counts, create-under-parent, and inline delete that rehomes the
questions. A category is never offered its own descendant as a parent, and one
with subcategories refuses deletion rather than orphaning them.
Tag vocabulary sanitised (scripts/sanitize_tags.py, idempotent, --apply to write)
The tags were model-generated per question, so the same concept recurred with
different casing and pluralisation. Applied to production, after a table backup:
83 renamed (Adhd→ADHD, Ige→IgE, 46,Xx→46,XX)
75 merged (Absence Seizures→Absence Seizure, Food Allergies→Food Allergy)
17 disease→keyword 27 unused deleted 6859 → 6740 tags
Symptom reclassification matches whole names only. Substring matching moved
"Whooping Cough" and "Rocky Mountain Spotted Fever" out of diseases, so the rule
now requires an exact match, and genuinely ambiguous terms ("seizure",
"jaundice", "murmur") are left alone rather than guessed at — misfiling a
diagnosis as a symptom is worse than an untidy vocabulary.
Test builder on mobile
The sticky bar's `margin: 4px -16px -100px` guessed the page's own padding and
overflowed when it differed; it now bleeds to the viewport instead. The bar was
also wrapping into three ragged rows — modes now span the top and Refresh sits
beside Create. Long facet names truncate rather than widening a row into a
sideways scroll, and panel rows got comfortable tap targets.
Tests: 9 new for the category page (reparent, descendant guard, delete-with-move,
default-to-uncategorized, subcategory refusal, create, search, server refusal);
the three category tests move off the bank suite with it. Full suites green:
96 backend, 133 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
Search
- Retrieval was hybrid in name only: the keyword filter was applied to the SQL
query, so results were the *intersection* of the two rankers. A question that
matched the meaning but not the literal string could never be returned. It is
now a union, fused with Reciprocal Rank Fusion (a text rank and a cosine
distance are not on comparable scales, so RRF uses only their orderings).
- Added a generated `search_vector` tsvector + GIN index, so the lexical half is
ranked full text rather than ILIKE substring matching.
- Chose Postgres + pgvector over OpenSearch/Elasticsearch: a search cluster
would add a second datastore to keep in sync and a JVM on this host, to
replace an index Postgres maintains inside the same transaction.
- Removed the keyword-only mode. It looks precise but silently drops the
question that asks the same thing in different words.
Embeddings — measured on 500 real questions, using each question's own
explanation as a paraphrase query (known answer, no hand labelling):
bge-small (local CPU, 384d) R@1 0.840 R@5 0.953 186ms/query
bge-m3 (LiteLLM proxy, 1024d) R@1 0.847 R@5 0.973 93ms/query
BGE-M3 wins on both quality and latency and needs no extra credential, since
llm.danvics.com already serves `openrouter-bge-m3`.
Three gaps this exposed, all fixed:
- Nothing recorded which model produced a stored vector, so changing models
silently mixed incomparable spaces. `embedding_model` / `embedded_at` now
stamp every vector, `GET /admin/embedding/health` reports current vs stale vs
missing, and regeneration defaults to stale-only.
- The generator read the model from env while the stamp read a Redis override,
so a vector could be labelled with a model that did not produce it. Both now
resolve through one function, with a regression test.
- Embedding at creation is best effort, and a failure left a question invisible
to semantic search forever. `retry_missing_embeddings` runs every 15 minutes
via Celery beat and backfills missing or stale rows.
- Query embeddings are cached in Redis per model, so typing is not a network
round-trip per keystroke.
`dimensions` is only sent to OpenAI's embedding-3 family; BGE-M3 rejects it.
Tests: 8 new backend tests (union not intersection, fusion ordering, per-ranker
failure degradation, provenance stamping, stale/missing accounting, generator
and stamp agreement). Full suites green: 95 backend, 127 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
Article page — cross-checked against AMBOSS's Library and article pages, which
drill Library → Clinical knowledge → Pediatrics → topic → article and never
list a topic's questions. You reach questions the other way, via "Start Qbank".
Ours printed every linked question on the reading page with its correct answer
and explanation, so opening an article spoiled its questions before the learner
ever attempted them. Replaced with a "Practise this topic" panel: how many
questions are linked, a count and mode picker, and a Create test button that
builds a test scoped to the article. Educators keep a collapsible membership
list for unlinking — stems only, no answers.
Lab values panel:
- Removed the Sources footer and its article deep links, as asked.
- One weight per row: the test name carries it, the range no longer competes.
Ranges use tabular figures and the age column lines up in a grid instead of
wrapping raggedly; the reference note no longer promises source links.
- Dropped the CSS for markup that no longer exists.
Tests: 5 new frontend tests for the practise panel (offers a test without
revealing stems/answers/explanations, article-scoped builder payload, error
handling, hidden for learners when nothing is linked, educator unlink); the
article and lab tests now assert those leaks are gone. Full suites green:
88 backend, 127 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
Matches the AMBOSS screenshot: "Set session topics" and "Session criteria" sit
side by side, the question count lives at the bottom of the criteria card as
"20 /64", and session type plus Start are a sticky bar at the foot of the
viewport so neither is behind a scroll.
- Two-column grid, stacking to one column under 900px.
- Facet rows now lead with a + affordance and show the selection as a chip
("Cardiology" "+1"); an unset facet stays plain "All".
- Reset moved to the top right with its ↺ icon.
- Adaptive session is its own row inside the criteria card, with the spark mark
and toggle on the right.
- A More expander holds what AMBOSS does not have — sharing, the exam time
limit, and the unused/incorrect explanation — instead of stacking them.
- Mode is a segmented control in the bar rather than radio cards in the flow.
Tests: 2 new frontend tests pinning the count to the criteria card and mode plus
Create Test to the sticky bar, and the time limit appearing under More only in
exam mode. Full suites green: 88 backend, 122 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
Interacted with AMBOSS's custom session builder to copy the actual pattern.
It never shows facet options inline: each facet is one row carrying its current
selection, and clicking it opens a panel with a search box, the helper line
"By default, all X are included unless filters are selected", an "Include
questions from:" checklist, and Reset / Done. A row with several selections
reads "Cardiovascular System +1", and the available count updates live.
Ours had the opposite: a narrow sidebar of oversized stacked headings with two
separate inner scroll panes, so Disciplines and Symptoms each showed their own
scrollbar and the whole column fought the form beside it.
- New FacetPicker + FacetRow components: slide-in panel on desktop, bottom
sheet on mobile, Escape and backdrop close, per-facet search and reset.
- CustomQuizPage is now a single 720px column: Set test topics (filter search +
Exams / Systems / Disciplines / Symptoms / Articles / Saved rows), Test
criteria (title, adaptive toggle, Difficulty and Status rows, sharing),
Question count with the live pool, and Test type as two radio cards.
- One cross-facet "Filter search" lists matching options from every facet and
toggles them in place, matching AMBOSS's search-first entry point.
- Difficulty and Status moved out of the sidebar into their own pickers; mode
is radio cards rather than a select.
The builder payload and the /questions/builder contract are unchanged.
Tests: 4 new frontend tests (facet summary including +N, close-keeps-selection
and per-facet reset, cross-facet search toggling, reset-all-topics); the
existing builder tests now drive the pickers. Full suites green: 88 backend,
120 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
Analysis / recommendations (AMBOSS parity, verified on next.amboss.com):
- GET /study-tools/recommendations ranks focus areas by the study time most
likely to raise the score. Readiness is the learner's accuracy in a category
shrunk toward their own overall accuracy in proportion to sample size, so two
unlucky answers do not read as a knowledge gap; it unlocks after 40 answers.
Relevance is the share of the bank a category holds. Counts roll up through
the category tree, so a system inherits its children's questions.
It is deliberately not called EPC and does not claim to predict an exam.
- New /analysis page: Performance and Recommendations tabs, readiness summary,
adaptive-session box, and expandable focus rows showing questions seen,
answered correctly, the linked article and a per-topic practice action.
Per-category educator grants:
- category_grants table (migration p8b9c0d1e253) plus utils/category_grants.py
resolving a grant to the category and all of its descendants.
- Question create, edit, delete, bulk and the manager summary now accept a
moderator OR an educator granted the affected categories, and refuse moves
that would push a question out of the holder's scope. Summary counts are
scoped to the grant.
- Moderator endpoints to list, add and revoke grants, plus /my-grants driving
the nav link and the manager's scope banner; grantable-users avoids handing
moderators the admin-only user list.
- GrantsPanel in the question manager: grant, list and revoke with inline
confirmation.
Question page:
- The category trail was a fixed 78px band that wrapped into several rows and
pushed the stem down the page, followed by three more stacked strips. It is
now one scrollable meta line (breadcrumb + difficulty + type) and a single
AMBOSS-style action bar (Mark / Listen / Listen through / Clear) between the
stem and the options. Difficulty is exposed on the runner payload.
Deploy fix: index.html shipped with no cache header, so browsers kept serving
the previous bundle references and a release looked like nothing had changed.
nginx now sends no-cache for HTML and immutable long-cache for hashed assets.
Tests: 16 new backend (recommendation shrinkage, roll-up, locking, grant scope
across create/edit/delete/bulk/summary, moderator gate) and 10 new frontend.
Full suites green: 88 backend, 116 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
Quiz management area (AMBOSS parity, verified against next.amboss.com):
- GET /quizzes/sessions returns one management row per accessible quiz —
attempt state, live answered/total from Redis, last score and activity —
so the page no longer fans out per-quiz requests.
- QuizzesPage rebuilt as a session list grouped by day with a progress bar,
a state-aware primary action (Start / Resume / Review) and an action menu
matching AMBOSS: Analysis, Repeat, Rename, Share, Edit, Category, Delete.
Rename and delete confirm inline; no browser popups.
- Sessions / Library / Categories tabs replace the flat card grid.
- QuizPage honours ?restart=1 so Repeat always begins a fresh attempt.
Question manager (new moderator page at /questions/manage):
- GET /questions/manage/summary counts editorial gaps; /questions/bank gains
a `needs` filter (category / explanation / difficulty / private) so the
health tiles double as one-click filters.
- POST /questions/bulk applies category, difficulty, sharing or delete to up
to 500 checked questions in one call, moderator-only.
- Question edit/create modals extracted to components/QuestionEditors.jsx and
shared by the question bank and the manager instead of being duplicated.
Showcase articles:
- scripts/seed_showcase_articles.py seeds eight short starter articles across
the main pediatric systems, each filed under a real category, with stable
hex section IDs and links to bank questions from the same category.
Mobile: dedicated stylesheets for both pages — rows stack, the action menu
becomes a bottom sheet and the bulk bar docks to the bottom edge.
Tests: 9 new backend tests (session feed states, ordering, Redis-outage
degradation, visibility; bulk actions, gap filters, moderator gate) and 9 new
frontend tests. Full suites green: 72 backend, 106 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yhHB8Pc7oQqyqn2Vo9DXA
Filters (Exams, Disciplines, Symptoms, Categories, Articles, Saved, Difficulty) open in a scrollable side drawer with close button; the main column stays clean. 97 frontend tests pass.
Create/bank pages use AMBOSS-style facets: Exams, Disciplines, Symptoms, Systems, Articles, Saved. Personal question libraries with add-to-library in study modal. Adaptive session shortcuts from performance (including weakest topics). Quiz restart with fresh attempt. Category counts computed with two grouped queries. Migration n7a8b9c0d142. 63 backend and 97 frontend tests pass.
Bank filters (state, difficulty, categories with educator controls, tags) live in a left side box; main column keeps search and list. Create-page settings/actions grouped into one column with full-contrast primary button. 97 frontend tests pass.
Sample quiz demonstrates option explanations, key points with article links and linked cards. Bank study modal shows per-option explanations and key points. Performance shows main categories with an expandable hierarchy. AMBOSS-style picker polish (chevrons, search box, switch, auto title). Mobile spacing fixes. 97 frontend tests pass.
Systems live in a collapsible side box with search and drill-down subcategories; filters apply live. Mobile collapses the panel. 97 frontend tests pass.
Key points on questions link into article sections (AMBOSS-style) with samples; difficulty tagging with builder/bank filters; adaptive session algorithm prefers unanswered questions then recycles older incorrect ones, weakest categories first with damping; question create/edit is now admin/educator only; expired exams no longer auto-submit on resume; exam suspend messaging updated. Migrations k4f5a6b7c819, l5a6b7c8d920, m6a7b8c9d031. 63 backend and 97 frontend tests pass.
PREP provenance becomes keyword tags tied to the source quizzes; PREP question categories retired. Response statistics get a persistent hide/show toggle. Comments redesigned with avatars, badges and a cleaner compose box. Create Custom Test is now a prominent card on the Quizzes page only. Hierarchy conversion gains pediatric sub-specialties and disease children. 96 frontend tests pass.
Subject tags map to 26 canonical systems via a reversible, backed-up conversion script; questions keep old categories as extra links. Category add/edit now opens a proper dialog instead of stacking above the chips. 95 frontend tests pass.
Questions support an explanation per option, edited in the question dialog and shown in study feedback. Keys must match current options. Migration j3e4f5a6b708. 60 backend and 95 frontend tests pass.
Timed quizzes start as exams and learning quizzes as study without a second mode prompt; reopening resumes automatically. Removed quiz code display from in-progress list and the verbose statistics basis sentence. Lab rows keep logical age order per test. 93 frontend tests pass.
Lab references deep-link to article sections or external sources, show linked cards with study links, and educators can attach cards and article targets. Grouped panel layout. Migration i2d3e4f5a607. 57 backend and 93 frontend tests pass.
Accuracy per category from completed non-expired general-bank answers, counting each question in its primary and additional categories. 56 backend and 90 frontend tests pass.
Quiz share links replace the PIN copy with a public /share/{token} landing page; owners can enable/revoke without showing the full link. Moderated article/question comments with approval flow, bounds and rate limits. Educator AI article drafts/refine and private card generation with Celery job polling. Migrations f2a1c9d4e801 and g4b7e2f5a903. 50 backend and 85 frontend tests pass.
Escape raw HTML in article markdown, honor section deep links, filter card link listings by bank visibility and publication status, validate source sections. 44 backend and 77 frontend tests pass.
Orthobullets-style Cards naming, per-card link panel to attach/unlink bank questions, article linked-content counts. New FlashcardsPage test suite; 75 frontend tests pass with build.
Tutor questions require owned selected attempts; similarity context filters eligibility before ranking. Uploads move to a permission-aware boundary with reference ACLs, canonical legacy aliases, pre-mutation attachment checks and card-aware moderator rules. Nginx stops caching media and supplies native byte ranges. Verified 37 deployed-image backend tests, 69 frontend tests/build, real pgvector/Nginx/browser checks, and two independent reviews.
Remove the challenge from standalone and landing login plus backend verification; retain registration/contact protection and existing password, email verification, SSO and login rate-limit checks. Verified 18 backend tests in deployed image and 17 frontend tests plus build.
Recovery snapshot of the existing worktree before the Orthobullets-inspired revamp. Includes explanation images, classification snapshots, quiz visibility/resume fixes, quiz codes, TTS options and bot formatting. Secret heuristic and Python syntax checks passed; not a release or full behavioral validation.
Adds a 6th feature bullet (with NEW badge) and a 7th icon tile for the
pediatric calculators + bedside emergency reference that ships with the
Pediatric AI Scribe app. Mentions weight-based dosing, sepsis, status
epilepticus, RSI, burns, anaphylaxis pathways.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously sent ?category_id= (empty string) which FastAPI rejects
with 422 validation error when parsing as int.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Restore original single-column 600px layout
- Keep documents abridged to 5 most recent with 'Show all' button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ai_decide now samples 4 points across the section (start, 1/3, 2/3, end)
instead of just the first 30 + last 20 pages. This gives accurate strategy
detection on large documents where the answer format might be deeper in.
New ai_answer extraction mode:
- Extracts questions from Q&A-format PDFs that have no answer key
- AI picks the correct option from each question's choices
- Generates explanation using document context + medical knowledge
- Useful for PDFs like practice tests where answers were never included
- Available manually and as an ai_decide strategy
Flashcard decks can now be renamed:
- PATCH /flashcards/{deck_id} updates title
- Inline edit on FlashcardsPage with responsive layout (input full-width,
buttons wrap under it so Cancel never overflows the card)
- Title truncates with ellipsis when not editing
Note: generate mode (textbook -> MCQs) is unchanged per user request.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Suspend now pauses the timer instead of letting it run out:
- 'Suspend & Leave' sends suspended=true with time_left to backend
- On resume, backend re-anchors started_at to now with held time_left
- Closing tab without suspending continues to run the timer (unchanged)
Timer-expired auto-submits are marked with expired=1 and excluded from:
- Attempt history (GET /attempts/history)
- Dashboard stats (quiz count, total attempts, average score)
- Attempt list (GET /attempts)
- DDL: ALTER TABLE quiz_attempts ADD COLUMN expired INTEGER DEFAULT 0
Course-quiz decoupling is preserved — these changes only touch
non-course quizzes (Quiz.course_id IS NULL).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Local uploads appeared stuck at 0% because onUploadProgress
doesn't fire granularly on fast local networks or when network
buffering makes the browser report instant completion.
Changes:
- Animated indeterminate progress bar when percent unavailable
- Elapsed seconds counter so user sees activity
- Separate "Uploading" vs "Processing on server" stages
- Hint message about large PDFs taking time
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Creators preview courses without enrolling, so markComplete and SCORM
onComplete were hitting the progress endpoint and getting 403. Now
guarded by enrollment check — buttons hidden and callbacks skipped
for non-enrolled users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SCORM runtime data (bookmarks, scores, lesson position) now saved to
Redis via GET/PUT /courses/{id}/lessons/{id}/scorm/data endpoints.
Data loads before iframe renders and saves on Commit/Finish/Terminate.
90-day TTL on stored data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SCORM content expects window.API (1.2) or window.API_1484_11 (2004)
on the parent frame. Without it, content fails to initialize.
Implements all 8 required methods for both SCORM versions:
- Initialize/LMSInitialize, Terminate/LMSFinish
- GetValue/LMSGetValue, SetValue/LMSSetValue
- Commit/LMSCommit, error methods
- Auto-marks lesson complete when content reports completion
- Removed iframe sandbox to allow parent frame API access
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Certificate: PDF generated on course completion, download from course page
- SCORM: upload ZIP packages as lesson type, served in iframe, manifest parsed
- QTI 2.1: export selected/all questions as XML, import QTI files into bank
- Uses fpdf2 for certificate PDF generation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add list-docs, fix-stuck-docs, reprocess-doc CLI commands
- Create ADMIN.md with deployment, CLI, database, and troubleshooting docs
- Add HIBP warning to Settings page password change
- Remove separate Account nav link (already in Settings)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fully decouple course quizzes from main quiz system (hidden from
dashboard stats, history, search, results page)
- Course quiz results show "Back to Course" instead of retake/delete
- Add allow_review toggle for course creators to control answer review
- Show quiz title on course page, hide pool size from students
- Add course thumbnails to browse cards
- Replace passlib with bcrypt directly (compatible with existing hashes)
- Add HIBP breached password warnings on register/reset/change password
- Add CLI management tools (reset-password, set-role, stats, etc.)
- Fix quiz PATCH endpoint: ownership check instead of moderator-only
- Add max_length validation on course/module/lesson titles
- Fix score display bug on results page (0 of N when review disabled)
- Fix question count on course quiz start (show per-attempt, not pool)
- Improve suspend warning for timed course quizzes with max attempts
- Clean up validation error messages (show "Invalid email" not Pydantic dump)
- Add DDL migration for allow_review column
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove category, thumbnail_url references (not on Course model)
- Fix sort_order → position (matching actual column name)
- Fix CourseCreate schema: only title + description
- Fix tab labels: "My Created" → "Created by Me"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LMS / Course System:
- Course → Module → Lesson hierarchy (any user can create)
- Lesson types: text, video (Vimeo/YouTube/local), document, quiz, live_session
- Video provider auto-detection from URL
- BBB API integration (create/join meetings) with env config
- Course enrollment with per-lesson progress tracking
- AI content generation/refinement for text lessons
- Draft/published/archived status workflow
- Subscription gate placeholder (requires_subscription flag)
- Thumbnail upload support
- Module/lesson reorder with up/down controls
User Quiz Creation:
- Any user can create quizzes from question bank (was moderator-only)
- User quizzes: is_published=0, is_shared=0 (private by default)
- Fixed section_id fallback: None instead of hardcoded 1
Manual Question Creation:
- POST /questions/create endpoint for manual MCQ entry
- CreateQuestionModal on QuestionBankPage with options, radio for correct answer
- Auto-generates embedding on creation
Frontend:
- CoursesPage: Browse/My Courses/Created tabs with search and pagination
- CourseDetailPage: Student view with module accordion, lesson viewer, progress
- CourseEditorPage: Full course builder with AI generate, question bank browser
- Courses link in Navbar
- Create Question button on Question Bank (available to all users)
Backend:
- 5 new tables: courses, course_modules, course_lessons, course_enrollments, course_lesson_progress
- Course model + schemas + router (22 endpoints)
- BBB_SERVER_URL + BBB_SECRET config
- Updated CLAUDE.md with LMS documentation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move GET /shared before GET /{deck_id} so FastAPI matches it correctly
(was being caught by /{deck_id} and failing int parse on "shared")
- Remove duplicate /shared endpoint left after move
- Fix corrupted middot character (U+FFFD) in My Decks card count display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Users can share/unshare their flashcard decks (toggle button)
- Shared tab: paginated list of all shared decks sorted by avg rating
- Star rating system (1-5): click stars to rate, updates average live
- Each user can rate each shared deck once (upsert on re-rate)
- Admin can force-unshare any deck (remove from shared listing)
- Shared decks are study-able by anyone (read-only access)
- Owner name displayed on shared deck cards
- Load more button for shared decks (12 per page)
- New DB table: flashcard_deck_ratings (user_id+deck_id unique)
- New column: flashcard_decks.is_shared (default 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix: flashcard task used wrong Redis key prefix (job: vs extraction:)
causing progress to stay on "pending" after completion
- Flashcards are now user-scoped: each user sees only their own decks
- Soft-delete decks: DELETE moves to trash, ?permanent=true to destroy
- Trash tab on flashcards page: restore or permanently delete decks
- Delete individual cards with inline confirmation in browse view
- Card edit/delete now allowed for deck owner (not just moderators)
- ExtractionProgress label prop: shows "Generating Flashcards" not
"Extracting Questions" for flashcard jobs
- Added deleted_at column to flashcard_decks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CLAUDE.md: comprehensive AI synopsis for working on the codebase
(architecture, critical rules, common patterns, what not to do)
- Fix ExtractionProgress showing "Extracting Questions" for flashcards
— now shows "Generating Flashcards" with correct navigation on done
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New feature: generate flashcards from PDF sections using AI, completely
separate from the existing quiz system.
Backend:
- FlashcardDeck + Flashcard models with cascade deletes
- flashcard_tag_links table for tag classification (reuses question_tags)
- /api/flashcards/ router: CRUD for decks, browse/search cards, tag filtering
- generate_flashcard_deck Celery task with chunked processing + progress
- FLASHCARD_PROMPT in extraction_modes.py (15 cards per chunk)
- "flashcard" added to admin model task types
Frontend:
- FlashcardsPage: deck grid + card browser with search/filter
- FlashcardStudyPage: flip cards, mark known/review, keyboard nav,
shuffle, progress bar, completion screen
- DocumentDetailPage: "Create Flashcards" button alongside "Extract Quiz"
- Navbar: Flashcards link
- AdminPage: flashcard in model task dropdown
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- App.jsx: simplify to single Routes block with Outlet layout pattern — fixes 404
- App.jsx: /home always accessible regardless of auth state
- LandingPage: show Dashboard/Quizzes/Question Bank nav links + Go to App CTA when logged in
- LandingPage: hero shows app navigation buttons when signed in
- LandingPage: fix apostrophe JSX syntax error in textarea placeholder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- teach.py, questions.py: replace f-string SQL with parameterized CAST(:vec AS vector) queries
- auth.py: add reusable check_rate_limit() Redis helper
- teach.py: rate limit /chat to 30 req/10min per user
- tts.py: rate limit /speak to 60 req/hr per user
- teach.py: stronger system prompt — no clarifying questions, use markdown, answer directly
- TeachChat.jsx: render assistant messages with ReactMarkdown (already in package.json)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- teach.py: use _proxy_model() + pass api_key/api_base from settings (fixes LiteLLM provider error for openrouter/bedrock models)
- teach.py: accept model_id in ChatRequest so frontend can select model
- main.py: remove titan-embed-v2 from general seed, auto-delete legacy entry on startup
- main.py: kill stale idle-in-transaction DB connections at startup to prevent DDL lock hangs
- main.py: set lock_timeout=10s on DDL connection as fast-fail safety net
- TeachChat.jsx: fetch /teach/models, show selector dropdown in header when >1 model available
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Embedding:
- Embedding model now configurable via Admin UI (More tab) or LITELLM_EMBEDDING_MODEL env
- Calls LiteLLM proxy directly via httpx (bypasses LiteLLM library param validation)
- Passes dimensions=1024 to proxy; Redis setting overrides env var
- Default model: ge-gemini-embedding-001 (Gemini AI Studio, 1024-dim)
- Test button in admin UI to verify model works
- Fixed vector_service to use httpx + Redis model (was broken with non-prefixed model names)
Polly:
- Global enable/disable toggle in Admin → More settings (stored in Redis)
- /tts/voices filters out polly/* when disabled
- /tts/speak rejects polly requests when disabled
Job cancellation:
- POST /quizzes/job/{job_id}/cancel endpoint
- Cancel button on JobsPage for running jobs
- Celery task checks Redis status at each chunk boundary and exits cleanly
- Fixes DB lock on restart caused by cancelled jobs leaving open transactions
Admin UI:
- Settings tab renamed to "More" (heading: More Settings)
- Model row overflow fixed (minWidth: 0 + ellipsis on model_id)
- Embedding model search shows all proxy models (no auto-filter by "embed")
- Navbar correctly excludes cancelled/failed jobs from "extracting" count
README:
- Added Rebuild & Restart section with commands
- Updated embedding model reference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: setQuizMode(mode) was called BEFORE the network fetch completed.
React immediately re-rendered showing the quiz, but quiz.questions still had
the initial load data (without correct_answer for study mode). On fast desktop
networks the fetch completed before the user could tap; on slower mobile
networks the user tapped an option while correct_answer was still null,
making every answer appear incorrect.
Fix: setQuizMode(mode) moved to AFTER setQuiz(quizRes.data) and
setAttemptId(). The ModeSelectScreen stays visible while data loads,
then switches to the quiz view only when all data (including
correct_answer) is ready.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Mobile touch fix:
- Added touch-action: manipulation and -webkit-tap-highlight-color: transparent
to .question-card .option CSS — prevents mobile browsers from interfering
with tap events (double-tap zoom, highlight flash, delayed clicks)
Documents hidden for regular users:
- Dashboard: stats card hides Documents count for non-moderators
- Dashboard: document list section completely hidden for non-moderators
- Documents API not called for non-moderators (saves a request)
Timer improvements:
- Custom timer input on ModeSelectScreen — set any number of minutes
for exam mode (overrides the quiz default, or add time to untimed quizzes)
- timeLeft saved to Redis progress every 1.5s (alongside answers, mode, voice)
- On resume: timer restarts from the saved remaining time
- Timer auto-submits when it reaches 0 (already worked, now also works on resume)
- Progress bar shows remaining time relative to what was left at resume
Nextcloud settings — server-side storage:
- GET /auth/me/settings — load user settings from Redis
- PUT /auth/me/settings — save settings to Redis (linked to user_id)
- SettingsPage loads Nextcloud config from server on mount
- Saves to BOTH server (cross-browser persistent) and localStorage (for UploadPage)
- Clear function wipes both server and localStorage
- Switching browsers and logging in restores Nextcloud settings
Also fixed: saved.attemptId → saved.attempt_id in ModeSelectScreen resume check
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Root causes:
1. joinedload(Quiz.questions) was SLOWER (129ms vs 75ms) with the custom
secondary relationship — removed, back to lazy load
2. Redis progress could have stale attempt_id from a completed submission:
GET /attempts/progress now validates the attempt exists and is not completed
before returning; stale entries are auto-deleted
POST /attempts/{id}/submit now clears Redis progress server-side on submit
3. Navbar jobs polling was running every 4s even with no active jobs,
causing unnecessary requests — now polls every 30s when idle,
every 3s when jobs are actively running
Also cleared 3 stale Redis progress entries that were pointing to
already-completed attempts (which caused silent submit failures).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Extraction modes (no restart needed — code ready for next Celery deploy):
- New QuizCreate.extraction_mode field: standard|questions_only|two_step|regex|ai_decide
- extraction_modes.py: independent implementations that don't touch standard path
- questions_only: extract Q+options, correct_answer="PENDING" for manual fill
- two_step: separate answer key section scan + phase1/2/3 matching
- regex: AI detects answer pattern, generates regex, applies to full doc
- ai_decide: AI reads samples from start+end and picks strategy
- DocumentDetailPage: Extraction Mode dropdown with description per mode
- quiz_tasks.py: routes to correct mode, standard path completely unchanged
Database:
- Deleted 11 orphaned questions from PREP 2013 extraction (quiz 12 was already deleted)
- 268 questions remaining (all PREP 2012)
UI fixes:
- Nextcloud section in Settings now only shown to moderators/admins
(regular users can't upload PDFs so they don't need Nextcloud)
- Upload PDF already hidden in navbar for non-moderators (confirmed correct)
- Resume quiz: now async — study mode quiz data loaded BEFORE showing quiz
so correct_answer is available immediately for feedback
- Resume saves and restores voice selection
- voice field added to ProgressSave schema and Redis storage
- Progress save dependency includes selectedVoice
Attempts:
- POST /attempts/start: reuses existing incomplete attempt by default (fresh=false)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
In-progress quizzes section (Quizzes page):
- Shows all incomplete (not submitted) attempts in an amber ⏸ panel
- Each shows quiz title, start date, question count
- Resume button → navigates to quiz (which loads saved answers from Redis)
- Delete button (ConfirmButton) → deletes attempt + clears reminders
Quiz navigation warning:
- beforeunload handler warns when closing browser tab/window mid-quiz
- Leave confirmation modal when navigating away via React Router
(shows "Your progress is saved — resume later" + Stay/Leave buttons)
Quiz progress:
- Scroll on Next/Previous REMOVED per user request — page no longer scrolls
- Progress saved to Redis on every answer/navigation (1.5s debounce)
- Works across browsers, survives logout
- ModeSelectScreen loads saved progress from API and shows Resume button
Attempt management:
- POST /attempts/start now REUSES existing incomplete attempt (no more
creating duplicate incomplete attempts on every quiz start)
Pass fresh=true to force a new attempt
- GET /attempts/in-progress — list incomplete attempts for current user
- DELETE /attempts/{id} — delete attempt + reminders
Cleaned up 10 duplicate incomplete attempts from the database
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Critical bug fix:
- DELETE /quizzes/{id} was returning 500 due to {\"quiz_id\": None} update using
the DB column name instead of Python attribute name (source_quiz_id).
Fixed with {QuestionModel.source_quiz_id: None, synchronize_session=False}
Extraction fix:
- Two-phase extraction was incorrectly triggering for PREP 2012/2014 format
documents that have 'Preferred Response:' in their explanation text.
Fix: check for inline 'Correct Answer:' first — if found, always use standard
extraction regardless of 'Preferred Response:' appearing elsewhere.
Quiz trash bin:
- DELETE /quizzes/{id} now soft-deletes (sets deleted_at)
- GET /quizzes/trash — list deleted quizzes (moderator)
- PATCH /quizzes/{id}/restore — restore from trash
- DELETE /quizzes/{id}/permanent — permanent delete (must be in trash first)
- TrashPage.jsx — accessible via Settings → Admin → Trash
Hide/publish quizzes:
- is_published column on quizzes (1=visible, 0=hidden)
- PATCH /quizzes/{id}/publish?published=false — hide from regular users
- Moderators see all quizzes; regular users only see published
- 👁/🙈 toggle button per quiz card (moderators only)
Quiz progress resume (cross-browser via Redis):
- POST /attempts/progress — save {answers, current_idx, mode} to Redis (7 days)
- GET /attempts/progress?quiz_id=N — retrieve saved progress
- DELETE /attempts/progress/{quiz_id} — clear on submit
- QuizPage auto-saves to Redis every 1.5s (debounced) while in progress
- ModeSelectScreen loads saved progress from server, shows Resume button
- Works across browsers, devices, and after logout
Delete attempt:
- DELETE /attempts/{id} — user can delete own attempt + clears reminders for that quiz
ConfirmButton component:
- Replaces all window.confirm() / window.prompt() across the app
- Double-click pattern: first click shows [Confirm] [Cancel] inline
- Applied to: QuizzesPage, DocumentDetailPage, QuizEditPage, QuestionBankPage
Category delete (QuestionBankPage):
- window.prompt() replaced with inline modal dialog with select dropdown
- User chooses where to move questions before deletion
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Extraction fixes:
- OCR normalization: 'Pref erred' → 'Preferred', 'ltem' → 'Item' applied to boundary
scan, Phase 1 questions, and Phase 2 answer key content before AI processing
- Chunk boundary: Phase 1 chunks now capped at (answer_section_start - 1) so no
chunk bleeds into the answer section — (51, 100) becomes (51, 55) for PREP 2013
- Result: Phase 1 gets 2 clean chunks (1-50 and 51-55), Phase 2 gets pages 56-227
Category creation in DocumentDetailPage:
- Replaced window.prompt() with inline input form (more reliable, no browser quirks)
- Fixed option value type: String(c.id) ensures consistent string comparison with
selectedQuestionCategoryId state (prevents type mismatch in controlled select)
- "+ New" button toggles inline form; Enter key or Add button submits
Deletion safety (confirmed):
- Deleting a quiz: questions detached to bank if exclusive, kept if shared — NEVER deleted
- Deleting a question category: questions uncategorized or moved — NEVER deleted
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Two-phase extraction:
- Detects end-of-document answer key format by scanning last 40 pages for
"Preferred Response:" (PREP 2013, 2014 etc use this vs PREP 2012 inline "Correct Answer:")
- Phase 1: Extract questions with item_number field, allow null correct_answer
- Phase 2: Extract answer key (item_number → letter) from last 40% of document
- Phase 3: Match questions to answers by item number, resolve letter → full option text
- Unmatched questions go to skipped list with reason shown in Jobs page
- Standard inline format (PREP 2012) unchanged
Updated extraction prompts:
- item_number field added to all extractions for cross-referencing
- Image content rule: "Item CXXXB" figure references must NOT be treated as new questions
- Recognises both "Correct Answer: X" and "Preferred Response: X"
- ANSWER_KEY_PROMPT: dedicated prompt for extracting answer key tables
Quiz navigation scroll:
- Clicking Next, Previous, or question number now scrolls the question card
into view (smooth scroll to start of question-card div)
Code: extract_questions_no_answers(), extract_answer_key(), _call_model() added to ai_service.py
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Bugs fixed:
- 'Select all' now fetches ALL matching IDs from server (GET /questions/bank/ids)
not just the currently loaded page — works for 268 questions
- Bulk category assign: fixed 'Remove category' value ('0' → 'remove'),
now shows error message on failure instead of silent fail
- Search mode (Keyword/Semantic/Hybrid) moved to a clearly labelled dropdown,
visually separated from category filter chips — eliminates confusion
- Category chips now have a 'Filter:' label to distinguish from other buttons
New features:
- Page size selector: 50 / 100 / 200 / All — loads the requested count in one shot
- Bulk actions moved to its own clearly labelled bar (only shown for moderators)
- Category delete with questions: prompts user to choose target category before deleting
(uses new move_to param on DELETE /question-categories/{id})
- GET /questions/bank/ids endpoint for server-side select-all
Backend:
- DELETE /question-categories/{id}?move_to={id}: move questions to another category
before deletion instead of always leaving them uncategorized
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Quiz layout:
- Desktop: sticky right sidebar (200px) showing all question numbers with
answered/current status — like UWorld. Main content fills the rest.
- Mobile: sidebar hidden; a "N / Total ▲" toggle button between Prev/Next
reveals a collapsible grid of question numbers below the nav buttons
- Question dots: circle buttons colored blue=current, green=answered, gray=unanswered
- Both sidebar and mobile grid close/update on selection
Jobs page (/jobs — moderator):
- List of all extraction jobs (from Redis, same session or different browser)
- Each job shows: status badge, questions extracted, skipped count
- "Details" expands to show:
- Skipped questions warning: names + explanation of why (no correct answer found)
- Chunk breakdown: per-50-page results
- Full extraction log (collapsible <details>)
- Live polling for running jobs
- "Open Quiz" button links directly to the quiz
Navigation:
- Jobs page linked from NavBar jobs badge dropdown
- Route: /jobs (moderator required)
- App.jsx updated with new route
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- safeNavigate: removed window.confirm() entirely — navigating between
question numbers never loses answers (they're in React state), so the
popup was wrong and annoying
- handleSubmit: replaced window.confirm() with a 3s toast pill at the
bottom of the screen (dark, slides up, fades away automatically)
- ModeSelectScreen: removed skipped-questions warning block — internal
extraction detail, not useful to show users
- Added fadeInUp CSS keyframe for the toast animation
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Jobs (cross-browser/cross-session):
- POST /quizzes/ stores job_id in Redis under user key (extraction:user_jobs:{uid})
- GET /quizzes/jobs returns all recent jobs for current user from any browser/session
- Navbar JobsBadge polls /quizzes/jobs API every 4s (not localStorage)
- Shows all recent jobs with status badges; links to quiz when complete
- Badge visible even after extraction completes so you can always get back
Mobile navbar fix:
- .navbar .container height was overriding dropdown to 52px (clipping all links)
- Fixed by using .navbar-inner class for the header row only
CLI extract command:
python manage.py list-sections [doc_id] — list docs + sections with IDs
python manage.py extract <section_id> — inline blocking extraction
python manage.py extract <section_id> --bg — background via Celery
python manage.py jobs — show all extraction jobs in Redis
python manage.py jobs --user <email> — filter by user
Quiz delete + question bank:
- When a quiz is deleted, questions that belong ONLY to that quiz are deleted
- Questions shared with other quizzes (via junction) are kept in the bank
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Fonts:
- Added Google Fonts: Inter (UI), Playfair Display (headings), Source Serif 4 (reading)
- Default theme now uses Inter — clean, modern, professional
- Warm Brown theme: body text uses Source Serif 4 (screen-optimized modern serif,
not old-fashioned Georgia), question text uses Source Serif 4 for readability,
headings use Playfair Display (elegant, editorial), UI elements use Inter
- Result: literary feel without looking like a Word document
Mobile Navbar (hamburger menu):
- Desktop: horizontal nav unchanged
- Mobile: logo + right-side controls (jobs badge + hamburger icon)
- Hamburger animates to X when open, full-width dropdown below navbar
- Active route highlighted; auto-closes on navigation
Jobs indicator:
- Amber spinner badge in navbar when extractions are running
- Tracks jobs in localStorage (pedquiz_jobs key)
- ExtractionProgress updates localStorage as steps arrive
- Click badge for dropdown showing current step per job
CSS:
- .nav-desktop / .nav-mobile-controls classes for responsive nav
- Default cards: slightly refined shadow and border
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Extraction is now fully async via Celery — UI shows a live progress panel,
job continues even if page is closed. Large documents are processed in
50-page chunks to extract all questions (not just first ~50 pages).
Backend:
- app/tasks/quiz_tasks.py: new Celery task 'extract_quiz'
- Writes step-by-step progress to Redis (extraction:steps:{job_id})
- Splits large page ranges into 50-page chunks, processes each separately
- Reports per-chunk results and running total
- Falls back to synchronous if Celery/Redis unavailable
- POST /quizzes/ now returns {job_id, status:"pending"} immediately
- GET /quizzes/job/{job_id} polls progress: steps[], status, quiz_id on completion
- Celery task list updated to include quiz_tasks
Frontend (DocumentDetailPage):
- ExtractionProgress modal component: monospace step log, auto-scrolls, spinner
- Polls job status every 2 seconds via /quizzes/job/{job_id}
- "Open Quiz →" button appears when done
- "✕ closes — job continues in background" shown while running
- beforeunload warning when job is active (preventing accidental close)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Themes / CSS:
- Warm Brown theme: parchment cream background, dark brown navbar, Georgia serif font
beautiful and easy on eyes for reading medical content
- Default theme: slightly warmer bg, softer card shadows, 1200px container (less boxy)
- Navbar: sticky, height-fixed, overflow-x scroll on mobile (no wrapping), smaller text on mobile
- Mobile container: 14px padding for more space
Bug fixes:
- Login page: axios 401 interceptor no longer triggers window.location.href for auth endpoints
(was causing immediate page reload before error message was visible)
- Dashboard quizzes stat now shows distinct quizzes attempted (not created by user)
- PostgreSQL email collation: ALTER users.email to COLLATE "C" in migration
(prevents B-tree index corruption with en_US.utf8 locale causing user lookups to fail)
- get_current_user: verifies email on ALL protected endpoints, returns 403 with X-Unverified header
Frontend auto-logs out and redirects to /login where resend option is shown
DocumentDetailPage:
- Delete Section button with confirmation on each section row
- Create question category inline from dropdown (+ New button → prompt)
- Question Bank Category dropdown now shows question count
Dashboard:
- total_quizzes stat counts distinct attempted quizzes (not user-created quizzes)
Backend:
- DELETE /documents/{id}/sections/{section_id} endpoint added
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Question sharing (edits now propagate):
- quiz_question_links junction table: quizzes reference questions, not own copies
- Existing questions migrated to junction (idempotent ON CONFLICT DO NOTHING)
- from-bank and from-category create quiz by adding junction links (no copying)
- Editing a question via QuizEditPage or bank Edit modal updates the ONE record
visible in all quizzes that reference it
- Delete from quiz: only deletes Question record if no other quiz references it
- quiz_service: fixed position counter (was using len() incorrectly, now enumerate)
- question.quiz_id renamed to source_quiz_id in Python (DB column still quiz_id)
- All attempts/quizzes/search code updated to use junction helper or source_quiz_id
Bug fixes:
- PostgreSQL en_US.utf8 collation B-tree index corruption on users.email
Fixed by ALTERing column to COLLATE "C" in migration + immediate repair
- get_current_user now blocks unverified users on ALL endpoints (403 + X-Unverified header)
api/client.js intercepts X-Unverified=true and auto-logs out to /login
- Search (quiz + bank): correct_answer and explanation now included in matching_questions
so study modals work correctly
- Keyword search: order_by updated to use source_quiz_id
- Bulk category: changed from PATCH with body array to POST with Pydantic body
(fixes null category_id removal)
- Question bank: edit button added, answer highlighting removed from list view
- QuestionBankPage: Keyword/Semantic/Hybrid search toggle
- Login: login error message stays visible (removed immediate reload)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Architecture:
- Questions are primary objects in a bank, tagged with question categories
- QuestionCategory is a separate taxonomy from QuizCategory (different concepts)
- Extraction → questions added to bank, optionally tagged to a question category
- Quizzes can be created from: individual question selection, question category, or PDF extraction
Backend:
- QuestionCategory model + question_categories table
- question_category_id column on questions table (nullable, SET NULL on delete)
- GET/POST/PATCH/DELETE /api/question-categories/
- POST /api/question-categories/{id}/create-quiz — create quiz from all questions in a category
- PATCH /api/questions/{id}/category — assign single question to category
- PATCH /api/questions/bulk-category — assign multiple questions at once
- GET /api/questions/bank?category_id=&uncategorized= — filter by category
- QuizCreate schema now accepts question_category_id for extraction
- quiz_service.create_quiz_from_section accepts question_category_id param
Frontend:
- DocumentDetailPage: Add to Bank Category dropdown in Quiz Settings (optional)
Labels extracted questions with the selected category on creation
- QuestionBankPage: full rewrite
- Category chips for filtering (All / Uncategorized / named categories)
- Create category button inline
- Checkbox multi-select with bulk category assignment
- Create Quiz modal: choose from selected questions OR all from a category
- Each question shows its category badge and quiz source
- Study modal with instant answer feedback
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Security:
- Nginx: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, CSP, Referrer-Policy, Permissions-Policy headers
- Redis-backed login rate limiting (survives container restarts)
- Admin litellm/models endpoint: api_key moved from GET query param to POST body
- Nextcloud credentials moved from localStorage to sessionStorage (cleared on tab close)
UX / Layout:
- Login: unverified users see inline "Resend verification email" option
- QuizPage mobile: TTS Listen button on its own row below question text
- QuizPage mobile: Voice selector on its own row in header card (not squashed with timer)
- QuizEditPage: scroll position preserved after saving a question edit
Quiz Categories:
- New QuizCategory model + quiz_categories table
- category_id column added to quizzes table
- GET/POST/DELETE /api/categories endpoints
- Quizzes grouped by category in QuizzesPage; moderators can assign via 🏷 menu
- Uncategorized section shown when categories exist
Question Bank:
- GET /api/questions/bank — search all questions across quizzes
- POST /api/questions/from-bank — create new quiz from selected questions (copies, originals untouched)
- QuestionBankPage: search, checkbox select, study modal, create quiz form
- "Question Bank" link added to Navbar
Search:
- "View all N questions →" button expands to full question list
- Each question has a Study button opening in-place modal with study mode
- Summary view shows 2 questions per quiz with Study button
Extraction prompt:
- Stronger emphasis on correct_answer field with step-by-step letter → full text example
- Explicit instruction never to store just the letter
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- attempts.py: dashboard quiz_stats now queries by attempted quizzes not
created quizzes — regular users now see per-quiz breakdown correctly
- documents.py: moderators/admins can fetch documents by ID (were getting
404 despite seeing them in the list); also restrict section creation and
delete to moderators via require_moderator
- nextcloud.py: sanitise filename in Content-Disposition header (strip
quotes, backslashes, newlines to prevent header injection)
- UploadPage.jsx: importing from Nextcloud no longer switches tab to local,
preserving the selected-file info display
- ResultsPage.jsx: fill-blank correct answers now show correct-bg (green)
instead of always showing wrong-bg (red)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add key prop to TTSButton based on question id + answer state
so component remounts (and audio stops) on both question navigation
and answer selection in study mode
- Set AWS Polly Joanna (neural) as default TTS voice
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- FastAPI backend with JWT auth, roles (admin/moderator/user)
- PDF upload (up to 500MB) with streaming, PyMuPDF text extraction
- ChromaDB vectorization per page with metadata
- LiteLLM AI question extraction from PDF (not generation)
- Image extraction from PDF pages, graceful fallback
- Quiz modes: timed (countdown timer) + learning (answers shown inline)
- Page-by-page question navigation with dot navigator
- TTS endpoint using LiteLLM (Google Vertex / OpenAI voices)
- Admin dashboard: AI model management per task, user role management
- Moderator role: upload PDFs, create sections, generate quizzes
- Spaced repetition reminders via SMTP email (SM-2 intervals)
- APScheduler daily reminder jobs
- Celery + Redis for background PDF processing
- React frontend with all pages
- Docker Compose deployment (nginx + backend + celery + redis)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>