Commit graph

6 commits

Author SHA1 Message Date
Daniel
d99605c3c0 stabilize assistant and add diagrams 2026-05-08 00:23:00 +02:00
Daniel
18a69d2a13 feat: improve clinical assistant visual sources 2026-05-07 02:45:17 +02:00
Daniel
355c2a999b feat(notes): trash + restore + DOMPurify sanitizer + 9 contract tests
Soft-delete for notes — Daniel asked for "deleted notes go to trash"
so a slip of the finger doesn't lose work.

Schema: migrations/1777090000000_notes-trash.js adds a deleted_at
timestamptz column to personal_notes (NULL = active) plus an index
on (user_id, deleted_at).

Server (src/routes/notes.js):
  GET    /api/notes              now filters deleted_at IS NULL
  GET    /api/notes/trash        new — list trashed items, newest-
                                  deleted first
  DELETE /api/notes/:id          now soft-deletes (sets deleted_at)
  DELETE /api/notes/:id?hard=1   hard-delete, only allowed on items
                                  already in trash (UI bug can't
                                  erase an active note)
  POST   /api/notes/:id/restore  pull a note out of trash
  POST   /api/notes/trash/empty  hard-delete every trashed note for
                                  the user

Frontend (public/components/notes.html + public/js/notes.js +
public/css/styles.css):
  • Sidebar gets two tabs — "Notes" / "Trash (n)" with live count
  • Trash tab shows deleted-at timestamps, Restore + delete-forever
    per row, Empty-trash button at the bottom
  • Active list and trash count refresh in parallel after every
    save / delete / restore
  • Delete button in the editor now says "Move to trash" and uses
    the showConfirm helper (no native dialogs)

Sanitizer swap (public/js/notes.js):
  Replaced the homegrown allowlist walker with DOMPurify (already
  loaded from cdnjs in index.html, used by learningHub.js too).
  Custom HTML sanitizers historically have bypasses; DOMPurify is
  the right primitive.

Tests (test/notes-sanitize.test.js — node:test + jsdom + dompurify
       as new dev deps):
  9 contract tests covering script-tag stripping, inline event
  handlers, img onerror, iframe/object, style attributes, every
  preserved tag in the allowlist, javascript: URI rejection,
  null/undefined input, and nested-script-inside-paragraph. Total
  test count: 37 → 46 passing.

SW cache bumped to pedscribe-v12-notes5.
2026-04-25 01:02:49 +02:00
Daniel
ac39554c3a feat(notes): personal notes with rich-text editor + voice dictation
New "Notes" tab under Clinical Tools — a per-user scratchpad that's
explicitly NOT fed into AI prompts (distinct from user_memories).
Two-pane layout: searchable list on the left, rich-text editor with
title + save/edit/delete on the right.

Backend:
  migrations/…_add-personal-notes.js  — personal_notes table
    (id, user_id → users ON DELETE CASCADE, title, body, created_at,
    updated_at) with indexes on user_id + (user_id, updated_at).
  src/routes/notes.js — CRUD + one AI endpoint:
    GET    /api/notes           list, newest-updated first
    GET    /api/notes/:id       fetch one
    POST   /api/notes           create (title + body required)
    PUT    /api/notes/:id       update
    DELETE /api/notes/:id       remove
    POST   /api/notes/from-voice  transcript → { title, body }
                                   via callAI (admin-controlled
                                   provider — never selectable by
                                   the clinician).
    Body + title encrypted at rest via the same cryptoUtil used for
    user_memories; 500-note per-user cap; 200-char title / 50 KB
    body limits.

Frontend:
  public/components/notes.html — empty-state card ("Hello 👋"),
    sidebar list with search, editor head with voice-bar (Dictate /
    Pause / Resume / Stop + live timer + pulse indicator), Tiptap
    body, metadata footer. Uses existing .tp-* toolbar classes.
  public/js/notes.js — lazy-init on first tab activation; Tiptap
    editor built from window.Tiptap (same bundle the Content
    Manager uses); delegated list clicks; Ctrl/Cmd+S to save;
    uses app.js's AudioRecorder + transcribeAudio so the STT
    pipeline is shared. On stop → transcribe → /api/notes/from-
    voice → drop the AI-structured title + body into the editor;
    clinician reviews then saves.
  public/css/styles.css — 70 lines of .notes-* styles (card
    layout, warm empty-state with gradient icon + tips, pulse
    animation for the recording indicator, focus states, hover
    nudges).
  public/sw.js — bump cache from pedscribe-v12 → pedscribe-v12-
    notes1 so clients pick up the new module/component.

Admin-controlled STT provider: the recorder posts its audio blob
to the existing /api/transcribe (Google / LiteLLM / ElevenLabs /
Browser Whisper — whatever admin wired up in Settings). Users
cannot pick the provider from this UI.
2026-04-24 06:18:41 +02:00
Daniel
63f77aa9cf Batch of security + scale fixes
Age parser (src/routes/billing.js):
  - Now sums year + month + week + day matches so "4 yr 11 mo"
    (59 months) correctly maps to the 5-11y billing bracket instead
    of being billed as 1-4y. Added bounds sanity check.

Graceful SIGTERM shutdown (server.js):
  - Closes the HTTP listener first, then drains batched audit queues,
    then ends the Postgres pool. 9-second hard deadline to beat
    Docker's 10-second SIGKILL. Previously an in-flight note save
    during a container restart could truncate the write.

Explicit LLM fallback opt-in (src/utils/ai.js):
  - The OpenRouter / LiteLLM silent fallback now requires admin
    setting `ai.allow_model_fallback = true` (default: false). If
    primary fails and fallback is disabled, the error is surfaced
    to the caller. Prevents silent spillover from a BAA-covered
    primary to a non-covered fallback.

Prompt injection delimiters (src/utils/promptSafe.js):
  - Wraps user transcripts, dictations, refine-instructions, and
    pasted documents in <UNTRUSTED_*>...</UNTRUSTED_*> tags and
    appends an explicit system instruction telling the model to
    treat the wrapped content as data rather than commands.
  - Applied to soap.js, hpi.js, refine.js. Extend to other AI
    routes incrementally.

Cross-tab logout sync (public/js/authFetch.js, auth.js):
  - BroadcastChannel('pedscribe-auth') — logout in one tab posts
    a message; all sibling tabs clear state and reload, dropping
    any PHI-containing UI immediately.

Backup code race-free consumption (src/routes/auth.js):
  - tryConsumeBackupCode() now uses a Postgres transaction with
    SELECT ... FOR UPDATE so concurrent login attempts using the
    same code serialize. First wins, second sees the already-
    shortened array.

Optimistic encounter locking (migrations/...add-encounter-version):
  - saved_encounters.version INTEGER NOT NULL DEFAULT 1
  - POST /api/encounters/saved accepts an expected_version and
    rejects with 409 if the row has advanced. Falls back to
    last-write-wins if the client doesn't pass one (backward compat).

Audit log batching (src/utils/auditQueue.js):
  - Audit / api_log / access_log writes are buffered in memory and
    flushed every 1s or every 50 entries via one multi-row INSERT.
    Under load this reduces DB pressure by ~50x. On SIGTERM the
    shutdown path drains the queue before exiting.
2026-04-14 05:24:40 +02:00
Daniel
b5abbb69fc Add node-pg-migrate for versioned schema changes + better mobile UA labels
Infrastructure only — no existing data or tables modified.

  src/db/migrate.js           — programmatic runner, fires at boot after
                                 the existing idempotent initDatabase()
  migrations/1744600000000...  — intentionally empty example, documents
                                 the file shape. Registered in the new
                                 pgmigrations tracking table so it won't
                                 rerun.
  .node-pg-migraterc.json     — CLI config (migrations-dir, utc naming)
  docs/migrations.md          — workflow + conventions
  package.json                — migrate:up/down/new/status npm scripts
                                 (status is a direct pgmigrations query
                                 since node-pg-migrate v7 lacks a status
                                 subcommand)

src/utils/sessions.js:
  - parseUserAgent now recognizes the Capacitor wrapper (UA suffix
    "PedScribe-Android" / "PedScribe-iOS") and labels sessions
    "PedScribe (Android)" instead of "Chrome on Android".

Going forward: schema changes go in /migrations as versioned files
with up() + down(); the inline init in database.js is the implicit
baseline for everything already in production.
2026-04-14 05:06:19 +02:00