Commit graph

4 commits

Author SHA1 Message Date
Daniel
9d407ca1d9 feat: settings as places with addresses; repair nested cross-references
Settings was one 600px column holding the account form, a theme picker,
a Nextcloud integration, a document list and an admin grid, in that
order, with no way to link to any of it. It is now a section list beside
one panel, with the section in the URL — so "change your password" is a
link and Back works. On a phone the list becomes a scrolling strip
rather than a second level of navigation.

- The exam objective moves in. It scopes the bank, the filters and now
  the knowledge profile, which makes it a setting; it was only reachable
  from a dropdown in the header.
- The notifications panel is gone. Its one control switched quiz
  reminders, and the reminder scheduler was removed earlier today — it
  was a toggle wired to nothing.
- Form fields are 16px on touch so iOS does not zoom the page in on
  focus and refuse to zoom back out; nav rows are 44px targets.

Also fixed, found in an agent's report rather than by looking:

  37 cross-references across 25 articles are nested and broken —
  `[[363|[[245|gastroesophageal reflux]] disease]]`, which renders as
  literal brackets and resolves to nothing. The first linker pass linked
  the longest title, then let a shorter one cut into the result. The
  current pass cannot do this (a finished marker is stashed), but the
  damage was already in the database and strip_owned could not see it:
  its label group stops at the first "]". link_articles now unwraps the
  inner marker, keeping the outer — the longer, more specific title.

And the ArticleSplitView flake: the preview card appears on a 350ms
timer and the query allowed 2s, which the full parallel run exceeded
often enough to fail a different case each time. Tried fake timers
first; they fight waitFor. A longer allowance is the honest fix — the
test is about the split view, not about how fast the box is. Four
consecutive clean runs.

Frontend 274/274.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-11 12:46:13 +02:00
Daniel
595b619515 feat: cross-links follow a rule instead of linking every mention
3,560 links became 2,191, and the distribution is the point: Seizures
was linked 119 times and is now 48, across the 50 articles that mention
it — about one per view, which is what a first mention means.

  1. First mention per view, not per section. Short, Long and Clinical
     are read separately so each earns one; nine sections did not earn
     nine. Metabolic Acidosis: 39 links to 19.
  2. Lists are jump lists, prose is not. A differential or causes list
     keeps a link on every distinct condition — that is the one place a
     reader wants ten in a row.
  3. The 23 titles mentioned in more than 5% of articles link from lists
     only. "Seizures may occur" is not a topic anyone breaks off reading
     to visit; it is the vocabulary of paediatrics.
  4. A finished marker is now stashed, so a shorter title cannot re-cut
     one already made.
  6. A link whose label is not the target's title was written by hand
     and is never touched, which is what makes --apply re-runnable: it
     strips only its own work and reapplies the rule.

Correcting myself: I first measured hub terms on text that was already
linked, where the word pattern cannot see a mention wrapped in a marker.
That undercounted by four times and made the corpus look far less
repetitive than it is. The threshold is measured on clean prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-11 05:10:24 +02:00
Daniel
8f73e0f75b fix: one analysis layout, and an unsat session is the same page at zero
Two bugs, one visible cause. `.an-page`, `.an-rail` and four more classes
were defined in both AnalysisPage.css and AnalysisSessionPage.css with
different values — one a 280px grid, the other 260px. Once the two pages
shared a rail both stylesheets loaded together, the later won, and the
content column collapsed to rail width: "General Pediatrics" wrapped one
letter per line and the table headers floated away from their rows.

AnalysisShell now owns the frame and the session list for both views.
The page stylesheets style their content and nothing else.

And a session nobody has sat is no longer a bespoke "nothing here" panel.
GET /attempts/quiz/{id}/analysis answers with the same shape at zero —
0%, 0/20, every row "skipped" — so it is visibly the same page the
learner will see filled in, with a line saying why the figures are zero
and Start below. A part-finished session says how many are outstanding
and offers Resume. Once an attempt exists the quiz address returns the
real analysis, so both ways in reach the same page.

Mobile: below 1000px the rail becomes a band above the content that
starts closed — on a phone the first thing on screen should be the
analysis asked for. Search field is 16px on touch so iOS does not zoom
the page in and refuse to zoom back out; rail rows are 44px targets.

Backend 226/226, frontend 265/265.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-11 05:08:56 +02:00
Daniel
1f77d421c7 feat: cross-link the articles, and strip what the analysis page replaced
The linking was the gap
The marker system was built weeks ago — resolves by id, survives a rename, shows
a preview on hover — and not one of 333 articles used it. Every article was
written in isolation, so a piece on croup named stridor and epiglottitis and
offered no way to reach either. `scripts/link_articles.py` reads what is written
and links it: 3,718 cross-references across 307 articles, by id, so a later
rename cannot break them.

Conservative on purpose, because a wrong link is worse than a missing one: only
the first mention in a section, whole words, longest title first so "Otitis media
with effusion" beats "Otitis media", never inside an existing link, marker,
heading, code span or table, and never an article to itself.

That exposed a second thing: the reading view had its own Markdown pipeline with
its own cross-reference regex, and it only understood the old slug form. It would
have printed every one of those 3,718 links as literal brackets. Article prose
now goes through the same renderer as the rest of the site.

Short and Clinical looked empty
Both are usually a single section, and everything starts collapsed, so the tab
showed one heading over blank space. A view of one section is not a contents
page; it opens.

Removed
Quiz reminders — emailed nudges to retake anything under 75%, with a scheduler
that existed solely to send them: the model, the service, the scheduler, the
email, the table. Article comments. The dashboard's in-progress list and its
stat cards, both of which the analysis page now answers better.

One mistake worth recording: the first pass at removing the reminder cleanup used
a regex that took 109 lines with it, including an unrelated endpoint. The test
suite caught it (`/attempts/quiz/{id}/in-progress` returning 404 instead of 403),
and the file was restored and edited by exact match instead.

208 backend, 249 frontend green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-11 03:44:44 +02:00