feat(chat): modifica dei messaggi e firma di chi risponde

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>
This commit is contained in:
2026-08-21 22:42:48 +02:00
parent 0d7186ba03
commit 94f4a54248
13 changed files with 735 additions and 82 deletions
+9
View File
@@ -13,6 +13,7 @@ import { OtpGate } from "@/components/client/OtpGate";
import { PreviewBanner } from "@/components/client/PreviewBanner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { normalizeTaskStatus } from "@/lib/task-status";
import { getAdminIdentity } from "@/lib/settings";
export const revalidate = 0;
@@ -136,6 +137,10 @@ export default async function ClientPage({
);
}
// Come si firma chi risponde in chat. Una lettura sola per pagina, condivisa
// da tutti i progetti: è la stessa persona in ogni tab.
const admin = await getAdminIdentity();
if (projects.length === 1) {
// D-09: single project → direct view without selector
const view = await getProjectView(projects[0].id);
@@ -150,6 +155,8 @@ export default async function ClientPage({
projectId: view.project.id,
messages: view.comments,
reads: view.channel_reads,
adminName: admin.name,
adminAvatarUrl: admin.avatarUrl,
}}
preview={preview}
/>
@@ -200,6 +207,8 @@ export default async function ClientPage({
projectId: view.project.id,
messages: view.comments,
reads: view.channel_reads,
adminName: admin.name,
adminAvatarUrl: admin.avatarUrl,
}}
embedded
preview={preview}