From c3d2afa61f8e07961584b22cf047c3b996834f16 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Fri, 21 Aug 2026 22:44:30 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20modifica=20messaggi=20e=20firma=20in=20?= =?UTF-8?q?chat=20=E2=80=94=20STATUS,=20STATE,=20design=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La cosa da rileggere fra sei mesi: propagare una modifica richiede insieme il filtro allargato a edited_at e il watermark del client sul massimo dei due timestamp. Una sola delle due e o la modifica non arriva, o arriva a ogni giro per sempre. Più il perché di due scelte che sembrano sviste: il non-letto resta su created_at, e la foto è un URL esterno perché l'upload su volume non esiste (la deroga LOCKED #5 è scritta ma mai costruita). Co-Authored-By: Claude Opus 5 --- .planning/STATE.md | 11 +++--- STATUS.md | 65 +++++++++++++++++++++++++++++++ design-reference/DESIGN-SYSTEM.md | 17 +++++++- 3 files changed, 87 insertions(+), 6 deletions(-) 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`).