Approval state lived only in each card's local React state, so after
committing (or after reloading a saved conversation) the Add / Review &
add / Import buttons reappeared active and the same records could be
added again. Persist the resolution onto the message data part via a
chat-panel handler (setMessages), and initialize each card's status from
data.resolved so committed/discarded proposals stay locked across
re-render and history reload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The import preview only showed counts + skipped-row errors with no way to
fix anything. Now every parsed record is editable before import:
- backend: shared validatePatientImport() (used by the previewImport tool)
+ POST /api/ai/import/validate for dry-run re-validation; the import
preview payload now carries the original records (and the source record on
each invalid row) so the UI can edit them.
- frontend: the import card gets a "Review & edit" dialog listing every
record with a ready / needs-fixing badge; opening one reuses the full
PatientFormDialog in a new non-persisting review mode (editable file
number) and re-validates on save, so skipped rows can be fixed and
included.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Migration prompt now tells the agent to map/normalize an uploaded export
into temetro's shape itself (gender words, non-numeric IDs, delimited
allergy/med/problem lists, per-visit encounters) and to fix-and-re-preview
skipped rows rather than telling the clinician to reformat the file.
- Guarantee a non-empty reply: when the model ends on a tool call with no
text, ask it to summarize (external+Veil path) and fall back to a generic
line; the streaming path appends the same fallback. Raise the step cap to 8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real-world exports were rejected wholesale (file number must be digits,
sex must be M/F, every allergy/med/problem/encounter a full object), so
AI imports skipped everything. Normalize at the schema edge instead:
strip non-digits from the file number, map gender words to M/F, accept a
bare string for allergies/medications/problems (filling sensible
defaults), and default missing encounter fields (type → "Visit"). The
manual patient form shares this schema and benefits too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a real file-storage layer to the backend and wire upload UI into the
frontend.
backend:
- new `attachments` table (org-scoped, links to a patient file number and
optionally a lab result) + Drizzle migration
- `/api/attachments` route: upload (multer → disk under UPLOAD_DIR),
list, stream/download, delete; gated by patient:write OR lab:write via
a new requireAnyPermission helper so lab staff can attach analyses
- UPLOAD_DIR env (default ./uploads) + a persistent docker volume
frontend:
- lib/attachments.ts client (multipart upload, list, delete, preview URL)
- staged file picker in the patient Add/Edit dialog (uploaded after save)
and the lab Add-result dialog (linked to the result)
- a Files section in the patient sheet that lists attachments and opens
them in a preview dialog (images inline, others via download)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Enable AI assistant" toggle already disabled the AI for everyone
(including owners/admins) at the policy layer, but the copy read as an
employee-only control and the Analysis surface stayed visible. Clarify
that the master switch covers admins too, and close the gaps: hide the
Analysis nav/command entry and redirect /analysis (alongside the chat
home) to /patients when AI is disabled. The employee-only toggle remains
the secondary option that keeps AI for owners/admins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single proposal card crammed every proposed record into comma-joined
summary lines — an inventory import became an unreadable wall of text.
Render inventory/invoice records as a compact scrollable item list, and
add an Edit button (single card + per-row in the batch review) that opens
a per-kind edit dialog so the clinician can adjust the data — whole record
or individual items — before it is committed.
New RecordEditDialog is schema-driven (EDIT_SCHEMAS) per action kind,
including add/remove rows for invoice line items and inventory items.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The record graph was a cramped React Flow box wedged inside a sheet
section. Redesign it Obsidian-style — round nodes with the label
underneath, a soft glow, and a hover-focus that highlights a node plus
its neighbours while dimming the rest.
Move it out of the inline section: the sheet now shows an "Open graph"
button (closes the sheet, opens the graph in a large dialog) and a
clickable list of the patient's records (problems + visits); clicking a
record opens a detail dialog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CLINICAL_RESOURCES includes "lab" but the employee detail dialog only
mapped patient/appointment/prescription/task, so the lab row rendered as
the raw i18n key with no icon. Add a FlaskConical icon and a
"Lab results" label.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The composer's SELECT is now a clinician-facing mode picker instead of a list
of LLM vendors (the model still comes from Settings → AI and continues to drive
the Veil consent gate). The mode travels with each send:
- backend appends an Analysis/Graph directive to the agent's system prompt
- Graph mode renders an Obsidian-style RecordGraph for the /patient fast-path
(new data-recordGraph message part reusing the shared graph component)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The marketing landing page is not a separate "temetro-landing" GitHub repo and
is not in the monorepo — it lives in the sibling ~/Desktop/temetro/landing-page
folder (next to the docs site), its own git repo. Fix both the root and
frontend CLAUDE.md notes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- replace the attach "+" menu (whose programmatic fileInput.click() was dropped
by user-activation gating, so files never attached and no chip appeared) with
a native <label> paperclip + a direct appointment button
- shared-appointment cards are now clickable for both parties, opening an
AppointmentDetailDialog with the full snapshot (when/type/provider/patient/status)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- "Mark paid" button (settles invoice + all installments) when not paid/void
- per-installment Pay button + Paid badge; auto-marks invoice paid when the
last installment clears
- overdue badge on past-due unpaid installments; due-through timeframe hint
- installments + split control hidden once the invoice is paid
All via the existing PUT /api/invoices/:id (no backend change).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The realtime "Live" hospital card is dropped in favour of a six-month patient
visits area chart, aggregated client-side from real appointments. Renames the
analysis.live.* i18n block to analysis.area.* and removes the dead
live-hospital-chart component.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>