# Start the bot from the results web page — design Date: 2026-10-14. Approved section by section by the operator in chat (approach A, sections 1–3). ## Goal The operator starts a Charge Validation run from the local results page (`run_web.bat` → http://127.0.0.1:8000) instead of the terminal. While it runs, the page shows live status; it can stop it; when it ends, the page shows that run's results. ## Decisions (operator, 2026-10-14) | Question | Answer | |---|---| | Controls | Start + live status + Stop (no settings editing on the page) | | Save mode | **VERIFY only.** A page-started run is always `SAVE_CORRECTIONS=0`, whatever `.env` says. LIVE-WRITE stays terminal-only. | | Message boxes | **Off** for the run's information boxes (per-claim, review-before-close, "Run the Lookup?", summaries). Boxes that need a person to act in the browser — CAPTCHA, 2FA manual step, security image — **stay on**. | | Approach | A: the web server starts `run.py` as a separate child process (not a thread, not Task Scheduler). | Out of scope (YAGNI): editing settings from the page, scheduling, more than one run at a time, LIVE-WRITE from the page. ## Components ### New: `web/bot_runner.py` — process control only - `start() -> dict` — starts `python run.py` (the project's own `run.py`, `sys.executable`, cwd = project root) as a child process with no console window of its own (`CREATE_NO_WINDOW`); the Chrome window still shows. The child's stdout/stderr go to `Logs\web_run_.out`, which catches a crash before the bot opens its own log; the bot's `Logs\run_.log` (named in the lock file) is the status source. Refuses (raises `AlreadyRunning`) when the lock file names a live pid. - `stop() -> dict` — `taskkill /PID /T /F`: the bot and its Chrome close together. Clears the lock file. Status becomes "Stopped by operator". - `status() -> dict` — `running` (bool), `pid`, `started_at`, `log_path`, `exit_code` (when this web process started it and it has ended), `stopped_by_operator`. - The command is fixed. Nothing from the request reaches the command line or the environment. **Child environment** — set on the child process, so it wins over `.env` (`settings.py` loads `.env` with `os.environ.setdefault`): ``` SAVE_CORRECTIONS=0 ECW_KEEP_OPEN=0 # no "Press Enter to close" at the end QUIET_BOXES=1 # run-flow information boxes go to the log CONFIRM_BEFORE_LOOKUP=0 ``` Everything else follows `.env` (claim file, `CLAIM_PROCESS_LIMIT`, `RESULTS_DB`, proxy, ...). ### Lock file: `Logs\bot_run.json` `{"pid", "started_at", "log_path", "source": "web" | "terminal"}`. - Written by the **bot itself** at the start of `main()` (after `runlog.start()`, so `log_path` is known) and removed when `main()` returns — so a terminal run is seen by the page too. - Before writing it, `main()` refuses to start (exit 2, before login) when the file names a pid that is alive: two runs would share one eCW login guard and one Charge Summary. - A file naming a dead pid is stale: removed and ignored (a killed run cannot remove its own file). - `bot_runner.start()` checks it too, so the page answers 409 at once. ### New: `web/run_status.py` — read the run log Pure functions over log text (no I/O except reading the file tail): - **Phase** from the last matching line: `[LOGIN]` / `[CAPTCHA]` → "Logging in"; `[OTP]` / `[2FA]` → "Waiting for the 2FA approval mail"; `[OK] Logged in` → "Logged in"; `[CLAIMS]` → "Checking claims"; process ended → "Finished" or a failure text by exit code. - **Progress** from `[CLAIMS] --- claim n/N: ---` → `n`, `N`, current claim id. - **Counts** from the new `[RESULT]` lines (below) → Pass / Fail / Review. - **Tail**: the last 30 lines. - **Exit codes** (from `flows/export_medcor_ready.main`): 0 finished; 2 config / mode gate; 3 login guard refused (its `[GUARD]` line shown); 5 claims phase failed; 6 2FA timed out; other → "ended with code N". ### Changed: `flows/export_medcor_ready.py` - After each claim's outcome is known in `_process_claims`, one machine-readable line (terminal runs print it too): ``` [RESULT] claim 3/7 9126000: Review ``` The word is the Category the Charge Summary gets for that claim; a claim that gets no Charge Summary row (not found in the grid) prints `Not found`. - `main()` writes / removes the lock file as above. ### Changed: `core/msgbox.py` - New setting `QUIET_BOXES` (default off; `settings.py`). - `info_box(title, msg, needs_person=False)`: with `QUIET_BOXES=1` and `needs_person=False`, no box — one log line instead: `[BOX] : <first line of msg>`. - `ok_cancel` / `confirm` are unchanged (every remaining caller is a person-needed step: CAPTCHA, 2FA manual, LIVE-WRITE confirm — the last is never reached in VERIFY). The security-image `info_box` in `auth/login.py` passes `needs_person=True`. ### Changed: `web/app.py` + `web/templates/index.html` Endpoints: | Method | Path | Answer | |---|---|---| | POST | `/api/bot/start` | 200 `{status}`; 409 already running; 403 bad Origin/token | | POST | `/api/bot/stop` | 200 `{status}`; 409 nothing running; 403 bad Origin/token | | GET | `/api/bot/status` | `{running, phase, progress, counts, tail, started_at, exit_code, message}` | Page: a "Bot run" card above the results. - Idle: **[Start run]** and "VERIFY mode — nothing is saved to eCW". - Running (polls `/api/bot/status` every 2 s): phase, progress bar "3 / 7", current claim, Pass/Fail/Review chips, collapsible "Live log" (last 30 lines), **[Stop]** (with a confirm dialog). - Ended: "Finished in 4m 12s — exit 0" (or the failure text); the results table reloads on the newest run. ## Security The page was read-only; Start/Stop are its first actions. The existing TrustedHost check stops DNS rebinding but not a cross-site POST from another tab, which could burn eCW login attempts. So every POST needs both: 1. `Origin` (or, when absent, `Referer`) is `http://127.0.0.1:8000` or `http://localhost:8000` — else 403. 2. Header `X-Run-Token` equals a token made at server start (`secrets.token_urlsafe(32)`) and embedded in the page — else 403. Compared with `secrets.compare_digest`. No password on the page; `.env` credentials are read only by the bot. Responses keep `no-store`. The page stays bound to 127.0.0.1. The live log tail can carry PHI, as the results table already does — local only. ## Error handling - Already running (page or terminal) → 409 "A run is already going (started HH:MM)". - Guard refused (exit 3) → the `[GUARD]` line shown ("next attempt allowed at ..."). - Stop mid-claim: the claim form may stay open in eCW. VERIFY saves nothing; the next run's `clear_screen` closes it. - Web server restarted or page closed: the bot keeps running; on reload the page finds it through the lock file. `exit_code` is unknown then (the run is shown "Ended" when its pid is gone). - Log file not there yet → phase "Starting". ## Testing (standalone "N/M passed" scripts) - `tests/test_bot_runner.py` — a fake child script in place of `run.py` (injectable command): start → running → stop → stopped; second start → `AlreadyRunning`; stale lock (dead pid) cleared; the child sees the forced env (`SAVE_CORRECTIONS=0`, `ECW_KEEP_OPEN=0`, `QUIET_BOXES=1`, `CONFIRM_BEFORE_LOOKUP=0`) even when the parent env says otherwise. - `tests/test_run_status.py` — sample log text → phase, progress, counts, tail; each exit code's text. - `tests/test_web_app.py` (added cases) — POST without/with wrong Origin or token → 403; right Origin + token → 200 (runner stubbed); status JSON shape; the token is in the page. - `tests/test_msgbox.py` (added cases) — `QUIET_BOXES=1`: info box logged, not shown; `needs_person=True` still shown. - Flow test — `[RESULT]` line per claim; `main()` lock file written, refused on a live one, removed at the end.