Tre cose che mancavano all'inbox admin, tutte senza migration.
1. Dall'inbox era impossibile aprire una conversazione: getConversations()
costruiva la lista dai commenti, quindi un cliente compariva solo dopo aver
scritto lui. Ora la lista parte da `clients` e i commenti la arricchiscono.
Ordine: prima chi ha scritto (per recenza), in coda i clienti muti in
alfabetico, cosi' l'inbox resta un inbox.
2. Menzioni «@Nome», rinviate dalla chat a canali. Modello senza schema: il tag
si riconosce confrontando il testo con i nomi noti del cliente (nome intero,
nome di battesimo, brand), insensibile ad accenti e maiuscole. Il body resta
quello che l'admin ha scritto, quindi la menzione sopravvive alla modifica di
un messaggio e resta leggibile ovunque finisca, mail compresa.
Confini di parola su ENTRAMBI i lati: senza quello a sinistra,
«scrivimi a mario@teckell.it» conteneva un tag «@Teckell».
3. Un tag manda una mail. E' l'unico messaggio che esce dal portale: per il
resto il cliente entra quando gli pare, ma il tag e' la dichiarazione che
quel messaggio non puo' aspettare il prossimo accesso. Nessuno scheduler --
parte dalla stessa azione che scrive il messaggio, fuori transazione: se
Resend e' giu' il messaggio in chat resta comunque scritto.
Destinatari: whitelist OTP + email della scheda, deduplicati. Con zero
indirizzi il compositore lo dice PRIMA, invece di lasciar credere che sia
partita una mail che non partira'.
Il pulsante della mail punta a `?chat=<canale>`, validato lato server e passato
come prop: leggerlo nel browser vorrebbe dire renderizzare il pannello chiuso e
riaprirlo dopo l'idratazione.
La casella di risposta diventa controllata (ReplyComposer): il suggerimento del
tag deve leggere il testo mentre lo scrivi e reinserirlo al caret giusto.
Invio manda, Shift+Invio va a capo -- come nel pannello del cliente.
Verificato: `npm run build` e `eslint` puliti, parser delle menzioni provato su
9 casi. NON verificato a schermo ne' contro il DB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Due mancanze emerse provando la chat a canali in produzione.
**Modifica dei messaggi** (migration 0022, additiva, già applicata a
prod). 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, annotata in STATUS.md.
Il punto delicato non è la scrittura ma la propagazione: il poll chiede
`created_at > since` e una modifica non cambia `created_at`, quindi
l'altra parte vedrebbe il testo vecchio fino a un reload. Il filtro ora
guarda anche `edited_at`, e il watermark del client è il massimo fra i
due su tutti i messaggi — senza, il server rispedirebbe lo stesso
messaggio a ogni giro per sempre. Il merge per id già esistente fa il
resto, quindi niente duplicati.
Il non-letto resta ancorato a `created_at` di proposito: correggere un
refuso non deve riaccendere il pallino di un canale già letto.
Si modifica solo ciò di cui si è autori — il controllo è su `author`,
non solo sulla proprietà dell'entità, e lo rifà il server.
**Firma in chat.** Il nome era la stringa "iamcavalli" cablata nel
pannello: il cliente leggeva il marchio dove si aspetta una persona. Ora
arriva da `settings` (nessuna migration) con foto via URL esterno, che
rispetta il vincolo LOCKED #5 — l'upload su volume non esiste, la
deroga per l'audit è scritta in CLAUDE.md ma non è mai stata costruita.
Avatar rotto o assente ricade sul monogramma.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Il portale aveva una sola conversazione con un selettore di fase in un
dropdown: il cliente non vedeva dove c'era del non letto, e una risposta
admin poteva atterrare su un'entità diversa da quella della domanda.
Ora i messaggi si organizzano in canali — "Generale" più uno per fase —
derivati in un solo posto (src/lib/chat-channels.ts) così che le due
sponde concordino sulla stessa chiave. Task e deliverable non sono più
scrivibili ma lo storico non resta orfano: rientra nel canale della fase
proprietaria conservando il nome dell'entità come badge.
- migration 0021 (additiva, già applicata in prod): client_channel_reads
per il letto/non-letto per canale lato cliente, più il primo indice mai
esistito su comments (entity_id, created_at)
- GET/POST /api/client/chat: polling dei messaggi e ricevuta di lettura
- ChatPanel: tab per canale, pallino di non letto, modalità full-screen
- inbox admin: tab per canale con targeting dell'entità corretta in
risposta e snapshot di adminLastReadAt sul thread
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nuova pagina admin /admin/conversazioni: vista WhatsApp-style (lista
conversazioni a sinistra, thread + risposta a destra) che aggrega i
messaggi di tutti i clienti dalla tabella comments, cross-cliente.
- Migration additiva 0014: clients.admin_last_read_at per tracciare
letto/non-letto (pallini + badge in sidebar). Applicata a prod.
- conversations-queries.ts: aggregazione entity_id → cliente
(general/phase/task/deliverable), getConversations /
getConversationThread / getUnreadConversationsCount.
- Risposte admin salvate come commento "general" (visibili anche nella
chat cliente e nel CommentsTab della scheda).
- Voce di menu + badge non-letti (AdminSidebar/AdminShell/layout).
- Token semantici per dual light/dark; refresh manuale (no polling).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>