# VRP - B200 - Architettura & Linee guida

**Stato:** APERTO 2026-07-02 (pomeriggio). Segue [VRP_B100_analisi](VRP_B100_analisi.md).
Documento vivo: si corregge mentre si costruisce.

---

## 0. Servizi della piattaforma

Tutti condividono **la stessa identita' e sessione** (login/logout comune):

- **VRV** - diario di viaggio
- **foto / leNostre** - archivio + slideshow
- **wiz_foto** - wizard import foto -> leNostre (gia' LAN-only)
- **VRS / share** - condivisione (futuro)
- (candidato: maps)

Oltre a auth/DB/log/util, la piattaforma offre **meta-utility trasversali** riusate da
tutti i servizi: la **ricerca foto** (`vrn_foto_search`, sull'indice `lenostre.foto`) e
il **motore immagini** (`vrp_img`: resize/cache/EXIF on-the-fly). Il motore immagini
segue il pattern **motore puro + cappelli**: `vrp_img_serve`/`vrp_img_resolve` non sanno
nulla di radici o accessi; ogni servizio mette un **cappello** sottile che sceglie la
radice e le regole (maps/leNostre pubblico; ANW e Nextcloud LAN-only + cache privata).
`/srv/http/maps/img.php` resta il cappello storico (per ora intatto).

---

## 1. Linee guida di scrittura (FERME)

1. **Niente guardie difensive.** E' codice NUOVO e sappiamo cosa facciamo.
   - Definizioni **uniche e documentate** di variabili, costanti, funzioni.
   - Vietato `if (!defined(...))`, `if (!function_exists(...))` e simili.
   - **Tutte** le `define`/costanti stanno nei **file di piattaforma** (un
     manifesto unico, vedi `platform/boot/defines.php`).
   - Se a runtime salta un *not defined* o *already defined* -> **si corregge nel
     file di piattaforma**, non si mette una guardia. Fail-loud, non fail-safe.

2. **Codice essenziale e pulito.** Poche righe, chiare. Stile anni '80: conta il
   KB, non il framework. Una responsabilita' per file.

3. **Commenti estraibili per documentazione/manuali.** Ogni funzione pubblica ha
   un blocco doc in testa, con tag, cosi' un generatore puo' ricavarne
   documentazione e manuali. Convenzione:

        /**
         * @desc   Cosa fa, in una riga.
         * @param  string $x  descrizione
         * @return array      descrizione
         * @manual sezione: Sessione   (per il manuale utente, opzionale)
         */

   Un `docs/genera_doc.php` cammina `platform/lib/` ed emette markdown/HTML.

4. **Naming:** vedi la sezione *Naming convention* qui sotto. In breve: trattino
   nei nomi file e nelle classi CSS, underscore negli identificatori PHP/JS,
   UPPER_SNAKE per le costanti, zero stili inline.

5. **La parola "bootstrap"** indica al massimo un *framework HTTP* - MAI il nostro
   avvio, che e' `3_2_1_Go.php`.

6. **Configurazione = JSON** (stile `meta/version.json`), non `config.php`.

7. **Documenti in ASCII puro:** accenti con l'apostrofo (`e'`, `piu'`, `perche'`),
   niente trattini lunghi o caratteri Unicode (evita il mojibake).

---

## 1b. Naming convention

**Prefisso = servizio.** Tre lettere `vr?`:

| prefisso | servizio |
|----------|----------|
| `vrp` | piattaforma (VittRos Platform), condiviso |
| `vrv` | vittrosviaggi (diario) |
| `vrs` | vittrosshare (share) |
| `vrw` | wizard foto (wiz_foto) |
| `vrn` | leNostre / foto |

**Regola d'oro: il separatore dipende dal linguaggio.** Il trattino `-` NON e'
valido negli identificatori PHP/JS (`$vrp-x` = sottrazione!). Quindi:

| Artefatto | Regola | Esempio |
|-----------|--------|---------|
| File PHP di piattaforma | `vrp_nome.php` (underscore) | `vrp_auth.php`, `vrp_session.php` |
| File CSS / JS | `vrp-nome.css` (trattino) | `vrp-doc.css`, `vrp-media.js` |
| Funzioni PHP | `vrp_nome()` snake_case | `vrp_login()`, `vrp_db_conn()` |
| Variabili PHP | `$vrp_nome` snake_case | `$vrp_user`, `$vrp_cfg` |
| Costanti | `VRP_NOME` UPPER_SNAKE | `VRP_SESSION_TTL`, `VRP_DB_AUTH` |
| Classi PHP (se servono) | `VrpNome` PascalCase | `VrpSession` |
| Classi / id CSS | `.vrp-nome-*` (trattino) | `.vrp-doc-sidebar` |

**Condiviso vs specifico:** `vrp_*` sta nella piattaforma (comune a tutti);
`vrv_* / vrs_* / vrw_* / vrn_*` e' specifico del servizio. Una sessione condivisa
e' `vrp_session_start()`; una lista-post del diario e' `vrv_post_list()`.

**Costanti:** tutte nel manifesto `platform/boot/defines.php` (coerente col
principio "niente guardie": una define, un posto solo).

---

## 2. Architettura delle directory (PROPOSTA)

Principio-chiave: **separare cio' che e' servito dal web da cio' che e' interno.**
Gli interni della piattaforma stanno FUORI dalla web-root (o protetti LAN-only
come `/wiz_foto`). Il 403 di oggi su `/lab/` e' il promemoria giusto.

        lab/                         culla VRP (casa definitiva: da decidere)
        |- docs/                     documenti + manuali generati
        |- index.php                 lettore dei documenti (md -> html)
        `- platform/                 il cuore VRP  (NON servito direttamente)
           |- boot/
           |  |- 3_2_1_Go.php         avvio unico dell'app
           |  |- defines.php          TUTTE le define/costanti (sorgente unica)
           |  `- env.json             config per ambiente (channel/base_url/debug)
           |- lib/                    librerie PHP (1 responsabilita' per file)
           |  |- auth.php             identita' condivisa: login/logout
           |  |- session.php          sessione con timeout (da leNostre50)
           |  |- db.php               connessioni (una per DB/servizio)
           |  |- media.php            foto/thumb (img responsivo)
           |  `- ...
           |- ajax/                   endpoint AJAX sottili (chiamano lib/)
           `- http/                   (eventuale) routing/micro-framework HTTP
        public/  (o web/)            SOLO cio' che e' servito dal web
           |- js/  css/  assets/
           `- index.php               front controller -> include boot/3_2_1_Go.php

   (Nomi `platform/` vs `vrp/`, `public/` vs `web/`: da confermare col Mastro.)

---

## 3. Identita', login, sessione

- **Login comune:** si parte dalla pagina di **VRV** (`login.php`).
- **Logout comune:** si parte dalla pagina di **leNostre50**.
- **Sessione con timeout (FATTA - erede di leNostre50, rafforzata):** il timeout
  vive in `vrp_session_check()` (`platform/lib/vrp_auth.php`), non in un file
  `vrp_session.php` a se'. Controllo server-side autoritativo su `vrp_last`
  (copre anche gli AJAX) + meta-refresh come comodita' UX per le schede ferme.
  Il meccanismo completo (battito `beat=1`, rimbalzo, i due regimi 1-tab vs
  multi-tab) e' spiegato sotto in **3a. Timeout di sessione**.
- **Tutti i servizi** (VRV, foto/leNostre, **wiz_foto**, share) usano la **stessa**
  identita' e sessione.

## 3a. Timeout di sessione (come funziona, chiaro-chiaro)

FATTO 10/07 (CR-010). Qui il meccanismo per non ricascarci. TTL = costante
`VRP_SESSION_TTL` (da `env.json` -> `session.idle_timeout`, oggi **900s = 15 min**).

Due pezzi che collaborano:

1. **Controllo server-side (autoritativo)** = `vrp_session_check()` in
   `platform/lib/vrp_auth.php`, chiamato a OGNI boot da `3_2_1_Go.php` prima di
   ogni output. Logica: se `(ora - vrp_last) > TTL` -> `vrp_logout('timeout')`;
   altrimenti rinnova (`vrp_last` in sessione + `last_activity` nel DB). Copre
   anche le chiamate AJAX (a differenza del solo meta-refresh). E' LUI che decide.

2. **Meta-refresh lato client (comodita' UX)** = in `public/index.php`, ogni
   pagina loggata scrive
   `<meta http-equiv="refresh" content="N;url=?p=logout&t=1&beat=1">` con
   `N = (TTL - inattivita') + 5s`. Serve perche' una scheda FERMA non fa
   richieste: senza il meta, per quella scheda il server non "scatterebbe" mai.
   Quando N scade, la scheda si auto-manda al logout.

**Il rimbalzo "sei ancora vivo"** (`platform/pages/logout.php`): se arrivi al
logout per timeout (`t=1`) ma in realta' hai lavorato ALTROVE (idle < TTL, perche'
un'altra pagina/servizio ha gia' rinnovato `vrp_last`), NON ti slogga: ti rimbalza
a `?p=home&beat=1`. Cosi' non vieni buttato fuori mentre eri attivo su un altro
servizio/scheda.

**Il BATTITO (`beat=1`) - la chiave anti-immortalita':**
- Il meta-refresh e il suo rimbalzo sono marcati `beat=1`.
- `vrp_session_check()` con `beat=1`: **controlla** il timeout (e slogga se
  scaduto) ma **NON rinnova** `vrp_last`/`last_activity`.
- Perche': senza, il battito conterebbe come ATTIVITA'. Solo l'attivita' VERA
  (navigazione, lavoro nei servizi) deve tenere viva la sessione.

**I due regimi (perche' prima sembrava incoerente):**
- **1 sola scheda:** c'e' un solo meta-refresh; scade a idle = TTL+5 -> idle > TTL
  -> logout. Funzionava gia' prima del fix (esperimento #84: morta a +905s).
- **2+ schede sfasate (>5s):** PRIMA del fix erano IMMORTALI. Il rimbalzo di una
  scheda (`logout -> home`) veniva contato come attivita' e rinnovava il timer;
  due timer sfasati si passavano il testimone all'infinito (esperimento #88: viva
  da un giorno). COL fix, i battiti sono passivi: nessuno rinnova, l'idle cresce,
  e la prima scheda che batte dopo il TTL slogga tutti. Morte a
  ~ultima-attivita'-VERA + TTL.

**Nota collegata - le righe-AZIONE non si corrompono:** le azioni utente
(`vrp_insert_action`, registro opzione A) sono righe-EVENTO nella stessa tabella
`sessions`. Nascono gia' CHIUSE (`ended_at=NOW()`), e sia il bump di
`vrp_session_check` sia `vrp_logout` agiscono solo su
`ended_at IS NULL AND action='login'` -> non riscrivono piu' le azioni a
'timeout'. Vedi [VRP_DB_catalogo](VRP_DB_catalogo.md) (tabella `sessions`).

---

## 3b. La cornice: navigazione e amministrazione (manifesti)

**Idea portante (2026-07-04).** Ogni servizio si *registra* alla piattaforma con un
piccolo **manifesto** (un JSON, coerente con "config=JSON"): nome, slug, icona,
permesso richiesto, URL del servizio, URL del suo pannello admin. E' l'evoluzione
del registro `services` -> `VRP_SERVICES` gia' introdotto (config federata 03/07).

> La piattaforma legge tutti i manifesti e costruisce da sola due cose: la barra di
> navigazione e il menu dell'amministrazione. Aggiungere un servizio domani =
> depositi un manifesto, e compare da solo. Nessun cablaggio a mano. E' il supervisore
> config-driven che registrava i moduli in shared memory, riportato a galla.

**La piattaforma dipinge la cornice sulle pagine dei servizi (2026-07-07).** Le pagine di
un servizio ospitato non cablano nulla: al boot, dopo la guardia, la piattaforma
bufferizza il loro output e vi inietta la **cornice comune** - lo **sfondo**
(`vrp_service_bg`, da manifest.wallpaper) e il bottone **Esci** (`vrp_service_esci`, ->
`vrp_url('public/?p=logout')`, solo per utente loggato). Stessa **guardia salva-binari**:
inietta solo in pagine HTML col tema (dopo il `<link .css>` o prima di `</head>`); CSV,
download e immagini (es. i thumbnail di nc_thumb) restano INTATTI.

        VRP  (la casa: config JSON, utils, DB, auth, login/logout)
         |
   +-----+------ shell condivisa (livrea camper) ------+-----+
   |        barra di navigazione + app-switcher              |
   |     (mostra SOLO i servizi a cui hai diritto)           |
   |                                                          |
 [VRV]        [leNostre]        [share]        [Admin VRP]
  |  \           |  \             |               |
 uso  admin     uso  admin       uso  admin      utenti / permessi
 del  suo       del  suo         del  suo        sessioni+IP / config
 srv  pannello  srv  pannello    srv  pannello   (trasversale)

**Due altezze di amministrazione** (cosi' il dilemma "console unica o un'admin per
servizio?" si scioglie da solo):

- **Altezza 1 - admin di PIATTAFORMA (trasversale):** utenti, permessi-servizi,
  registro del portinaio (sessioni + IP), config globale. Roba che non e' di nessun
  servizio perche' e' di tutti. **Una console sola**, qui ci stanno le sezioni
  espandi/chiudi.
- **Altezza 2 - admin di ogni SERVIZIO (specifico):** post/categorie di VRV, indice
  foto di leNostre, link di share. Intrinsecamente del servizio: **resta DENTRO il
  servizio**. Se la piattaforma la inghiottisse dovrebbe sapere tutto di tutti -
  l'esatto contrario della federazione ("ogni uscio il suo fascicolo").

**Navigazione / passaggio tra servizi:** un **app-switcher** fisso nella shell
condivisa, riempito dai permessi dell'utente (mostra SOLO i servizi a cui ha diritto).
Login unico -> salto istantaneo, niente ri-autenticazione. Non sei mai in un servizio
"nudo": la cornice della piattaforma e' sempre sopra, cosi' sai dove sei e come uscire.
La "pagina per navigare" *e'* questa cornice.

**Modello scelto (istinto di vitti):** **A - portale con inquilini.** L'admin di
piattaforma e' un atrio; da li' apri il pannello admin del singolo servizio, che pero'
indossa la **livrea** e la barra comuni ([[project-livrea-admin]]). Servizi
indipendenti, si evolvono da soli. (Scartato **B - cruscotto unico a fisarmonica**:
la piattaforma dovrebbe conoscere le viscere di tutti -> accoppiamento stretto, col
tempo un cassetto delle cianfrusaglie.)

**APERTO (da riprendere - la colazione ha chiamato 04/07):**
1. Il servizio vive **dentro** la cornice (barra sempre sopra, contenuto sotto, stile
   app unica) o e' un **sito a se'** con solo un link "torna alla piattaforma"?
2. Permessi: basta "usi il servizio si'/no" oppure servono i **gradi** dentro il
   servizio (Elena *scrive* su VRV ma solo *guarda* leNostre)? Il ruolo sta sulla
   **porta di casa** o su **ogni uscio**?

---

## 4. Base dati (PROPOSTA - da confermare)

**Modello: identita' accentrata, contenuti federati** (un solo server MariaDB,
piu' database = piu' contesti; JOIN cross-DB possibili). L'auth e' UNA sola
sorgente di verita' (`vrp_auth`), altrimenti il login comune e' impossibile.

**Il catalogo completo dei database** - tabelle, scopo, credenziali, migrazioni -
sta nel documento dedicato: [VRP_DB_catalogo](VRP_DB_catalogo.md).

In breve (verificato 02/07): l'indice foto ESISTE gia' (`lenostre.foto`, 35k
righe, con `lat`/`lon` e flag `anw`), mantenuto dal wizard -> la ricerca su DB
e' un frutto maturo. Gli utenti oggi sono sdoppiati (`vittrosviaggi.utenti` +
`photo.users`) -> da unificare in `vrp_auth`.

---

## 5. Prossimi passi (concreti)

- [x] leggere `leNostre50/search50.php` (sessione = meta-refresh; search = filesystem)
- [x] modello DB confermato (identita' accentrata) e naming `platform`/`public`
- [ ] leggere `vv/login.php` + `logout50.php` -> definire il flusso login/logout comune
- [x] scrivere `platform/boot/vrp_defines.php` (manifesto define) + `3_2_1_Go.php`
      (avvio) + `env.json` (config minima) - FATTO 03/07: boot pulito, zero
      guardie, fail-loud (chiavi obbligatorie), lint+boot CLI OK
- [x] portati dal vecchio launcher, ripuliti in convenzione VRP (03/07):
      `lib/vrp_log.php` (logging guidato da VRP_LOG_LEVEL, erede del guardian) +
      `lib/vrp_util.php` (vrp_h/url/asset_url/client_ip/is_lan/csrf); il launcher
      ora avvia anche la sessione e carica log+util+db. NON portati (rimandati a
      vrp_auth/vrp_session): auth.php, roles.php, perm.php, guard_admin.php.
- [x] pagina di configurazione (03/07): edita env.json, tutte le voci,
      validazione+backup+ripristino, guardia LAN + CSRF. Collaudata via CLI.
- [x] FRONT CONTROLLER `public/index.php` (03/07): unica porta web, tabella
      rotte in whitelist (home/search/config), guardie per-rotta (config=LAN),
      livrea comune (nav+testata) via `public/css/vrp-lab.css` (classi vrp-*,
      zero stili inline). Viste spostate in `platform/pages/` (FUORI da public,
      raggiungibili solo dal FC). Vecchie pagine standalone -> redirect 302.
      `VRP_PAGES_DIR` aggiunta al manifesto. Collaudo HTTP reale (php -S + router
      che simula l'IP): home 200, config 200 da LAN / 403 da fuori + link
      nascosto, 404 su rotta ignota, shim 302, css 200.
- [x] schema env.json arricchito (03/07): sezioni `paths` (shared_dir/media_root/
      media_url) e `db` (registro nome->file creds, i SEGRETI restano nei file, nel
      JSON solo il puntatore). Manifesto: VRP_SHARED_DIR/MEDIA_ROOT/MEDIA_URL +
      VRP_DB_CREDS costruita dal registro (tolta la const da vrp_db.php: una define,
      un posto solo). `vrn_foto_url` usa VRP_MEDIA_URL. Config page: sezioni Percorsi
      e Database editabili, con validazione (is_dir/is_readable). Collaudato.
- [x] guardia anti-accesso-diretto nelle viste (03/07): se aperte senza boot
      (`!defined('VRP_BOOT_DIR')`) rimbalzano 302 al FC. Glossario in
      [VRP_acronimi](VRP_acronimi.md).
- [ ] **DA FARE (sicurezza): chiudere `/lab/platform/` in nginx.** Oggi e' servito
      pubblicamente -> `env.json` scaricabile da Internet (200). Serve `sudo` (vitti).
      Blocco da aggiungere nel server di `/etc/nginx/conf.d/99-vittros.conf`:

          # VRP Lab: gli interni della piattaforma NON si servono dal web
          location ^~ /lab/platform/ { deny all; }

      Lascia aperti /lab/public/ (front controller + css) e /lab/ (lettore doc).
      FATTO 03/07: aggiunto in ENTRAMBI i server (:80 default + :443 duckdns);
      env.json ora 403 su entrambi. -> ora i secrets nel JSON sono percorribili.
- [x] config federata per servizio (03/07): env.json ha un registro `services`
      (nome -> file, come il registro DB), risolto in VRP_SERVICES; ogni servizio
      ha il SUO file in `platform/config/<nome>.json`. La pagina config edita: la
      piattaforma (env.json, PERCORSI in sola lettura), il DB, e un pannello per
      servizio (JSON editabile, tutti i campi presenti/futuri) + "aggiungi
      servizio". Seed: vrv (release/codename/roles/vvdog, NIENTE dati storici -
      progetto nuovo), vrn. Collaudato: save/add/invalid-json. SEGRETI: prossimo
      passo, ora che /lab/platform e' chiuso.
- [ ] **PARCHEGGIATO 03/07 (da decidere): come si EDITANO i JSON per-servizio.**
      La v1 e' solo una textarea del JSON grezzo (una "vetrina"): vitti giustamente
      chiede come scriverli davvero. Rotta `config` sganciata dal front controller
      (i file restano: pages/config.php + platform/config/*). Da riprendere: la UX
      di editing (campi strutturati per le voci note? schema? generico
      chiave/valore?) PRIMA di ri-agganciarla. Poi i secrets per servizio.
- [x] **timeout d'inattivita' FATTO 04/07** (`vrp_session_check` in vrp_auth.php):
      TTL da env.json (`session.idle_timeout`, default 900s) -> `VRP_SESSION_TTL`;
      chiamato in fondo a 3_2_1_Go.php PRIMA di ogni output. Se la sessione loggata e'
      inattiva oltre il TTL -> `vrp_logout()` (autoritativo, copre anche AJAX);
      altrimenti aggiorna `$_SESSION['vrp_last']` + `sessions.last_activity`. In piu' il
      FC mette il meta-refresh (erede di leNostre50) verso `?p=logout` quando loggato.
      Collaudo: logica scadenza (CLI) OK, meta-refresh e last_activity su richiesta OK.
- [x] **WALLPAPER + trasparenze + bottone Esci FATTO 04/07:** sfondo di contesto
      configurabile dal JSON - piattaforma in `env.json` (`ui.wallpaper` ->
      `VRP_WALLPAPER`), e per servizio nel manifest (`wallpaper`). Il FC mette classe
      `vrp-bg` + una `:root{--vrp-wp:url(...)}` (unica cosa dinamica; il resto in CSS):
      pannelli `.vrp-cfg-section`/`.vrp-card` in `rgba(...)` traslucido con velo scuro
      per leggere (stile VRV). La home mostra il wallpaper di ogni servizio come sfondo
      della sua cartolina (colpo d'occhio). Barra: badge utente + **bottone Esci** vero
      (`.vrp-nav-logout`). Foto: `public/wallpapers/sfondo_spagna.jpg` (tramonto Elena+
      Frida dal post 112) su login+VRV. Collaudo: body.vrp-bg + var + foto servita 200
      (1.7M). RESTA: login "gemella" della VRV vera + pagina di logout leNostre50
      (foto invernale Elena+vitti) allo scadere del timeout; wallpaper per leNostre.
- [ ] BUG segnalato (vitti 04/07): nel post 112 (`/vv/`... o `/vv_test/`) lo
      **slideshow globale non funziona** - da investigare nel VRV vivo.
- [x] indice foto GIA' ESISTE e popolato: `lenostre.foto` (35k, lat/lon/anw), mantenuto da `vrw`
- [ ] riscrivere `search50` come query su `lenostre.foto` (riusare `?azione=cerca` di gestore_foto.php); filtrare `anw=0` per il pubblico
- [x] `docs/genera_doc.php` FATTO: estrae le API da `platform/lib/` -> `VRP_API_lib.md` (7 funzioni, tag @desc/@param/@return/@manual)
- [x] **presa in carico servizi (take-up) FATTA 04/07:** `lib/vrp_services.php` con
      `vrp_services()` legge la "targa" (blocco `manifest`) di ogni servizio dal
      registro `VRP_SERVICES` e ne fa la lista per la navigazione (aggiungi un
      servizio = un puntatore in env.json, compare da solo - il supervisore Siemens
      risorto). Targa separata dagli interni: `config/vrv.json`+`vrn.json` hanno
      `manifest` (slug/label/icon/blurb/url/enabled) sopra, gli interni del servizio
      sotto. `home.php` disegna le cartoline dai servizi. Ripescato da
      `vv/lib/core/meta.php` il lettore JSON -> riscritto `vrp_load_json()` in
      vrp_util (fail-loud, no guardie, no fallback muti). Lint+boot CLI+fail-loud
      (file mancante / JSON rotto / manifest assente) OK. `url` `/vv/` e
      `/leNostre50/index.php` da confermare.
- [x] **DB `vrp_auth` CREATO + seminato 04/07:** database nuovo (utf8mb4, InnoDB) con
      3 tabelle - `users` (id/username UNIQUE/email/pass_hash/display_name/avatar/
      is_active/created_at/last_login_at), `user_services` (PK utente+servizio ->
      `role`: la concessione PER-servizio), `sessions` (php_sid/user_id NULL=anonimo/
      ip/user_agent/service/action/started/last_activity/ended: il registro del
      portinaio, erede di `photo.user_history`). Utente dedicato **`vrp_auth_app`** a
      privilegi minimi (S/I/U/D su vrp_auth.*), NIENTE root; segreto in
      `secrets/db_vrp_auth.php` (640), registrato nel registro `db` di env.json.
      Seminati vitti (admin vrv+vrn) ed Elena (editor vrv+vrn) via `vrp_db('vrp_auth')`
      - catena env->segreto->PDO collaudata (connesso come vrp_auth_app). Migrazione
      dei vecchi utenti sdoppiati (vittrosviaggi.utenti + photo.users) = passo dopo.
- [x] **pagina gestione utenti FATTA 04/07** (`pages/users.php`, rotta `?p=users`
      LAN+CSRF nel FC): elenco con foto/dati/chip-ruoli; form nuovo/modifica con
      username, nome, email, attivo, password (vuota=invariata, `password_hash`),
      upload foto (`public/avatars/u<id>.<ext>`), e un menu ruolo PER servizio con le
      opzioni prese dal `roles` del file di config del servizio (aggiunto `roles` a
      vrn.json: admin/editor/viewer/guest). Cancellazione con conferma (CASCADE pulisce
      i grant). Riusa le classi vrp-cfg-*/vrp-table + poche vrp-usr-*. Collaudo HTTP
      reale (php -S + curl con sessione/CSRF): GET elenca vitti/elena coi chip;
      POST crea utente con ruoli e hash verificato (password_verify OK); POST cancella
      e ripulisce. UX: password mascherata con occhiolino, bottone "Nuovo utente"
      sempre visibile, dir `public/avatars` a setgid gruppo http (fix permessi upload).
- [x] **LOGIN di piattaforma FATTO 04/07** (`lib/vrp_auth.php` + `pages/login.php` +
      `pages/logout.php`): `vrp_login` verifica username+password (`password_verify`),
      rigenera la sessione, scrive `$_SESSION['vrp_uid']` e una riga nel registro
      `sessions` (php_sid/user_id/ip/user_agent/action='login'); `vrp_logout` timbra il
      logout; `vrp_current_user()` (memoizzata) porta i ruoli per-servizio;
      `vrp_is_admin()` = ruolo 'admin' in un servizio (regola interim, poi ruolo di
      piattaforma dedicato). FC: rotte login/logout, guardia `admin` su `users`
      (non-admin -> 302 login), nav con badge utente + Esci. Collaudo HTTP completo OK.
      **La pagina utenti NON e' piu' aperta a chiunque sia in LAN.**
- [ ] **AUTH FLOW (deciso a voce 04/07, da realizzare col login):** il link del
      servizio punta al suo ingresso (es. `/leNostre50/index.php`); il login e' UNICO
      a livello di piattaforma (sessione condivisa fra i servizi). Nel `manifest`
      serve un campo `auth: required|public`: **leNostre = required** (se non loggato
      -> login di piattaforma, con ritorno al servizio); **VRV = public** (non forza
      il login). CHIARITO: arrivando dalla piattaforma NON sei "admin" d'ufficio, sei
      la TUA identita' col TUO ruolo (da vrp_auth). Public = login non forzato, non
      "tutti admin". Conseguenza sul DB: il ruolo e' PER-servizio (Elena editor in
      VRV, viewer in leNostre) -> la concessione e' (utente x servizio) -> ruolo.
      - [x] campo `auth` FATTO 04/07: `vrv:public`, `vrn:required` nei manifest; la
        home mostra i pubblici a tutti (badge "ospite") e i privati solo a chi ha un
        ruolo (badge col ruolo). Login/logout resi IMMERSIVI ('bare', niente barra):
        login = benvenuto+form sul tramonto (pannello in alto a dx); logout = foto
        invernale leNostre50, messaggio diverso per timeout (?t=1) vs logout manuale.
      - [x] **IL CUORE - FATTO 05/07 (dettagli in [B300](VRP_B300_diario_coding.md)):
        i servizi EREDITANO login+ambiente dalla piattaforma, senza
        `require 3_2_1_Go.php` in ogni pagina.** Realizzato con `auto_prepend_file`
        (`lab/public/.user.ini` -> 3_2_1_Go.php), guardia d'area config-driven
        (`vrp_guard_request`), e librerie dei servizi caricate DAL REGISTRO
        (`vrp_load_service_libs`: ogni servizio dichiara `"lib"` nella targa - il
        puntatore, non cablato nel loader). Primo servizio dentro: wiz_foto (VRW),
        6 pagine a ZERO require. Meccanismo originale previsto:
        (1) SESSIONE CONDIVISA (stesso dominio, cookie su `/`); (2) **`auto_prepend_file`**
        = 3_2_1_Go.php caricato da PHP prima di OGNI pagina del dominio (via `.user.ini`
        o nginx) -> ambiente ovunque, zero require a mano; (3) ponte: i servizi leggono
        `vrp_current_user()` invece del loro login. **METODO DI VITTI**: copiare i
        servizi standalone (`/srv/http/maps`, `vv`, `leNostre50`) sotto `/srv/http/lab/`
        (`/lab/maps`, `/lab/vv`, `/lab/leNostre50`), smanettare sulle COPIE, e quando
        funziona rimetterli al posto giusto. Tocca app vive -> backup + a tappe.

---

## 6. Requisiti di sistema (dipendenze esterne, NON di piattaforma)

Cose che il codice USA ma che NON sono nostre: binari del sistema, estensioni PHP,
servizi, job cron. Vanno documentate perche' se manca il pezzo il servizio si rompe
in un punto lontano dal codice. Regola: il codice le invoca a **path assoluto** o le
**cerca** (`command -v`) e degrada con un messaggio chiaro, mai un crash muto.
Censimento 2026-07-09.

### 6a. Binari invocati dal codice (runtime)

| Binario | Chi lo usa | A cosa serve | Note |
|---------|-----------|--------------|------|
| **PHP GD** (estensione) | `vrp_img.php` | resize/thumbnail al volo (imagecreate*/imagejpeg) | e' IL motore immagini; se manca, thumb rotte |
| **ffmpeg** (`/usr/bin/ffmpeg`) | `share/thumb.php` | estrae 1 fotogramma dai video per l'anteprima | path assoluto (php-fpm PATH scarno); cache `share_storage/.vthumb/` |
| **exiftool** (`/usr/bin/vendor_perl/exiftool`) | `wiz_foto/lenostre_index.php` | legge EXIF/GPS per l'indice `lenostre.foto` | vive in `vendor_perl` -> path ASSOLUTO obbligatorio; `-n` da' lat/lon gia' firmate |
| **heif-convert** / **magick** / **convert** | `wiz_foto/heic_to_jpg.php`, `vrw.php` | HEIC iPhone -> JPG (catena di fallback) | libheif + ImageMagick; si sceglie il primo presente via `command -v` |

### 6b. Servizi di base

- **MariaDB** (un solo server): tutti i DB (`vrp_auth`, `vittrosviaggi`/`vrv`, `lenostre`,
  `photo`). Le DDL le lancia **root** (l'app-user ha solo S/I/U/D). Vedi
  [VRP_DB_catalogo](VRP_DB_catalogo.md).
- **nginx** + **PHP-FPM**: servono il lab. `auto_prepend_file` (`lab/public/.user.ini`)
  carica `3_2_1_Go.php` prima di ogni pagina sotto `/lab/public` -> ambiente ovunque.
  nginx nega `/lab/platform/` (interni fuori dal web). Upload: `client_max_body_size`
  (era 50M, alzato a 2g sul :80) deve reggere i file grossi.

### 6c. Manutenzione fuori-piattaforma (cron)

- **Rotazione log**: NON e' `logrotate.d` (che copre solo nginx/samba/...). La fanno
  script su misura in `/srv/http/tools/`, dalla crontab di `vitti`:
  `ups_rotate.sh` (02:10) e `sessione_rotate.sh` (02:15, `RETENTION_DAYS=14`).
- **BUCO NOTO (09/07):** il log della piattaforma **`vrp.log`**
  (`vittrosviaggi_shared/logs/vrp.log`, da `VRP_LOG_FILE`) **NON e' ruotato** da
  nessuno. Oggi innocuo (6 KB, log level basso), ma va aggiunto un rotatore (una riga
  stile `sessione_rotate.sh`). NB: il **registro-azioni** (`vrp_insert_action`) sta nel
  **DB** (`sessions`), quindi lo svuota il **Purge** in Admin, non il logrotate.

---

## Le tre parole del diario: BOZZA, VERSIONE, RILASCIO

Scritto l'11/08/2026, dopo una domanda di Elena in collaudo: filtrando
"viaggi + bozze" uscivano tre post, ma uno solo (il 19) non mostrava niente in
anteprima. Aveva ragione lei, e il motivo e' che **la parola "bozza" indica DUE
cose diverse**.

### 1. `post.bozza` = lo STATO dell'articolo (non pubblicato)

E' una colonna della tabella `post`. Vuol dire "articolo da finire", lo decide
l'autore, e **non ha niente a che vedere con la scrittura in corso**.
E' QUESTO che guarda il filtro della lista (`inc/diario_list_lib.php:92`:
`bozza = 1`).

### 2. `post_drafts` = il LAVORO NON SALVATO (la pastiglia ✏️)

Una riga per post, con il testo che si sta battendo nell'editor. Serve a non
perdere niente se il computer si spegne. Nella lista e' la colonna calcolata
`has_draft` (`d.post_id IS NOT NULL`), NON il filtro.

### Il ciclo, a chiare lettere

    prima modifica  ->  nasce la BOZZA (post_drafts)
    SALVA           ->  il testo diventa una VERSIONE numerata (post_versions)
                        e la bozza SPARISCE
    RILASCIO        ->  ufficializza il lavoro: da li' in poi non e' piu' una
                        bozza salvata, e' la versione pubblicata

Quindi: **un articolo puo' essere "bozza" (non pubblicato) senza avere nessuna
bozza di lavoro aperta.** E' il caso del post 19 - misurato: `bozza=1`,
`post_drafts=0`, 8 versioni salvate. Il 58 invece ha tutte e due (`bozza=1`,
1 riga in `post_drafts`), e il 64 e' il figlio INGLESE del 44, anche lui con
lavoro non salvato dentro.

📌 **Le etichette NON si cambiano** (deciso da vitti l'11/08: «Elena ha capito e
lasciamo cosi'»). Si e' scelto di spiegare invece di rinominare: la spiegazione
sta nella voce **"Info bozza"** del menu Salva/Chiudi dell'editor
(`js/vrv-modifica-post-barra.js`, `vrvMostraInfoBozza`), che e' il punto in cui
uno si fa la domanda.

⚠️ **Non esiste un manuale utente dell'editor**: l'unico `manuale.html` in giro
e' quello della MAPPA (`/srv/http/maps/manuale.html`, "VRV Navigation").
Quando si scrivera' quello del diario, questa sezione ci va dentro per prima.
