# Patient Recall Engine

Predicts which lapsed dental patients will come back, ranks them by what they are actually
worth, and runs the outreach sequence until they book.

Built by Mike Rodgers (RIG). Sold to private dental practices at $4,500 setup + $1,200/month.

![icon](icon-256.png)

---

## What is in here

```
recall-engine/
  app/                  the working product (open index.html)
    index.html          shell
    app.css             styles
    engine.js           scoring model, value model, sequencing, campaign simulation
    app.js              UI, live AI copy generation, CSV export, canvas chart
    data.js             260-patient synthetic cohort (generated)
  tools/
    generate_cohort.py  regenerates app/data.js, deterministic
    verify_app.py       10-check Playwright verification of the running app
  landing.html          sales page
  icon.svg              512x512 hand-authored
  icon-256.png          raster export
  icon-512.png          raster export
  GTM.md                ICP, triggers, 5-email sequence, 3 LinkedIn posts, objections, 10 prospect types
  PRICING.md            price, ROI math, sensitivity, displacement, my margin
  README.md             this file
```

## How to run it

No build step, no server, no dependencies.

```bash
open app/index.html        # the product
open landing.html          # the sales page
```

Both are plain HTML/CSS/JS and run from `file://`.

To regenerate the demo cohort:

```bash
python3 tools/generate_cohort.py --n 260 --seed 20260812
```

To verify the app actually works (requires `playwright`):

```bash
python3 tools/verify_app.py
```

This drives a real Chromium instance: loads the app, checks the fleet health indicator,
counts table rows, opens a patient, triggers a live AI draft against the fleet, moves the
capacity slider and confirms the KPIs change, applies filters, re-sorts, runs a campaign,
confirms the chart canvas painted, and downloads the CSV. Last run: **10/10 passed, 0 console errors.**

---

## What the app does

**Recall list.** All 260 lapsed patients scored and ranked by expected recovered revenue.
Set front desk capacity with the slider and the cut line moves; everything below it dims and
is suppressed. Filter by provider, insurance, unused benefits, pending treatment. Sort any column.
Export the work list to CSV.

**Patient drawer.** Click any row. Shows the return probability, a log-odds waterfall of all
15 features that produced it, the first-year value breakdown component by component, and the
5-touch sequence with the per-touch booking probability and the channel chosen for that patient.

**Outreach copy.** Draft SMS / email / call script buttons generate copy from that patient's
own chart facts, live, against Qwen3-Coder-30B on the blackwell node.

**Campaign.** Runs the sequence over 26 days, seeded and deterministic. Reports patients booked,
production recovered, front desk hours consumed, net gain at 38% contribution margin, and a
hand-drawn canvas chart of cumulative bookings and production. Plus the per-patient booked list
showing which touch converted them.

**Model tab.** Every coefficient in the scoring model, the value model, and the sequence hazard,
rendered live from the same object the engine uses. Nothing is hardcoded in the docs.

---

## Real vs. stubbed - the honest list

### Real

| | |
|---|---|
| Scoring model | Real logistic regression over 15 features. Live arithmetic in `engine.js`, coefficients visible in the Model tab. |
| Value model | Real. Four components with stated fee-schedule basis. |
| Ranking and capacity cut | Real. Sorts by probability x value, cuts at the slider. |
| Channel routing | Real rules. SMS if mobile, email if deliverable, phone task only above the $260 EV bar. |
| Campaign simulation | Real seeded Monte Carlo. One uniform per patient against the model probability, conversion attributed to a touch by hazard share. Deterministic, so the same work list always gives the same answer. |
| AI copy generation | **Real and live.** Browser calls Qwen3-Coder-30B on the blackwell node (3x RTX PRO 6000, vLLM). Round trip is 400-900ms. Few-shot prompted, with hard guardrails against inventing staff names, patient quotes, diagnoses, prices, or appointment times. |
| Fallback copy | Real template engine, runs automatically if the fleet is unreachable. The app degrades instead of breaking. |
| CSV export | Real. 16 columns, downloads. |
| Chart | Real canvas, hand-drawn from the simulation output. |
| Filters, sort, search, deep links | Real. |
| ROI math in PRICING.md and landing.html | Real arithmetic, verified against the code. Break-even is stated. |

### Stubbed or synthetic

| | |
|---|---|
| **The 260 patients** | **Synthetic.** Generated by `tools/generate_cohort.py`. Shaped like a PMS export but no PHI and not derived from any real record. |
| **The coefficients** | **Priors, not a fit.** Seeded from published dental reactivation benchmarks and calibrated so the cohort produces a realistic distribution. They have never been fit on a real practice's booked/not-booked outcomes. In a live install that is the first thing that happens after 90 days. |
| **PMS integration** | **Not built.** No Dentrix/Eaglesoft/Open Dental connector exists. Today this is a CSV in, CSV out. The connector is setup-fee work, done per practice. |
| **Message sending** | **Not built.** Nothing sends. The app drafts copy and exports a work list; delivery goes through whatever tool the practice already pays for. This is deliberate - I am not becoming a messaging vendor. |
| **Booking / calendar writeback** | **Not built.** Front desk books in the PMS as normal. |
| **Multi-tenant, auth, persistence** | **None.** Single-practice, in-memory, no accounts, no database. State resets on reload. |
| Case study numbers in GTM.md | The sequence and posts reference cohort figures from this repo, which are synthetic. **There is no paying customer yet.** No email in the sequence claims a named client result, because there is not one to claim. |

### The honest summary

The **decision layer is real and working** - scoring, ranking, value modeling, capacity
allocation, sequencing, and live AI copy generation all run in front of you on real
infrastructure. That is the part that is hard and that is the part being sold.

The **plumbing on both ends is not built** - nothing reads a PMS automatically and nothing
sends a message. Today that is manual setup work, which is exactly what the $4,500 setup fee
pays for. It is honest at 1-5 customers and becomes the bottleneck at 25.

---

## Fleet

Live AI runs on:

```
blackwell   100.67.126.117:8000   vLLM, Qwen3-Coder-30B-A3B-Instruct, 3x RTX PRO 6000
```

vLLM serves `access-control-allow-origin: *`, so the browser calls it directly from `file://`
with no proxy. The ollama nodes on the fleet return 403 on CORS preflight from a null origin,
which is why blackwell is the endpoint wired into the app.

Change the endpoint at the top of `app.js`:

```js
const AI = { url: 'http://100.67.126.117:8000/v1/chat/completions',
             model: 'Qwen/Qwen3-Coder-30B-A3B-Instruct' };
```

If the node is down the health dot in the header goes red and every draft falls back to the
local template engine. Nothing breaks.

---

## Known issues

- Single practice only. No tenancy.
- Nothing persists. Reload resets the campaign and the capacity slider.
- The campaign simulator uses one seed (42). Re-running gives the same answer by design, but
  there is no way to sample a distribution from the UI.
- No mobile layout for the app. The landing page is responsive; the app is desktop-only,
  which is fine because it is used at a front desk workstation.
- Copy generation is one-shot per click. There is no regenerate-with-feedback loop.
