- deletePatient() lib fn (DELETE /api/patients/:fileNumber already existed)
- confirmed delete button in the patient sheet (full-clinician only)
- aggregate appointments/prescriptions/invoices into the sheet (by file #)
- new shared RecordGraph (@xyflow/react + d3-force) linking problems↔visits,
added as a "Record graph" section + reusable by AI Graph mode
- i18n keys for delete + new sheet sections
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The file pickers read event.target.files but reset event.target.value = ""
before the file was actually used: in the AI composer the value reset ran
before React's deferred setState updater read the (now-empty) live FileList,
so no chip appeared; in Messages the reset ran before the length check, so it
returned early. Snapshot the files into a plain array synchronously, before the
reset. This is why attaching seemed to fail (no badge) — especially while typing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "+" attach control called fileInputRef.current.click() programmatically.
When the textarea is focused with text, clicking "+" blurs it first and the
browser drops the programmatic picker-open (user-activation gating), so no file
could be attached while typing. Replace it with a real <label> wrapping the
file input — native label activation always opens the picker, regardless of
focus/text state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- chat composer: attach works with text (sr-only file input); lighter
bordered input box
- messages: defer "+" menu actions past close so file/appointment pickers fire
- tasks: top-level New task button
- chat: condensed appointment card with "View in calendar" deep-link;
appointments page auto-opens the month calendar from ?calendar=&date=
- lab: work-queue rows expand to show task detail + complete
- inventory: clickable item detail dialog
- pharmacy: Dispense action records a dispense + "Recently dispensed" feed;
expiring badge uses endDate and only flags <=2 days left
- prescriptions: keyboard nav in patient search, inventory-backed medication
combobox (free-text fallback), optional start/end dates; thread dates through
- sidebar: whole nav scrolls as one
- i18n keys for all new strings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add optional startDate/endDate columns to prescriptions (schema,
validation, types, service) — when set, endDate drives expiry.
Add a new append-only `dispenses` resource (schema, types, validation,
service, route at /api/dispenses) recording who received which medication,
gated on the existing inventory RBAC statement. Migration 0020.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- new org_ai_policy table + GET/PUT /api/ai/policy (read for any member,
write owner/admin only); migration 0018
- /api/chat hard-blocks (403) when AI is off for the caller
- Settings → AI "Availability" section: enable AI, or disable for
employees only (owners/admins keep access); read-only for non-admins
- sidebar, command palette and route guard hide/redirect the AI chat
when it's disabled for the current user
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add a `status` field to tasks (backend schema/validation/service/types
+ frontend types), kept in sync with the legacy `done` flag
- migration 0017 adds the column and backfills done tasks to "done"
- rewrite the tasks page as a three-column board: per-column add buttons,
draggable cards, and a status mover in the detail sheet
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chat fixes (#1/#6/#7):
- chat-input passes raw File[] up instead of inlining file text
- chat-panel sends files as FileUIPart and renders them with the
ai-elements Attachments component (no more raw text dumps)
- backend extracts text-like file content for the model in routes/chat
- Chain-of-Thought now defaults to collapsed
- file upload works regardless of whether the input has text
AI add-to-inventory (#2):
- new proposeInventory tool (validates items, streams an approval card)
- system prompt distinguishes stocking inventory vs. billing a patient
- ActionPreviewCard commits inventory via POST /api/inventory
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
real Live card, persistent chat history
Analytics & earnings:
- Analytics now carries real money computed from invoices (billed/paid/
outstanding + by-month); new Earnings section on the Analysis page drawn with
the project's Bklit chart components (shared EarningsChart).
AI agent reaches the whole clinic:
- new read tools getClinicInfo / getAnalytics / listInventory render clinic,
analytics (with a Bklit earnings chart) and inventory cards in chat
- proposeInvoice turns an uploaded purchase/medication list into an invoice
(new "invoice" action-preview kind → createInvoice); invoices/appointments
auto-create/link a patient (ensurePatient) so they hit the Patients page
Live card:
- plots real data — patients checked in today — via GET /api/analytics/live
(polled); value pill clamped and margins widened so nothing spills the card
Persistent AI chat history (Claude-style):
- ai_chat_threads + ai_chat_messages (migration 0016); per-user, org-scoped
thread CRUD under /api/chat/threads
- chat panel owns a thread id, loads /?thread=<id>, and auto-saves after each
exchange; sidebar lists past chats (open/delete), "New chat" starts fresh
Verified with backend typecheck + frontend tsc + next build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Chat input toolbar was clipped in the empty state: the form's overflow-hidden
made its flex min-height resolve to 0, so the vertically-centered layout
squeezed it and hid the bottom toolbar (attach/add-patient/model/mic/send).
Add `shrink-0` to the form; let the empty-state column scroll.
2. AI-imported appointments now create/link a patient: services.ensurePatient
reuses a same-name patient or creates one (auto file number, source "ai");
appointments.createAppointment calls it when the booking has no file number,
so imported people appear on the Patients page.
3. Many proposals collapse into one BatchActionPreviewCard → a review dialog
with per-row remove and "Add all" (commits sequentially), instead of one
Add/Discard card per record.
Plus: widen the Live chart right margin so the value pill stays inside the card.
Verified with `next build`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ai-elements `reasoning.tsx` imports `streamdown`/`@streamdown/*`, which are
not installed (it's a dormant component). Importing it into chat-panel made the
whole chat fail to render — taking the input toolbar (attach, add-patient, model
picker, mic, send) down with it.
Render the reasoning block with a self-contained COSS Collapsible + Shimmer
instead (no streamdown). The other AI-elements I wired in (tool, queue,
suggestion, shimmer) have their deps installed and stay. Verified with a full
`next build`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the existing ai-elements into the chat:
- collapsible tool-call cards (name, params, result, status) for native tool
parts on the streamed path
- a reasoning block (shimmer while thinking, "Thought for Ns") when the model
emits reasoning; backend forwards it via toUIMessageStream({ sendReasoning })
- a message queue: typing + Enter while the assistant is responding queues the
message (shown above the input, removable) and auto-sends when it goes idle
- starter suggestion chips on the empty state, each tied to a tool
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend (new `invoice` RBAC resource, granted to clinicians + reception):
- invoices table (line items + installments as JSONB), types, zod validation,
service (CRUD + splitIntoInstallments + auto invoice numbers), org-scoped
REST routes mounted at /api/invoices, activity logging (migration 0015)
Frontend:
- lib/invoices.ts API client + money/date helpers
- /invoices page: list with KPIs and search, create/edit dialog (searchable
patient combobox, inline line-item editor, live total), detail sheet to split
a bill into equal monthly installments, delete, and Download PDF
- dependency-free PDF via a print-styled window (browser "Save as PDF")
- sidebar "Invoices" entry under the Patients group; "Added by AI" badge honored
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- the inline chat error now has an X to dismiss it, and is the single surface
for errors (dropped the duplicate toast) so failures show once and clearly
- the import preview card caps the skipped-rows list (scrolls, one line each
with a "+N more" tail) so a file full of validation errors no longer renders
a giant card
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Live card no longer streams on mount: a "Go live"/"Pause" toggle starts/stops
the 1s simulation, so the page stays idle by default (perf). Widen the live
chart's left margin so the y-axis values sit inside the card.
- Weekly-appointments bar chart: drop the misused <BarYAxis>, which printed the
weekday categories down the left and overflowed the card (vertical bars only
need the x-axis).
- Patient-growth area chart: one x tick per month so every point is labelled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- new reusable <Combobox> (Base UI Autocomplete): arrow-key + Enter navigation
- New Appointment dialog: patient search and provider are now searchable
comboboxes (provider sourced from /api/staff/providers) instead of a hand-
rolled dropdown and a free-text input
- appointment rows are clickable and open an <AppointmentDetailSheet> to edit
date/time/type/provider/status or delete; "Added by AI" badge shown on rows
and in the sheet for source="ai" records
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stop blocking AI imports/proposals on missing non-critical fields. Records
the chat agent drafts now save with safe placeholders, auto-generated file
numbers, and a source="ai" marker that surfaces an "Added by AI" badge so a
clinician can review/edit them later.
Backend:
- add `source` (manual|ai) column to patients/appointments/prescriptions
(migration 0014) + canonical types, services, validation schemas
- relax patient/appointment validation: empty file number allowed, demographic
+ type/provider/initials fall back to placeholders (initials derived from name)
- patients.generateFileNumber() auto-assigns an MRN when one is missing
- proposeAppointment accepts a name when no file number resolves; AI commits +
/api/ai/import stamp source="ai"
Frontend:
- `source` on Appointment/Patient/Prescription types; AI commits send source="ai"
- reusable <AiBadge> shown on the Patients table/detail and prescriptions list
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The placeholder only mentioned patient lookup, but the chat now answers
plain-language questions, displays records, adds, and imports. Update it to
"Ask anything, or type /patient 10293".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an Add-item button to the Inventory page that opens a COSS form dialog
(name required; form, strength, stock, reorder point, location, expiry). On
submit it persists via createInventory and prepends the saved row so it
appears immediately. Mirrors the appointments add-dialog pattern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the chat feel alive and let the agent act on the clinic — safely.
UX (frontend):
- Stream `data-step` parts from each tool into an inline Chain-of-Thought
trace, plus a "Thinking…" shimmer while a request is in flight (works even
on the non-streamed external+Veil path).
- Replace the modal Veil consent Dialog with an inline, once-per-session
"Veil" confirmation above the input (with a "Use local model" option).
- Render new list + action-preview cards; chat-input now uses COSS tokens.
Agent (backend):
- Add display tools (listAppointments / listTasks / listPrescriptions) and
propose tools (proposeAppointment / proposeTask / proposePrescription) that
validate as a dry run and stream an approval card — nothing is written until
the clinician approves, via the existing RBAC-gated create endpoints.
- previewImport now also covers single-patient add + migration.
- System prompt: display + add only, never edit/delete or alter the schema;
stronger migration guidance. ToolContext carries the viewer for task scoping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chat defaulted to a Claude model, so a user who saved only a Gemini key got
a silent failure (backend derived Anthropic, found no key, errored — and the UI
showed nothing).
- Backend: resolveModel now falls back to whichever provider actually has a key
(preferring the configured one), so a Gemini key just works regardless of the
picked model. Clear 400 if no provider is configured at all.
- Frontend: the chat seeds its model/effort from the saved AI config, and now
surfaces request failures as both a persistent alert banner and a toast —
never silent.
- Settings: switching provider auto-selects that provider's default model.
- Refreshed the model catalog to current ids (Gemini 2.5 Pro/Flash + 2.0 Flash;
dropped the retired gemini-1.5-pro); per-provider default model updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`docker compose up` now works with no .env editing. A new entrypoint
(docker-entrypoint.sh) generates any missing secret (BETTER_AUTH_SECRET,
AI_CREDENTIALS_KEY) on first start and persists it to the temetro_secrets
volume, so values stay stable across restarts (rotating them would log users
out / invalidate stored AI keys). Real values passed via .env/compose still win.
This also fixes the boot failure where the compose backend ran with
NODE_ENV=production but never passed AI_CREDENTIALS_KEY through, tripping the
production guard. The secret is now an optional pass-through and BETTER_AUTH_SECRET
no longer hard-fails when unset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The frontend dependency install (1000+ packages) failed in Docker with
ECONNRESET / "network aborted" mid-stream. Add npm fetch retries and a long
timeout so a transient registry drop is retried rather than failing the build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The build stage ran `npm ci`, which tried to compile better-sqlite3 — a
dev-only transitive dependency of @better-auth/cli — and failed in Alpine for
lack of Python/node-gyp. The build step is just `tsc` (no native binaries
needed) and better-sqlite3 is never used at runtime (the prod stage omits dev
deps), so install dev deps with --ignore-scripts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the mock chat with the live backend agent:
- chat-panel uses @ai-sdk/react useChat against /api/chat (credentials included),
passing the selected model + effort per send. The `/patient <file#>` fast-path
is kept as an instant client-side shortcut.
- Renders the agent's streamed data parts: patientCard → record cards
(PatientResult), labCard → new LabChartCard (visx area chart with a high/low
flag badge in the top-right corner + recent values), importPreview →
ImportPreviewCard (the human approval gate: review counts/issues, then commit
via POST /api/ai/import — nothing is written until approved).
- Veil consent: a one-time dialog before the first send to a cloud model,
explaining that identifiers are de-identified before leaving the clinic.
- Attached text files (csv/json/txt…) now include their content in the message
so the agent can parse an export for import.
Shared chat message/data-part types in lib/ai-chat.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New per-user AI settings section (visible to every clinician, not just admins):
- Inference mode: cloud API key vs local Ollama.
- Provider + key (OpenAI / Anthropic / Gemini) via a select; the key field is
write-only and shows a "key set" indicator from the backend's apiKeySet,
never echoing the stored secret. Default model + effort per provider.
- Local Ollama: base URL + model name with a "Test connection" probe.
- Veil: de-identification level (full / names / off) with mode-aware copy.
Talks to the new /api/ai/config + /api/ai/test endpoints via lib/ai-settings.ts.
The old mock clinical selects (specialty/facility/time range/access/response)
are dropped entirely rather than relocated — they were placeholder knobs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real LLM chat replacing the mock, backend-centric per the plan:
- Multi-provider API-key mode (OpenAI / Anthropic / Gemini via the AI SDK) plus
local Ollama (OpenAI-compatible endpoint). Provider is derived from the
picked model id; the matching stored key is used. New user_ai_settings table
holds per-user config with provider API keys encrypted at rest (AES-256-GCM,
src/lib/crypto.ts, keyed by AI_CREDENTIALS_KEY).
- POST /api/ai/config (get/put, secrets never returned), POST /api/ai/test
(Ollama ping / key presence), POST /api/ai/import (approved migration commit,
re-validated server-side, reuses the audited patient service).
- POST /api/chat: streamText agent with tools (getPatient, getPatientLabs,
searchPatients, previewImport). Real record data streams to the clinician as
custom data parts (cards) while the model sees only Veil-redacted results.
- Veil (src/services/ai/veil.ts): de-identifies patient identifiers to tokens
before external calls, resolves tokens on tool args, and rehydrates the final
answer. Bypassed for local Ollama. External mode runs non-streamed so the
rehydrated text is correct. Every call is audited (provider + Veil level).
- Shared role-scoping helpers extracted to src/lib/role-scope.ts (reused by the
patient routes and chat tools so visibility rules match).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the five clinical context selects (access, response mode, specialty,
facility, time range) with one model picker styled like the reference design:
a primary model list with descriptions, an Effort submenu, and a "More models"
submenu. Move the Add-patient pill up into the toolbar and drop the now-empty
bottom context-selector card so the input is a single clean rounded surface.
Model/effort selection is lifted to ChatPanel so it can travel with each send
(wired to the backend agent in a later phase). Adds a shared model catalog
(lib/ai-models.ts) reused by the picker, Settings, and the chat wiring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard the per-org count lookup so the build (tsc -p) doesn't fail on the
possibly-undefined first row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pharmacy: the sidebar entry now expands into Pharmacy + a new Inventory page
(searchable medication stock with derived in-stock/low/out availability,
backed by /api/inventory). Fix the dispensing-queue "Expiring" badge so it
only flags courses ending within the next 7 days, not ones already elapsed.
- Lab "Add analysis result": the patient picker no longer lists patients until
you type and supports arrow-key + Enter selection; the test field offers a
catalog of common analyses with an Advanced option (custom analysis + ref
range); submitted results now show immediately in a Recent results feed.
- Analysis: add a Live panel (real-time line chart) and replace the flat trend
sparklines with bklit-ui area + bar charts (installed from the @bklit shadcn
registry; chart CSS variables wired into the theme for light and dark).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CommandItem rendered the icon flush against the label. Add a gap (and normalize
icon sizing) so the two no longer touch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an `inventory` resource mirroring the prescriptions feature end-to-end:
Drizzle table (org-scoped, indexed), domain type, zod validation, service
(list/get/create/update/delete), and an RBAC-gated CRUD router mounted at
/api/inventory. Grant the new `inventory` statement to roles — pharmacy gets
read/write, full clinicians read/write/delete, reception/lab none — in both the
backend access control and (mirrored) the frontend. Record writes in the
activity log via a new `inventory` entity type. Includes the migration and an
idempotent demo seed script.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same layout, better inbox: avatars on conversation rows, a numeric unread
badge (kept live by the socket handler), primary-tinted timestamps for unread
rows and softer hover states. In the thread: day separators
(Today/Yesterday/date), consecutive same-sender messages within 5 minutes
grouped with one sender label + one timestamp and tightened corners, and a
"Start a conversation" button on the empty state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Count messages from others newer than the caller's read pointer alongside the
existing last-message lookup, expose it as unreadCount on
ConversationSummary, and derive the unread boolean from it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Myself/Other assignee picker looked like two loose buttons; replace it
with the COSS Tabs segmented control, with the department select (now five
departments) living in the "Other" tab panel. State semantics unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New /lab department home: the lab's task queue (tasks assigned to the Lab
department, with done toggle) and an "Add result" dialog — pick a patient,
enter test/value/flag/date — that posts to the new labs append endpoint via
appendLabs() in lib/patients.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New /pharmacy department home: KPI cards (active prescriptions, expiring
within 7 days from parseable durations, patients on medication) over a
dispensing queue of active prescriptions (oldest first) with search, a
mark-completed action and the existing detail sheet. No create/delete UI —
prescribing stays with clinicians.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lab staff hold lab:write but not patient:write, so they can't go through the
wholesale PUT update. The new endpoint appends lab rows (positions continue
after the current max), bumps updatedAt, records activity and notifies the
clinic — without touching the rest of the record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the backend role changes (drop viewer, add pharmacy + lab and the lab
statement). Replace the binary requiresClinical gating with access areas
(clinical / pharmacy / lab) probed from Better Auth permissions —
prescription:delete marks full clinicians, prescription:write the pharmacy
area, lab:write the lab area — so pharmacy doesn't inherit the AI chat or
notes. Pharmacy and lab land on their new dashboards and get their own nav
items; reception behavior is unchanged. Includes the i18n keys for the new
pages and the messages polish that lands in the following commits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the unused read-only "viewer" role and remap any members (and pending
invitations) holding it to "member" via a custom migration. Add a "lab"
statement (read/write) granted to all full clinicians, plus two new
provisionable department roles: "pharmacy" (patient/appointment read,
prescription read+write — no delete, which doubles as the full-clinician
marker the frontend probes) and "lab" (patient/appointment read, task queue,
lab results via the new statement). Both join TASK_DEPARTMENTS so tasks can
be assigned to them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- sidebar + auth logo now invert to black in light mode (dark unchanged),
same trick the docs site uses — no second asset needed
- prescriptions page: search by patient, file number, medication,
prescriber or status, with no-match / empty states
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Records tab: data sources, import/export, retention (replaces placeholder)
- Developers tab: API base URL, access-tokens empty state, resources
- Signing tab: how-it-works steps + backup key entries
- Team member dialog: header band, per-resource permission rows with icons
and action chips, COSS Select for role change
- Care team: hint that clicking a member shows their permissions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ToggleRow supports controlled checked/onCheckedChange; CopyField copy works
- ProfilePanel loads/saves preferences via /api/settings, name via updateUser
- dirty tracking with a sticky save bar that follows the scroll
- Delete account: password-confirmed dialog wired to authClient.deleteUser
- clinician ID / handle now show the real session user
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Messages: search the inbox and the compose member picker.
- Care Team: clickable member rows open an employee dialog showing role +
permissions, with change-role (updateMemberRole) and remove.
- Patients: Primary Care is now a provider dropdown (defaults to self for a
doctor); add a Transfer action + dialog wired to the transfer API.
- Activity: entries are clickable, opening a detail dialog.
- Analytics: Section takes a columns prop so each row fills evenly (no orphan
card in Appointments).
- Add lib/staff.ts (listProviders), transferPatient client, rolePermissionSummary
helper, and i18n keys for all new strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add patients.primary_provider_id (FK to user) + migration; persist it through
create/update and surface it on the Patient shape.
- Scope patient list/get for the `doctor` role to their own panel (with a
createdBy fallback for legacy rows); admin/owner/member/reception/viewer keep
seeing every patient.
- Add POST /api/patients/:fileNumber/transfer to reassign a chart (updates the
provider link + PCP label, records activity, notifies the clinic).
- Add GET /api/staff/providers (any member) listing clinical-capable members for
the PCP picker and transfer dialog.
- Scope the activity feed: non-admins see only their own actions; owners/admins
see the whole clinic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>