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

36 KiB
Raw Blame History

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 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:

# /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
# === 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

  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

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ă:

# /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.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 stallwsp_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.