fix(positions): sync latest scraper fixes from main repository
All checks were successful
Build and Push Docker Image / build (push) Successful in 36s

This commit is contained in:
2026-04-24 21:34:38 +00:00
parent 429a2832fd
commit a05ba3b8a8
4 changed files with 385 additions and 97 deletions

View File

@@ -1006,8 +1006,11 @@ async def login_to_schwab(username: str, password: str) -> Optional[List[Dict[st
mfa_code = None
try:
logger.info("Waiting 5 seconds for email code to arrive before checking webhook...")
await asyncio.sleep(5)
async with aiohttp.ClientSession() as session:
for idx in range(60): # 2 minutes, every 2 seconds
for attempt in range(2):
print(f"Checking webhook for code (attempt {attempt + 1}/2)...")
try:
async with session.get("https://n8n.ext.ben.io/webhook/schwab-token") as resp:
if resp.status == 200:
@@ -1016,34 +1019,47 @@ async def login_to_schwab(username: str, password: str) -> Optional[List[Dict[st
# Parse based on expected n8n output formats
code = None
if isinstance(data, dict):
code = data.get("code") or data.get("token") or data.get("body", {}).get("code")
code = data.get("code") or data.get("token") or data.get("login_code") or data.get("body", {}).get("code")
elif isinstance(data, list) and len(data) > 0:
code = data[-1].get("code") or data[-1].get("token")
code = data[-1].get("code") or data[-1].get("token") or data[-1].get("login_code")
if code:
mfa_code = code
logger.info(f"Got MFA code from webhook: {mfa_code}")
break
else:
logger.warning("Webhook returned data but no code found inside.")
else:
logger.warning(f"Webhook returned status code {resp.status}")
except Exception as e:
logger.debug(f"Webhook poll error: {e}")
if idx % 10 == 0:
print(f"Still waiting for webhook code... ({idx*2}s/120s)")
await asyncio.sleep(2)
if not mfa_code and attempt == 0:
logger.info("Token not found, waiting 10 seconds before 1 retry...")
await asyncio.sleep(10)
except Exception as loop_e:
logger.error(f"Error during webhook polling loop: {loop_e}")
logger.error(f"Error during webhook checking: {loop_e}")
if mfa_code:
logger.info("Entering MFA code into form...")
try:
target = page
iframe_element = await page.query_selector('#lmsIframe')
if iframe_element:
target = await iframe_element.content_frame() or page
# When on the sws-gateway-nr OTP page, the form is rendered
# directly on the page — there is no #lmsIframe wrapper here.
# Only look for the iframe when on the client.schwab.com login page.
current_page_url = page.url
if 'sws-gateway-nr' in current_page_url or 'otp' in current_page_url:
logger.debug(f"OTP page detected ({current_page_url}), querying form directly on page")
target = page
else:
target = page
iframe_element = await page.query_selector('#lmsIframe')
if iframe_element:
target = await iframe_element.content_frame() or page
# Commonly used ids and attributes for OTP inputs on Schwab
code_input = await target.query_selector('input[type="text"], input[type="tel"], input[name*="code" i], input[id*="code" i], input[autocomplete*="one-time-code" i]')
if code_input:
await code_input.fill(str(mfa_code))
logger.info(f"Filled OTP field with code: {mfa_code}")
# Sometimes the submit button specifically says 'Trust device' or similar
submit_btn = await target.query_selector('button[type="submit"], button:has-text("Continue"), button:has-text("Verify"), button:has-text("Submit"), button:has-text("Log in"), button[id*="submit"], button[id*="continue"]')
@@ -1052,7 +1068,10 @@ async def login_to_schwab(username: str, password: str) -> Optional[List[Dict[st
print("Submitted MFA code successfully.")
await page.wait_for_timeout(5000)
else:
logger.warning("Submit button not found after filling OTP — waiting anyway")
await page.wait_for_timeout(5000)
else:
logger.error("OTP input field not found on page")
except Exception as e:
logger.error(f"Failed to enter MFA code: {e}")