Commit graph

4 commits

Author SHA1 Message Date
Daniel
febb14490c feat: ground AI drafts in the library and PubMed, and mend the card system
Some checks failed
Tests / backend (push) Failing after 15s
Tests / frontend (push) Successful in 28s
Tests / e2e (push) Failing after 26s
**Two sources an AI draft can draw on**, both off until an administrator turns
them on, both appended to the prompt as extra material rather than woven into
it — so a draft with nothing to draw on is byte-for-byte the draft that has
been working well.

- *The clinical library.* The indexed shelf the clinical assistant already
  searches, over MCP on the internal network. Ported from ped-ai: sessions are
  reused, a dead one is reopened once, and a library that cannot be reached
  never fails the article — it just means the educator is writing without it,
  and the progress line says so.
- *PubMed.* NCBI's E-utilities, no key required. Ported whole, including the
  two lessons that cost somebody an afternoon over there: PubMed ANDs every
  term, so "bronchiolitis management in infants" can find nothing where
  "bronchiolitis management" finds six — hence the query ladder — and three
  esearch calls in a row will trip the rate limit, hence the spacing. The
  reference list is written from the records rather than by the model, so every
  line is a paper that exists with a PMID somebody can look up.

Measured on the live stack: 24 excerpts, 6 papers, 6 references, 6 in-text
citations, in one draft.

**The card system, which turned out to be half-built:**

- There was no way to make a deck by hand, and no way to edit a card at all —
  you could browse, view and delete. Both are there now, the editor taking
  front, back and a picture.
- Filing, writing, sharing and deleting are all educator work now, behind one
  named gate rather than four scattered checks. A learner studies.
- A deck generated from an article inherits that article's category instead of
  landing in Uncategorized for somebody to file by hand.
- A link inside a card previewed instead of going. A card is a box a few lines
  tall, often inside a flipping panel, and a hover card anchored in one is
  clipped by it — so the link read as broken because clicking it did nothing.
  Where there is no room to preview, the honest behaviour is to take you there.

**An AI draft belonged to no editorial queue.** Nothing set `generated_by`, so
a drafted article was neither "generated, unread" nor anything else: the tile
counted it and there was nowhere to click. Drafts are stamped with the model
that wrote them, and there is now a plain Drafts queue that cannot be fallen
through.

**The sign-in code email** is laid out rather than written: the code is the
biggest thing on the screen, then which account it signs into, then a way back
to the page, then permission to ignore the whole thing.

Also: a back link out of a deck, in the same words as the rest of the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-13 02:44:42 +02:00
Daniel
3279e14bb2 refactor: remove the LMS
There will be no courses. What was there: one draft called "jk" with two empty
lessons, and 4,000 lines of code around it — courses, modules, lessons,
enrolments, per-lesson progress, SCORM, BigBlueButton, completion certificates,
three React pages, a router, two models.

Its real cost was everywhere else. Every query that measured practice had to
remember `Quiz.course_id.is_(None)`, and forgetting it in one place would have
silently mixed course attempts into a learner's analytics; the bank predicate
carried a subquery to exclude a course's own questions from every search,
recommendation and share; quiz access had a second, parallel rule about
enrolment. All of that is gone, so the remaining rules say what they mean.

`quizzes.allow_review` goes with it. It was only ever enforced for a course
quiz, so it had become a promise nothing keeps — the public session page was
still offering "no answer review" about sessions that review fine.

The fixtures' question 5 lived in a course quiz and stood for "a question that
exists but is not in your bank". There is no such thing now — a question is in
the bank unless it is deleted — so the counts it kept out of the numbers are
back in, and the tests that turned on it now turn on deletion or on the
attempt that actually holds a question.

Files the LMS uploaded stay on disk and stay protected: LEGACY_LMS_PREFIXES in
app/utils/upload_access.py is what keeps them unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 23:27:51 +02:00
Daniel
5d59e00144 refactor: remove per-question sharing
`Question.is_shared` defaulted to 1 and was only ever set by a route nothing
called, so in practice it divided the bank into "everything" and "everything,
plus your own private ones" — a distinction that cost every recommendation
denominator a join and never changed an answer. Who may reach the bank is the
site's own access rules; who may manage a question is the category grant tree.

So the two predicates the whole bank was built on are now the same thing, and
say what they actually mean: a question is out of reach if it has been deleted
or belongs to a course. Nothing else. The column is dropped, the route that set
it is gone, the bulk "share" action with it, and the Private tile and pill go
from the question manager.

The tests that turned on it have been rewritten rather than deleted, because
the rule they were really about survives: revoking a question still revokes
every session carrying it — by deleting it, which is the only revocation left.
Several others named a category holding exactly two reachable questions and
then answered two particular ids; that category holds four now, so they name
the pair instead. A session's own sharing flag is untouched — that is a
different thing, and it is still how a session is handed to somebody.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 08:42:51 +02:00
Daniel
9b51a17507 feat: flashcard decks share the question and article category tree
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
2026-09-10 01:40:03 +02:00