# PROJECT_STATUS — IRIS Chat
**Last updated:** 2026-07-27
**Owner:** Von (COO) · **Canonical spec:** `PROJECT_BRIEF_IRIS_CHAT.md` (v1.3)

## Current phase
**Phase B — Deployment build (following `DEPLOY_PROMPTS.md`, step by step).** The prototype is being turned into a real, hostable iOS + Android app via the guardrailed recipe in `DEPLOY_PROMPTS.md`. **Steps 1 through 5.6B are done, all committed and MERGED to `main`** (branch `deploy/step5.6b-attachments-ocr` fast-forward merged 2026-07-23; tree clean; pushed to origin). Steps 1–5.6A: look-before-touching plan → production proxy → repoint the app → native wrap, doctor-only → durable offline → full automated QA suite → standalone admin console on a managed DB → admin console sign-in & roles → admin write-back, roster management & per-lookup name resolution (5.6A also gained paged roster browsing [#59], the product category column, and the units-by-group dashboard card). **What landed on the just-merged branch:**
> - **Step 5.6B — attachments read for real** (DECISIONS #60): on-device OCR for photos (Apple Vision / ML Kit, committed Capacitor plugin `mobile/plugins/iris-ocr`) and native Excel/Word/text reading (unzipped in the web view, nothing uploaded). All extracted text funnels through ONE choke point into the **existing** on-device masking before anything else — tables via the existing code-conversion, free text via the same masking a typed message gets; **no new name-detection was written for attachments, and no raw file/image leaves the device**. Guarded by `tests/attachment_masking.mjs` (11/11).
> - **Surname-first name leak CLOSED** (DECISIONS #55, blocker #4): the all-caps "SURNAME, Given, age" patient form is masked on-device (auto-convert) with an ask-backstop; the proven age/preposition nets are untouched. masking 46/46, fuzz 109, verified live ("2 OTEM for SM-33").
> - **Admin console review before hosting** (Von's three items): **AI Cost** — ported the full prototype dashboard into the standalone console (budget + status, unit economics, trend, codes-only attribution, Sonnet-vs-Haiku projection; DECISIONS #52); **Reconcile** — kept as a labeled placeholder (locked because the Data Team pull-through file isn't connected, not a permissions lock); **Requests** — built a real queue (DECISIONS #61): doctors raise a request (correct/cancel a play, or supply follow-up) masked on-device with unknown names blocked, admins resolve-and-notify + add to-dos. Guarded by `admin/proof/prove_requests.mjs` (17/17) and `tests/request_masking.mjs` (6/6).
>
> The recipe runs 5.5 → 5.6 → 5.6A → 5.6B (DONE) → 5.7 (hand-off docs done; awaiting IT to host) → 6 (multi-phone) → 7. **Step 5.7 — hand-off docs produced (Von chose endorse-to-IT, DECISIONS #62): `TEST_HOSTING.md` (instructions for IT) + `HOST_ENDORSEMENT.md` (the note Von forwards), plus a full plain-language IT handoff package (`README_IT.md` → `ORIENTATION.md` / `METHOD.md` / `HOSTING.md` / `FAQ.md`) and a root `README.md` that routes IT straight to it. All merged to `main` and PUSHED to GitHub. Next actions are external and Von's: grant IT access to the private repo + send the repo link; IT stands up the temporary test host and returns the two links (proxy URL + admin console URL), which unblock Step 6.** **Not started: Step 6, Step 7.** **Before real doctors, ONLY ONE item remains:** setting **`IRIS_ADMIN_USERS` with real accounts at hosting time, before the admin console is reachable** — the committed demo logins (`admin`/`admin-demo`, `viewer`/`viewer-demo`) must stop working (a hard requirement in the Definition of Done + Step 5.7). The storage decision (#8/#56) is settled. See below.

> ▶ **WHERE WE ARE (end of 2026-07-26 session) — all work committed AND pushed to `origin/main` (HEAD `bc3bb09`; tree clean).** Two sessions of work landed today, from Von's live testing. **(A) Doctor-app fixes:** a real hospital-name leak fixed — a comma with no following space ("70yo,makati med") no longer slips a name past masking (#79, masking now 47/47); the assistant asks unit source inside/outside (#76), asks everything missing in ONE scannable numbered message (#78/#80), and out-of-stock alerts now carry the doctor's own masked wording (#81). **(B) Structural single-source-of-truth:** the web app (`app/iris_chat_app.html`) was made doctor-only (embedded admin console + demo `IRIS-ADMIN`/PIN 0000 removed), the durable offline queue was unified into it, and the phone bundle (`mobile/www`) is now GENERATED from it by `mobile/build-www.mjs` — output byte-identical to the known-good bundle, with fail-loud rails (no admin creds, no moved anchors) and a `--check` drift gate. The prototype was cleaned the same way, so the demo admin PIN is gone from ALL files. **A PostToolUse hook now auto-regenerates the phone bundle on every web-app edit and runs full QA against the fresh bundle** (`.claude/regen_mobile_on_app_edit.sh`), so drift is structurally impossible. See DECISIONS #79–#85. **Deployment status unchanged:** Steps 1–5.6B done; Step 5.7 hand-off docs done (now also state the bundle is generated); **still awaiting IT to host** (returns proxy + admin URLs) to unblock Step 6 (multi-phone). Pre-doctor item still open: real admin logins at hosting time. Post-launch: roster items #84 (acRoster duplicates the seed) and #85 (empty roster degrades vs missing falls back). Local test front (8765) left stopped.**
>
> ▶ **WHERE WE ARE (end of 2026-07-23 session) — deploy track, Steps 1 through 5.6B done, all committed and MERGED to `main` and pushed. The just-merged branch added Step 5.6B (attachments/OCR), the #55 surname-first fix, and the AI Cost / Reconcile / Requests console work (see the Current phase block above). Only pre-doctor item left: real admin logins at hosting time. Recap of Steps 1–5:**
> - **Step 1** — `DEPLOY_PLAN.md` (mapped every outbound call; two decisions taken: split admin console out [#53], new store listing [#54]).
> - **Step 2** — `/server` production proxy (holds the API key in an env var; forwards masked text; whitelisted review channel; never logs content).
> - **Step 3** — repointed the app's outbound calls to the proxy on a copy (`app/iris_chat_app.html`); one config value; diff = call path only.
> - **Step 4** — Capacitor wrapper in `/mobile`, **doctor app only** (admin console removed from the phone build); Android project generates here, iOS needs `brew install cocoapods` then one command (documented in `mobile/README.md`).
> - **Step 4.5** — offline parked-reports queue moved to **persistent on-device storage** (survives close/restart; files exactly once; only codes-only reports written to disk).
> - **Step 5** — full **automated QA suite** (`node tests/qa.mjs`: no-report-lost, masking-holds fuzz, end-to-end, stress, speed; reuses the 46-case masking + live e2e; re-runs on every app/test edit via the hook).
>
> **Step 5.5 (2026-07-23) — DONE: standalone admin console on a managed database.** Storage decision settled first (blocker #8 → DECISIONS #56: a managed database). Extended the Step 2 proxy into the shared backend — a SQLite managed-DB local instance (`server/store.py`) persists codes-only filed reports (plays/stock/feedback) + the whitelisted review channel and serves them on GET endpoints; every report write is field-whitelisted server-side and play items reduced to `{code,qty}`, so a real name can't be stored on a codes-only report. Built the standalone `/admin/index.html` browser tool that reads those reports **as codes only** (no roster / code→name view; the only real names shown are the review-queue + feedback whitelisted fields). No change to the doctor phone app. Proof (`admin/proof/prove_step5_5.mjs`, 15/15) files a real report through the shipping masking engine into the store and asserts: the play appears as codes; no real name in any codes-only report; review = whitelisted fields only (smuggled patient/report-text dropped). Verified visually in the browser too (Reports = codes; Review queue = sanctioned names, whitelisted fields). Masking suite still 46/46. On branch `deploy/step5.5-standalone-admin`.
>
> **NOT STARTED:**
> - **Step 5.7** — hand the repo off for temporary test hosting (self-host or endorse to IT); get back the proxy URL + admin console URL (recipe in `DEPLOY_PROMPTS.md`; nothing built).
> - **Step 6** — multi-phone test into the ONE shared console (several phones/testers → one admin console, no collisions, correct attribution) before any public submission.
> - **Step 7** — IT_RUNBOOK.md (host the proxy + managed DB, set secrets, build/sign/submit to the stores) + final Definition-of-Done check.
>
> 🔴 **ONE OPEN ITEM BEFORE REAL DOCTORS:**
> 1. ~~**Surname-first name leak (DECISIONS #55, blocker #4).**~~ **RESOLVED 2026-07-23.** The all-caps surname-first patient form ("for SANTOS, Maria") is now masked on-device — a new `extractSurnameFirst` auto-converts the "SURNAME, Given, age" shape (preposition-led and no-preposition), and the net-3 gate no longer mistakes an all-caps surname for a code (asks as a backstop). masking 46/46, holds 109, verified live ("2 OTEM for SM-33"); blocker #4 closed.
> 2. **Real admin logins before hosting (DEPLOY_PROMPTS DoD + Step 5.7).** 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 the demo logins stop working — real credentials live only in the host's environment, never in the repo. Hard requirement, gates Step 5.7 hosting.
> - ~~**"Where do filed reports get stored?" (blocker #8)**~~ — **RESOLVED 2026-07-23 (Von): a managed database** (DECISIONS #56), built in Step 5.5 (SQLite local instance now; hosted DB in production). No longer a blocker.
>
> Still carried for the hosted build (not blocking the deploy steps): open item #7 (P2 feedback masking, P4 logout wipe, R1 HTML→structured-segment masking); #5 (backend token-cost controls); the Sonnet-vs-Haiku model decision (#50).

> ⚠️ **AT SESSION START — proactively offer this to Von, don't wait to be asked:** the full app (with live assistant replies) runs on the Mac only via a **session-only local test harness** (mini-server + API key proxy) that must be recreated each session. If Von wants to test the real experience, rebuild it: (1) copy `prototype/iris_chat_prototype.html` into the session scratchpad, rewriting the `https://api.anthropic.com/v1/messages` URL to `/api/messages`; (2) run a small Python mini-server there that serves the file and forwards `/api/messages` to the Anthropic API with a key read from a local `.apikey` file (never committed); (3) launch via the preview tool on port 8765. Von will need to paste an Anthropic API key (and should rotate the one from 2026-07-06). Without this, `localhost` shows only the on-device masking half — the assistant can't reply. **As of 2026-07-07 the harness (`scratchpad/local_app.py`) is also a tiny shared backend:** it stores data and exposes `GET/POST /api/plays`, `/api/roster`, `/api/review` (JSON store in `scratchpad/store.json`), so the doctor app and admin console talk to one backend end-to-end. Also session-only — rebuild each session. The prototype calls these best-effort with graceful fallback, so it still runs standalone (e.g. inside Claude.ai) without them.

## Done so far
- 2026-07-27 · **App icons no longer depend on the phone: emoji swapped for our own drawings, so Android matches iPhone (DECISIONS #86).** Von saw that the icons were "default to iPhone" while some doctors use Android. Every control was an emoji character, and emoji are drawn by the phone's own font — so the same character looks different on iOS and Android, and three were broken on Android: the sign-out power symbol (not an emoji at all — grey glyph or empty box), the mountain on TERRA (no colour version, fell back to a flat grey triangle, visible in Von's screenshot), and the woman-doctor on the Co-Player button (a joined emoji that splits in two on older Android). Replaced with 12 hand-drawn SVG icons defined once in `app/iris_chat_app.html` and reused across the Player pill, the three header buttons, the BLAZE/BREEZE/TERRA bars, the "my plays" button, attach, the Location quick-chips, and the three name-check buttons. They take the colour of the text around them, so nothing else needed restyling. **No masking, extraction, crediting, or AI-prompt code was touched;** emoji inside chat sentences (🔥 📅 ✓ ⚠) were deliberately left alone — they are prose, not controls. Verified by rendering the real header/bars/chips/composer/name-check card headless in Chrome at 1× and 2× (all 12 icons correct at 64px, 14px, 12px). Phone bundle regenerated and in sync; **QA green — all 8 runnable suites pass** (masking 47/47, holds 109). Still on emoji: `pilot/iris_chat_tester.html` and `prototype/iris_chat_prototype.html` — cosmetic only, neither ships to a doctor's phone.
- 2026-07-26 · **Auto-regenerate the phone bundle on every web-app edit + QA the fresh bundle (PostToolUse hook; extends DECISIONS #83).** Editing `app/iris_chat_app.html` now automatically runs the generator (`mobile/build-www.mjs`) to rebuild `mobile/www`, then the existing QA hook runs against that fresh bundle — closing the gap where QA read the old bundle and never read `app/` directly. Regen fails loud (blocks) if the generator refuses (admin credentials in output, or a moved anchor). Wired between the test-app refresh and the QA hook in `.claude/settings.json`; script `.claude/regen_mobile_on_app_edit.sh` fires only on web-app edits. Verified end-to-end (a marker added to the web app auto-flowed into the bundle and reverted). Committed + pushed (`bc3bb09`). Note: a mid-session Xcode-license prompt briefly blocked git until Von accepted it.
- 2026-07-26 · **Steps 5b/5c — phone bundle now GENERATED from the web app; prototype made doctor-only; drift made structurally impossible (DECISIONS #83; post-launch items #84/#85).** Built `mobile/build-www.mjs`, which produces the phone bundle from the single source `app/iris_chat_app.html` by three tiny transforms (clean title, `config.js`, two blank lines) — output byte-identical to the known-good phone bundle at commit 66df914 (same sha b6d3632). Fail-loud rails (all negative-tested): each transform must match once; the output is rejected if it contains admin credentials; the test-only source override can't write the real bundle. Added `node mobile/build-www.mjs --check` (regenerate-in-memory drift gate: exit 0 in sync, 1 on drift, writes nothing). Updated the handoff docs (`mobile/README.md` + ORIENTATION.md + FAQ.md) to say www is generated, never hand-edited. **5c:** applied the Step-2 doctor-only trim to `prototype/iris_chat_prototype.html`, so the demo `IRIS-ADMIN`/PIN 0000 credential is gone from ALL files; masking 47/47 + holds 109 still verified against the prototype. Logged two post-launch roster items: acRoster duplicates the seed (#84), and an empty roster degrades masking while a missing one falls back (#85). On `main`.
- 2026-07-26 · **Step 5a — durable offline queue ported into the web app (unifies it with the phone bundle; DECISIONS #82).** The web app (`app/iris_chat_app.html`) still had the older in-memory parked-reports queue, while the phone bundle (`mobile/www`) had the durable one (Step 4.5: survives app close/restart, files exactly once, only codes written to disk). Spliced the phone bundle's exact offline region into the web app so the two are now byte-identical there; the phone bundle was NOT touched. Verified: the durable-offline suite (no-report-lost, 27 checks) now passes against the web app, matching the phone bundle's 27; masking still 47/47 + 109 holds against the web app; full QA green; the single script block compiles. The web app and phone bundle now differ in ONLY the two-line deploy header (title + `config.js`) and two blank lines — clearing the way for the trivial generator (Step 5b). Phone bundle confirmed byte-identical to commit 66df914 (sha b6d3632). On `main`.
- 2026-07-26 · **Admin Stock tab shows the doctor's own (masked) wording for each out-of-stock alert (DECISIONS #81).** Von wanted the doctor's exact message shown in the Stock tab as a reference for the precise wording and tone, formatted so the row isn't crowded. What's shown is the on-device masked message (codes only — same text the AI and the doctor's own bubble see), never raw, so no real name reaches the admin store. The doctor app now remembers the masked message and attaches it to the stock alert (`note`); the server whitelists that one masked field (smuggled fields still dropped); the admin row shows it on its own line as a quiet italic quote under the codes. Verified live: "Good am. PRAZ out of stock at SATURN 33 — wala na since last week" (Taglish tone intact, codes only). QA green; all 7 admin proofs pass on a clean backend. Committed with the day's other changes.
- 2026-07-26 · **Two fixes from Von's live test: a hospital-name leak on a glued comma, and clarify messages reformatted for readability (DECISIONS #79, #80).** Testing "gave 2 praz to MC 70yo,makati med …", Von found **"makati med" reached the AI in the clear** — a real hospital name leak. Cause: the on-device masker splits on spaces only, so "70yo,makati" (no space after the comma) became one token and the known "makati med"→NEPTUNE 21 alias never matched. Fixed in all three engine copies (app, mobile/www, prototype) by un-gluing a comma/semicolon that has no following space, but only before a letter so "1,000" stays intact — masking core untouched. Added a scripted regression case; **masking now 47/47**, holds 109, full QA green, and verified live (glued "70yo,makati med" and "65yo,makati med" both mask to NEPTUNE 21). Separately, Von asked to make the assistant's questions easier for a busy doctor to read: multi-part clarifies were one dense paragraph. Now the assistant lays them out as a short lead line + a numbered list (one question per line), and the chat renders the line breaks (new `aiProse` helper) — verified live. **Flagged:** mobile/www (the phone bundle) is behind on the #76/#78/#80 doctor-prompt changes and needs a dedicated resync before it ships. Not committed yet.
- 2026-07-26 · **Assistant batches all missing-info questions into one message; no re-asking (DECISIONS #78).** Von, testing the source feature, found the assistant asked missing details one at a time (two messages) and once almost re-asked "single or multiple" after he'd already said "just me". Changed the assistant's instructions (app + prototype; no name-hiding logic touched) so it asks for everything still missing in a single numbered message the doctor answers at once, folds the inside/outside question into that same message, and never asks — or "just to confirm"-s — something already stated or implied. Also fixed a splitting bug the live test caught (several products for the same patient at the same Location is one play, not a batch). Net effect: fewer back-and-forth turns and fewer AI calls (lower cost). Verified live: "gave 2 praz to Maria Santos 76 at Makati Med" → one message asking both inside/outside and single/multiple; "inside and just me" went straight to the confirm card. Masking 46/46 + all suites green; live dialogue eval 13/13. Not committed yet.
- 2026-07-26 · **Sales org restructured: Teams → Managers → Reps, with focus-filtered assignment (DECISIONS #77).** Von asked to turn the flat sales-rep list into a real org: reps report to specific managers (a manager table), teams exist (KASD for doctors, Hospital for hospitals, maybe a "both" team later) with managers/reps tagged to a team, the sales Add button should use the roster's floating dialog with dropdowns, and the Sales team tab should sit after AI Cost. Von chose: a rep's team + manager are set directly on the rep, and a team's focus filters who's assignable where (a doctor offers only doctor-side people, a hospital only hospital-side). Built across server + console (doctor app + the codes-only pipeline untouched): new team/manager entity types with focus, a resolver so the by-rep dashboard labels managers vs reps, a coherent reseeded demo, the Sales team tab as three sub-views (Teams/Managers/Reps) with a floating Add dialog whose dropdowns narrow sensibly (a rep's manager list follows the chosen team), inline edit, archive, and the roster assignment dropdowns filtered by focus. Verified live end-to-end; QA green (masking 46/46); all 7 confidentiality proofs pass (rep endpoints 44/44, no doctor-name leak). Runs on the session-only local stack. Not committed yet.
- 2026-07-26 · **Assistant asks unit SOURCE (inside/outside) + admin Reports Source column (DECISIONS #76).** Von, testing the local stack, noticed the confirm card already had the inside/outside (I/O) split but the assistant never asked — it silently defaulted every unit to inside — and the admin Reports tab had no column showing where the product came from. Audited first: the I/O model already runs end-to-end in the doctor app; the only gaps were the AI not being required to ask, and the backend stripping `source` to `{code,qty}` before storage. Built three small changes with no masking logic rewritten: (1) a NEVER-ASSUME rule so the assistant must ask "inside the Location (I) or outside (O)?" before confirming, splitting an Avatar into two items when it's some of each (app + prototype); (2) the backend now carries a validated source (H/OH only) through to storage — it's game-vocabulary, not a name; (3) a new **Source** column (Inside · I / Outside · O) on the admin Reports table and CSV. Verified live end-to-end: JD28 logged 6 OTEM, answered "4 inside, 2 outside" → card showed OTEM6 (I4-O2) → backend stored two items with H and OH → admin Reports shows Inside · I (4) and Outside · O (2). QA green (masking 46/46, all suites); all 7 console proofs pass (codes-only proof 15/15 with the new field); confidentiality intact (source is codes only, the whitelist still drops any smuggled name). Runs on the session-only local stack (real backend `server/proxy.py` + both web pages). Not committed yet.
- 2026-07-25 · **Sales-rep coverage + by-rep dashboard (DECISIONS #75).** Von asked to assign sales reps (Manager / Rep 1 / Rep 2) to doctors and hospitals and break the dashboard down by rep to see who's driving adoption. Built: a new top-level **Sales team** tab to manage reps (code, name, role); **Sales team columns + dropdowns** on the Doctor and Hospital rosters (add + edit); and two admin-only dashboard cards — **doctor adoption** (per rep: active vs total doctors, with a LAGGING flag, plays, units) and **location coverage**. Reps are internal staff in an admin-only roster; assignments are codes, resolved the sanctioned admin-only way (new `/api/admin/resolve-reps` + `/api/admin/rep-coverage`), never touching the codes-only pipeline or the AI. The list can sync from an HR/CRM system in production. Verified live; proof 44/44; all proofs + QA green. Doctor app untouched. On branch `improve/gate-location-vocab`.
- 2026-07-25 · **Open-item count badges on the work-queue tabs (Review queue / Issues / Stock).** A red count pill on each of those tabs flags how many items still need action, so staff see the workload without opening each tab. Counts: Review queue = undisposed reviews + open play-corrections; Issues = unresolved duplicate groups; Stock = open alerts + open supply follow-ups. Recomputes on every load/resolve; hides at zero. Console only; verified live (3/1/2). QA green.
- 2026-07-25 · **Admin tab order follows the back-office workflow (Von's sequence).** Dashboards · Roster · Reports · Review queue · Issues · Stock · Reconcile · AI Cost · Feedback · Audit log (see the picture → set up → output → act → ops → oversight). The removed Requests tab's slot is taken by the locked Reconcile placeholder. Display order only; landing tab still Review queue (offered to change to Dashboards). Console only.
- 2026-07-25 · **Issues ⇄ Review Queue linked; a duplicate + the doctor's request resolve in one click (DECISIONS #74).** Von noticed one real problem (a double entry) can surface in both tabs — auto-detected in Issues and reported by the doctor as a cancel/correct request in the Review Queue — and asked to resolve it with minimal tab-hopping. Now the corrections card flags "possible duplicate — View in Issues →", and the Issues duplicate card shows the doctor's matching request inline with a pre-checked "reply when I void"; "Keep this one — void the rest & reply to doctor" voids the extra copy, fixes the report, and notifies the doctor in one click (matched by doctor+Era, request shown in full for a human check). Verified live; console only; all proofs + QA green. On branch `improve/gate-location-vocab`.
- 2026-07-25 · **Action queues scaled for the back office (DECISIONS #73).** Von asked how the console copes with hundreds of items without burying open work. The Review Queue, play-corrections card, and Stock previously showed every item (resolved included) with no filter/paging. Now all three **default to Open** (with an Open/All toggle), have a **text filter**, **page at 25** with a "Showing X–Y of Z" line, and show **"N open · M total"** in the header; the Review Queue also has **type chips** (All / Unknown names / Territory). Verified live with 32 seeded reviews. Console only. Production follow-up noted: page these reads server-side (like the roster) at thousands of items. On branch `improve/gate-location-vocab`.
- 2026-07-25 · **Requests re-homed to where the work lives; play correct/cancel now executable (DECISIONS #72).** Von asked to drop the standalone Requests tab and surface requests contextually. **Supply follow-ups** now appear in the **Stock tab** (beside out-of-stock alerts, same resolve-and-notify), and **correct/cancel-a-play** requests appear in the **Review Queue** as their own "play corrections" card with a reply-to-doctor note and an "Open Reports →" link. Execution lives in the **Reports tab**, which now has per-play **Edit** (fix units / Avatar / Location, codes only, audited — new `/api/admin/play/edit`) and **Void** (cancel). The Requests tab and the admin to-do feature were removed. Doctor app + request storage untouched (doctors still read resolutions back). Verified live (Stock resolve, corrections card, Edit 4→2, Void); proof 38/38; all proofs + QA green. On branch `improve/gate-location-vocab`.
- 2026-07-25 · **Unknown-Location resolution now closes the loop (DECISIONS #71; answers Von's "1 item or 2?").** Tested: a single report at a Location that's both unknown to the roster and not on the doctor's list shows as **one** review item, not two — the territory check skips temporary/unknown Locations, so it's a single "unknown Location, add it" item. Von asked to make that one resolution do everything: now **Add to roster** for an unknown Location also (a) moves the filed play off its temporary code to the real Location (the #66 Location fast-follow, now done — pending badge clears), and (b) offers a **pre-ticked "also assign this Location to the reporting doctor"** so it won't be flagged off-assignment next time. Verified live (Cebu Doctors Hospital → CEBU 45: play moved off LOC-PEND-9, JD28 assigned, audited). Proof 33/33; all proofs + QA green. Server + admin only; doctor app untouched. On branch `improve/gate-location-vocab`.
- 2026-07-25 · **Roster/review ease-of-use pass (DECISIONS #70): pick Locations from the roster, access ID defaults to the alias, guided add + Add-with-preview dialog.** Follow-ups Von asked for after using #68/#69, all console-only. **(1)** The off-assignment **Reassign** now offers a **dropdown of existing Locations** instead of typing a code. **(2)** Assigning a doctor to Locations (add + edit) is now a **tick-box picker** of existing Locations, not comma-separated codes. **(3)** Issuing a doctor's **Access ID pre-fills their alias** (easy to recall), still editable. **(4)** The **add experience** for all three roster types is a clean **"Add …" dialog** (Add Doctor / Add Location / Add Product) with a code auto-suggested from the name, a live "available / already used" check, the Location picker for doctors, a unit field for products, and a live preview of how the entry will read before you save. **Layout declutter (same day):** the crowded inline add form was removed, the Active/Archived toggle became a compact segmented control in the table's top-right, and the single Add button sits next to Filter. No server change, so every confidentiality proof still passes (27/27 on the new proof, plus 15/15, 19/19, 43/43, 17/17, 10/10, 9/9) and QA is green (masking 46/46, live 13/13). Doctor app untouched. On branch `improve/gate-location-vocab`.
- 2026-07-25 · **Off-assignment flag fixed + doctor roster gains assigned Locations and admin-issued access ID/PIN (DECISIONS #68, #69).** Von, testing the local stack, asked why a valid-looking play (JD28 @ NEPTUNE 21) sat in the Review Queue. It was the territory flag (a doctor logged a play at a Location not on their assignment list) shown with only "Mark handled" — and the console was printing the Location **code** in the red real-name column, so a code looked like a leaked name. **Three things done. (1) Off-assignment flag:** kept in the Review Queue but now with real actions — **Add to their Locations**, **Deny** (Von's call: this also **voids the play** so it drops out of report totals, reversible), and **Reassign** (fix a mistyped Location); the row now reads "Player not assigned to this Location" instead of dressing a code up as a name. **(2) Doctor roster shows assigned Locations** as codes, editable — this was in the prototype and drives the flag; the "Add to their Locations" action writes here. **(3) Admin issues sign-in access:** an **Access ID** (the company card, unique per doctor) and a **PIN** that is **set/reset only** — stored hashed, **never shown again** and never logged (the console shows only "✓ set"); the admin sets it and hands it to the doctor. Note surfaced in the console: on this local test app the doctor app still signs in with its built-in demo cards; the hosted build will use these. Verified live end-to-end (add/deny/reassign; PIN set flips "✓ set" with no secret in the response; duplicate ID rejected; viewer blocked; audit carries no PIN). New proof `prove_assign_and_credentials.mjs` 27/27; QA green (masking 46/46, live 13/13); all prior proofs pass on a throwaway backend. Server + admin console only; doctor app untouched. On branch `improve/gate-location-vocab`.
- 2026-07-24 · **Review Queue + Issues: clearer columns, cross-tab reference numbers, and real duplicate resolution (DECISIONS #67).** Three things Von raised. **(1)** The Review Queue's "Temp code" column was actually showing the doctor — fixed to show the real temporary code, and added a **Sent by** column (the doctor who typed it) so the back office can clarify with them directly. **(2)** Every review entry and filed play now has a visible **reference number** (RV-… and R-…) that is **clickable to jump between the Review Queue and the Reports tab**; the link is recorded when the play is filed so it stays reliable even after a name is resolved. **(3)** The Issues tab now actually resolves a possible duplicate: **Keep all (not a duplicate)** or **Keep one — void the rest**, where voided copies are set aside (reversible, restorable, audited) and drop out of the report totals and the doctor's own view. Verified live end-to-end (columns, two-way ref jumps, keep-one → voided + report drops it, Restore). New proof `prove_trace_and_void.mjs` 9/9; QA green; all prior proofs pass on a clean backend. On branch `improve/gate-location-vocab`.
- 2026-07-24 · **Resolving a temporary Co-Player now closes the loop on plays already filed (DECISIONS #65).** Von asked whether resolving a PEND code in the Review Queue updates both the admin report and the doctor's own "my plays" window. It didn't — the temp code was baked into the filed play, so the ⏳ "pending review" badge never cleared and the admin report kept the temp code. Now: **Add/Link** rewrites the play's Co-Player to the **permanent code** (codes only, never a name; badge clears); **Not real** keeps the play but marks the slot **"not part of the game."** Scoped to the doctor who filed it, so one doctor's PEND-1 never touches another's. Both the admin Reports tab and the doctor's window reflect it; every resolution is audited with the count of plays updated. Verified live end-to-end (JD28: PEND-1→MIGS, hourglass gone; rejected→"not part of the game"). New proof `prove_coplayer_backfill.mjs` 10/10; QA green; all four console proofs pass on a clean backend. Co-Player only for now — the same fix for an unknown Location is a fast follow. On branch `improve/gate-location-vocab`.
- 2026-07-24 · **Roster archive/restore — the reversible "delete" (Von asked where the delete function was; DECISIONS chosen: Archive).** There was no delete at all — only Add and Edit. Built **Archive** (reversible) rather than a hard delete: an entry can be archived out of the active roster and restored any time; it's never lost, a past report's code still resolves to its name, and there is deliberately no permanent-delete endpoint. Console: an Archive button per row (with a plain-language confirm), an Active/Archived toggle, Restore in the archived view; the Add form is hidden while viewing archived. Server: an `archived` flag, the browse list filters active vs archived, per-lookup resolve still finds archived entries, and a new admin-only, audited archive/restore endpoint (viewer 403, unauth 401). Verified live end-to-end (archive → leaves active list → appears in Archived → still resolves → restore → back). On branch `improve/gate-location-vocab`.
- 2026-07-24 · **Admin console density pass (Von — "font too big, table too big"). Presentation only; doctor app untouched.** The whole console scales off one CSS variable (`--fs`), and it was set oversized for a data tool (`clamp(15.5px … 21px)` — ~21px on a wide monitor). Lowered it to `clamp(13px … 15.5px)` so everything shrinks proportionally to a proper back-office data density, and tightened the review/data table rows (cell padding 9px→6px). Verified live at desktop width: the review-queue table now fits each row (type · code · name · action buttons) on one line without wrapping, and the AI Cost dashboard tiles/tables stay balanced; no console errors. Console-only CSS — no data path or exposure changed; QA still green. On branch `improve/gate-location-vocab`. Easily dialled further if Von wants it smaller/larger.
- 2026-07-24 · **Two fixes Von found while testing the local stack (DECISIONS #63, #64). On branch `improve/gate-location-vocab` — `main` deliberately frozen while IT stands up the test host.** **(1) The "quick check" card now offers a Location, and speaks game vocabulary.** When the app spots a leftover word that might be a real name it asks the doctor who it is. A hospital the roster doesn't know yet had **no correct answer** — the only safe-looking choice sent the typed hospital name in the clear. Added a **📍 Location** option that takes the same route an unknown Co-Player already takes: a temporary code on screen, the typed name to the back-office review queue. Also relabelled the buttons — "Patient" and "Another doctor" are real-world words on a screen a colleague can read (invariant #6) — to **Match** (Von's choice) and **Co-Player**. No masking logic was rewritten; it reuses what was already proven. **(2) The review queue could not add a name to the roster.** Real entries showed only "Mark handled": the console and the server both recognised only the older internal spellings of "unknown doctor / unknown hospital", not the ones the app actually sends, so every genuine unknown name fell through to a dead end. Fixed both, and **restored the original console's clean-up flow (#37)** in place of the bare prompt box: the admin corrects the spelling the doctor typed, assigns the permanent code, and for a doctor ticks the Locations they cover. The raw typed wording is kept as an alternate name and pushed to the phones, so the same wording is recognised from then on. Verified live end-to-end on the local stack (typed name → temporary code on screen → review queue → cleaned-up roster entry → alternate-name link), every disposition audited. QA green (masking 46/46, all nine suites); proofs 5.5 15/15, 5.6 19/19, 5.6A 43/43, requests 17/17.
- 2026-07-24 · **IT handoff package on `main` (merged + pushed).** A self-contained set of plain-language docs for an IT team with no project or AI-development context: `README_IT.md` (index + reading order) → `ORIENTATION.md` (what the system is, the on-device name→code idea, the three pieces, and a two-terminal local run + guided tour) → `METHOD.md` (the working method with real repo examples; honest about the 5.6A scope creep and that the written records caught it) → `HOSTING.md` (the task + six hard requirements + what to send back) → `FAQ.md`. Cross-references the existing `TEST_HOSTING.md` (the click-by-click steps) rather than duplicating it. Added a root `README.md` so a bare repo-link landing page routes IT straight to `README_IT.md` — so endorsing is just: grant repo access + send the link. Security audit done first (read-only): repo is private; no real API key / credentials / patient data committed; the only committed sensitive items are the demo logins (by design) and demo doctor cards; the one caveat is that the demo roster uses some real public hospital names as sample data. All committed to `main` and pushed; tree clean, in sync with GitHub. Note: `IT_RUNBOOK.md` (the production runbook, Step 7) is intentionally not written yet.
- 2026-07-23 · **Deploy Step 5.7 — hosting hand-off (DEPLOY_PROMPTS.md, DECISIONS #62). Docs only; no app code.** The hand-off so real-phone testing (Step 6) has a reachable place to talk to. Confirmed the storage gate is settled first (managed DB, #56). Laid out both paths (self-host vs endorse-to-IT) with what each needs from Von; **Von chose endorse-to-IT.** Produced two plain documents: **`TEST_HOSTING.md`** — a step-by-step temporary-test-host sheet for IT (host the Python `/server` proxy with the API key + real admin logins as secrets and its database on a persistent volume → proxy URL; host the single-file `/admin` console pointed at the proxy → admin URL; set the doctor app's one proxy-URL value; plus the update loop and a done-checklist), and **`HOST_ENDORSEMENT.md`** — the plain note Von forwards to IT. Both carry the two hard rules: the API key is generated/held by IT and never sent in a message or the repo, and `IRIS_ADMIN_USERS` must be set with real accounts before the console is reachable (the last pre-doctor item). Only the two links come back — never the key. It's a temporary test host, not production (Step 7). On branch `deploy/step5.7-hosting-handoff`, **not merged (stopped per Von).** **Next is external:** IT stands it up and returns the proxy + admin URLs, which unblock Step 6.
- 2026-07-23 · **Admin console review before Step 5.7 (Von's three items) — AI Cost full dashboard, Reconcile clarified, Requests queue built (DECISIONS #52, #61).** Von reviewed the standalone admin console before hosting and raised three items. **(1) AI Cost** — the tab was a slimmed-down version; ported the **full prototype dashboard** into the standalone console (period selector, monthly budget + red/amber/green status, unit economics, by-model table, cost-over-time trend, codes-only attribution by Player/Location/function, Sonnet-vs-Haiku projection). Still codes only; the proxy now stamps a server-side timestamp so the time-based views work. **(2) Reconcile** — clarified it's locked because the Data Team pull-through file isn't connected (not a permissions lock); **kept as a labeled placeholder** per Von. **(3) Requests** — was a locked placeholder; **built a real queue**: doctors raise a request from the app (correct/cancel a play, or a supply follow-up), the text is masked **on the device** (unknown names blocked — never sent), it lands in the admin Requests tab, the back office resolves with a note that's pushed back to the doctor, and admins can add their own to-dos. Verified live end-to-end (a request naming "Kenzolin at Makati Med" stored as "PRAZ at NEPTUNE 21"; the doctor saw the resolution on sign-in). Proofs: requests 17/17, request-masking 6/6, AI-cost verified live; QA green (46/46), 5.6a proof 43/43. On branch `deploy/step5.6b-attachments-ocr`.
- 2026-07-23 · **Surname-first name leak CLOSED (DECISIONS #55, blocker #4) — done before Step 5.7 at Von's direction.** The last real safety gap before real doctors. The app hid a patient written the normal way ("Maria Santos, 52") but leaked the surname when written **surname-first in all-caps** ("for SANTOS, Maria, 52") — it mistook the all-caps surname for one of its own codes. Von chose the thorough fix (auto-convert + ask backstop). Now the app **auto-hides** the "SURNAME, Given, age" shape on the phone (both "for SANTOS, Maria, 33" and the no-preposition "SANTOS, Maria, 52 …"), folding both names into one code; and the name-gate no longer treats an all-caps surname as a code — anything the auto-convert misses gets the same one-tap "is this a patient?" question. Careful guards keep it from questioning real codes or medical shorthand (IV, MG, STAT) or nagging on ordinary words. **The name-hiding engine's proven core (the age and preposition nets) was not touched — this only adds the surname-first handling around it.** Verified: name-hiding **46/46** (no regression), the fuzz suite grew to **109** checks (now asserting the surname itself is hidden, in all-caps and no-preposition forms), full QA green, and an adversarial probe shows normal reports gain no false questions. **Verified live** (signed in as JD28): "2 prosec for SANTOS, Maria, 33" showed on screen as **"2 OTEM for SM-33"** — no real name anywhere. Applied to the shipping bundle, the app copy, and the reference prototype. On branch `deploy/step5.6b-attachments-ocr`.
- 2026-07-23 · **Deploy Step 5.6B — attachments read for real: on-device OCR + native Excel/Word (DEPLOY_PROMPTS.md, DECISIONS #60).** Replaced the prototype's stand-ins (a photo asked the doctor to *type* what it said; Excel/Word were refused with "export to CSV") with real on-device reading — and kept the one rule that can't break: every bit of freshly-read text runs through the **existing** on-device name-hiding before anything else, and no image or file ever leaves the phone. **Two readers:** (1) **Excel (.xlsx) / Word (.docx)** are read right in the app on the phone (they're zip packages — the app unzips and pulls out the text using a built-in browser feature, no extra software, nothing uploaded); older `.xls`/`.doc` politely ask for a re-save. (2) **Photos** are read by a real on-device text reader — **Apple's Vision on iPhone, Google's ML Kit on Android** — shipped as a small add-on (`mobile/plugins/iris-ocr`); the photo is read on the phone and never uploaded, and the doctor still reviews the text before it's used. In a plain browser (our testing), if the phone reader isn't present the doctor types the text — the name-hiding that follows is identical. **The masking engine was reused untouched** (House Rule 2), so the 46 name-hiding checks are unaffected. **Verified:** a new automated check (`tests/attachment_masking.mjs`, 11/11) builds a real Excel and Word file, reads them with the shipping code, and proves the names come out as codes and any leftover text is masked/gated; **full QA green** (46/46 masking). **Verified live in the app** (served the real doctor bundle, signed in as JD28): a real Excel file filed as codes only — "Cardinal Santos / Kenzolin / Maria Santos, 52" showed on screen as **SATURN 33 · MS-52 · PRAZ6** — with **no upload** and **no real name anywhere on screen**; a real Word file's text masked to "gave 6 PRAZ to MS 52 at SATURN 33" when sent. **Boundary (same as Step 4):** the phone-side OCR is real, code-reviewed source that **compiles on IT's machine** during the native build; it is device-verified when IT builds — the Excel/Word reading and the whole masking path are fully verified here. Doctor app changed; admin console untouched. On branch `deploy/step5.6b-attachments-ocr`. **Next: Step 5.7 (hosting handoff) — NOT started, stopped per Von.**
- 2026-07-23 · **Admin dashboard — "units by group" (Blaze/Breeze/Terra) via per-lookup category resolution (Von's request).** Brought the product segmentation totals back to the Dashboards tab — the motivating "report at your window" view. A new **admin-only, audited** endpoint (`GET /api/admin/resolve-category`) resolves product codes → their category **group only** (game vocabulary IV/Inhaler/Nutra), for **exactly the codes in the plays**, never a name, never the whole product dictionary. The **"Units by group" card is admin-only**; viewers keep the codes-only dashboard and can't even fetch categories (endpoint 403 for them). Categories resolve on the server **per-lookup when the dashboard is viewed**, each lookup written to the audit log (`category_resolve` with the codes); the client holds only group labels for the codes in view, cleared from memory on sign-out. Verified live (Blaze 10 / Breeze 13 / Terra 8 from seeded plays; viewer sees no group card). Confidentiality proof extended (groups only, no name, no dump, admin-only, audited): `prove_step5_6a.mjs` **43/43**; QA green (46/46); doctor app untouched. On `deploy/step5.6a-admin-writeback`.
- 2026-07-23 · **Admin roster — product category column (Von's catch).** The product roster table was missing the **segmentation** each product counts toward (🔥 Blaze / 🌊 Breeze / ⛰ Terra — the motivating "report at your window" element). Added a **Category** column to the PRODUCT roster and a category dropdown on add/edit; products only (doctors/hospitals unchanged). The backend already stored/accepted the product `group` field (DECISIONS #44) — this surfaces it in the standalone console. Verified live (BLUK 🔥 Blaze, BREE 🌊 Breeze, DOO ⛰ Terra; edit dropdown preselects the current category; add-with-category persists — NUTRAX → group Nutra; doctors table has no category column). Proof extended (product keeps its category through add + edit): `prove_step5_6a.mjs` **36/36**; QA green (46/46); doctor app untouched. On `deploy/step5.6a-admin-writeback`. **Note/offer:** the codes-only Dashboards tab does not yet show a "units by group" breakdown (it was dropped in the standalone build to avoid a code→name dictionary); now that group is server-side it could be added via per-lookup — offered to Von, not built.
- 2026-07-23 · **Admin roster — paginated browsing (Von's request, DECISIONS #59).** The Step 5.6A Roster tab showed nothing until you searched (friction for back-office staff). Now opening the tab **browses**: it loads page 1 from the server (**50 per page**), with next/previous, sortable Code/Real-name columns, and the old search kept as a **filter** on top — for Doctors, Hospitals, and Products. **Protection preserved:** a new admin-only `GET /api/admin/roster/list` returns exactly one page, **hard-capped server-side at 50** (out-of-range pages clamp to the last), so the full code→name table can never come back in one response; the browser holds only the current page; there is no dump-all endpoint (`/api/roster` still exposes only alt-name links). **Every page view is audited** (`roster_browse`: rtype, page, exact row range, total, filter/sort) — browsing is as attributable as a name reveal. Viewers unaffected (list is admin-only; codes only). Verified live (Products 63 entries → "Showing 1–50 of 63 · Page 1/2", Next → 51–63; browser holds 50 of 63; filter + sort work; audit shows the ranges). Confidentiality proof updated to assert the new rule (paged access allowed, bulk dump blocked, browse audited): **`prove_step5_6a.mjs` 35/35**; 5.5 15/15, 5.6 19/19; **QA green** (46/46). Doctor app untouched. On branch `deploy/step5.6a-admin-writeback`. **Stopped per Von.**
- 2026-07-23 · **Deploy Step 5.6A — admin write-back, roster management, per-lookup name resolution (DEPLOY_PROMPTS.md).** Turned the read-only console into a working back-office tool — every capability admin-only and audit-logged. **Roster (seeded from the prototype's DEMO data, not real Bell-Kenz data):** stored server-side in the managed DB; a **Roster management** tab (admin) to **search / add / edit** doctors, hospitals, and products; **server-side per-lookup name resolution** (one code → one real name, audited) powering "Reveal names" in Stock. The full code→name table is **never** downloaded to the browser — searches are bounded and an empty query returns nothing. **Write-back actions (server endpoints + console buttons):** review-queue dispositions — **add-to-roster** (promote a temp code to a real entry), **link-to-existing** (the self-growing alternate-name library, propagated to the doctor app through the existing `/api/roster` links so it's recognised on every device), **reject**, late-entry **accept/roll**, product **noted**; **Stock resolve-and-notify** (writes the update the doctor app already reads back); **mark feedback reviewed**; **mark a duplicate issue resolved**; **lock an Era**. **Roles unchanged:** a viewer stays codes-only (names redacted); every write requires the admin role and lands in the audit log. **Invariants intact:** the roster never ships to a doctor's phone; no real name reaches the LLM. **Doctor app UNTOUCHED.** Refined the QA confidentiality test's wording to the new rule (the codes-only plays pipeline stays name-free; the sanctioned admin resolution is a separate, audited, per-lookup channel). Verified live in the browser (admin Roster search → ANIKA = Dr. Anna Reyes; review disposition buttons; role-aware locks). **Proof `admin/proof/prove_step5_6a.mjs` 28/28** (resolution admin-only/viewer-403/unauth-401; no bulk dump; add/edit; add-to-roster → resolvable; alt-name link reaches the doctor app; stock/feedback/issue/era write-back; codes-only pipeline still name-free; every reveal + change audited). 5.5 proof 15/15, 5.6 proof 19/19, **QA green** (46/46 masking). On branch `deploy/step5.6a-admin-writeback`. **Next: Step 5.6B — doctor OCR/attachments; not started (stopped per Von, not merged).**
- 2026-07-23 · **Deploy Step 5.6 — admin console sign-in and roles (DEPLOY_PROMPTS.md).** Put a real front door on the standalone admin console BEFORE it is allowed to hold the roster or resolve real names (Steps 5.6A onward). **Built:** (1) **Authentication** — a username/password login (salted PBKDF2, 200k rounds; passwords only ever stored hashed); session tokens; the console shows **nothing** until signed in. (2) **Roles, enforced server-side** — `viewer` (codes only) vs `admin` (may see the sanctioned real names). The server itself strips the real name for a viewer: the review-queue typed name and the feedback free-text come back as `🔒 admin only` — the browser is never trusted to hide it. Admin gets an extra **Audit log** tab. (3) **Audit log** (append-only, in the managed DB) — records who signed in / failed to sign in / signed out, and every time an admin reveals real names; admin-only to read. (4) **Roster never bulk-downloaded** — no endpoint ships the code→name table. (5) Users come from an env var (`IRIS_ADMIN_USERS`); demo `viewer`/`admin` seeded with a warning if unset. **Doctor app UNTOUCHED (House Rule 5):** its codes-only reads (`/api/plays`, `/api/stock`, `/api/roster`) and all writes stay open exactly as before — only the console-only, real-name endpoints (`/api/review`, `/api/feedback`, `/api/ai_usage`) and the new `/api/admin/*` now require a session. Verified live in the browser (login gate → viewer sees redacted names, no Audit tab → admin sees "Dr. Jimmy Sy" + the Audit tab showing login/logout/reveal events). **Proof `admin/proof/prove_step5_6.mjs` 19/19** (unauth→401; login + bad-password; viewer redacted vs admin real name; viewer→403 on audit; audit records login/login_failed/reveal; roster carries no code→name map). Step 5.5 proof updated for auth (**15/15**); **QA suite green** (46/46 masking). On branch `deploy/step5.6-admin-auth`. **Next: Step 5.6A — admin write-back + roster; not started (stopped per Von, not merged).**
- 2026-07-23 · **Deploy Step 5.5 — standalone admin console on a managed database (DEPLOY_PROMPTS.md).** The back-office screen, carved out of the prototype into its own hostable browser tool that reads the REAL filed reports — so during phone testing we can open a browser and confirm a report a phone filed actually shows up. **Storage decision settled first (the Step 5.5 gate):** filed reports weren't persisted anywhere the console could read (the Step 2 proxy only forwarded the AI call + review channel); Von chose a **managed database** (DECISIONS #56, resolving blocker #8). **What was built:** (1) extended the Step 2 proxy into the shared backend — `server/store.py`, a **SQLite managed-DB local instance** (zero dependencies; production swaps in the hosted DB, same shapes), persisting codes-only filed reports (plays/stock/feedback) + the whitelisted review channel, served on GET endpoints the console reads; **every report write is field-whitelisted server-side** and play `items` reduced to `{code,qty}`, so a real name can't be stored on a codes-only report even from a buggy client. (2) `admin/index.html` — a **standalone, single-file, no-dependency** browser console (Reports / Stock / Review queue / Feedback tabs) that reads reports **as codes only**; it holds **no roster / code→name lookup** and never un-codes a report; the only real names it shows are the review-queue + feedback whitelisted fields (the sanctioned back-office channel, DECISIONS #47). Config = one backend-URL value (`?base=` / `window.IRIS_BACKEND_BASE` / same-origin); short IT README. **No change to the doctor phone app** (the app already POSTs to these endpoints; the proxy just implements them now). **Proof — all three the step asks for, 15/15** (`admin/proof/prove_step5_5.mjs`): files a real report through the **shipping app's own masking engine** into the store, then (1) the play appears in the console feed as codes ("gave 6 Kenzolin to Maria Santos, 52 at Cardinal Santos" → filed as `SATURN 33 · MS 52 · PRAZ ×6`); (2) **no** real product/hospital/patient/doctor name appears in any codes-only report; (3) a review-queue entry appears with **only** its whitelisted fields — the sanctioned typed name present ("Dr. Jimmy Sy"), a deliberately-smuggled patient name and report-text **dropped**. Also verified **visually in the browser** (Reports tab = codes only; Review-queue tab = real names, whitelisted fields only). Masking suite still **46/46**. **Restyled to match the original prototype admin console (Von's request):** same LOOK (light theme, dark header + left sidebar, purple accent, `ac-*` card components) and STRUCTURE (the same 10 tabs in the same order) so it feels familiar to the back office — **presentation only, no data exposure widened.** Roster / Reconcile / Requests are **locked placeholders** (present for familiarity, expose nothing — Roster would be the code→name lookup; Reconcile needs the Data-Team pull-through file; Requests is demo-only). Added **Dashboards / AI Cost / Issues** as codes-only views (from plays / usage). **Stock stays codes-only** (the prototype resolves codes→real product/hospital names; deliberately not reproduced). Console never reads `/api/roster` and never un-codes a report; proof still 15/15, QA suite green. On branch `deploy/step5.5-standalone-admin`. **Next: Step 5.7 (hand off for hosting) — not started (stopped per Von, not merged).**
- 2026-07-22 · **Deploy Step 5 — full automated QA suite built + a real name-leak it found, fixed (DEPLOY_PROMPTS.md).** The safety net that proves nothing broke, on every future change. One command — `node tests/qa.mjs` — runs the whole battery and reports each as PASS/FAIL/SKIP: **no-report-lost** (offline → park → files exactly once across app-closed-mid-queue, restart, signal flicker, crash-mid-flush, 300-report bulk; and no real name ever written to disk — 27 checks); **masking-holds** (hammers the name-gate with typos, foreign/Taglish names, junk, fast sloppy input through the *real* extraction→gate→mask path — 106 checks); **end-to-end** (doctor masks → backend stores codes-only → admin reads it, no real name reaches the back office — 6 checks); **stress** (4,000 random + extreme inputs with no crash, and a 1,000-report queue with injected failures that never drops or doubles — 13 checks); **speed** (on-device masking ~1.2 ms/message, effectively instant; AI round-trip timed when the harness is up); plus the existing 46-case name-hiding regression and the live assistant journey (harness-gated, skipped when the API key isn't up). Every test **reads the real engine and the real offline queue straight out of the shipping app**, so it always checks what ships. Wired to **re-run automatically on every app/test edit** (project hook now runs the whole suite, ~6s, and blocks on red). **A real leak the suite caught — and fixed (Von's call):** writing a patient **surname-first with a comma** ("Dela Cruz, Roberto, 40") left the given name "Roberto" in the clear. It was **pre-existing** (identical in the prototype and shipping app — not caused by any deploy step). Fix: the name-verify gate now also flags a name-like word sitting right after an already-hidden patient, so the doctor confirms it before anything is sent — it never mis-hides, only asks; normal plays are unaffected and the 46 existing cases still pass. **One residual, tracked and reported:** an *all-caps single surname* after a preposition ("for SANTOS, Maria") can still leak — the suite prints this as a KNOWN GAP every run; it needs a dedicated, comprehensive surname-first masking task (recommended before pilot). On branch `deploy/step5-qa-suite`. **Next: Step 6 — test on our own phones; not started (stopped per Von).**
- 2026-07-22 · **Deploy Step 4.5 — offline parked reports now survive a restart (DEPLOY_PROMPTS.md).** Closed the one gap that could actually lose a report. Previously the offline "parked reports" queue lived only in temporary memory, so a report waiting to send could be lost if the app closed, crashed, or the phone restarted before signal returned. Now the queue is saved to the phone's **persistent on-device storage** (the wrapped app's web-view storage): it's restored on launch, saved every time a report is parked, and each item is removed from storage the instant it files. **Changed only where the queue is stored** — masking, the name-gate, extraction, the confirm logic, and the auto-file-on-reconnect behavior are untouched (the diff is confined to the three offline-queue functions). **Exactly-once guaranteed:** a concurrency guard plus save-after-each-success means a report files once — never lost, never doubled — even if the app dies mid-send; and on next launch (if online) a leftover queue auto-files. **Confidentiality kept:** only codes-only report writes (a filed play, a stock alert, cost metering) are written to disk; the back-office review channel (the sanctioned typed name, #47) and feedback (free text, P2) are **never** written to disk, so no real name is ever persisted (invariant #1) — they keep their prior in-memory-only behavior. **Proven in the browser (a page reload = an app restart):** 3 reports parked while offline survived the reload and filed once each; two more filed on in-session reconnect (5 total, each exactly once, no duplicates); repeated reloads never double-sent; and a review item's real name + feedback text were confirmed **not** written to disk. Masking suite 46/46. On branch `deploy/step4.5-durable-offline`. **Scope note flagged to Von:** a half-finished *unconfirmed* free-text turn (`pendingAI`) is NOT persisted across a full restart — doing so needs full session/conversation restore, which is beyond a storage-location change; its in-session resume is unchanged. **Next: Step 5 — automated QA suite; not started (stopped per Von).**
- 2026-07-22 · **Deploy Step 4 — wrapped the repointed app into native iOS/Android, doctor-app-only (DEPLOY_PROMPTS.md).** Packaged the web app into real iPhone/Android apps with **Capacitor**, reusing the web code as-is (no rewrite, no new features). New `mobile/` folder holds the Capacitor project: `www/index.html` is the **doctor-only** build, `www/config.js` is the single value IT sets (the hosted proxy URL), plus `capacitor.config.json`, `package.json`/lock, and an IT README. **Doctor-only split (per DECISIONS #53):** removed the admin console — the admin HTML shell and its ~950 lines of JS — from the phone build; the only shared symbol the on-device masking reads (`acRoster`) was relocated next to the masking engine, and the admin login path + admin credentials were removed, so **no admin console or admin credentials ship on a doctor's phone**. The roster lookup tables the masking needs stay (they must, for on-device masking). **Verified end-to-end on-device:** the doctor bundle boots with **zero code errors**, signs in from the roster (JD28), and masks on screen — "gave 6 kenzolin to Maria Santos, 52 at Cardinal Santos" rendered as **"gave 6 PRAZ to MS 52 at SATURN 33"** (no real name), with the AI call correctly routing to the proxy path; masking suite **46/46** on the bundle. **Native projects:** `npx cap add android` generates a real Android Studio project (verified here); iOS needs CocoaPods installed first (`brew install cocoapods`) then the same one command — documented in the README. The native `ios/`/`android/` folders are generated on IT's machine (gitignored) so they match IT's toolchain; camera/photo permission entries (Info.plist / AndroidManifest) are documented for IT to add. On branch `deploy/step4-wrap`. **Boundary:** offline park-and-file works in the wrapper as-is, but making the parked queue survive an app close/restart (persistent storage) is the **next step, Step 4.5** — not done here (stopped per Von). Original prototype and the Step 3 full repointed app (`app/`) untouched.
- 2026-07-22 · **Deploy Step 3 — repointed the app's outbound calls to the proxy (DEPLOY_PROMPTS.md).** The single real change to the app itself. Done on a **copy** (`app/iris_chat_app.html`) — the original prototype is untouched and still the Claude.ai reference. Added **one config value**, `PROXY_BASE` (empty string = same origin, which is how the local harness already behaves; set it to the hosted proxy's URL for the wrapped app, or override at runtime via `window.IRIS_PROXY_BASE`), and routed all four outbound calls through it: the AI call (was the absolute Claude.ai bridge URL, now `PROXY_BASE+'/api/messages'`), plus the three backend paths (`apiGet`, `apiPost`, and the offline-outbox replay). **The diff is four call-path hunks and nothing else** — masking, name-gate, extraction, game vocabulary, confirm logic, attachments, and offline handling are byte-for-byte identical. Proven two ways: the full unified diff shows only those lines, and the 46-case name-hiding suite run against the copy passed **46/46**. On branch `deploy/step3-repoint`. **Next: Step 4 — wrap the repointed web app into iOS/Android (doctor app only, per DECISIONS #53); not started (stopped here per Von).**
- 2026-07-22 · **Deploy Step 2 — production proxy built in `/server` (DEPLOY_PROMPTS.md).** Hardened the session-only local test server into a small, hostable "middleman" — the key-holding server that lets the app reach the AI on its own once IT hosts it. Two endpoints only: `POST /api/messages` forwards the app's **already-masked** message verbatim to Anthropic (so prompt caching stays intact) using the key from the **`ANTHROPIC_API_KEY` environment variable** — never in code, repo, logs, or responses; and `POST /api/review` re-enforces the #47 field whitelist **server-side** so no patient identity or free-text can ever be stored, forwarding either to an admin endpoint (`REVIEW_FORWARD_URL`) or a local file. It never masks/unmasks and never logs message content (events only), with CORS + a `/health` check for hosting and **zero third-party dependencies** (Python standard library — trivial for IT to run). Shipped with `.env.example`, a `.gitignore` (key + store never committed), and a short README for IT. **Verified live:** refuses to start with no key; the review whitelist dropped injected `patient`/`report_text`/`secret` fields (only the 7 sanctioned fields stored); a masked body forwarded end-to-end (Anthropic returned its own 401 on a dummy key, proving the forward path); CORS preflight works; and the event log carried no message text, no names, and no key. On branch `deploy/step2-proxy`. **Next: Step 3 — point the app's one outbound-call config at this proxy (not started; stopped here per Von).**
- 2026-07-22 · **AI cost dashboard — action-driving upgrades (Von asked to build all five, DECISIONS #52).** Turned the cost tab from "watching" spend into "managing" it. Added: (1) a **monthly budget with an alert** — set a budget and see spend-to-date and projected month-end against it, with a green/amber/red status that flips to red when you're on track to overspend; (2) **cost per doctor and per report** — the per-unit numbers you actually budget with; (3) a **"clarify overhead" figure** — how much money goes to the app's back-and-forth questions vs actual logging, with the yearly peso saving from cutting it in half (this justifies investing in better on-device reading); (4) a **one-line "switch to Haiku" saving** so the model decision is a glance; (5) a **time-period selector** (All / Today / 7 days / 30 days / This month) that filters every figure. All topped with a **health strip** at the top so an executive sees "are we fine, and if not, the one thing to do" in two seconds. Also made switching the AI model a one-line change (a single named constant) and confirmed the dashboard's model label follows the actual model automatically (not hardcoded). Verified live: the budget flips red when over, the period buttons re-label and filter, and every figure recomputes. Boundary: budgets and real time-windows need saved history — the demo resets each session; the real build keeps it.
- 2026-07-22 · **AI cost analytics — trend over time + where the cost comes from + a caching answer (Von's follow-up, DECISIONS #52).** Extended the AI Cost tab with three things Von asked for. (1) A **cost-over-time trend** chart so spend can be watched through time (auto-buckets by day/hour/minute as history grows). (2) A **"where is the cost coming from"** view — three breakdowns showing the share of spend **by doctor, by hospital (Location), and by function** (logging a play vs the back-and-forth "clarify" turns vs stock alerts vs Q&A). The function view is the useful one for savings: it shows how much goes to actual logging vs clarifying round-trips. To make this possible, each AI call is now tagged with codes-only context (Player, Location, function) — never a real name. (3) A short **answer to Von's caching question built into the tab**: it shows the cache-reuse rate and explains that caching is a saving that's already on and gets more efficient as usage grows (steady traffic keeps the shared instruction sheet "warm"), trending to a floor — the bigger future savings are fewer clarify round-trips and a cheaper model. Verified live: four calls across two doctors, three functions, and three hospitals were attributed correctly, with 75% of calls reusing the cache.
- 2026-07-22 · **AI cost dashboard — real-time spend monitoring (second improvement Von asked for, DECISIONS #52).** A new "AI Cost" tab in the admin console shows, live, what the AI is costing: spend this session, average cost per report message, tokens in/out, how much money the caching is saving (and what share of input is served from cache), a per-model breakdown, and a projection to full scale. The number is metered where it really lives — the backend proxy that makes the AI calls sees every call's token usage, so it records it; the hosted build meters at the same point. The projection scales the live average to 1,000 doctors (≈180,000 messages/month) and prices the same usage at Sonnet vs Haiku, so the model-choice decision (see the Haiku analysis) is backed by live numbers. Verified live end-to-end: two real reports were metered — the first cached the instruction sheet, the second read it back at about a tenth of the price (91% of input from cache) — and the projection read ~$19K/year on Sonnet and ~$6K/year on Haiku, matching the earlier hand estimate. Figures are shown in **pesos** at an indicative exchange rate the admin can adjust (default ₱58/$1), and the projection has **two scenarios side by side** — at the current number of users (doctors on the roster) and at a manually-entered target (default 1,000) — both priced at Sonnet vs Haiku. Boundary: the demo log resets each session; the real build keeps history and can add daily trends, budgets/alerts, and per-doctor views. **Both improvements Von raised this session are now built.**
- 2026-07-22 · **Admin Stock tab — "Add to roster" is now a working button (Von's request).** The pending-hospital marker in the Stock tab is now an actionable "Add to roster" button that opens the same add-to-roster form the Review queue uses (correct the name, assign a Location code, save). It reuses the exact same flow — no duplicate code — so an unknown hospital reported in a stockout can be added to the roster without leaving the Stock tab; once added the cell reads "✓ on roster". Verified live: the button opened the form pre-filled with "Ace Valenzuela", assigned code ACE 07, saved to the roster, and the row flipped to on-roster.
- 2026-07-22 · **Admin Stock tab now shows the real hospital name for an unknown Location (Von's catch).** When a doctor reports a stockout at a hospital not yet on the roster, the alert used to show only the temporary code (LOC-PEND-1) in the admin console — useless to the supply team. Now the Stock tab shows the actual hospital the doctor typed (e.g. "Ace Valenzuela"), marked "pending — add to roster". It does this the safe way: the stockout alert itself still carries codes only; the real name reaches the back office through the existing review-queue channel (the one sanctioned place a typed name is allowed to travel, invariant #1), and the Stock tab looks it up there — matched to the reporting doctor so two doctors' temporary codes can't be confused. Verified live end-to-end. (No change to what the doctor sees — they still see the code.)
- 2026-07-22 · **Masking leak fixed while testing OS reports — unknown hospital after "in" (tester VON-18, Critical/leak).** Von re-tested the out-of-stock feature with a realistic line — "Good am. Kenzolin not available in Ace Valenzuela" — and the unknown hospital "Ace Valenzuela" showed on screen and reached the AI in the clear (the product Kenzolin masked fine). The name-hiding step recognized a hospital only after "at"/"sa", not "in". Fixed: "in" is now a recognized Location word, with a small guard list (stock, supply, total, home, bedside, night, …) so everyday phrases like "in stock" or "at home" don't get mistaken for a hospital. Now the unknown hospital converts to a temporary code (LOC-PEND-1) on screen and to the AI, and only the back-office review queue sees the real name so the team can add it to the roster. This also closes a known review item (C3, the "at home/bedside" false-positive). Verified live end-to-end; the automated name-hiding checks grew 43 → 46/46 and the full assistant suite stays 13/13.
- 2026-07-22 · **Product out-of-stock reporting — doctor reports it, admin resolves, doctor gets the update (DECISIONS #51).** First of two improvements Von asked for this session. A doctor can now tell the app a product is out of stock at a hospital in plain language ("Prosec is out of stock at Makati Med"); the app converts the product and hospital to codes on the phone before anything leaves it (so the AI and the backend only ever see codes), replies respectfully, and shows a "Supply alert" card the doctor taps to send. It lands in a **new, separate "Stock" tab in the admin console** (Von's call vs folding it under Concerns) where the back office sees the real product and hospital names, resolves the alert with a short update, and that update is **pushed back to the doctor**, who sees it on next sign-in. Fully verified live end-to-end (doctor→admin→doctor): the doctor's message masked correctly on screen, the alert stored codes-only, the Stock tab mapped it to Prosec · Makati Medical Center, the resolution pushed back, and the doctor saw "📦 Update on your supply alert…". Guarded by a new automated sample; full e2e 13/13, masking 43/43. Boundaries for the real build: offline OS reports aren't queued yet, and the admin's free-text update gets the banned-word filter but a real name typed into the note is filtered server-side in the hosted build. **Second improvement (AI cost dashboard) — not started yet.**
- 2026-07-22 · **Haiku cost analysis + live Haiku evaluation (DECISIONS #50).** Von is weighing Claude Haiku for the hosted build to cut cost. Priced the AI at 1,000 doctors with caching on: **Haiku ~$5–8K/yr, Sonnet ~$15–20K/yr** (the model built/tested on Sonnet is 3× Haiku; caching only discounts the repeated instruction sheet, not the conversation, so the lever to go lower is the model). Then **tested Haiku for real**: pointed the local harness's app at `claude-haiku-4-5`, ran the full sample library, and drove a live doctor→admin session in front of Von. **Safety perfect** — masking 43/43, and no real name leaked / no banned word on screen in any case (the name-hiding is on-device, so it doesn't depend on the model). **Everyday reporting works** — logged a single play, a shared/referral co-play (Co-Player THOR), and a 2-play batch; all four reached the admin Reports as codes (4 lines/18u) and the unrecognized "Dr. Ken" short form escalated to the Review queue (PEND-1). **Two non-safety rough edges vs Sonnet:** back-dating asks one extra confirmation (the single sample-library FAIL → 11/12), and once re-asked for an age crammed onto the same line — both fixable with a small prompt nudge. Verdict: Haiku is viable for the pilot and confirms the ~$5–8K/yr path; recommend a small prompt polish + clean re-run before committing. Decision left to Von (Haiku cheaper/slightly more back-and-forth vs Sonnet tested/3× cost). Made `tests/e2e_plays.mjs` model-selectable via `IRIS_MODEL` (defaults to sonnet) so either model can be re-run.
- 2026-07-21 · **Two confidentiality/reliability fixes Von prioritised: verify lone names, and verify offline reports (DECISIONS #48, #49).** **(1) Lone-name verify gate.** If the app spots a leftover word that could be a real name but has nothing to convert it (no age, no "Dr."), it now stops and asks the doctor — "is this a patient? a doctor? not a name?" — instead of letting it reach the AI. The doctor's answer converts it to a code on the phone; the name is shown only during that check and cleared after. This closes the long-standing lone-name leak (blocker #4). Verified live: "juan had 2 prosec" stopped for verification, nothing reached the AI until answered, then sent as "J had 2 OTEM." Guarded by 7 new automated checks (suite 36 → 43). Honest limit: it's a smart guess based on a name list + sentence position, so a very unusual name with no clues could still slip — the real app can plug in a fuller name model behind the same gate. **(2) Offline report verification.** The doctor can now ask "what are my plays this Era?" even with no signal — the report is computed on the phone (no AI needed) and each play shows its status: "⏳ waiting to send" vs already filed, with a header count. So nothing is silently lost — if a saved report didn't go through, the doctor sees it and can re-file. Verified live: offline, the report rendered with the un-sent play marked "waiting to send"; on reconnect it filed automatically and flipped to done.
- 2026-07-21 · **Privacy fix — bounded the one place a real name is allowed to travel (Von's call, DECISIONS #47).** The code review found the app was sending unrecognized real names (an unknown doctor/hospital/product the doctor typed) to the back-office review list, while the spec claimed no real name is ever transmitted — a genuine contradiction. The reason it happens: to ever add a new doctor/hospital/product to the system, someone in the back office has to see what was typed (this powers the self-growing name library and add-to-roster). Von chose to keep that, but make it clean. Now the typed name travels on exactly one path — the locked back-office review list — and never to the AI and never into the reports data; the app sends only a fixed short list of fields, so nothing else (and never a patient name) can slip through. I also fixed the misleading code comment and rewrote the spec so it stops contradicting itself. **Verified live:** a play naming an unknown hospital and unknown doctor sent only those two names (as codes-plus-typed-name) to the back office; the AI saw only codes ("6 PRAZ at LOC-PEND-1 with PEND-1 to MS 52") and the reports store got no real name. Masking 36/36. **Related, not yet done:** the in-app Feedback box still sends its text as typed — a doctor could put a name there; masking feedback is a separate follow-up (review finding P2).
- 2026-07-21 · **Automatic offline handling — no toggle (Von's request, DECISIONS #46).** For hospitals with patchy signal. The app senses the connection itself — there is no offline button. If the doctor sends a report with no signal, nothing fails or is lost: the report is masked and saved on the phone, and a quiet amber line above the message box says the saved reports will send automatically when back online. The moment signal returns, the app files them and tells the doctor "✓ back online — your saved reports were filed automatically," with no action from them. A confirmed play or an attached file needs no assistant, so it queues and files cleanly. A free-text play where the assistant still needs to ask something is held and finished automatically on reconnect (it resumes the conversation itself). **Verified live end-to-end:** went offline, attached a file and submitted — the backend received nothing while offline; on reconnect both plays filed automatically and the doctor was told; a typed play sent offline was completed by the assistant the moment signal returned. Masking suite 36/36. **Boundary:** the saved-reports queue lives in memory in this prototype; the real phone app keeps it in durable storage so a queued report survives the app being closed.
- 2026-07-21 · **Attach a file or a photo of a report (Von's request, DECISIONS #45).** Some doctors have a secretary tabulate their plays into a file, or write the report by hand and photograph it — so the app now accepts an attachment, not only typed text. A 📎 button in the composer opens one attach flow that feeds the existing "review all plays" card. **Everything happens on the phone.** For a **file** (a CSV/text table), the app reads it in the browser, converts every real name — product, hospital, patient, co-player — to a code on-device by direct lookup, and shows the coded plays to check and submit; **nothing is uploaded and it never goes to the AI**, so this path is instant and works offline. Anything it can't recognize (e.g. a product not in the game) marks that play "needs a fix" and blocks submit — an unrecognized real name is never sent. For a **photo**, the image is held on the phone and never uploaded; the doctor confirms the text (on the real phone this is read automatically by the built-in OCR), which then runs through the same on-device masking. **Verified live end-to-end (doctor→admin):** a 4-row file converted to codes with zero network calls during reading and zero real names in any payload; an unknown product (Aspirin) correctly blocked submit; a clean file submitted codes-only and appeared in the admin Reports; a photo's play reached the AI already masked ("6 PRAZ at SATURN 33 to MS 52"). Masking suite 36/36. **Boundary:** the demo reads CSV/text tables + photos; native Excel/Word reading and real on-device OCR are real-app (Expo) build items.
- 2026-07-21 · **Product category in the product database + "product not in the game" handling (Von's request, DECISIONS #44).** Two things. (1) **Category is now part of the product database.** Each product's category — the game element it counts toward (🔥 Blaze / 🌊 Breeze / ⛰ Terra, the three groups on the doctor's on-time tracker) — used to live only in the seed data, so it looked hardcoded. The admin Products roster now shows a **Category** column and lets you **assign each product's category** (a dropdown when adding a product and when editing one). Verified live: moved a product between categories and it stuck. (2) **A product not on the roster is no longer silently accepted.** When a doctor reports a product that isn't in the game, the app now **tells them it isn't part of the game and can't be logged** (nothing is recorded), and **reports it to the admin console** — an entry in the Review queue ("Product not in the game") showing the Player code and the exact product name they typed, so the team can see what was reported and decide whether to add it. Verified live end-to-end: a doctor reported "Aspirin"; they were told it isn't part of the game, nothing logged, and it appeared in the admin Review queue as DAWN/Aspirin. Masking 36/36. **On-time reporting nudge — BUILT (Von picked three of the options).** A persistent bar above the doctor's message box now shows, in one line: the plain status ("July · 2 plays logged"), a **deadline countdown** ("20 days left to file" — the 10th of the next month, measured from the real current date — whose colour escalates to amber then red as the deadline nears), and a **reporting streak** ("🔥 3-month streak" when consecutive months have plays). All computed on-device from the doctor's own history; refreshes on sign-in and after every logged play. Verified live (calm and near-deadline red states).
- 2026-07-21 · **Removed doctor-app demo scaffolding + made "Send feedback" real (Von's request, DECISIONS #43).** Removed three demo-only pieces Von flagged: the "Demo cheat sheet" button, the "Admin review queue (demo)" button, and the "Try: Gave 6 Kenzolin…" hint under the composer. **Feedback now goes to the admin team instead of a group chat.** Before, "Send feedback" only copied text to the doctor's clipboard to paste into a pilot chat — nothing was captured centrally. Now the doctor picks a category (Something broke / A name showed on screen / Confusing / Suggestion / Praise), types a note, and taps Send; it's submitted to the shared backend under the Player code only (never a real name), and appears in a **new "Feedback" tab in the admin console** — Date · Player · Category · Message · Status, with a "Mark reviewed" action. A "name showed on screen" report is highlighted in red as a top-priority privacy issue. Verified live end-to-end: DAWN sent a Suggestion and a "name on screen" report; both appeared in the admin Feedback tab, codes-only, the safety one highlighted, and Mark-reviewed clears it. Masking 36/36. Also **removed the demo card IDs/PINs from the sign-in screen** (per Von) — dropped the "Demo doctor card / Demo admin card" hint and genericized the card-ID placeholder, so sign-in no longer exposes any usable credentials.
- 2026-07-21 · **"Credited to me by other Players" is now real data, not a sample (Von's catch).** Von noticed the doctor's "Credited to me by other Players" table showed two rows (ANIKA/ZEUS) that nobody had entered — leftover demo/sample data. Removed the hardcoded sample and made the table **derive from real plays**: it now shows only plays another doctor actually logged naming this doctor as the co-player (pulled from the shared backend at sign-in, scoped to the current month). It's empty — with an honest "No plays credited to you by other Players this Era" — until a real co-play exists. Verified live end-to-end: DAWN's credited table was empty; then, signed in as another doctor (JD28), logged a play crediting Dr. Daisy Tagarda; back as DAWN, the credited table correctly showed that one real play (SATURN 33 · JC-50 · PRAZ ×6 · started by JD28). Masking 36/36. (This was the last hardcoded sample on the doctor side; the admin console's demo samples were already cleared on 2026-07-16.)
- 2026-07-21 · **Doctor stats made flexible — any period, any breakdown, always a table (Von's request, DECISIONS #42).** A doctor can now ask for their plays/stats over any period — this month, month-to-date, year-to-date, a named month ("June"), or a date range — and any breakdown — a plain list, per hospital (Location), or per product (Avatar) — and always gets a clean **table**, never a sentence or paragraph. The assistant only recognizes that a report was asked for; the app reads the requested period and grouping from the doctor's own words and computes all the totals on-device (deterministic, invariant #7). It also pulls the doctor's earlier plays from the shared backend at sign-in, so MTD/YTD/date-range reports span past months, not just the current session. **Tested live in the doctor window end-to-end:** logged four plays across May, June and July at two hospitals with three products, then ran "what are my plays this month", "YTD per product", "per hospital YTD", and "for June" — each returned the correct table; the same four plays then showed in the admin console's Reports (July 2 lines/18 units, June and May switchable by month). Masking suite 36/36. **Also fixed a harness bug found during the test:** the local test server could silently drop plays when several were logged quickly (a concurrent-write race in the session-only mini-server) — added a write lock so every play lands; also baked that fix into the `/test-app` skill so future rebuilds include it.
- 2026-07-21 · **Admin console fluid typography + responsive check of Dashboards / Reports / Reconcile.** Von noted the fonts looked small and wanted the console to stay optimized whether the browser window is minimized or full-screen. The console used fixed text sizes, so on a big window the type looked small and sparse as the content spread out. Introduced one fluid base text size for the whole console that grows with the window and tied every text size to it, so the type now scales smoothly at any window size instead of a fixed small point size — readable minimized or maximized. (Von then noted it still felt small on a large window, so the scale was raised: ~15.5px minimized, ~20px at a normal laptop width, up to 21px on large monitors.) Confirmed the three tabs Von named — Dashboards, Reports, Reconcile — render cleanly at wide-desktop, laptop, and tablet-portrait widths (they already share the responsive shell fixed earlier). Masking suite 36/36 (visual change only).
- 2026-07-21 · **Admin console responsive polish for browser use (desktop / laptop / tablet).** The console is browser-based, so tuned all three form factors. Desktop/laptop: raised the content width cap (1180 → 1440px) so bigger screens use more of the window instead of centering with wide empty margins. Tablet: moved the point where the layout switches from the left sidebar to the top tab-bar (820 → 900px), so a common iPad held in portrait now gets the roomy full-width layout instead of a squeezed sidebar — laptops and desktops keep the sidebar. Touch: enlarged the tap targets (tabs, small buttons, inputs, search) on touch devices only, so buttons are comfortable to tap on a tablet without changing anything on a mouse-driven desktop. Also (earlier same day) fixed the Roster tab so the Hospitals and Products panels fill the full width side by side. Verified live at wide-desktop (1680), tablet portrait (834), and laptop (1280) widths. Masking suite 36/36 (layout-only change).
- 2026-07-21 · **Admin-issued doctor card + PIN — where login credentials live (Von's question, DECISIONS #41).** Von asked where the access card/PIN the admin team assigns to doctors is saved. It wasn't anywhere manageable — the card IDs and PINs were hardcoded demo constants with no admin screen. Now they live on the doctor's roster record, so issuing access is just part of managing a doctor. The admin **Doctors roster gained an Access column**: it shows each doctor's card ID and whether a PIN is set, and in Edit mode you can **issue or reset a card and PIN**. Login now checks against the roster; a doctor with no card issued can't sign in. Governance built in to match the real system: the **PIN is never shown back** (set/reset only) and never leaves the back office (the roster sync sends only code + display name, never the PIN). Verified live end-to-end: issued a new card to Dr. Ken Dela Cruz and it saved; the rewired sign-in logs a doctor in from the roster record; an un-issued card, a wrong PIN, and an unknown card are all rejected. Masking suite still 36/36. **Prototype caveat:** because the PIN is deliberately kept out of the shared backend, a card issued in the admin window isn't visible to a *separately-running* doctor window — the issue-then-sign-in loop is complete inside one running app. Making that handoff live across devices is a small real-build backend step (a sign-in endpoint that verifies the PIN server-side and returns only the alias).
- 2026-07-21 · **One-tap quick-access Location chips, ranked by usage (tester VON-17, DECISIONS #40).** The doctor composer now shows the Player's assigned Locations as tappable chips (beside the "What are my plays this Era?" button). Tapping one drops "at <Location> " into the box so the doctor just adds the product/units/patient — less typing. The chips are ordered by how often the doctor reports at each Location (most-used first; seeded from the shared backend on sign-in so it reflects past months, and updated as new plays are logged), capped at four. Codes-only on screen. Needed one masking change — a Location code is now recognized as itself, so a code entered by the chip (or typed) is read as that Location, not as an unknown hospital. Verified live: DAWN sees 📍 PLUTO 12 / 📍 NEPTUNE 21; tapping fills the composer and the play masks correctly (at PLUTO 12 6 PRAZ to MS-45); the most-used Location rises to the front. Masking suite 35 → **36/36**.
- 2026-07-21 · **Doctor alternate names + a self-growing name library (tester VON-16, DECISIONS #39).** Doctors now get admin-managed **alternate names** (nicknames, initials, abbreviations) the app recognizes — the same idea as the hospital abbreviations. Co-player matching reads the admin **Doctors roster** (each doctor carries a `names` list = real name + admin-entered alternates), rebuilt on every edit and on sign-in; the signed-in doctor is excluded so their own name still resolves to "me". The admin Doctors roster gained an "Also recognized as" editor (mirrors hospitals). **The headline — a self-improving dictionary:** a name the app doesn't know yet still goes through under a temporary code and lands in the review queue, where each item now has a **quick-link** — pick an existing doctor/hospital from a dropdown and click "Add as alternate name" (one selection, one click). That attaches the typed name, recognizes it immediately, and **persists it to the shared backend** (`roster_links`), which the doctor app and admin console pull on sign-in/refresh — so a name one admin adds is recognized on every device, and the library keeps building as more doctors use the system. Verified live **cross-tab** end-to-end: doctor names "Dr. Kenzo" (unknown → PEND-1) → admin links it to Dr. Ken Dela Cruz in two clicks → doctor signs in again → "with kenzo" is recognized as THOR. Masking suite 31 → **35/35**. (Residual: an unknown *bare* nickname with no honorific/preposition still isn't captured — the pre-existing extraction gap, blocker #4.)
- 2026-07-21 · **Three fixes from a live test of a real report format + two it surfaced (tester VON-15, DECISIONS #38).** Von tested "For reporting po: JU 60/F UST Hospital Saptaz 2.25g 42 vials Attending: Dr. Daisy Tagarda" (the reporting doctor named herself; single play). (1) **Patient initials kept whole** — the Match code trimmed "JU" to "J-60"; `patientInitials()` now keeps a short all-caps abbreviation as-is (JU-60, AB-45) while an ordinary first name still gives one letter. (2) **Hospital abbreviations, admin-managed** — "UST"/"USTH" now convert to a Location because the admin can list a hospital's **alternate names / branch names** in the Hospitals roster (Edit → "Also recognized as"); the masker reads names from the editable roster via `rebuildPhrases()`, and UST is seeded as Location PLUTO 12. Before this an unknown hospital typed without "at/sa" stayed visible in the clear. (3) **"Attending is me" = single play** — a doctor writing their own name as attending is now recognized as the Player (rendered "me", Game Mode SINGLE), not a phantom Co-Player; `isSelfName()` matches on-device against the signed-in doctor's own real name (`USER_REALNAME`, set at login, never sent). **Also surfaced + fixed:** the PH honorific **"Dra"** was missing from the doctor detector (a latent leak — "Dra <name>" for a real co-player would have shown in the clear); and the confirm card's hardcoded "You (JD28)" now shows the actual signed-in alias. Added demo doctor Dr. Daisy Tagarda (card IRIS-2608 / PIN 5678, alias DAWN). Verified live end-to-end (doctor app → admin Reports: DAWN · PLUTO 12 · CAZ 42 · solo). Masking suite grew 25 → **31/31** with six new cases.
- 2026-07-16 · **Review-queue "Add to roster" now opens a clean-up form before saving (tester VON-14).** Previously "Add to roster" promoted the raw temp entry straight in (keeping the ugly PEND code and unedited typed name). Now it opens a modal where the admin corrects the details before anything is saved: for a Doctor — fix the real name spelling/format, assign a proper Player alias (replaces the temporary PEND-N), and optionally tick assigned Locations from the hospital master list; for a Location — fix the hospital name and assign a Location code. Save validates (name + alias required, alias/code must be unique) then writes the clean record to the roster and marks the review item ✓ Added; Cancel or backdrop closes with no change. Verified live end-to-end: PEND-1 "dr jimmy sy" → cleaned to "Dr. Jimmy Sy", alias LOKI, tagged NEPTUNE 21 → saved and appears correctly in the Doctors roster; review item marked ✓ Added to roster.
- 2026-07-16 · **Doctor window got a Sign-out button (tester VON-13).** The doctor app header had no way to log out (only alias · 🐞 · ? · 🛡️). Added a ⏻ Sign-out button that returns to the login gate and wipes the session — clears the conversation, streak counters, watermark, and alias — so the next sign-in starts clean; codes-only data already submitted to the backend is untouched. Verified live: sign in as JD28 → ⏻ → back to the login screen, fields cleared. (The admin console already had a working "Sign out" top-right — Von's report flagged both, but only the doctor side was missing.)
- 2026-07-16 · **Co-Player now shows in the admin report + Review-queue temp-code actions (tester VON-11/12).** (1) **Report table:** the Era report dropped the Co-Player on a shared play — a co-play looked solo. `reportRows()` now carries `other_md` and the Reports table has a **Co-Player** column (real column, and added to the CSV export). Verified live: a JD28 · NEPTUNE 21 · OTEM ×6 co-play shows Co-Player PEND-1; a solo play shows "—". (2) **Review-queue toggles:** temporary-code Doctor/Location rows were dead ("⏳ Needs review", no action). Added two disposition buttons per Von's model — **Add to roster** (promotes the temp code to a real roster entry; admin renames the code in the Roster tab) and **Not in the game / Not a real Location** (leaves roster + reports untouched; reporting doctor to be informed). Verified live: "Add to roster" on PEND-1 "dr jimmy sy" adds them to the Doctors roster and marks the item ✓ Added; reject marks ✓ and changes nothing. **Open decision raised (see blockers #6):** the *credit-sharing math* when a co-player is added is deterministic code that must come from the Data Team's worked examples (invariant #7) — the report shows the co-player but does NOT yet split/attribute units; that formula is undefined and I did not invent it.
- 2026-07-16 · **Cleared the admin console's demo sample data (keep roster) for live end-to-end testing.** Von is testing that a play logged on the doctor app reflects in the admin console. Emptied the four DEMO-ONLY seed arrays in the prototype — `ADMIN_PLAYS`, `PULLTHROUGH`, `ADMIN_ISSUES`, `ADMIN_REQUESTS` — so Dashboards / Reports / Reconcile / Issues / Requests now show ONLY real submitted plays (each seed kept in an adjacent comment for one-line restore). Suppressed the now-inaccurate "SAMPLE DATA" amber pills (`SAMPLE_BADGE=''` + removed the hardcoded Dashboards pill) and reworded the three "Sample … for the demo" notes to say the tabs reflect real submitted plays. Roster untouched (Doctors, Hospitals→Locations, Products→Avatars all intact); Review queue was already live-only. Verified live: Dashboards and Reports read all-zeros; posted one synthetic coded play (JD28 · SATURN 33 · PRAZ ×6) → it appeared as the single Reports row, no demo rows mixed in; then reset the backend store so Von starts from a clean slate. **Reversible:** restore the seed arrays and the `SAMPLE_BADGE` span text to bring the demo console back for Wave 0.
- 2026-07-13 · **`/test-samples` skill — one-command live replay (option #1).** Wrapped the end-to-end sample replay + the fast masking-only checks into a project skill (`.claude/commands/test-samples.md`) that triggers on "run samples" (and variants), ensures the local harness is up, runs the whole library against the live assistant, reports a plain pass/fail summary, and **auto-captures any failure** as a logged finding (feedback CSV + regression note, record-as-you-go) — highest severity for a leaked name or banned word. Honest boundary documented in the skill: it catches problem *types we've taught it to check*; a brand-new problem still needs a human/agent eye once, then becomes a permanent sample. Project-scoped (IRIS only), alongside `/check-masking` and `/test-app`.
- 2026-07-13 · **Two fixes found by live sample-testing + admin end-to-end audit (tester VON-09, VON-10).** (1) **Banned-word slip:** asked "who developed you", the assistant replied "usage-reporting assistant" — two banned real-world words on screen (invariant #6). Fixed by rewording the prompt's self-description and adding a full banned-word output filter (`gamifyOut`, applied only to displayed AI text, not the doctor's input so report-trigger words survive); added three off-topic samples ("what can you do", "who developed you", "do you know Dr X") and a banned-word check to the e2e suite (now 12/12). (2) **Era-filter bug:** a backdated previous-month play showed in the current Era's admin report — the report/dashboard/reconcile aggregations weren't Era-scoped. Fixed with `playsForEra()`, an Era selector on the Reports tab, and Dashboards/Reconcile scoped to the open Era. Verified live end-to-end (doctor app → shared backend → admin console): all 5 logged plays + 8 issue flags reflect in the admin console; the June play now files under Era 2606, not 2607. Masking 25/25.
- 2026-07-13 · **Saved sample-play library + end-to-end replay suite (eval Phase 2).** Von asked for a saved set of the actual messages a tester types so the app can auto-run them instead of re-typing. Built `tests/sample_plays.mjs` (readable catalog of typed messages per feature — single, two-product, no-age patient, surname collision, known/unknown referral, backdating, multi-play, query) and `tests/e2e_plays.mjs`, a runner that replays each through the WHOLE pipeline — on-device masking AND the **live assistant** (via the session harness proxy) — and checks each function: play understood, codes correct, confirm/confirm_batch produced, period/co-player handled, and — on every message — **no real name ever sent**. Reads the real masking engine, era logic, and system prompt straight from the prototype (never a private copy). **9/9 sample plays pass** end-to-end, including the multi-play batch. Complements the fast masking-only checks (`masking_eval.mjs`, 25/25) which need no assistant. Add a row to `sample_plays.mjs` for each new behavior. **Also (housekeeping):** logged today's multi-play verification in the tester feedback log (VON-08) and corrected a stale Wave 0 scenario (unknown doctor now = temporary code, not a blocked send).
- 2026-07-13 · **Multi-play as a first-class feature — slice 1 of 4 (DECISIONS #35, next step 4a).** A doctor can now bundle several plays in one message and get a single review-all card: the assistant returns every ready play in one structured reply and the app renders a stacked, per-play-checkable list (Location · Username · units · mode · Co-Player · Era) with one "Submit all N" button. Chosen experience = **review-all list** (Von's call: fastest while every play is still shown and checkable before anything logs). Each play in a batch runs the same deterministic checks as a single play (off-assignment Location, backdated closed-month, duplicate); an incomplete or look-alike play is marked "⚠ needs a fix" and **blocks submit** until the doctor corrects it in words. Kept the parser hardening (the whole batch is ONE JSON object with a `reports[]` array — never several concatenated objects). Refactored `commitPlay` into a bubble-less `recordPlay` shared by single-play and batch submit. Masking untouched (25/25). Verified live end-to-end in a static harness: a 3-play June batch renders, submits all three, fires the off-assignment flag (NEPTUNE 21) + three late-entry flags; an incomplete play disables Submit-all. **Remaining slices:** (2) polish conversational per-play edits; (3) duplicate detection *within* a batch (two identical plays in one message); (4) regression case + docs. **Not yet verified with the live model** that it reliably emits `confirm_batch` — needs the API-key harness (a full session test with Von).
- 2026-07-06 · Project brief v1.0 → v1.3 (architecture locked: standalone app, hybrid on-device masking, full pipeline, game-vocabulary display layer) — see brief §3, §4a–4d
- 2026-07-06 · Working prototype (`prototype/iris_chat_prototype.html`): full capture + query loop, silent in-place masking, two-net patient extraction, AI clarify with never-assume/inference rules, schema-validated confirms with auto-repair, Confirm Game Play table + units summary, streaks/progress, in-app feedback button
- 2026-07-06 · Pilot kit created: `pilot/PILOT_PLAN.md`, `pilot/TESTER_GUIDE.md`, `pilot/feedback_log_template.csv`
- 2026-07-06 · Claude Code working rules: `CLAUDE.md`
- 2026-07-06 · Claude Code setup guide for the standalone `iris-chat` repo: `CLAUDE_CODE_SETUP.md`
- 2026-07-06 · **Git repo live:** initialized `main`, `.gitignore` added, first commit `cc83cee`, pushed to private GitHub repo `github.com/vsguarino/iris-chat`. This Mac authenticates via fine-grained PAT stored in macOS keychain (SSH key was generated but not used — see DECISIONS #24)
- 2026-07-06 · **Wave 0 facilitator pack** (`pilot/WAVE0_FACILITATOR_PACK.md`): run checklist, demo roster cheat-sheet (real→code, pulled from prototype seed dictionaries), pass/fail answer key for all 10 scripted scenarios, copy-paste recruiting message. Documents the Claude.ai-only build constraint → Wave 0 runs as hands-on 1-on-1/screen-share sessions, not a public link
- 2026-07-06 PM · **Local standalone test harness** (session-only, NOT in repo — lives in the session scratchpad): a tiny Python mini-server that serves the prototype AND proxies AI calls to the real Anthropic API with a server-side key, so the full app (assistant replies + confirm card) runs as a standalone site on the Mac at `localhost:8765` — a local stand-in for the Wave 1 hosting/proxy. Uses model `claude-sonnet-4-6`. **Must be recreated each session; the API key Von pasted should be rotated at console.anthropic.com when convenient.**
- 2026-07-06 PM · **Prototype hardened via live COO testing** (many commits, `f25e7a9`…`8279141`). New/changed behavior: shorthand recognition (kenz/cardinal/makati) + wider Edit-button feedback; plays report shown as a table; **two report tables** (own plays + "credited to me by others", sample data) shown together on any report request; tappable "what are my plays this Era?" prompt; **unrecognized co-players and hospitals no longer block** — they get stable per-entity temporary codes (PEND-1, LOC-PEND-1) and are flagged in a device-side **admin review queue** (🛡️ icon, demo-only); doctor **surname shortcuts** (e.g. "Reyes") offer a confirm-by-CODE prompt ("Is this ANIKA?"), never showing the real name; hospital typos auto-correct; assistant stops asking "Single or Multiple" once a co-player is already named. Bugs found+fixed while testing: "JCat" name-glue, a multi-word-surname leak, an all-lowercase "dr" leak, and a Kenzolin/Kenzipine fuzzy-match collision. Regression cases recorded in `prototype/REGRESSION_CASES.md`; tester findings logged in `pilot/feedback_log_template.csv`.
- 2026-07-06 PM · **Design decisions resolved** (see DECISIONS): #18 login = company-issued PIN/ID card, stay-logged-in per phone; #25 co-player credit = two separate views; #26 screenshot watermark = sanctioned anti-leak approach; #27 press-and-hold-to-reveal = rejected (would defeat codes-only protection). Communication preference added to `CLAUDE.md` (How to talk to Von).
- 2026-07-07 · **Doctor-side polish:** removed the "+ Match" manual patient chip, replaced with a one-tap "📊 What are my plays this Era?" quick-access button; header shows the signed-in doctor's Player alias (single-sourced constant that also feeds the AI prompt) in place of the placeholder streak (removed — a real streak needs server-side cross-month history).
- 2026-07-07 · **Login gate implemented** (DECISIONS #18): company IRIS-card ID + PIN sign-in in front of the app; wrong credentials rejected; a `role` on the card routes doctors → chat, admins → admin console; signed-in card drives the alias.
- 2026-07-07 · **Screenshot watermark implemented** (DECISIONS #26): faint tiled diagonal trace code (alias·YYMMDD, e.g. `JD28·260706`), on-device after sign-in, code-only (never a real name), pointer-events-transparent.
- 2026-07-07 · **Admin console built — all 7 tabs** (behind its own admin login, demo card `IRIS-ADMIN`/`0000`): Roster (real-name↔code lists, searchable, working Add), Review queue (temporary codes), Dashboards (stats + CSS bar charts), Reconcile (match vs pull-through → resolve variances → lock the Era), Reports (coded Era report + working in-browser CSV download), Issues (flagged plays/duplicates/masking cases → resolve), Requests (doctor requests + open items → mark done). Real names appear ONLY here (the privileged back office), clearly separated from the doctor app.
- 2026-07-07 · **Doctor app ↔ admin console connected through a shared backend** (DECISIONS #29): local harness grown into a tiny store (`/api/plays`, `/api/roster`, `/api/review`). Doctor's confirmed play is POSTed as codes-only; admin reads live and merges over demo samples (with a ↻ Refresh button). **Verified end-to-end:** a doctor-logged play ("6 Kenzolin at Cardinal Santos to Maria Santos, 52" → PRAZ ×6 · SATURN 33 · MS-52 · H) flowed through the backend and appeared in the admin dashboard.
- 2026-07-07 · **Admin console reformatted to desktop/window layout** (DECISIONS #28): breaks out of the phone frame to full window, left sidebar nav, multi-column content. Doctor app stays mobile.
- 2026-07-07 · **Distributable tester file** `pilot/iris_chat_tester.html` — self-contained snapshot of the prototype for Wave 0 (doctor + admin in one file, Claude.ai-bridge URL so live AI works as a Claude.ai artifact). Regenerate from `prototype/iris_chat_prototype.html` when the prototype changes. ⚠️ **STALE after the 2026-07-09 changes** (tone, copy, caching, game-vocabulary substitution, off-assignment flag, unified Doctors table) — regenerate before Wave 0.
- 2026-07-09 · **Eval suite Phase 1 built** (`tests/masking_eval.mjs`): automated name-hiding checks that lift the real masking engine out of the prototype and replay every regression case, asserting no real name ever appears in the sent text (11/11 pass). Auto-runs via a project hook whenever the prototype's masking code is edited (`.claude/check_masking_on_edit.sh`). Phase 2 (assistant-conversation grader) deferred until we tune the assistant. New working rule: **record significant changes as they happen** (CLAUDE.md).
- 2026-07-09 · **Assistant tone fix** (DECISIONS #31, tester finding VON-01): the assistant was opening with a casual "Hey Player JD28!"; added an explicit professional/courteous TONE block to the system prompt (no casual greetings). Verified live.
- 2026-07-09 · **Unified Doctors table** (DECISIONS #32, tester VON-07): merged the separate "Accredited MDs" roster and the "Assignments" tab into ONE Doctors table — real name · alias · assigned hospitals — with add/edit/delete and per-doctor hospital tagging. Hospitals stays a master list; an inline "+ New hospital" adds to the master list then tags the doctor. One `acDoctors` record per doctor is the single source the off-assignment check and the admin console both read; the Assignments tab was removed. Verified end-to-end; checks still 22/22.
- 2026-07-09 · **Full admin roster CRUD** (DECISIONS #28, tester VON-06): the Roster (MDs, Hospitals, Products) and Doctor↔Location assignment tables now support add / edit / delete behind an "✎ Edit ⇄ ✓ Done" toggle — clean read-only view by default, edit view with inline row edit + delete + add. Roster edits use an editable copy of the masking dictionaries; a hospital added here immediately becomes a selectable Location in assignments. All operations verified live. Checks still 22/22.
- 2026-07-09 · **Admin console made responsive** (DECISIONS #28, tester VON-05): it was cramped/clipped when the window wasn't full-desktop width. Now: left sidebar + wide content at ≥820px; below that a horizontal top tab-bar with full-width content, and wide tables scroll inside their card (no page overflow). Verified at 375px.
- 2026-07-09 · **Doctor↔Location assignment + off-assignment flag** (DECISIONS #32, tester VON-04): each doctor has a set of assigned Locations (roster data, server-side per doctor; demo card for now). A play at a Location not in that set still posts (non-blocking) but a deterministic on-device check raises a codes-only admin-review flag (Player + Location) shown in the doctor-app drawer and the admin console. Admin validates manually. **Admin tools built same day:** a new **Assignments** tab (per-doctor table — add/remove Locations) and **disposition buttons** on each flag in the Review queue ("Add Location to Player" / "Deny play"). All verified live; the deterministic rule is guarded by the automated checks (22/22).
- 2026-07-09 · **Game-vocabulary word substitution** (DECISIONS #20 first slice, tester VON-03): typing "hospital"/"doctors" (or typos) now renders as "Location"/"Player" on the doctor's own message bubble and in the text sent to the AI — real-world category words never reach the screen (invariant #6). Deterministic, on-device, codes untouched; covered by the automated checks (now 17/17).
- 2026-07-09 · **Prompt caching + token optimization prototyped** (DECISIONS #30, open item #5): split the AI system prompt into a cacheable static rulebook and a per-Player tail (alias/date/filed plays kept OUTSIDE the cached prefix, so all Players share one cache and the date never busts it). **Verified live in the local harness:** the 2,370-token rulebook is written to cache once, then read at ~10% price on every following message; the assistant still behaves correctly (codes only, professional tone, correct confirm). This is the reference structure IT will implement in the real backend proxy. Cheaper-model routing deferred until the dialogue eval set exists.
- 2026-07-09 · **Welcome + placeholder no longer reveal the hiding mechanism** (DECISIONS #9 extended, tester VON-02): reworded the welcome message and input placeholder to game vocabulary only (were "type the real names… converts to your Avatars" / "real names are fine"). Also moved the greeting to render **on sign-in**, personalized by Player code, instead of pre-rendered behind the login screen. Then per Von, stripped the sign-in screen further: removed the confidentiality shield line entirely and trimmed the subtitle to just "Sign in with your company-issued IRIS card." The sign-in screen now says nothing about how confidentiality is handled. (Small "Guardian shield on" header badge kept post-login.)
- 2026-07-12 · **Hardened AI-reply parsing (follow-up tester finding: raw JSON on screen).** When a doctor bundles several plays, the model can emit multiple confirm objects at once; the reader was grabbing all of them (invalid) and an earlier fallback printed the raw data into a chat bubble. Now `callAI` uses a brace-aware `firstJSONObject()` that lifts only the first complete object (ignoring surrounding prose and trailing objects; returns nothing on a truncated one → clean prose fallback, never raw code), so a bundled reply still yields a valid single confirm card. Also `normalizeReport` now aliases the game-vocabulary field names the model sometimes uses (Location→hospital, Match/Username→patient, other_Player→other_md) so a valid confirm never renders a blank card. Verified live: the exact multi-play message yields a clean "Play 1 of 4" card, no raw JSON on screen.
- 2026-07-12 · **Fixed the "Signal lost" crash on multi-play messages + made AI replies resilient (tester finding).** Von bundled several plays in one message; the assistant replied in prose (not the strict JSON the app requires), so the parse threw and it showed a misleading "Signal lost — nothing left your device" — even though the request was 200 OK and the reply was codes-only. Fixes: (1) `callAI` now distinguishes a real transport/API failure (→ retry path) from a good 200 reply that isn't clean JSON (→ falls back to showing the assistant's sentence as a clarify, JSON/markdown stripped, so the chat continues instead of crashing); (2) system prompt now enforces ONE PLAY AT A TIME — exactly one confirm per reply; when a doctor lists several plays, it confirms the first, then the next after each is logged; (3) corrected the error copy, bumped max_tokens 1000→1200. Verified live: the same multi-play message now yields a clean "Play 1 of 4" confirm (June 2026, backdated), no crash.
- 2026-07-12 · **Previous-month reporting made conversational (per Von, DECISIONS #34 revised) + two extraction fixes.** Replaced the composer month dropdown/toggle with a fully conversational flow: the doctor just says the period ("these are for last month", "back in May") and the assistant sets the month and carries it forward across plays (REPORTING PERIOD block in the system prompt); removed the dropdown/banner/`LOG_PERIOD`. Verified live end-to-end: "these are for last month…" → confirm card shows Era 2606 · June 2026 with the backdated notice. Along the way fixed two on-device extraction bugs Von hit: (a) a time phrase like "for last month" was being mis-masked as a patient code "LM" — added time/period words to the patient stop-list (regression case 11); (b) the glued age typo "88yrold" wasn't read as an age — the reader now strips a trailing "old" and re-tests the unit (case 10). Checks now 25/25.
- 2026-07-12 · **Fixed a patient-name leak (tester finding, invariant #1).** Von hit a case where a Location clause immediately before the patient with a comma — "makati med, juan dela cruz 98yo, kenz 5" — sent the real name "juan dela cruz" in the clear (the assistant then asked for initials). Cause: the patient-name scanner swept the hospital-name tail across the comma into the Match, and the comma-in-name defeated the in-place swap so nothing was masked. Fix: the name scan now stops at a comma/semicolon/colon boundary; the patient converts correctly to JDC-98. Added automated regression case 9 — checks now 23/23. (`extractMatch()`; REGRESSION_CASES case 9.)
- 2026-07-12 · **Duplicate-play + backdating build (4 slices, DECISIONS #33–34).** (1) **Backdating fix:** the target month/Era a doctor states is now actually sent to the backend and shown on the confirm card ("2605 · May 2026") with a plain "counts toward…" line — previously the Era was parsed on the card but dropped on submit, so late plays filed to the wrong month. (2) **On-device duplicate catch:** before logging, a play is compared to this session's plays by identity (codes only); a look-alike triggers an "is this a separate play or the same one?" prompt rather than a silent block — "Separate" logs it, "Same" cancels. (3) **Admin handling:** backdated plays into a closed month raise a **late-entry flag** in the Review queue with "Accept into Era ‹past›" / "Roll into current ‹open›" dispositions; the Issues tab gained a **live duplicate net** that surfaces genuine repeats reaching the backend (cross-session/device) separately from the demo examples. (4) **Catch-up mode:** a composer period selector files a backlog of unstated-month plays to a chosen past month (explicit month always overrides), with an amber active-period banner. Masking untouched — checks still 22/22. Also fixed the session harness mini-server to return the shapes the prototype expects (`{plays}`, roster object) so the live doctor→admin flow works. Verified live end-to-end.
- 2026-07-11 · **Admin console "SAMPLE DATA" badge** — audited the console live (all buttons wired and working; PDF export intentionally disabled). Five tabs run on hardcoded demo data (Dashboards, Reconcile, Reports, Issues, Requests); added an amber "SAMPLE DATA" pill to each so Wave 0 testers don't read the figures as real. The two genuinely live tabs (Roster, Review queue) stay unbadged. Verified live.
- 2026-07-07 · **Fixed login in sandboxed frames** (Claude.ai artifacts): sign-in was driven by `<form>` submit, which artifact sandboxes block, so "Sign in" did nothing in Claude.ai. Now driven by a plain button click + Enter key (`doLogin()`). Tester file refreshed with the fix — **hand testers the updated copy** (the first one had the bug).

## Next steps (ordered)
1. **Wave 0 (internal) — kit ready, now execute:** (a) load the prototype into a Claude.ai window and confirm the AI loop responds; (b) send the recruiting message (facilitator pack Part D) to 3–5 SFE/marketing/data-team testers; (c) run the 10 scenarios per the answer key; (d) triage feedback into `pilot/feedback_log_template.csv` twice that week
2. **Hosting for Wave 1:** hand IT the backend-proxy spec (PILOT_PLAN §5) — static page + serverless proxy holding the Anthropic API key; prototype AI only works inside Claude.ai until then. **Backend spec must include prompt caching + token-cost controls (see blocker #5) — build before any tester/pilot deploy.**
3. **Wave 1 (doctors):** select 5–10 friendly accredited MDs; distribute `TESTER_GUIDE`; 2–3 week window; weekly triage cadence
4. **Scripted-dialogue eval suite — Phase 2 DONE (2026-07-13):** `tests/sample_plays.mjs` + `tests/e2e_plays.mjs` replay the saved sample messages through masking + the live assistant (12/12), triggerable via the `/test-samples` skill ("run samples"). **Remaining:** grow the sample library as new behaviors/bugs appear; bring the **admin console** under automated test (plays/issues reflected + Era-filtered report — currently verified by hand only).
4a. **Multi-play as a first-class feature — BUILT 2026-07-13 (review-all list, DECISIONS #35).** Slices 1 (batch confirm + review card + per-play deterministic checks) and 3 (intra-batch duplicate) done + verified live end-to-end. **Remaining polish:** conversational per-play edits ("in play 2, units are 4" currently re-emits the whole batch via the assistant — fine, but could be smoother), and folding multi-play into the fast masking-only tier. Backend already accepts one coded play at a time; a batch is just a loop.
5. **Real build = two front-ends on one shared backend** (DECISIONS #28, #29): doctor app = React Native/Expo (mobile); admin console = **separate desktop web app**. Both prototyped and ready to spec: login (PIN/ID card, DECISIONS #18 — done in demo); watermark (DECISIONS #26 — done in demo); the full admin console (all 7 tabs) as the spec basis for the web app; "credited to me by others" + dashboards/reports fed by real server data (currently demo samples); temporary-code review queue backed by the real admin tool. Backend holds the roster (real↔code key) server-side, receives codes-only from the doctor app, and enforces per-doctor scoping. Start the Expo build once Wave 1 feedback stabilizes composer behavior.

## Open items / blockers
8. ✅ **"Where do filed reports get stored?" — RESOLVED 2026-07-23 (Von): a managed database (DECISIONS #56).** The `/server` proxy (Step 2) forwards the AI call and the whitelisted review channel but does **not persist filed plays** (or stock/feedback) anywhere the admin console could read — by design it "logs events, never content." Von's decision: filed reports (codes-only) go to a **standard hosted/managed database** alongside the proxy; the doctor app sends codes-only → proxy/data-service writes to the DB → the separate admin console reads via an authenticated read path (realizes DECISIONS #29's "one shared backend" for production; scales straight into the real build). Everything stored stays codes-only (invariant #1); the only sanctioned real-name fields remain the review-queue whitelist (#47). **Unblocks the rest of Step 5.5** (build the standalone `/admin` tool wired to this store) and Step 5.7 (hosting the proxy + this DB). Build not started — decision recorded per Von's "point 1 only."
1. 🔴 **Hosting/proxy** — doctors cannot use the prototype until IT deploys the hosted build (prototype's AI bridge is Claude.ai-only)
2. ✅ **Legal/DPA posture — RESOLVED 2026-07-22 (Von).** The on-device masking + alias-table approach is confirmed with legal; no longer a Wave 1 blocker. (DECISIONS #17)
3. 🔴 Pilot MD selection + KASS coordination for tester aliases
4. 🔴 Residual extraction gap: lone/edge patient names (brief §9 item 10) — **mostly closed by the net-3 verify gate (DECISIONS #48), but one live leak is OPEN and must be fixed before the pilot (DECISIONS #55).** A leftover possible-name blocks send and asks the doctor to verify instead of flowing to the AI (guarded by eval cases; masking suite 46/46). **Fixed 2026-07-22:** surname-first with a comma ("Dela Cruz, Roberto") left the given name in the clear — the gate now also flags a name-like word right after an already-extracted patient. **STILL OPEN (🔴, blocker for Wave 1):** an *all-caps single surname* after a preposition ("for SANTOS, Maria") is read as a code by the gate and not extracted → the surname leaks. `tests/masking_holds.mjs` prints this as a KNOWN GAP every run. Needs a **dedicated comprehensive surname-first masking task** (handle surname-first in all shapes without false-gating real codes like "MS 52"; add regression cases). net-3 remains a heuristic overall — measure the real miss rate in pilot; the Expo build can swap in a fuller on-device name model behind the same gate.
7. 🟡 **Code-review findings to carry into the Expo build (2026-07-21).** Two full reviews ran (correctness/privacy/scale + a pilot-readiness security/reliability pass). **Resolved:** P1 real names to /api/review (DECISIONS #47); P3 lone-name leak (net-3 gate, DECISIONS #48); C3 "at &lt;word&gt;" false-positive (NONPLACE stoplist, VON-18, 2026-07-22 — same change added "in" as a Location preposition, closing an unknown-hospital leak). Remaining, tracked for the rewrite: **(P2)** the in-app Feedback box POSTs the doctor's free-text unmasked — a doctor could type a real name; mask/guard `/api/feedback` (quick follow-up, offered to Von). **(P4)** sign-out doesn't clear `state.pendingAliases`/`pendingCounters`/`lastUserMsg` — on a shared phone, names doctor A typed for unknown entities linger into doctor B's session; add to the logout wipe. **(R1)** the masking pipeline threads HTML strings through the business logic and mutates them by regex — in React Native this must become structured segments with codes rendered separately (the biggest thing that won't transplant unchanged; port the 46-case eval as a merge gate on the new engine). **(C3 — resolved 2026-07-22)** the "at &lt;word&gt;" hospital net false-positived on "at home/bedside/night"; fixed with the NONPLACE stoplist added in VON-18. Minor: `normalizeReport`/report-range date handling hard-codes July 2026 fallback and filters by filing date. See the pilot-readiness assessment for the full go/no-go punch-list (hosted backend, durable storage, concurrency-safe writes, banned-word output filter server-side, legal/DPA sign-off).
6. 🟡 **Co-Player credit-sharing math is undefined (needs Data Team).** The admin report now *displays* the Co-Player on a shared play, and "Add to roster" promotes an unknown co-player. But what the units DO when a co-player is credited — split 50/50, full credit to the starter (role field), shared to both, or something else — is deterministic crediting code (invariant #7) that must come from Data Team worked examples. Define this rule before the report numbers can be trusted for a co-play. Until then a shared play attributes all units to the logging Player and just names the Co-Player alongside.
5. 🟡 **Token-cost controls in the backend — build before tester/pilot deploy.** (Prompt-caching structure now prototyped + verified in the local harness 2026-07-09 — see DECISIONS #30; remaining work is the real backend implementing the same structure.) At scale (1,000 doctors × 1–2 reports/day, several AI messages per report) the app resends a large fixed instruction sheet (~2,000 tokens) on every message. Required in the backend-proxy spec:
   - **Prompt caching** on the fixed instruction sheet (Anthropic prompt caching) — single highest-ROI cost lever; ~10% price on repeat reads. 5-min cache TTL is fine (a doctor's report is one sitting). Confirm the cached prefix clears the model's minimum-cacheable size or it silently won't cache.
   - **Restructure the instruction sheet so the per-doctor alias and the date sit OUTSIDE the cached prefix** — otherwise all 1,000 doctors get separate caches and the date breaks the cache daily. Small prompt-design change; can be teed up in the prototype now.
   - Trim conversation history resent per call (currently last 16 messages); reduce clarify round-trips via better on-device extraction (ties to #4); consider a cheaper model for simple turns — only after it passes the dialogue eval set.

## Session log
| Date | Summary |
|---|---|
| 2026-07-25 (queues + requests) | Reworked the standalone Requests tab away (DECISIONS #72): supply follow-ups → Stock, play corrections → Review Queue, with per-play **Edit** (new `/api/admin/play/edit`, codes-only, audited) + **Void** in Reports; to-dos dropped. Then scaled the action queues (DECISIONS #73): Review Queue / corrections / Stock now default to **Open** with a text filter + 25/page paging + "N open · M total" counts (Review Queue also type chips). Console-only for #73; #72 added one server endpoint. Proof `prove_assign_and_credentials` 38/38; all proofs + QA green. `improve/gate-location-vocab`. |
| 2026-07-25 (unknown-loc loop) | Answered Von's "1 item or 2?" by running it live (JD28 at an unknown, unassigned Location → **one** review item; territory check skips temp Locations). Then, per Von, made resolving an unknown Location close the loop (DECISIONS #71): **Add to roster** now backfills the filed play off its LOC-PEND code to the real Location (#66 fast-follow done) and offers a pre-ticked "also assign to the reporting doctor". Verified live (CEBU 45); proof `prove_assign_and_credentials` 33/33; all proofs + QA green. Server + admin only; doctor app untouched. `improve/gate-location-vocab`. |
| 2026-07-25 (UX pass) | Ease-of-use follow-ups on the roster + review queue (DECISIONS #70), all console-only: **Reassign** picks from a **dropdown of existing Locations**; assigning a doctor to Locations uses a **tick-box picker** (shared `LOCATIONS` cache); **Access ID defaults to the doctor's alias** (editable); and the **add experience** got both a **guided inline** form (auto-suggested code + live dup-check, Location picker for doctors, unit for products) and an **"Add with preview…" dialog** for all three roster types. Product unit is now editable in the edit row too. No server change → all proofs hold (new `prove_assign_and_credentials` 27/27; 15/15, 19/19, 43/43, 17/17, 10/10, 9/9); QA green (masking 46/46, live 13/13). Four commits on `improve/gate-location-vocab` (not pushed; `main` frozen for IT). |
| 2026-07-25 | Rebuilt the session-only local test stack (doctor app at localhost:8765/, admin console at /admin.html, one shared backend = the real `server/proxy.py` + a scratchpad launcher that also serves the two pages; reused the saved API key). Then, from Von testing it: **off-assignment flag** overhauled (DECISIONS #68) — kept in the Review Queue but with Add-to-Locations / Deny (voids the play) / Reassign, and a plain "not assigned to this Location" note instead of a code in the real-name column; **doctor roster** gained an **Assigned locations** column (shown + editable, #69) and **admin-issued Access ID + set/reset-only hashed PIN** (never displayed or logged; unique access ID; viewer blocked; audited). New proof `prove_assign_and_credentials.mjs` 27/27; QA green (masking 46/46, live 13/13); prior proofs 15/15, 19/19, 43/43, 17/17, 10/10, 9/9 on a throwaway backend. Server + admin only; doctor app untouched. Three commits on `improve/gate-location-vocab` (not pushed; `main` stays frozen for IT). |
| 2026-07-23 (roster paging) | Changed the admin Roster tab from search-only to **paginated browsing** (DECISIONS #59): opens showing page 1 (50/page), next/previous, sortable columns, search kept as a filter; Doctors/Hospitals/Products. Protection preserved — new admin-only `/api/admin/roster/list` is hard-capped at 50 per response (no dump-all endpoint; browser holds one page), and every page view is audited with its exact range. Viewers unaffected. Confidentiality proof updated (paged access allowed, bulk dump blocked, browse audited) → 35/35; QA green (46/46); doctor app untouched. On `deploy/step5.6a-admin-writeback`; stopped per Von. |
| 2026-07-23 (Step 5.6A) | Added a hard Definition-of-Done + Step 5.7 requirement to `DEPLOY_PROMPTS.md` (real `IRIS_ADMIN_USERS` before hosting; demo logins must stop working), merged Step 5.6 to `main` (clean), then **built deploy Step 5.6A — admin write-back, roster management, per-lookup name resolution.** Roster seeded from the prototype's DEMO data, held server-side; admin-only search/add/edit; per-lookup resolution (audited, no bulk dump). Write-back: review dispositions (add-to-roster, alt-name library → doctor app, reject, late accept/roll), stock resolve-and-notify, mark feedback reviewed / issue resolved, Era lock — all admin-only + audited. Viewer stays codes-only; roster never on a phone; no name to the LLM; doctor app untouched. Refined the QA confidentiality test wording. Verified live; proof 28/28; 5.5 15/15, 5.6 19/19; QA green (46/46). Branch `deploy/step5.6a-admin-writeback`; **not merged, stopped before 5.6B per Von.** |
| 2026-07-23 (Step 5.6) | Merged the 5.6/5.6A/5.6B deploy-prompt edits to `main` (clean), then **built deploy Step 5.6 — admin console sign-in & roles.** Real login (salted PBKDF2 + session tokens), server-enforced roles (viewer = codes only with real names redacted; admin = sees sanctioned names + an Audit tab), append-only audit log of sign-ins and real-name reveals, and no bulk roster download. Users from `IRIS_ADMIN_USERS` (demo defaults + warning). **Doctor app untouched** — only the console-only real-name endpoints + `/api/admin/*` now require a session; the doctor app's codes-only reads and writes stay open. Verified live (viewer redacted / admin full / audit trail). Proof 19/19; 5.5 proof updated for auth 15/15; QA green (46/46). Branch `deploy/step5.6-admin-auth`; **not merged, stopped before 5.6A per Von.** |
| 2026-07-23 (later) | Restyled the standalone admin console to **match the original prototype admin console** (Von's request) — same light theme, dark sidebar, purple accent, and the same 10 tabs in the same order, so it's familiar to the back office. **Presentation/structure only; no data exposure widened** (confirmed with Von before building the tabs that would have): Roster/Reconcile/Requests are locked placeholders; Dashboards/AI Cost/Issues added as codes-only views; Stock stays codes-only (prototype's code→name resolution deliberately not reproduced). Verified all tabs live in the browser; proof 15/15, QA suite green (46/46 masking). On `deploy/step5.5-standalone-admin`; **not merged** per Von. |
| 2026-07-23 | Merged the Step 5.7/Step 6 deploy-prompt edits to `main` (clean). Settled the Step 5.5 storage gate: **managed database** (DECISIONS #56, resolves blocker #8). Then **built deploy Step 5.5** — extended the Step 2 proxy into the shared backend with a SQLite managed-DB local instance (`server/store.py`) persisting codes-only filed reports + the whitelisted review channel (every write field-whitelisted server-side), and a **standalone `/admin` browser console** that reads them as codes only (no roster/code→name view; real names only in the review-queue + feedback whitelisted fields). No change to the doctor app. Proof `admin/proof/prove_step5_5.mjs` 15/15 (real report through the shipping masking engine → console reads codes; no name in any codes-only report; review = whitelisted fields only, smuggled patient/report-text dropped) + visual browser confirmation. Masking 46/46. Branch `deploy/step5.5-standalone-admin`. **Stopped before Step 5.7 per Von.** |
| 2026-07-22 (EOD) | Session close-out. Added deploy **Step 5.5** (carve the admin console into a standalone `/admin` browser tool that reads REAL filed reports) to `DEPLOY_PROMPTS.md` + a Definition-of-Done line — recipe only, nothing built. Recorded the two open items before the pilot: the **surname-first name leak** residual (DECISIONS #55, blocker #4) and the **"where do filed reports get stored" decision** that Step 5.5 depends on (blocker #8 — the proxy forwards but doesn't persist plays). **Git housekeeping:** the deploy work spanned eight sequential topic branches forming one linear chain; all are fully contained in `deploy/step5.5-prompt-admin-console`. Merged that tip into **`main`** (fast-forward — nothing stranded) and **pushed everything to GitHub** so both Macs sync. Deploy Steps 1–5 done; Step 5.5 and Step 6 not started. |
| 2026-07-22 (PM) | **Deployment build session — worked `DEPLOY_PROMPTS.md` Steps 1→5, one at a time, each on its own branch.** Step 1 `DEPLOY_PLAN.md` + two decisions (split admin console [#53], new store listing [#54]); Step 2 `/server` production proxy (key in env var, whitelisted review channel, never logs content); Step 3 repointed the app's outbound calls to the proxy on a copy (`app/iris_chat_app.html`, diff = call path only); Step 4 Capacitor wrapper in `/mobile`, **doctor app only** (admin console stripped from the phone build); Step 4.5 offline queue → **persistent on-device storage** (survives restart, files exactly once, codes-only to disk); Step 5 full **automated QA suite** (`tests/qa.mjs`) wired to re-run on every app/test edit. The QA suite caught a real, pre-existing patient-name leak (surname-first "Dela Cruz, Roberto") — **fixed** it (net-3 gate flags a name after an extracted patient); **one residual open** (all-caps single surname "for SANTOS, Maria", DECISIONS #55, blocker #4) to fix before the pilot. Added Step 4.5 to the deploy recipe + Definition of Done. **Stopped at end of Step 5; Step 6 (real-phone testing) not started.** All committed on `deploy/step5-qa-suite`. |
| 2026-07-22 | Session wrap-up. Shipped two features end-to-end this session — product out-of-stock reporting (DECISIONS #51: doctor report → admin Stock tab with real-name resolution + add-to-roster → resolve-and-notify loop; plus VON-18 masking leak fix for "in <hospital>") and a full AI Cost dashboard (DECISIONS #52: live spend in PHP, cache savings, trend, by-doctor/Location/function attribution, budget+alert, unit economics, clarify/Haiku levers, period selector). All committed. **Von's call: next session pivots to full development for deployment** — see the NEXT SESSION banner under Current phase. |
| 2026-07-22 | AI cost dashboard action-driving upgrades — built all 5 Von requested (DECISIONS #52): monthly budget + green/amber/red alert, cost per doctor + per report, clarify-overhead % with peso saving, one-line Haiku saving lever, and a period selector (All/Today/7d/30d/Month) filtering everything — topped with an executive health strip. Also extracted the app model to a single AI_MODEL constant (one-line Haiku switch; dashboard label auto-follows). Verified live: over-budget flips red, period filters/re-labels, levers compute. Boundary: budgets/periods need persisted history (demo resets per session). |
| 2026-07-22 | AI cost analytics (Von follow-up, DECISIONS #52): added a cost-over-time trend (adaptive day/hour/minute) and "where the cost comes from" breakdowns by doctor / Location / function, plus an in-product answer to "does caching get cheaper over time" (hit rate + why). Moved metering to the client so each call carries codes-only context (Player/Location/function); proxy just forwards. Also converted all figures to PHP at an editable indicative rate and split the projection into current-users vs an editable target. Verified live: 4 calls / 2 doctors / 3 functions / 3 Locations attributed, 75% cache reuse. |
| 2026-07-22 | Built the second improvement: **AI cost dashboard** (DECISIONS #52). Harness `/api/messages` proxy now meters each call's token usage + model to `ai_usage` (where the real hosted proxy would); new admin "AI Cost" tab prices it into live spend, cache savings, by-model, and a 1,000-doctor projection re-priced at Sonnet vs Haiku (ties to DECISIONS #50). Verified live: 2 app calls → $0.0178, cache read at ~10% price (91% of input cached), projection ~$19.2K/yr Sonnet / ~$6.4K/yr Haiku — matches the earlier estimate. Boundary: harness log resets per session; hosted build persists + adds trends/budgets. Both of Von's session improvements now done. |
| 2026-07-22 | Von's request: made the Stock tab's pending-hospital marker a working "Add to roster" button that reuses the Review queue's openRosterModal flow (correct name, assign Location code, save). An unknown hospital from a stockout can be rostered without leaving the tab; cell then reads "✓ on roster". Verified live (Ace Valenzuela → ACE 07). |
| 2026-07-22 | Von's catch on the OS feature: an unknown hospital showed as the bare temporary code (LOC-PEND-1) in the admin Stock tab — useless to the supply team. Now the Stock tab resolves a pending code to the doctor's typed hospital name via the review-queue channel (invariant #1 intact — the stock alert stays codes-only), marked "pending — add to roster", matched by reporter+code. Added `player` to the unknown-hospital/co-player `queueReview` flags so the match is unambiguous. Verified live. |
| 2026-07-22 | Tester VON-18 (Critical/leak), found while re-testing OS reports: "Kenzolin not available in Ace Valenzuela" leaked the unknown hospital — the unverified-hospital net matched only "at"/"sa", not "in". Fixed: added "in" + a NONPLACE stoplist (stock/supply/total/home/bedside/night/…) so "in stock"/"at home" don't mint a phantom Location (also closes review C3). Now "Ace Valenzuela" → LOC-PEND-1 on screen + to the AI; real name only in the review queue (invariant #1). Verified live; masking 43 → 46/46, e2e 13/13. |
| 2026-07-22 | Built the first of two improvements Von raised: **product out-of-stock reporting** (DECISIONS #51). Doctor reports an OOS product in plain language → on-device masking converts product+hospital to codes → respectful "Supply alert" confirm → codes-only to `/api/stock`. New **separate admin "Stock" tab** (Von's placement call) maps codes back to real names, resolves with a short update, and pushes it back to the reporting doctor, who sees it on next sign-in. Verified live end-to-end doctor→admin→doctor; new e2e sample; full e2e 13/13, masking 43/43. Boundaries: offline OS not queued; admin note banned-word-filtered on-device, real-name-in-note filtered server-side in the hosted build. **Second improvement — AI cost dashboard — not started.** |
| 2026-07-22 | Costed the AI at 1,000 doctors and ran a live Haiku evaluation (DECISIONS #50). With caching: Haiku ~$5–8K/yr vs Sonnet (the tested model) ~$15–20K/yr — caching discounts only the repeated instruction sheet, so the model is the real cost lever. Pointed the harness at claude-haiku-4-5 and tested: masking 43/43, no leaks/banned words in any case; live doctor→admin worked (single, co-play THOR, 2-play batch → admin Reports 4 lines/18u; unknown "Dr. Ken" → Review queue PEND-1). Two non-safety gaps vs Sonnet (back-dating asks an extra confirm = the 11/12 FAIL; one age re-ask), both prompt-fixable. Haiku judged viable for pilot pending a small prompt polish + re-run; final model choice left to Von. `tests/e2e_plays.mjs` now takes `IRIS_MODEL` (defaults to sonnet). |
| 2026-07-21 | Fixed the two items Von prioritised from the pilot-readiness review. Net-3 name gate (DECISIONS #48): a leftover possible-name now blocks send and asks the doctor (patient/doctor/not-a-name) instead of reaching the AI — closes the lone-name leak (blocker #4 → 🟢), guarded by 7 new eval cases (suite 36 → 43/43; writing them caught a real bug). Offline report verification (DECISIONS #49): report/stats requests now answer on-device (work offline, instant), and each play shows a send status (⏳ waiting to send vs filed) so the doctor can verify nothing was lost and re-file — Von's "ask what were the recent reports" workaround. Both verified live. Still open from the review: P2 feedback unmasked, P4 logout wipe, R1 HTML→RN masking, C3 stoplist. |
| 2026-07-21 | Pilot-readiness / go-no-go review (2nd agent pass). Verdict: conditional NO-GO for real doctors today (no hostable artifact + open confidentiality gaps), GO to commission the Expo + backend build from the blueprint. Ordered punch-list captured for IT: hosted backend proxy (server-side key, prompt caching, banned-word output filter, per-doctor scoping, auth endpoints); close the confidentiality gaps; re-architect masking to structured segments + port the eval as a merge gate; durable offline storage; concurrency-safe writes; legal/DPA sign-off; business scale inputs; defer crediting (run pilot as capture-and-confirm). |
| 2026-07-21 | Privacy fix (DECISIONS #47) — closed the code review's top finding (P1): unrecognized real names were POSTed to the back-office review list while the spec said no real name is ever transmitted. Von chose "keep it, make it clean": the typed name now travels only to the locked back-office review channel (never the AI, never the plays store), `queueReview` sends a field whitelist so no patient/free-text can ride along, the misleading comment is fixed, and the canonical spec (brief §4 + CLAUDE.md invariants #1/#3) is reconciled to match the built behavior. Verified live (unknown hospital+doctor → codes to AI, names to review-only). Masking 36/36. Logged the remaining review findings as open item #7 (P2 feedback unmasked, P4 logout wipe, R1 HTML-string masking → RN structured segments, C3 "at &lt;word&gt;" false-positive). |
| 2026-07-21 | Session goals from Von: (1) attach a file/photo of a report, (2) online/offline capability, (3) architecture + code review (iOS/Android, 1000 users, latency, QA). **Built #1** — the file + photo attach flow, on-device masking into the review-all card, verified live doctor→admin with zero real-name leakage (DECISIONS #45). **#3 review** ran (background code audit): surfaced real items — P1 unknown real names are POSTed to /api/review (contradicts invariant #1 — the biggest one to fix in the Expo build), P3 the known lone-name extraction gap still reaches the AI, and RN-portability notes (HTML-string masking must become structured segments). **#2 offline** — options framed for Von (offline-first: capture+mask offline, queue codes-only, auto-sync; free-text-needing-AI is the only path that must wait). Attach + offline both stress the extraction gap (a bulk file/photo of real names needs net-3 before send). Next: agree the offline scope and the Expo-build fix-list. |
| 2026-07-21 | Product category surfaced in the product database + unknown-product handling (Von, DECISIONS #44). Admin Products roster gained an editable Category column (Blaze/Breeze/Terra per product; add + edit); the doctor's on-time tracker reads the product's category. A product not on the roster is now rejected with a plain "not part of the game — can't be logged" message (nothing logged) and reported to the admin Review queue (Player code + typed product name). Verified live doctor→admin (reported "Aspirin" → told + flagged). Masking 36/36. Also removed demo card IDs/PINs from sign-in (earlier same day). Open: Von's question on improving the on-time reporting nudge — suggestions proposed, awaiting his pick. |
| 2026-07-21 | Removed the last doctor-side sample data: the "Credited to me by other Players" table (Von's catch) was hardcoded ANIKA/ZEUS rows; now derived from real backend plays where another doctor named this doctor as co-player, scoped to the current month, empty until a real co-play exists. Verified live: empty for DAWN → logged a co-play as JD28 crediting her → her credited table showed the one real row. |
| 2026-07-21 | Flexible doctor stats/reports (Von's request, DECISIONS #42) + admin console responsive/typography pass. Reports now support any period (this month / MTD / YTD / named month / date range) and any breakdown (list / per Location / per Avatar), always rendered as a table, computed on-device (invariant #7); the app loads the doctor's prior plays from the backend at sign-in so MTD/YTD span months. Ran a full live test in the doctor window (4 plays across May–July, 2 Locations, 3 products) and confirmed each report table is correct and the plays reflect in the admin Reports by Era. Fixed a concurrent-write race in the local harness (added a write lock; also updated the /test-app skill). Earlier in the session: admin console fluid typography (text scales with the window, ~15px minimized to 18px full-screen) and confirmed Dashboards/Reports/Reconcile are responsive across desktop/laptop/tablet. |
| 2026-07-21 | Admin-issued doctor card + PIN (Von's question, DECISIONS #41). Login credentials (IRIS card ID + PIN) moved from hardcoded demo constants onto the doctor's roster record; the admin Doctors roster gained an Access column to issue/reset a card and PIN. Login now authenticates against the roster; a doctor with no card can't sign in. PIN never shown back and never sent to the backend/doctor app (roster sync = code+display only) — mirrors the real "set/reset, never read" rule. Verified live: issued IRIS-2609 to THOR (saved); rewired login signs a doctor in from the roster; un-issued card / wrong PIN / unknown card rejected. Masking 36/36. Caveat: cross-window handoff needs a real-build backend auth endpoint (PIN kept out of the shared store by design). |
| 2026-07-21 | Quick-access Location chips (tester VON-17, DECISIONS #40). The doctor composer shows assigned Locations as one-tap chips ordered by usage (most-reported first, seeded cross-session from the backend, updated per play); tapping one pre-fills "at <Location> " so the doctor just adds the play. Required recognizing a Location code as itself in the masker (else "at PLUTO 12" → unknown hospital). Verified live incl. the frequency reordering. Masking suite 35 → 36/36. |
| 2026-07-21 | Doctor alternate names + self-growing name library (tester VON-16, DECISIONS #39). Doctors now recognized by admin-managed nicknames/initials/abbreviations (co-player matching reads the Doctors roster, self excluded); admin Doctors roster got an "Also recognized as" editor mirroring hospitals. Built the growth loop: the review queue gains a quick-link (dropdown of existing doctors/hospitals + one "Add as alternate name" click) that attaches an unknown typed name to an existing entry and shares it across devices via the backend (roster_links, applied on sign-in/refresh). Verified live cross-tab: unknown "Dr. Kenzo" → linked to Dr. Ken Dela Cruz in 2 clicks → doctor app then recognizes "kenzo" as THOR. Masking suite 31 → 35/35. |
| 2026-07-21 | Live testing with Von on a real report format ("JU 60/F UST Hospital … Attending: Dr. Daisy Tagarda"). Shipped three fixes (tester VON-15, DECISIONS #38): keep the doctor-typed patient initials whole (JU-60, not J-60); recognize hospital abbreviations/branch names via an admin-managed "Also recognized as" field on the Hospitals roster (UST/USTH → Location PLUTO 12), with the masker reading from the editable roster (`rebuildPhrases()`); and recognize a doctor naming themselves as attending as a single play ("me"), not a Co-Player, via an on-device self-name check against the signed-in doctor's own name. Two more issues surfaced and fixed: the PH honorific "Dra" was missing from the doctor detector (latent leak) and the confirm card hardcoded "You (JD28)". Added demo doctor DAWN (Dr. Daisy Tagarda, card IRIS-2608/5678). Verified live end-to-end doctor→admin. Masking suite 25 → 31/31. |
| 2026-07-16 PM | Live testing with Von on the cleaned console. Two fixes: the Co-Player now shows as its own column in the admin Era report (+CSV) — a shared play no longer reads as solo; and the Review-queue temporary-code Doctor/Location items got working disposition toggles (Add to roster / Not in the game). Verified both live end-to-end. Raised the open decision that co-player credit-sharing math is undefined and must come from the Data Team (blocker #6) — display done, unit-splitting deliberately not invented. |
| 2026-07-16 | Rebuilt the session-only local harness (doctor app + admin console on one backend, live AI proxy). Per Von, cleared the admin console's demo sample data while keeping the roster, so a play logged on the doctor app reflects cleanly in the admin console with no demo rows mixed in — emptied the four seed arrays, suppressed the SAMPLE DATA badges, reworded the demo notes (all reversible). Verified live end-to-end (synthetic play → Reports row) then reset the store to a clean baseline. Next: Von runs his doctor→admin reflection test. |
| 2026-07-06 | Project formalized. Brief v1.0→v1.3, prototype built + 6 bug cycles fixed (surname false-match, redundant co-player questions, incomplete confirm payloads, typo-tolerant age net, flex-crush layout, in-place substitution). Pilot kit + feedback tooling created. Claude Code setup guide added; repo decision = standalone `iris-chat`. |
| 2026-07-06 | Git repo initialized and pushed to private GitHub `vsguarino/iris-chat` (commit `cc83cee`). GitHub auth on this Mac set up via fine-grained PAT in keychain after the SSH-key web form wouldn't submit (browser quirk). Next: Wave 0 internal testing. |
| 2026-07-06 | Wave 0 prep: built facilitator pack (`pilot/WAVE0_FACILITATOR_PACK.md`) — roster cheat-sheet, 10-scenario answer key, recruiting message — grounded in the prototype's actual seed dictionaries. Wave 0 confirmed Claude.ai-only (hands-on sessions). Next: execute Wave 0 (confirm AI loop, recruit 3–5, run scenarios). |
| 2026-07-06 PM | Stood up a local standalone test harness (mini-server + API key, session-only) so the full app runs on the Mac. COO stress-tested live and drove a big round of prototype hardening: shorthand recognition, table reports, dual report tables, tappable stats prompt, non-blocking unrecognized co-players/hospitals with temporary codes + admin review queue, doctor surname confirm-by-code, hospital typo tolerance, and removal of the redundant Single/Multiple question. Fixed 4 masking bugs (incl. 2 real leaks) found during testing. Resolved decisions #18, #25, #26, #27. Next unchanged: execute Wave 0. |
| 2026-07-09 | Built eval-suite Phase 1: automated on-device name-hiding checks (`tests/masking_eval.mjs`, 11/11) that read the live masking engine and guarantee no real name leaks; wired to auto-run on any prototype masking edit. Added the "record as you go" working rule. Fixed assistant tone (tester VON-01, DECISIONS #31) — no more casual "Hey". Afternoon plan set: token caching (#30), banned-word "don'ts" filter (#20), tester feedback, extraction net-3 (#21). |
| 2026-07-07 | Big build day. Doctor-side polish (Match→plays quick-access, alias header, streak removed). Implemented login gate (DECISIONS #18) and screenshot watermark (DECISIONS #26). Built the **full admin console** (7 tabs: roster/review/dashboards/reconcile/reports/issues/requests) with working search, add, CSV download, and reconcile→lock flows. Decided admin = **separate desktop web app** (#28) and reformatted the console to full-window desktop layout. Grew the harness into a **shared backend** (#29) and **connected doctor↔admin end-to-end** — verified a doctor-logged play flows through the backend to the admin dashboard, codes-only. Flagged token-cost/prompt-caching as a build-before-pilot backend requirement (#30, open item #5). Created a distributable tester file (`pilot/iris_chat_tester.html`) and fixed a Claude.ai-artifact login bug (form-submit → button/Enter). Next unchanged: execute Wave 0. |
| 2026-07-12 | Built the duplicate-play + backdating feature set in four committed slices (DECISIONS #33–34): send target Era to backend + show period on confirm card; on-device duplicate look-alike prompt; admin late-entry disposition + live duplicate detection in Issues; catch-up period selector for bulk backlog entry. Audited the admin console live (all buttons wired; five tabs run on demo data — added the SAMPLE DATA badge on 07-11). Fixed the session harness server contract. Masking checks 22/22. Next unchanged: execute Wave 0 (regenerate the stale tester file first). |
| 2026-07-12 PM | COO live-testing pass — found and fixed four real issues, each with an automated regression case (suite grew 22→25). (1) **Patient-name leak** (invariant #1): a Location clause before the patient with a comma ("makati med, juan dela cruz 98yo") sent the real name in the clear — name scan now stops at a comma. (2) **Glued age typo** "88yrold" now read as age 88. (3) **Previous-month reporting made conversational** per Von — dropped the dropdown; the doctor says the period in plain words and the assistant carries it forward; required adding time/period words to the patient stop-list so "for last month" isn't mis-masked as "LM". (4) **Multi-play robustness**: bundling several plays crashed with a misleading "Signal lost", then printed raw JSON — fixed with resilient first-object parsing (never shows raw code), game-vocab key aliasing, and a ONE-PLAY-AT-A-TIME prompt rule; **multi-play deferred to a real feature (DECISIONS #35, next step 4a)**. All verified live end-to-end. Next unchanged: execute Wave 0 (regenerate the stale tester file first). |
| 2026-07-13 | **Multi-play built as a first-class feature (review-all list, DECISIONS #35) + eval Phase 2 + two fixes it surfaced.** Von picked the review-all experience; built it in verified slices: batch confirm (`confirm_batch`, one object with a `reports[]` array so the parser hardening holds) + a stacked per-play-checkable card with one Submit-all; `commitPlay` refactored into a bubble-less `recordPlay` so each play in a batch runs the deterministic checks; incomplete/duplicate plays block submit; intra-batch duplicate catch. **Eval Phase 2:** saved sample-play library (`tests/sample_plays.mjs`) + end-to-end runner (`tests/e2e_plays.mjs`) that replays the actual typed messages through masking + the LIVE assistant — 12/12. **Ran all samples live in the doctor window** (single, multi-play, unknown doctor/hospital, both-unknown, backdating, query, off-topic) and **audited the admin console end-to-end** (all 5 logged plays + 8 issue flags reflect there). Two fixes found: **VON-09** banned-word slip ("usage-reporting" on screen) → `gamifyOut` output filter + prompt self-description reword + banned-word check in the suite; **VON-10** backdated play showing in the current Era's report → Era-aware admin report (`playsForEra`, Era selector, Dashboards/Reconcile scoped to open Era). **Added the `/test-samples` skill** ("run samples" → live replay + auto-capture problems). Masking 25/25, e2e 12/12. Next unchanged: execute Wave 0 (regenerate the stale tester file first); finish multi-play polish (conversational per-play edits) + bring the admin console under automated test. |
| 2026-07-09 PM | Big COO-driven build/test afternoon (tester findings VON-01…07). Fixed assistant tone; stopped welcome/placeholder/sign-in copy from revealing the hiding mechanism (DECISIONS #9). **Prototyped + verified prompt caching** — static rulebook cached, per-Player alias/date outside the cache (DECISIONS #30; token-cost blocker now 🟡). **Game-vocabulary substitution** — hospital→Location, doctor→Player, typo-tolerant (DECISIONS #20 first slice). **Off-assignment Location flag** — deterministic on-device check flags a play at a hospital not assigned to the doctor; play still posts; admin disposes (DECISIONS #32). Admin console: **disposition buttons**, **made responsive**, **full roster CRUD**, then **merged MDs + assignments into one Doctors table** (name·alias·hospitals) with a Hospitals master list + inline "+ New hospital" (VON-07). Eval-suite Phase 1 grew to **22/22** (name-hiding + gamify + off-assignment) and auto-runs on prototype masking edits. Next unchanged: execute Wave 0. |
