Files
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

115 lines
3.9 KiB
Python

"""
Is the bot allowed to run right now, and if not, when?
python tools\\check_guard.py
Exists because "SystemExit: 3" kept coming up and the only way to find out why
was to start a whole run — which then refused, told you nothing about the clock,
and left you guessing whether to try again in one minute or twenty. This reads
`Logs\\attempts.json` and prints the answer. It starts no browser, touches eCW
not at all, and **records no attempt**, so running it is free.
Exit code mirrors the bot's: 0 = a run would proceed, 3 = it would be refused.
"""
import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parent.parent / "src"))
# Windows hands these scripts a cp1252 console, which cannot encode the arrows
# below. See runlog._force_utf8_console.
for _s in (_sys.stdout, _sys.stderr):
try:
_s.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from datetime import datetime, timezone, timedelta
from ecw_bot import settings as cfg
from ecw_bot.core import guard
def _local(dt):
return dt.astimezone().strftime("%H:%M:%S") if dt else "-"
def _ago(dt, now):
if not dt:
return ""
secs = (now - dt).total_seconds()
return f"{secs/60:.0f} min ago" if secs >= 90 else f"{secs:.0f}s ago"
def main() -> int:
state = guard._read()
now = datetime.now(timezone.utc)
print(f"Now : {_local(now)}")
print(f"State file : {guard.STATE_PATH}")
print()
last = guard._parse(state.get("last_attempt"))
success = guard._parse(state.get("last_success"))
locked = guard._parse(state.get("locked_until"))
window = now - timedelta(hours=1)
recent = sorted(d for d in (guard._parse(x) for x in state.get("recent", []))
if d and d > window)
print(f"Last attempt : {_local(last)} {_ago(last, now)}")
print(f"Last success : {_local(success)} {_ago(success, now)}")
print()
# Rule 1 — the 5 minute cooldown between attempts. Waited out, not refused.
print(f"Cooldown ({cfg.LOGIN_COOLDOWN_SECONDS}s between attempts)")
if last:
left = cfg.LOGIN_COOLDOWN_SECONDS - (now - last).total_seconds()
if left > 0:
waits = left <= cfg.COOLDOWN_MAX_WAIT_SECONDS
print(f" {left:.0f}s left"
+ (" → the bot WAITS this out and then runs"
if waits else " → too long to wait, it would exit 3"))
else:
print(" clear")
else:
print(" clear (no attempt recorded yet)")
print()
# Rule 2 — the rolling hour. This one refuses; it is the real protection.
print(f"Hourly limit ({len(recent)} of {cfg.LOGIN_MAX_PER_HOUR} in the "
f"last hour)")
for d in recent:
print(f" {_local(d)} leaves the window at "
f"{_local(d + timedelta(hours=1))}")
if len(recent) >= cfg.LOGIN_MAX_PER_HOUR:
clears = min(recent) + timedelta(hours=1)
print(f" FULL. Clears at {_local(clears)} — "
f"{(clears - now).total_seconds()/60:.0f} min from now.")
else:
print(f" room for {cfg.LOGIN_MAX_PER_HOUR - len(recent)} more")
print()
# Rule 3 — eCW showed its restriction page.
print("Restriction lockout")
if locked and now < locked:
print(f" eCW refused us at "
f"{state.get('restricted_at', '?')[:19]} UTC. "
f"Locked until {_local(locked)}.")
print(" Usual cause is the browser losing the proxy, not the account "
"— check tools\\check_net.py first.")
else:
print(" none")
print()
refusal = guard.check()
if refusal:
print(f"VERDICT: a run would be REFUSED (exit 3)")
print(f" {refusal}")
return 3
print("VERDICT: clear — a run would proceed")
return 0
if __name__ == "__main__":
_sys.exit(main())