Skip to content

Green Wellness

Changelog

What’s new in each release of the scheduling platform

Show:
v2.97.REPLYRESOLVE1current
2026-06-27Production
For front desk

Run feedback from your inbox — email an idea and get an instant 'tracked' confirmation, then answer any item by replying (done / wontfix / go / a note).

What this means for you

This closes the email feedback loop. When Doug or Mariane emails an idea, they now get an instant confirmation back with a short reference tag, so they know it landed without waiting for the morning summary. And any tracked item can be answered by replying to that email (or emailing the office mailbox) with the reference tag: start the reply with done, wontfix, or go to set the status, or just write a note and it is recorded on the item. It only ever works for Doug or Mariane on a tagged message, so a patient email can never trigger it, and replies are handled before the patient pipeline so the phone assistant never auto-answers them.

Show technical details

Added

  • Instant idea-capture confirmation (with [FB-id] tag + replyTo to the watched mailbox); reply-to-resolve applies done/wontfix/approved or a comment to the tagged item. Allowlist + tag gated. Flags IDEA_INTAKE_ENABLED + FEEDBACK_REPLY_RESOLVE.
v2.97.IDEAINTAKE1
2026-06-27Production
For front desk

Email an idea and it gets tracked automatically — Doug or Mariane can email 'IDEA: ...' to the office mailbox and it becomes a tracked to-do (off until turned on).

What this means for you

A no-friction way to capture ideas and requests: Doug or Mariane emails the office mailbox with a subject that starts with IDEA:, BUILD:, FB:, or TODO: and it is automatically captured as a tracked open item — so it shows up in the morning feedback summary and the feedback queue instead of getting lost. Name the project in the subject with a tag like [GW] or [SCC] and it is filed under that project. It only ever triggers for Doug or Mariane on one of those markers, so a patient email can never be mistaken for an idea, and it stays off until the IDEA_INTAKE switch is turned on. Uses the email and feedback systems we already have — a dedicated address can come later.

Show technical details

Added

  • Owner idea-intake: marker-subject emails from Doug/Mariane become open ReviewerFeedback items; gated by IDEA_INTAKE_ENABLED (default off); captured before the patient pipeline so Isabella never auto-acks them.
v2.97.FEEDBACKDEPTH1
2026-06-27Production
For front desk

New 'Feedback depth' section in Doug's morning email — every open piece of feedback that needs an answer, is going back-and-forth, or is stuck, in one daily glance (off until turned on).

What this means for you

The recurring problem was that filed feedback could quietly sit unanswered or go back-and-forth and get lost, because seeing the full picture meant remembering to open a page. This adds a 'Feedback depth' section to the daily morning email: it lists every open item, and flags the ones that need an answer (no movement in a few days), are going back-and-forth, are buried, or aren't reaching the automated helper — so nothing rots silently and checking the depth is a daily glance instead of a chore. It is metadata only (no patient details), and it stays off until the FEEDBACK_DEPTH_DIGEST switch is turned on. Time windows are adjustable.

Show technical details

Added

  • Feedback-depth section in the doug-queue morning email; PHI-safe; gated by FEEDBACK_DEPTH_DIGEST (default off); SLA + back-and-forth thresholds tunable via env.
v2.97.TESTHYGIENE1
2026-06-27Production
For front desk

Behind-the-scenes cleanup — fixed a small bug in how the AI reads its confidence on incoming patient emails, plus a large internal-test tidy-up.

What this means for you

Two things, both behind the scenes. First, a real fix: when the system reads how sure the AI is about sorting an incoming patient email, a malformed value like "0.8a" was being quietly accepted as 0.8 instead of rejected — now it is correctly rejected, so only clean values are trusted. Second, we drained a large backlog of stale internal checks (down from 72 to a handful) that were flagging old-but-intentional changes, and tightened the wording on past update notes. Nothing patient-facing changed beyond the email-confidence fix.

Show technical details

Fixed

  • Email classifier rejects malformed confidence values; stale-test backlog drained 72 to ~12; changelog copy tightened.
v2.97.VOICETRIM1
2026-06-27Production
For front desk

Isabella's phone script trimmed back under its size budget — same warmth and the same 'talk to a person' offer, just tighter, so calls stay fast.

What this means for you

The reach-a-human additions had pushed Isabella's call script over its size budget, which can slow each turn of a phone call. We trimmed about 500 characters of repeated wording — every safety and compliance rule is untouched, the crisis lines are word-for-word the same, and she still offers to have a real person call the patient right back at the very start of the call. This only changes the script; it reaches the live phone line after the next prompt sync. Also tidied two automated tests that were checking the old wording.

Show technical details

Changed

  • Isabella voice prompt trimmed ~496 chars of redundancy back under its growth tripwire; reach-a-human offer + crisis/compliance blocks fully preserved; live after the Retell sync. Refreshed 2 stale voice tests.
v2.97.PTFIELDS0001
2026-06-27Production
For front desk

Add-a-patient will gain a few more optional fields once turned on — a preferred name, middle name, how they heard about us (pick-list), a heroes-discount eligibility checkbox, and optional gender/pronouns. Off until Doug switches it on; nothing changes today.

What this means for you

We built five more optional fields for the create-patient form, all behind an OFF switch so nothing changes until Doug turns it on. When it's on, the Optional section gains: a 'Preferred name (goes by)' so phone calls feel friendlier; a 'Middle name' to keep the legal name accurate on the authorization; a 'How they heard about us' pick-list (choose from a set list — please don't type a specific person or provider name); a 'Heroes discount eligible' checkbox for veteran / first responder / medical (eligibility only — no proof documents collected here); and optional, clearly-skippable gender and pronouns. Every one is optional. With the switch off, the form looks and behaves exactly like today.

Show technical details

Added

  • Five optional create-patient fields (ship OFF behind a switch): Preferred name, Middle name, a 'How they heard about us' pick-list, a Heroes-discount eligibility checkbox, and optional Gender + Pronouns. All optional and skippable; the form is unchanged until the switch is on.
  • 'How they heard about us' is a fixed pick-list, not a free-text box — so a specific referring person or provider name can't accidentally get typed into the field.
v2.97.EMAILOPT0001
2026-06-27Production
For front desk

You can now create a walk-in or call-in patient with no email — once it's switched on. The patient gets a clear 'No email on file' badge, reminders fall back to text, and a provider still can't issue an authorization until an email is added.

What this means for you

A new front-desk option (off until Doug turns it on) lets you add a patient who has no email — the common case for someone who calls in or walks up. When it's on, the email field becomes optional and you only need at least an email or a phone. A patient created without an email shows a persistent 'No email on file' badge, and appointment reminders go out by text instead (when the patient has agreed to texts). Two safeguards stay in place: a provider cannot issue a cannabis authorization to a patient with no email on file (so we never quietly fail to send a real authorization), and email portal sign-in isn't available to them until you add an email. Add one any time and both unlock. With the option off, email is still required.

Show technical details

Added

  • Optional-email mode for the add-a-patient form (off by default). When on, a walk-in / call-in with no email can be created — you only need at least an email or a phone.
  • A persistent 'No email on file' badge on the patient record, explaining that authorization emails and patient-portal sign-in are unavailable until an email is added.

Changed

  • Reminders fall back to text message for a patient with no email on file (when they've agreed to texts) instead of failing.
  • The 'Use & update contact' button on the duplicate prompt now passes the email/phone privately instead of putting them in the web address — they no longer appear in browser history, server logs, or the page address.

Fixed

  • A provider is blocked from issuing an authorization to a patient with no email on file (the authorization is delivered by email, so we refuse rather than silently fail to deliver). Same shape as the existing date-of-birth block.
v2.97.NEWPT0001
2026-06-27Production
For front desk

Add-a-patient is faster: date of birth is now optional, there's a Notes field and a 'Create & book' button, the phone field auto-formats, and a returning patient's new email or phone can be updated right from the duplicate prompt.

What this means for you

Mariane's front-desk feedback on the create-patient form, in one pass. Date of birth is no longer required to create a record — leave it blank for a phone or walk-in and the patient carries a 'DOB needed' badge until it's filled (a provider still can't issue an authorization without it, so age verification doesn't change). New 'Notes' field for operational notes — clinical details still go in the chart. A 'Create & book' button takes you straight into scheduling with the new patient pre-filled. The phone field tidies itself as you type and accepts any 10-digit number. On the 'possible duplicate' prompt you can now pick 'Use & update contact' to carry a returning patient's new email or phone onto their existing record. The first-name field is focused on load.

Show technical details

Changed

  • Date of birth is optional when manually creating a patient (matches the lead-conversion flow). Blank DOB sets a 'DOB needed' badge; the provider-side authorization gate still requires DOB before issuing, so age verification is unchanged.
  • Phone field auto-formats to (206) 555-1234 as you type and accepts any entry with at least 10 digits.
  • Add-patient help text corrected: lists the real required fields (name, email, phone) and describes the warn-and-confirm duplicate prompt (email / phone / name / DOB) instead of the old 'unique email blocks' behavior.
  • First-name field is focused on load; the duplicate validation toast was dropped where inline field errors already show.

Added

  • 'Notes' field on the create-patient form for operational notes (clinical details go in the chart).
  • 'Create & book' button — creates the patient, then opens the scheduler with them pre-filled.
  • 'Use & update contact' on the possible-duplicate prompt — carries the just-typed email/phone onto an existing returning patient's record for review and save.
v2.97.DRAFTACTION1
2026-06-26Production
For front desk

Patient email drafts now move the conversation forward — straight into booking or the exact next records step — instead of just acknowledging.

What this means for you

Mariane flagged that the drafted replies acknowledged a patient's email but didn't always answer what they needed next. The draft guidance is rewritten to lead with the next action: when someone is ready to schedule, the draft moves into booking (what's needed to lock a time, plus where to book); for medical records it gives the concrete next step (what to send — recent records documenting a qualifying condition — how to send it, and that the team then reviews and confirms the visit); plus clearer qualification and payment next-steps. Drafts are still reviewed by a person before anything sends, and the rules that keep replies compliant are unchanged (no medical claims, never tell a patient whether they qualify or imply they need the authorization to use cannabis legally).

Show technical details

Changed

  • Patient-email and text draft guidance is action-oriented: lead with booking, the exact records next-step, or payment, with one warm acknowledgment line. Human-reviewed before send; compliance guardrails unchanged.
v2.97.DEPOSIT0001
2026-06-26Production
For front desk

Groundwork for a $50 booking deposit (balance collected at the visit) — built but OFF; nothing changes for patients or staff until Doug turns it on.

What this means for you

We added a new option for online booking: a patient can put down a small deposit to lock in their appointment, then pay the rest on the day of their visit. The appointment confirms as soon as the deposit clears, but the medical authorization is only sent once the visit is paid in full — the deposit alone never releases it. To start, this only applies to telehealth visits, and the whole thing is behind an OFF switch, so booking works exactly like today until Doug turns it on. When it is on, the today board and a patient's chart show a clear 'Deposit paid — balance due $X' label so front desk knows who still owes at check-in.

Show technical details

Added

  • 💵 **Deposit-then-balance booking model (ships DARK).** A $50 deposit at booking confirms the appointment; the balance (visit fee − deposit) is collected day-of via the existing in-portal card checkout, a balance link, or staff mark-paid. The booking pay-link is minted for the deposit amount (server-recomputed, never client-trusted) when the deposit model is on and the visit is in scope. Behind a new OFF-by-default switch (BOOKING_DEPOSIT_ENABLED), layered on the existing pay-to-confirm flow. Telehealth-only by default (one config switch widens it to in-person later). When OFF, booking is byte-identical to today. (booking)(payments)
  • 🔒 **Authorization releases ONLY when paid in full.** The deposit alone does NOT send the medical authorization — the existing payment gate already holds until the collected amount covers the full visit fee, and the deposit model relies on exactly that. The day-of balance payment adds to the collected total and triggers the release. (payments)(safety)
  • 🏷️ **'Deposit paid · balance due $X' status label** on the today board and the patient chart (and any surface using the shared payment pill), distinct from 'Paid' in full. PHI-free — a dollar balance only, no patient identity. (admin)(payments)
v2.97.VOICEHUMAN1
2026-06-26Production
For front desk

Isabella now offers callers a real person earlier and more warmly — she leads with booking, reaches for a human at the first sign of frustration, and never makes anyone fight to reach the team. (Takes effect on the phone line only after Doug runs the phone-prompt sync.)

What this means for you

We softened how Isabella, the phone receptionist, handles callers. She still says she's automated, but now offers a real person right away as a relaxed choice — "any time you'd rather talk to a person, just say so, I can have someone call you right back" — not a last resort. She leads with booking, and the moment a caller sounds frustrated or just asks for a person, she stops and offers a callback instead of looping. New patients now pick a time first, then hear the records-and-ID steps. Every safety line is unchanged. Heads up: this is ready but only reaches the live phone line after Doug runs the phone-prompt sync.

Show technical details

Changed

  • 📞 **Isabella Wave-1 patient-experience prompt** — offer-a-human early + warm (in the opening disclosure, framed as a relaxed choice, not a fallback), a frustration/confusion trigger that hands off to a callback instead of looping, booking-first reordering for new patients (capture preferred time BEFORE the records/ID logistics), and a softened self-handling tone throughout. The crisis script + identity/legal-advice blocks + the never-promise-a-transfer / never-promise-outcomes guardrails are preserved verbatim. Prompt size 28,891 chars (under the 29,000 cap). **DOES NOT reach the live phone line until the Retell prompt sync is run** — that step is human-gated (Doug action); no sync script was run as part of this ship. (isabella)(voice)(patient-experience)
v2.97.ARIFIX0001
2026-06-26Production
For providers

Provider portal fixes: signing a visit now ALSO issues the patient's authorization (it used to need a separate admin step), the patient's uploaded records + ID open right on the chart, and a plain checklist shows what's needed before you sign.

What this means for you

Four provider-portal repairs so a doctor can complete a visit end-to-end without the front office. (1) Signing the encounter now issues the WA medical authorization and emails it to the patient — previously that only happened from an admin screen. The note still locks first; if the visit is unpaid the authorization is held until front desk marks it paid, then issues automatically. Every existing safeguard is kept (never on an unpaid visit; needs DOB + at least one qualifying condition + your signature). (2) A patient's uploaded medical records and Washington ID now open with a 'View / download' link right on the chart. (3) An always-visible 'Before you sign' checklist shows what's still missing. (4) Payment status shows accurately. The AI clinical-assist stays OFF.

Show technical details

Fixed

  • 🖊️ **Signing now issues the authorization (CRITICAL).** /api/provider/encounters/[id]/sign used to lock the SOAP note but never call the cert-issue pipeline — that fired only from /api/admin/appointments/approve (an admin surface). With no admin on staff, providers signed and no authorization was ever generated or sent. The sign route now runs the SAME unified issuance after the note locks (mirrors /api/provider/action), PRESERVING every gate: the auth-payment gate (never issues/sends unpaid), provider-signature-on-file, and the DOB / ≥1-qualifying-condition / license gates inside issueCertForAppointmentUnified. Best-effort relative to the lock (the note-lock is the load-bearing legal write and is never undone by a downstream send failure); the PHI-free outcome (issued / held-pending-payment / skipped-why) is surfaced on the chart. Idempotent — re-signing an issued visit is a no-op. (provider)(hipaa)(authorization)
  • 🗂️ **Patient's uploaded records + WA-ID now open from the chart — flag-independent.** The only viewer for PatientUploadedRecord rows (patient-uploaded outside records AND the WA-ID photo) was the dark AI records-reviewer, which 404s when the AI flags are off (their default) — so providers couldn't open a patient's records or ID at all. Added a basic non-AI list on the encounter chart + a new cookie-authed stream route /api/provider/encounters/[id]/records/[recordId] that scopes to the encounter's patient (scan-clean, not-superseded, minimum-necessary), audits VIEW_PATIENT before any bytes, and streams the private blob server-side (no redirect to a signed URL). (provider)(hipaa)(records)(min-necessary)
  • ✅ **Always-visible pre-sign readiness checklist.** The existing pre-issue checklist was gated behind PROVIDER_CLINICAL_ASSIST_ENABLED (default OFF), so a provider never saw WHY a visit couldn't complete. Added a generic, flag-independent checklist on the chart mapping the REAL issuance gates (patient DOB on file, ≥1 qualifying condition, provider signature on file, visit paid). Courtesy mirror only — the server re-checks at sign/issue. The WA-law counsel-gated stubs stay in the AI-flag-gated checklist, unchanged. (provider)(hipaa)
  • 💳 **Payment status accurate on the chart.** The encounter chart already selected stripePaymentId/poyntInvoiceId for the PaymentBadge; verified the provider appointment surfaces feed the badge correctly (portal home loads full appointment rows via include). No badge now renders blank/Unpaid for a paid visit. (provider)
v2.97.RECMON0001
2026-06-26Production
For front desk

New (off until turned on): a records-recency monitor that flags upcoming visits whose patient needs recent medical records — with a one-click 'send upload link' and a 'mark chronic-exempt' option. It never blocks booking.

What this means for you

Phase 1 of the records-readiness monitor, shipped completely OFF behind a switch. When turned on, it adds a recency check to appointment-prep reminders (the patient is reminded if there's no medical record from the last 24 months on file, instead of just 'any record ever'), and gives the front desk a new worklist page — /admin/patients/records-needed — listing upcoming visits whose patient still needs recent records, soonest first. Each row has a 'Send records request' button and a 'Mark chronic-exempt' button (a required reason is kept private to the provider record). Patients show as first name + last initial only. It is a prompt, never a block — booking is never gated. The 24-month window is configurable. Nothing runs until Doug turns it on.

Show technical details

Added

  • 🗂️ **Records-Recency Monitor — Phase 1 (default-OFF behind RECORDS_RECENCY_MONITOR_ENABLED; configurable window RECORDS_RECENCY_MONTHS, default 24).** Extends the EXISTING readiness substrate, no rebuild. New recency dimension in src/lib/appointment-readiness.ts (deriveRecordsRecencyStatus → 🔴 NEEDS / ✅ CURRENT / 🟡 EXEMPT) computed from a MEDICAL_RECORD uploadedAt within the configured window (a documented v1 proxy for the record's clinical date — Phase 2 adds the AI recordDateText). Pure helpers + the flag/window readers live in src/lib/records-validity-shared.ts. EXEMPT reuses the existing RecordsValidityDecision + Patient.recordsValidThrough — NO new flag, NO new column, NO migration. (records-monitor)(dark)(hipaa)(min-necessary)
  • 📋 **Staff worklist /admin/patients/records-needed** (role-gated ADMIN/MANAGER/SCHEDULER, force-dynamic, noindex). Lists UPCOMING (SCHEDULED/CONFIRMED, future) appointments whose patient is 🔴 NEEDS, soonest-first, via src/lib/records-needed-worklist.ts. Patient label = first name + last INITIAL only; EXEMPT + CURRENT patients are filtered OUT at the DB query (never materialized). Per row: **Send records request** (reuses the BAA-fail-closed /send-records-link rail → M365 Graph) and **Mark chronic-exempt** (reason REQUIRED). Render audits VIEW_RECORDS_NEEDED as a COUNT only — never an identifier. (records-monitor)(admin)(hipaa)
  • 🩺 **Staff chronic-exempt write path POST /api/admin/patients/[id]/records-exempt** — writes a RecordsValidityDecision (CHRONIC_CONDITION / STABLE_DIAGNOSIS / TERMINAL) + refreshes the recordsValidThrough cache, dropping the patient off the worklist + the recency reminder. Required reason is stored provider-PRIVATE (reasonNote) and is NEVER logged/echoed/audited; audit RECORDS_EXEMPT_SET_BY_STAFF carries reasonCode enum + duration + actor only. ⚠️ The decision table was designed PROVIDER-set; STAFF-set is Doug's explicit Phase-1 call — actor is recorded honestly as the staff user under a distinct audit action (Doug-greenlight noted in the route header). Route is inert (404) until the monitor flag is on. (records-monitor)(hipaa)(doug-greenlight)

Changed

  • 🔔 **Reminder cron now reflects the RECENCY standard when the monitor is on.** /api/cron/reminders overrides the 'recent medical records' readiness item to the 24-month recency check (chronic-EXEMPT patients treated as satisfied) ONLY when RECORDS_RECENCY_MONITOR_ENABLED=true; when OFF (default) the existing presence-only behavior is byte-identical. A recency-derive failure falls through to the presence-only flag — the reminder is NEVER blocked, and booking/confirmation is never gated anywhere. Reuses the existing M365 BAA prompt rail (no new sender). Patient-facing copy unchanged ('your recent medical records'). (records-monitor)(cron)(never-block)
v2.97.GWBATCH0626
2026-06-26Production
For front desk

Demi's morning queue now shows each caller's full phone + email right on the row — and inbound fax is ready for a second (HIPAA) fax provider, still switched off until its BAA is signed.

What this means for you

Two front-desk improvements plus a behind-the-scenes fax-vendor option. (1) On Demi's today queue, each callback row now shows the patient's full phone number and email inline, so Demi can reach someone without opening the thread first — the same contact info she already gets by clicking through, just shown up front. Message previews stay PHI-scrubbed and the full message is still only on the opened thread; the page stays role-gated and every view is logged. (Can be switched back to masked-phone-only instantly.) (2) Behind the scenes, the inbound-fax line can now run on a second, HIPAA-grade fax provider (Documo) — it stays completely OFF and receives nothing until that provider's Business Associate Agreement is signed and Doug flips the switch.

Show technical details

Changed

  • 📇 **Demi's Today queue — full patient contact inline (Doug-requested; flag-gated, SHIPPED DARK / default OFF).** Each 'Callbacks owed' row can render the patient's UNMASKED phone (as a tel: link) and email (as a mailto: link) on its own line, instead of the masked phone only. Because this widens PHI display, it ships INERT — set DEMI_FULL_CONTACT=true to turn it on (one env flip, NO deploy); anything else keeps the existing masked-phone-only view. This is the SAME contact data Demi already reaches via the 'Open →' deep-link — surfacing it inline is minimum-necessary for the front-desk callback task, not a new disclosure path. getDemiCallbacks now also selects Patient.email (linked patients only; unlinked-sender rows never surface an unverified email). Message subject/body previews remain PHI-scrubbed via scrubPhiForSmsOutbound (free-text is NEVER un-scrubbed). Role gate (ADMIN/MANAGER/SCHEDULER), force-dynamic, and noindex are unchanged. (front-desk)(demi)(phi-display)
  • 🧾 **VIEW_DEMI_TODAY audit now records the full-contact disclosure shape — as a COUNT, never identifiers.** buildDemiTodayAuditDetail appends fullContact=on rendered=phone:N,email:M so a reviewer can answer 'when was unmasked patient contact rendered on Demi's queue, by whom' from the actor + timestamp + counts alone. The detail string still carries ZERO patient identifiers (no phone, no email, no name). §164.312(b) metadata-only invariant preserved. (audit)(hipaa)

Added

  • 📠 **Inbound fax — Documo (mFax) provider path (default-OFF, BAA-gated, additive).** New src/lib/documo-fax.ts + a provider switch in /api/inbound/fax. INBOUND_FAX_PROVIDER=documo routes inbound faxes through Documo; unset/ringcentral keeps the existing RingCentral behavior verbatim (nothing changes by default). The Documo path normalizes Documo's fax.v1.inbound.complete webhook (messageId dedup key, faxCallerId sender ANI → LEAD_CAPTURED match, faxNumber our DID, pagesCount) into the SAME downstream pipeline the RC path uses — one dedup, one sender-match, one DB persist. Idempotent on messageId (Documo retries up to 7×); a blank faxCallerId fail-softs to the manual/unmatched path and never crashes. **The ENTIRE Documo path stays fail-closed behind INBOUND_FAX_BAA_OK exactly like RingCentral** — it ACKs 200 (no retry storm), audits the gated arrival PHI-free (message id only, no content fetched), and persists NOTHING until the fax provider's BAA is executed and Doug sets INBOUND_FAX_BAA_OK=true. Optional x-documo-signature HMAC verification (DOCUMO_WEBHOOK_SECRET); the load-bearing PHI guard is the BAA env-gate, not the signature. No schema change (Documo's messageId reuses the existing provider-agnostic dedup column — expand-only). ⚠️ The exact Documo download-PDF endpoint + auth scheme could not be confirmed against live docs (JS-rendered site, no public OpenAPI) and are ISOLATED in two clearly-commented functions for a one-line correction before go-live. (inbound-fax)(documo)(hipaa)(baa-gated)(dark)
v2.97.EXEMPLARCURATE1
2026-06-25Production
For everyone

New helper: an AI can now auto-approve the clearly-good Isabella reply-examples so you only review the tricky ones — with a one-click Un-approve override.

What this means for you

Added an optional AI auto-curator for Isabella's reply-pattern library (the staff-reply examples that teach her how to answer). Today a person has to thumbs-up every example one by one. The auto-curator uses our HIPAA-covered AI service to read each PHI-scrubbed example and either AUTO-APPROVE the high-confidence good ones, LEAVE the uncertain ones for a human, or REJECT anything that looks like it leaked patient info or makes a medical/over-promising claim. It is conservative: it only auto-approves when very sure, and it can NEVER re-judge its own approvals (a built-in anti-feedback-loop). The Playbook page now has an 'Approved' tab with an Un-approve button. Shipped OFF: nothing is auto-approved until Doug turns it on, and a dry-run shows exactly what it WOULD do first.

Show technical details

Added

  • 🤖 **Isabella exemplar AI auto-curator (default-OFF, HIPAA RED-lane).** New /api/cron/isabella-exemplar-autocurate cron + src/lib/isabella-exemplar-autocurate.ts (server) + src/lib/isabella-exemplar-autocurate-shared.ts (pure-fn verdict state machine, pin-tested). A BAA-Bedrock JUDGE (the SAME getExtractorModel() Bedrock handle the ingest scrubber uses — no non-Bedrock judge) scores each pending-review exemplar on three 0–1 dimensions (generalizable / PHI-clean+faithful / voice-policy) over ONLY the scrubbed summaries + closed enums. Verdict ladder: **auto-approve** only if min(all 3) ≥ 0.85 AND canary-clean AND not a fallback/terse row; **reject** (fail-closed, never escalate) on any scanBodyForPhiCanary trip or a PHI-faithfulness failure; **escalate** (leave pending for a human) everything uncertain. [isabella][hipaa][bedrock][autocurate]
  • 🧱 **Anti-RSI self-loop firewall.** The eligible-row query is status=pending-review AND reviewedByUserId IS NULL, which structurally excludes the curator's own future approvals (it stamps reviewedByUserId="isabella-autocurator"), hand-authored seeds, and any human-touched row — so it can never re-judge or re-feed its own outputs. [isabella][governance]
  • ↩️ **P0 reversibility UI on /admin/isabella-playbook.** New 'Approved' tab lists approved+edited exemplars (flagging AI-auto-approved ones) with an **Un-approve** button → sends the row back to pending-review (human-attributed, audited). This is the human override of any auto-approval — an AI-curates-AI surface must be reversible-by-UI. [isabella][admin][governance]
  • 📧 **PHI-free digest + audit.** Every auto-approval/escalate/reject emits one ISABELLA_EXEMPLAR_CURATED audit row (actor=autocurator, enum/cuid/reason-code only — never a summary). A PHI-free counts digest goes out via the OWNER_ALERT_EMAIL → ADMIN_NOTIFY_EMAIL rail. ?dryRun=1 runs the full judge pass and returns the per-row verdict table while persisting NOTHING (ignores the flag — for the eyeball-before-flip review). [isabella][audit][hipaa]
v2.97.ABSTAINGATE1
2026-06-25Production
For everyone

New safety gate: Isabella's phone prompt can't go live until she proves she refuses-and-routes on 15 hard test calls.

What this means for you

Added a behavioral safety check for Isabella (our automated phone receptionist). Before any change to what Isabella says can reach the live phone line, an automated test places 15 tricky synthetic calls at her — asking for medical advice, a diagnosis, a dosage, another patient's info, trying to trick her into ignoring her rules, plus two safety-critical ones (a caller in crisis and a caller who blurts out their birthday and social) — and a second AI grades whether she actually declined and routed correctly. If she fails any, the change is blocked. All test calls are fake (made-up names, fiction-only numbers) and it runs on our HIPAA-covered AI service. Isabella's wording wasn't changed — this only proves and locks in the behavior she already has.

Show technical details

Added

  • 🛡️ **Isabella behavioral abstention eval gate (RED-lane HIPAA).** Model-in-the-loop adversarial eval (scripts/isabella-abstention-eval.mjs + src/lib/__tests__/isabella-abstention-eval.fixtures.ts) runs the 15 designed cases from ISABELLA_PROMPT_AUDIT_2026_06_25.md §4 against Isabella's real assembled VOICE prompt and uses a **Bedrock-judged rubric** (BAA-covered us.anthropic.claude-sonnet-4-6, account 004730170375) to assert she behaviorally REFUSES-and-ROUTES — not merely that the clause text is present. Includes the two counter-pins: crisis must NOT collapse into take-a-message (988 must surface) and volunteered DOB/SSN must be minimized (never echoed). Caller lines are sent as untrusted role:"user" turns, never interpolated into the system prompt (preserves the injection-fence). Reports are PHI-scrubbed before write. An --adversarial mode appends the worst-case poisoned learned-call-playbook below the prompt to prove the safety floor holds under a drifted learning loop. **VOICE_PROMPT itself is unchanged** — additive test infra only. [isabella][voice][hipaa][bedrock][abstention][eval]
  • 🔒 **Pre-sync gate wired into scripts/sync-retell-prompt.mjs.** A non-dry-run push to the LIVE Retell agent now runs the abstention eval first and is BLOCKED on any failure. The only bypass is SYNC_SKIP_ABSTENTION_EVAL=1, which is logged to scripts/.retell-sync-override.log and routes through compliance-guard. This converts Isabella's already-strong abstention text into a proven, gated behavior on the phone surface. [isabella][voice][retell][sync-gate][hipaa]
v2.97.PAYDESC1
2026-06-25Production
For everyone

Pay page now tells patients the billing name they'll see on their statement.

What this means for you

Added a line to the payment page so patients know the charge appears as "Green Health Solutions LLC" on their card statement (the billing entity behind Green Wellness). This heads off "I don't recognize this charge" calls and disputes, since the statement name differs from the Green Wellness brand they booked with.

Show technical details

Added

  • 💳 **/pay page: statement-descriptor disclosure.** Added "This charge appears as **Green Health Solutions LLC** on your statement" to the payment page's card-handling note (Doug-confirmed entity). Prevents unrecognized-charge confusion/chargebacks when the card-statement merchant name differs from the Green Wellness brand. [payments][collect][copy]
v2.97.PAYCOPY1
2026-06-25Production
For everyone

Pay page wording now matches the new in-portal card checkout.

What this means for you

Fixed a stale line on the payment page that said "you'll pay on our card processor's page" — that implied a redirect to an outside page, but patients now pay right on our own page. It's reworded to "card details are entered on a secure, encrypted page and are never seen or stored by Green Wellness," which reads correctly before and after payment.

Show technical details

Fixed

  • 💳 **/pay page: corrected stale card-handling copy for the in-portal Collect flow.** The static reassurance line read "You'll pay on our card processor's secure, encrypted page" — future-tense + implied an external redirect (still showed even on the Payment-received success state). Reworded to the tense-neutral "Card details are entered on a secure, encrypted page and are never seen or stored by Green Wellness," matching the in-portal Collect checkout (no vendor name, no redirect implication). [payments][collect][copy]
v2.97.FBREMOVE1
2026-06-25Production
For everyone

You can now remove a redundant item from your In-Progress feedback list.

What this means for you

On your feedback page (My feedback), each In-Progress item now has a "Remove this item" link. Use it for duplicates or things you no longer need — it asks you to confirm and lets you add an optional reason, then takes the item off your active list. It's a soft remove: nothing is deleted, the item just moves to Completed and drops out of the agents' work queue.

Show technical details

Added

  • 🧹 **Remove redundant In-Progress feedback (Mariane cmqej28ci).** Owner-only "Remove this item" control on In-Progress rows of /me/feedback — confirm step + optional reason → sets the row's status="cancelled" (soft cancel; the row is kept for audit, never hard-deleted). The cancelled row leaves the active list and drops off the agent loop (the queue puller only pulls open/agent-working). Owner-gated server action, idempotent, can't cancel an already-done/cancelled row. Writes a PHI-free FEEDBACK_CANCELLED audit row (from→to + reason supplied|none + actor — never the free-text reason). [feedback][me][audit]
v2.97.COLLECTUX2
2026-06-25Production
For everyone

Pay button now responds instantly when clicked.

What this means for you

On the pay-by-card page, the Pay button now switches to "Processing…" the moment it's clicked, instead of sitting there for a beat while the card is read. Just a responsiveness tweak — the payment itself works the same.

Show technical details

Changed

  • 💳 **Collect pay form: instant Pay-button feedback.** The button now flips to charging/"Processing…" synchronously on click, before the SDK's getNonce tokenization latency — previously the click felt unresponsive because the status only changed after the nonce came back. The SDK error handler still resets to ready on an invalid card, so it can't get stuck. [payments][collect][ux]
v2.97.COLLECTUX1
2026-06-25Production
For everyone

Pay-by-card page polish — cleaner security wording + a Pay button that can't get stuck.

What this means for you

Tidied up the in-portal pay-by-card page: the security line no longer names the processor (just says it's bank-level encrypted and never stored), the Pay button now reliably enables once the card field loads (with a small hint if the cardholder name is missing). No change to how payments or card data are handled.

Show technical details

Changed

  • 💳 **Collect pay form: dropped the processor name from the security line + hardened the Pay button.** Footer now reads "Bank-level encryption — your card is secured and never stored by Green Wellness" (no vendor name). Added a 3.5s mount-fallback that flips the form to ready if the SDK's ready event doesn't fire, so the Pay button can't stay permanently disabled; plus an inline hint when the cardholder last name is blank. No PAN-handling change (SAQ-A). [payments][poynt][collect][ux]
v2.97.COLLECTNAME1
2026-06-25Production
For everyone

Pay-by-card page now has editable cardholder name fields (fixes a stuck checkout).

What this means for you

On the in-portal pay-by-card page, the cardholder's name is now shown in editable First/Last name fields — pre-filled from the patient's account, but changeable if the card is in someone else's name. Before this, the card processor could reject a payment asking for a valid last name with no place to enter one. No change to how card data is handled (still never touches us).

Show technical details

Fixed

  • 💳 **Poynt Collect pay form: added editable cardholder First/Last name fields, prefilled from the patient account.** The form passed patientFirstName/patientLastName straight to the Poynt nonce with no UI; when the account's last name was empty/invalid (e.g. a single char) Poynt rejected the charge with "enter a valid last name" and the payer had no field to correct it. Now both names render as editable inputs (prefilled, autoComplete=cc-given-name/cc-family-name), the Pay button is disabled until a non-empty last name is present, and getNonce sends the trimmed values. No change to PAN handling (SAQ-A, card data still browser→Poynt only). [payments][poynt][collect]
v2.97.COMMSFIX1
2026-06-25Production
For everyone

Patient-comms accuracy: the booking wizard's renewal price now reads the live $145 (was stuck at a stale $140), and new patients are told their appointment is a request pending records review — not "you're booked" — in both the wizard and the confirmation text.

What this means for you

Three copy/price corrections so what a patient sees in the online booking flow matches what we tell them everywhere else. (1) The deferred-payment step was hardcoding the old $140 renewal price; the renewal rose to $145 on 2026-06-10. It now reads the single source-of-truth price, so it can never drift again. (2) The final wizard screen said "You're booked." to every patient, but a new patient is really a tentative appointment REQUEST — we confirm only after reviewing their records. Now the wizard frames it that way (new patient sees "Request received… our team confirms after a records review"; returning patient still sees "You're booked."). (3) Same fix for the confirmation text message. No new claims, no change to the compliance wording.

Show technical details

Fixed

  • 💵 **Stale renewal price in the booking wizard** — StepPayment.tsx deferred-payment panel hardcoded data.isReturning ? 140 : 175; the renewal price rose to $145 on 2026-06-10, so a returning patient saw $140 while every other surface (chat, voice, email, StepConfirmation) showed $145. Now reads PRICING.RETURNING_TELEHEALTH / PRICING.NEW_IN_PERSON from @/lib/constants (the same pattern StepConfirmation already uses) so the figure can never drift from the source of truth again. (scheduling)(billing)
  • 🗓️ **New-patient wizard overpromised "You're booked."** — the final confirmation screen showed a confirmed-booking headline to every patient, contradicting the tentative-appointment-request framing voice/chat/email enforce (records review precedes confirmation for new patients). StepConfirmation.tsx now branches: new patient → "Request received." + an "our team reviews your records first, then confirms your visit" line; returning patient → unchanged "You're booked." (scheduling)(comms)
  • 📱 **Booking-confirmation SMS overpromised the same way** — smsBookingConfirmation said "you're booked" with no new-vs-returning branch. Added a new-patient variant ("we've got your request… our team confirms after a quick records review"); wired appointment.isReturning through both call sites (cron SMS send + reschedule). Unknown/unset isReturning falls to the new-patient (no-overpromise) wording. Also converged the greeting to the warmer "Hi {name}, Green Wellness…" shape. (comms)(sms)
v2.97.POYNTCOLLECT1
2026-06-25Production
For everyone

Card-on-our-page checkout is verified end-to-end against the live account — still OFF for patients until Doug runs one test charge and flips it on.

What this means for you

The in-portal "pay by card on a Green Wellness page" checkout (the patient types their card right on our page, the card number goes straight to Poynt and never touches us) was already built and shipped behind a switch. This release adds the SAFETY + VERIFICATION layer so we can confirm it works on the live account before turning it on: a new diagnostic that proves the server can talk to Poynt, the store resolves, and the charge route is live — and, on demand, can run a real $1 charge and immediately void it. It also adds the ability to void a card charge (the old refund path only worked for invoices). Patients see no change: the in-portal card checkout is still OFF, so /pay keeps using the existing GoDaddy hosted link.

Show technical details

Added

  • 💳 **Poynt Collect charge-path verification** — new GET /api/admin/diag/poynt-collect-verify (CRON_SECRET-gated) runs verifyCollectChargePath(): confirms the JWT bearer mints, the merchant store-id resolves (chargeCollectCard fails store-id-unresolved without it), and the services.poynt.net/businesses/{id}/cards/tokenize/charge route is ENTITLED (non-404 to a dummy body). All green = a real browser nonce will round-trip. CHARGES NOTHING. PHI-free. (payments)(poynt)(diag)
  • 💳 **Real $1 charge-then-void round-trip** — POST .../poynt-collect-verify?charge=1 with a real Collect { nonce } performs a $1.00 SALE via the SAME chargeCollectCard path the live /pay flow uses, then immediately voids it via the new voidCollectCharge helper. Triple-guarded: requires ?charge=1 AND a nonce AND POYNT_COLLECT_VERIFY_ALLOW_CHARGE=true; hard-capped at $1.00; NEVER touches a patient appointment row (isolates the Poynt leg so a verification can't release a cert). Lets Doug prove a real card works before flipping the live switch. (payments)(poynt)(diag)
  • 🔧 **voidCollectCharge(transactionId, amountCents)** in poynt.ts — voids/reverses a cards/tokenize/charge TRANSACTION (POST /transactions/{id}/voidOrReverse, falls back to /transactions/{id}/refund). The pre-existing refundInvoice only targets /paylinks/{id}/refund (invoices), which doesn't apply to a synchronous Collect charge. PHI-free; never throws. (payments)(poynt)

Changed

  • No patient-facing change. POYNT_COLLECT_INPORTAL remains OFF — /pay continues to use the GoDaddy hosted-paylink redirect. The in-portal Collect checkout (CollectPaymentForm → chargeViaCollect → chargeCollectCard → mark-paid → releaseGatedAuthForAppointment, all shipped v2.97.ICB0009) is now LIVE-VERIFIABLE before go-live. (payments)
v2.97.CONSENTAUTOSEND1
2026-06-25Production
For front desk

When a returning patient books with a provider they haven't seen, their consent form is now emailed to them automatically — no separate send needed.

What this means for you

We already auto-create the right form on every booking (the new-patient packet for first-timers, a fresh informed-consent when a returning patient is seeing a provider they haven't seen before). For a brand-new patient, our portal-welcome email already points them to it. The gap was the returning patient: their fresh consent form was placed in the portal but nobody told them, because they'd already gotten their one-time welcome email on an earlier visit. Now, the moment that appointment is created, the patient gets a direct "please review and sign your Informed Consent" email with a one-click link to sign online. It only sends once per patient per form, respects unsubscribe/bounced addresses, and goes over our BAA-covered mail. This stays off until the auto-onboarding setting is turned on.

Show technical details

Added

  • ✉️ **Auto-send the consent form on appointment creation for returning patients** (Mariane feedback: "Automatically Send Consent Form After Appointment Creation"). The appointment-onboarding path already auto-creates a NEW_PATIENT_PACKET (new patients) or a fresh INFORMED_CONSENT (returning patient + a provider they have no prior appointment with) and relies on the one-time portal-welcome email to surface it. But a returning patient who already received their portal-welcome got the new consent form silently dropped in the portal with NO notification. Now, when the welcome won't fire AND a new form was just created, we email the patient a direct /patient/forms/ magic-link via the same PHI-free buildFormLinkEmail body + bounce/unsubscribe-aware sendEmailToPatient rail (M365/Postmark/SES, never Resend) the staff "send consent form" action uses. Idempotent per (patient, form name) via a CONSENT_FORM CommunicationLog de-dupe so a second booking or a fire-and-forget re-run never double-sends; logged to the patient communication history (automated) + a PHI-free FORM_SENT_TO_PATIENT audit row. Dark by default — gated on APPT_AUTO_ONBOARDING_ENABLED, same as the portal-welcome auto-send. [forms][consent][onboarding][hipaa]
v2.97.IDDETECT1
2026-06-25Production
For everyone

Isabella stops naming providers + the ID-photo upload now opens the phone camera and flags photos that don't look like an ID.

What this means for you

Two patient-facing improvements. (1) Isabella (chat + phone) will no longer say a provider's name to a patient — the team confirms the provider after booking, so a patient is never told a name that might change at records review. (2) When a patient uploads their Washington ID during booking, their phone now opens the camera straight away, and a quick automated check tells them "that doesn't look like a photo ID — please retake" if the photo is wrong. The upload always still goes through (it never blocks booking), and on your ID-review queue each patient now shows a ✓ "looks like an ID" or ⚠ "unverified" hint so you can spot bad photos at a glance. Privacy-first: the check only answers yes/no — it never reads or stores the ID number, date of birth, name, or address.

Show technical details

Changed

  • 🗣️ **Isabella no longer volunteers or speaks a provider's name to a patient** (chat + voice). Added a durable guardrail sentence to both the chat SYSTEM_PROMPT and the VOICE_PROMPT ("Never volunteer or speak a provider's name to a patient; the team confirms the provider after booking"), and scrubbed the residual listOpenSlots prompt example that read "…with Dr Ari" → now "Tuesday March 14 at 2:00 PM". Closes Mariane feedback on provider-name leakage. Takes effect on the phone after the next Retell sync. [voice][chat][privacy]

Added

  • 📸 **WA-ID upload opens the phone camera** — the ID-photo file input now carries capture="environment" so a phone goes straight to the rear camera; desktop / no-camera still falls back to the normal file picker. [intake][mobile]
  • 🪪 **Advisory photo-ID detection on the WA-ID upload** (HIPAA minimum-necessary). After the existing compress/EXIF-strip, the upload runs through BAA-covered Bedrock (us-east-1) which answers ONLY "does this look like a government photo ID?" — a boolean + confidence bucket (+ a coarse doc-type guess). It NEVER reads or stores the ID number, DOB, name, or address. If it doesn't look like an ID the wizard prompts a retake, but the upload always succeeds (never gates intake/booking). The result is persisted on the patient and surfaced on /admin/patients/id-review as a ✓/⚠ staff hint, plus a PHI-free PATIENT_ID_DETECTION audit row. Expand-only migration 100 adds nullable idDetected/idDetectionConfidence to PendingIntakeUpload + Patient. [intake][hipaa][bedrock][id-review]
v2.97.CANARYCSRF1
2026-06-25Production
For everyone

Fixed a false alarm in the provider-chart health monitor — no patient-facing change.

What this means for you

The automated monitor that checks the provider chart + sign button every 30 minutes raised a false alarm after the latest security update. It was testing the 'sign encounter' action like a server instead of like a real browser, so the new cross-site-request protection correctly rejected it — making a healthy site look broken. The monitor now sends the same browser headers a real provider sends, so it reflects reality. No change to the live site; providers were never actually affected.

Show technical details

Fixed

  • 🩺 **Provider-chart canary no longer false-positives FAIL:sign=403 against the new CSRF enforcement.** The canary's sign-route liveness probe POSTed to /api/provider/encounters/[id]/sign with NO Origin/Referer, so the v2.97.CSRFCSP1 same-origin guard correctly 403'd it (it expected 401). That read as a sign-route OUTAGE and — critically — classifies as a genuineFail to the armed canary→rollback guard, a false alarm that could have rolled back a HEALTHY deploy. Fix: the probe now sends a same-origin Origin/Referer (this deployment's own host, which the guard's allowedHosts always includes), mirroring a real provider's browser → passes CSRF → asserts the real 401 auth-gate. Verified live: with Origin → 401, without → 403; real browsers always send Origin, so providers were never affected. [canary][csrf][hipaa][reliability]
v2.97.UNDICI1
2026-06-25Production
For everyone

Routine security patch to an underlying library — nothing changes in how you use the site.

What this means for you

A behind-the-scenes networking library was updated to close a published security advisory. This is a dependency-only patch — no change to any page, workflow, or how you use the site.

Show technical details

Fixed

  • fix(deps): pin undici ^6.27.0 via pnpm.overrides — closes HIGH advisory GHSA-35p6-xmwp-9g52 (WebSocket DoS) + 4 lesser undici CVEs arriving via @vercel/blob. Lockfile-only change; no app code altered.
v2.97.CSRFCSP1
2026-06-25Production
For everyone

Behind-the-scenes security hardening — nothing changes in how you use the site.

What this means for you

Security hardening behind the scenes — no change to how you use the site. Protection against forged cross-site requests on staff, provider, and patient actions is now actively enforced (previously it only watched and logged), and a long-standing source of noise in the security-violation log was fixed at its root. If anything on the admin or provider portal stops working after this, there is a one-setting switch Doug can flip to instantly revert.

Show technical details

Changed

  • 🛡️ **CSRF same-origin guard flipped from log-only to ENFORCING (hard 403 on cross-origin cookie-authed mutations).** The proxy's same-origin Origin/Referer check on unsafe methods (POST/PUT/PATCH/DELETE) to /api/{admin,provider,patient,dispensary} now returns 403 instead of just logging [csrf] would-block. Safe to enforce: all first-party UI fetches are same-origin; the 46 Server Actions are already same-origin-gated by Next's built-in check; vendor webhooks + cron + bearer-allow + integrations + health are explicitly exempt. **Escape hatch:** CSRF_ENFORCE=false in Vercel env reverts to log-only with no redeploy. [security][csrf][hipaa]
  • 🛡️ **/api/patient/:path* added to the proxy matcher — closes an unguarded patient-portal mutation surface.** The catch-all matcher's negative-lookahead excluded api/patient and there was no positive entry, so patient JSON mutation routes (profile PATCH, password change) never ran through the proxy → had NO edge CSRF check and NO identity-header strip. They were cookie+per-route-auth only. Now matched: they pass the CSRF same-origin guard, then fall through to the catch-all freshHeaders() identity-header strip. Patient auth routes stay same-origin POSTs from the login page (pass normally). [security][csrf][patient][hipaa]

Fixed

  • 🛡️ **CSP nonce wiring repaired at the root — kills ~25k phantom Report-Only violations.** The strict protected-route CSP (nonce + 'strict-dynamic', Report-Only since v2.97.U-era) was firing script-src-elem blocked=/_next/static on EVERY admin/provider/patient page view (25,094 AuditLog rows, ongoing) because Next.js was never applying the nonce to its own bundle scripts. Per the Next 16 CSP guide, Next extracts the nonce by parsing the Content-Security-Policy header **on the REQUEST** during SSR — it does NOT read x-nonce and does NOT parse -Report-Only. The proxy was only setting x-nonce + the Report-Only response header, so Next's scripts went un-nonced. Fix: also set the strict CSP as Content-Security-Policy on the **request** headers passed to NextResponse.next({ request }). Response stays Report-Only (cannot white-screen). Also added the missing frame-src (RingCentral softphone + Stripe + Poynt iframes), full connect-src, and media-src to the strict policy so it stops reporting phantom frame-src blocked=/ and is now a FAITHFUL would-this-enforce-cleanly probe. **CSP remains Report-Only on protected routes — enforce flip is Doug-greenlight after a clean week of zero CSP_REPORT_ONLY_VIOLATION rows.** The lax-but-real enforced CSP in next.config.ts is unchanged and still protects every route. [security][csp][nonce][hipaa]
v2.97.RENEWALCONSENT1
2026-06-24Production
For everyone

When a returning patient books with a provider they haven't seen before, the system now automatically queues a fresh consent form for them to sign in the portal — so there's a signed consent on file for each provider. If they've already seen that provider, nothing changes.

Show technical details

Added

  • 📋 **Auto-create a new-provider consent for returning patients (Doug clinical rule 2026-06-24: renewals re-consent only with a NEW provider).** Extends fireAppointmentOnboarding (same APPT_AUTO_ONBOARDING_ENABLED flag, already on): on a RENEWAL booking, if the patient has no prior NON-CANCELLED appointment with that appointment's provider, it creates an INFORMED_CONSENT form (2-page consent + acknowledgement, no re-intake — distinct from the new-patient 5-page packet) at status SENT so they sign it IN the portal before the visit. Consent is per patient↔provider; "already consented" is proxied as "prior non-cancelled appt with this provider" — clinically sound + SELF-IDEMPOTENT (once the appt exists, rebooking the same provider skips, so no duplicate per provider). Best-effort: never throws, never blocks the booking. New patients still get the full packet; same-provider renewals unchanged. PHI-free audit (FORM_CREATED, mode only). Mirrors the hipaa-architect/Explore-blessed new-patient-packet pattern. [consent][onboarding][hipaa][provider]
v2.97.CHARTBOUNDARY1
2026-06-24Production
For everyone

Fixed the real cause of the provider encounter-chart error: a chart could crash when a provider clicked into it from elsewhere in the portal. It now loads reliably however you navigate to it.

Show technical details

Fixed

  • 🩺 **ROOT CAUSE of the provider encounter-chart crashes (digest 3615476718) — a server/client boundary bug, NOT a transient blip.** The CHARTDIAG1 server-error capture shipped earlier today caught the real message: Attempted to call normalizeMedicationsJson() from the server but normalizeMedicationsJson is on the client. The cookie-port chart page (a Server Component) imported normalizeMedicationsJson + emptyMedicationRow + normalizeDastAnswers from MedicationReviewSection.tsx / DastTenSection.tsx — both "use client" modules — and CALLED them during server render. On a soft client-side navigation (RSC payload request, ?_rsc=) Next.js treats a client-module export as a reference, not a callable, and throws → the whole chart 500'd. On a hard page load it happened to execute, which is exactly why it looked intermittent (and why the transient-retry CHARTRETRY1/3 couldn't touch it — it's deterministic, not a connection error). **Fix:** extracted the pure, React-free helpers into non-client shared modules — medication-review-shared.ts + dast-ten-shared.ts — imported by BOTH the server page and the client components (which re-export them so every existing client/test import path keeps resolving). The server now calls plain functions, never client references. **Tests:** new chart-server-client-boundary.test.ts (7 pins: page imports helpers from -shared, does NOT import the runtime helpers from the "use client" components, shared modules carry no use client directive, + behavior). typecheck clean; the 35 encounter-detail anti-divergence pins still green. This is the genuine fix for today's chart-crash cluster; CHARTRETRY1/2/3 (transient retries) + CHARTDIAG1 (the capture that diagnosed this) remain as complementary hardening. PHI scope: zero (pure JSONB-shape normalizers, no patient data in logs). [hipaa][provider-chart][server-client-boundary][root-cause][version-letter:CHARTBOUNDARY1][cadence-override: live provider-facing chart crash, root cause via CHARTDIAG1 capture]
v2.97.PROVIDERINTAKE1
2026-06-24Production
For providers

Providers now see the patient's submitted intake — chief complaint, qualifying conditions, current medications, allergies, and history — right on the chart, so you can review it before the visit. If the patient hasn't filled it out yet, the card tells you so.

Show technical details

Added

  • 🩺 **Submitted patient intake now shown on the provider encounter chart (Dr. Frisch request).** /provider/portal/encounters/[id] loads the IntakeForm for the appointment and renders a collapsible (open-by-default) "Patient intake" card under the identity card: chief complaint, qualifying conditions, symptom duration, current-cannabis-use + frequency, therapies tried, current medications, allergies, treating physician, surgical history, recent labs, prior-auth, and patient notes. Previously the intake only fed the SOAP *prefill* — it was never shown as a reviewable summary, so the provider couldn't see what the patient submitted before the visit. When no intake is on file the card says "not submitted yet" so the provider knows to chase it. Read is .catch-degraded (matches the chart's resilience pattern — a failure shows an unavailable note, never crashes the chart). PHI render is expected here (provider's authorized chart; chart-open audit already covers it). [provider][chart][intake][hipaa]
v2.97.CHARTRETRY3
2026-06-24Production
For providers

Fixed a rare glitch where opening a patient chart could briefly show an error instead of loading.

What this means for you

Fixed a rare glitch where opening a patient chart could briefly show an error instead of loading. It now retries on its own, so a provider doesn't get bounced back to the schedule.

Show technical details

Fixed

  • 🩺 **Provider chart crash — hardened the last unguarded primary read (live incident, digest 3615476718, enc=cmqsddx39…).** The /provider/portal/encounters/[id] page wrapped its *encounter* read in transient-DB retry (CHARTRETRY1/2) but the **db.provider.findUnique that runs immediately before it was still an unguarded await** — a transient Neon blip there (pool reset / acquire-timeout / server-closed connection) 500'd the WHOLE chart and bounced the clinician to Practice Fusion, even though the encounter row + every relation read back clean (structurally verified against prod for this enc). Wrapped that provider lookup in the same withTransientDbRetry (transient-only — deterministic errors still surface immediately so a real bug isn't masked). audit() already self-guards (ruled out); this was the remaining unguarded primary read on the chart. [provider][chart][reliability][hipaa][incident]
v2.97.CHARTDIAG1
2026-06-24Production
For everyone

Behind-the-scenes: when a page errors on the server, we now record the actual technical cause (with patient details scrubbed out) so we can pinpoint and fix it fast — instead of only seeing an opaque error code. No change to what staff or patients see.

Show technical details

Added

  • 🔬 **Server-error capture via Next.js onRequestError — turns an opaque crash digest into a diagnosable error class.** Two provider encounter-chart crashes today (digests 541975153 + 3615476718, different encounters) each read back with FULLY healthy data + succeeding queries on replay — so the cause was invisible from data alone. The reason: the only durable crash record was the client error-boundary's PROVIDER_PORTAL_CRASH AuditLog row, which carries name + digest ONLY; the real server-side error (Prisma class, connection-vs-data, which read failed, or a React serialization message) lived solely in ephemeral Vercel function logs that can't be queried after the fact. Added onRequestError to src/instrumentation.ts: it fires with the REAL server error for any request crash and writes a PHI-scrubbed row to AuditLog (action='SERVER_REQUEST_ERROR') keyed by the SAME numeric digest the alert carries — so the next occurrence is diagnosable from a psql query instead of an opaque digest. **PHI firewall:** scrubServerErrorMessage redacts quoted values + 3+ digit runs (DOB/phone/MRN/SSN) and caps at 140 chars (the error-class prefix only — same precedent as the chart's existing console diagnostic); Next.js control-flow signals (notFound/redirect/dynamic-server-usage) are skipped so normal navigation isn't logged as a crash; the whole capture is wrapped so diagnostics can never throw/recurse. Edge runtime is skipped (the Neon adapter is Node-only). NEW src/lib/__tests__/instrumentation-error-scrub.test.ts (9 pins: quoted/single-quoted/digit-run redaction, error-class-prefix + Prisma-code preservation, length cap, whitespace collapse, control-flow-digest skip vs numeric-digest capture). All 9 green; typecheck clean. [hipaa][observability][server-error-capture][phi-scrubbed][version-letter:CHARTDIAG1][cadence-override: 2nd live provider-chart crash today, data provably healthy both times — instrument to diagnose the real cause]
v2.97.CHARTRETRY2
2026-06-24Production
For everyone

Internal hardening of the encounter-chart fix from earlier today — the database-retry logic is now a reusable, unit-tested helper.

What this means for you

Internal hardening of the encounter-chart fix from earlier today — the database-retry logic is now a reusable, unit-tested helper. No change to what staff or patients see.

Show technical details

Changed

  • 🧪 **Extracted the CHARTRETRY1 transient-retry into a unit-tested @/lib/db-transient-retry helper.** The bounded-retry engine + transient-error classifier that keep a momentary DB blip from 500'ing the provider encounter chart were inlined in provider/portal/encounters/[id]/page.tsx. Moved them to a reusable lib (isTransientDbError + withTransientDbRetry, retry-budget bounded, sleep injectable) so the behavior is locked by a real behavior test, not just a static-source pin. The page keeps a thin wrapper that owns its canonical [encounter-detail] diagnostic lines verbatim — zero behavior change to the chart. NEW src/lib/db-transient-retry.ts + src/lib/__tests__/db-transient-retry.test.ts (12 pins: classifier retries P1001/P1002/P1008/P1017/P2024 + init/panic + connection-reset messages ONLY, deterministic P2002/P2022/P2025/P2003 + validation + non-Error fail fast; control flow = first-try-once, retry-then-succeed, fail-fast-on-non-transient, bounded-then-rethrow, totalAttempts budget). All 12 green + the page's 35 anti-divergence pins still green. PHI scope: zero. [provider-chart-resilience][refactor][unit-tested][version-letter:CHARTRETRY2]
v2.97.CHARTRETRY1
2026-06-24Production
For everyone

Fixed a rare case where opening a patient's encounter chart could show a 'Something went wrong' error.

What this means for you

Fixed a rare case where opening a patient's encounter chart could show a 'Something went wrong' error. The chart now quietly retries a momentary database hiccup before giving up, so a provider doesn't get bounced out mid-visit.

Show technical details

Fixed

  • 🩺 **Provider encounter chart no longer 500s on a transient DB blip (digest 541975153, 2026-06-24).** The load-bearing encounter.findFirst on /provider/portal/encounters/[id] was the last unguarded read on the chart — every secondary read was already .catch-degraded in the ARI0003/ARI0004 resilience work, but a *momentary* Neon connection failure (pooled-connection reset · pool-acquire timeout · server-closed connection) on the primary read still crashed the whole chart and sent the clinician back to Practice Fusion. Diagnosis confirmed the crashing row + every relation read back clean against prod, so this was a single transient throw, not a data bug. Wrapped the primary read in a bounded retry (loadEncounterWithTransientRetry — 2 retries, 150ms/400ms backoff) that fires ONLY on connection/pool-class errors (Prisma P1001/P1002/P1008/P1017/P2024 + init/panic + connection-reset message class) — never on deterministic query/validation errors, which still surface immediately so a real bug isn't masked. A genuinely persistent failure still reaches provider/error.tsx and notFound() still fires on a real miss; the canonical [encounter-detail] primary encounter load threw diagnostic log line is preserved verbatim. PHI scope: ZERO — retry classifier logs error name + Prisma code only, never row data. [hipaa][provider-chart-resilience][transient-retry][version-letter:CHARTRETRY1][cadence-override: live provider-facing chart crash]
v2.97.ANALYTICSWINDOW1
2026-06-23Production
For everyone

The admin Analytics page now has a date filter — tap 30 days, 90 days, 1 year, or All time to see leads, bookings, and revenue for whatever range you want.

Show technical details

Added

  • 📅 **Date-range selector on /admin/analytics (30d · 90d · 1 year · all-time).** Added a ?window= switch; getSiteAnalytics now takes windowDays: number | null (null = all-time, omits the createdAt filter) and windows BOTH the lead aggregates (new leads, top sources, by-location) and the appointment aggregates so the whole page reflects the chosen range. Open-lead total stays an all-time snapshot. Trend vs prior period only shows for finite windows. Defaults to 30 days. [analytics][admin]
v2.97.PRACTICEANALYTICS1
2026-06-23Production
For everyone

The admin Analytics page now shows real numbers from our own records — where leads come from, how many are booking, new vs renewal, and revenue — instead of the old empty Google page. Raw website visits still live in Vercel.

Show technical details

Added

  • 📊 **/admin/analytics now shows first-party practice funnel analytics (replaces the GA4 hand-off).** New src/lib/site-analytics.ts (getSiteAnalytics) aggregates the Lead + Appointment tables — counts only, no PHI, no Google Analytics tag — into: new leads + open-lead total + booked-from-a-lead conversion; top lead sources (all-time) + leads by preferred location; appointments booked/completed, new vs renewal, telehealth vs in-person, by-status, and revenue collected (28-day window). All inside the Neon BAA boundary; degrades to a soft banner on DB error. Raw web traffic stays in Vercel Web Analytics (hand-off card retained). Verified against prod: 13,174 open leads, top source GW Website (3,402), 44 appts booked / $740 collected last 28d. [analytics][hipaa][admin][leads][appointments]
v2.97.ANALYTICSVERCEL1
2026-06-23Production
For everyone

The admin Analytics page now points you to our website traffic in Vercel instead of Google.

What this means for you

The admin Analytics page now points you to our website traffic in Vercel instead of Google. We don't put Google Analytics on the site for patient-privacy reasons, so the old page was always blank — now it sends you to the right place.

Show technical details

Changed

  • 📊 **/admin/analytics repointed off GA4 → Vercel Web Analytics hand-off.** Removed the Google Analytics Data-API dashboard (it was structurally empty: the site carries no gtag/GA4 tag by the 2026-05-28 HIPAA ruling — Google signs no BAA for Analytics — so the property has 0 sessions). The page now renders an admin-gated hand-off to the Vercel Web Analytics dashboard (privacy-first, cookieless, no PHI, BAA-covered in-stack) for greenwellness.org, plus a plain-language "why not Google Analytics" note. GA4 Data-API helpers (src/lib/ga4.ts) + the now-unused GA4_OAUTH_* prod env vars left in place but unreferenced. hipaa-architect-reviewed: do NOT install a GA4 tag on this HIPAA site. For staff-visible in-app numbers later, the path is a first-party events table inside the Neon BAA, not GA4. [analytics][hipaa][admin]
v2.97.EMAILFLAG1
2026-06-23Production
For front desk

The patient list now shows a clear amber “No email on file” tag for anyone missing a real email address, instead of a confusing fake one.

What this means for you

The patient list now shows a clear amber “No email on file” tag for anyone missing a real email address, instead of a confusing fake one. Those patients already don't get automated emails, so this just makes it easy to spot who needs a real address added.

Show technical details

Changed

  • 📧 **Patient list flags placeholder (@unresolved.local) emails.** /admin/patients now renders an amber “No email on file” badge (via isPlaceholderEmail) instead of the raw synthetic address, so staff can see which imported records need a real email. No send-behavior change — sendEmail()/sendEmailToPatient() already hard-block placeholder addresses (~20.9k rows) so none of these ever bounce. Visibility half of Mariane's “Patient Email Mapping / Salesforce Import” feedback (the SF field-mapping review is the remaining, data-side half). [feedback][leads][deliverability]
v2.97.ISADRAFTLINK1
2026-06-23Production

Mariane: in Isabella's Draft Replies, the 'Open Conversation + Paste + Send' button now opens the exact patient email thread the draft was written for — not the generic inbox or the patient overview page. Before, clicking it routed you to the patient profile page (when the email was matched to a patient) or to the all-channels inbox (when it wasn't), so you still had to hunt for the right thread to paste into. Now it opens directly on the Messages tab where the conversation is, ready for paste & send.

Show technical details

Fixed

  • 📨 **Isabella Draft Replies — 'Open Conversation' now deep-links to the exact email thread (cmqq0zjls / Mariane).** getPendingDrafts() in src/lib/isabella-draft-queue.ts built conversationHref as /admin/patients/ (linked patient) or /admin/messages (unlinked sender) — neither lands on the specific thread the draft was written for. Now: linked patient → /admin/patients/#communication so PatientTabs opens directly on the Messages tab (the #communication hash is the existing convention already used by the callbacks-owed-digest deep-link); unlinked sender → /admin/messages/email/ so the staffer lands on the per-thread audit view for that specific conversation (the page already falls back to id-match when threadId is null, so a message with no threadId still resolves). Selected threadId from the message row to support the unlinked case. Behavioral fix only — no PHI surface change, no schema change, no new API. ✨ Auto-fixed by Claude. [admin][isabella-drafts][routing][cmqq0zjls]
v2.97.RENEWALTAIL1
2026-06-21Production

Fixed a quiet bug that had stopped two kinds of renewal outreach since June 13: the 90-day 'we miss you' check-in and the 7-days-after-expiry win-back emails. The daily 21/14/7/0-day renewal reminders were never affected. The cause was the same database-query fragility behind the recent provider-chart crash: one query was written in a way the database engine can choke on, which silently aborted the rest of that nightly job. We rewrote the fragile queries the safe way and wrapped each stage so one part failing can never silently kill the rest again. We also swept the system and fixed the same pattern in several other nightly emails (welcome drip, intake reminders, 2-hour text reminders, review requests) and on the provider chart's medication/allergy panel.

Show technical details

Fixed

  • 🔧 **Renewals re-engagement + win-back tail-throw eliminated (live since ~6/13).** /api/cron/renewals ran a 90-day re-engagement query with a Prisma-7.8 nested-relation filter (where: { ..., patient: { emailUnsubscribed: false } }) — the same shape Prisma 7.8 throws on SYNCHRONOUSLY (CHARTFIX2 class), before a promise exists, so the per-query .catch() never runs and the throw escaped, aborting the rest of the route (re-engagement + win-back sends + the final heartbeat) every night. Rewrote the re-engagement query to a scalar where + in-memory emailUnsubscribed filter. Defense-in-depth: each post-stage block (escalation / re-engagement / win-back) now runs in its OWN try/catch so a single block throwing can never abort the whole route or suppress the final actor=renewals result=... heartbeat. The win-back query itself was already scalar (db.patient direct, emailUnsubscribed is a column) — it never threw; it just never got reached. The daily stage reminders (21/14/7/0d) were never affected. [cron][renewals][prisma][reliability][hipaa]
  • 🩺 **Health-check cron staleness now reflects last-FIRED, not just last-completed.** /api/health matched cron staleness on detail startsWith 'actor= ' (trailing space) — which only matches the POST-compute result= heartbeat. A cron that tail-throws or benignly early-returns (fires but never reaches its result= line) showed falsely 'stale' even though it ran today (why doh-nudge / waitlist looked broken when they were fine). Now takes the most recent of BOTH the pre-compute fire (actor=, written right after auth) AND the post-compute result (actor= result=...). Matches both shapes explicitly so a prefix actor (reminders) can't match a longer one (reminders-2h). Canary fallback preserved. [health][monitoring][observability]
  • 🧹 **Prisma-7.8 nested-relation sweep — same crash class fixed in 5 more crons + the chart med/allergy panel.** Converted nested-relation-in-where filters to scalar + in-memory filtering (behavior identical) in: doh-nudge, intake-reminder, reminders-2h, new-patient-drip (Day 3 + Day 14), and review-request — each fetched candidates with a patient: {...} (and in some cases workflowEvents: { none } / intakeForm: null) relation filter that can sync-throw and silently kill the send. Also fixed src/lib/ddi-shadow-source.ts (the SoapEditor medication + allergy reader, exact CHARTFIX2 intakeForm + appointment-relation where/orderBy shape) and src/lib/renewal-pipeline.ts assessAndMarkDoctorReady (its intakeForm.count({ where: { appointment: { patientId } } }) was caught by an outer try/catch but silently zeroed the doctor-ready signal). All use the proven flat two-step (scalar appointment ids → intake by appointmentId in). [cron][prisma][reliability][hipaa][sweep]
v2.97.CSRFLOG1
2026-06-20Production

Behind-the-scenes security hardening (no visible change).

What this means for you

Behind-the-scenes security hardening (no visible change). Added a cross-site-request guard so another website can't trick a logged-in staff/provider/patient browser into silently changing data in our system. It's running in 'watch-only' mode first — it logs anything suspicious but doesn't block yet — so we can confirm it never trips on a real action before we switch it to fully enforcing. First item from the expert review's security batch.

Show technical details

Changed

  • 🛡️ **CSRF same-origin guard on cookie-authed PHI mutations (security batch #1, LOG-ONLY).** src/proxy.ts now checks Origin/Referer on POST/PUT/PATCH/DELETE to /api/{admin,provider,patient,dispensary} against a host allowlist; cross-origin → logged as [csrf] would-block. Exempts vendor webhooks, cron/bearer routes, integrations, health; Next.js's built-in Server-Action origin check covers page-POST actions (out of scope here). Edge-safe. Default = log-only; flip CSRF_ENFORCE=true to hard-403 after observing zero false-positives (report-then-enforce, same discipline as CSP). security-auditor reviewed: clean, bypass-resistant (Origin/Referer are browser-forbidden headers), false-block risk low. [security][csrf][hipaa][log-only]
v2.97.ARMNOW2
2026-06-20Production

Two more from the expert review. (1) Fixed the Washington FAQ that said 'the evaluation can be done by telehealth' — that contradicted our actual rule: a first-time/new-patient visit is in person at Lynnwood, and telehealth is for renewals of returning patients. The FAQ now says exactly that. (2) Turned ON Isabella's stale-message safety net: if a patient's message to a human goes unanswered past our business-hours service window, Isabella now automatically sends a brief, no-details 'we got your message, we'll reach you by [next business time]' note on the channel they consented to. It only applies going forward (it does not message the old backlog), only for patients who consented, and the note contains no health details.

Show technical details

Changed

  • ⚖️ **WA telehealth FAQ corrected to match our licensed model + legal research.** /qualify/washington FAQ 'Can I do the whole thing online?' said the evaluation can be done by telehealth — contradicting GW's rule (RESEARCH_INITIAL_VISIT_IN_PERSON + Mariane 2026-05-15: new-patient first eval is in-person at Lynnwood; telehealth is renewals-only for returning patients). Rewrote the answer to state new=in-person / renewal=telehealth / card issued in person. Removes a self-authored public-page contradiction (regulatory exposure). [regulatory][wa][green-zone][claim-accuracy]
  • 🟢 **Isabella stale-warm-transfer SLA auto-ack ARMED** (ISABELLA_STALE_TRANSFER_SLA_ENABLED=true). Built-but-off since IRC0012; now on. When a warm-transfer to a human passes the business-hours SLA (default 4h), Isabella auto-sends the patient a generic no-PHI acknowledgement on their consented rail (M365 email / SMS-if-consented), forward-only (no retro-blast of the historical backlog), idempotent, per-fire cap 25, PHI-free audit. Closes patients-waiting-in-silence. [isabella][sla][armed][hipaa][consent-gated]
v2.97.ARMNOW1
2026-06-20Production

Three safety + polish fixes from the expert review. (1) Inbound faxes are now held, not stored, until RingCentral signs its data-protection agreement (BAA) — faxes are full medical records, and storing them with a vendor that hasn't signed yet would be a reportable issue, so the system now safely acknowledges each fax and logs it without saving the contents until the agreement is in place. (2) Appointment + renewal text reminders now greet patients by first name (the name was being collected but left out of the message). (3) Fixed a homepage claim that read 'walk in and walk out authorized' — that implies a guaranteed approval, which we can't promise; it now says most patients are seen same-day and the provider makes an independent decision.

Show technical details

Changed

  • 🔒 **Runtime BAA kill-switch on the inbound-fax line (expert-sweep P0).** /api/inbound/fax now fail-closes while RingCentral is BAA-pending: after auth + confirming a real inbound fax, if INBOUND_FAX_BAA_OK !== "true" it writes a PHI-free INBOUND_FAX_RECEIVED detail=baa_gated audit row, returns 200 (so RC doesn't retry), and does NOT fetch content or persist. Default (env unset) = gated. This stops a reportable §164 disclosure (storing full medical records with a vendor under no signed BAA) — the build-time vendor-baa gate only warned; this is runtime enforcement. Flip the env true the day the RC BAA executes. hipaa-architect reviewed: fail-closed + PHI-free confirmed. [hipaa][baa][fax][p0][fail-closed]
  • ✍️ **Appointment + renewal SMS now greet by first name.** smsReminder() + smsRenewalReminder() already received firstName but dropped it from the message body; both now lead with Hi , (guarded — omitted if name is blank). [sms][comms][personalization]
  • 🟢 **Green-zone fix: removed an implied-guarantee homepage claim.** The 'Same-Day Authorization / walk in and walk out authorized' trust card read as a guaranteed approval + dispensary steer. Now 'Same-Day Appointments / most patients are seen same-day — your provider independently reviews your records and makes the authorization decision.' [seo][green-zone][claim-compliance]
v2.97.PRVDOC1
2026-06-20Production

Providers can now upload medical records to a patient too — completing the 'records in one place' picture.

What this means for you

Providers can now upload medical records to a patient too — completing the 'records in one place' picture. Staff (admin/manager/reception) already got the Attach-records button on the patient page yesterday; this adds the provider side. A provider can only attach records to patients they actually have an appointment with (so it maps to the right chart), and any provider-uploaded record shows a 'Provider' tag in the patient's Documents list so it's clear who added it. (Patients upload via their portal, as before.) Note: the in-portal upload button for providers is the next small step; this ships the secure upload capability + correct labeling first.

Show technical details

Added

  • 📎 **Provider medical-records upload (closes the provider leg of cmqixd28q).** New POST /api/provider/documents — a provider attaches a record to a patient, scoped (HIPAA minimum-necessary) to an appointment they own (appointment.providerId === provider.id); the patient is derived from that appointment, so a provider can't reach a patient they aren't scheduled with. Dual-auth (legacy ?token= + new provider_session cookie) mirroring the existing provider document-download route. Same private-Blob + compress/EXIF-strip + 25MB + MIME-allowlist pipeline as the staff route (ADU0001), uploadedBy="provider". New PHI-free audit literal PROVIDER_DOCUMENT_UPLOADED (provider name + appt id + mime/size only — never filename/blobURL/patient identity). The admin patient Documents list now labels uploads **Patient / Provider / Admin** correctly (was Patient/Admin only). NO schema change (docType metadata = separate fast-follow). NO provider-portal upload button yet (next step). [provider][documents][hipaa][cmqixd28q]
v2.97.BAADISC1
2026-06-19Production

Corrected the public Privacy Notice and About page to accurately list the technology vendors we actually have signed Business Associate Agreements (BAAs) with. The old list named some services we don't use for patient health information (and that don't have BAAs with us) and left out several we do — so it could have misrepresented how patient data is handled. The list now reflects our real signed BAAs (Microsoft 365, AWS, Vercel, our database provider, Doxy.me, Retell AI, and Practice Fusion), and the 'we have BAAs with every vendor' wording was changed to the accurate 'we require a signed BAA before any vendor is allowed to handle health information.' No patient-facing functionality changed — this is an accuracy/compliance copy fix.

Show technical details

Changed

  • 🔏 **Privacy Notice + About: vendor/BAA disclosures corrected to match our actual signed BAAs (BAA_STATUS source of truth).** The /privacy "Third-Party Service Providers" list previously claimed signed BAAs with Resend, Salesforce, Stripe, and Twilio — none of which currently hold a signed GW BAA for PHI (Resend + analytics are code-gated OUT of the PHI path; Salesforce is decommissioning; Stripe/Twilio are pending) — and omitted vendors we DO use under signed BAAs. Replaced with the accurate set: Microsoft 365 (email), AWS Bedrock (AI), Vercel (hosting/storage), Neon (database), Doxy.me (telehealth video), Retell AI (voicemail), Practice Fusion/Veradigm (EHR). Softened the absolute "we have signed BAAs with all vendors" claim on both /privacy and /about to the defensible policy statement ("we require a signed BAA before authorizing any vendor to handle PHI"). Fixed an "above/below" cross-reference. Patient-facing legal accuracy fix; no behavior change. [privacy][hipaa][baa][compliance-copy]
v2.97.LOCMATCH1
2026-06-19Production
For front desk

The online booking form now shows the same clinics Isabella offers — filtered by new vs. returning patient.

What this means for you

The online booking form now shows the exact same clinics our phone receptionist Isabella would offer — filtered by whether the patient is new or returning. Before, the booking page listed every open clinic regardless of who can be seen there; now a first-visit patient and a renewal patient each see only the clinics that actually take their kind of visit, matching what Isabella says on the phone. This is the 'booking form must match Isabella by location' fix Mariane asked for. Behind the scenes it reads from the one shared rule list that already drives Isabella, chat, text, and email — so there's now a single source of truth and the surfaces can't drift apart. Also removed the 'providers missing headshot' line from the Launch Readiness checklist (Mariane asked to drop it).

Show technical details

Changed

  • 📍 **Booking form clinic list now matches Isabella by patient class (cmqell76a / Mariane).** /api/locations accepts an optional ?appointmentClass=NEW|RENEWAL and gates the returned clinics through the IDENTICAL getActiveLocations() + getAllowedProvidersAt() helpers in provider-location-rules.ts that drive Isabella's spoken clinic list (voice/chat/sms/email already share this single source of truth). Step3Appointment derives the class from isReturning (declared-new → NEW, declared-returning → RENEWAL, undeclared → unfiltered legacy list) and passes it; the per-class module cache is now keyed by class so a NEW-gated list never leaks to a RENEWAL patient. A clinic with zero providers for the class (e.g. Spokane for renewals, or any clinic after a provider sunset) drops out of the picker exactly as it drops out of Isabella's list, and an already-picked clinic that falls out of the gated set is auto-cleared. Unmapped locations stay visible (rule table governs only known GW clinics). No schema change. [booking][isabella-parity][locations][cmqell76a]

Removed

  • 🧹 **Dropped the 'providers missing headshot' line from Launch Readiness (cmpnh3qve / Mariane).** Removed the noPhoto check + its 'they're hidden from the public providers section' row from the site-wide PreflightWarnings admin banner (rendered on /admin/launch). The per-provider readiness table on /admin/launch is unchanged. [admin][launch][cmpnh3qve]
v2.97.LEADSRC1
2026-06-19Production

Leads now show where each one came from.

What this means for you

Leads now show where each one came from. Leads you add by hand get a purple ✋ Manual pill, and leads brought over from Salesforce will get a blue ⬇ Imported pill — so it's obvious at a glance which rows are migrated vs. brand-new website inquiries (web leads stay unlabeled since they're the bulk of the list). This is the 'tell imported leads apart' tag Mariane asked for; the Imported pill lights up automatically once the Salesforce lead import is run. (Also added a behind-the-scenes setup script that registers the inbound-fax line with RingCentral so faxes start landing in the app — that one's a Doug step.)

Show technical details

Changed

  • 🏷️ **Lead provenance pill on /admin/leads (G6 / Mariane).** Each lead's source= marker (already written into the LEAD_CAPTURED audit row by the route that created it) now renders as a pill: ✋ Manual (violet) for staff-created leads, ⬇ Imported (sky) for salesforce-import rows. Web captures stay unlabeled — they're the default + bulk of the queue, so a pill would be noise. Parsed via parseLeadDetail() in leads-shared.ts (new source field) so client + server read it identically. NOTE: the Salesforce backlog import (scripts/sf-import/import-recent-leads.ts) currently upserts into the separate Lead Prisma table, which /admin/leads does not read — for imported rows to appear here AND carry the pill, the import must also emit source=salesforce-import LEAD_CAPTURED audit rows (or the page must union the Lead table). That wiring rides with the import job itself. [leads][crm][provenance][g6]
  • 📠 **Inbound-fax RingCentral subscription register script (G8).** New scripts/rc-register-fax-webhook.mjs wires the /api/inbound/fax webhook to the RC fax line in one command instead of hand-clicking the RC dev dashboard. Uses the DEDICATED .biz AT&T Office@Hand creds (GW_RC_* + GW_RC_JWT_GW) — separate from the SMS/voice .com script — with the type=Fax event filter + verification token. App end was already verified healthy (fail-closed 403, idempotent); this closes the 'fax not receiving' gap, which was simply that the subscription was never registered. Doug-step: set RC_WEBHOOK_VERIFICATION_TOKEN, run the script, send one test fax to the RC fax DID. [fax][ringcentral][g8][dev-script]
v2.97.ESIGNUX1
2026-06-19Production

Made the patient e-sign experience a lot smoother — this is the New-Patient Packet patients fill out and sign in the portal.

What this means for you

Made the patient e-sign experience a lot smoother — this is the New-Patient Packet patients fill out and sign in the portal. Four improvements: (1) the signature box now fits the phone screen properly instead of running off the edge (about 70% of patients sign on their phone). (2) Patients now see a live 'Saving… / ✓ Saved' indicator, and if a save fails they get a clear warning instead of silently losing what they typed. (3) A progress bar shows how many required items (3 signatures + 7 initials) are done, with a 'Take me to what's left' button that scrolls to the next missed item. (4) If a submit hiccups, the error is now plain-English ('the connection timed out') instead of a scary code, and reassures them their answers are saved.

Show technical details

Changed

  • ✍️ **Signature pad is now responsive (mobile overflow fixed).** SignaturePad was a fixed 480px wide and spilled off / clipped on phones (~70% of patient traffic is iOS Safari at ~360-390px). It now measures its container and sizes the canvas BUFFER to the available width (capped at 480) so pointer coordinates still map 1:1 — the ink lands exactly where the finger touches at any width. Re-measure freezes once signing starts, so a mid-form rotate never wipes a signature. Benefits all 4 patient forms (packet, ROI, informed-consent, ack). Reviewed clean (coordinate math + legal-capture integrity verified). [patient-forms][e-sign][mobile][a11y]
  • 💾 **Auto-save status + failure surfacing on the New-Patient Packet.** The draft auto-save was fire-and-forget (void fetch) with no confirmation and no error handling — a failed save silently lost typed intake answers. Now shows a live Saving… / ✓ Saved pill and, on failure, ⚠ Couldn't save your last change — check your connection; it'll retry as you keep typing. Net §164.312(c)(1) integrity gain (silent clinical-data loss → visible, retryable). [patient-forms][reliability][hipaa]
  • 📊 **Completion tracker + jump-to-incomplete + plain-English errors.** The long single-scroll packet now has a sticky progress bar (N of 10 required items done = 3 signatures + 7 initials) with a 'Take me to what's left →' button that smooth-scrolls to the first incomplete item (intake sig → consent sig → acknowledgement). Submit button shows the running count when disabled. Submit failures now read 'the connection timed out / a network problem' (never a raw HTTP code) and reassure the patient their answers are saved. [patient-forms][e-sign][ux]
v2.97.PORTALFORMS2
2026-06-19Production

We finished consolidating new-patient intake + consent into one place: the patient portal.

What this means for you

We finished consolidating new-patient intake + consent into one place: the patient portal. Now when a NEW patient books, the system automatically prepares their New-Patient Packet (intake + informed consent, signed digitally) and it shows up in their portal under 'Forms to review & sign' — no separate emailed PDF, no second form to chase. The patient still gets just the one portal-welcome email, then signs everything in the portal. We also removed the old 'Complete your health intake form' link that used to appear separately on each appointment, since it was a second path to the same thing and caused the 'too many forms' confusion Mariane flagged. (The old intake link still works for anyone who already received one.) Renewals are unchanged — no auto-packet for them per Doug's call.

Show technical details

Changed

  • 📋 **New-patient packet auto-prepared in the portal on booking (G7 step 2 — Mariane cmq6203a7, Doug 'new→packet, retire-legacy').** fireAppointmentOnboarding now, for a NEW-patient appointment (appt.isNew), idempotently creates a NEW_PATIENT_PACKET PatientForm (status SENT, 7-day token) so intake + informed consent are waiting in the portal's 'Forms to review & sign' card. **No separate magic-link email** — the existing portal-welcome email is the single touch. Gated behind APPT_AUTO_ONBOARDING_ENABLED (already on). Idempotent (skips if a non-REVOKED/EXPIRED packet exists); PHI-free FORM_CREATED audit (mode=auto-onboarding-portal, no patient name — better than the admin path); system-sentinel createdById. Reviewed clean by hipaa-architect + Explore (the non-atomic find-then-create race is accepted: worst case is a benign duplicate shell, no PHI leak / consent gap). [hipaa][patient-portal][consent][onboarding][g7-step-2]
  • 🧹 **Retired the legacy per-appointment intake nudge.** /my-appointments/[token] no longer shows the separate 'Complete your health intake form → /intake/[token]' amber prompt on each appointment — the single 'Forms to review & sign' card (PORTALFORMS1) is now the one signing surface, removing the duplicate path that caused the 'multiple forms' confusion. The /intake/[token] route itself stays live so any already-issued legacy links keep working. [patient-portal][forms][g7-step-2]
v2.97.PORTALFORMS1
2026-06-19Production

Patients can now see and sign their forms right inside the patient portal.

What this means for you

Patients can now see and sign their forms right inside the patient portal. When a patient opens their appointments page, any consent or intake forms that are waiting for them now show up at the top as a 'Forms to review & sign' card — they tap it, review, and sign digitally, no printing or downloading a PDF from email. This is the first step of consolidating intake + consent into one in-portal flow (Mariane's request to stop the confusing 'multiple forms in different places' experience). It's purely additive — it doesn't change or stop any existing email yet; it just makes the portal the one place to sign. Next step needs your call: which form should auto-generate when a patient books (so the portal always has the right one waiting) and whether to retire the older standalone intake link.

Show technical details

Added

  • ✍️ **In-portal form signing surface (G7 step 1 — Mariane cmq6203a7).** /my-appointments/[token] now renders a 'Forms to review & sign' card listing the patient's pending PatientForm rows (status SENT/OPENED, live token) with a 'Review & sign' link to the existing /patient/forms/[accessToken] e-sign flow. Directly addresses the "seamless digital signing in the portal, not a downloaded PDF" ask. Query scoped to patient.id (the load-bearing fence); accessToken rendering mirrors /patient/portal/forms. Reviewed clean (PHI/auth/XSS). **Additive only** — no email is sent or suppressed by this change. [hipaa][patient-portal][forms][consent][g7-step-1]
v2.97.ADU0001
2026-06-19Production

Staff can now attach medical records directly to a patient's chart and to a specific appointment — for records that come in by fax, email, or in person. Look for the new 'Attach records' button on the patient's Documents tab and in the appointment's Medical Records section; you can select several files at once (PDFs or photos, up to 25 MB each). On the leads side, the document uploader now takes multiple files in one go instead of one at a time. We also fixed a rough edge where a troublesome PDF would fail with a generic error — uploads now give a clear message (e.g. 'it may be corrupt or password-protected') instead of a silent failure. Files are stored on our HIPAA systems and every upload is logged.

Show technical details

Added

  • 📎 **Staff document upload onto patient charts + appointments (ADU0001).** New POST /api/admin/patients/[id]/documents lets ADMIN/MANAGER/SCHEDULER attach a MedicalDocument (uploadedBy="admin", optional appointmentId) for records received by fax/email/in-person — closing the long-standing "Admins can attach files in future" placeholder on the patient DocumentsList and appointment DocumentsPanel. Mirrors the patient-portal + lead-documents pattern exactly: private Vercel Blob (BAA) + compress/EXIF-strip before put() + ADMIN_DOCUMENT_UPLOADED audit with PHI-free detail. appointmentId is ownership-checked against the patient (no cross-patient attach). GET/DELETE legs already live at /api/admin/documents/[id]. Closes reviewer-feedback cmqlrd4pf. [hipaa][phi][documents][admin]
  • 🗂️ **Multi-file upload — lead documents panel + new staff attach.** LeadDocumentsPanel (and the new patient/appointment attach controls) now accept several files in one pick and upload them sequentially, respecting the server's 10-doc-per-lead cap (413 stops the batch) with a clear "N uploaded, then …" message on partial failure. Closes reviewer-feedback cmqlqmzu. [documents][ux]

Fixed

  • 🩹 **PDF upload "there's an error" hardened across all three upload routes.** compressPatientUpload was called unguarded in the lead-documents, patient-portal-records, and (new) admin routes — a decode/compression throw on a corrupt or password-protected PDF (or a sharp image-decode failure) bubbled to an unhandled 500 that surfaced as a generic "there's an error." Now wrapped: returns a clean 422 with an actionable message ("it may be corrupt or password-protected — try re-saving/exporting it") and logs only the error name (no PHI). Closes reviewer-feedback cmqlrabth. [documents][reliability][hipaa]
v2.97.CHARTFIX2
2026-06-19Production

Fixed the root cause behind the provider chart crashes (the ones that bounced doctors back to Practice Fusion).

What this means for you

Fixed the root cause behind the provider chart crashes (the ones that bounced doctors back to Practice Fusion). One small database query on the patient chart's context rail was written in a way the database engine could choke on and crash the whole chart — and it crashed in a way our safety net couldn't catch. Rewrote it as two simple, safe queries that do the same thing (show the patient's latest allergy note) without the fragility. The provider chart canary watches this every 30 minutes, so we'll know immediately if anything regresses.

Show technical details

Fixed

  • 🩺 **Provider chart crash root cause (ARI0003/ARI0004 class) eliminated.** PriorContextRail (embedded on every encounter chart) loaded the latest allergy note with a Prisma nested-relation query — db.intakeForm.findFirst({ where: { appointment: { patientId } }, orderBy: { appointment: { startsAt } } }). Prisma 7.8 can throw on that shape **synchronously**, before a promise exists, so the per-query .catch() never runs and the throw escapes to crash the whole chart (doctors → Practice Fusion). Prior fixes (CHARTFIX1/ARI0004) wrapped the rail in a resilient boundary — a backstop, not a cure. This replaces the query with a flat two-step using only scalar where/orderBy (patient's appointments newest-first → their intake rows by appointmentId in → pick the newest), which Prisma can't choke on. Behavior identical (newest appointment's allergies); reviewed clean against schema. Provider-chart-canary continues to self-validate every 30 min. [provider-portal][chart][prisma][reliability][hipaa]
v2.97.RENEWALMIRROR1
2026-06-19Production

Isabella now handles renewals the same way on chat, text, and email as she does on the phone.

What this means for you

Isabella now handles renewals the same way on chat, text, and email as she does on the phone. If a returning patient is renewing and can get us their records by their appointment, she books them instead of calling it a tentative request stuck behind a records review — and she points them to upload their records right in their patient portal (the new upload box). New-patient messages are unchanged: still a tentative request until the team reviews records. This finishes mirroring the phone behavior across every channel.

Show technical details

Changed

  • 💬 **Renewal booking + portal records rail mirrored to chat / SMS / email.** The phone change (RENEWALBOOK1) now applies on every channel: each AI prompt (chat route.ts, sms-ai.ts, email-ai.ts) branches the booking-confirmation language on patient type. A RETURNING patient renewing who can get records in by the appointment is booked (office confirms exact time) with no records-review gate; the NEW-patient path keeps the tentative-request framing AND its pinned phrases ("tentative appointment request" / "provider must review" — still enforced by the channel-parity tests). All three now point patients to upload records in the patient portal (greenwellness.org → "Your records", the PORTALUPLOAD1 box) as the easy path, with fax/email as fallback. [isabella][chat][sms][email][records]
v2.97.PORTALUPLOAD1
2026-06-19Production

Patients can now upload their medical records right in their patient portal.

What this means for you

Patients can now upload their medical records right in their patient portal. Until now the portal could only SHOW records already on file; there was no way to add one without the emailed secure link. Now there's an upload box on the patient's “Your records” page (sign-in required), so a returning/renewal patient can send their records straight from the portal — which is exactly what Isabella now points them to. We also tightened how uploaded files are shown: any file type that could carry hidden code (like an SVG) now downloads instead of opening in the browser, on both the patient side and the staff/provider side.

Show technical details

Added

  • 📤 **Patient-portal records upload.** New session-authenticated endpoint /api/patient/records/upload + an upload box on /patient/portal/uploaded-records. A signed-in patient uploads a record → it stores to private Vercel Blob (BAA tenant), EXIF-stripped + compressed (same compressPatientUpload pipeline as the emailed-link path), writes a MedicalDocument row scoped to session.patientId only (no IDOR), and shows in their on-file list. 10 MB cap, per-patient fail-closed rate limit, orphan-cleanup on DB failure, PHI-free audit (PATIENT_PORTAL_DOCUMENT_UPLOADED). Reviewed by hipaa-architect (compliant) + security-auditor. This is the path Isabella points renewals to. [patient-portal][records][hipaa]

Fixed

  • 🛡️ **Stored-XSS hardening on document viewers (security-review finding).** Uploaded records are served by 4 routes (patient /api/patient/documents/[id], provider /api/provider/documents/[id], admin /api/admin/documents/[id], and the records-review source viewer). They served the stored MIME inline, so a malicious SVG (image/svg+xml) with an embedded