# VRP - B300 - Diario di coding

**Stato:** APERTO 2026-07-05. Segue [VRP_B200_architettura](VRP_B200_architettura.md).
Registro cronologico degli interventi di CODIFICA: cosa si e' toccato, perche',
quali file, e come e' stato collaudato. Le decisioni di architettura restano nel
B200; qui il "cantiere" giorno per giorno. ASCII puro. Voci nuove in fondo.

---

## 2026-07-05 - Cruscotto Amministrazione + Sessioni/Log + timeout

**Perche':** dalla home `?p=home` non c'era modo di raggiungere le pagine
riservate (utenti compariva in barra solo da LAN+admin; config era parcheggiata).
Serviva una porta unica per l'amministrazione, piu' due strumenti d'indagine.

**Fatto:**

- **Cruscotto** `platform/pages/admin.php`, rotta `?p=admin` (LAN+admin, unica
  voce in barra): atrio con 4 riquadri (Utenti, Configurazione, Sessioni, Log)
  che aprono in nuova scheda (`target=_blank` + `rel=noopener`), cosi' la console
  resta li'. Riusa `.vrp-cards`/`.vrp-card`. E' l'"Altezza 1" (admin di
  PIATTAFORMA) della sez. 3b del B200, in piedi.
- **Front controller** `public/index.php`: tabella rotte ampliata. `admin` in
  barra; `users`/`config`/`sessioni`/`log` con `nav=false` (fuori dalla barra,
  raggiunte dal cruscotto). `config` RI-AGGANCIATA (era sganciata dal 03/07).
- **Sessioni** `platform/pages/sessioni.php`: legge la vista `sessions_h` di
  vrp_auth (storico del portinaio). Scelta colonne con caselle; whitelist viva
  presa da `SHOW COLUMNS` (mai nomi arbitrari nella SQL). Sola lettura, ORDER BY
  started_at DESC, LIMIT parametrico.
- **Log** `platform/pages/log.php`: tail dal fondo (riciclato da
  `vv/vv_where_logs.php`) su `VRP_LOG_FILE` + candidati di sistema in whitelist
  (nginx / php-fpm / error_log). Filtro per livello (ERROR..DEBUG, riconosciuto
  da `][LIVELLO][`) e righe colorate.
- **CSS** `public/css/vrp-lab.css`: nuove `.vrp-cols-*` (selettore colonne) e
  `.vrp-log-*` (barra compatta + pannello). Pagine Log e Sessioni a tutta
  finestra ~92% con scroll INTERNO del pannello e testata compatta: attivato SOLO
  su queste due via `body:has(.vrp-log-view)` / `body:has(.vrp-sess-table)`,
  nessun flag nel FC. Intestazione tabella fissa (`position:sticky`).
- **timeout != logout** in `platform/lib/vrp_auth.php`: `vrp_logout($action=
  'logout')` timbra l'azione richiesta; `vrp_session_check` passa `'timeout'`;
  `logout.php` passa `'timeout'` se arriva dal meta-refresh (`?t=1`). Nelle
  Sessioni si legge `timeout` invece di `logout`. Scrittura idempotente
  (`WHERE ended_at IS NULL`): se scattano due vie, si timbra una volta sola.
- **session TTL** rimesso a 900s in `boot/env.json` (era 60 da una prova).
- **doc API** rigenerata: `docs/genera_doc.php` -> `VRP_API_lib.md`
  (6 file di `platform/lib/`, 31 funzioni documentate).

**Collaudo:** `php -l` su tutti i file toccati OK. Da localhost (LAN):
`?p=home` 200; `?p=admin|config|sessioni|log` -> 302 alla login (guardia
LAN+admin che funziona, nessun 500). Query `sessions_h` provata via PDO (colonne
+ righe reali, incluso un accesso da IP pubblico). Utenti admin: vitti / seneca /
lella (Elena e' editor, non vede il bottone).

**Aperto:** la UX vera per editare i JSON per-servizio (config e' ancora una
textarea); `:has()` richiede un browser recente (ok su Manjaro rolling,
altrimenti le due pagine ripiegano sulla resa normale).

---

## 2026-07-05 (pomeriggio) - Profilo self-service + auto-prepend + integrazione VRW

**Perche':** (1) un utente non-admin doveva poter modificare il PROPRIO profilo;
(2) far entrare il primo servizio (wiz_foto) nella piattaforma col metodo di
vitti: "la piattaforma fa tutto, i servizi ereditano l'ambiente". Obiettivo
dichiarato: nel codice delle pagine NON un solo `require`; se manca una funzione
la si aggiunge alla libreria di piattaforma (niente 'not defined' ne' 'already
defined').

**Fatto - profilo e ruolo:**

- **Profilo** `platform/pages/profilo.php`, rotta `?p=profilo` (serve solo essere
  loggati, non LAN ne' admin): l'utente cambia SOLO i suoi dati (nome, email,
  password vuota=invariata, foto). Username/ruoli restano all'admin. Riusa la
  logica di `users.php`.
- **Icona-ruolo nel badge**: `vrp_role_top()` (admin>editor>viewer>guest) +
  `vrp_role_icon()` in `vrp_auth.php`; il badge in barra e' ora un link a
  `?p=profilo` con l'icona del ruolo (admin=corona, il "Cesare imperatore").

**Ricognizione Nextcloud (mang3):** due account `vitti` e `lella`; i telefoni
caricano in `Photos/AAAA/MM/GG/`. Estensioni reali: solo `.jpg` (+ `.png` per
lella) e VIDEO (`.mp4/.mov`) - NIENTE `.heic` (anche l'iPhone carica jpg). Dato
in `/srv/dati/nextcloud/data` (bind mount). DECISO: importare anche i video
(Elena decide poi). Il 13T (HyperOS) ritardava l'auto-upload: batteria "nessuna
restrizione" + avvio automatico.

**Fatto - integrazione VRW (il metodo, rodato qui per i prossimi servizi):**

- **Copia** `/srv/http/wiz_foto` -> `/srv/http/lab/public/wiz_foto` (originale
  intatto = backup). Diff confermato identico prima di lavorarci.
- **Targa** `platform/config/vrw.json` (manifest + `paths`/`nextcloud`/
  `extensions`/`modes`), registrata in `env.json` `services` -> compare e si
  edita da `?p=config`. Politica d'accesso nella targa: `lan:true, admin:true`.
- **auto-prepend** `public/.user.ini` -> `auto_prepend_file=3_2_1_Go.php`: la
  piattaforma carica l'ambiente PRIMA di ogni pagina sotto `/lab/public`. Preso
  al volo senza reload di php-fpm. Il loader usa `require_once` -> nessun
  'already defined'.
- **Guardia d'AREA** `vrp_guard_request()` in `vrp_auth.php`, chiamata dal loader:
  per i servizi ospitati nel lab applica `lan`/`admin`/`auth` dalla loro targa,
  SENZA guardie nel codice del servizio. wiz_foto era esposto sotto public ->
  ora 302 se non LAN+admin.
- **Librerie dei servizi dal REGISTRO (il "puntatore")**: `vrp_service_config()`
  + `vrp_load_service_libs()` in `vrp_services.php`. Il loader NON nomina i
  servizi: gira su `env.json services`, e ogni servizio che dichiara `"lib"`
  nella targa viene caricato (config passata alla lib in `$vrp_service`).
  Aggiungere un servizio = registrarlo + `"lib"`, zero tocchi al loader.
- **`platform/lib/vrw.php`**: fold di `wiz_foto/_cfg.php` + `_pecore.php`,
  rinominato `vrw_`/`VRW_`, percorsi dalla targa (`VRW_STAGING_DEFAULT` ecc.) e
  `VRW_DEST_BASE_DEFAULT = VRP_MEDIA_ROOT` (leNostre dalla piattaforma).
- **sed sulle 6 pagine web** (wizard_import, upload_to_staging, unzip_amazon,
  heic_to_jpg, import_amazon, export_staging_csv): via TUTTI i `require`
  (`_cfg`/`_pecore`), `vv_->vrw_`, `VV_->VRW_`, e header-commento in testa
  ("questa pagina e' gestita dalla piattaforma...").
- **Front controller**: tolto anche il suo `require` del loader (lo da'
  l'auto-prepend) + header-commento.

**Collaudo:** `php -l` su tutti i file OK. `vrw.json` valido. Da CLI il loader
definisce `vrw_base_url`/`vrw_pecore_overlay` e `VRW_STAGING_DEFAULT`/
`VRW_DEST_BASE_DEFAULT` -> lib caricata DAL REGISTRO. Rotte: home 200, admin 302,
wiz_foto 302 (guardia attiva, prima era esposto). Runtime vero:
`wizard_import.php` RENDE senza un solo require, ereditando l'ambiente. Nelle 6
pagine: zero `require`, zero residui `vv_`/`VV_`.

**Aperto:** (1) `lenostre_index.php` -> `platform/lib/vrn_index.php` (ultimo
require, in `gestore_foto`+`popola`); (2) orfani `_cfg.php`/`_pecore.php` nella
copia da cancellare; (3) rename CSS `.vv- -> .vrw-`; (4) nuovo Step "Scarica da
Nextcloud" (foto+video, un account per volta); (5) ruolo `vrw` a vitti/lella ->
compare la cartolina in home. Prossimo servizio previsto: **VRS** (`/srv/http/
share`), policy "login + WAN" (targa: `auth:required`, senza `lan`).

---

## 2026-07-05 (sera) - Rifiniture admin (leggibilita', tail -f) + fix identita' log

**Perche':** giro di feedback di vitti sulle paginette admin (leggibilita',
spazio, aggiornamento del log) e un baco scovato leggendo il log.

**Fatto:**

- **Log - riga vuota**: il `<pre>` emetteva un `\n` OLTRE al blocco -> doppia
  spaziatura. Tolto il newline nel loop.
- **Log - toggle "Segui" (tail -f)**: checkbox in barra; se acceso la pagina si
  ri-aggiorna ogni 3s e scende in fondo (`follow=1` in URL, piccolo JS a fondo
  pagina); spento -> si ferma (per ragionarci sopra).
- **Log - riquadro** da nero pieno a semi-trasparente (stessa cura di Sessioni).
- **Sessioni - leggibilita'**: tabella con contenitore semi-opaco (rgba) cosi' il
  testo bianco risalta sullo sfondo; header sticky piu' opaco.
- **Sessioni - "Colonne" collassabile**: il selettore colonne e' un `<details>`
  (declutter); Righe/Applica/Reimposta restano visibili sotto.
- **Back-link "<- Amministrazione"** su Utenti e Configurazione (coerenza con
  Sessioni/Log).
- **FIX identita' log**: `vrp_log_identity()` leggeva `$_SESSION['vrp_user']` (mai
  valorizzato: il login salva `vrp_uid`) -> ogni riga usciva `@ospite#0@guest`.
  Era un segnaposto pre-`vrp_auth`. Ora usa `vrp_current_user()` + `vrp_role_top()`.
  Verificato via CLI (sessione simulata): `@vitti#1@admin`, `@lella#8@admin`. Le
  righe vecchie restano ospite (storia, non si riscrive).
- **Acronimo**: `vrs` non e' piu' "Sera" (che e' una CATEGORIA di post del VRV,
  non un servizio) ma **VittRos Share**; corretto `VRP_acronimi.md`.
- **doc API** rigenerata: `VRP_API_lib.md` (7 file, 42 funzioni).

**Collaudo:** `php -l` su tutti i file OK; rotte admin 302 (guardia). Identita'
log provata in CLI con `$_SESSION['vrp_uid']` impostato (vitti/lella).

**Aperto (coda UI):** layout wide+fisso+scroll interno per Utenti e Config +
sezioni collassabili; tab riusati (login / amministrazione / un tab per
servizio); bottoni servizio in home (ruolo `vrw` + placeholder VRShare).

---

## 2026-07-05 (sera, 2) - Coda UI: Utenti/Config wide, Sessioni paginate/sortabili, VRW+VRS in home

**Perche':** chiudere la coda UI (punti 1 e 3) e un giro di rifiniture di vitti
provando le paginette in browser.

**Fatto:**

- **Layout wide+fisso+scroll interno per Utenti e Config** (come Log/Sessioni).
  Introdotto un marcatore GENERICO e riusabile in `vrp-lab.css`: la testata sta
  in `.vrp-fixed-head` (fissa), il corpo in `.vrp-fixed-body` (scorre). Le liste
  di selettori `:has()` sono state ESTESE (non duplicate) per includere il nuovo
  marcatore. Nuova classe `.vrp-fold`: una sezione collassabile (`<details>`) con
  la testata resa come un vecchio `<h2>`.
- **Config** (`config.php`): 4 sezioni ora `<details vrp-fold>` (Ambiente/Logging/
  Database aperte, Percorsi chiusa perche' sola lettura). Le sezioni collassate
  restano nel form e continuano a inviare i loro campi al POST.
- **Utenti** (`users.php`): elenco e form sono una FISARMONICA ESCLUSIVA via
  l'attributo HTML `name="utenti-acc"` sui due `<details>` (accordion nativo, zero
  JS): apri uno -> l'altro si chiude. Tolto il bottone "Nuovo utente" ridondante.
  **Paginazione a 8** (`?pg=N`, prec/pagina X/Y/succ; pager nascosto se <=8).
  `$edit` ora preso per id (vale anche se l'utente sta in un'altra pagina).
- **Back-link "Amministrazione" TOLTO** dalle 4 paginette (users/config/sessioni/
  log): il ritorno resta la voce in barra. La testata piu' corta fa entrare 8
  utenti senza scrollbar.
- **Cruscotto admin** (`vrp-lab.css`): card a larghezza uniforme (`flex:1 1 220px`,
  max 320, altezza allineata dalla riga); titolo `.vrp-card b` su una riga che
  scorre (`overflow-x:auto`, scrollbar nascosta) invece di allargare la card.
- **Sessioni** (`sessioni.php`): **paginazione a 20** (campo "Righe/pag." 5-200,
  `?pg=N`) e **ordinamento** cliccando l'intestazione (freccia su/giu' sulla
  colonna attiva, ri-clic inverte). Colonna d'ordine sempre da whitelist
  (`SHOW COLUMNS`). Chiusura `$sess_url()` (closure) costruisce i link preservando
  lo stato (colonne/righe/sort) con override. Nuove classi CSS `.vrp-pager*` e
  `.vrp-sort`.
- **VRW + VRS in home** (coda punto 3): creata la targa `config/vrs.json` (VittRos
  Share, `url:/share/`, `auth:required`, NIENTE lib: e' un primo link al servizio
  standalone, integrazione vera = poi) e registrata in `env.json services`. Dato a
  **vitti e lella** il ruolo `admin` su `vrw` e `vrs` (in `user_services`), cosi'
  le due cartoline compaiono. Home ora: VRV(pub), VRN, VRW, VRS.
- **Fix 403 wiz_foto**: la targa vrw punta alla DIRECTORY `/lab/public/wiz_foto/`,
  che senza `index.php` dava 403 (autoindex off). Rinominato `wizard_import.php`
  -> `index.php` e aggiornati gli 8 back-link "Torna al Wizard" nei passi. nginx
  ha gia' `index index.php ...` -> la URL della cartella ora serve il wizard.
- **Permessi**: `boot/env_backups` era `drwxr-sr-x` (gruppo http senza scrittura)
  -> messo `g+w`, cosi' il web server puo' scrivere i backup di env.json al salva.

**Collaudo:** `php -l` su tutti i file toccati OK. Render CLI (utente vitti id 1):
Config = 4 fold + fixed-head/body; Utenti = fisarmonica esclusiva (default elenco
aperto; `?new=1` form aperto/elenco chiuso), paginazione provata a soglia 2 (pag
2/3 con prec/succ giusti); Sessioni = 1/2 default, sort username asc -> 1/7, pag 2
prec=1/succ=3, stato preservato nei link; home mostra le 4 card per vitti; wiz_foto
`index.php` rende il wizard.

**Aperto (coda UI):** resta solo il punto **2 - tab riusati** (login /
amministrazione / un tab per servizio). Idea di vitti: **"tutto wide"** anche il
cruscotto admin (rimandato). **Proliferazione tab del browser** (da risolvere
domani): i riquadri del cruscotto admin usano `target="_blank"` (admin.php) -> ogni
clic apre una scheda nuova e si moltiplicano; da ripensare insieme al punto 2
(tab/aree interne riusate invece di schede del browser). Minori: orfani `_cfg.php`/`_pecore.php` in wiz_foto da
cancellare; rename CSS `.vv- -> .vrw-`; `lenostre_index.php -> vrn_index`;
integrazione VERA di VRS share (login+WAN) e Step "Scarica da Nextcloud".

---

## 2026-07-06 - Tab riusati, cruscotto wide, wallpaper configurabile, gateway accesso, fix timeout

**Perche':** giro di feedback di vitti provando la piattaforma: (1) i riquadri
del cruscotto admin aprivano ogni volta una scheda nuova del browser
(proliferazione, "veramente fastidioso"); (2) il cruscotto admin andava reso
"wide" come le altre; (3) lo sfondo di piattaforma non era editabile dalla
pagina Config (c'era in env.json ma nessuna voce nel form); (4) aprire un'area
riservata da sloggati saltava MUTO alla login; (5) il timeout buttava fuori vitti
MENTRE lavorava sul VRW.

**Fatto:**

- **Tab riusati** (`pages/admin.php`): tolto `target="_blank" rel="noopener"`
  dai riquadri. Ora Utenti/Config/Sessioni/Log si aprono nella STESSA scheda; si
  torna dalla voce "Amministrazione" in barra. Fine proliferazione.
- **Cruscotto admin WIDE** (`pages/admin.php`): avvolto in `.vrp-fixed-head` +
  `.vrp-fixed-body`, cosi' scatta la macchina "wide+fisso+scroll interno" gia'
  esistente (`body:has(.vrp-fixed-body)` in vrp-lab.css). Zero CSS nuovo.
- **Wallpaper configurabile** (`pages/config.php`): nuova sezione **Aspetto**
  (fold) con la voce "Sfondo (wallpaper)" che edita `ui.wallpaper` in env.json.
  Salvataggio + validazione: se l'URL sta sotto la base (`/lab/...`) si mappa al
  filesystem e si controlla che il file esista (`ui.wallpaper (file non trovato)`
  fra gli errori). Messa in cartella la foto `public/wallpapers/buddha.jpeg`
  (da /tmp, gruppo http, 664). NB: env.json NON toccato a mano su richiesta di
  vitti: lo sfondo si sceglie dalla pagina (resta sfondo_spagna finche' non salva
  buddha).
- **Gateway di cortesia** (`pages/accesso.php`, rotta bare `accesso`, nav=false):
  quando serve l'accesso non si salta piu' muti alla login. Splash "Riservato -
  <SERVIZIO> richiede l'accesso", meta-refresh a 5s verso la login, bottone "Vai
  subito". Porta con se' `svc` e `next`. Agganciato in DUE punti:
  - FC (`public/index.php`) ramo admin-non-autorizzato -> `?p=accesso&svc=admin&
    next=<rotta>`;
  - `vrp_guard_request()` (`lib/vrp_auth.php`) per i servizi ospitati: i rami
    `admin`/`auth:required` ora vanno al gateway con `next=REQUEST_URI` (il ramo
    `lan` resta 403 secco).
- **`next` nella login** (`pages/login.php`): dopo l'accesso torni dove volevi.
  Solo percorsi same-origin sotto la base (`strncmp base`, niente `//` ne'
  a-capo) -> NIENTE open-redirect; ostili -> home. Portato anche nel POST (campo
  hidden) cosi' sopravvive all'invio del form.
- **Fix timeout scheda-fantasma** (`pages/logout.php` + `public/index.php`): la
  causa era il meta-refresh di una scheda di piattaforma lasciata ferma che dopo
  il TTL andava dritta a `logout&t=1` e sloggava, anche se sul VRW eri
  attivissimo (ogni pagina rinfresca `vrp_last` via `vrp_session_check` al boot,
  ma la scheda immobile non lo sa). Ora `logout.php?t=1` NON slogga se
  `time()-vrp_last <= TTL` (sessione viva altrove): torna a `?p=home`. Inoltre il
  meta-refresh del FC ora usa il tempo RESIDUO (`TTL - inattivita' + 5`) invece
  del TTL fisso.

**Collaudo:** `php -l` su tutti i 7 file OK. Contro nginx vivo (127.0.0.1 = LAN):
`?p=home` 200; `?p=admin` da anonimo -> 302 al gateway `?p=accesso&svc=admin&
next=...`; gateway rende meta-refresh 5s + "<SVC> richiede l'accesso" + bottone;
login `?next=` porta il campo hidden col target; login con `next=//evil.com`
RIFIUTA (0 campi next -> home). Render CLI come utente id 1 (stesso harness del
05/07): admin = 2 marcatori fixed-head/body e 0 `target="_blank"`; config = voce
"Aspetto" presente col valore wallpaper attuale.

**Aperto:** resta il punto **2 vero e proprio** (tab/aree interne riusate: per ora
la proliferazione e' spenta tornando alla navigazione in-scheda, ma un vero
"tab per servizio" e' rimandato); "tutto wide" anche altrove se vitti vuole;
integrazione VERA di VRS share (login+WAN) e Step "Scarica da Nextcloud"; orfani
`_cfg.php`/`_pecore.php` in wiz_foto; rename CSS `.vv- -> .vrw-`;
`lenostre_index.php -> vrn_index`. Da provare da loggato in browser: cruscotto
wide e scelta buddha dal form Aspetto.

---

## 2026-07-06 (2) - Sfondi per-contesto + regia dei 3 tab (named target)

**Perche':** feedback di vitti provando in browser. (1) Ogni CONTESTO deve avere
il suo sfondo, riconoscibile e cambiabile SENZA codice (es. "se il buddha non
piace a Elena lo cambio in un attimo"). (2) La home doveva essere wide anch'essa.
(3) Sui tab avevo esagerato (tutto in una scheda): vitti vuole 3 schede a nome
fisso, riusate.

**Modello sfondi deciso:** piattaforma (tutte le sue pagine, incl. Config) =
spagna; ogni SERVIZIO = il suo (VRW = maloia); pagina di CORTESIA = buddha. La
cortesia NON e' un servizio: fa parte della piattaforma, quindi nel JSON di
piattaforma ci sono DUE voci (normale + cortesia).

**Fatto:**

- **Sfondo cortesia configurabile** (`boot/vrp_defines.php`, `pages/accesso.php`,
  `pages/config.php`): nuova costante `VRP_WALLPAPER_CORTESIA` da
  `ui.wallpaper_cortesia` (vuoto = eredita quello di piattaforma). accesso.php usa
  lo sfondo cortesia con fallback. La sezione **Aspetto** in Config ora ha DUE
  voci: "Sfondo piattaforma" (`ui.wallpaper`) e "Sfondo cortesia"
  (`ui.wallpaper_cortesia`); validazione file-esiste su entrambe (loop). env.json
  NON toccato a mano: vitti sceglie dalle voci (buddha da salvare li').
- **Sfondo VRW = maloia** (`config/vrw.json`): messo `public/wallpapers/
  maloia.jpeg` (da /tmp, gruppo http 664) e valorizzato `manifest.wallpaper`. La
  cartolina VRW in home lo mostra gia' (`.vrp-card-wp`). Cambiabile dal riquadro
  `vrw` in Config. (Dipingere anche le PAGINE interne del VRW resta da fare: e'
  parte dell'integrazione VRW vera.)
- **Home WIDE** (`pages/home.php`): avvolta in `.vrp-fixed-head` + `.vrp-fixed-
  body` (riusa la macchina wide). Come admin/config/utenti/sessioni/log.
- **Regia dei 3 TAB (named target)** invece del `target=_blank` che moltiplicava
  o dell'unica scheda che collassava tutto:
  - Tab 1 (corrente, `_self`): login, piattaforma (home), **Configurazione**. La
    Config e' PIATTAFORMA: spostata dal cruscotto admin ALLA barra (nav=true),
    tolta dai riquadri di admin.php.
  - Tab 2 (`vr-servizi`): le cartoline dei servizi in home aprono tutte in questa
    scheda condivisa (`target="vr-servizi"`).
  - Tab 3 (`vr-admin`): la voce "Amministrazione" in barra e i riquadri del
    cruscotto (Utenti/Sessioni/Log) aprono in questa scheda condivisa
    (`target="vr-admin"`). Aggiunto supporto `target` per-rotta nel FC
    (`public/index.php`) e reso nella barra.
  - Ctrl/mid-clic per aprire altrove resta sempre possibile (comportamento
    nativo del browser).

**Collaudo:** `php -l` su tutti i file OK. Render CLI (utente id 1): home = 1
fixed-head + fixed-body, 4 cartoline `target="vr-servizi"`, maloia.jpeg sulla
VRW; admin = 3 riquadri `target="vr-admin"`, niente "Configurazione", wide;
config Aspetto = due voci ("Sfondo piattaforma"=spagna, "Sfondo cortesia"=vuoto).
FC intero: barra = Piattaforma | Amministrazione (target vr-admin) |
Configurazione (_self). Cortesia via HTTP: eredita spagna finche' non si salva
buddha.

**Aperto:** dipingere le pagine INTERNE dei servizi col loro sfondo (VRW=maloia)
= integrazione VRW vera; integrazione vera VRS (login+WAN) + Step Nextcloud;
orfani `_cfg.php`/`_pecore.php`; rename CSS `.vv- -> .vrw-`; `lenostre_index ->
vrn_index`; "tab per servizio" ancora piu' fine se i named target non bastano.

---

## 2026-07-06 (3) - Permessi scrittura config + sfondo che veste le pagine del servizio

**Perche':** (1) salvando la config: `file_put_contents(env.json): Permission
denied` - php-fpm gira come `http` ma i JSON erano a sola lettura per il gruppo.
(2) vitti vuole lo sfondo del servizio (VRW=maloia) NON solo sulla cartolina ma
sulle PAGINE del servizio, e modificabile "come piattaforma e cortesia".

**Fatto:**

- **Permessi scrittura** (una-tantum, non codice): `boot/env.json` e
  `config/*.json` -> `664` (gruppo http in scrittura); cartella `config/` ->
  `2775` (setgid) cosi' "Aggiungi servizio" puo' creare il nuovo file col gruppo
  giusto. `env_backups/` e `config/backups/` erano gia' `g+w`. NB: era proprio il
  buco che mancava dopo aver reso editabile la config dalla pagina.
- **Sfondo che VESTE le pagine del servizio, config-driven, zero codice nel
  servizio** (`lib/vrp_services.php` + `boot/3_2_1_Go.php`): due funzioni nuove.
  `vrp_current_service()` riconosce il servizio ospitato in cui cade la richiesta
  (url targa vs SCRIPT_NAME, come la guardia). `vrp_service_bg()` (chiamata al
  boot dopo la guardia) bufferizza l'output e, se c'e' un wallpaper nella targa,
  inserisce un `<style>` PRIMA di `</head>` (o dopo il primo `<link .css>`) che
  mette lo sfondo con un velo scuro (.58->.82) per la leggibilita'. Vince sul tema
  perche' inserito per ultimo nella testata. Le pagine del wizard NON emettono
  quasi mai `<body>`: per questo dipinge la PIATTAFORMA, non le pagine. CSV,
  download e binari (senza `</head>` ne `<link .css>`) restano INTATTI (il
  callback li lascia passare identici). Il velo forte + i pannelli opachi del tema
  VRW tengono tutto leggibile; maloia incornicia ai lati del `.vv-wrap` (920px).
- **Sfondo modificabile come piattaforma/cortesia** (`pages/config.php`): nel
  riquadro di OGNI servizio, sopra il JSON grezzo, una voce amichevole **Sfondo**
  (id `f-wp-<servizio>`, univoco). Fonde nel `manifest.wallpaper` al salvataggio
  (il campo e' la fonte di verita'), con la stessa validazione file-esiste.
  Impostato `config/vrw.json` -> maloia.

**Collaudo:** `php -l` OK sui file toccati. Motore d'iniezione provato su 3
campioni: pagina con `</head>` -> style prima di `</head>`; pagina col solo
`<link .css>` -> style dopo il link; testo CSV -> intatto. Render CLI config:
voce Sfondo per ogni servizio, VRW prefillato con maloia. Permessi verificati
(664 gruppo http, config/ 2775). DA VEDERE IN BROWSER (loggato): una pagina del
wizard vestita di maloia.

**Aperto:** provare in browser il wizard vestito (e valutare se il gestore_foto,
UI ricca, sta bene o va escluso); poi integrazione VRS vera + Step Nextcloud;
orfani `_cfg`/`_pecore`; rename CSS `.vv- -> .vrw-`; `lenostre_index -> vrn_index`.

---

## 2026-07-06 (4) - VRW: sfondo senza velo, tema wide/translucido, menu cruscotto, rename import

**Perche':** feedback di vitti provando il wizard in browser (dal fiume). (1) maloia
troppo scuro = via il velo, foto originale; (2) heic_to_jpg + import "wide e pannelli
trasparenti/opachi come le altre pagine"; (3) il menu a 6 bottoni -> cruscotto con le
voci vive, i vecchi passi Amazon (Upload/Unzip/CSV) da parte; (4) `import_amazon.php`
-> togliere "amazon" dal nome (non si usera' piu').

**Fatto:**
- **Sfondo servizio SENZA velo** (`lib/vrp_services.php`, `vrp_service_bg()`): tolto il
  `linear-gradient` scuro dall'iniezione; ora `body{background:url(<wp>) center/cover
  fixed no-repeat}` = foto originale piena. La leggibilita' la garantiscono i pannelli
  translucidi (sotto), non piu' un velo che scuriva la foto.
- **Tema wide + pannelli translucidi** (`wiz_foto/theme.css`), vale per TUTTE le pagine
  del wiz in un colpo: `.vv-wrap` max-width 920 -> **1100**; `.vv-card`/`.vv-banner`/
  `details` da colore pieno a **rgba(.74) + backdrop-filter blur(4px)** cosi' lo sfondo
  del servizio traspare (stile pannelli di piattaforma). Aggiunte classi cruscotto
  `.vv-cards`/`.vv-tile` (tessere translucide).
- **Menu wizard -> CRUSCOTTO** (`wiz_foto/index.php` riscritto): 4 tessere vive
  (Scarica da Nextcloud, Converti HEIC->JPG, Import in leNostre, Gestore foto); i vecchi
  passi Amazon (Upload/Unzip/Esporta CSV) spostati in un `<details>` "Vecchi strumenti"
  (declutter ma reversibile, niente cancellazioni).
- **Rename `import_amazon.php` -> `import.php`** (mv) + aggiornati i 3 link che lo
  puntavano: `heic_to_jpg.php` (x2), `unzip_amazon.php` (x1), e il menu. Header-commento
  aggiornato. (Restano cosmetici interni: nome report `import_amazon_v4_*.txt`, _README.)
- **Step 0 `nextcloud_import.php`** creato come STUB tematizzato ("in costruzione" + il
  piano): la tessera non e' un clic a vuoto. La macchina vera (leggere
  `/data/<acct>/files/Photos`, copiare in staging, poi Import) = prossimo giro.

**Collaudo:** `php -l` OK su index/nextcloud_import/import/heic_to_jpg/unzip_amazon e
vrp_services. Render CLI del menu: 4 `.vv-tile` giuste + "Vecchi strumenti", zero
residui `import_amazon` nei link. Iniezione senza velo confermata nel sorgente. DA
VEDERE IN BROWSER (loggato): maloia originale + pannelli translucidi + cruscotto.

**Aperto (prossimo giro):** la MACCHINA dello step 0 Nextcloud (usare le 2 foto del
fiume in `Photos/2026/07/06` come cavie); valutare gestore_foto (UI ricca) con lo
sfondo; poi integrazione VRS; orfani `_cfg`/`_pecore`; rename CSS `.vv- -> .vrw-`;
`lenostre_index -> vrn_index`; report/README residui con "amazon".

---

## 2026-07-06 (5) - Step "Scarica da Nextcloud": import DIRETTO in leNostre (FATTO)

**Perche':** costruire la macchina vera dello step 0 (finora stub). Vitti al fiume,
su 5G (221 Mbit -> banda non e' il problema): auto-upload e meta-refresh RIMANDATI,
focus sull'import da Nextcloud.

**Fatto:**
- **Motore di import nella libreria** (`platform/lib/vrw.php`): verificato che le
  funzioni-motore stavano SOLO in import.php (nessuna collisione). Aggiunte in vrw.php
  con nomi `vrw_*` NUOVI (niente clash con le copie legacy di import.php, che resta
  intatto): `vrw_is_jpeg`, `vrw_exif_date`, `vrw_filename_date`,
  `vrw_filename_year_hint`, `vrw_safe_target_path`, `vrw_is_duplicate_by_size`, e
  **`vrw_ingest()`** (importa UN file: data EXIF->nome->[mtime]->indecise;
  dest `AAAA/AAAA_da_archiviare/MMGG/AAAAMMGG-HHMMSS.jpg`; niente sovrascritture;
  doppioni per dimensione; ritorna un array esito, con flag `dry`). NB `vrw_ingest`
  ha un nome diverso da `import.php::vrw_import_file` APPOSTA (entrambe caricate? no:
  import.php e' una pagina; la lib gira ovunque -> nome diverso = zero redeclare).
  DEBITO NOTO: migrare import.php a `vrw_*` e togliere le sue copie (TODO snello).
- **Pagina vera** `wiz_foto/nextcloud_import.php` (ex stub): legge la targa vrw
  (`nextcloud`: data_dir=/srv/dati/nextcloud/data, photos_subdir=Photos,
  accounts=[vitti,lella]); selezione account (whitelist); scansiona
  `data_dir/<acct>/files/Photos/AAAA/MM/GG` e mostra le cartelle-giorno con conteggio
  jpeg (checkbox, piu' recenti in cima); opzioni "solo anteprima" (default ON) e
  "fallback mtime"; import in **copy** (gli originali restano su Nextcloud) via
  `vrw_ingest` -> leNostre. Whitelist dei giorni dal set scansionato (no traversal).
  Solo JPEG per ora (video = TODO). Livrea vv-theme (maloia + pannelli translucidi).
- Le vecchie funzioni restano in import.php: intatto, nessun rischio.

**Collaudo:** `php -l` OK (vrw.php + nextcloud_import.php). **DRY-RUN reale** (harness
CLI, utente 1, POST acct=vitti days=2026/07/06 dry_run=1): Lette 2, Importate 2,
Indecise/Doppioni/Errori 0; datazione `[NOME]` corretta (IMG_20260706_105246 ->
06/07 10:52:46); **zero scritture** su leNostre confermato. Permessi: `leNostre` +
`leNostre/2026` = drwxrwsr-x gruppo http -> php-fpm(http) scrive. Manca solo la prova
REALE dal browser (vitti loggato) -> deseleziona "solo anteprima" e importa.

**Aperto:** import REALE dal browser (le 2 foto fiume) + vederle in Gestore foto ->
poi mostrare a ELENA; VIDEO (vrw_ingest fa solo jpg, i .mp4/.mov vanno gestiti a
parte, magari in una cartella video di leNostre); barra "pecore" per lotti grossi
(ora e' POST sincrono); migrare import.php al motore `vrw_*`; auto-upload app +
meta-refresh servizio (rimandati).

---

## 2026-07-06 (6) - Rifiniture flusso wiz + auto-HEIC nell'import

**Perche':** feedback di vitti dopo il dry-run. (1) gestore_foto senza ritorno al
wiz; (2) heic_to_jpg con rimandi vecchi "Step 2/Step 4"; (3) import.php ormai
inutile (l'import Nextcloud va dritto in leNostre); (4) pagina Nextcloud troppo
lunga; (5) HEIC: metterlo tra i vecchi e convertirlo AL VOLO nell'import.

**Fatto:**
- **gestore_foto.php**: aggiunto `<a class="btn" href="index.php">↑ Wizard</a>` in
  testata (vitti: "poco visibile ma una volta imparato ok" - lasciato, si vedra' con
  Elena).
- **heic_to_jpg.php**: tolti i rimandi "← Step 2: Unzip" e "Step 4: Importa" (blocco
  nav + pannello pecore) -> resta solo "↑ Torna al Wizard".
- **index.php (menu)**: cruscotto ridotto a 2 tessere vive (Scarica da Nextcloud,
  Gestore foto). "Import da staging" e "Converti HEIC (manuale)" spostati in
  "Vecchi strumenti".
- **nextcloud_import.php**: giorni in un riquadro SCORREVOLE (max-height 220,
  overflow) + checkbox "seleziona tutti" (JS). Non allunga piu' la pagina.
- **AUTO-HEIC nell'import**: nuovo in `platform/lib/vrw.php` -> `vrw_is_heic()` +
  `vrw_heic_to_jpg()` (heif-convert -> magick -> convert, il primo disponibile;
  tool memoizzato; nomi NUOVI, niente clash con `vrw_pick_converter/vrw_run_convert`
  di heic_to_jpg.php). In nextcloud_import: la scansione conta jpg+heic; nell'import
  vero un HEIC viene convertito in un jpg temporaneo (sys_get_temp_dir, basename
  conservato per la datazione dal nome), poi ingerito e il temp cancellato; in
  anteprima e' solo segnalato. Tutti i tool presenti sul server (heif-convert,
  magick, convert).

**Collaudo:** `php -l` OK. Dry-run 2026/07/06 ancora 2/2. Menu = 2 tessere.
**Round-trip HEIC provato davvero**: magick crea un .heic di prova ->
`vrw_heic_to_jpg()` lo riconverte in jpg (172 KB, rc 0). Manca solo l'import REALE
dal browser (vitti sta per farlo).

**Aperto:** import reale + demo Elena stasera; **svuota Nextcloud dopo import via
WebDAV** (destructive: serve app-password per non desincronizzare l'indice NC -
proposto, non ancora fatto); video; migrare import.php al motore `vrw_*`.

**FIX 06/07 - banner "senza feedback":** vitti fa l'import reale (funziona: 2 file
in leNostre/2026/2026_da_archiviare/0706 come 20260706-HHMMSS.jpg) ma "nessun
feedback". Causa: i `.vv-banner.vv-ok/err/warn` avevano sfondo a **14%** (nati per
il fondo scuro pieno) -> su maloia sparivano. Reso opachi ~88% (dark tinto per
tipo, bordino colorato) in theme.css. Verificato riproducendo il POST reale
(harness): il banner c'era gia' nell'HTML (Lette 2 / Doppioni 2 al re-run = dedup
OK), era solo invisibile. Nota generale: i tint a bassa opacita' non reggono sopra
i wallpaper -> alzare l'alpha.

---

## 2026-07-07 - Motore img condiviso (vrp_img) + anteprime Nextcloud + Esci ovunque

Giornata nata dal collaudo dell'import con Elena (punch-list in memoria
project_vrw_demo_elena): rifiniture UX + il primo pezzo di piattaforma che serve
DAVVERO a piu' servizi (un motore immagini condiviso).

**Rifiniture pagina import (nextcloud_import.php):**
- Lista giorni da colonna singola a **griglia responsiva** (2-3 colonne,
  `repeat(auto-fill,minmax(180px,1fr))`), box piu' alto (poi 460px).
- **"Solo anteprima" default OFF** (`$dryRun` GET=false): il primo clic importa
  davvero. Elena vuole FARE, non guardare a vuoto.

**MOTORE IMMAGINI CONDIVISO - `platform/lib/vrp_img.php` (estratto da maps/img.php,
che resta INTATTO):** pattern **motore puro + cappelli**. Due funzioni pure, stile
di casa (@desc/@param, fail-loud): `vrp_img_resolve($src,$bases)` (web-prefix ->
path reale + guardia anti-traversal) e `vrp_img_serve($fsPath,$w,$opt)` (resize +
cache disco + orientamento EXIF + ETag; EMETTE ed esce). Costanti
VRP_IMG_ALLOWED_W/DEFAULT_W/QUALITY. **Nessun gate nel motore** (scelta chiave): chi
puo' vedere cosa lo decide il CAPPELLO; il motore ha i pomelli `cache_dir` e
`visibility:public|private` per gli alberi sensibili. Collaudo CLI: lint, traversal
-> NULL, ridotta una foto leNostre (480x328) *e* una Nextcloud (480x853) = serve
entrambe le radici; cache creata.

**ANW nel nuovo schema:** il motore non puo' esporre ANW (non gestisce accessi). La
regola odierna (path sotto `/srv/http/leNostre/ANW/` -> LAN-only + cache privata)
diventa un WRAPPER esplicito nel futuro cappello maps; STESSA ricetta del cappello
Nextcloud (albero sensibile = LAN + private). In collaudo non renderizzata alcuna
foto ANW di proposito.

**Cappello `wiz_foto/nc_thumb.php`** (thumbnail SORGENTE su Nextcloud per l'anteprima
prima dell'import): require `vrp_img.php`, gate `vrp_is_lan()`, data_dir dalla targa
vrw, `vrp_img_resolve($src,['/ncsrc'=>$dataDir])`, **DOPPIA CINTURA** (anti-traversal
+ il path reale deve contenere `/files/Photos/` -> note/BANCHE NON sbirciabili),
serve `cache_dir=null` (on-the-fly v1) + `visibility:private`.

**Anteprime per-giorno in nextcloud_import.php:** la scansione raccoglie i basename
jpeg per giorno (`$dayJpegs`; heic contati ma non sbirciabili). Ogni giorno ha 👁;
click -> pannello con griglia `<img loading=lazy src="nc_thumb.php?...">` on-demand.
**Selezione per-foto (richiesta di Elena):** ogni thumbnail ha una casella (default
spuntata=importa); deselezioni le schifezze -> l'import le SALTA (JS Set `ncExcluded`
-> hidden `exclude[]`; PHP: `$excl=array_flip($_POST['exclude'])`, skip su
`$rel.'/'.basename`). **Multi-cartella** GIA' possibile (esclusioni globali).

**Rifiniture 2a-3a prova browser:**
- Spiegazione lunga -> **tooltip** (title=) sull'h1 + icona ⓘ; la card mostra solo
  Sorgente/Destinazione.
- **Dopo dry-run pronto per import:** giorni restano `checked` (`$postedDays`), spunta
  anteprima si toglie (`(!$didImport && $dryRun)`), esclusioni preservate (JS
  `NC_EXCL_INIT` seed).
- **Toggle seleziona/deseleziona tutte** nel pannello (`#ncPrevToggle`, riusa
  l'handler per-foto via dispatchEvent).
- **Il giorno segue le foto** (`ncSyncDay`): aprire 👁 auto-spunta il giorno; deselezioni
  tutte -> si dispunta. Risolve "seleziono la foto ma il giorno resta vuoto".

**Esci in OGNI pagina-servizio - `vrp_service_esci()` in lib/vrp_services.php**,
chiamata nel boot `3_2_1_Go.php` dopo `vrp_service_bg()`. Gemella dello sfondo:
bufferizza l'output e inietta un "Esci" fisso (in alto a dx) SOLO nelle pagine-servizio
loggate (che non hanno la barra di piattaforma). **Stessa guardia salva-binari** dello
sfondo (inietta dopo `<link .css>` o prima di `</head>`; CSV/immagini/binari INTATTI ->
i JPEG di nc_thumb non si corrompono). URL assoluto `vrp_url('public/?p=logout')` (le
pagine-servizio sono fuori dal front controller).

**Collaudo:** `php -l` su tutti i file OK; ASCII puro, zero curly-quotes; cintura Photos
provata (Photos AMMESSO / Notes BLOCCATO); iniezione Esci provata su 3 output (pagina
wiz INIETTATO, con-head INIETTATO, binario JPEG INTATTO). NON verificato dal vivo nel
browser i pezzi che richiedono la sessione (render anteprime, comparsa Esci): li prova
vitti loggato.

**Doc:** B200 aggiornato (sez.0 strato meta-utility search+img; sez.3b la piattaforma
inietta cornice - sfondo+Esci - nelle pagine servizio). API rigenerate con
`genera_doc.php`.

**Aperto/debito:** cache privata su disco per nc_thumb (scegliere dir scrivibile+negata
da nginx); cappello maps ANW-aware quando si rifara' maps/img.php (originale ora intatto);
versione leNostre delle anteprime = riusa `vrn_foto_search`+`vrp_img`; svuota-Nextcloud
post-import (WebDAV, destructive); video; migrare import.php a vrw_ingest.

---

## VRS - Port dello Share nella piattaforma (08/07/2026) - CHIUSO

**Cosa:** portato il servizio Share (VittRos Share) da `/srv/http/share` (standalone, core
VRV) a `/lab/public/share` nella piattaforma. Primo servizio ospitato "pieno" dopo il wizard.

**Struttura:** lib `platform/lib/vrs.php` (funzioni `vrs_*`, convenzione VRP); pagine
`public/share/` (index, download, upload, create_dir, thumb, delete) + `anw/verboten.php`.
I dati `share_dirs`/`share_files` restano in `vittrosviaggi`, registrato come DB `vrv`
(secret stabile `secrets/db_vrv.php`). Identita' di piattaforma (`vrp_current_user`).

**Feature:** thumbnail immagini via `vrp_img` (thumb.php); anteprime VIDEO via fotogramma
`ffmpeg` (cache `.vthumb`) con badge play + fallback `onerror`; upload multi con PRG
(redirect in cartella + banner + overlay d'attesa col filmato pecore `.mp4`); cancella HARD
(unlink fs + DELETE riga + rmdir) con guardia `vrs_can_manage` (proprietario o admin);
griglia a mattonelle.

**Permessi:** ruoli VRS = `admin` + `registrato` (registrato = qualsiasi loggato, nessun
ruolo da assegnare). Fix `home.php`: la cartolina dei servizi `auth:required` la vede ogni
loggato (visibilita' = stessa regola della guardia d'accesso, non piu' per-ruolo).

**Verboten (gag 403):** pagina mockup con foto naturista gatata (LAN+login), 2 versioni
(full / purga: cartello `FRECH!` con ImageMagick sul petto + taglio basso), in `share/anw/`,
nessun link visibile.

**Sessioni admin:** filtri username (LIKE) + action (dropdown da valori distinti) + pulisci
filtri; sort su username gia' presente. Preparazione al registro azioni.

**Generatore doc:** `genera_doc.php` ora aggiunge **"Usato in"** (grep dei punti di chiamata,
`file:riga`) per ogni funzione. API rigenerate (83 funzioni).

**Collaudo:** `php -l` su tutti i file; giri HTTP autenticati (login bot usa-e-getta,
ripristinato): render share, thumb immagine+video JPEG puliti, upload PRG, delete hard,
verboten full+purga, cartolina home per utente senza ruolo vrs. `nginx client_max_body_size`
50M->2g (sudo vitti) per gli upload grossi.

**Aperto:** registro azioni (logging `vrp_action`, scelta A/B); VittExplorer -> `vrp_explorer`
(estrarre da share + media_popup VRV); leNostre50 sul vecchio `/maps/img.php` -> `vrp_img`;
unificazione utenti in `vrp_auth` (owner dei file vecchi).

---

## 2026-08-02 - VRW: i FILMATI entrano in leNostre (import + Gestore)

**Perche':** richiesta di Elena (vedi [RICHIESTE_ELENA](RICHIESTE_ELENA.md)). Il wiz
prendeva solo JPEG/HEIC, quindi dal 2025 i video non entravano PIU' in archivio: 2023
79 filmati, 2024 130 (di cui 125 messi a mano in `ANNO/9999 Filmati`), **2025 3, 2026 0**.
In piu' il rischio vero: pulendo Nextcloud dopo l'import si cancellavano filmati mai
archiviati (successo il 02/08: 3 filmati veri recuperati a mano dal cestino Nextcloud).
Il TODO era gia' scritto qui il 06/07 ("VIDEO: vrw_ingest fa solo jpg").

**Decisione presa con Elena:** i filmati vanno nella **cartella-giorno insieme alle
foto**, NON in una cartella video separata. Cosi' quando sposta le foto di un evento,
il filmato la segue (ed e' come stanno i 407 filmati gia' in archivio).

**Fatto:**
- **`platform/lib/vrw.php`** - costanti `VRW_VIDEO_EXT` (mp4/mov/m4v/3gp/avi/mpg/mpeg/
  mts/mkv/webm) + `VRW_FFPROBE`/`VRW_FFMPEG` a path ASSOLUTO (php-fpm ha il PATH scarno,
  regola B200 6a). Tre funzioni nuove: **`vrw_is_video()`**, **`vrw_video_date()`**
  (i video non hanno EXIF: `creation_time` del contenitore via ffprobe, riportato al
  fuso LOCALE o la cartella-giorno sbaglia di un'ora) e **`vrw_is_live_photo()`**.
- **Live Photo**: i `.mov` da 0,4-6 MB dell'iPhone sono il pezzo-video di una foto,
  non filmati. Si riconoscono dal gemello con lo stesso stem (`X.mov` + `X.jpg`) e si
  SCARTANO: la foto entra per conto suo. Scelto il gemello e non una soglia di
  dimensione - la soglia sbaglierebbe sui filmati brevi veri.
- **`vrw_ingest()` consapevole dell'estensione**: `$ext` = `jpg` per le foto (gli HEIC
  arrivano gia' convertiti), la sua per i filmati. Toccati i due punti che avevano
  `.jpg` cablato (nome datato + sequenza `indecise`). Catena data: EXIF -> **ffprobe
  (solo video)** -> nome -> mtime -> indecise.
- **`vrw_filename_date()` + anno a DUE cifre** (`26-03-11 15-37-37 9907.mov`): e' come
  nomina il caricamento automatico di Nextcloud, e nessun pattern esistente lo prendeva
  (per questo il .mp4 del 6/2, che non ha `creation_time`, sarebbe finito in indecise).
  Messo per ULTIMO ed accettato solo se `checkdate()` + ora valida, cosi' un numero di
  serie qualsiasi non passa per data.
- **`wiz_foto/nextcloud_import.php`** - scansione e import dei video; contatore `live`
  (clip scartate) nel banner e nel log; badge 🎬 col numero di filmati accanto al
  conteggio foto di ogni giorno; tooltip di testata aggiornato ("I video arriveranno"
  -> ci sono).
- **`wiz_foto/vrw_provider.php`** - il Gestore ELENCA i filmati (prima solo jpg/jpeg/
  png, per questo i 407 video in archivio erano invisibili): mattonella col fotogramma,
  bollino ▶, `href` al file. **NIENTE `full`**: il lightbox dell'explorer e' un `<img>`
  e un video lo romperebbe; senza `full` il video resta fuori dall'array immagini e il
  clic cade sul ramo `it.href` -> si apre nel lettore del browser.
- **`wiz_foto/vrw_vthumb.php` (nuovo)** - cappello "filmati": estrae un fotogramma con
  ffmpeg (a 2s, per non pescare il nero iniziale) e lo serve da `vrp_img`. Gemello di
  `nc_thumb.php` e di `share/thumb.php`. Cache in `VRP_SHARED_DIR/vrw_vthumb`, FUORI da
  leNostre (l'archivio va nel backup notturno: non deve ingrassare di roba ricavabile);
  chiave = sha1(path + mtime), cosi' si invalida da sola.
- **ZERO JavaScript**: `vrp-explorer.js` aveva gia' `href` e `badge`. Nessun file JS toccato.
- **Indice `lenostre.foto` non toccato**: `lenostre_index.php` filtra `jpe?g|png|heic`,
  quindi i video restano fuori dall'indice foto. Voluto: l'indice serve a GPS e ricerca
  FOTO. (Se un domani si vorranno i video cercabili, e' li' che si mette mano.)

**Collaudo (CLI, sola lettura):** `php -l` sui 4 file. Harness sui 3 filmati veri:
`26-03-11 15-37-37 9907.mov` ffprobe `14:37:37Z` -> **15:37:37** locali = identico al
nome (fuso giusto); il `.mp4` del 6/2 non ha `creation_time` -> ripiega sul nome ->
`0206/20260206-112343.mp4`; il `.mov` di set-2025 -> `0906/20250906-083955.mov`.
Live Photo: coppia sintetica `.mov`+`.jpg` -> scartata, filmato senza gemella ->
importato. Non-regressione foto: jpg con EXIF -> `20260202-131901.jpg`, identico a prima.
Date-dal-nome fasulle (`99-99-99`, mese 34) -> null. vthumb: risoluzione path OK,
traversal `/leNostre/../../etc/passwd` RESPINTO, fotogramma 640x1138 in 315 ms, immagine
vera (sala del concerto, non nera). Scansione sui dati veri: `lella` 2026/02/06 1 filmato
(prima invisibile), `vitti` 179 foto + 2 filmati.

**Aperto:** prova REALE dal browser da loggati (l'import e il Gestore rispondono 302 a
curl anonimo, come tutto il wiz) - da fare con Elena; poi la pulizia dell'account `vitti`
su Nextcloud (25 filmati nel cestino da guardare PRIMA di svuotare). Da valutare: `mpg` e
`avi` vecchi (ffprobe spesso non ha `creation_time` -> finiranno per nome o mtime).

---

## 2026-08-02 (2) - VRW filmati: i conteggi (collaudo di Elena)

**Perche':** Elena ha collaudato i filmati e ha trovato il difetto giusto: la cartella
`0206` mostrava **"0 foto"** pur contenendo il filmato appena importato. La sua proposta
("scrivere 0 file") non bastava: il numero non era solo etichettato male, era **sbagliato**
- veniva dall'indice `lenostre.foto`, che i video non li ha e non li deve avere.

**Fatto:**
- **`vrw_count_videos()`** in `platform/lib/vrw.php`: conta i filmati sotto una cartella,
  ricorsivo, dal FILESYSTEM (`RecursiveCallbackFilterIterator` che pota `.cestino`,
  `thumbs` e le cartelle nascoste). Misurato sul mang3: **0 ms** per una cartella-giorno,
  **12 ms** per un anno, **225 ms** per l'intero archivio (54.800 file, 424 filmati) -
  solo la radice paga, e paga una volta. Niente cache: costo noto e accettabile, meglio
  che inventarsi un'invalidazione (stile "codice essenziale" del B200).
- **Mattonelle-cartella** (`vrw_provider.php`): `sub` ora unisce le due fonti - le foto
  dall'indice, i filmati dal filesystem: `"12 foto · 2 🎬"`. Il conto video sta **fuori**
  dal `try` del DB: se il database e' giu' si perdono i numeri delle foto, non anche
  quelli dei filmati. `"0 foto"` resta quando non c'e' NIENTE (e' il segnale di
  "cartella gia' lavorata" a cui Elena e' abituata).
- **Conteggio in toolbar**: aveva lo stesso difetto al contrario (contava i video ma li
  chiamava "foto"). Scelta di Elena fra tre opzioni: **"2 foto · 1 filmato"**. Realizzato
  con l'unico ritocco JS di tutto il lavoro, e ADDITIVO: `vrp-explorer.js` usa
  `res.countLabel` **se il servizio lo manda**, altrimenti conta come sempre con
  `itemNoun` -> lo share non cambia di una virgola. L'etichetta la scrive il provider,
  che e' l'unico a sapere cosa e' foto e cosa e' filmato.
- **Lista dei giorni nell'import**: stessa cura, "1 foto · 1 🎬" era fuorviante per un
  giorno di soli filmati -> ora si nomina solo cio' che c'e' davvero.

**Collaudo:** `php -l` + `node --check`. `vrw_count_videos`: `0206` 1, `2026` 2,
`2024` 130, `2024/9999 Filmati` 125 - coerenti col `find`. Provider chiamato da CLI:
`0206` -> toolbar "1 filmato" e la mattonella sopra "1 🎬"; `0202 Parlasco` -> "8 foto"
(nessuna regressione); `2026_da_archiviare` -> "0 foto" + mattonella `0206` "1 🎬".
URL delle anteprime: `vrw_base_url()` e' corretto perche' l'AJAX dell'explorer passa
dalla STESSA pagina (`gestore_foto.php` fa `vrp_explorer_dispatch`), come confermato
dalla prova di Elena nel browser.

**Aperto:** ri-collaudo di Elena sui conteggi; poi la pulizia dell'account `vitti` su
Nextcloud (25 filmati nel cestino da guardare PRIMA di svuotare).

---

## 2026-08-05 - VRW: il bottone "Pulisci su Nextcloud" (richiesta di Elena)

Chiusura della richiesta del 03/08: dal Wizard, togliere da Nextcloud le foto
gia' archiviate, senza cambiare strumento. Progetto in `VRW_pulizia_nextcloud.md`
(scritto il 03/08, discusso e approvato oggi). Da collaudare: Elena, nel pomeriggio.

### I due punti aperti sono decaduti misurandoli

Vitti: «penso che Elena non produrra' piu' Live Photo, e non penso che ce ne
siano adesso». Verificato: su Nextcloud **zero `.mov`, zero heic** - 188 file in
tutto, 183 jpg + 3 filmati veri (nessuno con foto gemella) + 2 note. Quindi
niente spunta «togli anche le clip», niente scelta sugli heic: la **regola secca**
(«si propone solo cio' che si ritrova in leNostre») tratta bene tutt'e due i casi
da sola, e li lascia dov'e' sono senza una riga di codice apposta.

### Il vincolo dei filmati nel cestino, sciolto

34 filmati nei cestini, **33 non erano in archivio**. I doppioni apparenti erano
il telefono che carica due volte (`InstantUpload/Camera` + `Photos`): distinti,
18 di `vitti` (3,13 GB) e 9 di `lella` (di cui 5 sono clip di Live Photo, 1920x1440
hevc ~3 s). I 18 rimessi su Nextcloud **file per file** nella loro cartella-giorno
- NON ripristinando la cartella-mese dal cestino, che avrebbe riportato su anche
centinaia di foto gia' archiviate. Verificati 18/18, byte identici.

### Il codice (3 file + la targa)

| File | Cosa |
|---|---|
| `platform/lib/vrw.php` | `VRW_NC_CREDS` (puntatore ai segreti); `vrw_nc_creds()`, `vrw_nc_configurato()`, `vrw_nc_dav()`, `vrw_nc_trash()`; `vrw_archivio_indice()`, `vrw_nome_normalizzato()`, `vrw_gia_archiviato()`, `vrw_nc_esame_giorno()`; **`vrw_data_scatto()` estratta da `vrw_ingest()`** |
| `wiz_foto/nextcloud_import.php` | bottone 🧹 sulla riga del giorno, pannello del ricontrollo, conferma, registro azioni |
| `platform/config/vrw.json` | `nextcloud.creds` = puntatore al file dei segreti |
| `secrets/nextcloud_app.php` **(nuovo)** | le due password-applicazione, 640 gruppo `http`, fuori dall'albero web |

Backup `.bak_20260805_131826_prepulizia` per i tre file toccati.

### Le tre scelte non ovvie

1. **`vrw_data_scatto()` estratta apposta.** Il ricontrollo deve calcolare lo
   STESSO nome di destinazione che calcola l'import: se le due catene di date
   (EXIF -> ffprobe -> nome -> mtime) divergessero, direbbe «non archiviato» per
   file archiviatissimi, o peggio il contrario. Una sola sorgente di verita'.
2. **Il ricontrollo cerca in TUTTO l'archivio, non dove l'import aveva messo.**
   Il metodo del documento (rilanciare l'import in anteprima e leggere gli
   `SKIPPED`) sembra equivalente ma **non lo e'**: Elena sposta le foto nelle
   cartelle degli eventi, e li' l'anteprima non le ritrova piu'. Misurato: il
   giorno `2026/07/14` da' **18 gia' in leNostre**, ritrovate in `ANW/2026/0714
   Secchiello Selvaggio - Compleanno Seneca/` - col vecchio metodo sarebbero
   risultate tutte non archiviate, e il bottone non avrebbe pulito niente
   proprio nei giorni gia' sistemati. Il metro resta stretto: nome di
   destinazione (data al SECONDO) + dimensione IDENTICA al byte.
3. **La mtime non vale per la pulizia** (`NC_PULIZIA_MTIME = false`), nemmeno se
   l'import l'aveva usata: e' la piu' fragile delle date e qui si decide se
   buttare. Chi e' stato archiviato per mtime risulta «non ritrovato» e resta
   dov'e': si sbaglia dalla parte giusta.

### Prestazioni: una trappola presa e risolta

Prima versione: indice `nome-file -> [size,path]` e per ogni file una regex su
tutto l'indice -> **1,5 s** per il giorno da 135 foto (135 x 32.000 regex).
Ora la chiave dell'indice e' il **nome normalizzato** (minuscolo, senza la coda
`_001` dell'anti-collisione), quindi la ricerca e' un accesso diretto:
**14 ms**, stessi risultati. L'indice si costruisce in 0,21 s per 54.800 file.

### Collaudo fatto (CLI)

- Prova a vuoto WebDAV: PROPFIND 207 su tutt'e due gli account; file finto
  creato -> cancellato -> **nel cestino** -> **ripristinato** -> ripulito.
- **La chiave di `vitti` sull'area di `lella` da' 404**: l'admin non vede i file
  degli altri (sez. 4b del progetto: risposta definitiva, servono due chiavi).
- `vrw_nc_trash()`: cestina (204), e **ricancellare un file assente e' un
  successo** (404 trattato come ok: qualcuno l'ha gia' tolto).
- Account senza chiave: risponde male senza esplodere; se il file dei segreti
  manca, il bottone **non compare** proprio.
- I file finti delle prove tolti anche dal cestino: nessuna spazzatura lasciata.
  I 34 filmati veri nel cestino: intatti.
- `php -l` OK, zero virgolette tipografiche. Doc rigenerata: **229 funzioni**.

⚠️ Tolta `curl_close()` dal codice nuovo: dal PHP 8.0 non fa nulla e dall'8.5 e'
**deprecata** - riempiva il log di avvisi (e su una pagina AJAX romperebbe il
JSON, vedi il baco noto dell'«errore 200»).

### Resta

- **Collaudo di Elena**, un giorno solo dell'account `lella`.
- Importare dal Wizard gli 11 giorni dei filmati ripristinati; **solo dopo** si
  possono svuotare i cestini.

## 2026-08-05 (2) - VRW: il REGISTRO degli import (la scopetta regge alla cernita)

Seguito della voce precedente, aperto da una domanda di Vitti a codice finito -
il tipo di domanda che vale piu' di una giornata di collaudo.

### Il buco

«La scopetta controlla se le foto sono state importate, ma se Elena dopo fa una
cernita e ne cancella alcune, o le sposta nella cartella definitiva, non le
trovera' piu' e non permettera' la cancellazione. E' cosi'?»

Meta' si'. Lo **spostamento** era gia' coperto (la ricerca guarda in tutto
l'archivio). La **cancellazione** no. E Vitti ha aggiunto: **«Elena usa spesso il
rename»** - se rinomina un FILE, il confronto per nome-di-destinazione salta.

Poi l'osservazione che ha cambiato il disegno: **«se esiste il cestino del
Nextcloud + il cestino del VRW, mi sembra che stiamo facendo il caso piu' grosso
di quello che e'»**. Vero: la regola «deve stare in leNostre ADESSO» era stata
scritta immaginando la cancellazione come definitiva, ma ci sono **due reti**.
Si chiedeva alla scopetta una garanzia gia' data due volte.

**La domanda giusta non e' «e' in leNostre adesso?» ma «e' mai stata importata?»**
- un fatto che non cambia piu'. E la sa l'import.

### Fatto

Tabella **`vrp_auth.vrw_import`** (DDL lanciata da Vitti: `vrp_auth_app` ha solo
SELECT/INSERT/UPDATE/DELETE). Disegno suo: la history resta il diario umano con
la riga-riassunto, i dettagli file-per-file stanno a parte e **li legge solo la
scopetta**; `session_id` (FK a `sessions`, ON DELETE SET NULL) lega le due cose
per poter indagare.

- `vrw.php`: `vrw_import_registra()` (scrive, non-bloccante), `vrw_import_registro()`
  (legge un giorno in mappa `nome|dimensione`), e `vrw_nc_esame_giorno()` con il
  parametro `$registro` in coda (default `[]` -> chiamate vecchie invariate).
- `nextcloud_import.php`: raccoglie `$registrati` durante l'import vero e li
  annota dopo `vrp_insert_action()`, che gia' restituiva l'id della riga-riassunto.

### Le tre scelte da non tradire

1. **NON si annota la destinazione.** Annotarla farebbe invecchiare il registro
   al primo spostamento o rinomina: esattamente il male da cui deve salvare. Si
   annota il FATTO.
2. **`src_size` e' la dimensione dell'ORIGINALE su Nextcloud** (`$src`, non
   `$ingestSrc`): un heic convertito in jpg pesa un'altra cosa.
3. **Due strade indipendenti, basta una**: il registro (futuro, regge a
   spostamenti/rinomine/cernite) e la ricerca in archivio (passato, cioe' tutto
   quello importato prima che il registro esistesse). Resta su Nextcloud solo
   cio' su cui **nessuna delle due** sa dire di si'.

### Numeri, contro la paura della crescita

Vitti: «ho paura che si riempia all'infinito». `sessions` fa **1.560 righe in 3
settimane, 640 KB** = ~11 MB/anno. `vrw_import` cresce col materiale che passa:
in leNostre entrano ~2.700 file l'anno (2023: 2.589 / 2024: 3.034 / 2025: 2.483
/ 2026: 2.530) = **~350 KB/anno**. Vent'anni in 7 MB. **Trenta volte piu' piano
della history che c'e' gia'.**

### Verifica dell'altra paura ("non sono sicuro che il DB sia diverso")

Controllato invece di rassicurare: `db_vrv.php` -> `vittrosviaggi` (utente
`vviaggi_app`), `db_vrp_auth.php` -> `vrp_auth` (utente `vrp_auth_app`), piu'
`lenostre` e `photo`. E la prova: `SHOW DATABASES` **con le credenziali della
history** risponde **un database solo**, il suo. Da li' la roba di Elena non si
tocca nemmeno sbagliando a scrivere una query.
⚠️ Ma la correzione di Vitti restava giusta: `vrp_auth` **e' il DB da cui dipende
il VRW che Elena usa** (login/sessioni). Non "un DB che non c'entra": una
CREATE TABLE e' additiva e non tocca `sessions`, ma la prudenza era fondata.

### Collaudo (CLI, transazione ANNULLATA: zero righe lasciate)

| caso | esito |
|---|---|
| mai importato, non in archivio | **resta** (giusto) |
| nel registro | **si pulisce** |
| **dopo rinomina/spostamento/cestino** (indice archivio VUOTO = caso peggiore) | **si pulisce ancora** |
| non nel registro | **resta fuori** (mano non allargata) |
| strada vecchia (ricerca in archivio) | intatta: 2026/07/14 da' sempre 18 |

`php -l` OK, zero virgolette tipografiche, pagine 302. Doc rigenerata: **231
funzioni**. `sessions.id` e' `int unsigned` (non bigint): la colonna di aggancio
e' stata fatta uguale, se no la chiave esterna non legava - trovato guardando lo
schema invece di andare a memoria.

---

## 2026-08-05 (3) - VRW: l'ordine dei controlli, e il registro che non veniva letto

Coda delle due voci precedenti. Il registro c'era, funzionava, era collaudato -
e per una categoria di file **non veniva mai consultato**. Trovato rileggendo
`vrw_nc_esame_giorno()` dall'alto, non da un sintomo.

### Il difetto

I controlli sul TIPO del file (clip Live Photo, heic, non-media) stavano **sopra**
la consultazione del registro. Un heic usciva sempre dalla porta «entra convertita
in jpg, quindi l'originale non si ritrova uguale» e non arrivava mai alla prima
strada: **per gli heic la strada (a) era codice morto**. Un heic davvero importato
sarebbe rimasto su Nextcloud per sempre pur essendo archiviato - il contrario
esatto di cio' che il registro serve a garantire.

Nessun danno oggi: heic su Nextcloud zero, misurato ieri. E' il tipo di difetto
che si manifesta il giorno che qualcosa non si pulisce e nessuno capisce perche'.

### La regola, in una riga

> Il **tipo** del file dice solo se ha senso cercare un gemello identico in
> archivio: riguarda la strada (b), non la (a). Il **registro** e' un fatto, e i
> fatti si guardano per primi.

### Ordine nuovo

1. **clip Live Photo** -> resta fuori prima di tutto, **anche del registro**.
   In archivio non entra mai; se il registro dicesse il contrario sarebbe lui a
   sbagliare, e qui si tiene il file, non lo si butta.
2. **registro** (strada a).
3. **heic / non-media** -> restano fuori (qui il tipo conta).
4. **ricerca in archivio** (strada b), invariata.

Il punto 1 e' la scelta non ovvia: e' l'unico posto dove il registro **non** ha
l'ultima parola. Tenerlo in cima fa si' che il falso positivo noto di
`vrw_is_live_photo()` (il `1224 Natale dal Bruno.MOV` del 2011, MJPEG con la jpg
gemella - vedi il progetto sez. 8bis) continui a sbagliare **dalla parte giusta**.
Un file in piu' su Nextcloud costa quota; un file in meno costa un ricordo.

### Collaudo (CLI, cartella finta: nessun file vero toccato, niente DB)

Il `$registro` passato a mano, cosi' si prova la funzione e non il giro intero.

| file | nel registro? | esito |
|---|---|---|
| `a.jpg` | si' | **si pulisce** |
| `b.heic` | si' | **si pulisce** ← prima restava fuori |
| `c.heic` | no | resta (entra convertita in jpg) |
| `d.mov` + `d.jpg` gemella | si' | **resta** (vince il punto 1) |
| `e.txt` | no | resta (non e' foto ne' filmato) |
| `f.jpg` | no | resta (non importata, non ritrovata) |

`php -l` OK. Backup `vrw.php.bak_20260805_173425_preheic`. Aggiornati il progetto
(`VRW_pulizia_nextcloud.md` sez. 8 punto 2, corretta, e nuova sez. 12) e questa
doc. Il registro `vrw_import` e' ancora **vuoto**: nessun import vero e' ancora
passato dal codice nuovo, si riempie al primo. `AUTO_INCREMENT = 3` e' l'unica
traccia del collaudo di ieri, la cui transazione era stata annullata.

⏳ Resta il collaudo di Elena su un giorno solo dell'account `lella`.

---

## 2026-08-05 (4) - VRW: il collaudo di Elena, e i due buchi che ha fatto uscire

Collaudo fatto da Elena la sera stessa, in autonomia. **Passato.** E come sempre
il collaudo vero non conferma il codice: gli fa le domande che il codice non si
era fatto.

### Il giro completo funziona

Il registro `vrw_import` e' passato da vuoto a **591 righe** (206 `imported` +
382 `skipped` per `vitti`, 5 per `lella`) e la scopetta ha ripulito Nextcloud
giorno per giorno. Verificati a mano il primo giro (`lella` 2026/08/01, 2 foto) e
i filmati della Festa Alpini (`vitti` 2026/06/28, 6 file, 2,6 GB): pesi identici
al byte in leNostre, file **nel cestino** di Nextcloud e non svaniti, traccia in
`sessions`. La strada (a), il registro, ha funzionato al primo colpo su materiale
mai importato prima - che era esattamente il pezzo non ancora provato.

**Il vincolo dei filmati e' sciolto per davvero**: i 18 di `vitti` sono in
leNostre (piu' 2 arrivati dopo), quindi i cestini non sono piu' l'unica copia.

### 🐞 BUCO 1 - l'import ha lo STESSO punto cieco che aveva la scopetta

`vrw_ingest()` decide se una foto e' gia' entrata guardando **un solo posto**:
il path canonico `ANNO_da_archiviare/MMGG/AAAAMMGG-HHMMSS.jpg` (riga 347,
`vrw_is_duplicate_by_size($firstCand, ...)`). Appena Elena sistema il giorno
- cioe' sposta le foto nella cartella dell'evento - **quel posto torna libero**, e
al reimport successivo la foto viene ricopiata.

Misurato sul campo: l'import "tutti i giorni insieme" di Elena ha portato dentro
**~200 file**, di cui **172 lavoro vero** (mai archiviati prima) e **26 doppioni**
di foto che lei aveva gia' schedato in `ANW/2026/0714 Secchiello Selvaggio`,
`2026/0711 Elena Malata`, `2024/.../0914 Camping Kanegra`. Confermati confrontando
il **contenuto**, non il peso.

E' lo stesso male della sez. 11 del progetto, dalla parte dell'import invece che
della scopetta. **La cura esiste gia' ed e' li'**: il registro `vrw_import` sa
rispondere «questo file l'ho gia' portato dentro», ma `vrw_ingest()` non lo
consulta. Da fare con Vitti - non e' una riga, perche' `vrw_ingest()` e' condiviso
con gli altri import (zip Amazon, staging) che non hanno un registro.

### 🐞 BUCO 2 - i `_NNN` si moltiplicano a ogni giro

Corollario del primo, ma indipendente. Se il nome di destinazione **collide** con
una foto diversa scattata nello stesso secondo (i file `IMG_..._1.jpg` del
telefono), `vrw_safe_target_path()` accoda `_001`. Al giro dopo il confronto
guarda ancora il nome **base**, che ha un peso diverso -> non e' un doppione ->
`_002`. Poi `_003`. **Senza limite.**

Visto succedere in diretta: Elena ha lanciato lo stesso import tre volte
(18:15:56, 18:16:07, 18:17:24) e 3 foto hanno prodotto 6 copie in piu'.
Cestinate (soft) su sua autorizzazione. **Cura proposta**: il confronto deve
guardare anche le varianti `_NNN` - `vrw_archivio_indice()` normalizza gia' la
coda, la logica c'e', va portata dentro `vrw_ingest()`.

### 🐞 BUCO 3 (UI) - occhio e scopetta si pestavano i piedi

Segnalato da Elena come primissima cosa: «l'occhio è finito tra la parentesi e
non funziona più». La scopetta era stata infilata nella riga del giorno senza
allargare la traccia della griglia (180px) e senza `flex:none` sui bottoncini: il
flex li stringeva sotto la loro larghezza, il simbolo usciva dal proprio riquadro
e il clic finiva sull'etichetta. Sistemato in `nextcloud_import.php`: traccia a
215px, `flex:none` su occhio e scopetta, `overflow:hidden` sull'etichetta.
Backup `.bak_20260805_174624_preocchio`.

### 🔧 E un permesso, che non era codice

Spostare un filmato da `2026/9999 Filmati` dava errore. Non era il Gestore:
quella cartella e `2025/9999 Filmati` - create **a mano** il 02/08 durante il
recupero dei filmati - erano `drwxr-sr-x vitti:http`, cioe' senza scrittura per
il gruppo. php-fpm gira come `http`: poteva leggere ma non spostare. Rimesse a
`2775` come le sorelle 2023/2024. Sono le uniche due cosi' in tutto leNostre
(controllate tutte le cartelle).

**Promemoria**: quello che si crea a mano in leNostre va creato col permesso di
gruppo, se no il Wizard ci guarda dentro e basta.

### Una cosa che NON e' un difetto

Scansione dei doppioni su tutto leNostre (37.140 file, confronto sul contenuto):
**1.233 gruppi, 2.130 copie in piu', 2,98 GB**. Sembra un disastro, non lo e':
di quelli nati da un reimport ne resta **uno**. Gli altri sono la stessa foto
messa in due posti **apposta** - `001 Cagnolini/Seneca/` piu' la cartella del
viaggio, `ANW` (453), `web_wallpaper` (294). Elena: «è vero, una foto può stare
in più posti». Detto a lei chiaramente di **non** trattare quell'elenco come una
lista di cose da cancellare: si smonterebbe un ordine costruito in vent'anni per
recuperare 3 GB su 108 liberi.

---

## 2026-08-26 (sera) - 🅿️ Il parcheggio della passeggiata prende il suo bottone

**Dov'e':** `/srv/http/maps/convalida_foto.php` (fuori dal lab: la mappa e' un
albero suo, non lo scandisce `genera_doc.php`).

**Perche':** dal 26/08 mattina il filo tratteggiato delle foto 🥾 parte dal
parcheggio DICHIARATO invece che dalla tappa piu' vicina - a Colonnata la tappa
piu' vicina (Campocecina) sta a 4,6 km dall'altra parte delle Apuane. Ma
`passeggiate.json` era il quinto degli elenchi-che-comandano e l'unico **senza
bottone**: si scriveva a mano con l'editor. Domanda di vitti la mattina stessa,
«riesco a metterlo a posto io?», e la risposta era no.

**Fatto:**

- **Azione nuova `parcheggio`** nello sportello POST, accanto a `piedi`/`place`.
  Due versi: con `lat`/`lon` dichiara, con `togli:true` scorda.
- **Due funzioni** `passeggiate_load()` / `passeggiate_save()`, gemelle di
  `nascoste_load/save`.
- **Due bottoni** in barra: `🅿️ Il camper era qui` e `🅿️✖ Non era li'`.
  Leggono lo **stesso campo `lat, lon` del 📍**: e' la stessa domanda fatta per due
  cose diverse, e un secondo campo accanto al primo sarebbe stato solo un modo in
  piu' di sbagliare casella. Vale la lezione del 21/08: il campo si svuota dopo
  l'uso e se non c'e' una coppia di numeri il programma NON indovina.
- **Bollino 🅿️** sulla miniatura (blu, sopra al 🥾, che spesso convivono) +
  `data-parch` per accendere/spegnere i bottoni.

**Tre regole scelte, e il perche':**

1. **Stesse coordinate = stessa passeggiata**: si fondono invece di duplicare. E' il
   caso di chi si accorge dopo di aver dimenticato due foto.
2. **Una foto sta in UNA passeggiata sola**: prima di riassegnarla la si toglie da
   tutte. Se no spostare il parcheggio la lasciava rivendicata da tutt'e due e
   vinceva la prima dell'elenco - cioe' di nuovo il caso, che e' il difetto da cui
   e' nato tutto questo.
3. **Una passeggiata che elencava foto e non ne ha piu' sparisce**, ma le voci
   **senza** la chiave `foto` non si toccano mai: sono la "comodita' a distanza" dei
   viaggi vecchi, non hanno mai avuto foto da perdere.

**Unica azione delle sei che NON riscrive `map_points.js`**, ed esce dallo
sportello per conto suo prima del `rebuild_derived`: `passeggiate.json` lo rilegge
`map_bootstrap.js` a ogni apertura della mappa (fetch no-store), quindi basta
ricaricare - niente «Ricostruisci».

**Collaudo** (viaggio finto `trips/prova_park`, copia di monchio_2026, cancellato
dopo; il viaggio vero mai toccato, md5 di `passeggiate.json` invariato):

| # | prova | esito |
|---|---|---|
| 1 | parcheggio nuovo per 3 foto | voce creata, nome preso dalla cartella |
| 2 | 4a foto sulle STESSE coordinate | fusa, nessun doppione |
| 3 | foto di Colonnata spostata sul nuovo | Colonnata 4->3, nuovo 4->5 |
| 4 | `lat:999` e `lat:0` | rifiutate tutt'e due |
| 5 | 🅿️✖ su una foto sola | tolta, la passeggiata resta |
| 6 | 🅿️✖ su tutte le foto di Colonnata | Colonnata sparisce, "senza elenco" resta |

Piu': pagina vera del Monchio resa senza warning, 4 bollini 🅿️ sulle 4 foto
dichiarate, `php -l` e `node --check` (sul blocco `<script>` estratto dalla
pagina resa) puliti.

**Backup:** `convalida_foto.php.bak_20260826_171500_parcheggio`.

**Aggiornati:** `MANUALE_diario_e_mappa.md` (2.5bis: tabella a sei bottoni, "come
si dichiara" col bottone, "cinque elenchi" invece di "tre" - che erano gia'
quattro elencati, svista vecchia) e la testata di `convalida_foto.php`, che
documentava **tre** azioni POST su sei (mancavano `place` e `piedi`, aggiunte a
suo tempo senza toccare il commento).

**Resta aperto:** il gemello di questo bottone per la **briciola sdoppiata**
(`via_points.json`, deviazione andata-e-ritorno) - l'altra cosa che si fa solo a
mano.

---

## 2026-08-26 (sera, subito dopo) - Le due verita' della posizione di una foto

**Disegno completo:** [MAPS_posizioni_a_mano.md](MAPS_posizioni_a_mano.md).
Qui solo il cantiere.

**Perche':** guardando il 19/08 col bottone 🅿️ appena fatto, si e' visto che le
uniche foto SENZA nome del posto erano proprio quelle raddrizzate a mano col 📍.
Domanda di vitti: *«mi sembra di ricordare che avevamo in progetto di cambiare
l'indice delle foto, con i nomi e le coordinate manuali, cosi' se si rilegge
l'exif non si perde il lavoro fatto»*. Il progetto non era mai stato scritto (solo
una riga di promemoria); il timore era fondato e verificabile in due punti -
`ln_upsert()` fa `ON DUPLICATE KEY UPDATE ... lat=VALUES(lat)`, e
`popola_foto_index.php` fa `DELETE FROM foto`.

**E guardando meglio, era peggio del previsto:** l'indice per quelle 17 foto non
era vuoto, aveva il nome SBAGLIATO - `Ponte di Arcola`, `Castiglione Vara`,
`Bedizzano` x8, `Massa` x6 - calcolati sulle coordinate che il GPS si era
inventato. Al primo «Ricostruisci» il cartellino avrebbe scritto «Bedizzano»
invece di restare vuoto.

**La forma scelta - e NON e' "spostiamo la verita' nel DB":**
la verita' resta `trips/<v>/foto_posizioni.json`; l'indice ne tiene una **copia
rifacibile** in `lat_mano`/`lon_mano`; tutti leggono `lat_eff`/`lon_eff`, che le
calcola il database. Cosi' un `DELETE FROM foto` non fa danno (si riversa dai
JSON) e **nessun lettore puo' scordarsi di guardare la colonna giusta** - che e'
l'errore in cui era caduto `via_points.py` la mattina stessa.

**Fatto:**

- **ALTER** su `lenostre.foto` (lanciato da vitti da root, 35.408 righe, 0,9s):
  `lat_mano`, `lon_mano` DECIMAL(10,7) NULL + `lat_eff`, `lon_eff`
  `GENERATED ALWAYS AS (COALESCE(lat_mano, lat)) VIRTUAL`. Solo aggiunte.
- **`ln_posizioni_fill(PDO, ?string $trip)`** in `wiz_foto/lenostre_index.php`:
  legge i `foto_posizioni.json`, scrive `lat_mano`/`lon_mano`, e **dove il valore
  cambia azzera `localita`** cosi' il nome si rifa' sulla coordinata nuova. Le
  "tolte" si ripuliscono solo nella passata completa (leggendo un viaggio solo non
  si puo' sapere se un altro la rivendica).
- **`ln_localita_fill`** legge `lat_eff`/`lon_eff` invece di `lat`/`lon`.
- **Chiamata da tre posti**: `geocodifica_foto.php` e `popola_foto_index.php`
  (⚠️ PRIMA di `ln_localita_fill`, se no i nomi rinascono sull'EXIF), e il
  bottone 📍 di `maps/convalida_foto.php`.
- Nel bottone 📍 la chiamata e' in **try/catch**, e non e' pigrizia: l'indice e'
  una copia, il JSON che comanda e' gia' scritto - se il DB non risponde il 📍
  deve riuscire lo stesso. E il `ln_localita_fill` e' limitato alle **cartelle
  toccate**, se no il budget di rete se lo mangiano le 71 foto che un nome non
  possono averlo (vedi sotto).
- I contatori di `geocodifica_foto.php` e `popola_foto_index.php` passati a
  `lat_eff`.

**Collaudo:** sei prove, tutte passate; le due che contano sono il
`ln_reindex_target` (rilegge l'EXIF di 23 foto: posizioni e nomi intatti) e la
simulazione del `DELETE FROM foto` (26 posizioni rimesse dai JSON, 26 nomi
rifatti con **0 domande a Nominatim**). Dettagli e numeri in sez. 6 del disegno.

**Backup:** `lenostre_index.php` non ne aveva bisogno (solo aggiunte);
`geocodifica_foto.php.bak_..._posmano`, `popola_foto_index.php.bak_..._posmano`,
`convalida_foto.php.bak_..._posmano`.

**Trovato per strada, NON un difetto:** le 71 foto che restano senza nome non
sono un baco dell'attrezzo (l'avevo detto, sbagliando, prima di guardare). Sono
**4 posti soli**: 68 foto con `lat=0, lon=0` (fotocamere che scrivono zeri quando
non agganciano) e 3 scattate **dall'aereo sul Tirreno**. Nominatim non ha un nome
per il Golfo di Guinea ne' per il mare aperto, e risponde giustamente niente.

**🐞 APERTO, nato da li':** 68 foto risultano "col GPS" avendo `lat=0, lon=0`. Se
una finisse in un viaggio, la mappa la piazzerebbe al largo dell'Africa. Lo zero
non e' una posizione, e' l'assenza di posizione: va trattato come tale in
`ln_rows_from_json()` (dove l'EXIF diventa riga) e nei filtri `lat_eff IS NOT NULL`.

---

## 2026-09-04 - Dark sulle pagine di lavoro, musica del post, «I diari di Elena»

**Perche':** vitti, al mattino: "i servizi della piattaforma hanno un feeling
diverso, il wizard foto e lo share sono dark, il VRV chiaro". Nel pomeriggio si
sono aggiunte la musica nell'editor e la richiesta di Elena di condividere TUTTI
i post di una categoria, non un articolo alla volta.

Il lungo, con i numeri e le strade scartate, sta in
`docs/VRV_B100_dark_e_musica.md`. Qui il cantiere.

### 1. Il dark: un cappotto, non un tema

Misurato prima di toccare: il CSS del VRV e' **36 file / 8.309 righe con ~880
colori scritti a mano e 24 presi da variabile**. `vrv_base.css` le variabili le
dichiara e non le usa quasi nessuno. Un tema vero vorrebbe prima variabilizzare
tutto: giorni, su ogni pagina del diario.

Deciso con vitti: **solo le pagine di lavoro**; il diario che legge Elena resta
chiaro, li' il chiaro e' voluto (carta sopra le foto).

- NUOVO `public/vv/css/vrv-dark.css` - un file solo per tutte le pagine
  vestite, a sezioni: paletta comune, poi una sezione per pagina, il nome della
  sezione e' la classe del body. Si carica per ULTIMO e ridipinge quello che si
  vede. Per tornare indietro si toglie la riga di enqueue.
- Colori copiati NEI VALORI da `wiz_foto/theme.css`, non nei nomi: prendere i
  suoi nomi legherebbe due servizi che oggi non si conoscono.
- Vestite: `admin.php` (assaggio approvato) e il **TELAIO** di
  `modifica_post.php` - barra, tendine, testata, cornice, modale, segnalibri.
  NON il foglio: il contenuto lo disegna TinyMCE in un iframe suo, dove il
  cappotto non arriva. Resta chiara anche la toolbar di TinyMCE (skin suo).

**Il regalo:** i pochi punti che le --vrv-* le usano davvero sono `.vrv-panel` e
`.vrv-panel--strong` (vrv_layout.css) e i testi (vrv_components.css) - cioe' la
testata del titolo e la card dell'editor. Ridefinite 9 variabili, quei pezzi si
sono vestiti da soli.

**I bottoni della barra sono un CODICE COLORE** (media azzurro, anteprima rosa,
cofanetto marrone, lingue verde, mappa turchese, versioni viola, salva/chiudi
arancio): la tinta non e' cambiata, e' cambiata solo la LUCE. Elena li ritrova
dove li lasciava.

**Trappole, tutte trovate da vitti guardando:**
- il bianco di `.vv-container` sta dentro una `rgba()` a mano: l'alpha da sola
  non basta;
- il tema base tinge di verde `#219324` ogni button/input/select;
- `footer.php` carica bootstrap dal CDN DOPO i fogli del tema: ogni regola deve
  salire di specificita';
- **vestire il testo di una classe senza vestirne il fondo**: `.vrv-btn` lo
  portano anche i bottoni DENTRO i pannelli, che tinta loro non hanno - erano
  crema su crema, illeggibili (i DD di Cofanetto e Lingue). Il fondo scuro va
  messo sul BASE, i modificatori lo sovrascrivono;
- **le opacita' basse si rovesciano sul fondo scuro**: le lingue non tradotte
  stanno a `opacity: .28` (leggibile su chiaro, invisibile su scuro) -> .58.

### 2. Il wallpaper sparito dalle liste - non era il dark

vitti: "nelle liste ci doveva essere il wallpaper dei cappelli, non c'e' piu'".
Era cosi' su TUTTE le pagine del VRV, vestite o no.

La piattaforma lo sfondo lo metteva gia' lei e bene: `vrp_service_bg()`
bufferizza l'output del servizio e prima di `</head>` inietta
`body{background:url(<manifest.wallpaper>)}`. Ma `theme-default.css` cablava a
sua volta `body.theme-default{background:url('/vittrosviaggi_shared/...')}`, e
**`body` pesa 0,0,1 mentre `body.theme-default` pesa 0,1,1**: vinceva il tema, e
quell'indirizzo sulla 8443 e' un 404. La configurazione parlava e il CSS non la
faceva passare.

TOLTA l'immagine cablata dal tema, che ora dichiara solo il colore di ripiego.
**Regola:** nel VRV nessun tema deve scrivere una shorthand `background:` sul
body - schiaccia sempre l'iniezione della piattaforma, che ha specificita'
minima per costruzione. Un `background-color` va bene: sta sotto l'immagine.

### 3. Le tendine che non si chiudevano - un lavoro lasciato a meta'

vitti, provandole una per una: Cofanetto e Mappa si aprono e chiudono col clic
sul bottone; Lingue, Versioni e Salva/Chiudi si aprono ma per chiuderle bisogna
cliccare **ovunque tranne che sul bottone**.

Erano proprio quelle tre ad avere DUE gestori: le `bindDropdown()` in
`initVrvDropdowns()` di `js/modifica_post.js` piu' quelle di
`vrv-modifica-post-barra.js`. E tenevano lo stato in due posti diversi - classe
`.is-open` contro attributo `aria-hidden` - con
`.vrv-dd.is-open > .vrv-dd__menu` (0,4,1) che batte
`.vrv-dd__menu[aria-hidden="true"]` (0,3,1). Al clic su una tendina aperta il
primo gestore toglieva `is-open`, il secondo la RIAPRIVA.

Il commento in quel punto diceva gia' "NIENTE Cofanetto qui, due gestori si
pestano i piedi": il Cofanetto era stato staccato, le altre tre no.

**Trappola dentro la trappola:** `initVrvDropdowns()` e' dichiarata DUE VOLTE in
quel file, e fra due dichiarazioni omonime JavaScript tiene l'ULTIMA. Corretta
la prima non sarebbe cambiato niente. Sistemate entrambe, con un avviso sulla
copia morta. Non si e' perso niente: `keepMenuInViewport()`, l'unico extra che
`bindDropdown` dava a Versioni, e' una funzione vuota da tempo.

### 4. La musica del post nell'editor

Prima: `post.musica` si poteva scrivere SOLO da una casella di testo nuda in
`admin_post.php` (r.258) - si usciva dall'editor e si batteva a mano il nome
esatto del file. In `modifica_post.php` non c'era traccia della musica (il 🎵 in
alto a destra e' la Radio VV, un'altra cosa: suona `shared/mp3`).

Tendina SUA, non dentro il Cofanetto: **"la musica e' per post, non per gruppi
di post"** (vitti) - il cofanetto raggruppa piu' post, metterla li' insegnerebbe
una cosa falsa. Lo spazio non e' servito ritagliarlo: la barra aveva gia' ~200px
vuoti fra Mappa e Versioni.

- NUOVO `lib/musica_widget.php` - elenco brani + HTML della scaletta
- NUOVO `ajax/post_musica.php` - l'unica porta che scrive `post.musica`
- NUOVO `musica_picker.php` - il selettore, su **vrp_explorer** (stampo di
  `bg_picker.php`): provider di 30 righe, la cartella e' piatta
- NUOVO `ajax/musica_upload.php` - sportello CONDIVISO (lo usera' il JukeBox)
- NUOVO `js/vrv-musica.js`; TOCCATI `modifica_post.php`, i js/css della barra

**Scelte:**
- **la scaletta E' l'ordine delle righe**: niente numeretti da tenere allineati,
  si legge l'elenco dall'alto in basso e quello si salva;
- l'elenco lo stampa il SERVER con la pagina (nessuna chiamata all'apertura);
- **non riusata `vv_list_mp3()`** di `lib/audio.php`: filtra solo `.mp3` e
  avrebbe fatto sparire tutti gli Aizpute, che sono `.m4a`;
- si salva solo un nome che ESISTE nella cartella; i brani spariti restano
  visibili marcati "non trovato" invece di svanire in silenzio;
- permessi come `ajax/cofanetto_azione.php` (loggato + admin/editor), NON come
  `update_sfondo.php` che ha i controlli commentati;
- l'upload non sovrascrive un nome gia' preso: **numera**. Sovrascrivere
  cambierebbe la musica dei post che usano quel nome senza che nessuno se ne
  accorga.

**Corretti dopo le prove di vitti:**
- il ▶ suonava ma non fermava: lo stato si marcava DENTRO la promise di
  `play()`, e fra il clic e la marcatura il pannello non sapeva di star
  suonando. Ora si scrive PRIMA di chiamare `play()`;
- la prima versione stampava tutti i brani dentro la tendina ("e' troppo
  piena"): ora nella tendina c'e' solo la scaletta;
- due triangoli sulla mattonella, uno finto: era il `badge: '▶'` del contratto
  (decorazione, non si preme) messo OLTRE al pulsante vero. Tolto;
- **"non si puo' deselezionare"**: il clic nel selettore era solo aggiunta. Ora
  e' un INTERRUTTORE (un clic mette, un altro toglie) e le mattonelle gia'
  prese portano un ✓. Chi sceglie deve poter disfare dove sta scegliendo.

Resta il campo a mano di `admin_post.php` come via di fuga.

### 5. «I diari di Elena» - due voci di config, zero codice

Elena vuole mandare agli amici tutto il diario di viaggio. Non serviva scrivere
niente: `diario_lista_guest.php` filtra GIA' per categoria da URL
(`$_GET['categoria']`, ripulita e ricordata in un cookie 90 giorni). Mancava la
porta.

- NUOVO `platform/config/diari.json`; TOCCATO `platform/boot/env.json`
- **`lib` e' OPZIONALE**: `vrp_load_service_libs()` fa `if ($lib==='') continue`.
  Una cartolina-scorciatoia senza codice suo e' prevista dal caricatore.
- ⚠️ **l'url tiene il `.php` apposta**: `vrp_current_service()` confronta lo
  SCRIPT con l'url della targa PIU' LO SLASH, quindi un url che finisce in
  `.php` non puo' vincere quel confronto e la cartolina non ruba al VRV le sue
  pagine. Verificato sull'HTML servito.

### 6. La nota della Radio VV

Il bottone lo tinge di verde `button{background:#219324}` e l'emoji 🎵 e' verde
chiaro: verde su verde. Essendo un'emoji A COLORI non obbedisce a `color`, e un
filtro sul bottone avrebbe scurito anche il fondo -> la nota sta in uno span suo
e il filtro prende solo lei.

**Collaudo:** `php -l` su tutti i PHP toccati, `node --check` sui js, graffe dei
css bilanciate, nessuna variabile CSS usata senza dichiararla. Da HTTP:
`musica_picker.php` da non loggato 403, `post_musica.php` e `musica_upload.php`
"Devi essere loggato", home 200 con la cartolina e il link giusto, filtro ospite
8 post su `viaggi` (34 senza filtro), e su `diario_lista_guest.php` lo sfondo
iniettato resta quello del VRV. Il resto l'ha collaudato vitti dal vivo, pezzo
per pezzo.

**Backup:** `admin.php.bak_..._darkassaggio`,
`modifica_post.php.bak_..._darktelaio`, `modifica_post.js.bak_..._ddoppio`,
`vrv-modifica-post-barra.js.bak_..._musica`, `theme-default.css.bak_..._wallpaperconfig`,
`header_badge.php.bak_..._nota`, `env.json.bak_..._diari`.

**Aperto:** completare il dark sulle altre pagine di lavoro (admin_post,
admin_utenti, admin_config, diario_lista_admin, profilo); il **JukeBox** - una
pagina di piattaforma per suonare tutta la musica del VRV, da decidere se
assorbe la Radio VV (che suona `shared/mp3`, un'altra cartella); la toolbar di
TinyMCE lasciata chiara; il pruning del backup sacro che si ferma su una
directory non vuota.

---

## 2026-09-06 - Playing list del JukeBox, il MOTTO, e la soffitta degli avatar

**Perche':** tre richieste di vitti nella stessa mattina, che a guardarle bene
fanno **una domanda sola** - dove si mettono le cose di un utente che cambiano
nel tempo:

1. *"per il jukebox, le playing list, pensavo di aggiungere una tabella, legata
   all'user_id; ogni loggato avrebbe le sue playing list, gli ospiti potrebbero
   suonare la list di vitti, o di Elena, o farne una propria, ma non salvarla!"*
2. *"per l'avatar utenti... conservare gli avatar precedenti, oltre quello
   corrente"*
3. *"aggiungere un motto per utente, anche quello con lo storico"* - il suo:
   `Vitti, Uto, Ubi, Elena, Seneca & Frida, nihil separatum est`

Disegno e ragioni: [VRP_B100_motto_avatar_playlist](VRP_B100_motto_avatar_playlist.md).

**La forma, in una riga:** il CORRENTE sta in `users` (`avatar`, `motto`), i
PRECEDENTI in `user_storia`. Cosi' **nessun lettore di oggi si e' dovuto
accorgere del cambiamento**, e lo storico e' un registro che si appende e non si
rilegge mai per sapere "chi sono adesso" - quindi non puo' nascere una seconda
verita'.

**DDL** (lanciata da vitti come root, `docs/VRP_ddl_20260906_motto_avatar_playlist.sql`):
`users.motto VARCHAR(255)`, tabella `user_storia` (una sola per avatar e motto:
e' lo stesso mestiere), tabella `jukebox_playlist`.
📌 `vrp_auth_app` non ha CREATE/ALTER e il root di MariaDB **non e' unix_socket**:
vuole la sua password (cassaforte KeePass). Il `sudo mysql` che avevo suggerito
dava lo stesso errore - il sudo qui non c'entra niente.

**Fatto:**

- **`platform/lib/vrp_auth.php`**: `motto` aggiunto alla SELECT di
  `vrp_current_user()` (una parola, ed e' in mano a tutti: badge, profilo,
  utenti). Due funzioni nuove: **`vrp_user_ritira()`** (manda in soffitta il
  valore di prima; non scrive niente se era vuoto o se non e' cambiato - lo
  storico racconta i CAMBI, non i salvataggi) e **`vrp_user_storia()`** (i
  precedenti, dal piu' recente; torna vuoto invece di cadere se la tabella non
  c'e').
- **`platform/pages/jukebox.php`**: le playing list. Due azioni sul canale
  `?vx=action` gia' pronto del motore esploratore (**niente file ajax nuovo**),
  la lista renduta nella pagina (**un ospite non fa nemmeno una chiamata** per
  ascoltare la lista di Elena), e in console una tendina accanto alla coda.
  I brani stanno in un TEXT separati da `|`, **come gia' fa `post.musica`**: una
  playlist e' un elenco ordinato di brani, non merita ne' un formato nuovo ne'
  una tabella di righe. Si salva **LA CODA**, non la selezione: la coda gia' e'
  l'elenco ordinato che si sta ascoltando.
  Un clic su una lista **ACCODA**, come un clic su una mattonella: stessa
  grammatica, e due liste si attaccano una all'altra.
  🔑 Le chiavi si filtrano **contro i file che esistono davvero**, in lettura e
  in scrittura: un brano cancellato dalla cartella non rompe la coda di nessuno,
  e un `../` che arriva dal browser non entra nel database.
- **`platform/pages/profilo.php`**: campo Motto (max 160 - un motto che non sta
  nel badge non e' un motto), i motti di prima in elenco, e **"Le foto di
  prima"** con il bottone Riprendi.
  ⚠️ **L'avatar non si sovrascrive piu'**: il file si chiama `u<id>_<data>.<ext>`
  e quello vecchio RESTA sul disco. Era proprio il `@unlink` a rendere
  impossibile conservare i precedenti. I vecchi `u<id>.<ext>` restano validi:
  il nome sta scritto in `users.avatar` e nessuno lo ricalcola - **zero
  migrazione**.
  🔑 Riprendere una foto e' uno **SCAMBIO, non una copia**: un solo UPDATE, il
  disco non si tocca. Il nome arriva dal form, quindi si ricontrolla contro la
  soffitta VERA di quell'utente: senza quel controllo uno poteva prendersi
  l'avatar di un altro scrivendone il nome.
- **`platform/pages/users.php`**: motto nell'elenco e nel form, la soffitta in
  sola lettura (le foto le riprende il padrone dal suo Profilo), e la
  cancellazione di un utente ora porta via **anche le sue foto di prima** - i
  nomi si raccolgono PRIMA della DELETE, perche' il cascade si porta via le
  righe di `user_storia` e con loro l'unico posto dove c'era scritto come si
  chiamavano.
- **La pagina Utenti ha DUE FACCE** (vitti: *"gli admin possono tutto,
  modificare se stessi e gli altri; i loggati possono modificarsi, ma potrebbero
  vedere gli altri"*): l'elenco ("Chi c'e' in casa": foto, nome, motto, ruoli)
  lo vede chiunque sia loggato; creare/modificare/cancellare resta agli admin
  dalla LAN. La guardia e' **uscita dalla rotta** (`public/index.php`: `users`
  non e' piu' `lan+admin`) ed e' **entrata nella pagina**, dove sa a quale meta'
  si applica. Il lucchetto sta sul POST, non sui bottoni: un form nascosto non
  e' un form che non si puo' mandare.
- **Il motto nell'hover** (vitti: *"in hover sul badge, anche nella lista
  utenti"*): `vv/lib/header_badge.php` (badge del VRV) e `public/index.php`
  (barra di piattaforma). Se il motto non c'e', l'etichetta resta quella di
  prima: un title vuoto sarebbe un passo indietro.
- **CSS**: `.jb-liste*` in `vrp-jukebox.css`, `.vrp-sof-*` e `.vrp-usr-motto` in
  `vrp-lab.css`. Le foto della soffitta sono quadrate con l'angolo tondo, non
  cerchi: la forma dice da sola che non e' quella in carica.

**Collaudo** (tutto dal vivo su lab, con un utente usa-e-getta poi cancellato):
- JukeBox, 11 prove: brano finto e `../` scartati; stesso nome = aggiorna e non
  duplica; nome vuoto, coda vuota e CSRF storto respinti; **ospite: vede le
  liste di tutti, non puo' salvare**; cancella solo il padrone (o un admin), e
  chi non e' padrone si sente dire di no.
- Motto/avatar, 15 prove: primo motto -> 0 righe di storia (giusto: non c'era
  niente da ritirare); secondo motto -> il primo in soffitta; due foto -> tutte
  e due sul disco, la vecchia in soffitta; Riprendi -> scambio corretto;
  **Riprendi con un nome inventato (`u1.jpg`, la foto di vitti) -> negato**;
  admin che cambia il motto di un altro -> `ritirato_da` = l'admin; utente
  cancellato -> file spariti (2 su 2) e nessuna riga orfana.
- Elenco utenti: da loggato-non-admin niente Cancella, niente form, niente
  email, e un POST forzato risponde *"Da qui puoi solo guardare"* con vitti
  (id 1) ancora al suo posto.

**Aperto:** il manuale (JukeBox e Cestino non ci sono ancora); e il **ruolo
esterno**, che vitti aveva messo in attesa e che tocca le stesse tabelle.

---

## 2026-09-06 (2) - IL JUKEBOX NON MOSTRAVA I BRANI, e la cartella sembrava vuota

**Come e' uscito:** vitti dall'amaca, sul tablet: *"non vedo lo scaffale!"*, con
uno screenshot. Sullo schermo: `mang3:8443/?p=jukebox`, loggato come
UbiOneKenobi, la barra e lo sfondo a posto, e in mezzo **"0 brani"** e
**"Cartella vuota."**, con l'albero a sinistra vuoto.

📌 **Da riga di comando non si vedeva**, e per un motivo preciso: io il curl lo
facevo su `?p=jukebox&vx=scan`, cioe' mettendoci il `p` a mano. Il browser no.

**La causa:** `vrp-explorer.js` costruiva l'URL degli endpoint cosi':

    base() + '?vx=' + verbo          // base() = cfg.endpoint || location.pathname

Il JukeBox pero' **non e' un file .php suo**: e' una ROTTA del front controller
(`?p=jukebox`). Quindi `location.pathname` vale `/`, l'URL diventava
`/?vx=scan&node=` - **il `?p=jukebox` si perdeva per strada** - e la chiamata
finiva sulla HOME, che rispondeva **HTML invece di JSON**.
`readJsonSafe` trasformava il pasticcio in `{ok:false, err:...}`, ma `openDir`
non guardava `ok`: senza `items` la griglia scriveva "Cartella vuota." e
l'errore spariva senza lasciare traccia.

🔑 **Gli altri cappelli non ci erano incappati** perche' sono tutti file a se'
(`bg_picker.php`, `media_popup.php`, `share/index.php`, `gestore_foto.php`):
per loro `location.pathname` e' gia' l'indirizzo giusto. Il JukeBox e' **il
primo esploratore che vive su una rotta a query string**.

**Fatto:**

- `public/js/vrp-explorer.js`: **`vxUrl(verbo)`** al posto del `'?vx='` cablato -
  se l'endpoint ha gia' un `?`, attacca con `&`. La usano tutti e tre i canali
  (`api`, `action`, `upload`). Il caso `endpoint = ''` resta identico a prima.
- `public/js/vrp-explorer.js`: **`openDir` ora guarda `res.ok`** e alza un toast
  appiccicato con l'errore. *Una lettura fallita non deve somigliare a una
  cartella vuota*: chi guarda non ha modo di distinguere "qui non c'e' niente"
  da "non sono riuscito a chiedere", ed e' esattamente l'ora che ci si perde.
- `platform/pages/jukebox.php`:
  `vrp_explorer_render($provider, ['endpoint' => '?p=jukebox'])`. Relativo e non
  assoluto, cosi' vale sia su `/?p=jukebox` (porta 8443) sia su
  `/lab/public/?p=jukebox`.

**Collaudo:** riprodotto il guasto (`/?vx=scan` -> 4.491 byte di HOME, non
JSON), poi **Chrome headless su `?p=jukebox`**: le due mattonelle ci sono, con
"2 cartelle · 0 brani", "30 brani" e "23 brani", e l'albero pieno. Salvata una
playing list con l'URL nuovo (ok) e riprovata con quello vecchio (torna HTML,
come deve). Gli altri tre esploratori riprovati e intatti.

**La lezione:** *un URL costruito a pezzi non e' un URL finche' non lo si guarda
finito.* Il `'?'` cablato dava per scontato che l'endpoint fosse un percorso
nudo, e per otto pagine su nove era vero. E soprattutto: **avevo provato
l'endpoint, non il gesto**. Il curl passava perche' scrivevo io l'indirizzo che
il browser non avrebbe mai scritto.

---

## 2026-09-09 - INTERPRETE: parlare con il vicino di piazzola

**Perche':** vitti, dal campeggio di Kanegra: *"ho una richiesta da un vicino al
campeggio, posso usare il VRV per tradurre delle frasi dal tedesco
all'italiano?"*. Il motore c'era gia' (Gemini, `shared/bin/translate_lib.php`),
ma era tarato sull'opposto del bisogno: POST interi, in HTML, a pezzi, con la
barra di avanzamento e il salvataggio come post figlio. Qui serve una frase,
subito, su un tablet che passa di mano.

**Fatto:**

- **`platform/pages/interprete.php`** (nuova). Un form normale: si scrive, si
  tocca Traduci, la pagina torna con la risposta. Niente ajax e niente
  JavaScript - la traduzione di una frase sta sotto il secondo, e una pagina che
  si ricarica funziona anche con una riga di campo. Livrea di piattaforma
  (`vrp-fixed-head` / `vrp-fixed-body`), form dentro una `vrp-cfg-section`,
  testo della traduzione a 22px: si legge al sole, spesso lo tiene in mano
  l'altro.
- **Le lingue non sono quelle del diario.** `meta/translation/languages.json`
  sono le 7 in cui si PUBBLICA; qui ne servono 21, quelle che si incontrano in
  un campeggio d'Europa (croato, sloveno, polacco, ceco...). Si consegnano al
  motore col NOME INGLESE: `lang_name()` non riconosce il nome e lo ripassa
  tale e quale al prompt. Passare un codice sconosciuto (`hr`) sarebbe stato
  l'errore: il commento di `lang_name()` (01/09) dice che allora il modello
  RICOPIA invece di tradurre.
- **Riconoscimento automatico** come prima voce di "Dalla lingua": da una frase
  scritta da un altro non sempre si sa nemmeno che lingua sia. E' una riga di
  prompt (`the language of the text (detect it)`), non una libreria.
- **Il verso si gira** con un tasto (submit, non JavaScript). Lingue scelte e
  ultime 12 battute stanno nella sessione: e' un discorso, non un documento.
- **`public/index.php`**: rotta `interprete` e un flag nuovo `auth` nel filtro
  della barra (`if (!empty($r['auth']) && !$vrp_cu) continue;`) - in barra solo
  per chi e' entrato. Il lucchetto vero resta dentro la pagina, come in
  `fogli.php`: ogni frase e' una chiamata all'API che si paga.

**LA TRAPPOLA DELLA GIORNATA - una frase nuda non e' HTML.** Al banco, la
stessa frase tedesca tornava indietro IN TEDESCO. Non e' un caso: il prompt di
`translate_html()` finisce con `HTML:`, e sotto ci finiva una frase nuda; il
modello non trovava niente da fare e ricopiava. Misurato, non supposto - stessa
frase, quattro tentativi per parte:

    "Guten Tag! Duerfen wir den Hund an den Strand mitnehmen?"
      nuda            -> 4 copie su 4
      dentro <p>...</p> -> 0 copie su 4

Quindi le righe si impacchettano in paragrafi all'andata e si spacchettano al
ritorno. E il guardiano di serie non aiutava: `quota_copiata()` sotto le dodici
parole si arrende e torna 0.0 - e qui le frasi corte sono la regola, non
l'eccezione. Aggiunta `vri_e_una_copia()` (confronto alla buona: via
punteggiatura e spazi, tutto minuscolo) e fino a tre tentativi.

**Collaudo:** banco di prova che rende `?p=interprete` come lo renderebbe il
front controller, poi **Chrome headless** a 1280x900 e a 800x1000 (tablet).
Provati DE->IT, IT->DE su due righe, e croato con riconoscimento automatico:
*"Dobar dan, imate li slobodno mjesto za sutra?"* -> *"Buongiorno, avete un
posto libero per domani?"*. La voce Interprete c'e' in barra ed e' accesa.
⚠️ **Non provato dal vivo il cancello**: le sessioni di FPM stanno in un /tmp
privato (`PrivateTmp=yes`), quindi al banco la sessione e' finta. Il giro
login -> pagina lo fa vitti.

---

## 2026-09-09 (2) - FOGLI piu' leggibile, e il bottone che non esisteva

**Perche':** vitti, 08/09 sera: *"la pagina FOGLI, renderla piu' leggibile - e'
quella di CODE per Elena"*. Guardata a video (Chrome headless) prima di toccare
una riga, e il guaio si e' visto subito: l'elenco era una **tabella nuda
appoggiata sulla fotografia della landing**. I nomi dei file finivano sopra il
mare e le facce del Vulcano; e le tre colonne davano lo stesso peso al nome e
ai kB, che a chi cerca un documento non servono quasi mai.

**Fatto** (`platform/pages/fogli.php`):

- **L'elenco sta in un pannello** (`vrp-cfg-section`, lo stesso di
  Configurazione): stacca dalla foto e si legge. Dentro ci e' entrata anche la
  riga di spiegazione, che prima era sulla foto.
- **Una riga per documento**, alta 12px di padding - si centra col dito -, con
  **icona per tipo**, il **nome in 18px** e sotto, in piccolo, quando e' stato
  toccato / che tipo e' / quanto pesa. Tutta la riga e' il collegamento.
- **La data come la direbbe una persona**: «oggi alle 21:17», «ieri alle...»,
  «il 07/09 alle...» e solo oltre la settimana la data piena (`vrf_quando()`).
- **L'estensione resta a vista**, ma in sordina (`.vrf-est`): in casa c'e' un
  `kanegra.rtf` e un `kanegra.xlsx`, e togliendo il codino sembravano lo stesso
  documento. Provato: sono due righe che si distinguono a colpo d'occhio.
- Le quattro funzioncine di vetrina (`vrf_icona`, `vrf_tipo`, `vrf_titolo`,
  `vrf_quando`) stanno **nella pagina**, non in `lib/vrf.php`: li' c'e' il
  mestiere (percorsi, gettoni, WOPI), qui come si scrive a video.
- **La barretta del documento aperto** e' diventata un nastro suo
  (`.vrf-barretta`), col nome del documento in grande: prima erano tre pezzi di
  testo appoggiati sulla foto.

**LA SCOPERTA: `.vrp-btn` NON ESISTEVA.** Cinque punti la usavano (quattro in
`fogli.php`, uno in `users.php`) e in `vrp-lab.css` non c'era: quei bottoni
cadevano sul disegno di serie del browser. Il «Torna all'elenco» era un **link
blu sottolineato in mezzo alla fotografia**, e i due «Nuovo...» erano i
bottoncini grigi di sistema. Nessuno se n'era accorto leggendo il PHP - la
classe c'era scritta, sembrava a posto. Definita ora in `vrp-lab.css`
(`.vrp-btn` + `.vrp-btn--ghost`, stesso vestito di `.vrp-cfg-btn`): si aggiusta
anche `users.php` senza toccarlo.

Tolto anche il glifo `⛶` dal tasto «Tutto schermo»: meta' dei caratteri di
sistema non ce l'hanno e diventava un quadratino. Parole e basta.

**Collaudo:** Chrome headless a 1280x900, 820x760 e sulla vista a documento
aperto. La cornice dell'editor al banco resta grigia (il container non serve
una pagina aperta da `file://` con una sessione finta): quello che si e'
verificato qui e' l'IMPAGINAZIONE, il giro con Collabora vivo lo fa vitti.

---

## 2026-09-09 (3) - Interprete: il MICROFONO e la VOCE

**Perche':** vitti, subito dopo aver provato l'Interprete dal tablet: *"puo'
usare il microfono del tablet o del pc per il riconoscimento del linguaggio
parlato?"*. Si': lo fa il browser, non noi. `SpeechRecognition` per ascoltare,
`speechSynthesis` per parlare - in casa in Chrome e in Safari da anni. Zero
librerie, zero chiamate in piu' all'API.

**Fatto** (`platform/pages/interprete.php`):

- **&#127908; Parla**: si detta la frase invece di scriverla. Quel che il
  browser capisce finisce **subito** nella casella, anche prima di essere
  definitivo: chi parla vede che il tablet lo sta seguendo, invece di guardare
  un bottone rosso e sperare. Il bottone diventa rosso e batte mentre ascolta.
- **Finito di parlare, traduce da solo.** Sul materassino non si tocca due
  volte lo schermo. Se non e' arrivata nessuna parola, non parte niente.
- **&#128266; Ascolta** su ogni battuta: legge la traduzione ad alta voce nella
  lingua d'arrivo. Al campeggio vale piu' del testo - il vicino la SENTE.
- **`vri_bcp47()`**: il motore di traduzione lavora coi nomi inglesi
  ("German"), il microfono e la voce vogliono `de-DE`. Senza la nazione, il
  riconoscimento ascolta in italiano anche mentre parla un tedesco. Le opzioni
  della tendina portano il codice in un `data-bcp`, cosi' cambiando lingua
  cambia orecchio senza ricaricare.
- Con **«Riconosci da sola»** il microfono si spegne e lo dice («Per dettare,
  scegli la lingua di partenza»): non c'e' lingua da dare all'orecchio.
- Se il browser non ha queste cose, **i bottoni non compaiono** e la pagina
  resta quella di prima: si scrive e si legge.

⚠️ **IL MICROFONO VUOLE UN CERTIFICATO BUONO.** Il browser lo concede solo a
una pagina davvero sicura: da `casarosini.duckdns.org` c'e' Let's Encrypt e va;
aprendo la stessa pagina come `mang3` (autofirmato) resta muto. Il messaggio
d'errore lo dice con parole sue, invece di lasciare pensare a un guasto.

⚠️ **`submit()` da codice non manda il nome del bottone premuto**, quindi
`azione` non arriva: la dettatura funziona perche' il valore di ripiego lato PHP
e' `'traduci'`. Chi cambiasse quel ripiego spegnerebbe la dettatura senza
accorgersene - c'e' scritto nel commento, sopra la riga.

**Collaudo:** il microfono vero non si prova da qui, ma il GESTO si'. Banco con
un `SpeechRecognition` finto e uno `speechSynthesis` finto (montato con
`defineProperty`, che su `window` l'assegnazione diretta non attacca), e il
giro completo verificato: bottone visibile, spento con «Riconosci da sola» e
col perche' nel `title`, rosso mentre ascolta, il provvisorio che compare nella
casella, l'invio automatico con `lang=de-DE` alla fine, il giro a vuoto che NON
spedisce, e l'Ascolta che parla `de-DE` scegliendo la voce tedesca. Piu' il
solito Chrome headless per l'impaginazione.

---

## 2026-09-09 (4) - FOGLI grigi dal tunnel: NON e' il tunnel, e' il CERTIFICATO

**Il sintomo** (vitti, dal campeggio, sul wg0 di mang8): *"non mi fa vedere il
contenuto dei file, ma mi sembra che lo avevamo gia' messo a posto!"*. La
cornice si apre, la barretta c'e', dentro resta grigio.

**Cosa dicono i log** (nessuna supposizione - `access.log`, 09/09 12:58:59,
sorgente 10.9.0.5, referer `https://mang3:8443/...`):

    "GET  /lab/public/?p=fogli&apri=Nuovo testo....docx"      200 9145
    "POST /browser/cc5614c67e/cool.html?WOPISrc=..."          200 10749
    "GET  /wopi/files/Nuovo testo....docx?access_token=..."   200 339   (COOLWSD)
    ...e poi PIU' NIENTE.

Manca la riga `GET /cool/ws?WOPISrc=...` e manca `/wopi/.../contents`. Il 07/09,
quando funzionava, quelle due righe ci sono tutte e due - e il **filo di dati
partiva dal nome PUBBLICO**: la sorgente e' `93.55.232.194` (l'IP di casa, il
giro dal router), non un indirizzo di LAN.

**La lettura.** La pagina, la cornice, il gettone e la CheckFileInfo sono tutti
a posto: la piattaforma ha fatto il suo. Quello che non e' mai partito e' il
**websocket** dell'editor, che va allo stesso nome da cui si naviga - qui
`wss://mang3:8443/cool/ws`. Su `mang3` il certificato e' quello fatto in casa:
la PAGINA si apre lo stesso (si da' l'ok una volta e il browser se lo ricorda),
ma per il **filo di dati quel permesso non vale** e la connessione muore senza
un rumore. Ecco il grigio.

**Quindi: non c'entra WireGuard.** La correzione dell'08/09 (`vrp_is_lan()` che
riconosce `10.9.0.`) e' un'altra cosa e sta in piedi. Qui conta **quale NOME si
scrive nella barra**, non da dove si arriva: dal nome pubblico funziona anche
dentro il tunnel, da `mang3` no - e non funzionerebbe nemmeno stando in casa.

**Fatto** (`platform/pages/fogli.php`): `vrf_altra_porta()`. Quando si apre un
documento navigando su un nome diverso da quello pubblico della config, sopra
la cornice compare una riga rossa che lo dice **e offre il collegamento allo
stesso documento sul nome pubblico**. Non aggiusta niente: fa in modo che chi
guarda capisca, e che la prova sia un clic.

✅ **CONFERMATO dal vivo** (vitti, 09/09): aperto dal nome pubblico, *"si
funziona, i fogli si vedono!"*. La diagnosi dai log reggeva.

⏳ **La cura vera e' un'altra e va decisa**: o si fa fidare le macchine di casa
del nostro certificato, o si trova il modo di avere un certificato buono anche
per i nomi locali. Fino ad allora, per i Fogli si usa il nome pubblico.

**Anche:** il tasto «Tutto schermo» ha di nuovo il suo **glifo**, come lo
voleva vitti - ma **disegnato in SVG**, non un carattere: i simboli U+26F6 e
U+2924 mancano da meta' dei caratteri di sistema e diventavano un quadratino
(visto stamattina). Quattro angoli che puntano in fuori per allargare, in
dentro per tornare: cambia da solo quando si entra e si esce.

---

## 2026-09-09 (5) - JukeBox: il FOGLIO, giu' dalla barra, e la scena fantasma

**Perche':** la diagnosi era gia' chiusa l'08/09 e diceva che non era il
player: tutte le pagine stanno in `.vrp-wrap` (760px) e il JukeBox restava li'
dentro - la barra andava a capo e le mattonelle uscivano minuscole.

**Fatto:**

- **Il foglio.** `platform/pages/jukebox.php`: la testata in `.vrp-fixed-head`,
  tutto il resto in `.vrp-fixed-body`. Il `:has()` di `vrp-lab.css` fa il
  resto - 92% di larghezza, alto quanto lo schermo, testata ferma e corpo che
  scorre. Come la lista del VRV, non come il gestore foto.
- **Lo spazio va DATO.** Il solo marcatore non bastava: `.vx-shell` e' alto
  quanto il suo contenuto, e in mezzo alla pagina restava un buco con la
  fotografia sotto. Tre righe in `vrp-jukebox.css` su una classe NUOVA
  (`.jb-foglio`), non su `.vrp-fixed-body`: quella e' di tutta la piattaforma,
  e riscriverla da una pagina sola la romperebbe altrove - la lezione del
  `.vv-btn` della barra dell'editor.
- **Giu' dalla barra** (`nav => false` in `public/index.php`). La cartolina in
  home c'era gia' (`config/jukebox.json`): adesso la regola dell'08/09 e'
  intera - cartoline = servizi, barra = pagine di piattaforma, tendina = il
  resto.

**🐞 LA SCENA FANTASMA** (trovata guardando la pagina, non cercandola). Il
pannellone della scena - il ♫ grande con «tocca per tornare alla musica da
scegliere» - stava a video **anche a musica ferma**, sotto l'esploratore.
`display: flex` vince sull'attributo `hidden`, e la guardia
`.jb-scena[hidden] { display: none }` non c'era. Il bello e' che nello stesso
foglio la stessa trappola era gia' stata scritta **due volte**, per
`.vx-shell` e per `.jb-scena__nota`: mancava proprio sulla scena. Il JavaScript
era innocente - `scenaAccesa()` metteva e toglieva `hidden` come si deve, era
il CSS a non stare a sentire.

**Collaudo:** Chrome headless sulla pagina VERA (`https://localhost:8443/?p=jukebox`
- e' pubblica, non serve sessione) a 1280x860 e 820x900: l'esploratore riempie
la cornice, la console e' incollata in fondo, la scena non c'e' piu' a riposo,
la barra sta su una riga sola e il JukeBox non c'e' - la sua cartolina in home
si', terza dopo «VittRos Viaggi» e «I diari di Elena».
⏳ Resta da provare col suono acceso: che la scena si accenda dopo l'attesa e
riempia la cornice al posto dell'esploratore. Quello lo fa vitti.

---

## 2026-09-09 (6) - L'Interprete diventa un SERVIZIO A INVITO (per Lothar)

**Perche':** Lothar, il vicino di piazzola tedesco, ha accettato di fare da
collaudatore - e un collaudatore vuole il SUO accesso. vitti: *"pero' devo
creargli un account, altrimenti non vede le pagine dell'interprete"*. Vero, ma
un account normale apre **tutta la casa**: home, JukeBox, il diario da ospite e
l'elenco «Chi c'e' in casa» con i nomi di famiglia. A uno conosciuto stamattina
in campeggio si da' la chiave di una stanza, non del portone.

**Fatto** (deciso da vitti fra tre strade, ha scelto la chiave su misura):

- **`platform/config/interprete.json`** con `ruolo: true` e il suo `roles`.
  Niente `lib`: il servizio non ha codice suo da condividere - il mestiere sta
  in `shared/bin/translate_lib.php`, che e' del diario e resta li'.
- **`boot/env.json`**: registrato fra i `services`. Da qui in poi la pagina
  Utenti mostra da sola la casella «Interprete» accanto a VRV, VRN, Fogli...
  (`users.php` legge i ruoli dal `roles` di ogni config: non c'e' una riga da
  aggiungere li' dentro).
- **`pages/interprete.php`**: il cancello passa da «devi essere entrato» a
  «devi avere un ruolo per QUESTO servizio», come `fogli.php`. Resta vera anche
  la prima ragione del cancello: ogni frase e' una chiamata che si paga.
- **`public/index.php`**: `nav => false`. La mattina l'Interprete era in barra;
  la sera e' un servizio, e vale la regola dell'08/09 - cartoline = servizi,
  barra = pagine di piattaforma. Sale come cartolina in home.
- **Tolto il flag `auth`** che avevo aggiunto stamattina al filtro della barra:
  non lo usa piu' nessuna rotta. Un flag che non serve piu' e' un mistero per
  chi legge il codice fra sei mesi.

⚠️ **Da fare A MANO, e senza non si apre a nessuno**: in `?p=users` spuntare
«Interprete» per vitti, per Elena e per l'account di Lothar. Finche' la riga in
`user_services` non c'e', la cartolina non si vede e la pagina rimbalza su
`?p=accesso` - **anche a vitti**. Verificato: da ospite risponde 302 verso il
gateway di cortesia, come deve.

---

## 2026-09-09 (7) - LA DETTATURA NELL'EDITOR DEL DIARIO

**Perche':** vitti, dopo il microfono sull'Interprete: *"io parlavo per il
traduttore, ma anche per scrivere i testi non e' male!"*. Nei Fogli non si puo'
(Collabora e' una cornice di un altro dominio: dalla nostra pagina non ci si
scrive dentro), nell'editor del diario si': TinyMCE e' roba nostra.

**Fatto** (`public/vv/js/vrv-carica-tinymce.js`, un blocco solo in coda al
`setup(editor)`, piu' `vvDetta` in barra dopo «undo redo»):

- **La punteggiatura si detta.** A voce non arriva mai da sola: senza «punto»,
  «virgola», «a capo» un post dettato e' un muro di parole. Quindici voci, e
  le lunghe si provano prima delle corte - se no *punto interrogativo* diventa
  «. ogativo».
- **Il provvisorio NON entra nel post.** Il riconoscitore si corregge mentre
  ascolta; scrivere ogni ripensamento sporcherebbe il testo di Elena. Va in una
  strisciolina in fondo allo schermo; nel documento entra solo la frase FINITA,
  al cursore, con `editor.insertContent`.
- **Si riaccende da solo.** Chrome si ferma dopo qualche secondo di silenzio:
  finche' il bottone e' acceso si riparte, se no ci si ferma a meta' racconto.
- **Le maiuscole dei nomi restano.** Prima abbassavo tutto a minuscolo per
  cercare le parole di comando, e «Kanegra», «Seneca», «Lothar» tornavano
  minuscoli. Adesso si cerca senza badare al caso (la `i` nella regex) e il
  testo resta com'e'; la maiuscola la si mette solo a inizio frase e dopo . ! ?

🐞 **Due difetti trovati AL BANCO, non ragionandoci:**
1. `trim()` mangiava **l'a capo iniziale**: «a capo, il mare e' bello» finiva
   attaccato alla riga di prima. Ora si tolgono solo spazi e tabulazioni ai
   bordi, gli a capo no.
2. E poi restava **uno spazio davanti al `<br>`**. Il segno e l'a capo non
   vogliono niente davanti: sono due modi di sporcare il post.

**Collaudo:** il microfono vero da qui non si preme, ma il GESTO si'. Banco con
un riconoscitore finto **dentro TinyMCE vero** (pagina servita sulla 8443, dove
l'alias `/libs/` fa trovare l'editor). Verificato: il bottone c'e' in barra
(terzo, dopo Undo e Redo, con la sua etichetta), la striscia si accende, il
provvisorio si vede **e il post resta pulito**, la frase finita entra al cursore
- «siamo arrivati a Kanegra virgola con Seneca e Frida punto / a capo il mare e'
bello punto» diventa *«Siamo arrivati a Kanegra, con Seneca e Frida.<br>Il mare
e' bello.»* - il riconoscitore riparte da solo dopo il silenzio, e spegnendo il
bottone la striscia sparisce. Schermata presa.
⚠️ Vale l'avviso di sempre: il microfono lo da' solo il nome pubblico. Da
`mang3` il bottone c'e' ma il browser non lo concede - e l'errore `not-allowed`
lo dice in italiano, invece di lasciar pensare a un guasto.

**🐞 SUBITO DOPO - «vedo il microfono... ma non mi sembra funzionare».** Il
difetto era mio e stava in una SEQUENZA, non in una riga: quando il browser
nega il microfono manda **prima `onerror` e subito dopo `onend`**. L'errore
scriveva la sua spiegazione nella striscia, e mezzo millisecondo piu' tardi
`onend` la nascondeva (`if (!acceso) spegniStriscia()`). A video restava un
lampo, e chi guarda conclude giustamente che e' rotto. In piu' due `catch`
spegnevano tutto **in silenzio**, senza dire niente a nessuno.

Ora un avviso **si prenota sette secondi di video** (`avvisoFinoA`) e finche'
non scade nessuno lo puo' spegnere; i due catch muti parlano; e c'e' un caso in
piu' che in campeggio e' reale, `network`: il riconoscimento di Chrome gira sui
server di Google, quindi **senza internet non c'e' dettatura**, il server di
casa non basta.

Provato al banco riproducendo la sequenza vera (onerror + onend di fila): la
striscia resta, e dice *«Il browser non da' il microfono. Apri il diario da
casarosini.duckdns.org (non da mang3), e alla domanda rispondi Consenti»*.
Rifatto anche il giro buono: intatto.

**La lezione:** *un messaggio d'errore non e' scritto finche' non lo si e' visto
restare a video.* L'avevo provato accendendo e spegnendo, mai fallendo.

---

## 2026-09-09 (8) - «Pigio Parla e non compare nessuna riga» - due difetti miei

**Il sintomo:** vitti, sul tablet, dopo aver messo la spunta del ruolo: *"pigio
il bottone parla, quello con il microfono, ma non compare nessuna riga"*.

**Prima cosa: capire QUALE bottone.** Nei log (`access.log`, 14:03-14:06,
sorgente 10.9.0.5) si legge tutto il giro: `?p=users&edit=9`, `edit=15` - le
spunte del ruolo - e poi `?p=interprete`, con referer `https://mang3:8443/...`.
Non ha mai aperto l'editor del diario: il bottone e' **quello dell'Interprete**
(che infatti si chiama «Parla»; quello dell'editor e' un'icona senza scritta).
Lui l'ha confermato subito dopo. Cercare il difetto nel posto sbagliato sarebbe
costato mezz'ora.

**Difetto 1 - IL BOTTONE SPENTO NON DICEVA NIENTE.** Con «Riconosci da sola»
(che e' il valore di partenza!) il microfono lo disabilitavo, e la spiegazione
la mettevo nel `title`: cioe' in un suggerimento che si vede solo fermandoci
sopra il puntatore - e **su un tablet non si vede mai**. Uno preme, non succede
niente, e conclude giustamente che e' rotto. Adesso il bottone si preme sempre;
se manca la lingua, **lo dice** nella riga di avviso.

**Difetto 2 - E COMUNQUE DA `mang3` IL MICROFONO NON C'E'.** E' la stessa storia
dei Fogli chiusa oggi pomeriggio: il browser lo concede solo a una pagina col
certificato buono. Lo sapevo e non l'avevo scritto **da nessuna parte nella
pagina**: l'avevo detto a voce in chat, che non e' un posto dove uno va a
cercare. Aggiunta `vri_altra_porta()` (gemella di `vrf_altra_porta()` dei
Fogli, e con la stessa chiave `origine` nel JSON del servizio): navigando su un
nome diverso da quello pubblico compare una riga rossa che lo dice, chiarisce
che **scrivere e tradurre funzionano lo stesso**, e offre il collegamento alla
porta giusta.

**Collaudo:** banco con riconoscitore finto - bottone visibile, premibile, e al
clic la riga *«Per dettare scegli prima la lingua di partenza...»*; piu' la
schermata della pagina resa da un nome non pubblico, con l'avviso al suo posto.

**La lezione, che oggi si e' ripetuta tre volte:** *un impedimento che il codice
conosce va scritto NELLA PAGINA.* Prima il filo di dati dei Fogli, poi l'errore
del microfono che durava un lampo, adesso il bottone spento con la spiegazione
in un tooltip. Ogni volta il codice sapeva perche' non funzionava, e chi
guardava lo schermo no.

**🐞 E SUBITO DOPO: «ripete la stessa frase un mucchio di volte».** Prima prova
a voce vera della dettatura (vitti, 09/09 sera): il riconoscimento e' buono, ma
ogni frase finiva nel post piu' e piu' volte.

**Perche'.** In modo continuo `e.results` **non e' quello che e' cambiato
adesso**: e' l'elenco di TUTTA la sessione, che si allunga a ogni battuta. E
`e.resultIndex` torna indietro ogni volta che il riconoscitore ci ripensa,
quindi un pezzo **gia' definitivo ripassa dal ciclo** a ogni evento successivo -
e ogni volta lo riscrivevo. Al banco non si era visto perche' gli avevo dato un
risultato per volta, pulito: proprio la cosa che il browser vero non fa mai.

**Fatto:** un contatore dei pezzi definitivi gia' scritti (`visti`), azzerato in
`onstart` cosi' il riavvio automatico riparte pulito. Un pezzo entra nel post
solo la prima volta.

**Collaudo rifatto imitando Chrome sul serio** - elenco che cresce, resultIndex
che torna indietro, provvisori in mezzo, e un riavvio automatico:
`[f1] · [f1,pv] · [f1,f2] · [f1,f2] · [f1,f2,pv] · riavvio · [f3]` produce
*«Inizio. Oggi siamo a Kanegra. Il mare e' calmo. Seconda ripresa.»* - ogni
frase UNA volta, il provvisorio mai.

**La lezione:** *un banco che dice sempre di si' non e' un collaudo.* Le finte
vanno fatte disordinate come la realta', se no si prova solo il caso che si
aveva in mente scrivendo il codice.

---

## 2026-09-13 - TUTTA ALTEZZA: il foglio si allunga, non si allarga

**Perche':** vitti: *"pensi che riusciamo a fare un tutto schermo, nell'editor
del VRV?"*. E subito dopo, la correzione che ha cambiato il lavoro: *"forse piu'
che tutto schermo, sarebbe meglio tutta altezza, altrimenti si perde la
proporzione della pagina"*.

**Fatto** (`js/vrv-carica-tinymce.js`, `css/vrv_modifica_post.css`):

- Acceso il plugin di serie `fullscreen` (stava gia' in `/libs/tinymce/plugins/`,
  come `searchreplace` stamattina: bastava nominarlo) e messo in coda alla barra
  un bottone **vvSchermo**, che si chiama *Tutta altezza* e si accende quando e'
  attivo. Si esce con lo stesso bottone o con Esc; di serie arriva anche la
  scorciatoia Ctrl+Shift+F.
- **La larghezza resta quella della pagina.** Il fullscreen di TinyMCE prende
  tutta la finestra, e su uno schermo largo il foglio diventava 1.920px: le
  righe lunghissime non dicono piu' niente su come il post si leggera' nel
  diario. In `.tox-fullscreen` il foglio e' fissato a `--vrv-page-max` (1200px)
  e centrato, con un velo scuro ai lati.
- `resizeEditor()` **non tocca piu' l'altezza mentre si e' in tutta altezza**:
  il suo calcolo (fin dove arriva il footer) la riportava corta. Uscendo,
  l'evento `FullscreenStateChanged` gli fa rifare il conto.

**Il rosicchio degli !important:** il blocco della cornice mette
`width/max-width: 100% !important` su `.tox.tox-tinymce`. Senza pari forza il
`max-width: 1200px` non veniva nemmeno guardato, e la prima schermata l'ha
mostrato subito: foglio largo quanto lo schermo e scivolato mezzo fuori a
sinistra.

**Collaudo:** banco `_mastro_render/tuttaltezza_20260913/prova.html` con i CSS
veri della pagina di modifica (rosso, cornice, barra, pie'), e Chrome headless
a 1440x900 prima e dopo: il foglio passa da ~500px a ~850px di altezza e
**resta largo 1200**, centrato, con testata e pie' coperti.

**La lezione:** *"tutto schermo" e "tutta altezza" non sono la stessa cosa.* Chi
scrive un post guarda la larghezza per capire dove andranno a capo le righe:
allargare il foglio gli toglie l'unica misura che stava usando.

**🔁 Poche ore dopo, dal tablet: «non e' un vero tutto schermo».** vitti, con
l'iPad in mano: *"in landscape il tasto e' coperto dai pin di navigazione, e in
portrait non e' un vero tutto schermo, perche' le barre ci sono ancora"*. E la
proposta giusta: *"magari il pulsante toglierebbe tutta la cornice, anche
quella del browser"*.

**Fatto** (stessi due file):

- **Il tasto e' passato in testa alla barra**, subito dopo la lente. In coda
  finiva sotto `.vv-scrollpins`, i segnaposto fissi a destra: sul tablet in
  orizzontale erano proprio sopra. In tutta altezza i pin ora **spariscono**.
- **Il tutto schermo vero**, quello che toglie anche la cornice del browser:
  `requestFullscreen()`. Il tasto adesso fa le due cose in fila - prima il
  fullscreen di TinyMCE (che mette la classe sul body, cosi' il
  ridimensionamento che segue trova la guardia gia' alzata), poi quello del
  browser.
- **Chiesto per TUTTA LA PAGINA, non per il riquadro dell'editor** - ed e' il
  motivo per cui NON si e' usata l'opzione pronta `fullscreen_native`. A
  schermo intero il browser disegna solo il sottoalbero dell'elemento scelto, e
  le finestre dell'editor (Foto, Maserati, cerca, codice) vivono in
  `.tox-tinymce-aux`, **fuori** da quel riquadro: sarebbero sparite.
- **Esc rimette a posto tutt'e due.** Il tasto Esc lo intercetta il browser, non
  la pagina: senza un orecchio su `fullscreenchange` si restava su un foglio
  lungo dentro una pagina normale, con la testata nascosta e nessun modo ovvio
  di tornare indietro.
- Dove l'API non c'e' (Safari su iPhone) non succede niente: resta il
  tutta-altezza di prima.

**Collaudo, stavolta col GESTO vero:** Chrome headless pilotato via CDP (Node 26
ha `WebSocket` di suo, niente puppeteer) - `Runtime.evaluate` con
`userGesture: true` perche' il fullscreen lo si puo' chiedere solo sotto un
clic. Premuto il tasto: classe sul body ✓, `document.fullscreenElement` =
**HTML** ✓, pin `display:none` ✓. Aperta la finestra Cerca **stando a schermo
intero**: 480x249 visibili ✓ (era il rischio vero). Poi
`document.exitFullscreen()`, che e' quello che fa Esc: l'editor esce dal suo
tutta-altezza da solo, i pin tornano, la testata torna al suo posto ✓.

**La lezione:** *a schermo intero esiste solo il sottoalbero che hai scelto.* Il
riquadro dell'editor sembrava la scelta ovvia ed era quella che nascondeva meta'
dei comandi; la pagina intera costa una riga in piu' e non perde niente.

**🐞 E dal tablet vero: «la testata c'e' ancora».** Redmi Pad Pro (Android 16,
Chrome), non l'iPad: la cornice del browser spariva ✓ e i pin pure ✓, ma **il
foglio restava incastrato sotto la testata della pagina**, e in verticale la
barra sbordava a destra.

**Perche'.** Il foglio in tutta altezza e' `position: fixed`, e un fixed si
ancora al viewport **solo se nessun antenato crea un containing block**.
L'editor sta dentro `<article class="vv-card vv-edit vrv-panel
vrv-editor-wrap">`, e `.vrv-panel` (in `vrv_layout.css`) ha
**`backdrop-filter: blur(4px)`**. Tanto basta: il foglio restava appeso al
pannello - partiva sotto la testata e teneva la larghezza della pagina (1200)
anche su uno schermo da 840.

Al banco non si era visto per un motivo preciso: **avevo riprodotto la cornice,
non la catena degli antenati**. Messo il pannello vero attorno all'editor, il
guasto e' comparso al primo colpo, negli stessi numeri del tablet: foglio a
`y=106`, alto 804 su 1300 di schermo.

**Fatto:** mentre si sta in tutta altezza, su `.vrv-panel`/`.vv-card`/
`.vrv-editor-wrap` si spengono `backdrop-filter`, `filter` e `transform`. Dopo:
`0, 0, 840x1300` in verticale e `40, 0, 1200x800` in orizzontale - pieno sopra
e sotto, largo quanto la pagina e centrato.

**La lezione:** *un banco che copia il pezzo ma non i suoi antenati prova meta'
del problema.* E: **un `backdrop-filter` non e' una decorazione**, e' un
containing block - la stessa famiglia di guai dei blur sugli sfondi.

**E un terzo che stava fisso in alto a destra.** Screenshot delle 22:45, il post
183 a tutto schermo con la vignetta del Maserati: bello, ma il cartellino
`#global-user-badge` (*"UbiOneKenobi (editor)"*, con la nota della musica e
l'avatar) copriva **Grassetto, Corsivo e Sottolineato**. Sta `position: fixed`
con `z-index: 2147483647` - il massimo numero intero a 32 bit, cioe' sopra
qualunque cosa, foglio compreso. In tutta altezza sparisce anche lui, con i pin:
mentre si scrive non serve.

---

## 2026-09-14 - LE FOTO DEL POST PASSANO DAL RIDIMENSIONATORE

**Perche':** vitti, da Basovizza: *"i disegni del fumetto di Eleonora dove
finiscono? ... quello che volevo evitare e' di avere tante copie diverse delle
stesse foto/disegni. img.php funziona su quella cartella?"*.

**La risposta era no.** Le basi consentite di `img.php` erano quattro -
`leNostre`, `maps/img`, `shared/post_wallpaper`, `shared/foto_ospiti` - e la
cartella dove vivono le immagini dei post non c'era. Quelle immagini le serviva
**nginx dirette, a piena risoluzione**: aprire il post 183 voleva dire
scaricare 16 MB di originali, telefono compreso.

Da dire subito, perche' era la sua paura: **copie doppie non ce n'erano**. In
`post_183` ci sono 25 file e nessuna variante `_800`/`thumb`. Il rischio era
l'opposto - il giorno che fosse servita una miniatura, senza `img.php`
l'unica strada sarebbe stata fare un doppione.

Da sapere anche: **`vv/foto` non e' una cartella, e' un LINK** a
`/srv/http/vittrosviaggi_shared/foto`. Quindi i disegni stavano gia' sul
ripiano giusto - fuori da leNostre e fuori dall'albero del codice - solo che
nessuno lo sapeva guardando l'indirizzo.

**Fatto, in due passi.**

1. **La base nuova**, in `maps/img.php` E nel gemello `lab/public/media/img.php`
   (sono gia' segnalati l'uno nell'altro: si toccano insieme):
   `'/vittrosviaggi_shared/foto' => '/srv/http/vittrosviaggi_shared/foto'`.
   Cartella NOMINATA, non tutta `shared`: li' dentro stanno anche i segreti.
2. **L'uploader restituisce l'indirizzo servito** e non piu' quello diretto
   (`ajax/handle_upload_media.php`): `/maps/img.php?w=1920&src=...`, la stessa
   riga che `media_popup.php` usa gia' per le foto di leNostre. Il percorso si
   scrive per esteso (e' il link, non BASE_URL) e se il link un giorno non ci
   fosse, un `realpath` se ne accorge e si torna all'indirizzo diretto invece
   di servire un 404. `/maps/img.php` e non `/media/img.php` perche' e' quello
   che il VRV usa gia' dappertutto ed esiste anche fuori dalla piattaforma.

**Collaudo:** `striscia1_FINALE.jpg`, 1.826.632 byte di originale ->
**399.719** a w=1280 e **76.092** a w=480: ventiquattro volte piu' leggero alla
misura del telefono. Il gemello di piattaforma risponde byte per byte uguale.
Provata anche la porta sbagliata: `?src=/vittrosviaggi_shared/secrets/...`
risponde **404**. Poi l'indirizzo ESATTO che l'uploader ora scrive, calcolato
con le sue stesse righe e chiesto al server: 200, `image/jpeg`.
⚠️ Il giro completo dall'editor (scegli file -> compare nel post) lo fa vitti:
lo sportello vuole la sessione.

**Attenzione per il futuro:** nessuno, nel VRV, legge l'HTML del post per
cercarci dentro le immagini (nessun `preg_match` su `src=`, nessun DOM). Il
giorno che qualcosa lo facesse - il PDF, una copertina - dovra' sapere che
l'indirizzo puo' essere un `img.php?src=`, non solo un percorso nudo.

**La lezione:** *una cartella raggiungibile non e' una cartella servita.* Il
link c'era, l'indirizzo funzionava, le immagini si vedevano: sembrava tutto a
posto, e intanto ogni lettura del post si portava via i megabyte interi.
