# VRP - Catalogo dei Database

**Stato:** 2026-07-10. Elenco di TUTTI i database della piattaforma: tabelle,
scopo, credenziali, migrazioni, + accesso CLI e utenti del server. Modello di
riferimento in [VRP_B200_architettura](VRP_B200_architettura.md). Un solo
server MariaDB.

## Regola d'oro

- **Identita'** (utenti + sessione) = UN DB condiviso: `vrp_auth`.
- **Contenuti** = un DB per servizio (federati). I JOIN cross-DB restano
  possibili (stesso server).
- Per vedere TUTTI i database e per scrivere/DDL serve **root** (con la sua
  password, in cassaforte): l'utente di default della CLI (`vv_backup` via
  `~/.my.cnf`) e' in sola-lettura e vede solo `photo` e `vittrosviaggi`.
  Dettagli in "Accesso da riga di comando (CLI)" e "Utenti del server MariaDB".

## I database

### vrp_auth   [ESISTE - identita' condivisa + sessioni/azioni]
Identita' unica di piattaforma (login UNICO) + ruoli per-servizio + registro
accessi/azioni. Letto via `vrp_db('vrp_auth')`. Tabelle e vista:

- **`users`** (13 righe) - anagrafica dell'identita' unica (una persona = una riga).
  `id` [PK], `username` [UNIQUE], `email`, `pass_hash` (password_hash), `display_name`,
  **`motto`** (06/09/2026), `avatar`, `is_active` (0=disattivato), `created_at`,
  `last_login_at`. Letta da `vrp_login`/`vrp_current_user`.
  📌 `avatar` e `motto` sono il **corrente**; i precedenti stanno in `user_storia`.
- **`user_storia`** (06/09/2026) - avatar e motti **precedenti**, una riga per
  cambio. `id` [PK], `user_id` [FK->users.id, CASCADE], `cosa` ENUM('avatar','motto'),
  `valore` (nome del file oppure testo), `ritirato_il`, `ritirato_da` (chi ha fatto
  il cambio: il padrone o un admin). UNA tabella per due cose perche' e' lo stesso
  mestiere; UNA data sola perche' il "da quando" e' il `ritirato_il` della riga
  precedente. Registro che si APPENDE e non si rilegge mai per sapere "chi sono
  adesso" -> non puo' nascere una seconda verita'.
  Scritta/letta da `vrp_user_ritira()` / `vrp_user_storia()` (lib/vrp_auth.php).
- **`jukebox_playlist`** (06/09/2026) - le playing list del JukeBox.
  `id` [PK], `user_id` [FK->users.id, CASCADE], `nome`, `brani` (chiavi
  `<scaffale>/<file>` separate da `|`, in ordine - stesso idioma di `post.musica`),
  `creata_il`, `cambiata_il`; UNIQUE(`user_id`,`nome`) -> salvare con lo stesso
  nome AGGIORNA. Sta qui perche' e' appesa a un utente e il JukeBox non ha un DB
  suo (come `vrw_import`). Si LEGGE da tutti, ospiti compresi; la SCRIVE solo il
  padrone (o un admin).
- **`user_services`** (23 righe) - la mappa "chi puo' cosa": il RUOLO dell'utente in
  ogni servizio. `user_id`+`service` [PK composta], `role` (es. admin/editor/viewer/
  registrato/amik_nat), `granted_at`. Un utente ha piu' righe (una per servizio).
  Letta da `vrp_current_user` -> `roles[service]=role`.
- **`sessions`** (registro del portinaio E registro-azioni, opzione A) - `id` [PK],
  `php_sid`, `user_id` [FK->users.id, NULL=ospite], `ip`, `user_agent`, `service`,
  `version`, `action`, `detail`, `started_at`, `last_activity`, `ended_at`.
  Due tipi di riga: (1) CICLO-SESSIONE `action` in login/logout/timeout, la riga viva
  si aggiorna via UPDATE (`vrp_login`/`vrp_logout`/`vrp_session_check`); (2) EVENTO
  (azione utente) `action` in upload/download/delete/create_dir/... con `detail` umano
  ("caricato il file X"), INSERT nuova via `vrp_insert_action` (lib `vrp_action.php`).
- vista **`sessions_h`** - `sessions` LEFT JOIN `users` -> aggiunge `username` e
  `display_name` leggibili. La legge la pagina Admin **Sessioni** (`pages/sessioni.php`,
  filtri username/action, colonne configurabili incl. `detail`). Suffisso `_h`=vista umana.

- Creds: `vittrosviaggi_shared/secrets/db_vrp_auth.php` (utente app `vrp_auth_app`, solo
  SELECT/INSERT/UPDATE/DELETE -> le DDL le lancia root).
  ⚠️ **Il root di MariaDB qui NON e' unix_socket**: vuole la sua password (cassaforte
  KeePass VittRos.kdbx). `sudo mysql` non basta e da' `Access denied ... (using
  password: NO)`; il comando giusto e' `mariadb -u root -p <db> < file.sql`.
  Ultima DDL: `docs/VRP_ddl_20260906_motto_avatar_playlist.sql` (06/09/2026).

### vittrosviaggi   [ESISTE - VRV, diario]
Contenuti del diario. Tabelle principali:
- `post`, `post_drafts`, `post_versions`, `post_translations`, `post_trad_cache`

**Parentela dei post** (colonne di `post`, spiegate anche nei loro COMMENT,
quindi visibili con `SHOW FULL COLUMNS FROM post`):

| colonna | famiglia | domanda a cui risponde |
|---|---|---|
| `parent_id` | LINGUA | da chi sono stato tradotto (padre immediato) |
| `source_id` | LINGUA | a che famiglia appartengo (il capostipite) |
| `gemello_id` | ANW | chi e' il mio gemello di variante opposta |
| `variante` | ANW | sono `tessile` o `naturista` |

`parent_id` e `source_id` **coincidono sempre**, perche' si traduce solo
dall'italiano e gli alberi sono profondi un livello. Si separerebbero solo
traducendo *da una traduzione* (it -> en -> is): l'islandese avrebbe
`parent=en` ma `source=it`. Mai successo finora.

Si tengono tutte e due per un motivo solo: **il capostipite puo' portare
`source_id`** (punta a se stesso), `parent_id` no. Quindi la famiglia
intera, radice compresa, si legge in una query piatta senza ricorsione:

    SELECT * FROM post WHERE source_id = 117;   -- le 6 lingue delle Olimpiadi

`source_id` e' una **scorciatoia** (indice denormalizzato), non un secondo
concetto. La usano `visualizza_post.php` (selettore di lingua), le tre viste
`vw_post_effective*`, `ajax/create_translation.php`.

*Invarianti* da mantenere quando si creano o spostano post:
1. ogni figlio ha `source_id` valorizzato e uguale a quello del padre;
2. ogni capostipite con almeno un figlio ha `source_id = id`;
3. nessun `source_id`/`gemello_id` punta a un post inesistente;
4. `gemello_id` e' reciproco (se A punta a B, B punta ad A);
5. un post singolo puo' avere `source_id` NULL: il codice usa il fallback
   `source_id ?: id`.

Query di controllo (devono dare tutte 0):

    SELECT COUNT(*) FROM post WHERE parent_id IS NOT NULL AND source_id IS NULL;
    SELECT COUNT(*) FROM post p WHERE p.source_id IS NULL
      AND EXISTS (SELECT 1 FROM post c WHERE c.parent_id = p.id);
    SELECT COUNT(*) FROM post p WHERE p.source_id IS NOT NULL
      AND NOT EXISTS (SELECT 1 FROM post q WHERE q.id = p.source_id);
    SELECT COUNT(*) FROM post p WHERE p.gemello_id IS NOT NULL
      AND NOT EXISTS (SELECT 1 FROM post q WHERE q.id = p.gemello_id);
    SELECT COUNT(*) FROM post c JOIN post p ON p.id = c.parent_id
      WHERE c.source_id <> p.source_id;

*Riparazione 27/07/2026* su `vittrosviaggi_lab` (backup preso prima): 23
`source_id` mancanti riempiti (tutti da NULL, nessuna sovrascrittura), 1
`source_id` fantasma azzerato (post 65 -> 1), 2 `gemello_id` fantasma
azzerati (post 19 -> 156, post 58 -> 155: quei gemelli non esistono; se
servono, vanno ricreati). Il selettore di lingua era rotto: 14 traduzioni
su 17 non venivano trovate. **PRODUZIONE (`vittrosviaggi`) riparata lo
stesso giorno** (26 modifiche, 0 sovrascritture, backup prima): riverificato
il 29/07, figli orfani = 0 e il post 117 rende 6 lingue (prima 1).

*CR aperta*: rinominare `source_id` in `famiglia_id` (il nome dice
"sorgente" ma il significato e' "famiglia"). Non fatto subito: tocca tre
viste e mezza dozzina di file per zero funzionalita' in piu'.

### cofanetto + cofanetto_post   [CREATE 29/07/2026, solo lab]

Terzo livello, sopra la famiglia-lingue: raggruppa post **diversi** per tema
(una serie, un'annata di viaggi). La famiglia ha un capostipite naturale (il
post italiano), il cofanetto no -> ha una riga propria con un nome.

    COFANETTO  "L'inchiesta sui fazzoletti scomparsi"   <- cofanetto
      +-- FAMIGLIA  Ep.1 (125) --> en, de...            <- cofanetto_post
      +-- FAMIGLIA  Ep.2 (124) --> ...

*Decisione di disegno*: il cofanetto lega **famiglie, non post**. La riga di
`cofanetto_post` punta al **capostipite** (`famiglia_id` = `source_id ?: id`,
invariante 5 qui sopra), non alla singola lingua. Conseguenza pratica: il
visore disegna la barretta "prec / cofanetto / succ" risolvendo ogni voce
nella lingua in lettura, con ripiego sull'originale se quella puntata non e'
tradotta. La barretta cucita a mano nel contenuto (25/07) aveva invece i
link agli ID italiani -> da una traduzione si rimbalzava in italiano.

Nessun `cofanetto_id` dentro `post`: due tabelle nuove, zero ALTER sul
tavolo dove lavora Elena (vedi la regola sui DDL a fine documento).

`cofanetto`
  - `nome`, `slug` (UNIQUE, ASCII per l'URL), `sottotitolo`
  - `copertina_post_id` -> `post`, ON DELETE SET NULL. Se valorizzato e' un
    post disegnato a mano (il 167); se NULL l'indice lo genera il visore.
  - `categoria_id` -> `categorie`, ON DELETE SET NULL (filtri a chip)
  - `ordine`, `nascosto`, `data_creazione`

`cofanetto_post`
  - PK (`cofanetto_id`,`famiglia_id`); `famiglia_id` UNIQUE = una famiglia
    sta in **un solo** cofanetto (cosi' "prec/succ" non e' mai ambiguo).
  - `ordine` = numero di puntata dentro la scatola (1,2,3...)
  - FK: `cofanetto_id` -> `cofanetto` CASCADE, `famiglia_id` -> `post` CASCADE

Query di controllo (devono dare tutte 0):

    SELECT COUNT(*) FROM cofanetto_post cp
      WHERE NOT EXISTS (SELECT 1 FROM post p WHERE p.id = cp.famiglia_id);
    SELECT COUNT(*) FROM cofanetto_post cp JOIN post p ON p.id = cp.famiglia_id
      WHERE p.source_id IS NOT NULL AND p.source_id <> p.id;   -- non capostipite

Indice di un cofanetto in una lingua (query base del visore):

    SELECT cp.ordine, COALESCE(t.id, o.id) AS post_id
      FROM cofanetto_post cp
      JOIN post o ON o.id = cp.famiglia_id
      LEFT JOIN post t ON COALESCE(t.source_id,t.id) = cp.famiglia_id
                      AND t.lingua = :lang
     WHERE cp.cofanetto_id = :c ORDER BY cp.ordine;

*Primo contenuto* (29/07): cofanetto 1 = il Pentateuco dei fazzoletti,
famiglie 125/124/158/165/166 in ordine 1..5, copertina 167, categoria 2.

- `post_foto` (link post<->foto, oggi VUOTA), `foto` (stub, 2 righe)
- `categorie`, `configurazione`, `last_conf`, `vv_last_versions`
- `utenti`, `utenti_h`, `user_history`   (utenti VRV -> confluiscono in vrp_auth)
- `richieste_accesso`
- `share_dirs`, `share_files`   (contenuti del servizio Share/vrs; vivono QUI)
- viste: `vw_post_effective`, `vw_post_effective_public`, `vw_post_effective_staff`
- Registrato nella piattaforma come DB **`vrv`** (env.json) -> letto da `vrp_db('vrv')`.
- Creds: `vittrosviaggi_shared/secrets/db_vrv.php` (utente `vviaggi_app`, posto STABILE
  fuori dai release). Il VRV storico usa ancora `meta/secrets.local.json`.

### lenostre   [ESISTE - indice foto, 35.022 righe]
Indice delle foto dell'archivio leNostre.
- `foto`: `id`, `path` (unique), `cartella`, `nome`, `anw` (flag naturista),
  `larghezza`, `altezza`, `orientation`, `lat`, `lon`, `scattata_il`, `bytes`,
  `mtime`, `hash_md5`, `indicizzata_il`.
- Mantenuto da `wiz_foto/lenostre_index.php` (`ln_upsert`, `ln_delete_under`,
  `ln_reindex_target`) + `popola_foto_index.php`. Ogni mutazione FS lo riallinea.
- `gestore_foto.php` ha gia' l'endpoint `?azione=cerca` (LIKE su nome/cartella).
- vista **`media`** (creata 10/08/2026): `SELECT f.*, CASE ... END AS tipo FROM
  foto f`. Oggi = `foto` con in piu' la colonna calcolata `tipo`
  ('foto'|'video', dedotta dall'estensione del nome). Tutte le 35k righe sono
  'foto': i video del disco NON sono ancora indicizzati (scelta 10/08: solo foto
  per ora). E' il SEAM per i video futuri -> il codice NUOVO del VRN legge
  `media` e ramifica su `tipo`; quando i video entreranno nell'indice, la vista
  li assorbe senza cambiare i consumatori. Vista MERGE (usa `ix_cartella`).
  Il codice ESISTENTE (`vrn_foto_search`) resta su `foto` finche' non si fanno i
  video sul serio. Dettagli in `vrv_port/09_riconversione_lenostre50.md`.
- Creds: `vittrosviaggi_shared/secrets/db_lenostre.php` (posto STABILE, fuori dai release).

### photo   [ESISTE - LEGACY, da dismettere]
Solo tabelle utente di leNostre50: `users`, `user_h`, `user_history`,
`user_history_sav`. -> Migrare gli utenti in `vrp_auth`, poi dismettere.
(NB: si chiama "photo" ma NON contiene foto: quelle sono in `lenostre`.)

### vv_tmp   [ESISTE - da verificare]
Scopo da verificare (temporaneo?).

### vrs / share   [SERVIZIO VIVO - dati in `vittrosviaggi`]
Il servizio **Share** (`/lab/public/share/`, lib `platform/lib/vrs.php`) e' in produzione nel
lab. I dati vivono in `vittrosviaggi`, letti via DB `vrv`. Modello = due tabelle + i file su
disco (`share_storage/dirs/<dir_id>/<filename_stored>`); le miniature NON si copiano (al volo
via `vrp_img`, cappello `thumb.php`). Cancellazione HARD (rimuove file+tupla).

- **`share_dirs`** (7 righe) - l'ALBERO delle cartelle. `id` [PK], `parent_id` (la cartella
  madre; NULL/radice=root_dir_id 1), `name`, `owner_user_id` (proprietario, per i permessi),
  `created_at`, `updated_at`, `is_deleted` (colonna soft-delete presente ma oggi si cancella
  hard). L'albero e' "ognuno sa il padre".
- **`share_files`** (135 righe) - i FILE. `id` [PK], `dir_id` (in che cartella sta),
  `filename_original` (nome mostrato all'utente), `filename_stored` (nome sul disco, con
  suffisso _N anti-collisione), `mime_type`, `size_bytes`, `owner_user_id`, `created_at`,
  `updated_at`, `is_deleted`. I due nomi servono perche' due utenti possono caricare "foto.jpg".

Azioni loggate nel registro (`vrp_insert_action('vrs',...)`): create_dir, upload, download,
delete (file e cartella). Da valutare (non urgente) se estrarre in un DB dedicato `vrs`.

### registro azioni utente   [FATTO 09/07 - opzione A]
Log DB di "chi ha fatto cosa/quando" in italiano. SCELTA: opzione A = riga nella `sessions`
(colonna `detail` aggiunta, action='upload'/'delete'/...) mostrata nella pagina Sessioni coi
filtri. Funzione `vrp_insert_action(service,action,detail)` in `platform/lib/vrp_action.php`
(riusa `vrp_insert`, non-bloccante). Agganciata nello share (create_dir/upload/download/delete).
DA FARE: agganci import-foto/crea-post + bottone **Purge** nella pagina Admin (svuota le
righe-evento vecchie; distinto dal logrotate, che tocca i file di log non il DB).

## Credenziali (da centralizzare)

Stato attuale: i segreti dei DB usati dalla piattaforma sono TUTTI nel posto STABILE
`/srv/http/vittrosviaggi_shared/secrets/` (fuori dai release che ruotano), puntati da
`env.json` (registro `db`) e letti da `platform/lib/vrp_db.php`:
- `db_lenostre.php`  -> DB `lenostre` (indice foto)
- `db_vrp_auth.php`  -> DB `vrp_auth` (identita' + sessioni)
- `db_vrv.php`       -> DB `vrv` (= `vittrosviaggi`: diario + share)
Il VRV storico (fuori piattaforma) usa ancora `meta/secrets.local.json`.

**LEZIONE APPRESA (02/07):** le creds di lenostre stavano in `vv_work/testing/`
(un release DEV). Ritirando `vv_work` col B500, il path si e' rotto e `wiz_foto`
e' andato giu'. I segreti/config NON vanno in un release che ruota: spostati
nello shared stabile. Lavoro-piattaforma: un solo punto in `platform/boot/`,
letto da `vrp_db.php`.

## Accesso da riga di comando (CLI)

Attenzione: `root` NON e' piu' passwordless. Tre modi, in ordine d'uso:

1. **`mariadb` (senza argomenti)** -> entra come **`vv_backup`** grazie a
   `~/.my.cnf` (utente unix vitti): sola-lettura su `photo` e `vittrosviaggi`,
   e `show databases` mostra SOLO quei due (+ information_schema). Comodo per
   sbirciare il diario; NON vede vrp_auth/lenostre e non scrive.
2. **root (vede tutto, DDL)** -> `mariadb -u root -p` e incolla la password
   dalla cassaforte (KeePass `VittRos.kdbx`). ATTENZIONE: `mariadb -u root`
   SENZA `-p` FALLISCE ("Access denied ... using password: YES") perche' il
   `password=` di `~/.my.cnf` (di vv_backup) viene iniettato lo stesso; il `-p`
   sulla riga di comando lo scavalca. Il `-p` (senza valore attaccato) chiede la
   password in modo NASCOSTO: non finisce ne' in `ps` ne' nella history. NON
   usare mai `-p<password>` attaccato (finirebbe in chiaro).
3. **Da script/PHP** -> mai root: usa gli utenti-app via i secrets in
   `/srv/http/vittrosviaggi_shared/secrets/` (letti da `vrp_db()`). Es.:
   `require '.../platform/boot/3_2_1_Go.php'; vrp_row('vrp_auth', "SELECT ...");`
   E' il percorso collaudato (ogni servizio col suo utente, un DB ciascuno).

**Piu' utenti in `~/.my.cnf` (dal 10/07):** oltre al `[client]` di default
(`vv_backup`), il file ha gruppi extra selezionabili con `--defaults-group-suffix`:
`mariadb --defaults-group-suffix=vitti` -> utente `vitti` (scrive su
`vittrosviaggi`); `mariadb --defaults-group-suffix=mig` -> utente `vviaggi_mig`
(DDL su `vittrosviaggi`). Le password stanno in chiaro nel file (mode 600, e in
cassaforte).

## Utenti del server MariaDB

Ricognizione 2026-07-10 (letta da `mysql.user` come root). 16 utenti + il
`mariadb.sys` di sistema. Le PASSWORD non stanno qui (solo in cassaforte e nei
secrets): qui utente, host, privilegi e SCOPO.

### In uso - NON toccare

| Utente@host | Privilegi | Serve a |
|---|---|---|
| `root`@localhost / 127.0.0.1 / ::1 | ALL + GRANT OPTION | amministrazione, DDL. Pwd in cassaforte KeePass VittRos.kdbx |
| `vrp_auth_app`@localhost | S/I/U/D su `vrp_auth` | identita' + sessioni/azioni della piattaforma (secrets db_vrp_auth.php) |
| `vviaggi_app`@localhost | S/I/U/D + CREATE TEMP/LOCK/SHOW VIEW/EVENT/TRIGGER su `vittrosviaggi` | app VRV/diario + share (secrets db_vrv.php) |
| `lenostre_app`@localhost | S/I/U/D su `lenostre`.* ; S/I/U/D su `photo.users` + `photo.user_history` | indice foto + utenti legacy leNostre50 (secrets db_lenostre.php, db_photo.php) |
| `vv_backup`@localhost | SELECT/LOCK/SHOW VIEW/EVENT/TRIGGER su `photo` + `vittrosviaggi` | backup / lettura di comodo; e' il DEFAULT di `~/.my.cnf` |
| `vviaggi_mig`@localhost | S/I/U/D/CREATE/DROP/INDEX/ALTER/CREATE VIEW/SHOW VIEW/TRIGGER su `vittrosviaggi` | utente di MIGRAZIONE del DB VRV (DDL senza root) |
| `vitti`@localhost | S/I/U/D/CREATE/DROP/INDEX/ALTER/CREATE TEMP/LOCK/EVENT/TRIGGER su `vittrosviaggi` | utente personale di vitti sul diario (NON e' il default: quello e' vv_backup) |
| `mariadb.sys`@localhost | USAGE + SELECT/DELETE su `mysql.global_priv` | utente di sistema MariaDB, sempre presente. NON si tocca |

### Relitti / da rivedere

| Utente@host | Cosa puo' | Verdetto |
|---|---|---|
| `wordpress_user`@localhost | ALL su `wordpress`.* | WordPress non esiste piu' su manG3 (0 wp-config, 0 codice, verif. 10/07) -> **DROP sicuro** |
| `lella`@localhost | ALL su `wordpress`.* | account *MySQL* di WordPress, NON il login-app di Elena (quello e' un record in `vrp_auth`, via `vrp_auth_app`) -> **DROP sicuro**, Elena non ne risente |
| `SantUtto`@localhost / 127.0.0.1 / ::1 | quasi-super (SUPER, FILE, CREATE USER, PROCESS, RELOAD...) senza GRANT OPTION | ereditato dai tempi del NAS (~2 anni fa); non citato in codice/secrets (verif. 10/07) -> **DROP sicuro** |
| `backup`@localhost | RO su `photo` + `vittrosviaggi` (come vv_backup, un filo diverso) | PREDECESSORE di vv_backup; il cron `vv_backup_sacro.sh` usa `vv_backup` via `.my.cnf`, non questo (verif. 10/07) -> **DROP sicuro** |
| `vitti_user`@`%` | ALL su *.* + GRANT OPTION da QUALSIASI host + ALL su `vitti_user\_%`.* | *** SUPERUTENTE REMOTO *** stile pannello-hosting, raggiungibile da ovunque; non usato da niente (verif. 10/07) -> **DROP consigliato** (o restringere l'host da `%` a localhost) |

**Pulizia pronta (10/07, la lancia root):** prima un backup dei grant in un file
protetto fuori dal web, poi i DROP:
```sql
DROP USER 'SantUtto'@'localhost','SantUtto'@'127.0.0.1','SantUtto'@'::1';
DROP USER 'lella'@'localhost';
DROP USER 'wordpress_user'@'localhost';
DROP USER 'backup'@'localhost';
DROP USER 'vitti_user'@'%';   -- consigliato (superuser remoto)
FLUSH PRIVILEGES;
```

**Nota sicurezza (CHIUSA 10/07):** prima 7 utenti condividevano lo STESSO
hash-password (una sola password debole riusata). Dopo i DROP dei relitti
restavano `vitti`, `vv_backup`, `vviaggi_mig` su quella password -> **RUOTATE
10/07**, ora 3 password forti DISTINTE (32 char, non-umane) via `ALTER USER`,
allineate in `~/.my.cnf` (gruppi) e in cassaforte. root, vrp_auth_app,
vviaggi_app, lenostre_app avevano gia' password proprie.

## Migrazioni previste

1. **vrp_auth**: unificare utenti VRV + leNostre50 (delicato: ruoli, chi-puo-cosa).
2. **dismettere `photo`** dopo la migrazione utenti.
3. (eventuale) estrarre `share` in `vrs`.
