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

View File

@@ -428,7 +428,10 @@ class BTGoScraper:
logging.info("Start BTGO Scraper")
logging.info("=" * 60)
with sync_playwright() as p:
# Nu `with`: iesirea din el opreste Playwright inainte de except,
# iar screenshot-ul din _handle_error pica ("Event loop is closed").
p = sync_playwright().start()
try:
# Viewport 1366x768: BT afiseaza un splash "redirect to store"
# sub ~1100px latime; 1366 e sigur peste prag. Fata de 1920x1080
# randeaza ~50% mai putini pixeli -> CPU mult mai mic la randarea
@@ -515,6 +518,11 @@ class BTGoScraper:
logging.info("✓ Scraper finalizat cu succes!")
logging.info("=" * 60)
return True
except Exception as e:
self._handle_error(e)
return False
finally:
p.stop()
except Exception as e:
self._handle_error(e)
@@ -523,13 +531,13 @@ class BTGoScraper:
def login(self):
"""Autentificare cu username si password"""
self._update_progress("Deschid pagina de login...")
logging.info("Navigare catre https://go.bancatransilvania.ro/")
# 'networkidle' nu se atinge niciodata: pagina face polling continuu pe
# Google Analytics / LinkedIn / Facebook pixels. Folosim 'domcontentloaded'
# si asteptam apoi explicit link-ul Login.
self.page.goto('https://go.bancatransilvania.ro/', wait_until='domcontentloaded', timeout=30000)
self.page.wait_for_selector("a:has-text('Login')", timeout=15000, state="visible")
logging.info("Pagina incarcata")
# Din 09.2026 go.bancatransilvania.ro redirecteaza (302) spre pagina de
# marketing, fara link Login. Formularul e servit direct de goapp
# (/ -> /app/auth/login), in acelasi tab, fara popup.
# 'networkidle' nu se atinge niciodata (pixeli de tracking) -> 'domcontentloaded'
# + asteptarea pe input#user de mai jos.
logging.info("Navigare catre https://goapp.bancatransilvania.ro/")
self.page.goto('https://goapp.bancatransilvania.ro/', wait_until='domcontentloaded', timeout=30000)
try:
# Cookie consent - asteapta si accepta (GDPR wrapper)
@@ -547,15 +555,7 @@ class BTGoScraper:
except:
pass
# Click pe butonul LOGIN - deschide tab nou
logging.info("Click pe butonul LOGIN...")
with self.page.expect_popup() as popup_info:
login_link = self.page.get_by_role("link", name="Login")
login_link.click()
# Preia referinta la tab-ul de login
self.login_page = popup_info.value
logging.info("Popup/tab login deschis")
self.login_page = self.page
# Asteapta ca Angular SPA sa randeze formularul de login.
# goapp.bancatransilvania.ro afiseaza intai un splash ("Aplicatia ta
@@ -582,10 +582,24 @@ class BTGoScraper:
"(SPA blocat pe splash) - reincarc popup-ul..."
)
self.login_page.reload(wait_until="domcontentloaded")
else:
raise
# la a 2-a incercare nu ridicam aici: iesim din bucla si
# raportam explicit mai jos (pagina de login s-a schimbat).
if not form_loaded:
raise PlaywrightTimeout("input#user nu a aparut nici dupa reload")
# BT a mutat formularul de login: salvam dovezi si spunem exact
# ce URL/titlu am vazut, in loc de un timeout anonim de selector.
self._debug_shot(self.login_page, "login_page_changed")
try:
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
html_path = Path(self.config.OUTPUT_DIR) / f"login_page_changed_{ts}.html"
html_path.write_text(self.login_page.content(), encoding='utf-8')
logging.error(f" [DIAG] HTML salvat: {html_path}")
except Exception as dump_err:
logging.warning(f" [DIAG] Nu am putut salva HTML-ul: {dump_err}")
raise Exception(
"Pagina de login BT s-a schimbat: "
f"URL final {self.login_page.url}, "
f"titlu {self.login_page.title()!r}"
)
logging.info("[OK] Formular login incarcat")
# Verifica GDPR cookies si pe popup
@@ -850,6 +864,9 @@ class BTGoScraper:
time.sleep(1)
logging.info(" Format CSV selectat")
# Marcheaza documentele existente in "Recente" (lista e cumulativa)
self._mark_existing_documents()
# Click pe butonul Genereaza
generate_btn = self.page.get_by_role("button", name="Generează")
generate_btn.click()
@@ -879,9 +896,9 @@ class BTGoScraper:
logging.info(" Dropdown conturi deschis")
# Debug: listeaza toate heading-urile vizibile din dropdown
visible_names = []
try:
headings = self.page.locator("fba-account-details h4, .account-name, h4").all()
visible_names = []
for h in headings[:10]: # Max 10
try:
if h.is_visible(timeout=500):
@@ -893,6 +910,9 @@ class BTGoScraper:
except:
pass
if visible_names and nume_cont not in visible_names:
logging.warning(f" [WARN] Contul '{nume_cont}' lipseste din lista dropdown: {visible_names}")
# Selecteaza contul dupa nume - strategii multiple
account_selected = False
@@ -974,6 +994,9 @@ class BTGoScraper:
time.sleep(2)
# Marcheaza documentele existente in "Recente"
self._mark_existing_documents()
# Click pe butonul Genereaza (CSV deja selectat de la primul cont)
generate_btn = self.page.get_by_role("button", name="Generează")
generate_btn.click()
@@ -987,9 +1010,67 @@ class BTGoScraper:
logging.error(f" [EROARE] Download cont ulterior {nume_cont}: {e}")
return None
# Texte afisate de BT cand exportul nu are continut (nu exista fisier de descarcat)
EMPTY_EXPORT_PATTERNS = (
"nu exist", "nu s-au gasit", "nu s-au găsit",
"fara tranzactii", "fără tranzacții",
)
def _mark_existing_documents(self):
"""Marcheaza documentele deja prezente in lista 'Recente'.
Lista este cumulativa, deci `wait_for_selector("fba-document-item")`
se satisfacea instant pe un document vechi. Marcand cele existente,
putem astepta exact documentul NOU generat.
"""
try:
n = self.page.eval_on_selector_all(
"fba-document-item",
"els => { els.forEach(e => e.setAttribute('data-seen', '1')); return els.length; }",
)
logging.info(f" Documente deja in 'Recente': {n}")
except Exception as e:
logging.debug(f" Nu am putut marca documentele existente: {e}")
def _dump_download_debug(self, nume_cont, tag):
"""La esec: screenshot + outerHTML al listei de documente si al dropdown-ului."""
safe = nume_cont.replace(' ', '_').replace('/', '_').replace('\\', '_')
stamp = datetime.now().strftime('%Y%m%d_%H%M%S')
self._debug_shot(self.page, f"download_{tag}_{safe}")
blocks = {
"documente": (
"() => { const it=[...document.querySelectorAll('fba-document-item')];"
" if(!it.length) return null; let p=it[0].parentElement;"
" while(p && !it.every(i=>p.contains(i))) p=p.parentElement;"
" return p ? p.outerHTML : null; }"
),
"dropdown": (
"() => { const o=document.querySelector('.cdk-overlay-container');"
" return o ? o.outerHTML : null; }"
),
}
for name, js in blocks.items():
try:
html = self.page.evaluate(js)
if not html:
logging.error(f" [DIAG] {name}: negasit in DOM")
continue
path = Path(self.config.OUTPUT_DIR) / f"debug_download_{tag}_{safe}_{name}_{stamp}.html"
path.write_text(html, encoding='utf-8')
logging.error(f" [DIAG] HTML {name} salvat: {path}")
except Exception as e:
logging.error(f" [DIAG] Nu am putut salva HTML {name}: {e}")
try:
texts = self.page.locator("fba-document-item").all_inner_texts()
logging.error(f" [DIAG] fba-document-item ({len(texts)}): {texts}")
except Exception:
pass
def _wait_and_download(self, nume_cont, iban, timeout=20000):
"""
Asteapta generarea fisierului si il descarca.
Asteapta generarea fisierului NOU si il descarca.
Args:
nume_cont: Numele contului (pentru filename)
@@ -1000,13 +1081,29 @@ class BTGoScraper:
Dict cu informatii despre fisierul descarcat sau None
"""
try:
# Asteapta sa apara fba-document-item (indica ca fisierul e gata)
self.page.wait_for_selector("fba-document-item", timeout=timeout)
logging.info(" Document generat - descarc...")
# Asteapta documentul NOU (nu unul vechi ramas in "Recente")
new_selector = "fba-document-item:not([data-seen])"
try:
self.page.wait_for_selector(new_selector, state="visible", timeout=timeout)
except PlaywrightTimeout:
logging.error(f" [EROARE] Niciun document nou generat pentru {nume_cont} ({timeout}ms)")
self._dump_download_debug(nume_cont, "no_new_document")
return None
# Click pe document item pentru a descarca
new_item = self.page.locator(new_selector).first
item_text = (new_item.inner_text() or "").strip()
first_line = item_text.splitlines()[0] if item_text else "(fara text)"
logging.info(f" Document nou: {first_line}")
# BT afiseaza un item de EROARE (fara buton de descarcare) cand
# exportul nu are tranzactii in perioada selectata.
if any(p in item_text.lower() for p in self.EMPTY_EXPORT_PATTERNS):
logging.warning(f" [SKIP] Export fara tranzactii pentru {nume_cont}: {item_text[:160]}")
return None
# Click pe documentul NOU pentru a descarca
with self.page.expect_download(timeout=timeout) as download_info:
download_btn = self.page.locator("fba-document-item svg, fba-document-item path").first
download_btn = new_item.locator("svg, path").first
download_btn.click()
download = download_info.value
@@ -1028,6 +1125,7 @@ class BTGoScraper:
except Exception as e:
logging.error(f" [EROARE] Download fisier: {e}")
self._dump_download_debug(nume_cont, "download_exceptie")
return None
def read_accounts(self):