Hardware barcode scanner in the pharmacy Add-item dialog, Separated Panels
in Care team settings, and a pointer cursor on Activity rows. Also fixes
the README changelog badge, which was stuck at 0.14.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a camera barcode/QR scanner (components/scan/barcode-scanner.tsx,
@zxing/browser lazy-loaded) and a Barcode/NDC field to the pharmacy
Add-item dialog. Scanning a GS1 DataMatrix auto-fills expiry (AI 17) and
lot (AI 10 -> notes), with the GTIN (AI 01) as the barcode; plain 1D
barcodes drop straight into the field. Persists a nullable inventory
barcode column (migration 0036) through the type/validation/service, and
surfaces it on the item detail. i18n added to all locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repo had no .gitattributes at all, so nothing pinned line endings —
`git ls-files --eol backend/docker-entrypoint.sh` showed an empty attr/.
Git for Windows installs with core.autocrlf=true, which rewrites the
entrypoint to CRLF on checkout. The image then ships a script whose
shebang is "#!/bin/sh\r" and the container dies at startup with
exec /usr/local/bin/docker-entrypoint.sh: no such file or directory
naming the file it just copied in, which reads like the COPY failed.
Verified by building an image from a deliberately CRLF-ified copy of the
entrypoint: it reproduces that exact error, and passes with the sed.
Pin *.sh (and the Dockerfiles) to LF, and strip CR in the image before
chmod as well — .gitattributes only helps fresh clones, and a Windows
contributor who cloned before this commit still has CRLF on disk.
Renormalizing changes no tracked file today; this is purely a guard.
The arm64/Apple Silicon failure that prompted this is already fixed by
6127a0a (Turbopack has no musl/arm64 binding; the build uses webpack).
Confirmed by building both images on an arm64 Mac. Release publishes
linux/amd64 + linux/arm64, there are no bind mounts, no platform: keys
and no host.docker.internal, so the remaining cross-platform hazard was
just the line endings.
Also tighten .dockerignore: backend was shipping docker-compose.tunnel
.yml, fly.toml, railway.json, render.yaml, .env.example and
drizzle.config.ts into the build context; frontend was shipping
tsconfig.tsbuildinfo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A prescription written in the clinic never reached the patient's wallet.
createPrescription writes to the `prescriptions` table, but the pushed
bundle only carried `patient.medications`, which comes from the separate
`patient_medications` table and never grows when a prescription is
written. The patient approved a bundle identical to what they already
had, so the app's Prescriptions section showed nothing new — the
"New prescription: X" line in `changes` is display text, not data.
Pull listPrescriptions into the bundle alongside the appointments and
invoices that were already handled the same way, keeping the full
prescription shape rather than flattening to {name, dose, frequency}.
Also add `clinicId` (the org id) to the update event. Wallets pinned a
clinic's signing key against `clinicName`, which is mutable and falls
back to the literal "A clinic" for unnamed orgs, so unnamed clinics
collided on one pin and tripped a bogus "key changed" warning. It also
gives the wallet the relay routing id it needs to fetch document bytes.
Log the silent drops on this path: sendToWallet no-ops when an org has
no live hub, and applyUpdateResponse returned null without a word when a
response arrived unsigned or already resolved. Both looked, from the
clinic side, exactly like a patient who never tapped Approve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump root/backend/frontend to 0.14.0, move Unreleased notes under a dated
0.14.0 heading, and fix the stale README changelog badge (0.2.1 → 0.14.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- patients: move status filter to its own "Filter" row above the table
- patient sheet: name + ⋯ menu on one left row; Edit moved into the ⋯ menu
- settings: redesign sections with the COSS CardFrame surface (SettingsFrame)
- AI: add Automatic (auto-pick provider) and Off modes; default to Automatic
so a fresh install shows the setup banner until a provider is configured
- AI: fix the setup banner never showing (defaulted Ollama URL counted as
configured); add an "AI off" notice; add a fallback render for unknown chat
data parts so cards never silently vanish
- backend: include patient attachment metadata in the wallet record-update
bundle so pushed documents reach the wallet
- i18n: add mode/off/card keys across all five locales
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump version to 0.13.1 across root/backend/frontend and add the 0.13.1
CHANGELOG entry (COSS checkbox/table on the web, wallet-app home sheets).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump version to 0.13.0 across root/backend/frontend, add the 0.13.0 CHANGELOG
entry, and refresh stale CLAUDE.md docs (the AI chat is real and @ai-sdk/react
is installed; the signing/approval flow is built).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump root/backend/frontend to 0.12.1 and move the CHANGELOG notes under a dated
0.12.1 heading (centered wallet-sync stepper, timeline record history, and the
landing-page globe deferral). Adds the radix-ui dependency pulled in by the
Origin UI stepper/timeline primitives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump to 0.12.0 and record the changelog: in-dialog send-to-wallet stepper,
past-date-blocking appointment/invoice pickers, wallet-number-only portal link,
and removal of the stale Settings Features section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The portal 'link' action no longer asks the device for a name + file number.
The wallet is identified solely by its relay-verified wallet number: the clinic
attaches that number to the file ahead of time (Import from a patient app / QR
pairing), and linkWallet now just resolves the paired file (friendly 404 when
it hasn't been paired yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump root/backend/frontend to 0.11.0 and record the 0.11.0 changelog:
relay-routed Patient Portal + wallet linking, phone-ready portal QR,
appointments/invoices synced to the wallet, clinic-location reverse geocoding,
and the Arabic RTL sidebar fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- services/portal.ts: clinic-scoped portal actions (clinic info, doctors,
availability, linkWallet, conflict-checked booking, results, downloadable lab
files) plus handlePortalRequest to dispatch a relayed portal:request.
- relay-client.ts: handle portal:request on the hub and ack the result back down
the relay (device path identifies the patient by verified wallet number).
- patients: new nullable wallet_number column (+ migration); linking stores it,
and walletNumberForPatient now resolves via it so pushes work after a portal
link, not only after a permanent share.
- routes/portal.ts: GET /:clinic/link returns the relay-based pairing descriptor
(clinic signing key + relay URL) for the QR — no more localhost-baked API URL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Appointments and invoices live in their own tables, not on the Patient
snapshot, so a clinic->wallet push previously sealed only the patient record
and the patient's appointments/invoices never reached the wallet app. Load and
attach them to the sealed bundle ({ patient, appointments, invoices, changes }).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
backend: public GET /api/portal/:clinic/doctors and /availability, and
thread a chosen provider into portal bookings (conflict check unchanged).
frontend: fix site-wide Arabic RTL — anchor the sidebar right for RTL and
mirror the Switch thumb. Add a Patient Portal section (open/copy/QR) to
Settings → Signing and a "Use my current location" GPS button to the clinic
location editor. New i18n keys across all five locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Patient record:
- Add `bloodType` and `phone` to the patient model (schema, canonical types on
both backend + frontend, zod validation). `phone` is a demographic field
(reception can read/write); `bloodType` is clinical PHI, redacted for the
reception role. Surface both in the record sheet, chat summary card, and the
add/edit patient form. Migration 0033.
Clinic location:
- New org-scoped `clinic_settings` table (address/city/country + optional
lat/long), service, and routes: GET /api/clinic/settings (any clinician) and
PUT /api/clinic/location (owner/admin). Edited in Settings → Signing → Clinic
location. Consumed later by the wallet app. Migration 0034.
i18n:
- Translate all new keys into every shipped locale (en/de/fr/ar/so) and document
the "translate into every locale" rule in frontend/CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default http://localhost:8080 silently failed for clinics that joined
the network without setting RELAY_URL — inside Docker localhost is the
container itself, so the hub connection never reached the relay (endless
"relay unreachable" retries) and pairing QRs encoded an unreachable
localhost. Default to https://network.temetro.com so "Join Temetro Network"
works out of the box; self-hosters running their own relay still override
it. Also (re)ensure the hub is connected before pre-registering a pairing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
QR "scan to connect" pairing has no wallet number, so nothing registered
the request with the relay and the scanning device's response was rejected
("unknown or expired request"). The backend now pre-registers the pairing
request over the relay's new hub:expect event on POST /pair, and
re-registers still-pending requests on hub (re)connect so routing survives
a relay restart. /pair now requires the clinic to have joined the network
(clear 409, surfaced in the import dialog, localized in all five langs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The relay is now multi-clinic. Each clinic authenticates to the /hub
namespace by signing a challenge with its own Ed25519 clinic signing key
(a per-clinic identity, not a shared RELAY_TOKEN), and the relay routes
every device response back to only the clinic that originated the request
(keyed by requestId) — so clinics never see each other's traffic.
Backend:
- clinic_signing_keys.network_enabled + GET/PUT /api/signing/network
(owner/admin) to join/leave the network.
- relay-client keeps one authenticated hub connection per network-enabled
org (connectOrg/disconnectOrg, hubs map keyed by orgId); emitToWallet/
sendToWallet take orgId; offline flush is org-scoped.
- Wallet import/push return 409 until a clinic joins.
- RELAY_TOKEN is now optional/legacy (open relay needs no shared secret).
Frontend:
- "Join Temetro Network" toggle in Settings → Signing, localized in all
five languages (en, fr, de, so, ar).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Devices no longer connect to the backend directly. The /wallet Socket.io
namespace is removed from realtime.ts; a new services/relay-client.ts connects
to the standalone Temetro Network relay's /hub namespace (RELAY_TOKEN-auth),
emitToWallet delegates to its sendToWallet, and device responses + wallet:online
replay are handled there via the same wallet-share/wallet-updates services.
- Add RELAY_URL + RELAY_TOKEN env (env.ts, .env.example, docker-compose.yml);
the wallet-import QR (resolveRelayUrl) now points at RELAY_URL.
- Add socket.io-client dependency.
- Document the Temetro Network folder/service in root + backend CLAUDE.md.
- Bump root/backend/frontend to 0.7.0; CHANGELOG entry.
The relay service itself lives in github.com/temetro/temetro-network.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose temetro's own records as a read-only FHIR R4 server at /fhir,
authenticated with per-clinic API keys (tmf_… bearer tokens, SHA-256
hashed, shown once). Serves Patient, Observation (labs + vitals),
AllergyIntolerance, Condition, MedicationRequest, Encounter and
Appointment as text-only CodeableConcepts (temetro stores free-text
clinical values); CapabilityStatement at /fhir/metadata (unauth).
Searchset Bundles with _count/_offset pagination and self/next/prev
links; every request is org-scoped and written to the activity log.
Keys are created/revoked under Settings → Integrations (owner/admin).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A clinician can push an updated record to a wallet-linked patient (permanent
share). The snapshot is signed with the clinic Ed25519 key and sealed to the
wallet's X25519 key — derived from its Ed25519 wallet number via the birational
map, verified byte-for-byte against the wallet's own derivation. Stored pending,
delivered over the /wallet relay live and on the wallet's next authenticated
connect (offline catch-up). The patient approves/denies in-app; the wallet signs
its decision, the backend verifies it, and the record is replaced only on
approval. Wallet pins the clinic key (TOFU) and warns on change.
Backend: walletRecordUpdates table + service, ed25519PubToX25519Hex helper,
POST /api/patients/wallet/push, GET .../link/:fileNumber|updates|updates/:id,
wallet:update-request / wallet:update-response relay events.
Frontend: "Push to wallet" dialog with live status, wallet-link gating on the
patient sheet, "Sent updates" list under Settings → Signing, walletPush /
walletUpdatesList locale namespaces across all five languages. Bumps to v0.5.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record a clinician↔patient visit (or paste a transcript) on the patient
sheet; the backend transcribes it (OpenAI Whisper / Gemini), de-identifies
the transcript + context through Veil, and drafts a structured SOAP note
the clinician reviews and edits before saving — the same write-approval
gate as the chat agent.
Backend: POST /api/scribe/{transcribe,draft,save} (routes/scribe.ts,
services/ai/transcribe.ts), veil.redactText() free-text redactor,
appendEncounter service, audio MIME types on attachments. Gated by
patient:write + the clinic AI policy (reception/disabled-AI excluded).
Frontend: ScribeDialog + lib/scribe.ts, "Record visit" on the patient
detail, gated by clinical access + AI availability. New `scribe` locale
namespace across all five languages. Bumps to v0.4.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add three UI locales (so/ar/de) with full ~1,660-key translations alongside
en/fr, selectable in Settings → Profile. Arabic gets full right-to-left support:
- config.ts registers the locales and exports a `dirFor` helper; an inline
<head> script in layout.tsx sets <html dir/lang> before first paint (no RTL
flash), and i18n-provider keeps them in sync on language change.
- ~160 physical direction utilities converted to logical (ms/me/ps/pe/
start/end/text-start/text-end); directional chevrons/arrows get rtl:rotate-180;
chat-bubble align variants fixed to logical.
- IBM Plex Sans Arabic appended to the sans/heading font stacks for
per-character Arabic fallback.
- Language persists to the backend user_settings and re-applies on sign-in so it
roams across devices (localStorage stays the offline source of truth).
- New scripts/check-locales.mjs (npm run check-locales) enforces key/placeholder
parity and Arabic CLDR plural completeness.
Bump to 0.3.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>