# SCHEMI ADMIN — Aste Giudiziarie (aste.regla.it / pvp-scraper)

> Documento per uno **sviluppatore** che deve mettere le mani al pannello di controllo e
> alle impostazioni. Tre schemi:
> **1. Flusso dati completo** (con le TRE logiche nuove in corso di implementazione),
> **2. Dashboard / pannello admin** (com'è ora, come sarà),
> **3. Censimento COMPLETO delle impostazioni** (ogni parametro configurabile).
>
> **Verificato sul server di produzione al 20/08/2026** (Hetzner `167.233.25.108` +
> Ubuntu Bari `10.10.0.2` via wg0). Numeri, path, cron e timer sono LETTI dal server, non stimati.
>
> **Stato numeri (20/08):** lotti 293.505 · attive 187.082 · future attive 15.506 ·
> perizie scaricate 15.523 · coda AI pending 1.023 · `ai_ingest_control.paused = 1` (ingest FERMO,
> lo riprende un umano). Per il quadro architetturale completo vedi `DOCUMENTO_TECNICO.md`.

---

## LEGENDA STATO DELLE TRE LOGICHE NUOVE

Le tre logiche descritte come "nuove" NON sono ancora nel codice al 20/08: le sta implementando
un altro agente ORA. Qui sono documentate **come da contratto** (a valle dell'implementazione).
Dove il seme esiste già è indicato. Il file `impostazioni.json` **non esiste ancora** sul server
(verificato: `ls impostazioni.json` → No such file). Va creato dall'implementazione.

| Logica | Stato al 20/08 | Dove aggancia |
|---|---|---|
| (a) Filtro ingest AI da `impostazioni.json` | Seme presente: `caccia_perizie_ai.py:124 perizia_soglia_eurmq()` legge già `periziaEuroMqMin` da `settings.json`. Manca la fascia €/mq zona + prezzo_base + bypass anticipo | feeder AI / `caccia_perizie_ai.py` |
| (b) Retry ciclico `retry_perizie_giorni` | NON presente. Oggi lo sweep è "una tantum per copertura", non un retry ciclico a scadenza | `sweep_copertura.py` / scheduler portali |
| (c) Refresh TOTALE post-sync | **GIÀ ATTIVO in parte**: `aggancio_chain.sh` fa `immo_match ... --all`, `--all` dei vicini/comparabili ogni notte; OMI refresh completo la domenica (`sync_esterni.sh weekly`). Manca solo la garanzia esplicita "nessun per-asta stantio" | `aggancio_chain.sh` + `sync_e_aggancio.sh` |

---

# SCHEMA 1 — FLUSSO DATI COMPLETO (aggiornato)

Riparte dal `DOCUMENTO_TECNICO.md` §3 e vi innesta le TRE logiche nuove nei punti (a)(b)(c).

```
                              PVP  (API Ministero Giustizia)
                                       │  scraper.run  (lista incrementale, stop al watermark)
                                       ▼
                        ┌───────────────────────────────┐
                        │  UPSERT lotti + beni           │  id, prezzo_base, data_vendita,
                        │  data_acquisizione = oggi      │  citta, provincia, num_beni, siti…
                        └───────────────────────────────┘
                                       │  scraper.enrich (dettaglio) → RG, tribunale, url, contatti, L0
                                       │  scraper.group  → attiva/conclusa/gruppo/tentativo/ribasso_pct
                                       ▼
   ┌───────────────────── CANALE DETERMINISTICO (subito, in giornata) ────────────────────┐
   │  ID NUOVI (diff comm):                                                                │
   │    localizza.py --no-geocode --ids NEW     (cascata L0→L4 offline)                    │
   │    popola.py   --ids NEW --force           (immo_*, omi_*, + 5 raggi)                 │
   │  cron di recupero: localizza :17  ·  popola :47 (4x/dì)  ·  zona_favorevole :37 4,16  │
   └──────────────────────────────────────────────────────────────────────────────────────┘
                                       │
   ┌───────────────────── CANALE NOTTURNO (finestre 21:00–08:00 UTC) ─────────────────────┐
   │  portali_scheduler / chain 06:15 :                                                    │
   │    12 portali → resolver (id PVP / RG / prezzo±% / catasto) → perizia_dati (PDF),     │
   │                 allegati_portale, asta_foto, tentativi_storici                        │
   │                                                                                       │
   │  ┌────────── (a) FILTRO INGEST AI  ← impostazioni.json ───────────────────┐           │
   │  │ feeder → PRIMA di accodare in ai_ingest_queue applica il filtro:        │           │
   │  │   PASSA se  eurmq_zona_min ≤ lotti.immo_sale ≤ eurmq_zona_max           │           │
   │  │        AND  prezzo_min ≤ lotti.prezzo_base ≤ prezzo_max                 │           │
   │  │   BYPASS totale se fonte_priorita = 'anticipo_coordinate'              │           │
   │  │        (asta cieca senza coord: la perizia dà le triplette catastali →  │           │
   │  │         localizza L3; il filtro NON deve toglierle la priorità 1e9)     │           │
   │  └─────────────────────────────────────────────────────────────────────────┘         │
   │            │                                                                           │
   │            ▼   ai_ingest_queue                                                         │
   │  ingest_worker (30 thread, Fleet API 'sonnet') → persist_v2 (perizia_lotto2/bene2)    │
   │            → bridge_gestionale (perizia_estratta, ciò che legge il frontend)          │
   │            → localizza+popola per QUELL'id (post-perizia ri-localizza col catasto AI) │
   │                                                                                       │
   │  ┌────────── (d) DOCUMENTO SBAGLIATO → BLACKLIST + rientro in ricerca ─────┐           │
   │  │ Se l'AI dice "questo PDF NON è la perizia dell'asta" (esito fail_non_    │           │
   │  │ perizia / fail_lotto_assente / insufficiente + segnalazione             │           │
   │  │ 'documento_non_pertinente'), l'ingest_worker:                           │           │
   │  │   1) blacklista l'url (perizia_url_scartate) + l'url_orig del portale    │           │
   │  │   2) rimuove il pointer perizia_dati, pulisce lotto2/bene2/estratta      │           │
   │  │   3) rimuove la riga da ai_ingest_queue                                  │           │
   │  │ → l'asta torna "senza perizia" e RIENTRA nel retry ciclico (b): il giro  │           │
   │  │   dopo i motori escludono l'url scartato → promuovono un doc DIVERSO.    │           │
   │  │ NON scarta: 'ok', 'insufficiente' pertinente, 'fail_non_leggibile',      │           │
   │  │ rate-limit / rete / parse-error (transitori: già gestiti con retry).     │           │
   │  └─────────────────────────────────────────────────────────────────────────┘           │
   └──────────────────────────────────────────────────────────────────────────────────────┘

   ┌───────────────────── (b) RETRY CICLICO PERIZIE  ← retry_perizie_giorni (default 5) ───┐
   │  Bersaglio: aste SENZA perizia con TUTTI i canali già esauriti (portale_scan_stato:   │
   │  tutti i portali done/non_trovato, resolver esausti).                                  │
   │  Regola: ogni `retry_perizie_giorni` giorni RIPROVA da capo il giro completo,          │
   │  INCLUSO il ricontrollo del tag PVP (perizie_pipeline.py, tag PERIZ/STIMA sugli        │
   │  allegati dell'annuncio PVP: un allegato può comparire DOPO la prima passata).         │
   │  Non si esaurisce MAI finché  data_vendita ≥ oggi  (vendita futura).                   │
   │  Si ferma solo quando: perizia trovata  OR  l'asta diventa storica (passata).          │
   │                                                                                        │
   │     scoperta ──▶ giro1 (KO) ──[+5gg]──▶ giro2 (KO) ──[+5gg]──▶ … ──▶ perizia / storica │
   └────────────────────────────────────────────────────────────────────────────────────────┘

  ── ANTICIPO PERIZIA (già oggi) ─────────────────────────────────────────────────────────────
  Asta futura senza coordinate (has_coordinate 0/NULL) ma con perizia_dati.url → coda AI a
  PRIORITÀ MASSIMA (priorita=1e9, fonte_priorita='anticipo_coordinate'). Questa fascia
  BYPASSA il filtro (a): la perizia serve proprio a localizzarla via L3 (catasto da perizia AI).
```

## (c) REFRESH TOTALE POST-SYNC — nessun dato per-asta resta stantio

Ogni notte i DB esterni (idealista, immobiliare, booking; OMI la domenica) arrivano da Bari.
Subito dopo, la catena di aggancio **ri-tocca TUTTE le aste attive**, non solo le nuove:

```
  sync_e_aggancio.sh  (cron 02:10 — daily lun-sab; DOMENICA = weekly, aggiunge OMI)
        │
        ├─ sync_esterni.sh   (idealista/booking/immobiliare via wg0; DOMENICA anche omi.db FULL)
        │       └─ se sync KO → catena NON parte (dati vecchi ma coerenti)
        │
        └─ aggancio_chain.sh  [SOLO se sync OK]
             1  build_comparabili_idealista.py   → comparabili.db (idealista+immobiliare)   FULL
             2  hotel_vicini_calc.py             → lotti.hotel_vicini_*        (da booking)  FULL
             3  idealista_vicini_calc.py         → lotti.idealista_vicini_*    (da geo DB)   FULL
             3b immobiliare_vicini_calc.py       → lotti.immobiliare_vicini_*  (comparabili) FULL
             4  immo_merge.py                    → merge raw immobiliare → pvp_immobiliare.db
             5  immo_precalc.py                  → immo_stats
             6  immo_match.py --pvp … --immo … --ALL   → lotti.immo_*  su TUTTE le attive  ◀── (c)
```

- **`--all`** su `immo_match` (step 6) è il cuore di (c): dopo il merge immobiliare **ri-aggancia
  TUTTE** le aste, non un delta. Verificato in `aggancio_chain.sh` (STEP6-IMMOMATCH).
- **OMI**: refresh completo la **domenica** (`sync_esterni.sh weekly` → `omi.db` FULL ~750 MB da
  Bari) + timer mensile Hetzner `omi-aggiorna.timer` (`OnCalendar=*-*-10 04:17`) che rigenera la fonte.
- **vicini/comparabili**: già ricalcolati FULL ogni notte (step 1-3b).

**Invariante (c):** dopo un aggiornamento dei DB esterni, **nessuna colonna per-asta (`immo_*`,
`omi_*`, `*_vicini_*`, `comp_*`) resta stantia** — la catena passa su tutte le attive.

---

# SCHEMA 2 — DASHBOARD / PANNELLO ADMIN

Frontend Express `frontend/server.js` (pm2 **pvp-frontend**, porta **3210**, nginx
`aste.regla.it` → `127.0.0.1:3210`). Le pagine HTML stanno in `frontend/public/`.

## Mappa delle pagine

```
  /pipeline.html   ◀── PAGINA PRINCIPALE (striscia motori + azioni col token)
     │
     ├─ /motori_det.html            sotto-dashboard motori DETERMINISTICI (calcolo/sync)
     ├─ /portali.html               sotto-dashboard 12 portali perizie + resolver + sweep
     ├─ /ingest.html                monitor coda ingest AI (perizie)
     │
     ├─ https://regla.it/idealista/     sotto-dashboard idealista  (Ubuntu, proxy nginx)
     ├─ https://regla.it/immobiliare/   sotto-dashboard immobiliare (Ubuntu, proxy nginx)
     └─ https://regla.it/booking/       sotto-dashboard booking     (Ubuntu, proxy nginx)
```

> Nota: `idealista/immobiliare/booking` NON sono file locali (`ls frontend/public/idealista/` →
> non esiste). Sono su dominio **regla.it** (diverso da aste.regla.it), serviti dal server Ubuntu
> Bari e raggiunti via proxy nginx. Da `pipeline.html` sono link ASSOLUTI (`target=_blank`).

## /pipeline.html — pagina principale

API dati: `GET /api/pipeline/stato` (auto-refresh 30s). Struttura a striscia con **una card per
motore**; ogni card mostra: **in esecuzione?** (`in_corso`), **quando programmato**
(`prossima_esecuzione`), **velocità/volumi** (`dettaglio` per-motore), **problemi**
(semaforo `stato`: ok / warn / crit / pausa / finestra_chiusa).

**Due sezioni:**

| Sezione | Card (nome esatto lato server) |
|---|---|
| **Motori DETERMINISTICI** (calcolo/sync) | PVP scrape · Localizza + Popola · Sync DB esterni · Aggancio + Raggi · Disco (Hetzner) · Ubuntu risorse |
| **Motori SCRAPING / acquisizione** | Portali + Resolver + Sweep · Ingest AI (perizie) · Idealista HTTP · Idealista browser · Immobiliare · Booking (+ infrastruttura linea/ban/mobileproxy) |

**Fallback + azioni (protette dal token).** Barra "Token azioni" (header `X-Dash-Token`, salvato
in `localStorage` del browser, mai sul server in chiaro). Il token vero sta in
`/root/.dashboard_token` (chmod 600). Ogni azione = comando FISSO con `flock -n` (se già in corso →
`gia_in_corso`). Allowlist rigida lato server (`AZIONI` in `server.js`, POST `/api/pipeline/azione`):

| Gruppo | Azioni |
|---|---|
| Fallback / una-tantum | Idealista BROWSER (fallback) · Geocode boost (`--geocode-budget 500`) · Ruota IP dongle 1 (TIM) · Ruota IP dongle 2 (WindTre) |
| Avvio manuale primari | Sync DB esterni + aggancio · Catena perizie · Sweep copertura · Aggancio + raggi · Idealista HTTP · Immobiliare · Booking |
| Inline (in-process) | Metti in pausa / Riprendi ingest AI (scrive `ai_ingest_control.paused`) |

## /motori_det.html — sotto-dashboard deterministici

API: `GET /api/pipeline/deterministici` (refresh 30s). Pannelli e metriche REALI:

| Pannello | Metriche |
|---|---|
| Group / stato aste | aste attive · classificate · ribassate · concluse (storico) |
| PVP scrape — ultime 10 run | fase, avvio, durata, record visti/nuovi/aggiornati, stato |
| Localizza + geocode | orfane residue · geocode ok / fail · distribuzione `coord_fonte` (attive) |
| Popola / raggi — freschezza | per metrica (comp, idealista, immobiliare, hotel, isolamento): valorizzate · fresche (<24h) · % fresche |
| Sync DB esterni + aggancio | storia sync (durata/bytes) · storia aggancio (durata) da `pipeline_salute` |
| Archiviazione storiche | righe archiviate su S3 · ultima run · coda log |

## /portali.html — sotto-dashboard 12 portali

API: `GET /api/pipeline/portali` (refresh 10s). Pannelli e metriche REALI:

| Pannello | Metriche |
|---|---|
| Copertura perizie (grande) | copertura_pct · con perizia · lotti attivi · perizie estratte |
| Tabella 12 portali | per portale: risolto · non trovato · ambiguo · error · scan ok/err · ultimo run · cooldown (attivo/libero) · in finestra (si/no) |
| Ultimo sweep copertura | stato + dettaglio (da `pipeline_salute`) |
| Coda ingest per fascia | conteggi coda per fascia di priorità + totali |
| Catena perizie — ultime 20 righe | tail colorato di `perizie_chain.log` |
| Finestra resolver | APERTA/chiusa (21:00-08:00 UTC) |

## /ingest.html — monitor ingest AI

API: `GET /api/ingest/stato` (refresh 5s) + `POST /api/ingest/pausa`. Pannelli e metriche REALI:

| Pannello | Metriche |
|---|---|
| Card conteggi | pending · running · done · done_fail (scartate doc) · failed |
| Blacklist doc-sbagliato | `perizia_url_scartate` — url scartati perché il documento non era la perizia dell'asta. L'asta viene demota (pointer perizia_dati rimosso) e RIENTRA nel giro perizie: i motori (scegli_perizia / find_perizia / caccia) escludono l'url scartato e promuovono un doc diverso. Log `ingest.log`: riga `[documento-sbagliato]` |
| Barra controlli | stato coda (ATTIVA / IN PAUSA / RATE-LIMIT con `cooldown_until`) · bottone Pausa/Riprendi · **concorrenza 30 thread** (hardcoded nella pagina) · throughput /h e /24h · stima ore rimanenti |
| In lavorazione | ID · comune · prezzo_base · priorità €/mq · fonte_priorita · da (secondi) |
| Prossime 20 (per €/mq) | # · ID · comune · prezzo_base · €/mq zona · fonte_priorita |
| Ultime completate | ID · comune · esito (ok/insufficiente/…) · durata · chiusa · preview |
| Errori recenti | ID · comune · stato · esito · tentativi · errore · quando |

## BUCHI da colmare (il prossimo agente)

| Buco | Cosa manca | Dove aggancia |
|---|---|---|
| **Pannello OMI** | Nessuna sotto-dashboard per OMI (copertura omi_*, ultima quotazione, semestre, esito `omi-aggiorna`). Oggi OMI compare solo come voce del sync, senza pannello dedicato | nuovo `/omi.html` + `GET /api/pipeline/omi` |
| **Pannello mobileproxy** | Nessun pannello per le 2 chiavette LTE (IP corrente per dongle, ultima rotazione, quale pool è instradato via proxy, stato Control API `:8000`). Oggi solo le 2 azioni "Ruota IP dongle 1/2" | nuovo pannello + `GET /api/pipeline/mobileproxy` (via wg0 → `10.10.0.2:8000`) |
| **Pagina impostazioni** | NON esiste alcuna UI per editare i parametri (settings.json, finestre.json, impostazioni.json, engine). Oggi solo endpoint parziali (`/api/settings` limitato a orario+raggi, `/api/engine`). Vedi proposta in SCHEMA 3 | nuovo `/impostazioni.html` + estensione `/api/settings` |

---

# SCHEMA 3 — IMPOSTAZIONI (censimento completo)

Ogni parametro configurabile, per file. **A caldo** = riletto a ogni run/chiamata (nessun riavvio).
**Riavvio** = serve `pm2 reload` / restart del processo. **UI** = esposto in dashboard; **file** =
solo su file.

## 3.1 `settings.json` — /var/www/pvp-scraper/settings.json (VALORE ATTUALE)

```json
{ "scrapeHour": 4, "scrapeMinute": 30, "enabled": true,
  "isolamentoRaggioM": 500, "hotelRaggioM": 500, "idealistaRaggioM": 500,
  "periziaEuroMqMin": 1500, "immobiliareRaggioM": 500 }
```

| Parametro | Valore | Effetto | A caldo? | UI? |
|---|---|---|---|---|
| `scrapeHour` / `scrapeMinute` | 4 / 30 | Orario giro scrape PVP (letto da pm2 `pvp-scheduler`) | A caldo (scheduler rilegge) | **UI** (`POST /api/settings`) |
| `enabled` | true | Abilita/disabilita il giro scrape automatico | A caldo | **UI** |
| `isolamentoRaggioM` | 500 | Raggio (m) metrica isolamento (annunci entro X m) | A caldo | **UI** (isolamento POST salva qui) |
| `hotelRaggioM` | 500 | Raggio (m) hotel_vicini (booking) | A caldo | **UI** parziale |
| `idealistaRaggioM` | 500 | Raggio (m) idealista_vicini | A caldo | **UI** parziale |
| `immobiliareRaggioM` | 500 | Raggio (m) immobiliare_vicini | A caldo | **file** (non nel POST) |
| `periziaEuroMqMin` | 1500 | Soglia €/mq (immobiliare) sopra cui un'asta va in perizia AI. Letta da `caccia_perizie_ai.py:124` e `perizie_pipeline.py:18` (default 1500 se manca) | A caldo | **file** (non nel POST) |

> **Nota UI:** `POST /api/settings` accetta oggi SOLO `scrapeHour/Minute/enabled` + i 3 raggi
> iso/hotel/idealista. `immobiliareRaggioM` e `periziaEuroMqMin` esistono nel file ma **non sono
> editabili da UI**: si cambiano a mano nel JSON (a caldo). Range validati UI: raggi 0-20000.

## 3.2 `finestre.json` — finestre notturne per motore (VALORE ATTUALE)

```json
{ "_default":          {"inizio":"22:00","fine":"07:00","attivo":true},
  "ingest_ai":         {"inizio":"22:00","fine":"07:00","attivo":true},
  "portali_scheduler": {"inizio":"21:00","fine":"08:00","attivo":true},
  "resolver_chain":    {"inizio":"21:00","fine":"08:00","attivo":true} }
```

| Parametro | Effetto | A caldo? | UI? |
|---|---|---|---|
| `<componente>.inizio/fine` | Finestra oraria (UTC) in cui il motore pesante può girare. `inizio>fine` = attraversa mezzanotte | **A caldo** (`finestra.in_finestra()` rilegge a ogni chiamata) | **file** |
| `<componente>.attivo` | `false` → componente SEMPRE aperto (nessun vincolo) | A caldo | **file** |

Componenti gestiti: `ingest_ai`, `portali_scheduler`, `resolver_chain` (+ `_default`).
In dubbio (config rotta) → CHIUSO (conservativo). La **pausa utente ingest**
(`ai_ingest_control.paused`) resta **sovrana** e indipendente dalla finestra.

## 3.3 `impostazioni.json` — NUOVO (da creare, NON esiste al 20/08)

File proposto dalle 3 logiche nuove. Schema atteso:

| Parametro | Default | Effetto | A caldo? | UI? |
|---|---|---|---|---|
| `eurmq_zona_min` / `eurmq_zona_max` | (da definire) | Fascia €/mq zona (`lotti.immo_sale`): filtro ingest AI (a). Fuori fascia → non accodata | A caldo (riletto dal feeder) | **file** (poi UI) |
| `prezzo_min` / `prezzo_max` | (da definire) | Fascia prezzo_base per il filtro ingest AI (a) | A caldo | **file** (poi UI) |
| `retry_perizie_giorni` | **5** | Ogni N giorni riprova il giro perizie completo (b) per aste future senza perizia coi canali esauriti, incluso ricontrollo tag PVP | A caldo | **file** (poi UI) |
| `geocode_budget_orario` | 60 (oggi hardcoded nel cron) | Budget geocode Nominatim/ora. Oggi è `localizza.py --geocode-budget 60` nel cron `:17`; il boost manuale usa 500 | A caldo (se spostato in file) | **file** (poi UI) |

> Bypass del filtro (a): la fascia `anticipo_coordinate` (aste cieche, priorita 1e9) NON deve
> mai essere filtrata. Va gestito nel feeder prima di applicare min/max.

## 3.4 Altri file di configurazione

| File | Valore attuale | Effetto | A caldo? | UI? |
|---|---|---|---|---|
| `perizie_engine.txt` | `sonnet` | Motore AI ingest. Whitelist `{sonnet, gpt-5.6-luna}`; valore ignoto → sonnet | **A caldo** (worker + frontend rileggono) | **UI** (`POST /api/engine`) |
| `tuning.json` | `workers=8, throughput=96.01, pool=800` | Ultimo auto-tuning workers scrape, scritto da `scraper/config.py` | Auto (rigenerato) | **file** (informativo) |

## 3.5 Costanti / cap configurabili negli SCRIPT (hardcoded — si cambia in-file + reload)

### ingest_worker.py (`/root/ingest_worker.py`)

| Costante | Valore | Effetto |
|---|---|---|
| `POOL` | **30** | Thread pool = concorrenza globale ingest AI (la pagina ingest.html mostra "30 thread") |
| `GPT_POOL` | 3 | Sotto-pool concorrenza quando engine è un GPT (throttle) |
| `SLEEP_EMPTY` | 60 s | Coda vuota → chiama feeder e dorme |
| `RETRY_MAX` | 2 | Retry su PARSE_ERR/rete prima di `failed` |
| `STALE_RUNNING_MIN` | 30 min | `running` più vecchi → tornano `pending` (worker morto a metà) |
| `COOLDOWN_STEPS` | `[10,20,40,60]` min | Escalation cooldown su rate-limit AI |
| `ENGINES_OK` | `{sonnet, gpt-5.6-luna}` | Whitelist motori |

### portali_scheduler.py — profili per-portale (concorrenza / pausa / circuit-breaker)

| Portale | conc | pausa (s) | perizia | CB ban_threshold / cooldown_sec |
|---|---|---|---|---|
| astegiudiziarie.it | 1 | 9.0 | sì | 2 / 2700 (il più severo) |
| astetelematiche.it | 8 | 0.0 | sì | 4 / 1200 |
| spazioaste.it | 5 | 0.2 | sì | 3 / 1500 |
| astalegale.net | 5 | 0.2 | sì | 3 / 1500 |
| fallcoaste.it | 5 | 0.3 | sì | 3 / 1500 |
| EDICOM (pool logico) | 1 | 1.8 | sì | 3 / 1800 |
| garavirtuale.it | 4 | 0.6 | no | (default) |
| venditegiudiziarieitalia.it | 3 | 1.0 | no | (default) |
| gobidreal.it | 0 | 0.0 | no | (default) |

Altre costanti scheduler: `CDN_CAP=4` (semaforo aggregato spazioaste+astalegale, stesso CDN) ·
`BACKOFF_SEC=60` (linea giù) · `MAX_BLOCCHI_CONSECUTIVI=3` · `PAUSA_POOL_SEC=600` (fallback pool
senza CB) · `MP_DONGLE_PRIMARIO=2` (WindTre), `MP_DONGLE_FALLBACK=1` (TIM), `MP_MIN_PROBE_FALLITI=1`.
Da riga di comando: `--max N` (tetto aste/giro; la chain 06:15 usa `--max 400 --retry-errors`),
`--dry-run`, `--test N`.

### sweep_copertura.py — cap giornalieri per-portale (id accodati/giorno, rate-aware)

| Portale | cap/gg |
|---|---|
| astegiudiziarie | **60** (rate 9s) |
| astetelematiche | 120 |
| fallcoaste | 150 |
| spazioaste / astalegale | 120 (conc 3) |
| garavirtuale | 120 (conc 2) |
| gobidreal | 40 |
| venditegiudiziarieitalia | 120 (conc 2) |
| EDICOM | 120 |

Soglia sweep: ri-accoda ogni portale che cita l'asta da >48h; `--test N` per giri piccoli.

### astegiudiziarie_resolver.py

| Costante | Valore | Effetto |
|---|---|---|
| `MAX_DATA_IDS` | 60 | Non scaricare `/Data` per più di N candidati (safety) |
| (resolver delay) | ≥9.0 s/req | Delay hardcoded anti-ban (sequenziale) |

### garavirtuale_resolver.py

| Costante | Valore | Effetto |
|---|---|---|
| `CACHE_TTL` | 7 giorni | TTL cache sitemap XML |

### watchdog_pipeline.py — soglie disco / WAL / enrich

| Costante | Valore | Effetto |
|---|---|---|
| `DISK_WARN_GB` | 2.0 | Sotto → STOP_PORTALI (freno scheduler) |
| `DISK_CRIT_GB` | 1.2 | Sotto → anche PAUSA ingest AI |
| `DISK_OK_GB` | 2.6 | Sopra → rilascia il freno-disco (isteresi) |
| `WAL_MAX_BYTES` | 200 MB | Sopra → `wal_checkpoint(TRUNCATE)` |
| `ENRICH_MAX_SEC` | 2 h | Processi popola/zone/immo più vecchi → SIGTERM |
| `NO_PROGRESS_MIN` | 30 min | Stallo scheduler → graceful stop + retry |
| `STUCK_RUN_HOURS` | 1 h | `running` orfani in portale_scan_stato → `err` |
| `JOURNAL_VACUUM_EVERY_S` | 7 gg | `journalctl --vacuum-size=200M` settimanale |

### pvp_backup_s3.py — backup notturno

| Costante | Valore | Effetto |
|---|---|---|
| lifecycle daily | 30 gg | `backups/<nome>_<YYYYMMDD>.db.zst` |
| lifecycle monthly | 12 mesi | `backups/monthly/…` il giorno 1 del mese |
| `DB_LIST` | 10 DB caldi | pvp, omi, comparabili, pvp_immobiliare, demografia, idealista_geo(+mobile), zone_history, caccia_perizie, geo_cache. **Esclusi**: catasto.db, booking.db, idealista.db |
| scratch libero | ~2x dim. DB | serve per `VACUUM INTO` |

### archivia_storiche.py

| Costante | Valore | Effetto |
|---|---|---|
| guardia disco | <3 GB liberi → esce | Evita di riempire il disco durante l'archivio |
| batch snellimento | 20.000 id | Snellimento a batch delle colonne pesanti |

## 3.6 TIMER systemd e CRON (ricorrenze su entrambi i server)

### CRON Hetzner (`crontab -l`, orari UTC)

| Ricorrenza | Job |
|---|---|
| `17 * * * *` | `localizza.py --geocode-budget 60` (cascata + geocode orario) |
| `47 3,9,15,21` | `popola.py` (immo/zona/omi) |
| `37 4,16` | `zona_favorevole_calc.py` (comp_*) |
| `15 6 * * *` | `perizie_fallback_chain.sh` (portali+resolver+sweep+feeder) |
| `10 2 * * 0` | `sync_e_aggancio.sh weekly` (domenica, +OMI) |
| `10 2 * * 1-6` | `sync_e_aggancio.sh daily` (lun-sab) |
| `*/15 * * * *` | `watchdog_pipeline.py` |
| `@reboot` (+60s) | `watchdog_pipeline.py` (ripresa post-boot) |
| `5,20,35,50 * * * *` | `healthcheck_ubuntu.py` (sorveglia Bari) |
| `20 3 * * *` | `pvp_backup_s3.py` (10 DB → S3) |
| `0 5 2 * *` | `archivia_storiche.py` (storiche → S3) |

### TIMER systemd Hetzner

| Timer | OnCalendar | Cosa fa |
|---|---|---|
| `omi-aggiorna.timer` | `*-*-10 04:17` (mensile) | `omi_aggiorna.sh` (rigenera quotazioni OMI) |

### PM2 Hetzner (sempre attivi)

| Processo | Script | Ruolo |
|---|---|---|
| `pvp-frontend` | `frontend/server.js` (:3210) | Frontend + API dashboard |
| `pvp-scheduler` | `frontend/scheduler.js` | Avvia scrape all'orario di settings.json, raccoglie "avvia ora" |
| `ingest-worker` | `/root/ingest_worker.py` | Worker AI ingest perizie (30 thread) |

### TIMER systemd Ubuntu Bari (`10.10.0.2`)

| Timer | OnCalendar | RandomizedDelay | Cosa fa |
|---|---|---|---|
| `idealista-http.timer` | `*-*-01,15 04:00` | 7200 s | Batch API mobile idealista (primario) |
| `immobiliare-scraper.timer` | `*-*-08,22 04:00` | 7200 s | Crawl prezzi immobiliare.it (browser patchright) |
| `booking-prezzi.timer` | `*-*-* 04:30` (giornaliero) | 1200 s | Ciclo prezzi Booking |

### CRON Ubuntu Bari

| Ricorrenza | Job |
|---|---|
| `23 */6 * * *` | `refresh_idealista_geo.sh` (geocoding progressivo annunci idealista) |

### mobileproxy (Ubuntu Bari, `mobileproxy.service`)

- Config: `/opt/mobileproxy/etc/config.json`. Proxy per-dongle su porte **8080-8085**,
  Control API rotazione su **:8000**. 2 chiavette: dongle1=TIM, dongle2=WindTre (primario 2).
- Rotazione IP solo in **ban-recovery** (probe ancora 429/302, oppure ban_globale ≥3 pool).

## 3.7 File `.env` / segreti (SOLO nome + scopo — MAI contenuti)

| File | Scopo |
|---|---|
| `/root/.s3_pvp.env` | Credenziali S3 Hetzner Object Storage (chmod 600) |
| `/root/.mobileproxy.env` | Token Control API mobileproxy |
| `/root/.gobidreal.env` | Login gobidreal.it (GOBID_USER/PASS) per PDF gated |
| `/root/.dashboard_token` | Token azioni dashboard (header X-Dash-Token, chmod 600) |
| `/root/.uff_pw` | Password SSH verso Ubuntu Bari (usata da sshpass in sync_esterni.sh) |
| `/root/perizie_apikey_v2.txt` | Token Fleet API per il worker AI (mai stampato) |

## 3.8 PROPOSTA — pagina impostazioni unificata `/impostazioni.html`

Una sola pagina che legge/scrive i file di config via API estese. Controlli suggeriti:

| Sezione | Parametro | Controllo UI | File |
|---|---|---|---|
| Scrape | scrapeHour/Minute · enabled | time-picker + toggle | settings.json |
| Raggi metriche | isolamento / hotel / idealista / **immobiliare** RaggioM | 4 slider 0-20000 m | settings.json |
| Perizie | **periziaEuroMqMin** | number (€/mq) | settings.json |
| Filtro ingest AI (nuovo) | eurmq_zona_min/max · prezzo_min/max | 2 range-slider (double thumb) + nota "anticipo_coordinate bypassa" | impostazioni.json |
| Retry perizie (nuovo) | retry_perizie_giorni | number (giorni, default 5) | impostazioni.json |
| Geocode (nuovo) | geocode_budget_orario | number (req/h, default 60) | impostazioni.json |
| Finestre notturne | ingest_ai / portali_scheduler / resolver_chain: inizio/fine/attivo | 3 righe con 2 time-picker + toggle | finestre.json |
| Motore AI | perizie_engine | select whitelist {sonnet, gpt-5.6-luna} | perizie_engine.txt |
| **Solo lettura** (informativi) | cap/soglie scheduler·sweep·watchdog·backup | tabella read-only con link al file | script .py |

**Principi UI (per l'implementatore):** ogni scrittura conferma o fallisce RUMOROSAMENTE (mostra
il valore riletto dal server dopo il POST, mai "salvato" ottimistico); validazione range lato
server; i parametri "a caldo" applicano subito, quelli che richiedono reload lo dichiarano
esplicitamente; le costanti hardcoded degli script restano read-only (cambiarle = deploy).

---

*Fine documento. Verificato su 167.233.25.108 + 10.10.0.2 al 20/08/2026. Le tre logiche nuove
(filtro ingest, retry ciclico, refresh totale) sono documentate come da contratto: parte (c) è già
attiva in `aggancio_chain.sh`; (a) ha il seme in `caccia_perizie_ai.py`; (b) e `impostazioni.json`
sono da implementare.*
