Files
vreau-digital/chatGPT/data-quality/refresh-cadence-strategy-2026-05-11.md
T
Claude VM a6c03a091e initial: split from gov-agreg — vreau.digital standalone platform
Moved from gov-agreg/src/pages/achizitii/* to root (drop prefix).
- 22 pages migrated, 127 files total
- All internal links: /achizitii/X → /X (176 occurrences fixed)
- AchizitiiLayout subnav rewritten: /X paths, top-right link to vreaudigital.ro hub
- BaseLayout new (vreau.digital branding, OG tags, site URL)
- astro.config.mjs: site https://vreau.digital, server output (was static)
- docker-compose: port 5096 (vreaudigital is 5095), container vreau-digital
- deploy.sh: paths /opt/vreau-digital, log /var/log/vreau-digital-deploy.log

Backend shared with gov-agreg:
- PostgreSQL satra (same schemas: seap, firms, anaf, anre, ...)
- Photon, Martin tiles
- Infisical /vreaudigital path (DATABASE_URL etc. shared)

build: PASS (npx astro check 0 errors, npm run build 5s vite + 10s server)
2026-05-13 00:10:32 +03:00

625 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Refresh cadence master strategy — gov-agreg / vreaudigital.ro
**Data:** 2026-05-11
**Sub-agent:** S1 (refresh cadence master strategy)
**Bază date:** `architools_db` @ 10.10.10.166 — 29 GB
**Cuprinde:** 17 schemas, 2 sub-pipeline-uri (ANAF v9 + ANAF datornici), strategie captcha, monitorizare, idempotență, DR
**Audit-ul de prospețime anterior:** `chatGPT/data-quality/freshness-audit-2026-05-10.md`
---
## 0. Context & constrângeri
| Constrângere | Stare actuală |
|---|---|
| Host orchestrare | `satra` (10.10.10.166), Docker, Ubuntu, **disc la 85% (299/371 GB)** ⚠️ |
| Sistem de scheduling | systemd timers (3 active) + ad-hoc shell wrappers; **nu există crontab agregat pentru toți 13 scraperi** |
| Secrete | Infisical Machine Identity (`/opt/vreaudigital/.infisical-mi`) — refresh per wrapper |
| Anti-pattern interzis | `docker run -e $DATABASE_URL` (leakă via `ps`); folosim `--env-file` 600 + delete |
| Run-as | `bulibasa` (systemd), `root` (cron actual eterra/backup) |
| Captcha sources | ANAF datornici live, Bugetar Faza 2, ANI e-DAI 2022+ (Cloudflare Turnstile) |
| Buget | Mic — 2captcha (~$1/1000), playwright headless OK, headed pe Orchi doar la nevoie |
**Stat actual systemd (verificat azi):**
- `vreaudigital-anaf-daily.timer` → 02:00 zilnic, enrich-anaf.sh tier=daily, concurrency=2
- `vreaudigital-onrc-weekly.timer` → marți 03:00, import-onrc-fresh.sh
- `vreaudigital-mvs.timer` → 04:00 zilnic, refresh-mvs.sh (9 MV-uri seap)
**13 wrappers existente NE-programate prin systemd** (rulează doar manual sau via cron neagregat încă):
`scrape-aaas`, `scrape-aep-donatii`, `scrape-anaf-datornici`, `scrape-ancom`, `scrape-anre`, `scrape-asf`, `scrape-bugetar`, `scrape-cnas`, `scrape-cnsc`, `scrape-curteacont`, `scrape-gnm`, `scrape-regas`, `import-afir-historical`, `import-apia-fermieri`, `import-financials*`.
Audit-ul `scrape_log` confirmă totuși că **toți cei 9 scraperi cu schema dedicată au rulat în ultimele 24h** — deci există un cron ascuns (probabil în `bulibasa` user crontab, nu în `sudo crontab`). Strategia de mai jos **înlocuiește cron-ul ascuns cu un /etc/cron.d/ vizibil + systemd timers per scraper**.
---
## 1. Per-schema cadence table
Coloane: Schema · Sursă (ritm publicare) · Cadență recomandată · Wrapper · Runtime · Risc · Monitor signal (max age tolerat)
| # | Schema | Sursă upstream — ritm | Cadență recomandată | Wrapper | Runtime | Risc | Monitor signal (max age) |
|---|---|---|---|---|---|---|---|
| 1 | **seap.announcements** (WSP) | live | la 4h | `scrape-seap-wsp` (lipsește wrapper!) | 5-15 min | F5 WAF, ASP session | `wsp_sync_state.last_run_at` ≤ 6h |
| 2 | **seap.direct_acquisitions** | live | la 6h | `scrape-seap-da` (lipsește wrapper!) | 10-30 min | session expiry, retry storms | `sync_state[source=da].updated_at` ≤ 8h |
| 3 | **seap.entities + cui_location** | după WSP/DA refresh | seara, după daily | inclus în WSP wrapper | (incl.) | n/a | `entities.fetched_at` ≤ 24h |
| 4 | **anaf** (v9 enrichment — daily delta) | live API | zilnic 02:00 | `enrich-anaf.sh` TIER=daily | 1-2h | rate limit ANAF 503 | `firms.entities WHERE anaf_fetched_at > now-2d` count ≥ 1000 |
| 5 | **anaf.datornici** (data.gov.ro Q) | quarterly | trim 15-ian/15-apr/15-iul/15-oct | `scrape-anaf-datornici` SOURCE=datagov<Q> | 30-60 min | NEW — necesită captcha doar pt live | `anaf.datornici WHERE publication_date > now-180d` ≥ 1 |
| 6 | **anaf.datornici** (anaf.ro live) | live, captcha | trim — **opțional dacă plătim 2captcha** | `scrape-anaf-datornici` SOURCE=live | 2-4h | reCAPTCHA v2 | (decis în §3) |
| 7 | **firms.entities** (ONRC weekly) | săptămânal | marți 03:00 | `import-onrc-fresh.sh` | 30-60 min | bulk diff fail | `firms.entities.updated_at` ≤ 8 zile |
| 8 | **firms.financials** (ANAF bilanțuri) | anual (15-iul publicare an N-1) | 15 iul + 15 aug rerun | `import-financials.sh` | 2-4h | mărime CSV ~3GB | `firms.financials WHERE source_year = year(now)-1` ≥ 800k |
| 9 | **firms.financials_ong / banks** | anual | 20-iul | `import-financials-ong-banks.sh` | 1h | n/a | acelaşi |
| 10 | **fonduri.afir_plati** | anual data.gov.ro | 15-feb (date an N-1) | `import-afir-historical.sh` | 2-4h | CSV mare | `fonduri.afir_plati WHERE source_year = year(now)-1` ≥ 1M |
| 11 | **fonduri.beneficiar_anunt / proiect** (FEADR + FEGA) | live data.gov.ro | săptămânal lun 02:00 | `import-fonduri-beneficiari` (lipsește!) | 15-30 min | n/a | `fonduri.beneficiar_anunt.fetched_at` ≤ 8d |
| 12 | **regas.ajutoare** (Consiliul Concurenței) | lunar | luna 1 ale lunii 02:00 | `scrape-regas` | 10-15 min | n/a | `regas.ajutoare.fetched_at` ≤ 35d |
| 13 | **bugetar.entitate** (mfinante public registry) | lunar | luna 1 ale lunii 03:00 | `scrape-bugetar` | 30-60 min | n/a | `bugetar.entitate.fetched_at` ≤ 35d |
| 14 | **bugetar.executie** (Faza 2 — captcha) | lunar (raportare 30 zile decalaj) | **deferred** — vezi §3 | `scrape-bugetar-executie` (lipsește) | 4-8h pt 1000 entități | captcha + 1000 detail pages | (deferred) |
| 15 | **anre.licente** (3 surse: atestat/electricitate/gaze) | live | zilnic 03:00 | `scrape-anre` SOURCE=all | 3-5 min | TLS cert intermediary | `anre.licente.fetched_at` ≤ 36h |
| 16 | **anre.electricieni** | live (~100k entries) | săptămânal duminică 04:00 | `scrape-anre` SOURCE=electricieni | 30-60 min | pagination volume | `anre.electricieni.fetched_at` ≤ 8d *(when implemented)* |
| 17 | **ancom.operatori + drepturi** | live registry | zilnic 04:00 | `scrape-ancom` | 1-2 min | n/a | `ancom.operatori.fetched_at` ≤ 36h |
| 18 | **asf.entitati** | live (rebuild nightly) | zilnic 05:00 | `scrape-asf` | 2-3 min | "omit g-recaptcha" trick must hold | `asf.entitati.fetched_at` ≤ 36h |
| 19 | **cnsc.decizii** (listing) | live | zilnic 02:30 | `scrape-cnsc` MAX_PAGES=10 (incremental) | 2-5 min | session-based | `cnsc.decizii.fetched_at` ≤ 36h |
| 20 | **cnsc Stage 2** (PDF parse → decision_type) | după listing | săptămânal sâmbătă 02:00 | `cnsc-parse-pdfs` (lipsește) | 4-8h pt 30k | I/O storage PDFs | % decizii `WHERE decision_type IS NOT NULL` ≥ 90% |
| 21 | **cnas.documents** | lunar pe WP media | săptămânal lun 04:00 | `scrape-cnas` | 5-10 min | format CNAS schimbabil | `cnas.documents.fetched_at` ≤ 8d |
| 22 | **cnas.furnizori** (parse din PDF) | inclus în .documents | săptămânal | (incl.) | (incl.) | parser failure 25% | % docs `parse_status='ok'` ≥ 75% |
| 23 | **aaas.firme** | live portal | săptămânal lun 04:30 | `scrape-aaas` | 30s | listă mică (11 firme) | `aaas.firme.fetched_at` ≤ 8d |
| 24 | **curteacont.rapoarte** (Stage 1 listing) | live săptămânal | zilnic 05:30 | `scrape-curteacont` | 1-3 min | n/a | `curteacont.rapoarte.fetched_at` ≤ 36h |
| 25 | **curteacont Stage 2** (detail + PDF + audited CUI) | după Stage 1 | săptămânal duminică 03:00 | `curteacont-detail` (lipsește) | 4-6h pt 1133 | n/a | % rapoarte `WHERE audited_entity_cui IS NOT NULL` ≥ 50% |
| 26 | **aep.donatii_pf/pj/rvc + partide** | trimestrial (raportări) | trim 15-ian/15-apr/15-iul/15-oct + lunar smoke check | `scrape-aep-donatii` | 1h | banipartide.ro mortality | `aep.donatii_pj.fetched_at` ≤ 95d |
| 27 | **ani.declaratii** (PDFs) | live ANI dar **parser ne-implementat** | **deferred** | n/a | n/a | Cloudflare Turnstile | (deferred — multi-week) |
| 28 | **apia.fermieri** (CKAN data.gov.ro) | anual (campania an N publicată 1-mar an N+1) | 15-mar + lunar smoke | `import-apia-fermieri` | 5-10 min | volum mic actual (191 rows — needs investigation) | `apia.fermieri.fetched_at` ≤ 35d |
| 29 | **gnm.comunicate** (RSS) | săptămânal | zilnic 06:00 | `scrape-gnm` | 1-2 min | RSS format change | `gnm.comunicate.fetched_at` ≤ 36h ŞI `publicat_la_max > now-30d` |
| 30 | **gnm.amenzi_extrase** (Stage B fuzzy) | după Stage A | săptămânal duminică 05:00 | `gnm-extract-amenzi` (post-A2) | 30 min | NLP false positives | % comunicate flagged enforcement cu amendă extrasă ≥ 50% |
| 31 | **seap MV refresh** (9 materialized views) | după toate SEAP scrape | zilnic 06:00 (după WSP+DA) | `refresh-mvs.sh` | 5-15 min | dependență de WSP/DA | `mv_authority_concentration` ultim refresh ≤ 26h |
**Note critice:**
- **Wrappere lipsă:** `scrape-seap-wsp`, `scrape-seap-da`, `import-fonduri-beneficiari`, `scrape-bugetar-executie`, `cnsc-parse-pdfs`, `curteacont-detail`, `gnm-extract-amenzi`. Scraperele TypeScript există în `src/`, dar nu au wrapper `cron/scrape-*.sh` cu pattern Infisical MI → env-file → docker run. **Aceasta este lacuna #1 înainte de oricărei programări noi.**
- ANRE rulează deja zilnic via cron ascuns dar nu via systemd vizibil — strategia mută totul în systemd timers per scraper, ca **mvs.timer** azi.
---
## 2. Cron schedule recommendation
Două opțiuni implementabile:
- **(A) /etc/cron.d/govagreg-refresh** — un singur fișier vizibil, ușor de auditat.
- **(B) systemd timers per scraper** — match-uiește patternul existent (`vreaudigital-*.timer`), permite `journalctl -u`, status uniform.
**Recomandare: B (systemd timers)**, pentru că:
1. Patternul există deja (3 timere), iar `journalctl` e mai util decât `/var/log/cron`.
2. Per-unit `OnFailure=` permite alerting nativ.
3. `Persistent=true` reia rulările pierdute după reboot (cron-ul de pe satra nu are anacron).
4. `RandomizedDelaySec=` evită contenția în vârful 02:00-06:00.
### 2.1 Timer skeleton (canonical pattern)
Un template pentru fiecare scraper:
```ini
# /etc/systemd/system/vreaudigital-<scraper>.service
[Unit]
Description=vreaudigital — <scraper> refresh
Wants=network.target docker.service
After=network.target docker.service vreaudigital-prerequisites.service
[Service]
Type=oneshot
User=bulibasa
ExecStart=/opt/vreaudigital/services/seap-scraper/cron/scrape-<scraper>.sh
StandardOutput=journal
StandardError=journal
TimeoutStartSec=4h
OnFailure=vreaudigital-alert@%n.service
# /etc/systemd/system/vreaudigital-<scraper>.timer
[Unit]
Description=vreaudigital — <scraper> at <time>
[Timer]
OnCalendar=<schedule>
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
```
### 2.2 Recommended schedule (eșalonat pentru a evita contenție pe satra)
```
# === LIVE / NEAR-REAL-TIME (multiple ori pe zi) ===
vreaudigital-seap-wsp.timer OnCalendar=*-*-* 00,04,08,12,16,20:15:00 # 6× zi
vreaudigital-seap-da.timer OnCalendar=*-*-* 02,10,18:30:00 # 3× zi (mai greu)
# === DAILY off-peak (02:00-06:00, eșalonat la 5-15 min) ===
vreaudigital-anaf-daily.timer OnCalendar=*-*-* 02:00:00 # exista (enrich v9 daily)
vreaudigital-cnsc.timer OnCalendar=*-*-* 02:30:00
vreaudigital-anre.timer OnCalendar=*-*-* 03:00:00
vreaudigital-curteacont.timer OnCalendar=*-*-* 03:30:00
vreaudigital-ancom.timer OnCalendar=*-*-* 04:00:00
vreaudigital-asf.timer OnCalendar=*-*-* 04:30:00
vreaudigital-gnm.timer OnCalendar=*-*-* 05:00:00
vreaudigital-mvs.timer OnCalendar=*-*-* 06:00:00 # exista; mut de la 04:00 la 06:00 ca să fie după toate scraperele
# === WEEKLY (luni 04:00-06:00, sâmbătă/duminică pentru heavy) ===
vreaudigital-cnas.timer OnCalendar=Mon *-*-* 04:00:00
vreaudigital-aaas.timer OnCalendar=Mon *-*-* 04:30:00
vreaudigital-onrc-weekly.timer OnCalendar=Tue *-*-* 03:00:00 # exista
vreaudigital-fonduri-week.timer OnCalendar=Mon *-*-* 02:00:00
vreaudigital-anre-electricieni.timer OnCalendar=Sun *-*-* 04:00:00
vreaudigital-cnsc-pdfs.timer OnCalendar=Sat *-*-* 02:00:00 # Stage 2 heavy
vreaudigital-curteacont-detail.timer OnCalendar=Sun *-*-* 03:00:00 # Stage 2
vreaudigital-gnm-amenzi.timer OnCalendar=Sun *-*-* 05:00:00 # Stage B
# === MONTHLY (1 ale lunii, 02:00-04:00) ===
vreaudigital-regas.timer OnCalendar=*-*-01 02:00:00
vreaudigital-bugetar.timer OnCalendar=*-*-01 03:00:00
vreaudigital-apia.timer OnCalendar=*-*-01 04:00:00 # smoke check, full pe martie
# === QUARTERLY (15 ale luni 1/4/7/10) ===
vreaudigital-anaf-datornici.timer OnCalendar=*-01,04,07,10-15 02:00:00
vreaudigital-aep-donatii.timer OnCalendar=*-01,04,07,10-15 03:00:00
# === ANNUAL ===
vreaudigital-afir-historical.timer OnCalendar=*-02-15 02:00:00
vreaudigital-financials.timer OnCalendar=*-07-15 02:00:00
vreaudigital-financials-ong.timer OnCalendar=*-07-20 02:00:00
vreaudigital-apia-full.timer OnCalendar=*-03-15 02:00:00
# === DEAD-MAN'S SWITCH (vezi §4) ===
vreaudigital-heartbeat.timer OnCalendar=*-*-* 07:00:00 # alert dacă lipsesc date proaspete
```
**Total estimat încărcare:** ~35 min CPU/zi steady-state daily slot, ~2-4h/sâmbătă-duminică (heavy stages), ~6-10h în 15 ale lunilor Q (datornici + AEP), ~8h în 15-iul (financials annual).
### 2.3 Resource contention checklist
- **02:00-04:00 daily:** anaf (1-2h) + cnsc (2-5 min) + anre (3-5 min) + curteacont (1-3 min). ANAF rulează long, restul tick-uri scurte cu RandomizedDelaySec=300-600 evită overlap.
- **04:00-06:00 daily:** ancom + asf + gnm + mvs. Toate sub 15 min total. mvs (5-15 min) e ultimul.
- **Luni 02:00-05:00:** fonduri + cnas + aaas + apia smoke. ONRC pe MARȚI ca să nu se ciocnească cu nimic.
- **Weekend:** cnsc-pdfs + curteacont-detail + anre-electricieni + gnm-amenzi. Heavy lifts, niciun overlap.
- **Disc:** la 85% pe satra ⚠️. **Înainte de orice scraper PDF nou (cnsc-pdfs, curteacont-detail) — rezolvă §6 disc**.
---
## 3. CAPTCHA-blocked sources strategy
### 3.1 ANAF datornici live (anaf.ro/restante)
**Stare:** Singura sursă publică bulk (data.gov.ro Q1 2016) e statică. Pentru a actualiza 2016-Q2 → 2026-Q1 (38 trimestre) trebuie scrape live cu captcha.
**Two paths:**
| Path | Cost | Timp implementare | Acoperire |
|---|---|---|---|
| **(a) data.gov.ro Q-snapshots** | $0 | 2 zile (sursa trebuie verificată dacă publică Q-uri noi) | depinde de mfinante |
| **(b) 2captcha pe anaf.ro/restante live** | $1-3/1000 captcha | 1 săptămână + Playwright | toate Q-urile, on-demand |
**Recomandare:** path (a) prima — verifică data.gov.ro listing pentru dataset-uri `anaf-datornici-202X`. Dacă publică, scraperul existent (`SOURCE=datagovYYYY-QN`) deja gestionează. Path (b) doar dacă data.gov.ro nu publică sau e cu lag mare.
**Buget 2captcha pentru path (b) — backfill 5 ani × 4 Q × 1 captcha per fetch = 20 captchas total** (un Q = un download pe full set, nu per-entitate). **Buget: ~$0.10/an** (neglijabil). Costul real: timpul dev pentru integrare Playwright + 2captcha SDK = 2-3 zile.
**Pre-req decision:**
```
DACĂ Q4-2025 publicat pe data.gov.ro
ATUNCI nu plătim 2captcha — extindem `scrape-anaf-datornici.sh` cu SOURCE=datagov2025Q4
ALTFEL plătim 2captcha (~$0.10/an) ȘI investim 2-3 zile dev
```
### 3.2 Bugetar Faza 2 — execuție bugetară per entitate
**Stare:** `bugetar.entitate` = 18,822 entități; `bugetar.executie` = **0 rows**.
**Captcha analiza:** mfinante.gov.ro/static/10/Mfp/sit-Trezor/situatie_trezorerie.html — pagina detail per entitate cere captcha (Google reCAPTCHA v2). Per fetch = 1 captcha.
**Strategie scope:**
- Total entități × 60 luni = 18,822 × 60 = **1,129,320 fetches** dacă acoperim TOATE entitățile × tot istoricul (5 ani).
- Cu 2captcha la $1/1000: **$1,129/total**, ~$226/an pentru 5 ani amortizat.
- Reducem la **top-1000 entități după buget** × 60 luni = **60,000 fetches = $60 total**, ~$12/an. ← **RECOMANDARE**.
**Buget total bugetar Faza 2: $60-100 one-shot pentru top-1000 entități**. Refresh lunar incremental: 1000 × 1 lună = 1000 fetch/lună = $1/lună.
### 3.3 ANI new e-DAI 2022+ (Cloudflare Turnstile)
**Stare:** ANI mută `e-DAI` pe noua platformă (post-2022) protejat cu Cloudflare Turnstile (nu reCAPTCHA). 2captcha **suportă Turnstile** ($3/1000) dar e mai puțin fiabil; Playwright **headed** (cu browser real) e fallback.
**Volum:** ~1.3M PDFs (CLAUDE.md). Chiar la $3/1000 = **$3,900** pentru backfill complet — depășește bugetul. Refresh incremental ~50k/an = $150/an.
**Recomandare:**
- **Faza 0:** parserul PDF nu e implementat încă. Investește 4-6 săptămâni dev în parser ÎNAINTE de a cheltui pe captcha.
- **Faza 1:** scraping curent — folosește **Playwright headed pe Orchi** (RTX A4000, neutilizat noaptea) pentru sample 10k declarații, manual solving / fingerprint rotation. **Cost direct: $0.**
- **Faza 2 (dacă scalează):** 2captcha Turnstile pentru deltas anuale ~$150/an.
### 3.4 ASF "omit g-recaptcha-response" trick
ASF nu necesită 2captcha — scraperul curent omite parametrul `g-recaptcha-response` din POST și serverul răspunde oricum (bug în implementarea ASF). **Risk:** ASF poate fixa oricând acest bug. Monitor: dacă `scrape-asf.sh` începe să returneze 0 rows constant, investighează.
### 3.5 Decision rubric — investim captcha sau nu?
| Sursă | 2captcha cost/an | Valoare unlock | Vot |
|---|---|---|---|
| ANAF datornici live | ~$0.10 | mediu (path-a probabil rezolvă) | **NU prioritar** — verifică path-a întâi |
| Bugetar top-1000 | ~$12 (incremental) | mare (fluxuri bani publici) | **DA** după parser execuție repaired |
| ANI e-DAI 2022+ | ~$150 | flagship | **DEFER** până la parser PDF implementat |
| Bugetar toate 18,822 | ~$226 | mare dar redundant cu top-1000 | **NU** — top-1000 e suficient |
**Buget total 2captcha pe an pentru acoperire completă recomandată:** **$15-25/an** (Bugetar top-1000 incremental + ANAF safety net + ASF backup dacă trick-ul se strică).
**Buget total 2captcha pentru backfill one-shot:** **$60-100** (Bugetar top-1000 × 5 ani istoric).
**Buget extins dacă includem ANI:** **+$150/an pentru deltas**, $3,900 backfill (deferred).
---
## 4. Monitoring & alerting
### 4.1 Dead-man's switch — heartbeat zilnic
**Concept:** o singură query rulează la 07:00 zilnic, verifică `max(fetched_at)` per tabel cheie, alertează dacă > expected_cadence × 1.5.
**Implementare:** `vreaudigital-heartbeat.service` + Brevo SMTP (deja config).
```bash
#!/bin/bash
# /opt/vreaudigital/services/seap-scraper/cron/heartbeat.sh
set -euo pipefail
LOG=/var/log/vreaudigital-heartbeat.log
source /opt/vreaudigital/.infisical-mi
# ... (fetch DATABASE_URL + SMTP creds via Infisical, same pattern as refresh-mvs.sh)
# Define expected freshness (hours)
declare -A EXPECTED=(
["seap.announcements"]="6"
["seap.direct_acquisitions"]="8"
["anre.licente"]="36"
["ancom.operatori"]="36"
["asf.entitati"]="36"
["cnsc.decizii"]="36"
["curteacont.rapoarte"]="36"
["gnm.comunicate"]="36"
["firms.entities"]="192" # 8 days (weekly cron + buffer)
["cnas.documents"]="192"
["aaas.firme"]="192"
["fonduri.beneficiar_anunt"]="192"
["regas.ajutoare"]="840" # 35 days (monthly)
["bugetar.entitate"]="840"
["apia.fermieri"]="840"
["anaf.datornici"]="4320" # 180 days (quarterly)
["aep.donatii_pj"]="2280" # 95 days
)
ALERTS=()
for table in "${!EXPECTED[@]}"; do
schema="${table%.*}"
tbl="${table#*.}"
max_age=$(psql -tA -c "SELECT EXTRACT(EPOCH FROM (now() - max(fetched_at)))/3600 FROM ${table}")
threshold="${EXPECTED[$table]}"
if (( $(echo "$max_age > $threshold * 1.5" | bc -l) )); then
ALERTS+=("$table: ${max_age}h stale (threshold ${threshold}h)")
fi
done
if [ ${#ALERTS[@]} -gt 0 ]; then
BODY=$(printf '%s\n' "${ALERTS[@]}")
echo "$BODY" | mail -s "[vreaudigital] heartbeat: ${#ALERTS[@]} schemas stale" \
-S smtp="smtps://$BREVO_SMTP_HOST:$BREVO_SMTP_PORT" \
-S smtp-auth=login \
-S smtp-auth-user="$BREVO_SMTP_USER" \
-S smtp-auth-password="$BREVO_SMTP_KEY" \
-S from="alerts@beletage.ro" \
m.tarau@beletage.ro
fi
```
**Alternativă alerting:** n8n webhook (deja deployed la `https://n8n.beletage.ro`) — POST simplu, n8n trimite mai departe pe Telegram/Slack/email cu un singur workflow.
```bash
curl -fsS -X POST https://n8n.beletage.ro/webhook/vreaudigital-heartbeat \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg body "$BODY" '{type:"stale-data", alerts:$body}')"
```
### 4.2 Per-scraper OnFailure alert
Adaugă `OnFailure=vreaudigital-alert@%n.service` în fiecare timer. Template service:
```ini
# /etc/systemd/system/vreaudigital-alert@.service
[Unit]
Description=vreaudigital alert for %i
[Service]
Type=oneshot
User=bulibasa
ExecStart=/opt/vreaudigital/services/seap-scraper/cron/alert.sh %i
```
`alert.sh %i` extrage ultimele 50 linii via `journalctl -u %i -n 50` și le trimite la n8n webhook.
### 4.3 Top blind spots care necesită monitor azi
1. **`seap.sync_state[source=da].status = pending` din 2025-10-16** (208 zile!) — DA backfill blocat și nimeni nu primește alert. **Trebuie heartbeat dedicat pentru `sync_state` și `wsp_sync_state` care alertează dacă `updated_at < now() - 24h` sau `consecutive_errors > 5`**.
2. **WSP `last_run_at = 2026-05-07`** (4 zile stale, ar trebui la 4h). Patternul deja descris în audit ca lipsit — heartbeat fix-uiește.
3. **Disk 85% pe satra** — heartbeat trebuie să verifice `df -h /` și să alerteze la 90%.
### 4.4 Sample monitor query — copy-paste într-un singur SQL
```sql
SELECT 'STALE: '||t AS alert FROM (
SELECT 'seap.announcements' AS t, max(fetched_at) AS f FROM seap.announcements
UNION ALL SELECT 'seap.direct_acquisitions', max(fetched_at) FROM seap.direct_acquisitions
UNION ALL SELECT 'firms.entities', max(updated_at) FROM firms.entities
UNION ALL SELECT 'fonduri.afir_plati', max(fetched_at) FROM fonduri.afir_plati
UNION ALL SELECT 'regas.ajutoare', max(fetched_at) FROM regas.ajutoare
UNION ALL SELECT 'anre.licente', max(fetched_at) FROM anre.licente
UNION ALL SELECT 'ancom.operatori', max(fetched_at) FROM ancom.operatori
UNION ALL SELECT 'asf.entitati', max(fetched_at) FROM asf.entitati
UNION ALL SELECT 'cnsc.decizii', max(fetched_at) FROM cnsc.decizii
UNION ALL SELECT 'cnas.documents', max(fetched_at) FROM cnas.documents
UNION ALL SELECT 'aaas.firme', max(fetched_at) FROM aaas.firme
UNION ALL SELECT 'curteacont.rapoarte', max(fetched_at) FROM curteacont.rapoarte
UNION ALL SELECT 'apia.fermieri', max(fetched_at) FROM apia.fermieri
UNION ALL SELECT 'aep.donatii_pj', max(fetched_at) FROM aep.donatii_pj
UNION ALL SELECT 'gnm.comunicate', max(fetched_at) FROM gnm.comunicate
UNION ALL SELECT 'bugetar.entitate', max(fetched_at) FROM bugetar.entitate
UNION ALL SELECT 'anaf.datornici', max(fetched_at) FROM anaf.datornici
) x
WHERE f < now() - (
CASE
WHEN t LIKE 'seap.%' THEN interval '12 hours'
WHEN t IN ('anre.licente','ancom.operatori','asf.entitati','cnsc.decizii','curteacont.rapoarte','gnm.comunicate') THEN interval '54 hours'
WHEN t IN ('firms.entities','cnas.documents','aaas.firme','fonduri.afir_plati') THEN interval '12 days'
WHEN t IN ('regas.ajutoare','apia.fermieri','bugetar.entitate') THEN interval '52 days'
WHEN t = 'aep.donatii_pj' THEN interval '143 days'
WHEN t = 'anaf.datornici' THEN interval '270 days'
END
);
```
Rulează zilnic la 07:00. Dacă returnează rânduri → email/n8n.
---
## 5. Idempotency contract per source
**Cerință:** fiecare scraper TREBUIE să fie idempotent — re-rularea NU duplică, doar refresh `fetched_at`.
| Schema | Idempotency key | Mecanism (din cod existent verificat sau menționat în audit) | Status |
|---|---|---|---|
| seap.announcements | `(source, source_id)` | `ON CONFLICT (source, source_id) DO UPDATE` (confirmat audit) | ✅ |
| seap.direct_acquisitions | similar | similar | ✅ |
| firms.entities | `cui` PK | `ON CONFLICT (cui) DO UPDATE` | ✅ |
| firms.financials | `(cui, source_year)` | UPSERT | ✅ |
| fonduri.afir_plati | `(cnp_cui_hash, source_year, suma)` | hash unique | ✅ (audit) |
| fonduri.beneficiar_anunt | `(announcement_id)` | UPSERT | ✅ |
| regas.ajutoare | `(cui, an, masura)` | UPSERT | ✅ |
| anaf.datornici | `(cui, publication_date)` | `ON CONFLICT (cui, publication_date) DO UPDATE` (confirmat wrapper) | ✅ |
| anaf.lista_alba | TBD | gol — pipeline neimplementat | ⚠️ |
| aep.donatii_pf | `(partid, donator_nume, data, suma)` | composite UNIQUE | ✅ |
| aep.donatii_pj | similar | composite UNIQUE | ✅ |
| aep.donatii_rvc | similar | composite UNIQUE | ⚠️ are date eronate 2034 — necesită cleanup, dar UPSERT funcționează |
| bugetar.entitate | `cif` | UPSERT | ✅ |
| bugetar.executie | TBD | gol | ⚠️ |
| anre.licente | `(source, nr_autorizare)` sau sha1 | UPSERT pe sha1 (wrapper confirmă) | ✅ |
| anre.electricieni | `UNIQUE(nr_autorizare, nume_prenume)` (wrapper) | UPSERT | ✅ (când rulează) |
| ancom.operatori | `cui` | UPSERT | ✅ |
| ancom.drepturi | `(cui, tip_drept)` | UPSERT | ✅ |
| asf.entitati | `cui` | UPSERT | ✅ |
| cnsc.decizii | `(decision_no, decision_year)` | `ON CONFLICT (decision_no, decision_year) DO UPDATE` (wrapper confirmat) | ✅ |
| cnas.documents | `source_url_sha1` | UPSERT | ✅ |
| cnas.furnizori | `(document_id, row_index)` | UPSERT | ✅ |
| aaas.firme | `cui` | UPSERT | ✅ |
| curteacont.rapoarte | `(audit_year, report_no)` sau URL | UPSERT | ✅ |
| apia.fermieri | `(cnp_cui, campania)` | UPSERT | ✅ |
| ani.declaratii | `pdf_sha1` | UPSERT | ✅ (când parser funcționează) |
| gnm.comunicate | `URL sha1` | UPSERT | ✅ |
| gnm.amenzi_extrase | `(comunicat_id, violator_cui, suma)` | UPSERT | ✅ |
**Non-idempotent suspects (necesită review cod):**
- `anaf.lista_alba` — gol, pipeline neexistent. Când implementat, UPSERT pe `cui`.
- `bugetar.executie` — gol. Când implementat, UPSERT pe `(cif, an, luna, indicator)`.
- TED import (`import_ted.py`) — `publication-date` bug confirmat în audit; UPSERT-ul probabil funcționează, dar fix-ul de 1 linie e prerequisite.
**Action item:** după implementarea bugetar.executie și anaf.lista_alba, verifică explicit `ON CONFLICT DO UPDATE/DO NOTHING` în INSERT statements și adaugă teste de idempotență (rulează scraperul de 2 ori la rând și verifică `count(*)` constant).
---
## 6. Disaster recovery
### 6.1 RTO/RPO
**Componente:**
- DB `architools_db` @ 10.10.10.166 — 29 GB
- Codul pe `gitadmin/gov-agreg` Gitea — recuperabil în <1 min
- `.infisical-mi` files — secrets în Infisical, recuperabil cu MI restart
- Cron-uri/timere — în git repo (path `services/seap-scraper/cron/`)
**RTO (Recovery Time Objective):** ~2 ore — git clone + restore dump + restart timers.
**RPO (Recovery Point Objective):** depinde de backup cadence — vezi 6.2.
### 6.2 DB backup status (verified azi)
`sudo crontab -l` pe satra arată **DOAR**:
- `/opt/pug-tracker-scripts/scripts/backup-db.sh` la 03:00
- `/home/bulibasa/backup.sh` la 05:45
- eterra stats email la 06:30
**NU există backup explicit pentru `architools_db`** — trebuie verificat dacă `pug-tracker-scripts/backup-db.sh` sau `bulibasa/backup.sh` include `architools_db`. **Această este o gaură critică în DR**.
**Acțiune imediată:** verifică conținut `/opt/pug-tracker-scripts/scripts/backup-db.sh` și `/home/bulibasa/backup.sh`. Dacă `architools_db` lipsește, adaugă:
```bash
# /opt/vreaudigital/services/seap-scraper/cron/backup-db.sh
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/backups/architools_db
mkdir -p "$BACKUP_DIR"
DATE=$(date +%Y%m%d_%H%M)
source /opt/vreaudigital/.infisical-mi
# ... (fetch DATABASE_URL pattern)
# pg_dump custom format (compressed, parallelizable restore)
pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d "$PGDATABASE" \
--format=custom \
--jobs=4 \
--no-owner --no-acl \
--exclude-table='*staging_*' \
--exclude-table-data='*log*' \
--file="$BACKUP_DIR/architools_${DATE}.dump"
# Keep 7 daily, 4 weekly, 12 monthly
find "$BACKUP_DIR" -name 'architools_*.dump' -mtime +90 -delete
```
**Programare:** `vreaudigital-backup.timer OnCalendar=*-*-* 23:00:00` (înainte de scrape-urile de 02:00).
**Mărime estimată:** 29GB DB → ~6-8GB compressed (custom format ratio ~4×). Disc satra: 57GB liberi, suficient pentru ~7 zile retention pe satra + rotate spre **shop** sau **NAS Synology** via rclone/rsync.
### 6.3 Restore procedure (documentată)
```
# 1. Pe satra (sau host nou):
git clone https://git.beletage.ro/gitadmin/gov-agreg.git /opt/vreaudigital
cd /opt/vreaudigital/services/seap-scraper
npm install --omit=optional
# 2. Restore .infisical-mi
scp <safe-source>:/opt/vreaudigital/.infisical-mi /opt/vreaudigital/
chmod 600 /opt/vreaudigital/.infisical-mi
# 3. Restore DB
createdb architools_db
pg_restore --jobs=4 --no-owner --no-acl \
--dbname=architools_db \
/backups/architools_db/architools_<latest>.dump
# 4. Restart timers
sudo systemctl enable --now vreaudigital-*.timer
sudo systemctl list-timers | grep vreaudigital
```
### 6.4 Off-site backup
Recomandare: rsync zilnic `/backups/architools_db/` la **shop.avizero.ro:/srv/backups/satra-architools/** sau spre Synology NAS dacă există. **NU rsync direct la GitHub/Gitea** (29GB > limit).
```
# /etc/systemd/system/vreaudigital-backup-offsite.timer OnCalendar=*-*-* 23:30:00
rsync -avz --delete /backups/architools_db/ shop:/srv/backups/satra-architools/
```
---
## 7. Recommended action items, prioritized
### 7.1 This week (low effort, high ROI)
| # | Item | Effort | Impact |
|---|---|---|---|
| 1 | **Fix TED `publication-date` field** în `import_ted.py` (1-line) | 5 min | 100% TED publication_date populated |
| 2 | **Reset `seap.sync_state[source=da].status` din pending → null** + relansare backfill DA | 15 min | unlock 208-day-old backfill (potential ~8M rows) |
| 3 | **Investigate WSP stall**`wsp_sync_state.last_run_at = 2026-05-07`. Verifică cron-ul ascuns; dacă lipsește, creează `vreaudigital-seap-wsp.timer` per §2.2 | 1h | live SEAP daily feed restored |
| 4 | **Verifică backup DB** — citește `/opt/pug-tracker-scripts/scripts/backup-db.sh` și `/home/bulibasa/backup.sh`. Dacă `architools_db` lipsește, instalează `backup-db.sh` din §6.2 | 1h | DR readiness, RPO ≤ 24h |
| 5 | **Implementează `vreaudigital-heartbeat.timer`** din §4.1 + 1 query în §4.4 | 2h | dead-man's switch peste 17 schemas |
**Total week 1:** ~5h work, unlocks 4 critical paths.
### 7.2 This month (medium effort)
| # | Item | Effort | Impact |
|---|---|---|---|
| 1 | **Creează wrappere lipsă** pentru `scrape-seap-wsp`, `scrape-seap-da`, `import-fonduri-beneficiari`, `gnm-extract-amenzi`, `curteacont-detail`, `cnsc-parse-pdfs` (6 wrappere cu pattern Infisical MI) | 1 zi | uniformizează scheduling |
| 2 | **Migrează toate cele 13 wrappere existente la systemd timers vizibili** per §2.2 (înlocuiește cron-ul ascuns) | 1 zi | observabilitate `journalctl -u`, retry on failure |
| 3 | **Investigate ANAF datornici Q4 2025 publicare pe data.gov.ro** — dacă publicat, rulează `scrape-anaf-datornici SOURCE=datagov2025Q4`. Altfel începe integrare 2captcha | 1 zi | datornici devine fresh |
| 4 | **Disc cleanup pe satra** — staging tables 3GB (firms.staging_onrc_*) + log rotation + offsite backups să poată fi instalate | 4h | disc < 80%, room pentru cnsc PDFs Stage 2 |
| 5 | **CUI matcher rerun pentru cnas.furnizori, apia.fermieri, fonduri.beneficiar_proiect** (3 schemas cu 0% match) | 4h | unlock cross-source recipes |
### 7.3 Next quarter (high effort sau lower priority)
| # | Item | Effort | Impact |
|---|---|---|---|
| 1 | **CNSC Stage 2 PDF parser** — extract decision_type/summary pentru 29k decizii | 1-2 săpt | decizii filtrabile |
| 2 | **Curtea Conturi Stage 2** detail-page + audited_cui + PDF | 2 săpt | rapoarte legate la CUI |
| 3 | **Bugetar.executie Faza 2** + 2captcha pentru top-1000 entități (~$60 one-shot) | 2 săpt | flux financiar public |
| 4 | **ANI declaratii parser** (1.3M PDFs) — recommended deferred până confirmat parser ANRE/AAAS minor backlogs cleared | 4-6 săpt | flagship politicieni |
| 5 | **SEAP DA backfill 2017-2024** (~8M rows) — post DA sync_state reset | 2-3 săpt | acoperire achiziții directe completă |
---
## Anexa A — Snapshot scrape_log azi (2026-05-11)
| Schema | Last successful run | OK runs 7d |
|---|---|---:|
| aaas | 2026-05-10 17:51 | 6 |
| aep | 2026-05-09 20:58 | 4 |
| ancom | 2026-05-10 18:06 | 3 |
| anre | 2026-05-10 14:47 | 3 (4 errors) ⚠️ |
| apia | 2026-05-10 18:53 | 1 |
| asf | 2026-05-10 18:19 | 1 |
| cnas | 2026-05-10 18:08 | 67 (multiple PDF parses) |
| cnsc | 2026-05-10 19:19 | 4 |
| gnm | 2026-05-10 19:02 | 5 |
| **seap.wsp_sync_state** | **2026-05-07 03:01** (3 zile stale!) | n/a |
| **seap.sync_state[da]** | **2025-10-16** (208 zile stale!) | n/a |
**Concluzie:** 9 din 11 schemas live au rulat în ultimele 24h. SEAP WSP + DA sunt blind spots — heartbeat trebuie să le acopere explicit.
---
## Anexa B — Quick reference: existing systemd timers (current state)
```
/etc/systemd/system/vreaudigital-anaf-daily.timer → 02:00 daily → enrich-anaf.sh TIER=daily
/etc/systemd/system/vreaudigital-onrc-weekly.timer → Tue 03:00 → import-onrc-fresh.sh
/etc/systemd/system/vreaudigital-mvs.timer → 04:00 daily → refresh-mvs.sh
```
**Recomandare:** păstrează aceste 3 ca-s sunt, adaugă alte 18-20 timere pentru a acoperi celelalte schemas.
---
**Strategy doc complete.** Implementation poate începe imediat cu §7.1 items.
---
## Anexa C — AEP donatii (banipartide.ro): lag pattern confirmat 2026-05-12
**Verificare directă a sursei** (`https://www.banipartide.ro/app/json.php?mode=dt&ssid=<base64-SQL>`):
| Dataset | Total rânduri sursă | Max an pe sursă | DB rânduri | DB max an / max `data_donatie` |
|---|---:|---|---:|---|
| Donatori PJ (Monitorul Oficial 10k+) | 3,612 | **2024** (114) | 3,567 | 2024 / 2024-12-13 |
| Donatori PF (Monitorul Oficial 10k+) | 30,792 | **2024** (1,859) | 30,173 | 2024 / 2024-12-27 |
| RVC (Rapoarte Venituri/Cheltuieli) | 353,473 | **2023** (42,791) | 346,237 | 2023 / 2034-01-31 (erori OCR) |
**Concluzie:** sursa **NU are date 2025 sau 2026**. Ultima rulare a cron-ului (2026-05-11 09:15 satra) a importat deja toate rândurile existente (`seen=3612/30792/353473`). Diferența DB vs sursă (45/619/7236 rânduri) e dată de:
- PJ: 572 rânduri cu `data_donatie IS NULL` (multi-date strings ca `"11.10.2019; 13.11.2019"`) — parser-ul nu reține `an` în acele cazuri.
- PF: similar, 9,268 NULL pe `data_donatie`.
- RVC: 7,236 skip-uri pe upsert (rânduri cu format date neparsabil în limba română, ex. `"septembrie 2019"`).
### De ce nu există 2025/2026 pe sursă
**Mecanism legal (Legea 334/2006 + HG 10/2016):**
- Partidele politice raportează **donațiile peste 10× salariu minim** la AEP, care le publică în **Monitorul Oficial Partea I-A**.
- Termen legal: până la **30 aprilie anul N+1** pentru donațiile anului N (raport anual venituri/cheltuieli).
- Pentru campanii electorale: raportare separată în 15 zile de la finalul campaniei.
- Expert Forum (proiectul banipartide.ro) scanează MO, parsează PDF-urile și actualizează tabelul cca 1-3 luni după publicare.
**Calendar așteptat:**
| Date donații | Raport AEP în MO | Apariție pe banipartide.ro | Estimare disponibilitate gov-agreg |
|---|---|---|---|
| 2024 (anuale) | apr 2025 | mai-aug 2025 | ✅ deja în DB |
| 2025 (anuale) | apr 2026 | **mai-aug 2026** | 🕒 fereastră **acum** (mai 2026) aug 2026 |
| 2026 (anuale) | apr 2027 | mai-aug 2027 | 🕒 mai 2027+ |
| 2024 campanii electorale (PE, prezidențiale, locale, parlamentare) | 15-30 zile post-campanie | 1-3 luni mai târziu | ✅ în DB la `data_donatie` apropiat de turul de scrutin |
**Notă RVC:** Rapoartele anuale de venituri/cheltuieli (RVC) sunt mai lente — 2023 a apărut probabil în 2025. Așteptăm 2024 pe sursă în **iunie-octombrie 2026**.
### Recomandare de cadență (revizuită)
Cron actual `vreaudigital-aep-donatii.timer` = 1 ale lunii la 03:30 (= **lunar**, mai des decât §1 #26 care zicea quarterly). Asta e **OK pentru fereastra mai-august 2026** când e cel mai probabil să apară 2025 — îl prinde la prima rulare.
**Nu schimbăm cadența**. Heartbeat-ul (§4.1) ar trebui să fie tolerant la **95 zile** stale (cum e setat), pentru că între ianuarie-aprilie nu apare nimic nou și asta e normal.
### Next check
Următoarea verificare automată **15 iunie 2026** (~o lună după aceasta) — dacă sursa tot nu publică 2025, alarmă falsă; dacă publică, cron-ul de 1 iulie 03:30 va prinde inserțiile. Verificare manuală opțională: `curl` aceeași SQL ca aici, `python3 -c "..."` pentru count years.