Modify held the request open for a library search, a PubMed search, a web
search and a restating model call. That is minutes, and a browser gives up
first — Firefox abandons a non-streaming fetch at five minutes, the same
failure generating was moved off the request to fix in ef574edd. The server
carried on and saved the result while the person watched an error, and closing
the tab killed the work outright.
POST /my-resources/:id/refine now records the request and answers 202 with the
job, exactly as /generate does. The writing moved into refineResource(), which
the job runner dispatches to by kind; the job list, the five-second polling,
the restart recovery and the three-in-flight cap are all the work they already
did, unchanged. Ownership is checked again inside refineResource because the
resource can be deleted while the job waits.
The page follows the job instead of the response. Reporting is unchanged — the
unchanged reply, what was seen and what was searched — it is only said from the
job list now, so it still reaches the person who asked for it after a reload.
Twenty was the review ceiling while the form allowed thirty, so a 21-30 slide
deck was written, stored and then silently never reviewed — and modifying one
above twenty was shown only its first twenty pages while the prompt claimed
one image per slide. The three numbers are the schema's own limit now, so a
deck that can be asked for can also be stored, reviewed and edited: the form's
max, both generation clamps and MAX_SLIDES are all 60.
Measured at that size before this change: 45 pages cost 28,796 prompt tokens
and 2.2s to review, a 46-page deck with a fault on page 24 was found and
re-laid correctly, and a 44-slide deck used ~5,900 output tokens of the
16,000 ceiling.
Writing a resource held the request open for as long as it took: a library
search, one or two long model calls, a review, then figures. Measured end to
end that was six minutes on 2026-09-16 (00:40 to 00:46:16, resource 53), and
Firefox abandons a request that has said nothing for five — the browser
reported "NetworkError when attempting to fetch resource" while the server
carried on and saved the deck anyway, so a generation that worked looked
like a failure and left no status line.
The request now records what was asked and answers 202; the work runs on the
server as a job; the page lists what is being written, what landed and what
failed, polls while anything is in flight, and reloads the library when one
lands. Several can run at once, a reload loses nothing, and a boot pass marks
jobs stranded by a restart as failed rather than spinning for ever.
The same generation also ran with DeepSeek's thinking on, which is what made
it take minutes rather than seconds: the 16,000-token write spent the whole
budget reasoning and returned an empty reply (completion_tokens=16000,
reasoning_chars=51573), which fired the automatic retry at four times the
budget, and the 2,000-token reviews of that deck starved the same way four
times over. Thinking is now off for the writing, the review of it and a
revision — DeepSeek's own field, sent by the model wrapper. Other clinical
routes are deliberately untouched and keep the provider default.
The review inherits the writer's rule rather than hard-coding it, so a task
that wants reasoning can still ask. Migration 1781500000000_resource-jobs.js
adds user_resource_jobs; the container entrypoint applies it before the app
starts.
The download-transcript button is removed for anyone who is not an admin,
but the event binder still called addEventListener on it unguarded. The
throw stopped the binder before the saved-chat list loaded, before the
click handler for opening old chats was registered, and before the send
box was fully wired, so a non-admin saw no chats until New chat, could not
open old ones, and could not chat. The button is now bound only if present.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
POST /api/auth/login answers 410 for everyone, administrators included. The
sign-in screen never draws an email or a password: it is the provider's
button, or a sentence saying sign-in is not configured. The admin CLI no
longer resets passwords. The e2e harness mints its sessions inside the
container instead of signing in with a password.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
With OIDC on, every password route is shut except /api/auth/login for
administrators — the way back in if the provider is down, reached from an
"Administrator sign-in" link. The disable-local-auth setting, the
registration CLI command and the docs that described them are removed;
accounts, roles and invitations live in authentik.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
Every account comes through One Sign In; an administrator sends an
invitation link from there. The register route answers 410, the
registration-status route and the admin toggle are removed, and the
setting no longer exists in defaults, lockdown lists or seeds.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
The user, seeing four full questions stacked on the empty page: "the former
way… was better". Labels, three of them, as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
The re-render swapped "Pediatric Clinical Assistant" for a different heading
and line the moment the starter questions loaded. Same words as the page now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
The page class that hides the app sidebar and gives the chat its full height
was added only after the initialiser returned, so any error there left the
app menu and the chat rail side by side with the panels at half height. The
class goes on first now. The starter chips show the question itself rather
than a two-word label, four of them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
The picker labels carried each format's typical length ('15–25 cases, three
or four slides each, 60–90 minutes'), which sat under a slide-count field and
read as a second, contradicting number. The label is the name, the hint says
who the format is for, and the brief tells the model that the slide count
asked for is the length and the parts fit into it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
A refresh landed on a new empty chat with the conversation a click away in
the list. The open chat's id is remembered per account and reopened on
load; New chat forgets it; a chat deleted elsewhere is quietly gone.
DeepSeek models think by default: measured on ds-deepseek-v4.1-flash, a
three-sentence clinical answer spent 301 reasoning tokens and 2.9 s before
writing and gave the same answer in 0.9 s with thinking off. The assistant's
reasoning effort now comes from CLINICAL_ASSISTANT_REASONING_EFFORT ('low'
as before; 'none' switches thinking off where a model allows it, sent as
DeepSeek's own thinking field through LiteLLM).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
The morning's change forced every table to the screen width below 640px and
let cells break mid-word, so a four-column table read as columns of broken
syllables. A swipe is the better trade; the hint under the table says so.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
A theme is a look and the slide vocabulary is what one slide can be; neither
said what a journal club is (PICO, methods, appraisal, bottom line) or that a
board review runs in case-question-answer cycles, so every deck came out as
the same lecture. assets/deck-formats.json holds ten formats distilled from
how departments run each session — grand rounds, journal club, morning
report, M&M, board review, noon conference, simulation debrief, parent
education, QI project, research abstract — each a sequence of parts naming
the layouts it is usually drawn with and whether it may repeat. The brief is
prose after the layout vocabulary; a part is a unit the model reuses as many
times as the material needs, and every slide stays one of the twelve types.
Chosen beside the theme, 'Free' by default, stored on the deck and carried
through modifications.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
Deleting a generated image asked with the browser's own confirm(), a bare
dialog that looks nothing like the rest of the app. It now goes through
showConfirm like every other confirmation, with the danger styling and a
Delete button.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015j4L7Eu5Jp8gVMPsXdPaZ2
The sideways swipe was not reachable on the phone, so wide tables were cut
off. Below 640px the cells break their words and the table takes the full
width; wide tables get taller, never clipped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
The fragment's markup was inserted before its own stylesheet resolved, so a
first visit to the assistant tab flashed unstyled content. The pending
marker app.js already sets now also hides the markup; the Loading status
line is all that shows until the tab reports ready.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
The reader can start at the top of a long answer and scroll at their own
pace; the per-token and on-completion jumps to the bottom are gone, which
also stops a finger on the screen fighting the auto-scroll.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
The clinical-assist indexer no longer polls Nextcloud every five minutes; it
scans once at start and then on POST /api/v1/vector-sync/scan with a bearer
token. The admin panel shows what it reports and carries the button; the
address and token are settings (or the environment). The button stays usable
under lockdown, the fields do not.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
The author presses "Copy a share link" and sends it however they like.
Whoever follows it (app.pedshub.com/#share=<token>) is signed in first if
need be — the token survives the trip through the SSO — then shown what it
is and who from, and adds it with one press. Only the token's hash is
stored; a link lasts 30 days and can be withdrawn; accepting twice is
harmless; the owner following their own link changes nothing. Sharing by
email is gone: nobody is looked up by address. "Everyone signed in" stays
as a switch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
A resource was private with no way out but the author's own Nextcloud.
Share opens reading — open, preview, download — to one person at a time by
exact email (no account is ever listed) or to everyone signed in with one
switch; what others share appears in your library marked "Shared by …".
Writing never travels: modify, re-skin, delete and the share list stay the
author's, every write still filtered on user_id, and the read routes go
through one reader rule. Rows follow the resource and the person.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
A PowerPoint or Word file cannot be shown in a browser and a phone has
nowhere to open one. Preview renders the resource the way its download
would be built, turns it into one PNG per page (Gotenberg to PDF, pdftoppm
to pages), and shows the pages in an overlay that scrolls and pinch-zooms
like anything else. Rendered once per version — updated_at and theme are in
the key — and served from disk afterwards. The theme picker's sample deck
has the same Preview beside its download.
Every presentation now shows the theme picker in the library, since
markdown slides take a theme too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
[1] early in an answer and then "[2][1]" later showed "2 1". A run of
adjacent chips is now sorted by what it displays — a core rule after inline
parsing — so it reads "1 2". Text between two clusters keeps them apart,
every chip still points at its own source, and the numbering itself is
unchanged: only the order within a run moves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
nextcloud.url (or NEXTCLOUD_URL) names the site's Nextcloud. With it set,
the address field is hidden and the page reads "Sign in with Nextcloud",
with the app-password route underneath for whoever needs it; both routes
use the default when no address is given. Without it, nothing changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Topic is one line and Instructions say how; neither is the place for a list
of twelve topics or a case to build around. Details is: optional,
collapsed by default, up to 12,000 characters, quoted into the prompt as the
author's own account of what the resource must cover — material, not
commands, in both the deck and the markdown shapes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Signing in is a code from the PedsHub sign-in page, not a password; the
citation chips are numbered by first appearance; voices come from every
model the administrator added; the Learning Hub section — a feature that no
longer exists — is replaced by My Resources: decks, themes with sample
downloads, Modify, and the download formats. The two-factor and
change-password entries go with the passwords they described.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
The app has no pages and is not getting any. Another site can still send
someone straight to a tab by naming it in the hash: it is read once at
load, stored where sign-in looks for the last tab, and dropped from the
URL, so it survives the round trip through the SSO and does not stick to
the address bar. #resources, #deck and #decks all mean My Resources.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Two apps, two logos after all: the stethoscope stays here, the hospital
stays on the quiz. What the last change got right stays too — the login
card without a heading over a single button, the icon as an image in the
card and the menu — and the name now matches the SSO tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
The scribe wore a stethoscope and called itself PedAI while the quiz app
was PedsHub; one product, two identities. Both now use the PedsHub icon.
The login card shows the icon, the name and the tagline and loses the
"Sign In" heading that sat above a single button; the menu bar shows the
icon at 20px beside the name, since the full wordmark did not fit there.
Favicons and the PWA icons are rendered from the same SVG.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Sign-in is email → code at sso.pedshub.com, and new accounts come from an
invitation link minted there, so the app's own code emails and invite codes
recorded a path nobody can take. Gone: the login-code routes and their rate
limiters, the invite admin API and card, the invite field on the register
form, the "email me a code / use my password" choice on the sign-in screen
(an email now leads straight to the password), both utility modules, and
the invite-only setting. A migration drops login_codes and
registration_invites.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
One line of changing text is now a list the reader can follow: Analyzing
the question → Searching the clinical library → Found 7 sources → Writing
the answer, plus any step the server reports (looking at an image, drawing
one, completing a cut-off reply). Steps are added when the stream says work
began and ticked when it says it ended, so the list is a record of the real
work, not an animation on a timer; nothing delays the answer.
The one hand-off the server cannot signal — retrieval runs before the stream
opens — is done by the stylesheet, not a JS timer, so the autosave debounce
keeps its clock to itself.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
The export styles pinned the last column of every table to one unbreakable
line (width:1%; white-space:nowrap) — right for a numbers column, wrong for a
two-column clinical table whose second column is the whole finding: the
table grew to the width of its longest sentence and iOS then inflated the
text in those cells, so the "Finding" column read in a bigger font than the
"Feature" column. The pin goes, and the sheet opts out of text inflation.
With citations switched off the answer is still grounded in the retrieved
sources; only the markers are removed. The stripper now also removes the
escaped \[1, 2\] form and [src] placeholders, which used to survive as text.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU