# IRIS Chat — Deploy Prompts

**What this file is:** a recipe card you hand to Claude Code, one step at a time, to turn your working prototype into a real, hostable, publishable iOS + Android app — *without losing anything that already works.*

**You are not writing code.** You paste each prompt into Claude Code on your Mac, it does the work, and you check the result before moving to the next one. This file just keeps the steps in the right order and holds the guardrails that stop anything from drifting off-design.

---

## How to use this file

1. Work through the steps **in order**, one at a time. Do not skip ahead.
2. At the **start of every session**, paste the House Rules (below) first, so the guardrails are always in force.
3. **Commit after each step** (Claude Code will do this — it's just "save a checkpoint").
4. If any step reports something unexpected, **stop and read it** before continuing. The whole point of Step 1 is to catch surprises early.
5. Two decisions are yours alone — settle them at Step 1 (see next section).

---

## Before you start — two decisions only you can make

Claude Code will ask you these. It will not guess.

1. **Admin console:** your one file (`prototype/iris_chat_prototype.html`) contains *both* the doctor app and the admin console. Almost certainly the app shipped to doctors' phones should **not** carry the admin console inside it. Decide: split the admin console out (recommended), or knowingly ship both.
2. **Store listing:** you already have a live IRIS app on both stores. Decide: publish the new app as an **update to that existing listing** (smoother — existing doctors just get the update), or as a **brand-new listing**.

---

## The House Rules (guardrails)

> **Paste this at the start of every Claude Code session before any step.**

```
HOUSE RULES — IRIS CHAT DEPLOY. These override any impulse to "improve" things.

1. SOURCE OF TRUTH: PROJECT_BRIEF_IRIS_CHAT.md and DECISIONS.md. Do not contradict them.
2. DO NOT reimplement, refactor, or "improve" the masking, name-gate, two-net
   extraction, game-vocabulary display, or confirm logic. Preserve it exactly.
3. Masking stays ON-DEVICE. The server must NEVER receive, produce, or store
   unmasked text, and must NEVER log message content. Log events, never content.
4. The ONLY intended change to the app is the DESTINATION of outbound calls
   (the AI call, and the back-office review channel). Nothing else.
5. The Anthropic API key lives ONLY as a server environment variable. Never in
   the repo, never in the app, never printed in logs.
6. Work on a BRANCH. Never edit the original prototype file in place — copy it.
7. Commit after each step. If unsure about anything, STOP and ask me. Do not
   "fix" the design to make something pass.
```

---

## Step 1 — Look before touching (build nothing)

**Why this step:** Measure twice, cut once. Before a single line changes, Claude Code reads the *actual current repo* and tells you the true state — which also catches anything an outdated assumption got wrong. Nothing is built here, so nothing can break. This step also forces your two decisions to the surface.

```
Read in full: PROJECT_BRIEF_IRIS_CHAT.md, PROJECT_STATUS, DECISIONS.md, CLAUDE.md,
prototype/iris_chat_prototype.html, and everything in tests/ and pilot/.

Then, WITHOUT changing anything, produce DEPLOY_PLAN.md that:
(a) Lists EVERY place the app sends data outward — not just the AI call, but also
    the back-office review channel (DECISIONS #47) and anything from the
    attachment (#45) or offline park-and-file (#46, #49) features. For each,
    state where it currently goes and what it will need to point at once hosted.
(b) Confirms the AI call is keyless today and only authenticates via the
    Claude.ai bridge.
(c) Describes the existing session-only local test server so we can reuse it in
    Step 2 rather than rebuild it.
(d) Flags the admin-console question: the doctor app and admin console share one
    file — recommend how to separate them so doctors' phones don't carry admin.
(e) Lists which existing tests in tests/ already protect us, and what's missing.

Do NOT write any app code yet. End by asking me the two decisions:
admin-console split (yes/no) and store listing (update existing vs new).
```

---

## Step 2 — Harden the middleman (the proxy)

**Why this step:** Today the app can only reach the AI *inside* Claude.ai. This creates the small piece — the "middleman" — that lets the app reach the AI on its own once IT hosts it, and safely holds the secret key. You already have a throwaway version of this (the session-only local server), so this **upgrades that**, rather than inventing something new and unproven.

```
Using the existing session-only local test server as the starting point, produce
a small production proxy in /server:
- One endpoint that receives ALREADY-MASKED text from the app, forwards it to
  Anthropic using the ANTHROPIC_API_KEY environment variable (with prompt caching
  per DECISIONS), and returns the response.
- If the back-office review channel (#47) also needs a hosted destination per the
  Step 1 plan, include it here, keeping its whitelist-only, typed-name-only rule
  intact.
- The key is read ONLY from an env var. Never hard-coded, never committed, never
  logged. Add .env.example with a placeholder.
- The server must never mask, unmask, or log message content. It may log events
  (e.g. "request received", "error") but not the content.
- Keep it as simple as possible for a busy IT person to host. Add a short
  local-run README. Commit.
```

---

## Step 3 — Flip the one switch

**Why this step:** This is the single real change to the app itself — pointing its outbound calls at your new middleman instead of the Claude.ai bridge. Everything else stays byte-for-byte identical. Claude Code shows you proof that *only* this changed, so you can see with your own eyes that nothing else was touched.

```
On a COPY of prototype/iris_chat_prototype.html (do not edit the original),
change ONLY the outbound-call destination(s) identified in DEPLOY_PLAN.md — the
AI call, and the back-office channel if applicable — to a configurable proxy URL
read from ONE config value, instead of the Claude.ai bridge.

Everything else stays identical: masking, name-gate, extraction, game vocabulary,
confirm logic, attachments, offline handling — untouched.

Show me a diff proving ONLY the call path(s) changed. Commit.
```

---

## Step 4 — Put it in an app wrapper

**Why this step:** This takes your existing web app and packages it into real iPhone and Android apps — **reusing your code, not rebuilding it.** The result is something IT can open, build, and publish through your existing store accounts. No new features are added; wrapping only changes the container, not what's inside.

```
Wrap the repointed web app into real iOS and Android projects using Capacitor,
reusing the existing web code as-is with NO rewrite.
- Masking must keep running ON-DEVICE inside the app.
- The camera/photo attachment (#45) and offline park-and-file (#46, #49) features
  must be wired to the native equivalents so they work on a real phone.
- Produce standard Xcode and Android Studio projects our IT can open and build.
- Ship the doctor app only, per my admin-console decision from Step 1.
- Add NO features. Commit.
```

---

## Step 4.5 — Make offline reports survive a restart

**Why this step:** This closes the one gap that could actually lose a report. Today, when a doctor files with no signal, the app holds that report in temporary memory and files it automatically the moment signal returns — which works, *as long as the app keeps running*. But temporary memory is wiped if the app is closed, crashes, or the phone restarts. So a report parked while offline could quietly disappear before it ever files. This step moves the parked queue out of temporary memory and into the phone's **permanent storage**, so a waiting report survives the app closing, a crash, or a full restart, and still files itself when signal comes back. Nothing about how reports are written or how names are hidden changes — this only changes *where the waiting queue is kept*. It's the single most important reliability fix before real doctors rely on it in hospitals with patchy signal.

```
On the wrapped app, change ONLY where the offline "parked reports" queue is
stored — the outbox of reports waiting to send, and any half-finished report
being held for the assistant. Move it from temporary memory to the phone's
PERSISTENT on-device storage, so parked reports survive the app being closed,
crashing, or the phone restarting, and still auto-file when signal returns.

- Load the saved queue when the app starts; save it whenever a report is parked
  or the queue changes; remove an item only AFTER it has actually filed.
- Change NOTHING else: not masking, the name-gate, extraction, game vocabulary,
  the confirm logic, or the auto-file-on-reconnect behavior — only the storage
  location of the waiting queue.
- What is stored is codes-only, exactly as today (invariant #1) — never an
  unmasked entity, never a real name written to disk.
- Work on a branch. Prove it: park several reports while offline, fully close and
  reopen the app AND restart the device, restore signal, and show every parked
  report files EXACTLY ONCE — none lost, none doubled. If anything differs from
  the current behavior, STOP and tell me. Commit.
```

---

## Step 5 — Automated QA: prove nothing broke, over and over

**Why this step:** This is the real stress-testing, not a light check. Two things in your system must *never* fail: **no report is ever lost**, and **no real name ever leaks**. This step makes the machine hammer those — and everything else — automatically, and re-run every check on every future change, so a later fix can never quietly break something that used to work.

```
Build and run an automated QA suite. Reuse the existing tests/ (name-hiding,
sample-play replay) and ADD:

1. NO-REPORT-LOST test (highest priority, because of offline): simulate going
   offline, file many reports, come back online, and PROVE every report arrives
   EXACTLY ONCE — never zero, never double. Include nasty cases: app closed
   mid-queue, app restarted, signal flickering on/off.
2. MASKING-HOLDS test: hammer the name-gate with typos, half-names, foreign
   characters, pasted junk, fast sloppy input. A leaked real name is a failure.
3. END-TO-END flow: simulate a doctor filing a report and confirm it lands
   correctly in the admin console — the full journey, not one screen.
4. STRESS test: fire many reports rapidly and feed malformed/extreme inputs;
   confirm nothing crashes or drops. (Same pounding that found the original six
   bugs, now automatic.)
5. SPEED: time the on-device masking and the AI round-trip; flag anything slow.

Make ALL of these re-runnable on every change (regression). Report each as
pass/fail. If anything fails or differs from the original prototype's behavior,
STOP and tell me — do NOT change the design to force a pass. Commit.
```

---

## Step 5.5 — Build the admin console as a standalone browser tool

**Why this step:** In the next step we put the app on real phones and file real reports — and we need a way to check those reports actually arrived. That's what the admin console is for: the back-office screen where the team sees what was filed. But right now the admin console is still buried *inside* the original prototype and shows made-up demo data, so it can't tell us whether a real phone's report got through. This step carves the admin console out into its own separate browser tool that reads the *real* filed reports, so during phone testing we can open it in a browser and confirm, with our own eyes, that a report a phone filed shows up. It also becomes the tool the back office actually uses during the pilot. Nothing on the doctor's phone changes here — this is only about the office-side screen.

```
Build the admin console as a standalone, hostable browser tool that reads the
REAL filed reports. This is the back-office screen; it is NOT part of the doctor
phone app. Work on a branch.

1. FIRST, before building anything, state plainly: once a report reaches the
   backend/proxy, WHERE does it get stored, and WHERE will the admin console read
   it from? If filed reports are not currently stored anywhere the console can
   read (e.g. the proxy forwards but nothing persists them), STOP and surface
   that as a decision for me — do not invent a store or build on a guess.

2. Split the admin console OUT of the original prototype into its own standalone
   browser app in a new /admin folder. It must stand alone (open in a browser,
   hostable on its own) and share NOTHING that puts it on a doctor's phone.

3. Wire it to read the backend's REAL stored reports — not the old in-prototype
   demo/sample data — so reports filed by real test phones actually appear.

4. Preserve confidentiality. The admin console is the ONE intended place a
   sanctioned real name may appear by design — the back-office review-queue
   entries and the feedback entries. Even there it must show ONLY the whitelisted
   fields already defined for those channels. It must NEVER expose the
   codes-to-real-names lookup table, and never surface an unmasked name from a
   codes-only report (plays/stock/etc. stay codes only).

5. Keep it a simple, runnable, hostable browser tool with a short README for IT,
   so it can run locally during phone testing and be hosted for back-office staff
   during the pilot.

6. Make NO changes to the doctor phone app.

7. Prove it: file a report through the flow and show it appears correctly in the
   admin console; show that a review-queue (real-name) entry appears with only its
   whitelisted fields; and confirm that no codes-only report exposes an unmasked
   name. Commit.
```

---

## Step 5.6 — Give the admin console a proper front door (sign-in and roles)

**Why this step:** The admin console is about to become powerful. In the next two steps it will hold the roster (the master list that ties every code back to a real doctor, hospital, and product), turn codes back into real names for the people allowed to see them, and let the back office make changes. Right now it has **no login at all** — the only thing keeping people out is wherever it happens to be hosted (this is difference B2 in `PROTOTYPE_VS_BUILD.md`). That was fine while the console showed nothing but codes. It is **not** fine the moment it handles real names. So before we hand it any of those powers, we put a real front door on it: a proper sign-in, different levels of access, and a running record of who did what. This comes first, on purpose — the powers in the next steps are only safe once this is in place.

```
Build a proper sign-in and role system for the standalone admin console. This is
the front door that must exist BEFORE the console is allowed to hold the roster
or resolve real names (the next steps). Work on a branch.

1. Build real AUTHENTICATION — the console requires a genuine login, not just
   host-level access. No unauthenticated person can reach any view.

2. Support ROLES — at a minimum:
   - a codes-only VIEWER (sees codes only, exactly like the console does today),
   - a name-resolving ADMIN (may see the sanctioned real names).
   The role decides what the person can see and do, enforced on the SERVER, not
   just hidden in the browser.

3. Add an AUDIT LOG that records, at least: who signed in and when; who resolved
   which code to a real name; and who performed which write action. This is the
   accountability trail for every time a real name is revealed or a change is made.

4. The roster is NEVER bulk-downloaded to the browser. A name is resolved one
   lookup at a time, server-side, for an authorized user — the whole code-to-name
   table must never be shipped to the client.

5. Make NO changes to the doctor phone app.

6. Re-run the QA suite and report pass/fail. Commit.
```

---

## Step 5.6A — Let the back office actually work the queues (write-back + roster)

**Why this step:** Right now the console can only look, not touch. Back-office staff could watch the review queue fill up but couldn't resolve anything — unknown doctor and hospital names would just pile up unhandled, a stock alert couldn't be answered, feedback couldn't be closed out. The original prototype could do all of this; the standalone build deliberately started **read-only** for safety (this is the list of deferred actions in `PROTOTYPE_VS_BUILD.md`). Now that the console has a proper front door (Step 5.6), we restore those working abilities — resolving queue items, adding names to the roster, answering a doctor's stock alert — and the roster management that sits behind them. This is what turns the console from a window into a working tool.

```
Restore the admin console's ability to WORK, not just watch — every action behind
the Step 5.6 sign-in and recorded in the Step 5.6 audit log. Work on a branch.

1. Implement the deferred WRITE-BACK actions listed in PROTOTYPE_VS_BUILD.md:
   - review-queue dispositions (add as new, reject/not-real, accept/roll a late
     entry, mark an unknown product noted),
   - add-to-roster (promote a temporary code to a real roster entry),
   - the self-growing alternate-name library (link an unknown typed name to an
     existing roster entry so the app recognizes it thereafter),
   - resolve-and-notify a stock alert (send the update back to the reporting doctor),
   - mark feedback reviewed / mark an issue resolved,
   - lock an Era.

2. Add ROSTER MANAGEMENT — add and edit doctors, hospitals, and products.

3. Add SERVER-SIDE, per-lookup NAME RESOLUTION so the Stock tab and Review queue
   can show real names to AUTHORIZED users — one lookup at a time on the server
   (never a bulk roster download to the browser, per Step 5.6).

4. SEED the roster initially from the prototype's existing DEMO roster data — the
   made-up demo doctors/hospitals/products already in the prototype. Do NOT use
   real Bell-Kenz roster data.

5. Invariants stay intact: the roster NEVER ships to a doctor's phone, and no real
   name EVER reaches the LLM. Every action sits behind the Step 5.6 auth and is
   written to the audit log.

6. UPDATE the QA confidentiality tests to assert the NEW rules (authorized users
   may resolve a name server-side; unauthorized users and the doctor app/LLM never
   can) — update them, do NOT delete them. Then re-run the full QA suite and report
   pass/fail.

7. Make NO changes to the doctor phone app. Commit.
```

---

## Step 5.6B — Make attachments read for real (OCR and file parsing, safely masked)

**Why this step:** When a doctor attaches a photo of a handwritten report, or a file their secretary prepared, the prototype only *pretended* to read it — it asked the doctor to type or confirm the text by hand, because the real reading (photo OCR and Excel/Word/notes parsing) was never actually built (this is difference A6 in `PROTOTYPE_VS_BUILD.md`). This step builds the real thing: the phone actually reads the photo and the file on its own. There is one non-negotiable rule. That raw, freshly-read text can contain real patient and doctor names — so it must go through the very same on-device name-hiding as anything a doctor types, *before anything else happens*, and it must never leave the phone unmasked. We reuse the name-hiding engine that already works; we do not rebuild it.

```
Build real attachment reading for the doctor app, with the extracted text forced
through the existing on-device masking. This CHANGES the doctor app. Work on a
branch.

1. Implement real ON-DEVICE OCR for photos (Apple Vision on iOS, ML Kit on
   Android) — the phone reads the image itself, on-device, never uploading it.

2. Implement native parsing of Excel / Word / text / notes attachments on-device.

3. CRITICAL RULE — non-negotiable: all extracted text (from OCR or a parsed file)
   is RAW and may contain real names. It MUST pass through the EXISTING on-device
   masking and name-gate BEFORE anything else is done with it, and must NEVER
   leave the device unmasked. Extracted text is treated exactly like text a doctor
   typed — same masking, same name-gate, same confirm.

4. REUSE the existing masking engine — do NOT reimplement, refactor, or "improve"
   it (House Rule 2).

5. Because this changes the doctor app, re-run the FULL QA suite INCLUDING the 46
   name-hiding checks, and report pass/fail. If anything fails or a real name can
   reach the screen, the AI, or the network, STOP and tell me. Commit.
```

---

## Step 5.7 — Hand off for hosting (and get the links back)

**Why this step:** Before anyone can test on a phone, the app needs somewhere real to reach. The middleman (the proxy) and the admin console can't live only on your Mac during testing — they have to be running somewhere reachable, or the testers' phones have nothing to talk to. So hosting has to happen *before* the phone testing in Step 6, not after. This step is that hand-off.

The direction matters: you give the hosting party access to the repo and point them at the IT runbook, they stand it up and run it, and they send back two links — the proxy address (what the app calls) and the admin console link (where filed reports show up). Those two links are what Step 6 needs.

There are two ways to do this temporary test hosting, and either is fine: (a) you host it yourself for speed, so you're not waiting on anyone, or (b) you endorse it to IT and they host it. Whichever you pick, this is the **temporary test host** — the place it lives just long enough to run the phone testing. It is *not* the final production home; that's Step 7, done properly for real doctors.

One thing that must stay clear to everyone: improving the app stays yours. The repo is the single source of truth. Hosting is nothing more than running a copy of what's in the repo. The hosting party's job is to run it and to re-host updated copies when you improve it — never to redesign, "clean up," or change the app. Changes come from you, through the repo, and flow outward to whatever is hosted.

```
We are handing the app off to a temporary TEST host so we can do real-phone
testing in the next step. Build nothing new here — this step is about the
hand-off, not app code.

0. FIRST, confirm the storage decision from Step 5.5 is actually settled: where do
   filed reports get stored, and where does the admin console read them from?
   Hosting depends on this. If it is still open, STOP and surface it to me — do
   not hand anything off on a guess.

1. State plainly, in one short note, exactly what gets handed off and what comes
   back:
   - HANDED OFF: access to the repo, plus a pointer to IT_RUNBOOK.md as the
     instructions to follow.
   - COMES BACK: two links — the proxy URL (what the app calls) and the admin
     console URL (where we watch filed reports arrive).

2. The API key is NEVER sent in a message, email, or chat — not to me, not to the
   host, not in the repo. The hosting party generates and holds it themselves as a
   secret on their side, exactly as IT_RUNBOOK.md describes. If anyone asks me to
   send a key, the answer is no.

2a. HARD REQUIREMENT — real admin logins before the console is reachable. The
   admin console ships with committed DEMO logins (admin/admin-demo,
   viewer/viewer-demo) that are public in the repo. Before the console is hosted
   ANYWHERE reachable, IRIS_ADMIN_USERS MUST be set with real accounts so those
   demo logins STOP WORKING. Treat this exactly like the API key: real
   credentials live only in the host's environment, never in the repo, never in a
   message. If the console would be reachable with the demo logins still active,
   STOP — do not hand it off.

3. Give me the SELF-HOST alternative in case I stand it up myself for speed:
   the simplest way to put the /server proxy and the /admin console on a temporary
   hosting service, using a DEDICATED, spending-capped test API key that is
   separate from anything real — one I can disable or delete the moment testing
   ends. Keep it plain enough for me to follow.

4. Spell out the UPDATE LOOP clearly: when I improve the app after Step 6 feedback,
   how does the hosted copy get refreshed? Give me the two options — either I
   re-host the updated copy myself, or the host is wired to auto-update from the
   repo when it changes. Either way, improvements live in MY repo and flow out to
   the testers from there; the hosted copy is never edited directly.

5. Write me a short, plain-language endorsement note I can send to whoever hosts
   it — what this is, that it's a temporary test host, that they follow
   IT_RUNBOOK.md, that they must never send or ask for the API key in a message,
   and that they send back the two links. No jargon.
```

---

## Step 6 — Test on our own phones (BEFORE store submission)

**Why this step:** This is the point most people get backwards, so read it carefully. **You do NOT wait for the app stores to approve anything before testing on your own phones.** Once the app is wrapped (Step 4), it can be installed straight onto your own iPhone and Android — either plugged into the Mac, or through the stores' *private tester channels*: **TestFlight** for iPhone and **internal testing** for Android. Neither needs public store approval. This is the rehearsal that makes the later public launch go smoothly — you and a few colleagues hold the real app, on real phones, *days before* the public ever sees it. Public store approval is the LAST gate, only after you already know it works.

**The correct order:** wrap it (Step 4) → install on our own phones and test (this step) → fix anything found → *then* submit to the stores for public approval.

**Test the real-world shape, not just one phone.** In real use this isn't one person on one phone — it's several doctors, on several different phones, in different places, all filing reports that must *all* land correctly in the *one* shared admin console. That is the thing that has never actually been tried, and it's where the surprises hide. So this step deliberately puts multiple phones against the single console at once, and checks that: reports from different phones don't collide with or overwrite each other; two people filing at nearly the same moment both get through (nothing lost, nothing doubled); each report is tied back to the right doctor through their codes; and the console shows them all, together. The single-phone checks (offline, attachments, responsiveness) still matter and stay — this *adds* the multi-phone dimension on top of them. This stays an **internal-colleagues** test — you and a few teammates, not real doctors yet — because the surname-first name leak (DECISIONS #55) is still open and must be fixed before any real doctor touches it.

```
This is an INTERNAL-COLLEAGUES test — us and a few teammates on several phones,
NOT real doctors yet (the surname-first leak, DECISIONS #55, is still open).
Do two things.

FIRST, write a short how-to (TESTER_INSTALL.md) for our IT person explaining how
to get the wrapped app onto OUR OWN test phones WITHOUT public store approval:
- iPhone: via TestFlight (internal testers) or direct install from Xcode.
- Android: via Play Console internal testing track, or a direct signed build.
Make clear this needs only our existing company store accounts, not a public
review, and that this happens BEFORE any public submission. Note that we need
SEVERAL phones enrolled (a mix of iPhone and Android), not just one, because the
main thing we are testing is many phones reporting into one console.

SECOND, write REAL_DEVICE_CHECKLIST.md — a plain checklist for a non-developer to
run by hand on those test phones, covering:

Single-phone checks (keep all of these):
- Install and run on a REAL cheap/mid-range Android (not just iPhone) and confirm
  the doctor window feels responsive with no obvious lag.
- Install and run on a real iPhone.
- Turn the phone to airplane mode in a real setting, file reports, restore signal,
  and confirm every report filed — once.
- Take a real photo/attachment on-device and confirm it attaches and sends.
- Confirm the admin console (wherever it now lives) receives everything filed.

Multi-phone-to-one-console checks (the real point of this step):
- Several DIFFERENT phones, used by several DIFFERENT testers in different places,
  all pointed at the ONE shared admin console, all filing reports.
- Reports from different phones do NOT collide with or overwrite each other — each
  one lands as its own separate report.
- Have two testers file at nearly the SAME moment; confirm BOTH arrive — none lost,
  none doubled.
- Each report is attributable to the CORRECT doctor via their codes — the console
  shows whose report is whose, and none are mixed up.
- The console shows them ALL, together, at once.

Keep it in checkbox form, plain language.
```

---

## Step 7 — Write IT's instructions

**Why this step:** This is the thing that makes IT's job "just host it and publish it." It's written for a busy IT person who knows *nothing* about the project and assumes the app is already built — so no context is missing and nothing bounces back to you.

```
Write IT_RUNBOOK.md for a busy IT person NOT on this project, assuming the app is
already built. Plain numbered steps:
1. Where/how to host the /server proxy, and where to set ANTHROPIC_API_KEY as a
   SECRET (never in code).
2. How to build and sign the iOS app (Mac + Xcode) and the Android app using our
   EXISTING company store accounts.
3. How to submit — note we already have a live IRIS app, so follow my Step 1
   decision: update the existing listing vs new listing.
4. A final DEFINITION OF DONE checklist they must pass before shipping (see below).
No assumed context. Commit.
```

---

## Definition of Done (must all be true before shipping)

- [ ] **No report lost** — offline park-and-file proven to deliver every report exactly once.
- [ ] **Offline reports survive a restart** — parked queue is saved to on-device storage, proven to still file after the app is closed/reopened.
- [ ] **Masking holds** — name-gate survives typos, half-names, junk input; no real name leaks.
- [ ] **End-to-end works** — doctor files → admin console receives, every time.
- [ ] **Multiple phones report into one console** — reports from several phones all appear correctly in the single admin console, each attributable to the right doctor, none lost or doubled under near-simultaneous filing.
- [ ] **Admin console works as a standalone browser tool** — reports filed by the app appear in it correctly, showing only whitelisted fields, and it's runnable/hostable for back-office staff.
- [ ] **Admin console has a real front door** — it requires a genuine sign-in; roles separate a codes-only viewer from a name-resolving admin (enforced server-side); an audit log records who signed in, who resolved which code, and who made which change; and the roster is never bulk-downloaded to the browser.
- [ ] **The back office can work the queues** — review-queue dispositions, add-to-roster, the alternate-name library, resolve-and-notify, mark-reviewed/resolved, Era lock, and roster management all work behind the sign-in and are audit-logged; the roster still never ships to a phone and no real name ever reaches the LLM.
- [ ] **Attachments read for real** — on-device OCR and native file parsing work, and all extracted text passes through the on-device masking/name-gate before anything else, never leaving the device unmasked; the 46 name-hiding checks still pass.
- [ ] **Real Android tested** — responsive, no obvious lag, on a real mid-range device.
- [ ] **Attachments + offline** work on a real phone, not just the simulator.
- [ ] **Key is secret** — API key only in a server environment variable; not in repo or app.
- [ ] **Real admin logins are set** — `IRIS_ADMIN_USERS` is set with real accounts before the admin console is hosted anywhere reachable; the committed demo logins (`admin`/`admin-demo`, `viewer`/`viewer-demo`) must stop working. Real credentials live only in the host's environment, never in the repo.
- [ ] **Automated tests pass** and are set to re-run on every future change.

---

## What stays with you and IT (code can't do these)

- **Hosting the proxy** and loading the real API key — only your side holds that secret.
- **Building/signing/submitting** with your company store credentials.
- **Passing store review** (and choosing update-vs-new listing).
- **Real doctors** — the pilot is the final test no code replaces. Start with a **staged rollout** (a handful of doctors first, then widen), keep the **old IRIS as fallback** during the run, and watch the field closely.
