Commit graph

69 commits

Author SHA1 Message Date
Daniel
48a3b06ebb feat: export a recording; report a recorder that has silently died
Export
- Both stores already held the audio (server /audio-backups/:id/audio and
  the local IndexedDB record) but nothing exposed it, so a recording could
  not be taken out of the app. Each backup row now has a download, naming
  the file by its timestamp and using the extension actually recorded
  (webm, or m4a on iOS). A local record is only handed over to the account
  that owns it.

Robustness
- MediaRecorder had no onerror and nothing watched the audio track, so a
  recorder that failed, or a microphone claimed by another app, unplugged,
  or revoked, left the tab saying "recording" while capturing nothing.
  Both are now reported once, with the chunks captured so far kept, so
  stopping still returns the audio up to the failure.

Deliberately not added: a wake lock. Stopping when the screen sleeps or the
session ends is the intended behaviour — recording is meant to be
deliberate, and nothing is left behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-10 15:58:58 +02:00
Daniel
cf7a3161a8 fix: stop iOS zoom, compact model control, quieter composer, better search
iOS zoom (and the menu button scrolling away with it)
- Safari zooms the whole page when a text field under 16px takes focus and
  never zooms back; while zoomed, fixed chrome sits outside the visual
  viewport, which is why the menu button had to be scrolled to. All 144
  visible text controls were 13-15px. They are 16px on phones now.
  !important is deliberate: per-component rules like `.demo-field select`
  outrank any element selector, and a new component would slip through.
- The composer set 15px AFTER its own mobile 16px rule, so at equal
  specificity the base rule won; the size is per breakpoint now.
- Pinch-zoom is untouched: no user-scalable=no. Desktop unchanged (13/15px).

Composer
- "Ready" / "Looking up sources..." / "saved" repeated what the send button
  already shows by turning into Stop, and on a phone they wrapped under the
  model control. Kept in the DOM as live regions; nothing is drawn.
- The model control shows the word "Model" (76px) instead of ids as long as
  "openrouter-gemini-3.1-flash-image-preview", and opens the full list on
  click. The select stays as the state holder, so choosing still saves
  through the same change event.

Search
- Nothing typed lists the 19 destinations only; sub-items appear once there
  is something to match.
- Listing, matching and opening now read one attribute list instead of three
  that could drift, and bedside's emergency sections are searchable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-10 15:26:43 +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
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
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
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
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
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
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
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
8a03fbb689 fix: voice — recorder codec chain with iOS mp4 fallback, audio-session unlock on voice gestures, unbounded image poll until completion 2026-09-09 03:11:43 +02:00
Daniel
7fdbcd51e6 feat: ChatGPT-style chat list — abbreviated rows with relative dates, pinned-first ordering, ⋮ menu (Rename/Pin/Export/Delete); clinical context always on; warn-only budget; mic live-transcript fallback; learning-hub page mode; composer autosize; include-context icon fix 2026-09-09 00:14:34 +02:00
Daniel
cfaf8e957b feat: ship reviewed prompt history, conversation limits and account protections 2026-09-07 04:01:01 +02:00
Daniel
f31afcdbf4 Fix biometric login, and keep recording alive across screen lock
Biometric sign-in has never worked. auth.js drove
window.Capacitor.Plugins.NativeBiometric — the API of
capacitor-native-biometric, which is not a dependency of this project. The
installed plugin is @aparajita/capacitor-biometric-auth, registered as
BiometricAuthNative with an entirely different API and no credential
storage at all. bioPlugin() therefore always returned null, bioAvailable()
always resolved {ok:false}, and the button was never revealed.

Rewritten against what is actually installed, with no new dependency:
BiometricAuthNative (checkBiometry/authenticate) presents the prompt, and
the already-working SecureStoragePlugin — via the window.SecureStorage
wrapper — holds the credentials. Credentials are only read after
authenticate() resolves, so the OS still gates access. biometryType is a
numeric enum in this plugin, so the old FACE_ID/TOUCH_ID string maps are
replaced with a single lookup exposed as typeName.

Secure storage itself was fine and is unchanged: SecureStoragePlugin
matches the name the wrapper looks up and is registered in
capacitor.settings.gradle.

Recording across screen lock: window.nativeKeepAwake() called Capacitor's
KeepAwake plugin, which is also not installed here, so it silently did
nothing and the device slept mid-encounter — taking the WebView's
MediaRecorder with it. keepAwake() is now a method on the existing
NativeRecording JavascriptInterface, which sets FLAG_KEEP_SCREEN_ON, and
is bound to the WebView so it survives the launcher's navigation to the
remote origin. The Capacitor plugin remains a fallback.

If the screen is locked anyway (power button, incoming call), the activity
pauses and Chromium throttles timers for hidden WebViews, starving
MediaRecorder's chunk delivery. MainActivity now calls resumeTimers() on
pause while recording. The foreground service was already correct — it
holds a partial wake lock and declares FOREGROUND_SERVICE_TYPE_MICROPHONE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 01:12:47 +02:00
Daniel
cddc1a4d79 document architecture and harden rendering 2026-05-10 01:07:56 +02:00
Daniel
795ad9ffae harden clinical assistant source handling 2026-05-09 19:59:04 +02:00
Daniel
416fff624a feat: add patient education handouts 2026-05-08 22:26:53 +02:00
Daniel
db2ecca45d remove top bar model selector 2026-05-08 08:28:54 +02:00
Daniel
ea52890908 gate browser speech recognition setting 2026-05-08 08:05:23 +02:00
Daniel
20fc6798a9 remove browser whisper transcription 2026-05-08 07:33:12 +02:00
Daniel
d48a19a891 honor admin default model selections 2026-05-08 06:49:50 +02:00
Daniel
289b0197e7 remove redundant app wrappers 2026-05-08 06:21:55 +02:00
Daniel
6c25e8ff05 fix: polish clinical assistant citations 2026-05-06 23:04:40 +02:00
Daniel
4f129b24e1 feat: don't-miss tooltip after notes + bedside suture selector
#2 — Don't-miss tooltip (encounters HPI + sick visit, max 5)
- New POST /api/dont-miss returning {points: [{point, why}]} capped at 5
  (cap defended both in the prompt and server-side .slice(0,5))
- New dontMissTooltip prompt in prompts.js
- New suggestDontMiss() helper in app.js mirroring suggestBillingCodes;
  inserts an orange-bordered card next to the note output, silent on empty
- Wired into liveEncounter.js (encounter HPI) and sickVisit.js. Not added
  to wellvisit/soap/hospital/chart per spec.

#1 — Bedside suture selector
- New ES module public/js/bedside/sutures.js (~300L) following the
  burns.js pattern: site × age × tension × cosmetic × contamination ×
  hours-since-injury → material, size, technique, removal day range,
  glue/Steri-strip alternative, warnings, tetanus reminder.
- 15 anatomic sites covered (face, eyelid, lip vermilion, intraoral,
  ear, scalp, neck, trunk, upper/lower ext, hand, foot, joint surface,
  genitalia, fingertip).
- Bites: cat/human → don't-close-primarily warning; dog bite to hand →
  loose-approximation note. Heavy contamination → delayed primary
  closure. >12h non-face/scalp → judgment call note.
- Removal days shown as ranges (3–5, 7–10, 10–14) per source norms,
  not single midpoints.
- Subungual hematoma trephination guidance corrected: any painful
  hematoma with intact nail and no displaced fracture (especially if
  25–50% or more), per current UpToDate guidance.
- Inline citation: Roberts & Hedges 7e (2019), Fleisher & Ludwig 8e,
  AAP Section on EM, UpToDate (Pope JV).
- Pill registered in sub-nav SECTIONS + bedside/index.js. Persists
  active state via existing UIState helper.

All 46 tests pass.
2026-04-28 03:09:38 +02:00
Daniel
e625c634b6 Security hardening: low-risk easy wins
- JWT_SECRET fails fast at startup in production
- CORS fails closed if APP_URL + CORS_ORIGINS are both missing
- Explicit HSTS (1y, includeSubDomains, preload)
- Rate limit sensitive auth endpoints (change-password, 2FA)
- /api/health now returns {ok:true}; details gated behind admin auth
- Login enumeration removed — generic 401 + dummy bcrypt on miss
- ReDoS guard: 20KB input cap on /suggest-codes
- showToast uses textContent, no innerHTML
- clearSession() clears service worker caches on logout
- OIDC state is now HMAC-signed and stateless (survives restart)
- SSRF guard on admin-set OIDC issuer (blocks private IPs, requires HTTPS)

Adds docs/mobile-build.md covering APK build, release, git push,
keystore, and troubleshooting for both PedScribe and PedsHub apps.
2026-04-14 02:42:32 +02:00
Daniel
011fae9b7a Enhance audit logging: user agent, session ID, PHI access tracking
Loki logs now include:
- User agent string (browser/device identification)
- Session ID (ties actions to specific login session)
- Status field (success/failure)

New logging:
- encounter_load: logged when user opens a saved encounter (with label)
- copy_to_clipboard: logged when user copies note content (PHI access)
- Client event endpoint: POST /api/logs/client-event (auth required)

Encounter save/delete/load all include the encounter label for
patient identification in audit trail.

HIPAA audit trail now covers: who, what, when, from where, which
device, which session, what patient data, success/failure.
2026-04-11 06:17:05 +02:00
Daniel
e1ce374809 Native Android: biometric auth, foreground service bridge, mic fix
Major Android native improvements:

Biometric authentication:
- Native AndroidX BiometricPrompt on app launch (2nd launch onwards)
- Supports fingerprint, face, iris, and device PIN/password fallback
- Gracefully skips if no biometric hardware or first launch
- Uses SharedPreferences to track first launch

Microphone permission:
- Added MODIFY_AUDIO_SETTINGS permission (required for WebView audio)
- Added androidScheme: "https" in Capacitor config (getUserMedia requires
  secure context)
- WebChromeClient properly grants WebView permission after Android
  runtime permission is obtained
- Handles pending permission request across the async flow

Background recording bridge:
- NativeRecording JavaScript interface exposed to WebView
- startForegroundService() / stopForegroundService() callable from JS
- Web app calls these on recording start/stop in liveEncounter.js
- AudioRecordingService keeps CPU awake + shows notification when recording
- Recording survives screen lock via foreground service + wake lock

Also:
- USE_BIOMETRIC permission added to manifest
- androidx.biometric:biometric dependency added to build.gradle
- Haptic fallback to navigator.vibrate when Capacitor plugins unavailable
2026-04-11 03:42:23 +02:00
Daniel
11d4880337 Mobile app: haptics, deep linking, share intent, push notifications, keep-awake
Native improvements:
- Add haptic feedback on recording start (heavy) and stop (medium)
- Add keep-screen-awake during recording (nativeKeepAwake)
- Add isNativeApp() detection helper
- Android: deep linking (pedscribe:// + https://app.pedshub.com)
- Android: share intent for text/plain and application/pdf
- iOS: deep linking (pedscribe:// URL scheme)
- iOS: remote-notification background mode
- Add Capacitor plugins: haptics, keyboard, push-notifications,
  screen-orientation, share

Updated README with complete build/deploy instructions,
App Store listing suggestions, and icon generation guide.
2026-04-11 02:28:30 +02:00
Daniel
639a5d2873 Add automatic ICD-10 and CPT billing code suggestions
New feature: after generating any clinical note, the app automatically
suggests relevant billing codes displayed as clickable chips below the output.

Backend (src/routes/billing.js):
- POST /api/suggest-codes endpoint analyzes note text
- Extracts diagnoses from Assessment section via regex
- Looks up ICD-10 codes: local common pediatric map (40+ conditions)
  first, then NLM Clinical Tables API for unknown terms
- Suggests CPT E/M codes based on note type, visit complexity,
  ROS/PE system counts, and MDM level estimation
- Supports: outpatient (new/established), well visit (age-based),
  ED, inpatient (admit/subsequent/discharge)

Frontend (public/js/app.js):
- suggestBillingCodes() renders collapsible card with ICD-10 and CPT chips
- Click any chip to copy the code to clipboard
- Shows E/M level assessment (diagnosis count, ROS, PE, MDM complexity)
- Disclaimer: "Suggestions only. Always verify codes."

Integration: called after note generation in all 6 tabs
(encounter, SOAP, sick visit, well visit, hospital course, chart review)
2026-04-11 01:50:17 +02:00
Daniel
3d5b77721c Replace all browser dialogs with modern modal, add OIDC admin UI
- Add reusable showConfirm() modal component (supports plain confirm,
  input prompt, danger styling, Enter key)
- Replace ALL 18 confirm() and prompt() calls across 8 JS files with
  showConfirm() modal: admin user actions, session revoke, document
  delete, template delete, milestone management, transcription settings
- Fix broken admin reset-password (btn was undefined in scope)
- Add OIDC/SSO configuration UI to Admin Panel (issuer, client ID/secret,
  button label, disable local auth toggle, callback URL display)
2026-04-09 02:43:23 +02:00
Daniel
85f9af4ffc Remove prompt() dialogs, breach warnings, cost display; fix 2FA disable UI
- Replace browser prompt() with inline UI for: 2FA disable (password field),
  admin password reset (inline input), admin test email (inline input)
- Remove all password breach warning UI (login, register, settings)
  Backend HIBP check endpoint remains but is no longer called from frontend
- Remove model cost display from dropdown and header badge
- Hide empty cost-badge element in header
- Fix model dropdown to flat list (no category grouping)
2026-04-09 02:27:55 +02:00
Daniel
55f8e172e6 FAQ page, dep security patches, model dropdown and UI fixes
- Add FAQ tab with accordion sections: Getting Started, AI & Models,
  Voice & Transcription, Saving & Export, Privacy & Security,
  Well Visit & Sick Visit, Learning Hub, Troubleshooting
- Documents how AI learns from physician edits (correction tracker)
- Fix FAQ accordion (CSP was blocking inline script, moved to app.js)
- Patch all 5 npm vulnerabilities: nodemailer 8.0.5, xmldom, basic-ftp,
  path-to-regexp (npm audit now reports 0 vulnerabilities)
- Remove model category grouping from dropdown (flat list, no optgroups)
- Fix model dropdown dark background on options (white bg, dark text)
- Update FAQ model guidance to reflect admin-managed model selection
2026-04-09 01:56:11 +02:00
Daniel
09193538fb v6.2: Session management, password change, audit logging, refine context, UI fixes
Security:
- Add session management: users can view/revoke active sessions in Settings
- Add password change in Settings (requires current password, HIBP check)
- Force logout all sessions on password reset
- Fix logout to destroy server-side session (was only clearing cookie)
- Add trust proxy for correct client IP in rate limiting and audit logs
- Add CORS support for multiple domains (CORS_ORIGINS env var)
- Add HIBP breach check endpoint and inline warnings on password fields

Audit logging:
- Add audit logging to all 24 PHI-handling endpoints across 13 route files
- Covers: generation, transcription, TTS, refine, encounters, documents, Nextcloud
- All fire-and-forget (no response delay)

AI improvements:
- Refine now includes original source material (transcript, notes, labs)
  so AI can reference the full input when modifying output
- Add correction tracking (trackAIOutput) to sick visit and well visit tabs
- Fix sickvisit missing from encounter save noteIdMap

UI fixes:
- Non-blocking busy bar for transcription and AI generation (replaces full-screen overlay)
- Fix encounter recording: hide record button during recording (was showing two stop buttons)
- Fix ROS/PE "All WNL" stacking duplicate event handlers; add Clear buttons
- Enlarge AI instructions textarea in Learning Hub CMS

Domain:
- Primary domain now app.pedshub.com, with scribe.pedshub.com and peds.danvics.com as CORS origins
2026-04-08 20:27:45 +02:00
Daniel
869fa14a77 v6.1: Turnstile bot protection, LiteLLM provider, PPTX tables, audio backup fixes, docs
- Add Cloudflare Turnstile to login, register, and password reset forms
- Switch AI provider to LiteLLM, transcription to OpenAI Whisper
- Change domain to scribe.pedshub.com
- Fix PPTX export: add tables, bold/italic, numbered lists, code blocks, blockquotes
- Fix announcement banner close button (CSP was blocking inline onclick)
- Fix auth middleware: empty Bearer token now falls through to cookie auth
- Fix audio backups: only save on transcription failure, stop auto-deleting on success
- Soften AI correction injection to prevent model hallucination from correction history
- Fix LiteLLM TTS model name handling (no incorrect openai/ prefix)
- Expand AI instructions textarea in Learning Hub CMS
- Update README for v6 with all features and providers
- Add comprehensive docs/: architecture, API reference, database schema,
  authentication, AI providers, speech, learning hub, configuration, deployment
2026-04-04 22:56:24 +02:00
ifedan-ed
c38ce9445e v16: Make Browser Whisper CDN failure graceful with clear warnings
Some checks failed
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / build (push) Has been cancelled
REALITY CHECK: Browser Whisper CDN loading cannot work in all environments
- Corporate firewalls block cdn.jsdelivr.net
- Network proxies filter JavaScript CDN
- Workers + importScripts + cross-origin = blocked by CSP/CORS

SOLUTION: Graceful degradation
- Clear user-friendly error messages
- Automatic fallback to server transcription
- Warning banner in Settings if CDN blocked
- Comprehensive troubleshooting documentation

Changes:
- browserWhisper.js: Show toast on worker error, fallback gracefully
- app.js: Display CSP warning banner on preload failure
- settings.html: Add warning about network/firewall requirements
- BROWSER_WHISPER_TROUBLESHOOTING.md: Complete guide for users

Key Message:
Browser Whisper is OPTIONAL. Server transcription (Google/AWS/OpenAI)
is the primary method and works everywhere. Browser Whisper is a
privacy-focused bonus feature that requires CDN access.

User Experience:
- If CDN works: Great! Browser Whisper available
- If CDN blocked: No problem! Server transcription works perfectly
- Clear messaging: User knows what to expect
2026-03-31 16:18:46 +00:00
ifedan-ed
ee3729eb57 v15: Fix TTS preview + Browser Whisper preload with extensive debugging
BREAKING FIXES:
- TTS Preview: Added event.preventDefault(), console logging, proper init check
- Browser Whisper: Complete console logging pipeline, error handling, progress tracking
- Voice Preferences: DOMContentLoaded fallback, explicit button click handlers
- Whisper Worker: Console logs at every step, better error messages

Debugging Features:
- Console logs show: button clicks, init events, progress updates, errors
- Progress tracking: [WhisperWorker] Progress: model.bin 47%
- Error messages: Specific failure reasons (not generic failures)
- Timeout warnings: 30s check for stuck downloads

Audio Backup Confirmed:
- Deletes immediately on successful transcription (line 621-624 app.js)
- NOT after 24 hours - 24h is server retention limit for failed transcriptions
- User was correct - this is working as designed

How to Debug:
1. Open DevTools → Console (F12)
2. Click button
3. Watch for [VoicePrefs] or [BrowserWhisper] logs
4. Check Network tab for actual downloads
5. Report what you see in console
2026-03-31 15:28:38 +00:00
ifedan-ed
9016af8fe2 Fix TTS preview + Browser Whisper preload, add comprehensive docs
Some checks failed
Build TWA APK / build-apk (push) Has been cancelled
Build & Push Docker Image / build (push) Has been cancelled
Fixes:
- TTS preview: Better error handling, console logging, empty value check
- Browser Whisper: Add progress logging, 30s timeout warning, better UX
- Voice preferences: Clearer error messages

New Documentation:
- FEATURES_EXPLAINED.md: Complete guide to all v14 features
  - Audio backups explained (works every recording, not just on failure)
  - S3 integration setup guide (AWS, B2, MinIO)
  - Learning Hub default path explained (AI file picker starting folder)
  - Browser Whisper troubleshooting (download progress tracking)
  - TTS preview debugging steps
  - Comprehensive troubleshooting guide
2026-03-31 15:13:53 +00:00
Daniel Onyejesi
d1a7c97ecc Add browser-side Whisper transcription (local, zero network, HIPAA-safe)
- whisperWorker.js: Web Worker running @xenova/transformers Whisper in WASM
- browserWhisper.js: main-thread manager — audio→Float32 conversion, worker lifecycle
- transcribeAudio() checks BrowserWhisper.isEnabled() first, falls back to server
- Settings UI: enable/disable, model picker (tiny/base/small), pre-download button
- CSP: add wasm-unsafe-eval, cdn.jsdelivr.net, HuggingFace CDN domains
- Default: whisper-tiny.en (~39MB, ~2-3s per clip)
2026-03-31 07:30:17 -04:00
Daniel Onyejesi
0ce2735315 Fix Read aloud stop button: findReadButton now finds data-action=speak buttons
The button was always returning null because it searched for onclick=speakText
but all output cards use data-action="speak" data-target="id". Now checks
data-action first so the button correctly toggles to Stop during playback.
2026-03-30 21:37:00 -04:00
ifedan-ed
1c23f2dc12 Show TTS provider in toast, support full LiteLLM model paths
- TTS response now includes X-TTS-Provider header (google-tts, litellm/model, elevenlabs)
- Frontend reads header and shows actual provider in toast instead of hardcoded "Adam/ElevenLabs"
- CORS exposes X-TTS-Provider header so frontend can access it
- Updated .env.example: clarify that LITELLM_TTS_MODEL and LITELLM_STT_MODEL
  can be either the model_name alias OR the full provider/model path depending
  on your LiteLLM config (important for BAA compliance routing)
2026-03-30 13:37:20 +00:00
Daniel Onyejesi
e6091c299f Fix TTS axios/Vertex, generic toast, add stop button to encounter
- TTS: switch from OpenAI SDK to axios (same fix as STT), drop voice
  param since it's configured inside LiteLLM per model
- Fix 'ElevenLabs unavailable' toast shown even when provider is LiteLLM
- Add dedicated red Stop button to encounter recording UI
2026-03-29 22:09:56 -04:00
ifedan-ed
e58876ddd9 Zero-config speech: skip upload when no transcription API, use browser speech directly
- Add GET /api/transcribe/status endpoint — returns whether any server
  transcription provider (Whisper/AWS/Local) is configured
- Frontend checks status on login via checkTranscribeStatus()
- When no provider configured: recording stops instantly, keeps live
  Web Speech API text, shows friendly toast — no error, no upload wait
- Works in encounter, dictation, and SOAP tabs
- App now works fully out-of-the-box with just an AI provider key
2026-03-29 02:09:53 +00:00
ifedan-ed
bc22f80e25 Optimize transcription speed: remove artificial delays, add timing
- AWS Transcribe: remove 10ms delay between chunks (was adding ~1.25s/MB),
  increase chunk size from 8KB to 32KB (AWS max per frame)
- Add detailed timing logs (ffmpeg, streaming, total) for diagnostics
- OpenAI Whisper: use response_format='text' for faster response parsing
- Frontend: show transcription time in toast, request 16kHz sample rate,
  increase bitrate to 32kbps Opus (better quality, still small files)
- Return duration in API response for all providers
2026-03-29 00:59:30 +00:00
Daniel Onyejesi
4e5b6fed5a Add admin model management dashboard — enable/disable, custom models, default override
- Full model management UI in admin panel: toggle models on/off, add custom
  model IDs (any OpenRouter/Bedrock ID), set admin-configured default model
- /api/models now returns admin-set default model, frontend respects it
- Toggle switch CSS for clean enable/disable UX
- Backend already had the API endpoints, this adds the missing UI
2026-03-28 22:07:16 +00:00
Daniel Onyejesi
3b7994c2c1 v9: Major feature update — audio backup, SOAP save, Dragon memory, S3 docs, CI/CD, APK
Phase 1 — Critical Fixes:
- Fix SOAP instructions not clearing on Clear button
- Show transcription provider (AWS/OpenAI) in UI toast
- Fix silent transcription failures in dictation and SOAP modules
- Add IndexedDB audio backup system (24hr retention, retry from Settings)
- Prevent duplicate encounter saves with idempotency keys
- Add Save/Load/New bar to SOAP note generator

Phase 2 — Features:
- Dragon-like AI memory: auto-track user corrections, inject into prompts
- Per-section template categories (SOAP, HPI, well visit, sick visit)
- Bigger textarea for SOAP instructions
- S3 document upload/management (AWS S3, Backblaze B2, MinIO compatible)
- Faster transcription via lower bitrate recording (16kbps opus)

Phase 3 — APK & CI/CD:
- GitHub Actions: Docker build+push on version tags
- GitHub Actions: TWA APK build for Obtainium auto-updates
- Android TWA project with foreground service for background recording
- Enhanced PWA manifest with shortcuts and maskable icons
2026-03-28 21:08:32 +00:00
Daniel Onyejesi
8e509a7166 Remove Firefox speech notice, fix auth.js SSO flow race condition
- Remove Firefox speech recognition notice (not needed)
- Fix missing closing brace that made speech recognition unreachable
- Fix auth.js SSO token handling to prevent brief auth screen flash
2026-03-25 19:04:20 -04:00
Daniel Onyejesi
8e544ad5b9 Add OpenID Connect SSO + Firefox speech notice
OIDC/SSO:
- New /api/auth/oidc route with PKCE for secure authorization
- Supports Azure AD, Okta, Keycloak, PocketID, Google, any OIDC provider
- Admin configurable: issuer, client ID/secret, button label
- Option to disable local auth (force SSO only)
- Auto-creates users on first SSO login, links existing by email
- SSO button on login page, hidden until admin enables OIDC

Firefox:
- Show info toast on first recording that live preview requires
  Chrome/Edge; server-side transcription still works in all browsers
2026-03-25 18:54:44 -04:00