# Phase 26 — Anteprima admin del portale + toggle password sul login **Milestone:** v2.4 Post-vendita · **Stato:** ✅ in produzione (deploy Coolify 2026-08-08) **Commit:** `09a5b1f` (toggle password) · `187550f` (anteprima admin) > **Ricostruito a posteriori il 2026-08-08.** Lavoro non pianificato in roadmap, > nato da due attriti d'uso reali. 26 è il primo numero di fase libero. ## 1. Toggle mostra/nascondi password — `09a5b1f` Il campo password del login admin non offriva modo di rileggere quanto digitato: un accesso fallito era indistinguibile da un errore di battitura. Toggle **inline**, non un nuovo primitivo in `ui/`: `type="password"` compare una sola volta in tutto il codebase, un'astrazione avrebbe avuto un solo consumatore. `type="button"` perché dentro un `
` il default è submit; `tabIndex={-1}` per tenere il Tab sulla sequenza campo → Accedi. Classi a token semantici; gli hex literal preesistenti della pagina restano da migrare col resto del debito design. **Causa a monte (non era un bug):** `ADMIN_PASSWORD` è stata ruotata il 2026-07-28 **solo su Coolify**, e `.env.local` è rimasto alla precedente. Vale anche per `NEXTAUTH_SECRET`. `.env.local` non è allineato a produzione e non va trattato come fonte di verità per le credenziali. ## 2. Anteprima admin in sola lettura del portale — `187550f` Quando un cliente segnalava "non trovo una cosa" non c'era modo di guardare il portale con i suoi occhi: il gate OTP lascia entrare solo lui. Dall'elenco clienti (`ClientRow`) un'icona apre ora `/client/?preview=1` in una scheda nuova. **Come funziona.** `getClientGate()` accetta `{ previewRequested }` e salta il gate solo se il query param c'è **e** `getServerSession(authOptions)` è valida. Senza il param anche un admin vede il gate OTP — così il gate resta testabile dal vivo. Ritorna `preview: true` **senza sintetizzare una `ClientSession`**: un admin in anteprima non è un cliente autenticato, e confondere i due stati li renderebbe indistinguibili proprio dove serve distinguerli. Il flag viaggia via `PreviewProvider` / `usePreview()` e non per prop drilling: `ApproveButton` sta quattro livelli sotto la dashboard. **Perché sola lettura.** Il portale scrive davvero: `/api/client/approve` e `/api/client/comment` autenticano sul token nel body, non sulla sessione, e `deliverables.approved_at` è immutabile una volta impostato (**LOCKED #3**). Un click distratto approverebbe un deliverable in modo irreversibile. La protezione è a livello **UI, non API**: un admin può ancora chiamare le route a mano. È voluto — l'obiettivo è impedire l'incidente, non difendersi da sé stessi. ## ⚠️ Deviazione consapevole dal vincolo LOCKED #4 `CLAUDE.md` fissa: `/client/[token]/*` → token middleware, `/admin/*` → sessione Auth.js. Ora una route client legge **anche** la sessione Auth.js. Non indebolisce nulla — per i clienti il gate OTP è identico — ma la sezione LOCKED richiede approvazione esplicita prima di essere modificata. **Annotato in `CLAUDE.md` il 2026-08-08 con l'ok dell'utente.** ## Verifica **Verificato** col build di produzione su `:3100` contro il DB reale (sole letture): `?preview=1` senza sessione admin → gate OTP; con cookie di sessione contraffatto → gate OTP; `?preview=0`, `?preview=abc`, `?preview=` → gate OTP; nessun query param **con** sessione admin → gate OTP (nessuna regressione); sessione admin valida + `?preview=1` → portale con banner e composer disattivato, sia sul cliente a progetto singolo sia su quello a due progetti. **Non verificato dal vivo:** il ramo `ApproveButton` — in produzione la tabella `deliverables` è **vuota** (0 righe), quindi quel pulsante oggi non si renderizza mai. Wiring controllato solo a livello di codice. Il click dell'occhiolino sul login è verificato solo nel markup renderizzato (`type="button"`, `tabindex="-1"`, `aria-label`). **Nota di metodo:** Playwright non funziona contro `npm run dev` — la CSP blocca `eval` e i client component non si idratano. Va usato il build di produzione.