Skip to content

API + Agent layer, scheduling overhaul, and clinical depth#2

Merged
evangauer merged 25 commits into
mainfrom
feat/api-v1-compat-layer
Jun 2, 2026
Merged

API + Agent layer, scheduling overhaul, and clinical depth#2
evangauer merged 25 commits into
mainfrom
feat/api-v1-compat-layer

Conversation

@evangauer
Copy link
Copy Markdown
Owner

@evangauer evangauer commented Jun 2, 2026

Summary

A platform push toward an on-par-with-the-leaders PIMS plus the foundation of an agent-led product. 24 commits, 119 unit tests, full monorepo type-check + build green. Grounded in our user conversations: practices won't rip-and-replace their PIMS, so the wedge is "a second PIMS you own" + an API/agent layer, and the calendar needed to be trustworthy (per advisor feedback).

What's included

Platform / API

  • Public, versioned REST API (/api/v1) for clients, patients, appointments, with scoped, per-practice API keys (bcrypt + indexed prefix lookup, scopes, per-key rate limiting). Response shapes frozen independently of the DB via a mapper layer.
  • OpenVPM Agent — typed tool registry + Claude tool-use runner (prompt caching, write-gating, graceful no-key degradation). Available in-app (/agent console) and over the API (POST /api/v1/agent, agent:run scope). Tools: find clients/patients, patient summary, list/find-open-slots, book appointment, overdue vaccinations, dosing, list treatment plans, record vitals.

Scheduling / calendar (addresses "calendar not up to snuff")

  • Fixed a real bug: back-to-back appointments were falsely flagged as conflicts (non-strict overlap).
  • Room double-booking detection (was doctor-only); reschedule-aware conflict engine.
  • appointments.reschedule (move time/doctor/room) and an open-slot availability engine + availableSlots query; portal booking suggests open times.

Clinical depth

  • Weight-based drug dosing calculator (curated formulary, guard rails).
  • Vital signs (+ patient-detail Vitals tab), treatment plans linked to the problem list (with progress), inventory dispensing (stock decrements on invoiced products).

Revenue / data

  • Wellness plans / recurring billing (plans, enrollments, due-charge date math; charge capture deferred to a payment processor).
  • CSV import for clients/patients (lowers switching cost; pairs with existing JSON export).
  • Client portal self-service online booking.

Marketing site

  • "Don't switch. Connect." data-ownership positioning + Agent callout; new /updates changelog; email capture wired.

Testing

  • Added Vitest (first unit runner) wired into turbo + CI; pure logic (mappers, auth, dosing, scheduling, CSV, billing dates) is unit-tested.

Migration notes

  • Additive schema changes — run pnpm db:push on deploy: api_keys.keyPrefix, vital_signs, treatment_plans (+ items), wellness_plans, wellness_enrollments.
  • Optional env: ANTHROPIC_API_KEY (+ AGENT_MODEL) to enable the agent; everything else works without it.

Not included (needs credentials / approval)

Stripe (payments + wellness capture), Twilio/Resend (reminder sending), IDEXX/Antech (labs), Digitail live-mirror (requires Digitail partner approval + DPA).

Verification

pnpm test (119 passing), pnpm type-check, pnpm build all green. Live DB round-trips pending pnpm db:push against a real Postgres.

🤖 Generated with Claude Code

evangauer and others added 25 commits April 20, 2026 18:51
Visits the public GitHub repo in a clean browser context and verifies
first-time visitors see the expected repo landing state — README images
load, CI badge is present, no page errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apps/web had only Playwright e2e; pure logic (mappers, auth helpers) had no
unit coverage. Adds vitest with a node environment and a manual @/ alias,
a 'test' script, the turbo 'test' task, and a CI step so unit tests run on
every push/PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The apiKeys table existed but was never connected to any auth path. Adds:

- An indexed key_prefix column so a raw key can be narrowed to a small
  candidate set before a constant-time bcrypt compare (bcrypt hashes can't
  be looked up directly).
- lib/api-auth.ts: parse Bearer/X-API-Key, verify, enforce scopes and a
  per-key rate limit (reusing lib/rate-limit.ts), return a tenant context.
- An admin api-keys tRPC router (create/list/revoke) that returns the raw
  key exactly once and stores only the bcrypt hash.
- Unit tests for key generation, header extraction, and scope checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A versioned public REST surface for third-party integrators, built as thin
Next.js route handlers that query Drizzle directly and pass rows through a
pure, isolated mapper layer. Response shapes are owned by an explicit Zod
contract and frozen independently of the internal schema.

Endpoints: GET clients, GET clients/:id, GET patients (with ?client_id),
GET patients/:id, POST appointments. The appointment write fires the
previously never-called appointment.created webhook. Every query is scoped
by practiceId + isNull(deletedAt); access is gated by scoped API keys.

Mappers normalize internal vocabulary to integrator-friendly shapes
(species canine->dog, sex enum split into sex + neutered). Covered by
mapper unit tests and contract tests asserting output satisfies the schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds docs/api/README.md (auth, scopes, rate limits, error format, endpoints),
a README section surfacing the API, and a CONTRIBUTING guide for adding
compatibility endpoints/targets plus the unit + e2e testing workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes a clinical-depth gap vs leading PIMS (ezyVet/Provet have dosing
calculators; OpenVPM had none). Adds a pure calculator engine + a curated
starter formulary (8 common drugs with species-specific reference ranges),
exposed via a dosing tRPC router (formulary + calculate).

- Weight-based mg range, optional injectable volume from concentration,
  practical tablet-split suggestions, and a hard max-single-dose cap.
- Guard rails: rejects non-positive/implausible weight, unknown drugs, and
  refuses to extrapolate across species. Every result carries a verify-before-
  prescribing disclaimer and drug-level warnings (e.g. carprofen not for cats).
- 14 unit tests covering math, capping, tablet logic, and guard rails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clinical-depth gap: OpenVPM tracked weight only. Leading PIMS (Cornerstone,
Instinct) capture full vitals per visit. Adds a vital_signs table (temp, HR,
RR, weight, body condition score, pain score, mucous membrane, CRT, notes)
with patient/appointment/recorder links, plus a vitals router (listByPatient +
record) gated to clinical roles with sane physiologic input bounds.

Note: additive schema change — run pnpm db:push on deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The foundation for an agent-operated practice. An AI agent can now read and
act on practice data through a typed tool registry, always scoped to one
practice:

- lib/agent/tools.ts: 6 tools (find_client, get_patient_summary,
  list_appointments, book_appointment, list_overdue_vaccinations,
  calculate_drug_dose) each with a JSON schema for the model + a Zod schema
  for runtime validation. Write tools are flagged.
- lib/agent/runner.ts: Claude tool-use loop (Anthropic SDK) with prompt
  caching on the system prompt + tool defs, write-gating (allowWrites,
  default false), max-iteration guard, and graceful degradation when
  ANTHROPIC_API_KEY is absent.
- agent tRPC router (status + run), gated to admin/vet.
- 8 tests: registry integrity, read/write flagging, the pure dosing tool,
  and validation guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reframes the pitch around the validated insight that practices won't
rip-and-replace their PIMS. New homepage section positions OpenVPM as the open
data layer you connect to your current system — a live, exportable copy of your
own data you control — plus an OpenVPM Agent callout (operates the practice over
the open API, every write gated, scoped to your data).

Adds an /updates changelog page to push product notes out (API v1, the Agent,
dosing + vitals, the new direction), wired into nav, footer, and sitemap. Email
capture via the existing waitlist is linked from the updates CTA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashboard page (admin/vet) to run the OpenVPM Agent: instruction box with
suggested prompts, an opt-in 'allow writes' toggle, the agent's answer, and a
collapsible tool-call trace so staff can see exactly what it did. Shows a clear
setup notice when ANTHROPIC_API_KEY isn't configured. Added to the sidebar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
README: OpenVPM Agent subsection (tools, scoping, write-gating, BYO key).
.env.example: ANTHROPIC_API_KEY + optional AGENT_MODEL with a note that the
app works fully without them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit gap: stock quantities were never decremented when products were sold,
so inventory drifted from reality. Adds a pure computeStockDeductions helper
(aggregates product lines, ignores services and unlinked lines) and wires it
into createInvoice: real (non-estimate) invoices now decrement each product's
stock, clamped at zero so it never goes negative. 5 unit tests on the helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the #1 audit gap (no self-service booking). Clients can now request an
appointment from the portal: pick a pet, choose an appointment type, and a
preferred date/time. The request creates a real 'scheduled' appointment on the
practice calendar (flagged '[Portal request]') for staff to confirm, mirrors
into the communications inbox, and fires the appointment.created webhook.

- pure buildRequestedSlot helper (date/time validation, duration-based end,
  no-past-dates) with 5 unit tests.
- portal.getAppointmentTypes query + upgraded portal.requestAppointment
  (type-aware, ownership-checked, rate-limited 5/hour per portal link).
- New /portal/[token]/book page + 'Request appointment' button on the
  portal appointments page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ties the API-compat layer and the agent together: an external automation (or
an agent-led ops layer) can drive the practice through one natural-language
endpoint. POST /api/v1/agent, authenticated by an API key with the new
agent:run scope, runs the agent scoped to that key's practice. allow_writes
(default false) gates write tools; returns the answer plus a full tool-call
trace, or 503 when no ANTHROPIC_API_KEY is configured.

Documented in docs/api with the new scope and endpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clinical-depth gap: notes existed but there was no structured treatment
planning. Adds treatment_plans (linked to a patient and optionally a problem)
+ treatment_plan_items (ordered interventions with status), a treatmentPlans
router (listByPatient with a progress summary, create-with-items, updateStatus,
updateItemStatus — all practice-scoped and clinical-role gated), and a pure
summarizePlanProgress helper (skipped items excluded from the denominator)
with 5 unit tests.

Note: additive schema — run pnpm db:push on deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lowers switching cost — practices can migrate from their current PIMS using
the common CSV export format instead of hand-building JSON. Adds a pure,
dependency-free CSV parser (quoted fields, embedded commas/newlines, escaped
quotes, CRLF) and typed row mappers with normalized header matching
(First Name == first_name), returning valid records plus per-row errors for a
partial import. Wired into data.importClientsCsv / importPatientsCsv (admin),
reusing the existing insert + email->client linking. 8 unit tests.

Pairs with the existing JSON export in data.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pushes the agent-led thesis further by letting the agent use the clinical
features just shipped. Adds list_treatment_plans (read, with progress summary)
and record_vital_signs (write, gated; recordedBy left null since the agent
isn't a user row). Registry test updated for the new write tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recurring-revenue parity feature (ranked #3 in the audit). Adds wellness_plans
(plan definitions: price + monthly/annual interval) and wellness_enrollments
(client/patient, status, nextBillingDate). Pure date math
(computeNextBillingDate with month-end + leap clamping; enrollmentsDueOn) with
9 unit tests. wellness router: listPlans, createPlan, enroll, listDue (active
enrollments due on/before a date), markBilled (advances the next billing date),
cancel — practice-scoped and role-gated. Charge capture is deferred until a
payment processor is wired; markBilled advances the schedule.

Note: additive schema — run pnpm db:push on deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Makes the vitals feature usable by clinicians where they work. Adds a Vitals
tab to the patient detail page with a compact record form (temp, HR, RR,
weight, BCS, pain, notes) and a history table, mirroring the existing
self-contained-tab pattern (trpc.vitals.listByPatient + record, with cache
invalidation and toasts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses Adam's note that the calendar wasn't up to snuff. Two real defects:
back-to-back appointments (one ends exactly when the next starts) were flagged
as conflicts because the overlap check used non-strict comparisons, and only
doctor conflicts were checked — a room could be double-booked freely.

Adds a pure, tested conflict engine (lib/scheduling/conflicts.ts): strict
interval overlap, doctor AND room detection, cancelled/no-show excluded,
reschedule-aware (excludeId), with a clear conflict message. Wired into
appointments.create (now also validates end > start) and createRecurring
(skips occurrences that clash on doctor or room). 13 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The calendar had no way to move an appointment's time, doctor, or room — only
status changes. Adds appointments.reschedule: validates end > start, preserves
the existing doctor/room when not explicitly changed, and runs the conflict
engine excluding the appointment being moved so a drag-to-reschedule can't
false-positive against itself. Unblocks calendar drag-and-drop in the UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds /updates entries for the scheduling correctness overhaul (strict overlap,
room double-booking, reschedule) and the clinical wave (vitals, treatment
plans, wellness plans, online booking) so the changelog reflects what shipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A real calendar suggests free times instead of making staff eyeball gaps. Adds
a pure findOpenSlots engine (working window + slot length + busy intervals ->
free slots; supports a finer step than the slot, never runs past day end,
back-to-back aware) with 5 unit tests, exposed as appointments.availableSlots
(by date, duration, optional doctor/room, working hours). Reusable by the
portal booking flow and the agent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Connects the availability engine to a real surface: the portal booking form now
fetches open times for the chosen date (public portal.availableSlots query,
practice-wide busy intervals) and shows them as tappable chips that fill the
time field — instead of asking clients to guess a time blind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the booking loop for the agent: it can now find free times on a date
(optionally per doctor/room) via the availability engine, then book one with
book_appointment. Read-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openvpm Ready Ready Preview, Comment Jun 2, 2026 1:17pm

Request Review

@evangauer evangauer merged commit da37010 into main Jun 2, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant