diff --git a/.planning/STATE.md b/.planning/STATE.md
index 3aa2830..7006ec7 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -3,9 +3,9 @@ gsd_state_version: 1.0
milestone: v2.5
milestone_name: Audit
status: executing
-stopped_at: "v2.5 in PAUSA. Modifiche hub: A, B, C1, rifiniture e chat a canali (0021) in prod; C2 (TidyCal) bloccato sulle credenziali API. La chat non e' stata provata a mano: .env.local non autentica piu'."
-last_updated: "2026-08-21T16:10:00.000Z"
-last_activity: 2026-08-21 -- chat a canali (portale + inbox admin), migration 0021
+stopped_at: "v2.5 in PAUSA. Modifiche hub: A, B, C1, rifiniture, chat a canali (0021) e modifica messaggi + firma (0022) in prod; C2 (TidyCal) bloccato sulle credenziali API. Niente della chat e' stato provato a mano: .env.local non autentica piu'."
+last_updated: "2026-08-21T20:55:00.000Z"
+last_activity: 2026-08-21 -- modifica dei messaggi (0022) e firma di chi risponde in chat
progress:
total_phases: 4
completed_phases: 0
@@ -40,6 +40,7 @@ Phase 27 resta a metà — schema e fonti in prod, resto da scrivere.
| Rifiniture — tassonomie, "In revisione", tab pagamenti, riordino task | ✅ in prod 2026-08-20 (migration `0019`) |
| Portale cliente — stepper compatto/full-width, card offerta | ✅ in prod 2026-08-21 (`0020`); override provato su Caruso Speaker |
| Chat a canali — portale + inbox admin per canale | ✅ in prod 2026-08-21 (`0021`); build verde, **mai provata a mano** |
+| Chat — modifica messaggi + firma admin da `settings` | ✅ in prod 2026-08-21 (`0022`); menzioni rimandate, manca l'attribuzione |
| D — Whop → audit | ⏸️ dipende dal motore v2.5 |
Progress: [███░░░░░░░] 25% (v2.5)
@@ -92,6 +93,6 @@ Log completo in `PROJECT.md`. Vive per il lavoro corrente:
## Session Continuity
Last session: 2026-08-21
-Stopped at: **chat a canali** — "Generale" + una per fase, chiave derivata in `src/lib/chat-channels.ts` e condivisa fra portale e inbox admin (se le due sponde la calcolassero da sole, la risposta finirebbe in un altro tab). Migration `0021` applicata a prod prima del push, tabella riletta a conferma: `client_channel_reads` per il non-letto per canale lato cliente (l'admin resta su `admin_last_read_at`), più il primo indice mai esistito su `comments`. Build e lint verdi. **Scritto e buildato, non provato a mano**: le credenziali locali non lo permettono più.
-Next: (1) aprire il portale in prod e provare la chat — mandare un messaggio su una fase e verificare che la risposta admin torni nello stesso tab; (2) sbloccare TidyCal con token o documentazione; (3) `LEAD_WEBHOOK_SECRET` su Coolify — finché manca la route risponde 403 a tutti; (4) poi v2.5 da `src/lib/audit/schema.ts` + `agents/`.
+Stopped at: **modifica dei messaggi** (stile Slack: illimitata, senza storico, etichetta «modificato» — scelta deliberata, motivata in `STATUS.md`) e **firma di chi risponde**, che era la stringa `iamcavalli` cablata nel pannello. Migration `0022` applicata a prod prima del push e riletta a conferma; il nome e la foto (URL esterno, l'upload su volume non esiste) stanno in `settings`, nessuna migration. La lezione da non perdere: il difficile non era scrivere la modifica ma **propagarla** — il poll filtra su `created_at`, che una modifica non cambia, quindi servono insieme il filtro allargato a `edited_at` **e** il watermark del client sul massimo dei due; una sola delle due e o la modifica non arriva, o arriva a ogni giro per sempre. Build e lint verdi. **Niente di tutto questo è stato provato a mano.**
+Next: (1) provare la chat in prod — la verifica che conta è modificare un messaggio admin con la chat del cliente aperta e vedere il testo cambiare da solo entro ~20s senza duplicarsi; (2) sbloccare TidyCal con token o documentazione; (3) `LEAD_WEBHOOK_SECRET` su Coolify — finché manca la route risponde 403 a tutti; (4) poi v2.5 da `src/lib/audit/schema.ts` + `agents/`.
Resume file: None
diff --git a/STATUS.md b/STATUS.md
index d08c391..0cf20e0 100644
--- a/STATUS.md
+++ b/STATUS.md
@@ -183,6 +183,71 @@ così. Deploy atterrato, immagine taggata `ac74a81` = HEAD.
**Da fare:** aprire il portale di un cliente, scrivere su una fase, rispondere da
`/admin/conversazioni` e verificare che la risposta torni **in quel tab**.
+### Modifica dei messaggi + firma in chat (2026-08-21, in produzione)
+
+Due mancanze emerse **provando la chat in produzione** — che è esattamente il tipo di
+cosa che né il build né il lint possono dire.
+
+**Modifica dei messaggi**, modello Slack/Discord: si corregge un proprio messaggio
+**senza limite di tempo**, il testo precedente **non si conserva**, accanto all'ora
+compare «modificato».
+
+> ⚠️ **Scelta deliberata, non una svista.** Illimitato + nessuno storico significa che
+> un messaggio scritto mesi fa resta riscrivibile, e l'unica difesa dell'altra parte è
+> quell'etichetta. Il rischio è stato posto e accettato il 2026-08-21. Se un domani
+> servisse dimostrare cosa era stato scritto, la strada è una tabella `comment_edits`:
+> additiva, si aggiunge senza toccare niente di quanto c'è ora.
+
+**Il punto difficile non era scrivere la modifica, era propagarla.** Il poll chiede i
+messaggi con `created_at > since`, e una modifica **non cambia `created_at`** — quindi
+l'altra parte avrebbe continuato a vedere il testo vecchio fino a un ricaricamento
+completo. Il filtro ora guarda **anche `edited_at`**, e il watermark del client è il
+**massimo fra i due su tutti i messaggi**: se restasse il `created_at` dell'ultimo, il
+server rispedirebbe lo stesso messaggio modificato a ogni giro, per sempre. Le due cose
+vanno insieme — una sola delle due e o la modifica non arriva, o arriva all'infinito.
+Il merge per id era già in piedi, quindi nessun duplicato da temere.
+
+**Il non-letto resta ancorato a `created_at`, di proposito**: correggere un refuso non
+deve riaccendere il pallino di un canale già letto. Annotato nel codice, altrimenti al
+prossimo refactor sembra una svista da "sistemare".
+
+Autorizzazione: **si modifica solo ciò di cui si è autori** — il controllo è su
+`author`, non solo sulla proprietà dell'entità, e lo rifà il server (il pulsante
+nascosto non è una difesa). Migration `0022`, additiva, applicata a prod prima del push
+e riletta a conferma: 11 messaggi, 0 marcati come modificati.
+
+**Firma in chat.** Il nome era la stringa `iamcavalli` cablata nel pannello: il cliente
+leggeva il marchio dove si aspetta una persona, e il monogramma diceva `IA`. Ora nome e
+foto arrivano da `settings` — **nessuna migration**, la tabella è già un key/value con i
+suoi helper — e si impostano da `/admin/impostazioni`.
+
+La foto è un **URL esterno**, e non è un ripiego: `/api/uploads/[...path]` **non esiste**.
+La deroga al vincolo LOCKED #5 per le immagini dell'audit è scritta in CLAUDE.md ma il
+codice non è mai stato costruito (Phase 27 è ferma allo schema). Un upload vero avrebbe
+richiesto di modificare un vincolo LOCKED *e* costruire da zero volume persistente,
+Server Action, rotta di lettura, whitelist MIME e limite di dimensione. Avatar assente o
+rotto ricade sul monogramma, così un link che muore non lascia un buco.
+
+Nell'inbox admin il nome **non** cambia: lì i propri messaggi dicono «Tu (Admin)», che è
+già corretto. Il problema era solo come il cliente vede l'altra parte.
+
+⚠️ **Buildata, non provata a mano** (`.env.local` scaduto). **Da fare, in quest'ordine:**
+(1) con la chat del cliente **aperta**, modificare da `/admin/conversazioni` un messaggio
+dell'admin in quel canale — entro ~20s il testo deve cambiare da solo, senza duplicarsi;
+(2) verificare che il pallino **non** si riaccenda; (3) impostare nome e foto in
+`/admin/impostazioni` e ricontrollare il portale; (4) da `?preview=1` il pulsante
+«Modifica» non deve esserci.
+
+**Rimandato, con la ragione scritta:** le **menzioni** con più persone invitate in un
+portale. L'ostacolo non sono le notifiche ma l'attribuzione — `comments.author` contiene
+solo `client`/`admin`, non *quale* persona, quindi oggi tre invitati sono tre «Tu»
+identici. L'identità però esiste già: la sessione OTP porta l'email
+(`ClientSession = { clientId, email, iat }`) e `client_emails` è la lista degli invitati.
+Manca che le scritture della chat leggano la sessione invece del solo token — il lavoro
+già annotato in `client-chat.ts` come «un lavoro a sé». Ordine giusto: **attribuzione**,
+poi notifiche (Resend c'è già), poi eventualmente menzioni. Molto probabilmente vedere
+*chi* ha scritto risolve gran parte del problema da solo.
+
### v2.5 — Audit (Phases 27–30), in pausa
Il servizio di analisi sito (tre livelli: **Radiografia / Prima-Dopo / Rotta**) diventa
diff --git a/design-reference/DESIGN-SYSTEM.md b/design-reference/DESIGN-SYSTEM.md
index baa5cd7..c7d9ce5 100644
--- a/design-reference/DESIGN-SYSTEM.md
+++ b/design-reference/DESIGN-SYSTEM.md
@@ -151,7 +151,9 @@ across all future admin pages.
| `ClientProfitability` | `src/components/admin/dashboard/ClientProfitability.tsx` | Dashboard widget: valore orario reale per cliente (`getClientProfitability` → contrattualizzato ÷ ore tracciate) con badge margine (Ottimo `bg-primary/10 text-primary` / In Linea `bg-muted`). |
| `YearSelector` | `src/components/admin/YearSelector.tsx` | Selettore anno restilizzato a pill (`rounded-full border-border bg-card shadow-card`), frecce ←/→, valore `font-mono`. Guida i dati year-scoped della dashboard via `?year=`. |
| `ConversationsView` | `src/components/admin/conversazioni/ConversationsView.tsx` | Inbox messaggi clienti (`/admin/conversazioni`) a due pannelli: lista conversazioni filtrabile (search su nome/brand/ultimo messaggio, pallino unread) + thread attivo con **tab per canale** (Generale + una per fase, pallino accent sul tab non letto) e bolle messaggio (admin a destra `bg-primary`, cliente a sinistra). Badge entità solo sui messaggi storici di task/deliverable. La risposta va sull'entità del canale attivo. Auto-mark-read all'apertura, su snapshot di `adminLastReadAt`. |
-| `ChatPanel` | `src/components/client/ChatPanel.tsx` | Chat del portale cliente: FAB `h-14 w-14` fisso in basso a destra con pallino unread, drawer laterale `fixed inset-y-0 right-0 bg-card shadow-2xl` che in `expanded` diventa full-screen (`Maximize2`/`Minimize2`, `Esc` chiude prima l'espansione poi il pannello). Tab canale scrollabili orizzontalmente (`no-scrollbar`), invio ottimistico, polling `GET /api/client/chat` ogni 20s. |
+| `ChatPanel` | `src/components/client/ChatPanel.tsx` | Chat del portale cliente: FAB `h-14 w-14` fisso in basso a destra con pallino unread, drawer laterale `fixed inset-y-0 right-0 bg-card shadow-2xl` che in `expanded` diventa full-screen (`Maximize2`/`Minimize2`, `Esc` chiude prima l'espansione poi il pannello). Tab canale scrollabili orizzontalmente (`no-scrollbar`), invio **e modifica** ottimistici, polling `GET /api/client/chat` ogni 20s. La firma di chi risponde arriva da `settings` via `ChatData`, non è più cablata. |
+| `MessageRow` | `src/components/client/ChatPanel.tsx` | Una riga del feed, componente a sé perché la modifica ha bisogno di stato per messaggio (bozza, salvataggio, errore). Divider di giornata, raggruppamento dei messaggi consecutivi dello stesso autore entro `GROUP_WINDOW_MS`, badge entità per lo storico di task/deliverable, «(modificato)» inline dopo il corpo. |
+| `Avatar` (chat) | `src/components/client/ChatPanel.tsx` | Cerchio `h-9 w-9` con immagine da URL esterno e **fallback al monogramma** se `src` manca o il caricamento fallisce (`onError`). `
` nudo e non `next/image`: l'URL lo incolla l'utente in Impostazioni e non è noto in fase di build, quindi non si può dichiarare l'host in `next.config`. |
| `ChatProvider` | `src/components/client/ChatProvider.tsx` | Context del solo stato UI (aperto/espanso/canale attivo) — i messaggi restano dentro `ChatPanel`, unico a fare polling e a tenere la copia optimistic. Il canale sta nel context perché lo decide anche chi è fuori dal pannello: `openChat(phaseId)` dalla bolla di una `PhaseCard` apre la chat già sulla fase giusta. |
| `MessagesWidget` | `src/components/admin/dashboard/MessagesWidget.tsx` | Widget dashboard "Messaggi Clienti": chat clienti in attesa di risposta (`getConversations()` filtrato su `unread`), pill emerald "N Nuovi" + pallino pulsante, righe con anteprima e link `Rispondi →` deep-link a `/admin/conversazioni?c=`. Empty state "Nessun messaggio in attesa ✓". |
| `CopyLinkButton` | `src/components/admin/CopyLinkButton.tsx` | Icon button che copia un URL assoluto (`window.location.origin + path`) negli appunti; swap icona → check emerald per 1.5s con tooltip "Copiato!". Usato nella lista Clienti per il link profilo pubblico. |
@@ -237,6 +239,19 @@ resta a due pannelli, con i canali come tab dentro il thread attivo.
giorno 0. La 0021 aggiunge `(entity_id, created_at)`, che è esattamente il modo
in cui il feed si legge.
+**Pattern «Modifica» sui messaggi** (portale e inbox admin, stessa forma): pulsante
+testuale piccolo, non un'icona, coerente con i già presenti `Riprova`/`Elimina`. Si
+rivela al passaggio del mouse ma **deve restare raggiungibile senza puntatore**:
+
+ opacity-0 transition-opacity group-hover:opacity-100
+ focus-visible:opacity-100 [@media(hover:none)]:opacity-100
+
+L'ultima clausola non è un dettaglio — senza, il pulsante è irraggiungibile da telefono,
+ed è da telefono che il cliente apre il portale. In modifica la bolla diventa una
+textarea: `Invio` salva, `Esc` annulla, più i due pulsanti espliciti. Nell'inbox admin i
+controlli stanno **fuori** dalla bolla: sopra `bg-primary` un testo tenue non si legge.
+Il pulsante nascosto non è una difesa — chi può modificare lo ridecide il server.
+
### Clienti (lista) notes
`/admin/clients` migrata a "Quiet Luxury" (replica `design-reference/pagina-clienti.html`).