Commit graph

679 commits

Author SHA1 Message Date
Daniel
adcea2a0ca fix: image models can be added and offered; solid phone top bar on iOS
Image models
- The Clinical Assistant "Image models" list waited on an old
  #assistant-image-model dropdown that no longer exists, so discovery never
  reached it and only four hard-coded fallbacks appeared — with no way to
  add any of the gateway's 50 image models.
- Image Generation search rows now have + Add / Added. Added models are
  saved as clinical_assistant.image_model_roster (validated server-side:
  up to 100 ids) and appear in the Clinical Assistant list at once; ticking
  one there offers it to users. Anything already allowed or configured
  stays listed. Unsaved ticks survive an add.
- The roster notification is guarded, so it can never fail the settings load.

Phone top bar
- The page is drawn under the status bar (viewport-fit=cover) and its
  theme colour was the removed header's blue, so on an iPhone content showed
  scrolling at the top of the screen. The row is now a real fixed element
  that extends behind the status bar (env(safe-area-inset-top), 0 in a
  normal tab), the menu button, sources pill and drawers clear it, and the
  theme colour is white.

Verified in Chromium: + Add -> saved roster -> listed unticked; tick kept;
remove works. Top bar is the only thing in the top 48px on all 22 pages;
phone menu positions unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
2026-09-10 13:46:19 +02:00
Daniel
3af11e8b4a fix: source cards fit the phone; long names wrap, no nested scroll
.assistant-sources was a grid with an auto column, which grows to its
widest unbreakable content — so a long file name such as
"2021_Fleisher_&_Ludwig's_Textbook_..." widened every card past the screen.
The column is now minmax(0,1fr), cards shrink (min-width:0) and names break
onto new lines (overflow-wrap:anywhere, with word-break for iOS < 15.4).
On phones the list is one vertical scroll: no sideways pan and no excerpt
scroll box inside it.

Measured on iPhone 14 / SE, Android 360 and Pixel: before, cards reached
636px on every phone (list overflowed by 236-288px); after, the widest card
ends 12px inside the screen and nothing scrolls sideways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
2026-09-10 13:31:37 +02:00
Daniel
6e0609f285 refactor: drop the "Saved Chats" header and collapse arrow from the rail
The list is simply there now. Its collapse preference is removed too, so a
list someone had collapsed cannot come back hidden with no control to open
it. The header's CSS (desktop, phone and preview) goes with it.

Sizes, measured in Chromium before and after: switch, New chat and the
name card are unchanged (desktop card 848, phone 790). The list takes the
header's 16px, and the first date heading sits 10px below New chat on both
desktop (167 -> 151) and phone (173 -> 165; phone list top padding 2px ->
10px), matching the menu's 10px rhythm. The rail itself still never scrolls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
2026-09-10 13:27:29 +02:00
Daniel
f93994ad69 fix: collapsed strip, workspace-mode search, image dialog chat list
- Collapsed menu: its own plain white 52px column against the tiled ground,
  no border line, in both views. Flat, it had no edge.
- Collapsed + workspace launcher: assistant.css set the rail track to 0 and
  beat the 52px rule, so the collapsed menu (name card included) vanished.
- Search follows the view, not the URL: the workspace launcher keeps
  /assistant, so Workspace searched chats.
- Create image, "Base it on": a native <select> list is as wide as its
  longest option, so full titles pushed it past the dialog. Labels now keep
  the whole words that fit the select's width; the full title is on hover.

Verified in Chromium: account menu items on top in all 8 states; collapsed
strip 52px white with the name card at the same place in all 3 views;
search placeholder and results per mode; dropdown widest label 500px in a
506px box (279 in 280 on a phone).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
2026-09-10 13:17:50 +02:00
Daniel
8756c9a140 style: flat menu on the tiled ground in both views; tiled page cards
- The tiled ground moves from .main-content to .app-body, so the menu sits
  on it too and the grid keeps one origin whichever view is showing. A
  second tile on .main-content would have started 210px in and seamed.
- Desktop: the workspace sidebar is flat (transparent, no border), the same
  as the assistant rail, which drops the white box it briefly had. The phone
  sheets stay white because they slide over the page.
- Workspace page cards get the assistant chat panel's tiled-white fill.

Measured in Chromium: name card 848-892 at x=8, brand, switch and 210px
width identical in both views, expanded and collapsed; no page scroll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
2026-09-10 12:23:31 +02:00
Daniel
f3a2309536 fix: one menu shape in both views, measured; phone menu follows the view
Measured in real Chromium (1440x900 and 390x844) rather than inferred.

Desktop
- The assistant rail sat inside the content area's 14px padding, on the
  tiled ground. So its background differed from the workspace sidebar's,
  its name card sat 6px lower and further in, and the page was 28px taller
  than the window. It is now the same flush white sidebar with the same
  insets; the chat and sources panels carry the 14px. Measured: name card
  848-892 at x=8, brand, switch and 210px width identical in both views,
  expanded and collapsed, with no page scroll.

Phone
- The menu button opens the chat history in the assistant (its drawer) and
  the workspace list elsewhere. In workspace mode the drawer lists the
  workspace links. Choosing a page, switching mode or pressing « closes it.
- The drawer no longer scrolls as a whole: only the chat list does, so the
  name card is pinned at the foot, the same place as in the workspace menu
  (790-836 at x=12 in both).
- Both menus are one plain shape: same width, text rows, grey current row,
  no "Menu" header and one close control.
- The assistant page no longer overflows the phone by 16px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
2026-09-10 12:07:26 +02:00
Daniel
b8d2153cca fix: phone menu links, Workspace pill and sources column; image model discovery
Phone
- The desktop collapse rules (52px icon rail) were unscoped, and menu-hidden
  is remembered across viewports, so after collapsing on a desktop the phone
  sheet opened with its links, the Assistant/Workspace switch and the account
  name all display:none. Scoped them to min-width:769px.
- Workspace pill: the sheet now closes on tap, and the launcher is no longer
  display:none below 640px, so the pill opens the workspace instead of
  toggling a view that could never appear.
- The menu button has its own 48px row instead of floating over the page;
  layouts subtract --mobile-bar (0px on desktop, so desktop is unchanged).
- Sources open as their own scrollable band under that row, and the toggle
  sits in the row, instead of a sheet/FAB covering the composer.
- The sheet uses dvh, so the account card is not pushed below the visible
  screen by the browser toolbar.

Admin
- Image Generation card beside TTS and STT: search the gateway's image
  models and test one. No Set button: an image model is chosen per workflow.
  It searches only on demand, so opening Admin still makes one discovery call.
- Account card sits at the same height in every rail state; the model
  availability list points to where new models are added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
2026-09-10 11:39:32 +02:00
Daniel
924ce8716c fix: the account menu was clipped by the collapsed rail, and sat at two heights
.sidebar sets overflow:hidden and the collapsed rail is 52px wide, so the account
menu — laid out inside it — was cut to a sliver. Settings, FAQ and Log out became
unreachable behind the avatar exactly when the rail is narrow, which is the state
the screenshot shows. The collapsed menu is now positioned against the viewport
instead of the rail, so it cannot be clipped by the thing that opens it.

The card also sat lower in the assistant than in the app: the app column pads its
bottom and the assistant rail only padded the top. Both now use the same vertical
padding and the same row height, so the card lands on one baseline in either
view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161xNW1z4vPusrXKGWcQdQu
2026-09-10 10:27:25 +02:00
Daniel
2c9c2d12c0 fix: one mobile menu, and sources reachable on a phone
Three faults, all visible on a phone at once.

Two hamburgers, neither useful. The assistant added a drawer button of its own
AND the app's button was still there, stacked on top of it. Worse, the assistant
hid the app sidebar unconditionally, so the app's button opened something
invisible. A phone now has exactly one menu — the app sidebar — and the assistant
only replaces it with its own rail on desktop.

The collapse control had nothing to collapse on a phone, where the menu is a
sheet rather than a rail. It closes the sheet instead.

Sources were `display:none` below 640px with no alternative, so every SRC chip in
an answer pointed at nothing. They now slide up as a dismissible sheet, opened by
a button that appears only when an answer actually has citations, or by tapping a
citation itself. Tapping away or pressing Escape closes it — a sheet with no way
out is a trap.

The menu itself is sized for a thumb, borrowing the reference layout: a wide
sheet, close and search leading, tappable rows, and padding clear of the home
indicator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161xNW1z4vPusrXKGWcQdQu
2026-09-10 10:19:20 +02:00
Daniel
500fe2c12a fix: image Done state, chat titles that keep whole words, and readable extension cards
The Create image popup showed "Generating image…" forever even after the job
finished. The status poll called fetchAssistantImageJob, which was never
imported, so every tick threw ReferenceError — and the catch treated that like a
transient network failure and rescheduled, permanently. The import is added, a
test now asserts that every api.js function the assistant calls is actually
imported, and the poll distinguishes a programming error (surface it) from a
transient one (retry, but not forever).

Chat titles were hard-cut at 60 characters mid-word, so "Rickets Radiographic
Fea" was all the Create image picker could ever show. The server already allows
160, so titles now keep whole words up to that, and each view decides its own
visible length from the width it actually has rather than inheriting one cut made
at save time. A single very long token still falls back to a hard cut.

Extension cards led with the number at 20px with word-break:break-all, so
"5616/3764/5619" wrapped as "5616/3764/56 19" — unreadable, and unsafe to dial
from. The name leads now, since that is what the eye hunts for in a list of
fifty; the number follows in tabular figures and may only break between groups,
never inside a run of digits. Cards share a minimum height so a grid reads as
rows rather than a ragged mosaic.

The collapsed rail's brand kept its expanded margin-right:auto, which pushed the
stethoscope off the axis the two buttons sat on. Every child of the collapsed
head is now the same centred fixed-size box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e1PLqrKgAM9jQhFKRnbLd
2026-09-10 06:44:22 +02:00
Daniel
2f7233f317 perf: store real image previews in MinIO, generated at creation and on demand
Gallery tiles are 56px but were downloading the full ~280kB original. Previews
are now rendered with sharp and stored beside the originals in the same MinIO
bucket under a thumbs/ prefix, so nothing about credentials, lifecycle or backup
changes. Measured on live assets: 216-294kB originals become 13-19kB at 256px,
about 16x smaller; 640px is about 4x.

Both paths, as asked:
- Rendered when a job completes, so the first viewer never waits for a resize.
  A preview failure never unmakes a finished job.
- Rendered on demand for anything that has none — the existing 26 images work
  immediately with no backfill required, and the result is stored for next time.

Boundaries that matter more than the speed:
- Only 256 and 640 are honoured. An open width parameter would let a caller
  drive arbitrary resizes.
- Permission is checked against the ORIGINAL before a preview is served, so a
  preview can never widen who can see an image.
- Previews carry their own SHA-256 and owner headers, because the client
  verifies both on every asset; sending the original's checksum would be
  rejected as tampering, which is that check working correctly.
- Still private, no-store. The client asset pattern was widened to exactly
  ?w=256 and ?w=640 and nothing else.

Client-side downscaling stays as the fallback when a preview cannot be produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e1PLqrKgAM9jQhFKRnbLd
2026-09-10 06:39:30 +02:00
Daniel
2096057ebd perf: cache generated images for the session and decode thumbnails, not originals
Generated assets are served `private, no-store`, which is right for a clinical
app on a shared workstation — but it meant every gallery render re-downloaded
every image. Measured: 26 images averaging 279kB, so 7.2MB fetched to draw 56px
tiles, on every open of the Create image popup.

Two changes, both reusable anywhere in the app:

- A session cache holding decoded blobs in MEMORY ONLY, so nothing is written to
  disk and the no-store posture is unchanged. Entries are keyed by owner ticket
  as well as asset, and cleared when the account boundary moves, so one account
  can never read another's bytes out of memory. Concurrent tiles asking for the
  same asset share one request rather than racing.
- Any img carrying data-image-thumb gets a downscaled copy instead of the
  original, so a 56px tile no longer decodes a 300kB image. The gallery asks for
  256px and the in-chat preview for 640px; opening the full view still gets the
  original. Browsers without OffscreenCanvas, and any decode failure, fall back
  to the full image rather than showing nothing.

This does not reduce the first fetch. Serving genuinely smaller bytes needs
server-side resizing, which needs an image library this project does not carry —
worth a deliberate decision rather than adding a native dependency in passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 05:20:10 +02:00
Daniel
531996de1e feat: search finds what is inside a tab, and the menu head reads toggle, mark, search
Search now reaches sub-navigation, because people look for "bili" rather than
"Calculators". It reads whatever a loaded component exposes — data-calc,
data-subtab, data-section — so it covers every tab with sub-navigation instead of
one hard-coded list, and the relevant components are warmed when the palette
opens. Opening a result survives the component still loading, and reaching one
from the assistant navigates first and opens it after.

Menu head order is now show/hide menu, then the mark, then search. In the
collapsed rail every item is the same centred 52px box, so the icons finally
share one axis — the brand was a flex row with a gap and sat off-centre from the
buttons beneath it.

Settings, FAQ and Admin are no longer listed in the menu: they already have a
place in the account card, and listing them twice only made the tab list longer.
Their sections remain, and activateTab already tolerates a tab whose section
exists without a sidebar button.

The retry loops are named functions rather than IIFEs, which the module
entrypoint rules forbid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 05:16:05 +02:00
Daniel
2d6eaccd96 fix: collapse to an icon rail, keep the menu scrollable, and brand as PedAI
Collapsing hid the sidebar entirely, and the toggle had moved into .menu-head
while the hide rule still targeted direct children of .sidebar-nav — so the head
went, taking the toggle with it, and there was no way to reopen the menu.

Collapsed is now a 52px icon rail rather than a disappearance: the mark, the
toggle, search and the account avatar stay reachable in place, so nothing has to
be floated over the content and there is always a way back. The assistant rail
collapses to the same strip.

Scrolling. The workspace list in the assistant rail had no overflow rule, so its
list of every app tab ran off the bottom and scrolling only appeared once you
left for the app. In the app sidebar the opposite: the whole sidebar scrolled, so
the account card scrolled away with the list and Settings and Log out became
something to hunt for. Both now scroll only the list, with the head and the
account card pinned.

The toggle was also being styled twice — as a head-row icon button and by its own
older rule with a different size and margin. It is now only the behavioural hook
that flips the icon.

Brand is PedAI, which is short enough to sit in the head row without squeezing
the controls; the collapsed rail shows the mark alone.

The reachability test now walks the toggle's real ancestor chain and fails if any
collapse rule hides a branch it sits on, rather than asserting one rule's text —
this class of bug has broken twice by moving the toggle deeper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 05:06:23 +02:00
Daniel
c7820c5ae6 feat: one shell for every view, no header bar, and a search palette
The header bar is gone. Settings and Log out moved into the account card, so a
whole band of chrome had nothing left to hold. The brand now heads the menu
alongside the search and collapse controls, on one row rather than three stacked
full-width ones. btn-settings and btn-logout are retained hidden as the canonical
handlers other code already calls, rather than duplicating that logic.

One shell for every view. Each tab was a plain white page while the assistant and
the workspace launcher sat on a tiled card, so opening any menu item changed the
whole face of the app. The tiled ground and the card edge now belong to
.main-content, and only the contents differ. The assistant replaces the shell
card rather than nesting a second one inside it.

Search, in both views, from the same palette: the workspace searches the app
menu, the assistant searches saved chats. Both sources are already in memory, so
this needs no new endpoint. Cmd/Ctrl+K opens it, arrows and Enter work, and the
assistant publishes its chats rather than the palette reaching into its
internals.

The collapse icon is now directional angles that rotate when collapsed, so it
always states what the click will do; fa-table-columns said nothing.

Mobile: the sidebar slides in whole, so the in-sidebar collapse control is hidden
there, the account card sticks to the foot of a scrolling menu, and the palette
is full-bleed instead of a centred dialog on a small screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:56:37 +02:00
Daniel
91699e45cb feat: one menu for both views, a slim top bar, and an account card
The view now decides only what the menu LISTS. Everything structural is shared,
so switching redraws the content and nothing else:

- One width. The app sidebar is 210px; the assistant rail was 260px, so every
  switch shifted the content sideways.
- One surface. The tiled ground belongs to the shared panel rather than to one
  view's empty state, so the transcript and the workspace launcher sit on the
  same background.
- One menu ending. An account card closes both menus with Settings, FAQ, Log out
  and — for admins only — Admin. These were unlabelled icon buttons in the old
  header; log out reuses the existing flow rather than a second implementation.

The blue gradient header is now a slim white bar. Being the loudest element on
screen, it made any view without it look like a different product. Everything
measured against it follows from 66px to 52px.

The composer has room around it and centres on an empty transcript instead of
sitting at the very top, and Create image joins take home, export and attach in
the + menu — it acts on the conversation, so it belongs with them rather than as
its own button on the rail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:45:51 +02:00
Daniel
5cfd47ea66 docs: operator is built from source now
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:31:12 +02:00
Daniel
7b07a1239d docs: storage stack is versioned; record what remains
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:26:57 +02:00
Daniel
b55a748361 fix: remove the assistant's second top bar and give the toggle a menu icon
The assistant carried its own bar above the layout, on top of the blue app
header that both pages now share. On desktop it held a visually-hidden title, a
mobile-only drawer button and a status pill — so it was mostly dead space, and it
made the assistant taller than the app page it is supposed to match.

The bar is gone. The ready/busy indicator moved into the composer footer, next to
the work it describes, and the drawer button floats on phones rather than
occupying a strip the desktop layout does not have. The rail now sticks to the
top of the layout instead of 52px below a bar that no longer exists.

The menu toggle used fa-table-columns, a split-pane glyph that does not read as
"menu". Both copies now use fa-bars and rotate when collapsed to show which way
they will act.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:25:10 +02:00
Daniel
e1e23a99e6 docs: record the MinIO switch and the unversioned storage stack
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:18:12 +02:00
Daniel
db83255c58 feat: display-only sources toggle, signed-out preview, and a composer that carries the toolbar
Sources (correcting what I built earlier)
The previous toggle branched the SYSTEM PROMPT, so the same question could get a
different answer depending on a display setting — the bias this was meant to
avoid. The prompt is now unconditional: buildSystemPrompt takes no display
argument and is byte-identical either way. Hiding sources happens on the way out
— the server omits them and strips the now-orphaned [n] markers from the copy it
sends. The answer is generated, stored and exported with citations intact, so
turning the setting back on restores them without re-asking anything. Renamed to
clinical_assistant.show_sources; the old key is still honoured.

Signed-out preview (admin opt-in, default off)
A visitor may try the assistant; reaching for the workspace asks them to sign in.
Deliberately narrow:
- Reachable paths are an exact allow-list, not a pattern, so a new endpoint is
  private unless someone adds it on purpose.
- A preview visitor gets no identity at all (id: null), so nothing can be owned,
  saved, billed or addressed to them.
- The image tool is withheld rather than left to fail on a null owner, and no
  audit rows are written.
- A caller presenting a token is authenticated normally, so preview can never
  downgrade a real session; if the setting cannot be read, authentication is
  required.
- Actions needing an account are hidden rather than offered and refused.

Composer
The bar above the transcript is gone. Patient take home, Export PDF, Download
transcript and Attach images moved into a + menu in the composer, and the model
selector moved beside send — shown only when there is more than one model, as
before. Both views now start at the same top edge, so switching modes cannot
nudge the page up or down. On an empty transcript the tiled ground runs behind
and below the composer, which floats on it above centre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:12:36 +02:00
Daniel
b9038eafcc fix: the assistant loads again, the switch fits, and a hidden menu can be brought back
Three faults from moving the switch into the app sidebar:

1. Clicking Assistant did nothing. activateTab() bailed unless a matching
   .tab-btn existed, and removing the "AI Assistant" nav entry removed exactly
   that — so every /assistant visit fell through to the fallback tab. A tab
   reached from the mode switch legitimately has a section without a button, so
   activateTab now keys off the section and only touches the button if present.

2. The switch overflowed its box. The sidebar is 210px; without min-width:0 the
   two buttons refused to shrink below their content. They now shrink, with a
   slightly tighter font and gap so both labels fit.

3. "Hide menu" had no way back. The toggle sits inside the sidebar it collapses,
   so it disappeared with it. It now leaves the collapsing box and pins itself
   beside the content, in both the app sidebar and the assistant rail, and the
   label already flips to "Show menu".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
2026-09-10 02:56:40 +02:00
Daniel
6c2bafbe02 fix: style the shared chrome from the stylesheet the app page actually loads
The Assistant/Workspace switch and the menu toggle rendered as unstyled native
buttons in the app sidebar. Their CSS was in assistant.css, which is pulled in by
the assistant COMPONENT — so it is simply absent on "/", where those controls now
also render.

Both moved to styles.css, which every page loads. A stale duplicate of the switch
rules was also still sitting in assistant.css; that is exactly how two copies
drift and one silently wins, so it is gone.

Adds a test that walks the classes rendered in index.html and asserts each is
defined in styles.css and NOT duplicated in assistant.css, which would have
caught this before it shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
2026-09-10 02:49:53 +02:00
Daniel
18cee14549 docs: track the open work in TODO.md
Records what is genuinely outstanding and the non-obvious infrastructure facts
found while fixing it, so the next session does not rediscover them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
2026-09-10 02:45:07 +02:00
Daniel
87c3df67ac fix: restore the ListBucket grant the stale operator image dropped
Image generation was failing with "Generated image storage or migration
unavailable". The real cause was a 403 the catch block was swallowing: the
generated-images-app MinIO policy had lost its s3:ListBucket statement, so the
storage preflight's HeadBucket was denied while writes still worked.

Re-running the storage bootstrap tonight did it. The operator image carries its
own baked-in /opt/storage/check.py, and that copy is older than the file beside
the compose: the host copy grants ListBucket with a comment saying the preflight
needs it, the image copy does not, and running the image overwrote the good
policy. The image's copy also still resolves the store as "clinical-milvus",
from before that service was renamed.

The policy is restored, and the compose now mounts the host check.py (and
bootstrap_basic.py) over the image's, so what runs is what can be read and
reviewed here. Verified: bootstrap re-runs are idempotent again and storage
readiness passes after one.

Also styles the model choice as a real control — a matching chevron, hover and
focus states in the app accent — instead of a bare form element, in both the
composer pill and the Create image popup.

Adds a test that admin rows stack on a phone and that every flex block added
inside one can shrink, so the panel cannot start scrolling sideways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
2026-09-10 02:44:02 +02:00
Daniel
dd25e0edb7 feat: one Assistant/Workspace switch and one menu toggle, shared by both screens
The app IS workspace mode, so the switch belongs in both places and the only
difference between the two screens is which pill is highlighted:

- The app sidebar's "AI Assistant" nav entry is replaced by the switch, with
  Workspace highlighted. The assistant rail carries the same control with
  Assistant highlighted. Clicking Assistant from the app navigates to
  /assistant; inside the assistant, Workspace opens the launcher in place.
- Accent corrected: I had it backwards last round and turned the assistant
  purple. The app's accent is blue (.tab-btn.active in styles.css); the
  assistant now uses it throughout, so neither screen looks like a different
  product. Citation chips keep purple so evidence stays distinct from
  interactive blue.
- One menu toggle labelled "Hide menu", shared by the app sidebar and the
  assistant rail. It replaces three separate controls: a pin button, a floating
  expand button, and the assistant's own rail toggle. The toggle stays reachable
  when the rail it collapses is hidden, or there would be no way back.
- Workspace cards list clinical work only. Settings, FAQ, Admin, Docs and
  Content Manager stay in the app sidebar under Account, where they belong.
- Model availability is discovery-only again: a hand-typed id cannot be verified
  at that point, so the manual entry field is gone.

The in-page mode state machine is removed along with the whole class of
hidden-versus-display bugs it carried.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
2026-09-10 02:39:42 +02:00
Daniel
41ceaca413 fix: actually merge assistant and workspace into one interface
The mode switch was toggling the `hidden` attribute on elements that each carry
their own `display:` rule. A class selector beats the UA stylesheet's
[hidden]{display:none}, so every one of those toggles silently did nothing —
which is why workspace mode still showed the chat, and why coming back never
restored the saved-chat list. Every affected element was in that state.

CSS now owns both modes from a single body class, in one block, and the JS does
nothing but set that class. No element.hidden juggling remains.

One accent. The assistant used --blue for user bubbles, focus rings, blockquotes
and example pills while citations, cards and the mode switch used --purple, so
the two halves read as two different apps. Purple was already dominant (23 uses
to 6) and is the assistant's identity, so blue is gone entirely. Source cards
pick up the same shape, shadow and hover accent as the workspace cards, and a
targeted citation highlights in the accent instead of a bare border.

Go back is removed from both the topbar and the rail: the Assistant/Workspace
switch is how you leave now, and the dead handler went with it.

Create image no longer takes a full-width gradient row competing with New chat;
it is a square icon button beside it with a tooltip and an aria-label.

Also guards the delegated document listeners against double registration.
initIfNeeded already prevented a second bind in production, but nothing stopped
a stray bindEvents() from double-firing every click — which is exactly what the
test harness hit, activating a tab twice from one click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
2026-09-10 01:41:53 +02:00
Daniel
1971436c26 feat: workspace mode keeps both the rail menu and the cards, merged into one look
Both surfaces render from the same pass over the app's real .tab-btn list, so
neither can drift from the menu it mirrors.

- The card view now shares the empty state's graph-paper ground, card language
  and spacing, and is centred with a max width, so switching modes reads as the
  same app instead of a page that looks nothing like it.
- Cards carry the clinical work tabs only: Settings, Admin, Docs and FAQ stay in
  the rail list, where someone looking for them will go, rather than sitting
  alongside Encounter HPI and Calculators.
- On phones the card grid is hidden entirely and the chat stays on screen — the
  drawer already is the menu there, so a second copy only pushed it away.
- The Sources column and its 330px grid track both go in workspace mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-10 00:04:25 +02:00
Daniel
5940ebede7 fix: Workspace swaps the menu, it does not jump to a page of its own
Workspace mode rendered the app menu twice — once in the rail and again as a
card grid that replaced the chat column — so clicking it landed you somewhere
that looked nothing like the assistant. There is one menu now: the rail swaps,
the chat column stays where it is.

The Sources panel was already hidden in workspace mode, but its 330px grid track
was not, leaving an empty gutter. Both collapse states now drop the track.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-10 00:00:12 +02:00
Daniel
9d6bfed3b5 feat: compose the assistant's empty state after the reference UI
Borrowed from the Tinfoil chat layout, keeping our own palette:

- The empty state leads with a title and a single status pill instead of an
  oversized icon plus two paragraphs, so the composer is what the eye lands on.
- Example prompts are raised pill buttons rather than flat outlines.
- A faint graph-paper ground behind the transcript makes the composer read as a
  lifted surface; it switches off as soon as there are messages, where it would
  fight the text.
- New chat carries its Ctrl+Shift+O shortcut on the right, and the shortcut now
  actually works — scoped to the assistant workspace so it does not hijack the
  key elsewhere in the app.
- Saved Chats is a collapsible group with a chevron, remembered per browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 23:50:52 +02:00
Daniel
750efc4106 feat: Assistant / Workspace mode switch in the rail, the way Home and Code swap the page
The rail's Workspace section was a collapsible extra sitting under the chats.
It is now the second of two modes, selected by a segmented switch at the top of
the rail:

- Assistant keeps the chat column, the saved-chat list, New chat and Create image.
- Workspace REPLACES the chat column and the Sources panel with the app's own
  menu — a card grid in the main column and the same list in the rail — so
  nothing from the assistant is left half-visible beside it.

Both the rail list and the cards are built from the app's real .tab-btn
elements, so a tab added, renamed or hidden in index.html follows automatically.

Learning Hub moved down from second place to sit immediately before Content
Manager, with the other content tools rather than above the clinical ones.

Also removed dead CSS for .assistant-view-switch, .assistant-learning-view and
#assistant-learning-root: that markup no longer exists anywhere in the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 23:47:41 +02:00
Daniel
9788b167f2 refactor: retrieval is text-only; admins can add model ids discovery never returns
Multimodal removal
The multimodal path called nc_multimodal_search against a second hardcoded
collection whose embedding service (multimodal-embeddings:7999) was never
deployed and ENABLE_MULTIMODAL_RAG has always been false, so it only ever logged
"multimodal search skipped". Removed rather than left as dead weight:

- clinicalRetrieval: normalizeMcpMultimodalResponse, isVisualSourceQuery,
  isRadiologyQuery, buildMultimodalSearchQuery, classifyAndRerankMultimodalResults,
  selectMultimodalResults, visualIntent, visualMetadataScore,
  shouldRejectVisualSource, allowsFrontMatterQuery, looksLikeFrontMatterPage,
  looksLikeTextOnlyPage and MULTIMODAL_CANDIDATE_LIMIT (~140 lines).
- clinicalMcpClient: multimodalSearch.
- The route's visual/text slot split is gone; the whole search limit is text.
- The "[visual PDF page match]" prompt label and the "visual PDF page" source
  badge are gone with it.

Adding models
Model availability could only be ticked from what the gateway advertised, so an
admin could never offer a model discovery did not list. Each list now has a text
field: a typed id joins the same checkbox list, is enabled by default, is
de-duplicated, and persists through the normal allowed_models save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 23:35:06 +02:00
Daniel
b992c6600b feat: admin can turn citations off
New setting clinical_assistant.citations_enabled (default true, admin checkbox).
With it off, retrieval, grounding and every other rule are unchanged — answers
are still built only from retrieved sources — but:

- buildSystemPrompt swaps only the citation block: the "cite factual claims with
  [1]" rules are replaced with "do not include citations, source numbers or
  bracketed markers", and the note that the sourcing requirement itself is
  unchanged. Grounding, scope, table formatting and tone rules are byte-identical
  between the two modes.
- The server strips any stray [n] the model emits anyway, from the stored answer
  rather than only the view, so saved chats and exports match what was shown.
- No sources are sent to the client at all, and the status endpoint reports the
  mode so the UI hides the Sources panel and gives its 330px column back to the
  chat instead of showing an empty rail.

Validated as a boolean in adminConfig, like the feature.* keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 23:06:13 +02:00
Daniel
022869f01d fix: mobile layout regression, workspace menu in the assistant rail, prompt as principles
Mobile
body.assistant-workspace .assistant-layout is both later in the file and more
specific than the .assistant-layout rule inside @media (max-width:640px), so
phones were inheriting height: calc(100vh - 64px) — an offset for the app header
that workspace mode already hides — plus grid rules on a flex container. The
topbar then overlapped the content. Added a matching-specificity mobile override
restoring 100dvh and the flex layout.

Workspace menu in the rail
The saved-chats rail gains a collapsible Workspace section listing the app's own
tabs, so the assistant sidebar carries both its chats and the workspace
navigation. It is built from the real .tab-btn elements rather than a second copy
of the menu, so tabs added, renamed or hidden in index.html follow automatically
and admin-only tabs stay hidden.

Prompt
Rewritten as principles instead of an enumerated rulebook: "respond to the user's
latest message, not to an earlier one", "if it carries no question, ask what they
would like you to look up", "never repeat a previous answer", and for the tool
"use it when the latest message asks for a picture, or for a change to one you
just made, and not otherwise". No example words in any language remain — the
model reads the message as written. 1032 -> 788 characters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 22:24:10 +02:00
Daniel
89a79738c4 fix: collapsing the saved-chats rail no longer hands the chat column to the citations panel
.assistant-layout.history-collapsed set display:none on .assistant-history, which
removes it from grid flow. The layout still declared three tracks
(0 / minmax(0,1fr) / 330px), so the two remaining items shifted left: .assistant-main
took the 0 track and #assistant-sources took the 1fr. Collapsing the sidebar made
the chat vanish and rendered the citations in its place. The rail is now a
zero-width, visibility:hidden grid item, so the columns stay where they belong.

Also: inside the assistant workspace the topbar no longer repeats "AI Clinical
Assistant" — the page is the assistant. It stays in the DOM, visually hidden, so
screen readers still announce it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 19:31:44 +02:00
Daniel
a95236e363 refactor: the model decides about acknowledgements and images, not route heuristics
The previous fix put a hand-maintained multilingual word list in the route. That
does not generalise — the bug report itself was "Окей", and the next one would be
a language not on the list. The model already has the tool, the system prompt and
the whole conversation, so the policy belongs where it reads it.

Removed from src/routes/clinicalAssistant.js:
- GREETING_RE and its short-circuit (an ASCII keyword list that decided, before
  the model ever saw the message, that a greeting deserved a canned reply).
- The IMAGE_NOUN / IMAGE_REPEAT vocabulary lists and the answer-repetition
  backstop added earlier today.

Moved into the prompt and the tool definition:
- DEFAULT_BEHAVIOR now says that a greeting, acknowledgement or thanks in ANY
  language gets a one-sentence request for a clinical question, and that a
  previous turn's answer must never be repeated to justify a second image.
- The generate_image description says to call it ONLY when the user's latest
  message asks for a picture or a change to one just made, and that an
  acknowledgement of an existing image is not such a request.

Both are admin-editable (clinical_assistant.system_behavior), so this can now be
tuned without a deploy.

dispatchImageRequestFallback and IMAGE_REQUEST_PATTERN stay: that is the
compatibility path for a serving model that writes the image prompt as text
instead of calling the tool. It reads the USER's message only, never the model's
answer, so it cannot replay a previous turn — it was not the cause of this bug.

The DEFAULT_BEHAVIOR byte-hash lock in prompt-administration.test.js is updated
deliberately, which is what that guard is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 19:06:28 +02:00
Daniel
d790d427ac feat: saved chats grouped by recency, a visible sidebar toggle, and acknowledgements answered cheaply
Saved chats
Grouped the way Open WebUI groups them: Pinned, Today, Yesterday, Previous 3
days, Previous 7 days, Previous 30 days, then calendar months (with the year
once it is not the current one). Chats with no usable timestamp land in Undated
rather than disappearing.

Sidebar toggle
The collapse button used <i class="fas fa-sidebar">, which is a Font Awesome PRO
icon; on the Free 6.5.0 build this app loads it rendered nothing, so the toggle
has been an invisible button since it shipped. It now uses fa-table-columns,
keeps aria-expanded/aria-controls in sync, flips its label between "Hide saved
chats" and "Show saved chats" so the collapsed state is escapable, and animates.

Acknowledgements
"Окей", "Nice", "Perfect", "gracias" and friends now get the existing
"What clinical question would you like me to look up?" reply instead of a
retrieval and a paid generation. "yes", "sure", "no" and "more" are deliberately
excluded: answers end by offering more detail, so those must still be answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 18:59:45 +02:00
Daniel
d04a3fe53b fix: translate as HTML so tables and emphasis survive; stop repeat image generation
Translation formatting
LibreTranslate's text mode destroys markdown syntax. Verified against the live
container: table pipes come back as "←", the |---| delimiter row is translated
as prose ("Silencio."), and "**bold**" returns as "** bold**" which no longer
renders. Its html mode leaves tags — and bare [n] markers — completely intact.
Messages and the patient take home are now rendered to HTML, simplified (maths
and UI chrome flattened to text), and translated as HTML. Citation chips are
re-linked from the returned markers afterwards, which is the step the original
html path was missing. A text-mode fallback remains for builds that reject html.

Repeat image generation
Typing "Окей" or "Nice" after an image turn produced another image every time:
the model saw its own "I'll generate an educational image…" in the history and
repeated it. Recognising acknowledgements in every language is not possible, so
the rule is inverted — a short follow-up (<=3 words) that mentions nothing about
a picture does not get the image tool offered at all when the previous assistant
turn produced an image. Terse repeat requests ("again", "ещё", "another one")
still work. The worst case is that a terse question is answered in text.

In-chat images
Generated images render as a 320x240 thumbnail instead of filling the bubble,
and the image itself opens the full-resolution preview.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 18:49:58 +02:00
Daniel
4d92488f0c fix: translated answers keep their source chips; the patient take home can be translated
Translation
- Stop scrubbing markdown before sending it to LibreTranslate. The scrub
  deleted ordered-list numbering ("1. Give amoxicillin" -> "Give amoxicillin"),
  flattened tables into ambiguous whitespace and ate underscores inside
  identifiers. Raw markdown now goes to the translator unchanged.
- Render the translation through the same markdown pipeline as the original
  bubble, with the message's own sources, so [n] markers come back as the usual
  clickable .assistant-cite chips instead of escaped literal text. Headings,
  lists and tables survive with them.
- When the translator drops citation markers, surface the affected sources in a
  recovery block rather than letting the evidence disappear.
- Image cards are live nodes: they are now re-attached on every path out of a
  translation (success, failure and Show original), so a failed translation no
  longer silently removes a generating image from the message.

Patient take home
- Add a language selector to the take-home modal, reusing the existing
  /translate endpoint and offering only what the local LibreTranslate reports.
- Copy, Export and Email carry what the caregiver is actually reading; the
  original stays canonical behind "Original".

Conversation budget
- The admin field no longer prefills with the environment value, which turned
  the next Save into an accidental override and made the documented "leave
  empty to use the environment" path unreachable. The effective limit is shown
  as a placeholder instead.
- Report source 'default' honestly instead of naming an unset env var.
- The load-failure notice now lands on the <p> instead of an <input>'s
  textContent, where it rendered nothing.
- One validator for the budget everywhere: conversationLimit() replaces a
  parseInt that accepted "120000abc".

Other
- /assistant is addressed by its URL, not by ped_last_tab, so "/" no longer
  reopens the assistant; the URL follows tab changes and Back leaves it.
- Remove the dead DeepL path (it referenced an undefined DEEPL_BASES) and stop
  offering admins a provider the server silently ignores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 18:40:23 +02:00
Daniel
e22eb8dbdf fix: mobile menu scrolls as one column like Open WebUI — the saved-chats card grows with content instead of clipping
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 56s
Forgejo Android APK / Build signed APK (push) Successful in 2m5s
2026-09-09 17:55:57 +02:00
Daniel
9a42b96ec9 feat: admin controls the conversation budget — saved override wins over CLINICAL_ASSISTANT_CONVERSATION_CHARS, validated 1000-1000000, empty falls back to env
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 58s
Forgejo Android APK / Build signed APK (push) Successful in 2m7s
2026-09-09 17:49:57 +02:00
Daniel
598731e170 fix: conversation budget is env-only — admin panel shows CLINICAL_ASSISTANT_CONVERSATION_CHARS read-only and never saves the key; server ignores saved overrides
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m17s
2026-09-09 17:32:15 +02:00
Daniel
d6ba2ce2a6 fix: mobile drawer scrolls with iOS momentum and overscroll containment for long chat lists
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m9s
2026-09-09 16:55:50 +02:00
Daniel
3676d2c105 fix: mobile drawer rows now truly fit the drawer width (align-items:stretch) and the options ⋮ stays visible
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 56s
Forgejo Android APK / Build signed APK (push) Successful in 2m12s
2026-09-09 16:49:43 +02:00
Daniel
64f8cbc0f7 fix: mobile saved-chat titles smaller with clean ellipsis ending before the options button and more spacing between rows 2026-09-09 16:41:41 +02:00
Daniel
1267d1b7fc fix: login screen — remove Download Android app (APK) link and About Pediatric Clinical Tools block
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 55s
Forgejo Android APK / Build signed APK (push) Successful in 2m22s
2026-09-09 16:37:47 +02:00
Daniel
e1f1a58917 fix: Download transcript stays in the toolbar with its download icon (no empty round space); drawer keeps plain text Go back only
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 46s
Forgejo Android APK / Build signed APK (push) Successful in 2m3s
2026-09-09 15:37:21 +02:00
Daniel
b9a70fd370 fix: shorter menu words on mobile so every row fits the drawer width (Download, compact 13px rows with ellipsis)
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 57s
Forgejo Android APK / Build signed APK (push) Successful in 2m11s
2026-09-09 15:23:02 +02:00
Daniel
fe102d64b4 fix: mobile drawer is a plain text menu (no icons) — Go back and Download transcript rows; toolbar download hidden on mobile 2026-09-09 15:21:18 +02:00
Daniel
cbddc43bb6 fix: mobile Go back moves into the drawer menu; topbar keeps only the hamburger 2026-09-09 15:19:10 +02:00