Files
ecw-charge-validation/docs/superpowers/specs/2026-10-14-start-bot-from-web-page-design.md
T
ManikandanandClaude Opus 5.5 ba15a574ab Initial import: eCW Charge Validation bot and results Portal
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>
2026-10-23 18:38:17 +05:30

7.8 KiB
Raw Blame History

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_<ts>.out, which catches a crash before the bot opens its own log; the bot's Logs\run_<ts>.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 <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: <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: 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] <title>: <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.