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)
36 KiB
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=2vreaudigital-onrc-weekly.timer→ marți 03:00, import-onrc-fresh.shvreaudigital-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 |
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ă însrc/, dar nu au wrappercron/scrape-*.shcu 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), permitejournalctl -u, status uniform.
Recomandare: B (systemd timers), pentru că:
- Patternul există deja (3 timere), iar
journalctle mai util decât/var/log/cron. - Per-unit
OnFailure=permite alerting nativ. Persistent=truereia rulările pierdute după reboot (cron-ul de pe satra nu are anacron).RandomizedDelaySec=evită contenția în vârful 02:00-06:00.
2.1 Timer skeleton (canonical pattern)
Un template pentru fiecare scraper:
# /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).
#!/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.
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:
# /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
seap.sync_state[source=da].status = pendingdin 2025-10-16 (208 zile!) — DA backfill blocat și nimeni nu primește alert. Trebuie heartbeat dedicat pentrusync_stateșiwsp_sync_statecare alertează dacăupdated_at < now() - 24hsauconsecutive_errors > 5.- 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. - 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
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 pecui.bugetar.executie— gol. Când implementat, UPSERT pe(cif, an, luna, indicator).- TED import (
import_ted.py) —publication-datebug 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-agregGitea — recuperabil în <1 min .infisical-mifiles — 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.shla 03:00/home/bulibasa/backup.shla 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ă:
# /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țineanî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.