QuestionBankPage.css was not imported, so none of the filter-bar styling applied
— which is why "⚙ Filters" and the question count ran together with no card
behind them. Importing it fixes the whole bar, not just the spacing.
Quiz/test categories removed. PREP lives in study plans now, so the Categories
tab, the category selector in the session menu, and the handlers behind them
were managing a concept that no longer exists.
Sessions list shows the first six with "Show all" and a link to the full history
on the analysis page, instead of printing every session inline.
The analysis session rail now runs the full height of the viewport against the
left edge rather than sitting in a boxed card, so a long list is one continuous
column. Below 1100px it returns to a card above the content.
Tests: 136 frontend green, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01365DYKu14YtsBKv2ycW6eG
An admin can now say "you edit Step 1 Cardiology" rather than only "you edit this
category". Each dimension on a grant is nullable and means "any"; a grant covers
the questions matching all the dimensions it sets, and holding several grants is
the union of their coverage (migration c1d2e3f4a5b6). A check constraint refuses
a grant that names nothing, which would otherwise mean "everything".
Permission checks now run against a predicate over Question rather than a set of
category ids, so the exam and discipline dimensions actually take effect on edit,
delete and bulk actions instead of being silently ignored.
Pediatrics is unbound back to a global tag. With counts already scoped by the
learner's active exam, one global row gives the right number per exam, so
scoping the row bought nothing and duplicating a 6,740-tag vocabulary per exam
would have to be repeated for every rename and merge.
Tests: 123 backend green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01365DYKu14YtsBKv2ycW6eG
A discipline may now belong to one exam. `question_tags.exam_id` NULL keeps a tag
shared — Cardiology means the same thing whichever exam you sit — while a set
exam_id scopes it. Boards Pediatrics and a future Step 1 Pediatrics are therefore
separate rows over genuinely different bodies of content, not one label stretched
across both. Uniqueness moves from (name, type) to (name, type, exam) to allow it
(migration b0c1d2e3f4a5).
`scripts/bind_exam_tags.py` binds Pediatrics to Pediatrics Boards and tags the
884 questions in that exam that were missing it — the whole bank is paediatrics,
so it now reads 2,948.
Facet counts are computed within the learner's active exam, and a tag scoped to a
different exam is left out: an unscoped list offered disciplines that could not
match anything they were studying. With no exam chosen, everything is offered as
before.
Tests: 4 new backend (same name once per exam, unscoped list offers all, choosing
an exam scopes counts and hides other exams' tags, switching exam switches which
Pediatrics is offered). Full suite green: 123 backend.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Editing a question now snapshots its previous state. The last 5 are kept — the
value is undoing a recent mistake, not an audit trail, and an uncapped history of
full question bodies grows without bound (migration a9b0c1d2e3f4).
A restore snapshots the current state first, so the restore is itself undoable.
History is gated by the same per-category grant that gates editing, so it cannot
be read by someone who could not have made the edit. The question editor shows
the versions with their dates and a Restore action.
Also added docs/TODO.md tracking everything requested and not yet delivered:
AI Mode and its citation contract, global search, study-plan editing and
articles-in-blocks, admin settings revamp, image libraries and question folders,
media management, nested article sections with references and per-section notes
and feedback, per-question notes and feedback in the runner, tutorial mode, the
per-question performance table, the Overview dashboard, systems subsystems, and
dropping "Pediatrics" as a discipline.
Tests: 6 new backend (snapshot on edit, cap at five newest-first, restore,
restore is undoable, refused without edit rights, unknown version). Full suites
green: 119 backend, 136 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
The PREP sets were loose admin-generated quizzes. They are now study plans: one
per year, split into blocks of 50 numbered "Block 1", "Block 2", plus a
"PREP Mixed" plan of 300 drawn at random across every year. 12 plans, 2,821
questions, applied to production.
Block membership is snapshotted rather than stored as a filter — a plan you are
part-way through must not reshuffle between visits. Re-running the seeder updates
years whose questions changed and leaves the mixed draw alone unless --reshuffle.
Starting a block reuses the learner's existing quiz for it; without that,
reopening a block would create a duplicate test each time and scatter the
attempts across them. Only questions the learner may see are included.
Tests: 113 backend green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Images were findable only by the filename someone typed. `media_assets` gives
them a title, caption, alt text, a category on the shared tree and tags, with a
weighted tsvector so they are searchable now (migration y7e8f9a0b1c2).
The embedding column is filled from the caption today. A vision-capable model can
fill it from the image itself later without another migration — and because
`embedding_model` stamps every vector, a text-embedded caption and a
vision-embedded image stay distinguishable instead of being silently mixed in one
index. Adding "media" to the embeddable kinds is all the retry task, the full
regeneration and the health report needed.
`media_tag_links.tag_id` carries no ORM-level foreign key: `question_tags` is
created by raw DDL rather than a model, so the constraint lives in the migration
where the table actually exists.
Tests: 113 backend green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
An article embedded as a single vector, which finds the article but not the
paragraph — so a citation could only ever point at the top of a page. Sections
live in a JSON column and cannot carry a vector or a full-text index, so they are
now projected into `article_section_index`: one row per section with its own
embedding and weighted tsvector (migration x6d7e8f9a0b1).
- Rows are keyed by section id, so editing a section updates it, removing one
deletes it, and an unchanged section is not re-embedded on every save.
- `article_section` joins the embeddable kinds, so the retry task, the full
regeneration and the health report cover it without further changes.
- `hybrid_ids(db, query, "article_section")` searches it like any other corpus.
This is the groundwork for grouped search results (article, then the sections
that matched) and for AI citations that deep-link to the right section.
Tests: 113 backend green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Runner rail was leaking upcoming questions
It showed an excerpt for every question in the session, including ones the
learner had not opened — so the case was spoiled before they read it, the same
mistake the article page was making with answers. A question now shows its text
only once reached; the rest are bare numbers.
Analysis page
Added the "Latest sessions" rail beside the analysis, collapsible, each row
linking to resume or review.
SessionProgress component
A single flat fill cannot tell "answered 15, all right" from "answered 15, half
wrong", which is the thing worth seeing at a glance. Progress now renders as
correct / incorrect / remaining segments, shared by the analysis rail and the
dashboard panel.
Tests: the rail test now asserts an unopened question shows no excerpt and
reveals one when reached. Full suites green: 113 backend, 136 frontend.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Exams (migration v4b5c6d7e8f9)
"Pediatrics Boards" was a hardcoded checkbox that filtered nothing. Exams are now
rows: Pediatrics Boards and USMLE Step 2 CK ship seeded, and everything already
in the bank is linked to the boards. Membership is a link table, not a column,
because one paediatric cardiology question can count towards several exams.
The learner's choice lives on `users.active_exam_id`, so it follows them between
devices instead of sitting in one browser's storage. Choosing an exam scopes the
bank; a question with no exam links stays visible, since unlinked content is
unclassified rather than excluded. A switcher sits in the navbar.
AI mode — matching, never generating
Both entry points build a test from the educator-reviewed questions that already
exist, ranked against the request. Nothing is invented:
- POST /questions/builder/describe turns "what I want to study" into a test.
- POST /questions/builder/from-upload matches a document against the bank. The
file is read in memory and never stored — it is a search query, not a source
of questions, so there is nothing to retain or expire. 10 MB cap, 30 questions.
Handing a whole document to `websearch_to_tsquery` builds one enormous
conjunction that matches nothing, so text over 300 characters is reduced to its
most distinctive terms, OR-joined, before it reaches the lexical ranker.
Continue your study (migration w5c6d7e8f9a0)
A dashboard panel with the sessions in flight and the articles most recently
opened. `article_views` records one row per learner and article, written best
effort so a reading page never fails because a bookkeeping write did.
Tests: 5 new exam tests (active exams and counts, choice persisted and cleared,
unknown/inactive refused, bank scoping including unlinked questions, moderator-only
creation) and 7 for AI-mode matching (no questions created, invisible questions
excluded, no-match reported rather than an empty test, upload limits enforced).
Full suites green: 113 backend, 136 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Retrieval generalised beyond questions
`_text_for_question`, `embed_question` and `hybrid_question_ids` all hardcoded
the questions table, so there was nothing to call for an article or a card. That
layer is now corpus-agnostic:
- `Embeddable` mixin gives articles and flashcards the same embedding,
embedding_model and embedded_at columns questions have, plus a weighted
full-text vector (migration u3a4b5c6d7e8).
- `embed_record(row, kind)` is one code path for all three — they share an
embedding space, so they must share the model and provenance rules too.
- `hybrid_ids(db, query, kind)` ranks any corpus; `hybrid_question_ids` stays as
a thin alias for existing callers.
- Article and flashcard search moved off `ILIKE '%term%'`, which could not find
a jaundice article from "yellow newborn".
- The retry task and full regeneration now sweep every corpus, and the health
report breaks down current/stale/missing per kind.
- Articles embed on create and on edit, with failures left to the retry task.
Quoted phrases replace the keyword-only mode
`websearch_to_tsquery` already gives "absence seizure" exact-phrase semantics,
and the semantic ranker sits out a quoted query. That covers the one case a
keyword-only toggle was for — exact lookup — per query rather than as a sticky
setting whose every position returns a subset of the default.
Full-page question editor (/questions/new, /questions/:id)
Editing happened in a cramped modal. There is now a page with room for the stem,
per-option explanations, a searchable category picker with primary plus extras,
difficulty, and images. It shows the question's id with a copy button, and
Duplicate creates a variant without retyping the stem. `GET /questions/detail/{id}`
backs it, pathed under /detail/ so it cannot shadow the static routes.
Question bank filter bar restyled — the toggle and count read as one control
instead of two grey pills crowding the result count.
Tests: 101 backend green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Two `.custom-test-modes` rule sets survived the earlier rebuild. The later one —
from the radio-card design that the sticky bar replaced — won on source order, so
the control rendered as two oversized separate boxes instead of a segmented pair.
Removed it, along with `.custom-test-actions`, which styled an action row that no
longer exists.
Also tightened the bar: smaller segment padding, and Refresh is now a quiet ↻
affordance rather than a grey slab competing with Create Test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
Questions and articles both pointed at `question_categories`; decks had no
category at all, so the three content types could not be filtered together and a
topic's cards were unreachable from its category.
- `flashcard_decks.category_id` references the same tree (migration
t2f3a4b5c697), so one category now spans questions, articles and cards.
- `GET /flashcards/` takes `category_id` and includes descendants, so a parent
category picks up everything filed beneath it.
- `PATCH /flashcards/{id}` files or unfiles a deck, refusing a category id that
does not exist rather than storing a dangling reference.
- The cards page shows each deck's category as a selector.
Tests: 5 new backend (all three types resolve to the same id, descendant
filtering, file and unfile, unknown category refused, renaming leaves the
category alone) and 1 new frontend. Full suites green: 101 backend,
136 frontend, build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpfzbZ1QTLMeVYxM2kyq8m
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.
Clearly-marked sample articles, cards, question links and lab deep links; idempotent. Seeded in production so every linking feature is visible end to end.
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.
AI refine now sends the current body and sections to the model; invalid model section IDs are replaced with valid hex IDs. Job polling list raised to 200. 50 backend 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.