Mirror the existing POSTGRES_PORT override for the backend, frontend and adminer
host ports (BACKEND_PORT / FRONTEND_PORT / ADMINER_PORT) so a port clash on
`docker compose up -d` can be fixed via .env without editing the compose file.
Documented in .env.example and the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /api/version now reads the latest version from Docker Hub image tags (the
actual update channel clinics pull), falling back to the GitHub release if
Docker Hub is unreachable, with a shorter 1h cache and a `?refresh=1` bypass.
Settings → About & updates gains a "Check for updates" button that forces a
fresh, cache-bypassing lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Show me <name>'s medical record" relied on the model chaining
searchPatients → getPatient, but Gemini Flash often calls searchPatients
then emits the canned closing line ("Here's the record.") without the
second tool call, so no data-patientCard part is ever written and no card
renders. (The prior Gemini fix only covered the empty-schema list tools.)
searchPatients now writes the record card (data-patientCard, or
data-recordGraph in graph mode) and a source directly when EXACTLY ONE
patient matches — mirroring getPatient — so the common name-lookup flow no
longer depends on a second tool call. Multiple/zero matches keep returning
the disambiguation list. The tool description and system prompt tell the
model the card is already shown on a unique match so it doesn't double-render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The card-emitting chat tools (listAppointments, listTasks,
listPrescriptions, getClinicInfo, getAnalytics, listInventory) used an
empty `z.object({})` parameter schema. Google Gemini can't emit a
function call for a tool whose JSON schema has no properties — it prints
the call as `tool_code` text instead of invoking it — so the tool never
ran and the frontend never received the data part to render a card.
Give those tools a shared `emptyToolArgs` schema with one optional,
ignored field so the schema is non-empty and Gemini calls them. execute
bodies are unchanged; other providers ignore the extra field. This is the
same fix already documented for previewImport.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump root, backend, and frontend to 0.2.1 and add the 0.2.1 changelog section.
Patch release: pagination styling, patient-sheet header reflow, message
avatars, and CHANGELOG-based GitHub Release notes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump root, backend, and frontend to 0.2.0 and move the changelog Unreleased
notes under a dated 0.2.0 heading. Adds Patients pagination, per-patient record
history, patient summary PDF export, the messages bubble/attachment UI, the AI
import approval-card fix, and the Docker network-address fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add GET /api/activity/patient/:fileNumber (every audited change on one chart,
newest first, readable by any clinic member) and surface it as a Record
history timeline in the patient detail sheet. Add a Download summary action
that builds a clean, printable one-page clinical summary in the browser
(Save as PDF) — no PDF dependency, nothing leaves the server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/api/network reported the container's bridge IP (172.x) when running in
Docker, which surfaced as a broken/"Error" value in Settings → About &
updates. Skip container-internal interfaces (detected via /.dockerenv) so the
endpoint returns no address inside a container, and guard the panel against an
unexpected response shape — both paths fall back to the helpful
"open via the server's IP" hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give previewImport a concrete object schema (was z.array(z.unknown())) so
Google Gemini can emit a real function call — an array-of-unknown serializes
to an empty JSON schema, which made Gemini print the call as a `tool_code`
text block instead of invoking the tool. Validation stays lenient in
execute() via patientInputSchema. Also strengthen the system prompt: never
print tool calls/JSON or re-list record fields; keep prose to one sentence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retarget the release workflow, docker-compose image refs, and docs from the
placeholder temetro namespace to khalidxv (GitHub repo refs unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reference prebuilt temetro/temetro-{backend,frontend} images in docker-compose
(with a build fallback) and stop baking the frontend API URL. Add a
tag-triggered GitHub Actions release workflow that pushes both images and cuts
a GitHub Release, plus RELEASING.md, CHANGELOG.md, and a root version. Overhaul
the root and frontend READMEs (screenshot, features, prebuilt-image quickstart,
LAN access, updating).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add public GET /api/version (current version + GitHub-release update check,
cached, fail-soft) and GET /api/network (detected LAN addresses). Accept
localhost/private-LAN origins in CORS and Better Auth trusted origins via a
shared isAllowedOrigin helper, so staff can reach the app over the clinic
network. The seed-demo.ts fake test data is removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two changes so off-network wallet scanning actually works:
- Force the cloudflared edge connection over http2 (TCP/443) instead of
QUIC (UDP/7844) in both the Docker tunnel and the dev-tunnel script.
QUIC is blocked on many networks/Docker setups, which left the tunnel
stuck "Failed to dial a quic connection" and the QR pointing at a dead
URL — the root cause of "must be on the same network" failures.
- Gate the discovered quick-tunnel URL on real end-to-end reachability:
poll the tunnel's own /health until it answers (Cloudflare 1033 clears
in ~30s) before publishing it, and have /pair await that discovery so
the QR never carries a not-yet-live URL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `npm run docker:tunnel` (docker-compose.tunnel.yml overlay): a
cloudflared sidecar opens a public quick tunnel to the backend, and the
backend auto-discovers its https://…trycloudflare.com URL from cloudflared's
metrics endpoint (services/relay-url.ts) and bakes it into the wallet-import
QR — so a phone on any network can scan, with no install/account/PUBLIC_RELAY_URL.
PUBLIC_RELAY_URL still wins when set. Verified end-to-end: the backend logs
the discovered tunnel URL on startup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scan-to-connect only worked on the same Wi-Fi because the QR carried a LAN
relay URL. Give the relay a public address two ways:
- `npm run dev:tunnel` (scripts/dev-tunnel.mjs) opens a cloudflared tunnel
to localhost:4000 and starts the backend with PUBLIC_RELAY_URL set to the
public https URL, so the QR carries it and a phone on any network connects.
- Deploy configs for Fly.io (fly.toml), Render (render.yaml), and Railway
(railway.json), all building the existing Dockerfile.
Also harden resolveRelayUrl to honor x-forwarded-proto so the derived QR URL
is https behind a TLS proxy (iOS ATS requires wss).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Import from a patient app → QR" code baked the web app's own
API_BASE_URL (typically http://localhost:4000) into the pairing URI, so a
real phone could never reach the relay and "Scan to connect" silently
stalled. Use the backend's server-resolved, device-reachable relay URL
instead (PUBLIC_RELAY_URL, else the request host), surface it on the
WalletPairing type, and document PUBLIC_RELAY_URL in .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend: nullable wallet_number on wallet_share_requests (migration 0029);
createPairingRequest + POST /api/patients/wallet/pair; applyShareResponse
binds the authenticated wallet on QR (pairing) requests
- Frontend: Import dialog gains a "Show QR" mode rendering a temetro-pair QR
(react-qr-code) the patient scans; requestWalletPairing helper; i18n keys
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- auth: forgot-password now has Email/Username tabs; new public
/api/auth-helpers/reset-by-username resolves a username and hands off to the
existing reset flow (real email or admin-notify fallback)
- messages: conversations expose isSystem; System notices are read-only (no
composer, no call button) and styled distinctly (shield icon + badge)
- meetings: compact, larger control bar; self tile shows real initials and an
avatar (not black) when the camera is off; delete-room UI with confirm
- sidebar: username-only accounts show @username instead of a synthetic email
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chat: history pill now shows a History icon + a Start-new-chat (SquarePen)
button; removed the duplicate chat-history list from the sidebar.
Email: deployment-wide email provider config (Resend/Postmark/SendGrid/SMTP) in
Settings → Developers, with encrypted API key and a Send-test action. sendEmail
dispatches via the chosen provider (REST via fetch; SMTP via nodemailer).
Forgot password with no provider: alert the clinic admin(s) via a "System"
message card in Messages + a bell notification (seeded system user + per-clinic
System conversation); clicking deep-links to /settings?tab=careTeam&member=<id>.
Admins can set a member's password directly from the employee dialog
(PATCH /api/staff/:id/password via Better Auth's internal context — no admin
plugin needed).
Patient Portal: "New patient" booking path registers a demographics-only patient
then books; bookings reject double-booked slots (409).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New chrome-less (portal) route group with /portal/[clinic], a card-style choice
of "Book an appointment" vs "View my results", and step flows wired to a new
public backend router (src/routes/portal.ts):
- GET /api/portal/:clinic -> clinic name
- POST /api/portal/:clinic/appointments (verifies name+file#, books a confirmed
appointment that appears on the doctor's Appointments page)
- GET /api/portal/:clinic/results (minimal, safe status — no clinical values)
Excluded /portal from the auth proxy redirect so the kiosk loads without a
session. PHI exposure is intentionally minimal (see docs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend: GET /api/settings/records/export downloads the clinic's full patient
archive as JSON; POST /api/settings/records/import creates new patients (skips
existing file numbers, drops cross-clinic provider links). Both admin-gated.
Frontend: Records settings now has a working Export button and an Import flow
(choose file -> preview count -> apply) wired to the new endpoints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend: add tasks.assignee_user_id (nullable, fk user) + migration; persist
it through create/update and surface tasks to the assigned person in listTasks.
Frontend: New Task dialog gains a three-way Assignee control
(Myself / Department / Person) with a provider picker from /api/staff/providers;
task card + detail show the assignee's name when set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Live invite: ring a clinic member into a room (socket call:invite + a bell
notification); the invitee gets a toast with a Join action. Notifications of
type "meeting" deep-link to /messages/meetings?room=, which auto-joins.
- Scheduling: new scheduled_meetings table + /api/meetings/events (list mine,
create, delete); a Calendar tab on the Meetings page with a month picker
(meeting-day dots), the day's agenda, and a Schedule-meeting dialog
(title/date/time/participants).
- Redesign: rounded control bar with tooltips (mic/cam/screen/invite + separated
red Leave), speaking ring on tiles (Web Audio), and live room-occupancy counts
via call:presence broadcasts. Migration 0025.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Chat patient cards are now compact previews (header + key stat + "Click for
more"); cards size to content (items-start) instead of stretching to the tallest;
Edit record moved into the summary detail dialog
- AI citations render once per source per message (collapses per-word spam) and the
prompt now asks the model to cite sparingly (once per paragraph, not per list item)
- Sidebar sub-items use exact-match active state so Meetings no longer lights Inbox
- Analysis defaults to All Time, drops the 12m option, and clamps the chart window
to >=2 points so 30d/Today render instead of collapsing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend: meeting_rooms table + /api/meetings (list/create/delete, org-scoped),
and WebRTC mesh signaling over the existing authed Socket.io (call:join with a
≤4 cap and org authorization, call:signal relay, peer-joined/left, disconnect
cleanup). Migration 0024.
Frontend: Messages nav is now expandable (Inbox + Meetings); new
/messages/meetings page with a room list and a Discord-style call UI — a
useWebRtcMesh hook (camera/mic, screen share via replaceTrack, ≤4 mesh) and a
tile grid with a mic/camera/screen/leave control bar. New lib/meetings + i18n.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retrieval tools now register a PHI-free sourceId per record/list and stream a
data-source part with the real clinician-facing title; the system prompt asks
the model to cite facts inline as [[src:id]]. The chat renders those markers as
numbered inline citation chips (PreviewCard hover) via a Streamdown link
override, with a Sources footer fallback when the model omits markers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New staff_profile table (org + user, unique) holding a clinician's clinical
specialty. GET /api/staff and /api/staff/providers now include specialty; new
PATCH /api/staff/:userId (member:update) upserts it. Migration 0023.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In Graph mode, a natural-language request ("graph for John Smith") went
through the getPatient tool, which always emitted record cards — so the
clinician got Summary/Vitals cards instead of a graph (the mode prompt
even wrongly claimed the graph rendered automatically). Thread the
composer mode into the chat tools and have getPatient emit
data-recordGraph in graph mode (matching the client-side /patient
fast-path), with the cards kept for chat/analysis modes.
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 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>
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>
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>
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>