Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one commit with no earlier history: the tracked files were de-identified first (synthetic claim ids and patient data only; see Context.md section 7). Credentials, run logs, screenshots, claim reports and client workbooks are gitignored and not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.8 KiB
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— startspython run.py(the project's ownrun.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 toLogs\web_run_<ts>.out, which catches a crash before the bot opens its own log; the bot'sLogs\run_<ts>.log(named in the lock file) is the status source. Refuses (raisesAlreadyRunning) when the lock file names a live pid.stop() -> dict—taskkill /PID <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()(afterrunlog.start(), solog_pathis known) and removed whenmain()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: <id> ---→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: ReviewThe 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): withQUIET_BOXES=1andneeds_person=False, no box — one log line instead:[BOX] <title>: <first line of msg>.ok_cancel/confirmare unchanged (every remaining caller is a person-needed step: CAPTCHA, 2FA manual, LIVE-WRITE confirm — the last is never reached in VERIFY). The security-imageinfo_boxinauth/login.pypassesneeds_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/statusevery 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:
Origin(or, when absent,Referer) ishttp://127.0.0.1:8000orhttp://localhost:8000— else 403.- Header
X-Run-Tokenequals a token made at server start (secrets.token_urlsafe(32)) and embedded in the page — else 403. Compared withsecrets.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_screencloses it. - Web server restarted or page closed: the bot keeps running; on reload
the page finds it through the lock file.
exit_codeis 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 ofrun.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=Truestill shown.- Flow test —
[RESULT]line per claim;main()lock file written, refused on a live one, removed at the end.