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>
9.0 KiB
Run tracking and Portal Sign-in — design
Date: 2026-10-22. Agreed with the operator in chat, question by question
(Q1–Q31, all "yes" to the recommended answers; Q3 changed to "who" = the
Portal User). Terms: GLOSSARY.md. Decision record:
docs/adr/0001-run-events-in-files.md. Jira: EEWA-31 (Stage 1),
EEWA-32 (Stage 2), EEWA-33 (Stage 3), each blocking the next.
Goal
See every Run on the Portal: who started it, how long the eCW login took and where the time went, every Run Error, Claim Error and Warning with its time, and which claims a Run never reached. Only signed-in Portal Users see the Portal; every Run records who started (and stopped) it.
Decisions
| # | Decision |
|---|---|
| Q1 | Three kinds: Run Error (the Run stopped), Claim Error (one claim not checked), Warning (nothing stopped). Fail stays a Charge Summary category only. |
| Q2/Q28 | Per eCW Login Attempt: start, end, duration, 2FA mail wait, result; steps timed: page open, password typed, captcha, 2FA mail requested (the Log In click), mail received, approval clicked, home page. |
| Q3/Q8 | "Who" = the Portal User who signed in; Started By / Stopped By. |
| Q4 | Events go to Logs\events_<run>.jsonl (ADR 0001). |
| Q5/Q25 | Old run logs are not imported. Nothing is deleted. |
| Q6/Q18/Q19/Q22 | A Runs page on the Portal: list + detail, detail refreshes every 5 s while its Run is going. |
| Q9 | The Portal stays on 127.0.0.1. |
| Q10–Q17, Q20, Q21 | Sign-in (Stage 2, below). |
| Q14 | Terminal Runs are allowed; Started By = the Windows user. |
| Q15/Q23 | Stage 3: Run Error banner; started_by column in CV_ChargeSummary; the Excel sheet unchanged. |
| Q24 | Disk check at Run start: system drive under 2 GB = Warning, under 500 MB = Run Error, exit 2, before login. |
| Q26 | Claim Error = search raised / form not read / never finished loading; Charge Summary still says Review. A claim not found in eCW = Warning. |
| Q27 | Not Reached = queued claims a Run Error (or Stop) came before. |
Stage 1 — Run events and the Runs page (EEWA-31)
core/run_events.py (new)
start(log_path)— the events file is the log's name withrun_→events_and.log→.jsonl(run_20261022_141530.log→events_20261022_141530.jsonl); the Run's id is20261022_141530. Writesrun_start.emit(kind, **fields)— one JSON line,{"t": local ISO time to the millisecond, "kind": kind, ...}, flushed at once. Never raises: an event that cannot be written is dropped, the Run carries on.- Helpers:
warning(code, message, **f),run_error(step, message, exit_code, exc=None)(withtraceback.format_exc()when an exception is given),claim_error(claim, step, message),login_step(step, **f),login_attempt_start()/login_attempt_end(result, message="")(the module numbers attempts). - No OTP code, password or eCW username is ever put in an event.
Event kinds
| kind | fields |
|---|---|
run_start |
source (portal / terminal), started_by, mode (VERIFY / LIVE-WRITE), claim_limit, test_sheet (file name only), pid, log |
disk |
drive, free_mb |
login_attempt_start |
attempt |
login_step |
attempt, step: page_open, password, captcha_shown, captcha_solved, captcha_failed, twofa_requested, mail_received, approval_clicked, home |
login_attempt_end |
attempt, result: ok, twofa_timeout, captcha_failed, restricted, failed; message |
queue |
claims (list) |
claim_start |
n, total, claim |
claim_result |
n, claim, category (Pass / Fail / Review / Not found) |
claim_error |
claim, step (search / form / load), message |
warning |
code (not_found, fallback, dialog, cancel_retry, form_not_closed, disk_low, preflight), message, optional claim |
screenshot |
path (a failure shot), optional claim |
run_error |
step (config, disk, guard, login, restricted, twofa_timeout, preflight, claims, unexpected), exit_code, message, traceback |
run_end |
exit_code |
stopped |
stopped_by (written by the Portal after Stop; Stage 2 fills the name) |
started_by: env ECW_STARTED_BY (the Portal sets it, Stage 2), else the
Windows user. source: as bot_run.json has it (ECW_RUN_SOURCE).
Where the bot emits
flows/export_medcor_ready.py:main()starts the events afterrunlog.start()and writesrun_endin itsfinally;_mainwrites arun_errorat every non-zero return and in everyexcept, with the traceback; the disk check sits before login next to the CDP check;_login_with_retrywraps eachlogin()in attempt start / end;_process_claimswritesqueue,claim_start,claim_result,claim_error(search raised,_error,_not_evaluated) and the not-found Warning.auth/login.py/captcha.py/twofactor.py/mailbox.py: the login steps.core/fallback.pyreport(),core/page_utils.pyauto_dialog(the event only — its behaviour is unchanged, see Context.md §10) and the failureshot(),pages/claim_form.pyCancel retries and form-not-closed,pages/preflight.pyFAIL: Warnings / screenshots.web/bot_runner.pystop(): appendsstoppedto the Run's events file.
Runs page
web/runs.py(new): readsLogs\events_*.jsonlinto Run summaries and a Run's detail. A Run with norun_endis Running whenbot_run.jsonnames it and its pid is alive, Stopped when astoppedevent is there, else Ended without a record (killed). Result: Done (exit 0), Run Error (exit and step), Stopped, Running.GET /runs— newest first: start, Started By, Run Source, mode, duration, result, eCW Login Attempts + total login time, Pass / Fail / Review, Claim Errors, Warnings. Filters: from / to date, result.GET /runs/{run_id}— the login attempts table (each step's time, 2FA wait = mail received − 2FA requested), Run Errors (traceback folded), Claim Errors, Warnings, Not Reached claims, the timeline, screenshot links; refreshes every 5 s while Running.GET /runs/{run_id}/shot/{n}— the Run's n-th screenshot event, served only from insideScreenshots\and only as.png: a saved page's HTML served from the Portal's origin would run eCW's scripts there, so the detail page names where the DOM file is instead of linking it.- A "Runs" link on every page's header.
Disk check
Settings DISK_WARN_MB=2048, DISK_STOP_MB=500, on the system drive.
Stage 2 — Sign-in (EEWA-32)
CV_PortalUser(username, display name, role, PBKDF2-SHA256 hash + salt- iterations, must_change, disabled, created / changed) and
CV_PortalSignIn(time, username, event: sign_in / sign_out / wrong_password / locked / timeout) inCharge_Validation.
- iterations, must_change, disabled, created / changed) and
- Roles Admin / Operator / Viewer. Start / Stop: Admin, Operator. Users page and Sign-in history: Admin.
tools\add_portal_user.py(password withgetpass). Passwords ≥ 10 characters; change own; Admin reset → must change at next Sign-in; disable, never delete.- 5 wrong passwords → 15 minutes locked. Session cookie (HttpOnly,
SameSite=Strict), 30 minutes idle. Every page and
/apibehind it; 127.0.0.1 + Origin / token checks stay. - The Portal passes the signed-in user to the child as
ECW_STARTED_BY;stop()writesstopped_by.bot_run.jsonkeepsstarted_bytoo, so the Dashboard's run card says "started by …". - As built (
web/users.py,web/app.py): sessions in the Portal's memory (a restart signs everyone out); disabling a user, resetting their password or changing their role ends / updates their open session at once; an unknown username reads and costs the same as a wrong password; the last active Admin cannot be demoted or disabled, nobody disables themselves; every Admin change is aCV_PortalSignInrow withby_user; every form POST carries the page token and is checked for Origin;nextafter Sign-in never leaves the site.
Stage 3 — Alerts and started_by (EEWA-33)
- Dashboard banner when the last Run ended in a Run Error.
CV_ChargeSummary.started_by(nullable column, written from the Run's Started By).- As built: the banner also shows for a Run that ended without a record
(killed or crashed — no
run_end), never for Done, Stopped or Running; it names when, Started By, the step, exit code and message, how many claims were Not Reached, and links the Run.create_tables.sqladds the column to an existing table (IF COL_LENGTH ... IS NULL ALTER TABLE). The bot checks the column before each insert (results_db.has_column) and saves the rows without it while it is missing — a table not yet updated never costs a Run its rows. The Portal selects it, shows it as "Started By" in a claim's side panel, and a table without it reads "Results table needs an update — run create_tables.sql" (SQLSTATE 42S22).
Out of scope
Network access to the Portal, email / Teams alerts, importing old logs, deleting old events, editing settings from the Portal.