fix(login,download): login direct pe goapp, descarcare doar a documentului nou

- BT a mutat login-ul (09.2026): go.bancatransilvania.ro face 302 spre
  marketing; formularul e pe goapp.bancatransilvania.ro, acelasi tab, fara popup.
- Daca formularul nu apare: eroare explicita cu URL/titlu + screenshot/HTML.
- scripts/check_login_page.py: verifica pagina de login fara credentiale.
- Download: asteapta documentul nou din "Recente"; export gol -> [SKIP], nu timeout.
- Screenshot la eroare facut inainte de oprirea Playwright.
- opencode.json: agentii opencode nu pot citi .env.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DxPhZABSUf1RJXRWtwecqC
This commit is contained in:
2026-09-24 10:33:16 +03:00
parent ed9d3fc6d4
commit 3ac8ba68de
5 changed files with 396 additions and 27 deletions

182
scripts/check_login_page.py Normal file
View File

@@ -0,0 +1,182 @@
"""
Verifica daca pagina de login BT e inca la fel (campuri user/parola/submit).
Fara credentiale, fara .env: doar deschide pagina de login ca scraper-ul,
o observa si iese cu cod 0 daca formularul e intact, altfel 1 + motivul.
Nu completeaza campuri si nu submite nimic.
Chromium e lansat identic cu btgo_scraper.py (noul mod headless
--headless=new + UA de Chrome normal), altfel WAF-ul Akamai al BT raspunde
cu Access Denied. Nu folosim networkidle (nu se atinge niciodata pe site-ul
BT) -> domcontentloaded + asteptari pe selector.
Nota: lansarea browserului e duplicata din btgo_scraper.run() pentru ca
acolo e inline si btgo_scraper importa config (care citeste .env). Daca
lansarea va fi extrasa intr-un helper fara config, scriptul il poate importa.
"""
import sys
from datetime import datetime
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
# Aceleasi intrari ca in btgo_scraper.login() (ordinea conteaza).
LOGIN_ENTRY_URLS = [
"https://goapp.bancatransilvania.ro/",
"https://go.bancatransilvania.ro/",
]
USER_SELECTORS = ["input#user", "input[placeholder='ID logare']"]
PASSWORD_SELECTORS = ["input#password", "input[type='password']"]
SUBMIT_SELECTORS = [
"input[type='submit']",
"button[type='submit']",
"input[value*='Autentific']",
]
DATA_DIR = Path("data")
def launch(p):
"""Lanseaza Chromium exact ca btgo_scraper.run() (WAF-safe)."""
browser = p.chromium.launch(
headless=False,
slow_mo=0,
args=[
"--window-size=1366,768",
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-extensions",
"--disable-background-networking",
"--disable-renderer-backgrounding",
"--disable-features=Translate",
"--headless=new",
],
)
context = browser.new_context(
viewport={"width": 1366, "height": 768},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/130.0.0.0 Safari/537.36"
),
)
return browser, context
def first_visible(page, selectors, timeout=1000):
"""Primul selector din lista care are un element vizibil, sau None."""
for sel in selectors:
try:
el = page.locator(sel).first
if el.count() and el.is_visible(timeout=timeout):
return sel
except Exception:
continue
return None
def dump_diag(page):
"""Salveaza HTML + screenshot pentru diagnostic. Returneaza caile scrise."""
written = []
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
try:
DATA_DIR.mkdir(exist_ok=True)
except Exception:
return written
try:
html_path = DATA_DIR / f"check_login_page_{ts}.html"
html_path.write_text(page.content(), encoding="utf-8")
written.append(html_path)
except Exception:
pass
try:
shot_path = DATA_DIR / f"check_login_page_{ts}.png"
page.screenshot(path=str(shot_path))
written.append(shot_path)
except Exception:
pass
return written
def check_page(page, url):
"""Navigheaza la `url` si inspecteaza formularul. Returneaza un dict."""
info = {
"ok": False,
"url": url,
"final_url": None,
"title": None,
"user": None,
"password": None,
"submit": None,
"error": None,
}
print(f"[check] {url}")
try:
page.goto(url, wait_until="domcontentloaded", timeout=30000)
except Exception as e:
info["error"] = f"goto a esuat: {e}"
print(f" [WARN] {info['error']}")
return info
try:
page.wait_for_selector(
", ".join(USER_SELECTORS), timeout=60000, state="visible"
)
except PlaywrightTimeout:
info["final_url"] = page.url
info["title"] = page.title()
info["error"] = "formularul de login (input#user) nu a aparut"
print(f" [WARN] {info['error']} (URL final: {info['final_url']})")
return info
info["final_url"] = page.url
info["title"] = page.title()
info["user"] = first_visible(page, USER_SELECTORS)
info["password"] = first_visible(page, PASSWORD_SELECTORS)
info["submit"] = first_visible(page, SUBMIT_SELECTORS)
info["ok"] = bool(info["user"] and info["password"] and info["submit"])
print(f" URL final: {info['final_url']}")
print(f" titlu: {info['title']!r}")
print(f" user: {info['user']!r}")
print(f" parola: {info['password']!r}")
print(f" submit: {info['submit']!r}")
return info
def main():
with sync_playwright() as p:
browser, context = launch(p)
try:
page = context.new_page()
last = None
for url in LOGIN_ENTRY_URLS:
info = check_page(page, url)
if info["ok"]:
print("[OK] Pagina de login e intacta "
"(campuri user/parola/submit gasite).")
return 0
last = info
written = dump_diag(page)
missing = [k for k in ("user", "password", "submit")
if not last.get(k)]
print("[FAIL] Pagina de login BT s-a schimbat.")
print(f" URL final: {last.get('final_url')}, "
f"titlu: {last.get('title')!r}")
if last.get("error"):
print(f" Motiv: {last['error']}")
if missing:
print(f" Campuri lipsa: {', '.join(missing)}")
for path in written:
print(f" Dovezi: {path}")
return 1
finally:
browser.close()
if __name__ == "__main__":
sys.exit(main())