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>
This commit is contained in:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,175 @@
|
||||
"""
|
||||
Operator prompts.
|
||||
|
||||
Everything here can time out. An earlier run wedged for forty minutes on a
|
||||
plain MessageBoxW: the mailbox poll found nothing, the code fell back to asking
|
||||
a human, and nobody was at the desk — so the process sat holding a browser open
|
||||
with no log output while eCW went on rate-limiting the account. A prompt that
|
||||
can block forever is a prompt that will, so `MessageBoxTimeoutW` is used
|
||||
instead and callers are handed an explicit TIMEOUT.
|
||||
|
||||
`MessageBoxTimeoutW` is undocumented but has shipped in user32 since XP. If it
|
||||
is ever missing, the plain call is used and the timeout silently does not
|
||||
apply — better a hang than a crash at the moment a human is being asked for
|
||||
help.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
from . import watchdog
|
||||
|
||||
# Return values
|
||||
OK = "ok"
|
||||
CANCEL = "cancel"
|
||||
TIMEOUT = "timeout"
|
||||
|
||||
_MB_OK = 0x00000000
|
||||
_MB_OKCANCEL = 0x00000001
|
||||
_MB_ICONINFO = 0x00000040
|
||||
_MB_ICONWARN = 0x00000030
|
||||
_MB_TOPMOST = 0x00040000 # otherwise it can open behind the browser window
|
||||
# Second button (Cancel, on an OK/Cancel box) is the default one, so Enter or
|
||||
# Space picks Cancel instead of OK. Without it, Enter on the per-claim save
|
||||
# box meant OK — save the claim to live eCW (review item P1).
|
||||
_MB_DEFBUTTON2 = 0x00000100
|
||||
|
||||
_IDOK = 1
|
||||
_IDTIMEOUT = 32000
|
||||
|
||||
|
||||
def _raw_call(text: str, caption: str, flags: int, timeout_s: int) -> int:
|
||||
# A box waits on a person, not a frozen page: the claim watchdog's
|
||||
# clock starts again when it opens and when it closes.
|
||||
watchdog.beat("message box")
|
||||
try:
|
||||
return _call_box(text, caption, flags, timeout_s)
|
||||
finally:
|
||||
watchdog.beat("message box closed")
|
||||
|
||||
|
||||
def _call_box(text: str, caption: str, flags: int, timeout_s: int) -> int:
|
||||
user32 = ctypes.windll.user32
|
||||
try:
|
||||
fn = user32.MessageBoxTimeoutW
|
||||
fn.argtypes = [wintypes.HWND, wintypes.LPCWSTR, wintypes.LPCWSTR,
|
||||
wintypes.UINT, wintypes.WORD, wintypes.DWORD]
|
||||
fn.restype = ctypes.c_int
|
||||
return fn(None, text, caption, flags, 0, int(timeout_s * 1000))
|
||||
except AttributeError:
|
||||
return user32.MessageBoxW(None, text, caption, flags)
|
||||
|
||||
|
||||
def info(title: str, msg: str, timeout_s: int,
|
||||
needs_person: bool = False) -> str:
|
||||
"""Informational box. Returns OK or TIMEOUT.
|
||||
|
||||
With QUIET_BOXES on (a run the results page started), a box that does
|
||||
not need a person is one log line instead, and answers OK."""
|
||||
if cfg.QUIET_BOXES and not needs_person:
|
||||
first = (msg or "").strip().splitlines()[0] if (msg or "").strip() \
|
||||
else ""
|
||||
print(f"[BOX] {titled(title)}: {first}")
|
||||
return OK
|
||||
try:
|
||||
rc = _raw_call(msg, titled(title), _MB_OK | _MB_ICONINFO | _MB_TOPMOST, timeout_s)
|
||||
except Exception:
|
||||
return TIMEOUT
|
||||
return TIMEOUT if rc == _IDTIMEOUT else OK
|
||||
|
||||
|
||||
def confirm(title: str, msg: str, timeout_s: int,
|
||||
default_cancel: bool = False) -> str:
|
||||
"""
|
||||
OK/Cancel box. Returns OK, CANCEL or TIMEOUT.
|
||||
|
||||
`default_cancel=True` makes Cancel the default button, so Enter/Space
|
||||
answers Cancel. For boxes where OK writes to live eCW: there, a key
|
||||
pressed out of habit must not count as a decision. Off by default so the
|
||||
captcha and security-image prompts, where OK only lets a read continue,
|
||||
behave as they always have.
|
||||
"""
|
||||
footer = (f"\n\n(If there's no reply, this will cancel automatically "
|
||||
f"after {timeout_s}s.)")
|
||||
flags = _MB_OKCANCEL | _MB_ICONWARN | _MB_TOPMOST
|
||||
if default_cancel:
|
||||
flags |= _MB_DEFBUTTON2
|
||||
try:
|
||||
rc = _raw_call(msg + footer, titled(title), flags, timeout_s)
|
||||
except Exception:
|
||||
# No user32 (not Windows): nothing can be asked, so do not pretend a
|
||||
# human agreed.
|
||||
return TIMEOUT
|
||||
if rc == _IDTIMEOUT:
|
||||
return TIMEOUT
|
||||
return OK if rc == _IDOK else CANCEL
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Convenience wrappers that apply the configured timeout and log the timeout
|
||||
# =============================================================================
|
||||
# These were private helpers in auth.py. They are here because every caller
|
||||
# wants the same two things — the configured timeout, and a log line when a
|
||||
# prompt goes unanswered — and duplicating that per module is how one of them
|
||||
# ends up silently treating a timeout as consent.
|
||||
|
||||
from .. import settings as cfg # noqa: E402 (kept below the ctypes core)
|
||||
|
||||
TEST_PREFIX = "[TEST] "
|
||||
|
||||
|
||||
def titled(title: str) -> str:
|
||||
"""
|
||||
`title` as the box shows it: with `[TEST] ` in front when the claim ids
|
||||
come from a TEST_CLAIM_IDS_FILE sheet (review item D9, 2026-10-05).
|
||||
|
||||
A test run drives the same live eCW as a real one, so a screen with a
|
||||
box on it looks exactly like production to anyone watching — the client
|
||||
in a demo included. The [MODE] banner says "TEST MODE" in the log only;
|
||||
this puts it on every box. Applied in the raw `info()` / `confirm()`,
|
||||
the two functions every box reaches `_raw_call` through, not only in
|
||||
the `info_box` / `ok_cancel` wrappers: three callers use the raw ones
|
||||
directly (the 2FA manual box in auth/mailbox.py, which a test run does
|
||||
reach at login; "Run the Lookup?" in pages/claim_results.py; the export
|
||||
report box in pages/claim_report.py). Done here rather than at each call
|
||||
site so a box added later cannot forget it. The wrappers apply it too,
|
||||
so their "timed out" log line names the title the box showed; that is
|
||||
safe because the prefix is never doubled. Read at call time (this
|
||||
module imports settings below the ctypes core, and settings can change
|
||||
under a test).
|
||||
"""
|
||||
if cfg.TEST_CLAIM_IDS_FILE and not title.startswith(TEST_PREFIX):
|
||||
return TEST_PREFIX + title
|
||||
return title
|
||||
|
||||
|
||||
def info_box(title: str, msg: str, needs_person: bool = False) -> None:
|
||||
"""Informational box on the configured timeout. Never blocks the run.
|
||||
`needs_person=True` for a box a person must act on (it is never
|
||||
quieted by QUIET_BOXES)."""
|
||||
title = titled(title)
|
||||
# The keyword is passed only when set, so a test that replaces info()
|
||||
# with a three-argument stub keeps working.
|
||||
extra = {"needs_person": True} if needs_person else {}
|
||||
if info(title, msg, cfg.MSGBOX_TIMEOUT_SECONDS, **extra) == TIMEOUT:
|
||||
print(f"[UI] '{title}' timed out with nobody at the desk — continuing")
|
||||
|
||||
|
||||
def ok_cancel(title: str, msg: str, default_cancel: bool = False) -> bool:
|
||||
"""
|
||||
True only on an explicit OK. A timeout counts as 'no'.
|
||||
|
||||
Note the asymmetry with `claim_results.run_lookup`, which treats a timeout
|
||||
as "carry on": there, proceeding only reads. Here the prompts guard captcha
|
||||
and security-image steps where proceeding without a human is meaningless,
|
||||
so silence must not be read as agreement.
|
||||
|
||||
`default_cancel=True` (see `confirm`) for any box whose OK saves to eCW:
|
||||
the per-claim save box and the LIVE-WRITE start box.
|
||||
"""
|
||||
title = titled(title)
|
||||
result = confirm(title, msg, cfg.MSGBOX_TIMEOUT_SECONDS,
|
||||
default_cancel=default_cancel)
|
||||
if result == TIMEOUT:
|
||||
print(f"[UI] '{title}' timed out with nobody at the desk — treating as cancel")
|
||||
return result == OK
|
||||
Reference in new issue
Block a user