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:
10
CLAUDE.md
10
CLAUDE.md
@@ -36,6 +36,16 @@ BTGO Scraper - Playwright automation for extracting account balances and transac
|
|||||||
`TIMEOUT_2FA_SECONDS` and reports the wrong cause.
|
`TIMEOUT_2FA_SECONDS` and reports the wrong cause.
|
||||||
- **`networkidle` never fires**: BT pages poll trackers continuously. Use
|
- **`networkidle` never fires**: BT pages poll trackers continuously. Use
|
||||||
`domcontentloaded` + explicit selector waits.
|
`domcontentloaded` + explicit selector waits.
|
||||||
|
- **BT moved the login page (09.2026)**: `go.bancatransilvania.ro` now 302s to the
|
||||||
|
marketing site (no Login link); the form is served directly by
|
||||||
|
`goapp.bancatransilvania.ro/` (`-> /app/auth/login`), same tab, no popup. The
|
||||||
|
scraper sat broken with no clue: the only symptom was a bare `Timeout 15000ms`
|
||||||
|
from the old `a:has-text('Login')` wait. Now, when `input#user` does not show up
|
||||||
|
even after the reload retry, `login()` saves a screenshot + the page HTML in
|
||||||
|
`data/` and raises `Pagina de login BT s-a schimbat: URL final <url>, titlu
|
||||||
|
<titlu>`, so the real cause reaches the Telegram error message instead of dying
|
||||||
|
silently. When the bank moves the login again, run `scripts/check_login_page.py`
|
||||||
|
first (exit 0 = form intact, exit 1 = reason + evidence in `data/`).
|
||||||
|
|
||||||
## Coding Guidelines
|
## Coding Guidelines
|
||||||
|
|
||||||
|
|||||||
152
btgo_scraper.py
152
btgo_scraper.py
@@ -428,7 +428,10 @@ class BTGoScraper:
|
|||||||
logging.info("Start BTGO Scraper")
|
logging.info("Start BTGO Scraper")
|
||||||
logging.info("=" * 60)
|
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"
|
# Viewport 1366x768: BT afiseaza un splash "redirect to store"
|
||||||
# sub ~1100px latime; 1366 e sigur peste prag. Fata de 1920x1080
|
# sub ~1100px latime; 1366 e sigur peste prag. Fata de 1920x1080
|
||||||
# randeaza ~50% mai putini pixeli -> CPU mult mai mic la randarea
|
# 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("✓ Scraper finalizat cu succes!")
|
||||||
logging.info("=" * 60)
|
logging.info("=" * 60)
|
||||||
return True
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self._handle_error(e)
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
p.stop()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handle_error(e)
|
self._handle_error(e)
|
||||||
@@ -523,13 +531,13 @@ class BTGoScraper:
|
|||||||
def login(self):
|
def login(self):
|
||||||
"""Autentificare cu username si password"""
|
"""Autentificare cu username si password"""
|
||||||
self._update_progress("Deschid pagina de login...")
|
self._update_progress("Deschid pagina de login...")
|
||||||
logging.info("Navigare catre https://go.bancatransilvania.ro/")
|
# Din 09.2026 go.bancatransilvania.ro redirecteaza (302) spre pagina de
|
||||||
# 'networkidle' nu se atinge niciodata: pagina face polling continuu pe
|
# marketing, fara link Login. Formularul e servit direct de goapp
|
||||||
# Google Analytics / LinkedIn / Facebook pixels. Folosim 'domcontentloaded'
|
# (/ -> /app/auth/login), in acelasi tab, fara popup.
|
||||||
# si asteptam apoi explicit link-ul Login.
|
# 'networkidle' nu se atinge niciodata (pixeli de tracking) -> 'domcontentloaded'
|
||||||
self.page.goto('https://go.bancatransilvania.ro/', wait_until='domcontentloaded', timeout=30000)
|
# + asteptarea pe input#user de mai jos.
|
||||||
self.page.wait_for_selector("a:has-text('Login')", timeout=15000, state="visible")
|
logging.info("Navigare catre https://goapp.bancatransilvania.ro/")
|
||||||
logging.info("Pagina incarcata")
|
self.page.goto('https://goapp.bancatransilvania.ro/', wait_until='domcontentloaded', timeout=30000)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Cookie consent - asteapta si accepta (GDPR wrapper)
|
# Cookie consent - asteapta si accepta (GDPR wrapper)
|
||||||
@@ -547,15 +555,7 @@ class BTGoScraper:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Click pe butonul LOGIN - deschide tab nou
|
self.login_page = self.page
|
||||||
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")
|
|
||||||
|
|
||||||
# Asteapta ca Angular SPA sa randeze formularul de login.
|
# Asteapta ca Angular SPA sa randeze formularul de login.
|
||||||
# goapp.bancatransilvania.ro afiseaza intai un splash ("Aplicatia ta
|
# goapp.bancatransilvania.ro afiseaza intai un splash ("Aplicatia ta
|
||||||
@@ -582,10 +582,24 @@ class BTGoScraper:
|
|||||||
"(SPA blocat pe splash) - reincarc popup-ul..."
|
"(SPA blocat pe splash) - reincarc popup-ul..."
|
||||||
)
|
)
|
||||||
self.login_page.reload(wait_until="domcontentloaded")
|
self.login_page.reload(wait_until="domcontentloaded")
|
||||||
else:
|
# la a 2-a incercare nu ridicam aici: iesim din bucla si
|
||||||
raise
|
# raportam explicit mai jos (pagina de login s-a schimbat).
|
||||||
if not form_loaded:
|
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")
|
logging.info("[OK] Formular login incarcat")
|
||||||
|
|
||||||
# Verifica GDPR cookies si pe popup
|
# Verifica GDPR cookies si pe popup
|
||||||
@@ -850,6 +864,9 @@ class BTGoScraper:
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
logging.info(" Format CSV selectat")
|
logging.info(" Format CSV selectat")
|
||||||
|
|
||||||
|
# Marcheaza documentele existente in "Recente" (lista e cumulativa)
|
||||||
|
self._mark_existing_documents()
|
||||||
|
|
||||||
# Click pe butonul Genereaza
|
# Click pe butonul Genereaza
|
||||||
generate_btn = self.page.get_by_role("button", name="Generează")
|
generate_btn = self.page.get_by_role("button", name="Generează")
|
||||||
generate_btn.click()
|
generate_btn.click()
|
||||||
@@ -879,9 +896,9 @@ class BTGoScraper:
|
|||||||
logging.info(" Dropdown conturi deschis")
|
logging.info(" Dropdown conturi deschis")
|
||||||
|
|
||||||
# Debug: listeaza toate heading-urile vizibile din dropdown
|
# Debug: listeaza toate heading-urile vizibile din dropdown
|
||||||
|
visible_names = []
|
||||||
try:
|
try:
|
||||||
headings = self.page.locator("fba-account-details h4, .account-name, h4").all()
|
headings = self.page.locator("fba-account-details h4, .account-name, h4").all()
|
||||||
visible_names = []
|
|
||||||
for h in headings[:10]: # Max 10
|
for h in headings[:10]: # Max 10
|
||||||
try:
|
try:
|
||||||
if h.is_visible(timeout=500):
|
if h.is_visible(timeout=500):
|
||||||
@@ -893,6 +910,9 @@ class BTGoScraper:
|
|||||||
except:
|
except:
|
||||||
pass
|
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
|
# Selecteaza contul dupa nume - strategii multiple
|
||||||
account_selected = False
|
account_selected = False
|
||||||
|
|
||||||
@@ -974,6 +994,9 @@ class BTGoScraper:
|
|||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Marcheaza documentele existente in "Recente"
|
||||||
|
self._mark_existing_documents()
|
||||||
|
|
||||||
# Click pe butonul Genereaza (CSV deja selectat de la primul cont)
|
# Click pe butonul Genereaza (CSV deja selectat de la primul cont)
|
||||||
generate_btn = self.page.get_by_role("button", name="Generează")
|
generate_btn = self.page.get_by_role("button", name="Generează")
|
||||||
generate_btn.click()
|
generate_btn.click()
|
||||||
@@ -987,9 +1010,67 @@ class BTGoScraper:
|
|||||||
logging.error(f" [EROARE] Download cont ulterior {nume_cont}: {e}")
|
logging.error(f" [EROARE] Download cont ulterior {nume_cont}: {e}")
|
||||||
return None
|
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):
|
def _wait_and_download(self, nume_cont, iban, timeout=20000):
|
||||||
"""
|
"""
|
||||||
Asteapta generarea fisierului si il descarca.
|
Asteapta generarea fisierului NOU si il descarca.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nume_cont: Numele contului (pentru filename)
|
nume_cont: Numele contului (pentru filename)
|
||||||
@@ -1000,13 +1081,29 @@ class BTGoScraper:
|
|||||||
Dict cu informatii despre fisierul descarcat sau None
|
Dict cu informatii despre fisierul descarcat sau None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Asteapta sa apara fba-document-item (indica ca fisierul e gata)
|
# Asteapta documentul NOU (nu unul vechi ramas in "Recente")
|
||||||
self.page.wait_for_selector("fba-document-item", timeout=timeout)
|
new_selector = "fba-document-item:not([data-seen])"
|
||||||
logging.info(" Document generat - descarc...")
|
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:
|
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_btn.click()
|
||||||
|
|
||||||
download = download_info.value
|
download = download_info.value
|
||||||
@@ -1028,6 +1125,7 @@ class BTGoScraper:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f" [EROARE] Download fisier: {e}")
|
logging.error(f" [EROARE] Download fisier: {e}")
|
||||||
|
self._dump_download_debug(nume_cont, "download_exceptie")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def read_accounts(self):
|
def read_accounts(self):
|
||||||
|
|||||||
72
docs/opencode_agenti_orchestrare.md
Normal file
72
docs/opencode_agenti_orchestrare.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Agenti opencode (DeepSeek V4.1 Flash) orchestrati din Claude Code
|
||||||
|
|
||||||
|
Verificat live 15.09.2026, opencode 1.18.31. Docs: https://opencode.ai/docs/server/
|
||||||
|
|
||||||
|
## Model
|
||||||
|
`opencode-go/deepseek-v4.1-flash` (abonament opencode go). In API: `{"providerID":"opencode-go","modelID":"deepseek-v4.1-flash"}`.
|
||||||
|
|
||||||
|
## Varianta 1: one-shot (fara comunicare pe parcurs)
|
||||||
|
```
|
||||||
|
opencode run -m opencode-go/deepseek-v4.1-flash --title <lane> "<sarcina>"
|
||||||
|
opencode run -s <sessionID> "<mesaj urmator>" # continua aceeasi sesiune dupa ce s-a oprit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Varianta 2: server + HTTP (mesaje in timpul lucrului) — recomandat
|
||||||
|
Pornire (un server per proiect, pentru toate lane-urile lui, in directorul proiectului):
|
||||||
|
```
|
||||||
|
opencode serve --port <port proiect> # 127.0.0.1, fundal
|
||||||
|
```
|
||||||
|
**Port fix per proiect.** Proiectele ROA ruleaza in paralel; pe acelasi port, oprirea serverului dintr-un proiect
|
||||||
|
opreste lane-urile celuilalt si le poate muta directorul de lucru (verificat 15.09.2026: lane ROACONT ajuns in ROAFACTURARE).
|
||||||
|
|
||||||
|
| Proiect | Port |
|
||||||
|
|---|---|
|
||||||
|
| ROAFACTURARE | 4096 |
|
||||||
|
| ROACONT | 4196 |
|
||||||
|
| ROAIMOB | 4296 |
|
||||||
|
| btgo-playwright | 4396 |
|
||||||
|
|
||||||
|
Proiect nou: urmatorul port liber din tabel (+100), trecut aici inainte de pornire. In API trimite si `?directory=<radacina proiectului>`.
|
||||||
|
| Actiune | Apel |
|
||||||
|
|---|---|
|
||||||
|
| Sesiune noua | `POST /session` `{title}` → `id` |
|
||||||
|
| Sarcina (nu asteapta) | `POST /session/:id/prompt_async` `{model, parts:[{type:"text",text}]}` |
|
||||||
|
| Mesaj in timpul lucrului | acelasi `prompt_async` pe sesiunea ocupata |
|
||||||
|
| Stare | `GET /session/status` → `{id:{type:"busy"}}`; sesiunea lipseste cand e libera |
|
||||||
|
| Rezultat | `GET /session/:id/message` (parts cu `type=text`) |
|
||||||
|
| Oprire | `POST /session/:id/abort` |
|
||||||
|
| Evenimente live | `GET /event` (SSE) |
|
||||||
|
| Interfata vizuala | `opencode attach http://127.0.0.1:<port proiect>` (Marius poate urmari sesiunile) |
|
||||||
|
|
||||||
|
**Comportament verificat:** mesajul trimis cat agentul e `busy` intra in coada si e preluat la
|
||||||
|
pasul urmator (dupa apelul de tool curent), in aceeasi sesiune; agentul l-a respectat.
|
||||||
|
Nu intrerupe un tool aflat in executie. Pentru oprire imediata: `abort`, apoi mesaj nou.
|
||||||
|
|
||||||
|
## Prompt initial (obligatoriu)
|
||||||
|
opencode incarca singur `CLAUDE.md` din proiect si `~/.claude/CLAUDE.md`, dar acolo scrie cum sa
|
||||||
|
orchestreze Claude (delegare, gstack), nu cum sa execute. De aceea fiecare sesiune primeste
|
||||||
|
`COMUN\docs\opencode_prompt_initial.md` in campul `system` (verificat: agentul il respecta):
|
||||||
|
```powershell
|
||||||
|
$sys = [IO.File]::ReadAllText('D:\ROA\<PROIECT>\COMUN\docs\opencode_prompt_initial.md')
|
||||||
|
$b = @{model=$m; system=$sys; parts=@(@{type='text';text="Lane: <nume>. <sarcina>"})} | ConvertTo-Json -Depth 5
|
||||||
|
Invoke-RestMethod -Method Post "$u/session/$sid/prompt_async" -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($b))
|
||||||
|
```
|
||||||
|
Cu `opencode run`, fara server: pui promptul ca fisier atasat, `-f COMUN\docs\opencode_prompt_initial.md`.
|
||||||
|
Promptul impune: rol de executant, citirea `reguli_lucru.md`, interdictiile (commit, write-back,
|
||||||
|
binare, `sed -i`), censul cp1250 + CRLF, raportul `docs\raport_<lane>.md` si ultimul mesaj
|
||||||
|
`GATA <lane>` / `BLOCAT <lane>: motiv`.
|
||||||
|
|
||||||
|
## Permisiuni
|
||||||
|
Fara configurare, agentul se opreste la fiecare acces in afara proiectului (scratchpad, `D:\ROA\UTIL`) si asteapta
|
||||||
|
aprobare. Configurarea e globala, pentru toate proiectele, in `%USERPROFILE%\.config\opencode\opencode.jsonc`:
|
||||||
|
```jsonc
|
||||||
|
"permission": { "edit": "allow", "bash": "allow", "webfetch": "allow", "external_directory": "allow" }
|
||||||
|
```
|
||||||
|
Se aplica la pornirea serverului (`opencode serve`); un server deja pornit trebuie repornit.
|
||||||
|
O cerere ramasa in asteptare se aproba prin API: `GET /permission` → `POST /session/:id/permissions/:permissionID` `{"response":"always"}`.
|
||||||
|
|
||||||
|
## Reguli pentru lane-uri
|
||||||
|
- Sarcina contine numele lane-ului, fisierele exacte si criteriul de terminare.
|
||||||
|
- Orchestratorul verifica dupa fiecare lane: diff, octeti cp1252, CRLF, teste.
|
||||||
|
- Dupa lucru: opreste serverul (PID-ul real e cel care asculta pe port, nu cel din `Start-Process`):
|
||||||
|
`Stop-Process -Id (Get-NetTCPConnection -LocalPort <port proiect> -State Listen).OwningProcess` - doar portul propriu.
|
||||||
7
opencode.json
Normal file
7
opencode.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"read": { "*.env": "deny", "*": "allow" },
|
||||||
|
"bash": { "*.env*": "deny", "*": "allow" }
|
||||||
|
}
|
||||||
|
}
|
||||||
182
scripts/check_login_page.py
Normal file
182
scripts/check_login_page.py
Normal 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())
|
||||||
Reference in New Issue
Block a user