# Il giorno della migrazione

Procedura per portare i dati veri dalla vecchia applicazione PHP a Turso e spegnere la v1.
Ogni passo ha una verifica, non solo un'azione.

Ricavata dalla revisione finale della fase 2, dopo la lettura di tutto il codice.

## FATTO il 17 agosto 2026

I passi da 1 a 12 sono stati eseguiti. Quello che resta è in fondo, sotto "Cosa manca ancora".

- Database di produzione `promogest` creato su Turso, gruppo `default`, schema applicato
  (`ck_events_finestra` presente).
- I quattro JSON riscaricati freschi da FTP in `_dati-produzione-2026-08-17/` (fuori dalla repo,
  **non cancellare**) ed erano identici a quelli dell'11 agosto: Valentina non aveva importato nulla
  nel frattempo.
- Migrazione eseguita: **895 eventi** (243 Caseifici, 496 Salumifici, 156 Parmacotto), 116 insegne,
  244 insegne escluse, 0 alias, 1 utente.
- Verificato: zero date rovesciate, finestra 23/02/2023 → 20/09/2029, **i sette indici di mapping
  campo per campo su tutte e tre le aziende** (`parmacotto.insegna = 0` compreso), e l'hash `$2y$`
  della password che verifica con la password nota e rifiuta quella sbagliata.

### Una trappola trovata il 17 agosto, da sapere per i prossimi deploy

Vercel **blocca i deploy il cui commit ha un'email che non riesce ad associare a un account GitHub**.
I commit di questo progetto erano firmati con `15869726+paoloalby@users.noreply.github.com`, l'indirizzo
privato di GitHub, e il deploy risultava «Blocked» con la spiegazione solo dentro il dettaglio del
deploy: dalla lista sembrava semplicemente che il redeploy non partisse.

Rimedio applicato: `git config user.email "paoloalby@hotmail.com"` **locale a questo repository**, non
globale, per non cambiare gli altri progetti.

Nello stesso momento GitHub aveva un'avaria (API in *major outage*, webhook in *partial outage*), il
che ha reso la diagnosi più confusa di quanto fosse: sembrava che il redeploy non partisse per quello.

### Cosa manca ancora

1. **Il confronto a occhio del calendario** del mese corrente fra v2 e v1 aperte in due schede: le
   date devono coincidere giorno per giorno. È la verifica che chiude il cerchio sul bug che per
   cinque mesi ha spostato ogni data di un giorno, e nessun test la sostituisce.
2. **Il primo import vero dalla v2**, con l'Excel più recente di una sola azienda, guardando i
   segnali.
3. **Mezz'ora su un iPhone vero**: il gesto di installazione, la barra in basso sotto la tacca, e i
   dati in modalità aereo. Tutto il resto è stato provato dentro un `<iframe>` a 390 px, che
   riproduce le dimensioni ma non Safari.
4. **Avvisare Valentina che i conteggi saranno più bassi** di quelli a cui è abituata: vedrà «49
   prodotti» dove leggeva «264», perché adesso si contano i prodotti diversi e non le righe. Dopo
   l'incidente di giugno, una cifra più bassa senza spiegazione è il modo più veloce di farle perdere
   fiducia nell'applicazione.
5. **Puntare il dominio**, e lasciare la v1 raggiungibile e intatta per almeno un mese.

**Nessun backup.** Deciso da Paolo il 17 agosto: gli Excel sono la fonte e i dati si ricostruiscono
reimportando. Va però saputo che **le 244 insegne escluse e gli alias non stanno negli Excel**: sono
stati costruiti a mano nel tempo e nessun import li rigenera.

## Stato di partenza, aggiornato al 13 agosto 2026

La produzione **è già sana**. L'11 agosto sono stati corretti i mapping colonne, rigenerati gli eventi
con la pipeline vera e ripulita la lista delle insegne escluse dai 438 codici pratica. In produzione
ci sono 895 eventi, zero date rovesciate, 116 insegne, 244 insegne escluse, 0 alias.

Un backup dello stato precedente, con i 2.833 eventi spazzatura del 10 giugno, è in
`_backup-produzione-2026-08-11/` fuori da questa repo.

La copia in `promogest/data/` è allineata alla produzione a quella data. **Non è una garanzia per il
giorno vero**: se Valentina importa qualcosa nel frattempo, diverge senza che nessuno se ne accorga.
Il passo 6 esiste per questo.

## Prima, giorni prima e non la mattina stessa

**1. Deploy di prova su Vercel con un database Turso vuoto. FATTO il 13 agosto 2026.**

Progetto `promogest` su `paoloalby's projects`, database `promogest-test` nel gruppo `default`.
Nessun problema con `@libsql/linux-x64-gnu`: su Vercel la `npm install` gira su Linux e lo risolve da
sé, quindi il rimedio `@libsql/client/web` non serve.

Verificato con `curl` su `promogest.vercel.app`, dopo aver migrato i dati veri dell'11 agosto:

- `/api/*` senza sessione risponde 401 JSON, non redirect
- login riuscito con l'hash `$2y$` prodotto da PHP, che era la verifica più a rischio
- `GET /api/dati`: 895 eventi, 443 KB, 460 ms, zero date rovesciate
- import di `PIANO PROMO PARMACOTTO AL 27_07.xlsx`: `gravita: ok`, 156 eventi, 40 insegne,
  22 accorpamenti proposti, e la conferma sostituisce in blocco senza lasciare bozze
- header `x-vercel-id: fra1::`, quindi la regione del passo 3 è quella giusta

Due trappole trovate qui, non nel codice:

- SvelteKit rifiuta i POST `multipart/form-data` senza `Origin` corretto (403 "Cross-site POST form
  submissions are forbidden"). Dal browser non si presenta; con `curl` serve `-H "Origin: <url>"`.
- Su Hobby l'URL del singolo deployment è dietro l'SSO di Vercel e risponde 302 a tutto. Le prove
  vanno fatte sul dominio di produzione `promogest.vercel.app`, che è pubblico.

Il payload di `/api/dati` sta a 443 KB su 895 eventi. Regge, ma è il numero da tenere d'occhio quando
la fase 3 lo metterà in cache per l'uso offline.

**2. Variabili d'ambiente su Vercel.**

`JWT_SECRET` generato col comando che sta in `.env.example`, più `DATABASE_URL` e
`DATABASE_AUTH_TOKEN`.

Verifica: `JWT_SECRET` non è `cambiami`.

**3. Regione e runtime.**

Su Vercel Hobby la regione è una sola e di default è la Virginia. Deve essere `fra1`, altrimenti ogni
chiamata dallo smartphone di Valentina fa Italia, Virginia, Turso, Virginia, Italia.

Verifica: dopo il deploy, `.vercel/output/functions/*/.vc-config.json` contiene `"regions": ["fra1"]` e
`"runtime": "nodejs22.x"`.

**4. `npm run db:migrate` sul database Turso di produzione.**

Verifica: `SELECT sql FROM sqlite_master WHERE name='events'` contiene `ck_events_finestra`. È la
stessa riga che lo script di migrazione legge per ricavare gli estremi della finestra: senza, si ferma
da solo.

## Il giorno

**5. Fermare gli import sulla v1.**

Avvisare Valentina di non importare finché non è finita, o mettere l'applicazione PHP in sola lettura.
Non c'è nessun meccanismo che impedisca a un import di arrivare dopo la copia dei dati.

**6. Riscaricare i quattro JSON freschi da FTP.**

`ftp.duebytes.it` → `htdocs/app/vale/data/`, in una cartella nuova e datata. Poi lanciare la migrazione
con `--sorgente <quella cartella>`.

Il percorso predefinito dello script punta alla copia locale, che è allineata a oggi ma può non esserlo
domani. Un `--sorgente` dimenticato fallisce in silenzio su dati giusti ma vecchi, che è il tipo di
errore peggiore: non se ne accorge nessuno.

Verifica: confrontare la data del file e il conteggio degli eventi con quello che l'applicazione v1
mostra a schermo.

**7. Backup dei quattro JSON prima di toccare qualsiasi cosa**, in una cartella datata fuori dalla
repo. È l'unica copia dei dati della v1.

**8. Prova a vuoto**, cioè lo script senza `--apply`.

Se si ferma su un record fuori finestra, contarli prima di decidere:

```bash
jq '[.events[] | select(.sellout_start < "2020-01-01" or .sellout_start > "2099-12-31")] | length' events.json
```

Sui dati dell'11 agosto sono zero. Se domani non lo fossero, vuol dire che è rientrato un import
sbagliato e va capito prima di proseguire.

**9. Rivedere la lista delle insegne escluse prima di migrarla.**

Sono 244 voci. Quelle erano 682 e le 438 rimosse erano codici pratica aggiunti a mano per nascondere
la spazzatura di giugno: la cicatrice dell'incidente, non una configurazione.

Resta però una domanda aperta che è di Valentina e non tecnica: **quella lista taglia il 91% delle
righe di Caseifici**, lasciando 11 insegne su 136. Paolo ha confermato che le esclusioni sono
deliberate e servono a fare import mirati, ma la migrazione è il momento naturale per rifarle
confermare da lei guardando i numeri.

`sembraCodice()` in `src/lib/server/segnali.ts` riconosce le forme dei codici pratica ed è misurata al
98-100% su quelle: si può usare per fare l'elenco di ciò che eventualmente va tolto.

**10. `--apply --env-file <file con le credenziali Turso>`.**

Lo script pretende `--env-file` insieme a `--apply`, ed è giusto: senza, scriverebbe sul database
locale credendo di aver migrato la produzione.

## Verifiche subito dopo, in quest'ordine

1. `SELECT count(*) FROM users` uguale a 1, e **login vero con la password di Valentina**. L'hash è in
   formato `$2y$` prodotto da PHP: c'è un test che lo copre, ma va rifatto sui dati veri. Se non
   funziona, lei resta fuori e non esiste registrazione.
2. Conteggi di `events`, `companies`, `aliases`, `ignored_insegne` contro il JSON di partenza.
3. **I sette indici di mapping delle tre aziende, campo per campo.** È la conoscenza più costosa del
   dataset. `parmacotto.insegna = 0` è quello che uno `||` di troppo perde senza farlo vedere in
   nessun conteggio.
4. `SELECT min(sellout_start), max(sellout_end) FROM events`: plausibili, niente 1952 e niente 2242.
5. `GET /api/dati` in produzione, e confronto a occhio del calendario del mese corrente con quello
   della v1 aperta in un'altra scheda. **Le date devono coincidere giorno per giorno.** È la verifica
   che chiude il cerchio sul bug che per cinque mesi ha spostato ogni data di un giorno.

## Dopo

Tenere la v1 accesa e raggiungibile per qualche settimana, in sola lettura. È l'unico modo di
rispondere a "ma prima non c'era scritto così?" senza indovinare.

Non cancellare `_backup-produzione-2026-08-11/` né i JSON scaricati il giorno della migrazione.

## Se Valentina resta fuori (password persa)

Non c'è un flusso "password dimenticata" in app (I7 della revisione finale): si genera l'hash a mano
e si scrive sul database di produzione.

```
node -e "require('bcryptjs').hash(process.argv[1], 12).then(console.log)" '<nuova password>'
turso db shell promogest "UPDATE users SET password_hash = '<hash appena stampato>', token_version = token_version + 1 WHERE username = 'valentina';"
```

`token_version + 1` è necessario, non opzionale: è quello che invalida ogni sessione già aperta
(vedi `api/password/+server.ts`). Senza, un token rubato insieme alla password vecchia resterebbe
valido anche dopo il reset.
