From dc934fb04a3ad8886c956b56ba266b8e72067d3e Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Fri, 19 Jun 2026 18:52:16 +0200 Subject: [PATCH] =?UTF-8?q?docs(20):=20create=20phase=20plan=20=E2=80=94?= =?UTF-8?q?=20Knowledge=20Base=20Cliente=20transcript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 piani: 20-01 migration BLOCKING, 20-02 schema+actions, 20-03 UI. KB-01 e KB-02 coperti. Wave 1→2→3 sequenziali per vincolo migration-prima-del-codice. Co-Authored-By: Claude Sonnet 4.6 --- .planning/ROADMAP.md | 9 +- .../20-knowledge-base-cliente/20-01-PLAN.md | 232 +++++++ .../20-knowledge-base-cliente/20-02-PLAN.md | 401 +++++++++++ .../20-knowledge-base-cliente/20-03-PLAN.md | 626 ++++++++++++++++++ 4 files changed, 1266 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/20-knowledge-base-cliente/20-01-PLAN.md create mode 100644 .planning/phases/20-knowledge-base-cliente/20-02-PLAN.md create mode 100644 .planning/phases/20-knowledge-base-cliente/20-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index be96985..82f1187 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -190,7 +190,12 @@ Plans: 1. Esiste uno schema additivo `client_transcripts` (lead_id/client_id, testo, data, titolo/tipo, created_at) 2. L'admin incolla un transcript con data dal dettaglio lead/cliente 3. I transcript sono elencati in ordine cronologico nel profilo -**Status**: Pending planning — `/gsd-plan-phase 20` +**Plans:** 3 plans +Plans: +- [ ] 20-01-PLAN.md — Migration SQL 0009_client_transcripts (BLOCKING checkpoint prod) +- [ ] 20-02-PLAN.md — Schema Drizzle + getTranscripts + addTranscript/deleteTranscript actions +- [ ] 20-03-PLAN.md — TranscriptModal + sezione LeadDetail + wiring page.tsx +**Status**: Planned (2026-06-19) — ready for `/gsd-execute-phase 20` ### Phase 21: Agente AI — generazione preventivo **Goal**: L'admin seleziona cliente + offerta; l'agente AI (Claude) legge i transcript + i dati dell'offerta e genera una bozza di preventivo personalizzata, che l'admin può rivedere ed editare. @@ -228,6 +233,6 @@ Plans: | 17. Proposal AI — Builder, Pagina Pubblica & Email | — | ❌ Ri-scopata (v2.2) | — | | 18 (R1). Cleanup & Consolidamento | 3/3 | ✅ Done | 2026-06-19 | | 19 (R2). Pipeline CRM Kanban | 1/1 | ✅ Done | 2026-06-19 | -| 20 (R3). Knowledge Base Cliente (transcript) | — | Pending planning | — | +| 20 (R3). Knowledge Base Cliente (transcript) | 3 plans | Planned (2026-06-19) | — | | 21 (R4). Agente AI — generazione preventivo | — | Pending planning | — | | 22 (R5). Pagina pubblica preventivo + email | — | Pending planning | — | diff --git a/.planning/phases/20-knowledge-base-cliente/20-01-PLAN.md b/.planning/phases/20-knowledge-base-cliente/20-01-PLAN.md new file mode 100644 index 0000000..0ca0211 --- /dev/null +++ b/.planning/phases/20-knowledge-base-cliente/20-01-PLAN.md @@ -0,0 +1,232 @@ +--- +phase: 20-knowledge-base-cliente +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/db/migrations/0009_client_transcripts.sql +autonomous: false +requirements: + - KB-01 +must_haves: + truths: + - "Il file 0009_client_transcripts.sql esiste con DDL completo e idempotente" + - "La tabella client_transcripts è presente nel database di produzione" + - "Il codice schema-dipendente dei piani successivi non viene pushato prima che la migration sia applicata" + artifacts: + - path: "src/db/migrations/0009_client_transcripts.sql" + provides: "DDL CREATE TABLE IF NOT EXISTS client_transcripts con tutti i campi D-01/D-02" + contains: "CREATE TABLE IF NOT EXISTS client_transcripts" + key_links: + - from: "src/db/migrations/0009_client_transcripts.sql" + to: "database produzione" + via: "SSH tunnel + psql/docker exec" + pattern: "CREATE TABLE IF NOT EXISTS client_transcripts" +--- + + +Scrivere il file SQL della migration manuale per la tabella `client_transcripts` e applicarlo al database di produzione prima di pushare qualsiasi codice schema-dipendente. + +Purpose: Il progetto ha `drizzle-kit generate` rotto (meta-snapshot fuori sync). Ogni schema change richiede SQL a mano applicato a prod via SSH PRIMA del codice dipendente — invariante bloccante LOCKED in CLAUDE.md. + +Output: `src/db/migrations/0009_client_transcripts.sql` applicato a prod. Il checkpoint umano sblocca i piani 20-02 e 20-03. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/20-knowledge-base-cliente/20-CONTEXT.md + + + + + + + +Da 0005_phase_10_crm_leads_activities_reminders.sql: +```sql +CREATE TABLE IF NOT EXISTS "activities" ( + "id" text PRIMARY KEY NOT NULL, + "lead_id" text NOT NULL, + ... + FOREIGN KEY ("lead_id") REFERENCES "leads"("id") ON DELETE cascade +); +CREATE INDEX IF NOT EXISTS "activities_lead_id" ON "activities"("lead_id"); +``` + +Da 0008_offer_tier_schema.sql (header): +```sql +-- Phase 12: Offer Editor — additive schema ... +-- All statements additive/idempotent. No drops/truncates (Data Safety LOCKED). +``` + + + + + + + Task 1: Scrivere src/db/migrations/0009_client_transcripts.sql + src/db/migrations/0009_client_transcripts.sql + + - src/db/migrations/0008_offer_tier_schema.sql (pattern header + CREATE TABLE IF NOT EXISTS) + - src/db/migrations/0005_phase_10_crm_leads_activities_reminders.sql (pattern FK + index per tabelle CRM) + - .planning/phases/20-knowledge-base-cliente/20-CONTEXT.md (D-01 e D-02 — campi esatti) + + +Creare il file `src/db/migrations/0009_client_transcripts.sql` con il seguente contenuto esatto: + +```sql +-- Phase 20: Knowledge Base Cliente — tabella transcript datati per lead/cliente +-- Additive only. No drops/truncates (Data Safety LOCKED). +-- Applicare a prod via SSH tunnel PRIMA di pushare il codice dipendente (D-03). + +CREATE TABLE IF NOT EXISTS client_transcripts ( + id text PRIMARY KEY NOT NULL, + lead_id text REFERENCES leads(id) ON DELETE CASCADE, + client_id text REFERENCES clients(id) ON DELETE CASCADE, + title text, + content text NOT NULL, + call_date date NOT NULL, + created_at timestamp with time zone NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS client_transcripts_lead_id_idx + ON client_transcripts (lead_id); + +CREATE INDEX IF NOT EXISTS client_transcripts_client_id_idx + ON client_transcripts (client_id); + +CREATE INDEX IF NOT EXISTS client_transcripts_call_date_idx + ON client_transcripts (call_date DESC); +``` + +Note sui campi (da D-01/D-02): +- `id`: text PK NOT NULL — nanoid inserito dall'app, nessun DEFAULT SQL (pattern progetto) +- `lead_id`: nullable FK → leads(id) ON DELETE CASCADE (D-01) +- `client_id`: nullable FK → clients(id) ON DELETE CASCADE (D-01) +- `title`: text nullable (D-02) — titolo libero opzionale +- `content`: text NOT NULL (D-02) — testo grezzo illimitato +- `call_date`: date NOT NULL (D-02) — giorno della call, non timestamp +- `created_at`: timestamp with time zone NOT NULL DEFAULT now() (D-02) + +Gli indici su lead_id, client_id e call_date ottimizzano le query per lead (Phase 20) e future query per client_id (Phase 21+). + + + grep -c "CREATE TABLE IF NOT EXISTS client_transcripts" /Users/simonecavalli/Vault/IAMCAVALLI/src/db/migrations/0009_client_transcripts.sql + + + - File `src/db/migrations/0009_client_transcripts.sql` esiste + - Contiene `CREATE TABLE IF NOT EXISTS client_transcripts` + - Contiene `lead_id text REFERENCES leads(id) ON DELETE CASCADE` + - Contiene `client_id text REFERENCES clients(id) ON DELETE CASCADE` + - Contiene `content text NOT NULL` + - Contiene `call_date date NOT NULL` + - Contiene `created_at timestamp with time zone NOT NULL DEFAULT now()` + - Contiene 3 `CREATE INDEX IF NOT EXISTS` (lead_id_idx, client_id_idx, call_date_idx) + - Nessun DROP o TRUNCATE nel file + + File SQL completo e idempotente scritto, pronto per applicazione a prod. + + + + Task 2: [BLOCKING] Applicare migration 0009 al database di produzione via SSH + + - src/db/migrations/0009_client_transcripts.sql (verificare il contenuto prima di applicare) + + +La migration va applicata a prod via SSH tunnel PRIMA di eseguire i piani 20-02 e 20-03. Il database di produzione è accessibile solo tramite tunnel SSH. + +**Passi:** + +1. Aprire un terminale locale e avviare il tunnel SSH: + ```bash + ssh -L 54321:localhost:54321 root@178.104.27.55 + ``` + Lasciare questo terminale aperto per tutta la durata. + +2. In un altro terminale, applicare la migration con psql (il DATABASE_URL del progetto punta a 127.0.0.1:54321): + ```bash + cd /Users/simonecavalli/Vault/IAMCAVALLI + psql "$(grep DATABASE_URL .env.local | cut -d= -f2-)" -f src/db/migrations/0009_client_transcripts.sql + ``` + + Oppure, se il DATABASE_URL non funziona direttamente, applicare via docker exec sul server: + ```bash + # Sul server (nella sessione SSH aperta al punto 1): + docker exec -i psql -U -d < /path/to/0009_client_transcripts.sql + ``` + (Il nome del container e le credenziali sono in `.env.local` o nei secret Coolify.) + +3. Verificare che la tabella esista: + ```bash + psql "$(grep DATABASE_URL .env.local | cut -d= -f2-)" -c "\d client_transcripts" + ``` + Deve mostrare le colonne: id, lead_id, client_id, title, content, call_date, created_at. + +4. Chiudere il tunnel SSH solo dopo aver verificato il punto 3. + + Il file SQL 0009_client_transcripts.sql è stato scritto da Claude nel Task 1. Questo task richiede solo che tu applichi la migration a prod — nessun codice da scrivere. + + Eseguire in locale (con tunnel attivo): + ```bash + psql "$(grep DATABASE_URL .env.local | cut -d= -f2-)" -c "SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = 'client_transcripts' ORDER BY ordinal_position;" + ``` + Risultato atteso: 7 righe con le colonne id, lead_id, client_id, title, content, call_date, created_at. + + Digita "migration applicata" dopo aver verificato che \d client_transcripts mostra le 7 colonne attese. + + - La tabella `client_transcripts` esiste nel database di produzione + - `\d client_transcripts` mostra 7 colonne: id (text), lead_id (text, nullable), client_id (text, nullable), title (text, nullable), content (text, NOT NULL), call_date (date, NOT NULL), created_at (timestamptz, NOT NULL) + - 3 indici presenti: client_transcripts_lead_id_idx, client_transcripts_client_id_idx, client_transcripts_call_date_idx + + Database di produzione aggiornato con la tabella client_transcripts. I piani 20-02 e 20-03 sono sbloccati. + + + + + +## Trust Boundaries + +| Boundary | Descrizione | +|----------|-------------| +| developer → database prod | SQL applicato manualmente via SSH tunnel autenticato | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-20-01 | Tampering | 0009_client_transcripts.sql | mitigate | Revisione manuale del contenuto prima dell'applicazione (Task 2 step 1); file in version control | +| T-20-02 | Repudiation | Applicazione migration prod | accept | L'operazione è tracciata nel git commit di questo piano; nessun audit log applicativo necessario per DDL | +| T-20-03 | Denial of Service | DROP/TRUNCATE accidentale | mitigate | Il file usa solo CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS; nessuna istruzione distruttiva presente | + + + +```bash +# Verificare che il file SQL esista e contenga i campi obbligatori +grep -E "CREATE TABLE IF NOT EXISTS client_transcripts|content.*text NOT NULL|call_date.*date NOT NULL|lead_id.*REFERENCES leads|client_id.*REFERENCES clients" \ + /Users/simonecavalli/Vault/IAMCAVALLI/src/db/migrations/0009_client_transcripts.sql + +# Verificare assenza di istruzioni distruttive +grep -i "drop\|truncate" /Users/simonecavalli/Vault/IAMCAVALLI/src/db/migrations/0009_client_transcripts.sql | wc -l +# Atteso: 0 +``` + + + +- `src/db/migrations/0009_client_transcripts.sql` esiste con DDL completo e idempotente (KB-01) +- La tabella `client_transcripts` è presente nel database di produzione con le 7 colonne di D-02 +- Nessuna istruzione distruttiva nel file SQL +- Il checkpoint umano è stato completato e segnalato con "migration applicata" + + + +Dopo il completamento del Task 2, creare `.planning/phases/20-knowledge-base-cliente/20-01-SUMMARY.md` con: +- Migration file creato: `src/db/migrations/0009_client_transcripts.sql` +- Migration applicata a prod: sì/no +- Colonne verificate: lista delle 7 colonne confermate + diff --git a/.planning/phases/20-knowledge-base-cliente/20-02-PLAN.md b/.planning/phases/20-knowledge-base-cliente/20-02-PLAN.md new file mode 100644 index 0000000..b5ba204 --- /dev/null +++ b/.planning/phases/20-knowledge-base-cliente/20-02-PLAN.md @@ -0,0 +1,401 @@ +--- +phase: 20-knowledge-base-cliente +plan: 02 +type: execute +wave: 2 +depends_on: + - 20-01 +files_modified: + - src/db/schema.ts + - src/lib/lead-service.ts + - src/app/admin/leads/actions.ts +autonomous: true +requirements: + - KB-01 + - KB-02 +must_haves: + truths: + - "La tabella clientTranscripts è definita in schema.ts con le relazioni Drizzle corrette" + - "getTranscripts(leadId) restituisce i transcript in ordine call_date DESC" + - "addTranscript e deleteTranscript sono server actions con requireAdmin guard" + artifacts: + - path: "src/db/schema.ts" + provides: "Definizione tabella clientTranscripts + relazioni Drizzle per leads e clients" + contains: "export const clientTranscripts = pgTable" + - path: "src/lib/lead-service.ts" + provides: "Query getTranscripts(leadId) — tutti i campi incluso content completo" + exports: ["getTranscripts"] + - path: "src/app/admin/leads/actions.ts" + provides: "Server actions addTranscript e deleteTranscript con requireAdmin" + exports: ["addTranscript", "deleteTranscript"] + key_links: + - from: "src/app/admin/leads/actions.ts" + to: "src/db/schema.ts" + via: "import clientTranscripts" + pattern: "clientTranscripts" + - from: "src/lib/lead-service.ts" + to: "src/db/schema.ts" + via: "import clientTranscripts" + pattern: "getTranscripts" +--- + + +Aggiungere la definizione Drizzle della tabella `clientTranscripts` in schema.ts, la query `getTranscripts` in lead-service.ts e le server actions `addTranscript` / `deleteTranscript` in actions.ts. + +Purpose: Fornisce il layer dati completo per la UI del piano 20-03. Phase 21 (AI) leggerà i transcript via `getTranscripts(leadId)` — la firma deve restituire tutti i campi incluso `content` completo. + +Output: Schema Drizzle + query + actions pronte, build TypeScript senza errori. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/20-knowledge-base-cliente/20-CONTEXT.md +@.planning/phases/20-knowledge-base-cliente/20-01-SUMMARY.md + + + + +Pattern tabella CRM con FK lead_id (activities, linee 464-481): +```typescript +export const activities = pgTable("activities", { + id: text("id").primaryKey().$defaultFn(() => nanoid()), + lead_id: text("lead_id").notNull().references(() => leads.id, { onDelete: "cascade" }), + type: text("type").notNull(), + notes: text("notes").notNull(), + activity_date: timestamp("activity_date", { withTimezone: true }).notNull(), + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); +``` + +Pattern relazioni (leadsRelations, linea 631-635): +```typescript +export const leadsRelations = relations(leads, ({ many }) => ({ + quotes: many(quotes), + activities: many(activities), + reminders: many(reminders), +})); + +export const activitiesRelations = relations(activities, ({ one }) => ({ + lead: one(leads, { fields: [activities.lead_id], references: [leads.id] }), +})); +``` + +Pattern TypeScript types (fine file): +```typescript +export type Activity = typeof activities.$inferSelect; +export type NewActivity = typeof activities.$inferInsert; +``` + +Pattern query (lead-service.ts linee 51-57): +```typescript +export async function getActivityLog(leadId: string) { + return await db + .select() + .from(activities) + .where(eq(activities.lead_id, leadId)) + .orderBy(desc(activities.activity_date)); +} +``` + +Pattern requireAdmin + server action (actions.ts linee 164-205): +```typescript +async function requireAdmin() { + const session = await getServerSession(authOptions); + if (!session) throw new Error("Non autorizzato"); +} + +export async function updateLeadField(leadId: string, ...) { + await requireAdmin(); + // ... validazione ... + await db.update(leads).set({...}).where(eq(leads.id, leadId)); + revalidatePath("/admin/leads"); + revalidatePath(`/admin/leads/${leadId}`); +} +``` + +Import correnti in actions.ts (linee 1-16): +```typescript +"use server"; +import { z } from "zod"; +import { db } from "@/db"; +import { leads, quotes, tags } from "@/db/schema"; +import { eq, and } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +``` + +Import correnti in lead-service.ts (linee 1-4): +```typescript +import { eq, and, isNull, desc, gte, lte, ilike, or } from "drizzle-orm"; +import { db } from "@/db"; +import { leads, activities, reminders, quotes } from "@/db/schema"; +``` + + + + + + + Task 1: Aggiungere clientTranscripts a src/db/schema.ts + src/db/schema.ts + + - src/db/schema.ts (leggere tutto il file per capire dove inserire la nuova tabella e come aggiornare le relations esistenti) + - .planning/phases/20-knowledge-base-cliente/20-CONTEXT.md (D-01, D-02 — campi esatti) + + +Aggiungere in `src/db/schema.ts` tre blocchi, rispettando la struttura esistente del file: + +**1. Definizione tabella** — aggiungere dopo `// ============ REMINDERS TABLE` (dopo la chiusura della definizione reminders a linea ~499) e prima di `// ============ RELATIONS`: + +```typescript +// ============ CLIENT TRANSCRIPTS TABLE (Knowledge Base — Phase 20) ============ +// Transcript datati delle call, multipli per lead o cliente. +// lead_id e client_id sono entrambi nullable (D-01): un transcript può appartenere +// a un lead pre-conversione (Phase 20 UI) o a un cliente post-conversione (futuro). +// onDelete cascade su entrambe le FK — se il lead/cliente viene eliminato, i transcript vengono eliminati. +export const clientTranscripts = pgTable("client_transcripts", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + lead_id: text("lead_id") + .references(() => leads.id, { onDelete: "cascade" }), + client_id: text("client_id") + .references(() => clients.id, { onDelete: "cascade" }), + title: text("title"), // opzionale — es. "Discovery call 12 giugno" + content: text("content").notNull(), // testo grezzo illimitato (PostgreSQL text) + call_date: text("call_date").notNull(), // DATE come text "YYYY-MM-DD" — coerente col pattern date nel progetto + created_at: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), +}); +``` + +Nota su `call_date`: usare `text` invece di un tipo `date` Drizzle — il progetto gestisce le date come string ISO nei form e nel DB (vedi `activity_date` che è timestamp, ma `call_date` è date pura). Se Drizzle pg-core espone `date` nativo, usarlo; altrimenti usare `text("call_date").notNull()` per coerenza con i pattern di input da `` che restituisce stringhe "YYYY-MM-DD". + +**2. Relazioni** — aggiungere nei blocchi relations esistenti: + +Aggiornare `leadsRelations` (attuale linea ~631): +```typescript +export const leadsRelations = relations(leads, ({ many }) => ({ + quotes: many(quotes), + activities: many(activities), + reminders: many(reminders), + transcripts: many(clientTranscripts), // ← aggiungere questa riga +})); +``` + +Aggiornare `clientsRelations` (attuale linea ~503): +```typescript +export const clientsRelations = relations(clients, ({ many }) => ({ + projects: many(projects), + transcripts: many(clientTranscripts), // ← aggiungere questa riga +})); +``` + +Aggiungere le nuove relations dopo `remindersRelations`: +```typescript +export const clientTranscriptsRelations = relations(clientTranscripts, ({ one }) => ({ + lead: one(leads, { + fields: [clientTranscripts.lead_id], + references: [leads.id], + }), + client: one(clients, { + fields: [clientTranscripts.client_id], + references: [clients.id], + }), +})); +``` + +**3. TypeScript types** — aggiungere in fondo al file dopo `export type Reminder`: +```typescript +export type ClientTranscript = typeof clientTranscripts.$inferSelect; +export type NewClientTranscript = typeof clientTranscripts.$inferInsert; +``` + + + cd /Users/simonecavalli/Vault/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20 + + + - `grep -c "export const clientTranscripts = pgTable" src/db/schema.ts` restituisce 1 + - `grep -c "transcripts: many(clientTranscripts)" src/db/schema.ts` restituisce 2 (leadsRelations + clientsRelations) + - `grep -c "export const clientTranscriptsRelations" src/db/schema.ts` restituisce 1 + - `grep -c "export type ClientTranscript" src/db/schema.ts` restituisce 1 + - `npx tsc --noEmit` passa senza errori TypeScript relativi a schema.ts + + clientTranscripts definita in schema.ts con relazioni Drizzle bidirezionali e TypeScript types esportati. + + + + Task 2: Aggiungere getTranscripts a lead-service.ts e addTranscript/deleteTranscript ad actions.ts + src/lib/lead-service.ts, src/app/admin/leads/actions.ts + + - src/lib/lead-service.ts (leggere per vedere gli import correnti e dove appendere getTranscripts) + - src/app/admin/leads/actions.ts (leggere per vedere gli import correnti e la posizione di requireAdmin) + - src/db/schema.ts (dopo il Task 1 — per verificare il nome esatto dell'export clientTranscripts) + + +**In src/lib/lead-service.ts:** + +Aggiungere `clientTranscripts` agli import esistenti: +```typescript +import { leads, activities, reminders, quotes, clientTranscripts } from "@/db/schema"; +``` + +Appendere in fondo al file (dopo `getQuotesByLeadId`): +```typescript +// Get transcript log for a lead — call_date DESC (D-05) +// Restituisce tutti i campi incluso content completo (Phase 21 li legge in blocco). +export async function getTranscripts(leadId: string) { + return await db + .select() + .from(clientTranscripts) + .where(eq(clientTranscripts.lead_id, leadId)) + .orderBy(desc(clientTranscripts.call_date)); +} +``` + +--- + +**In src/app/admin/leads/actions.ts:** + +Aggiungere `clientTranscripts` agli import dal DB schema: +```typescript +import { leads, quotes, tags, clientTranscripts } from "@/db/schema"; +``` + +Aggiungere `nanoid` agli import (serve per generare l'id del transcript): +```typescript +import { nanoid } from "nanoid"; +``` + +Definire lo schema Zod per il transcript e appendere le due actions dopo le tag actions esistenti (dopo `renameLeadTag`): + +```typescript +// ── Transcript actions (Phase 20 — KB-02) ──────────────────────────────────── + +const addTranscriptSchema = z.object({ + lead_id: z.string().min(1), + title: z.string().optional(), + content: z.string().min(1, "Il testo del transcript è obbligatorio"), + call_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Data non valida (YYYY-MM-DD)"), +}); + +export async function addTranscript(data: z.infer) { + await requireAdmin(); + + const parsed = addTranscriptSchema.safeParse(data); + if (!parsed.success) { + return { success: false, error: parsed.error.issues[0].message }; + } + + try { + const [transcript] = await db + .insert(clientTranscripts) + .values({ + id: nanoid(), + lead_id: parsed.data.lead_id, + title: parsed.data.title || null, + content: parsed.data.content, + call_date: parsed.data.call_date, + }) + .returning(); + + revalidatePath(`/admin/leads/${parsed.data.lead_id}`); + return { success: true, transcript }; + } catch (error) { + console.error("addTranscript error:", error); + return { success: false, error: "Errore nel salvataggio del transcript" }; + } +} + +export async function deleteTranscript(transcriptId: string, leadId: string) { + await requireAdmin(); + + if (!transcriptId) throw new Error("ID transcript richiesto"); + + try { + await db + .delete(clientTranscripts) + .where(eq(clientTranscripts.id, transcriptId)); + + revalidatePath(`/admin/leads/${leadId}`); + return { success: true }; + } catch (error) { + console.error("deleteTranscript error:", error); + return { success: false, error: "Errore nell'eliminazione del transcript" }; + } +} +``` + + + cd /Users/simonecavalli/Vault/IAMCAVALLI && npx tsc --noEmit 2>&1 | grep -E "lead-service|actions" | head -10 + + + - `grep -c "export async function getTranscripts" src/lib/lead-service.ts` restituisce 1 + - `grep -c "orderBy(desc(clientTranscripts.call_date))" src/lib/lead-service.ts` restituisce 1 + - `grep -c "export async function addTranscript" src/app/admin/leads/actions.ts` restituisce 1 + - `grep -c "export async function deleteTranscript" src/app/admin/leads/actions.ts` restituisce 1 + - `grep -c "await requireAdmin()" src/app/admin/leads/actions.ts` è >= 3 (addTranscript e deleteTranscript devono averlo, più le actions esistenti) + - `grep -c "revalidatePath" src/app/admin/leads/actions.ts` aumenta di 2 rispetto al file originale (una per addTranscript, una per deleteTranscript) + - `npx tsc --noEmit` passa senza errori su lead-service.ts e actions.ts + + getTranscripts esportata da lead-service.ts, addTranscript e deleteTranscript aggiunte ad actions.ts con requireAdmin guard e revalidatePath. + + + + + +## Trust Boundaries + +| Boundary | Descrizione | +|----------|-------------| +| browser → server action | addTranscript e deleteTranscript sono "use server" — input non trusted | +| server action → database | Query Drizzle con parametri typed | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-20-04 | Elevation of Privilege | addTranscript / deleteTranscript | mitigate | `await requireAdmin()` come prima istruzione in entrambe le actions — lancia Error se sessione assente, blocca esecuzione | +| T-20-05 | Tampering | addTranscript — content | accept | Nessuna sanitization HTML richiesta (testo grezzo, non renderizzato come HTML — CONTEXT.md security note); Zod valida presenza e tipo | +| T-20-06 | Tampering | deleteTranscript — transcriptId | mitigate | L'ID viene passato dalla UI admin-only (protetta da requireAdmin); Drizzle usa query parametrizzata — no SQL injection | +| T-20-07 | Information Disclosure | getTranscripts | mitigate | Funzione usata solo in server components admin (`/admin/leads/[id]/page.tsx`); non esposta su API client (D-04 — client_id FK presente ma non UI client in Phase 20) | + + + +```bash +cd /Users/simonecavalli/Vault/IAMCAVALLI + +# Schema: tabella e relazioni presenti +grep -E "clientTranscripts|ClientTranscript" src/db/schema.ts | grep -v "^//" + +# lead-service: query con ordinamento corretto +grep -A 6 "getTranscripts" src/lib/lead-service.ts + +# actions: requireAdmin su entrambe le nuove actions +grep -B 1 "requireAdmin" src/app/admin/leads/actions.ts + +# Build TypeScript +npx tsc --noEmit 2>&1 | tail -5 +``` + + + +- schema.ts contiene `clientTranscripts` table, relazioni bidirezionali (leads + clients), e TypeScript types (KB-01) +- lead-service.ts esporta `getTranscripts(leadId)` che ordina per `call_date DESC` (KB-02, D-05) +- actions.ts ha `addTranscript` e `deleteTranscript` entrambe con `requireAdmin()` guard (KB-02) +- `npx tsc --noEmit` passa senza errori sui file modificati + + + +Dopo il completamento, creare `.planning/phases/20-knowledge-base-cliente/20-02-SUMMARY.md` con: +- Tabella Drizzle aggiunta: clientTranscripts con 7 campi +- Query aggiunta: getTranscripts(leadId) in lead-service.ts +- Actions aggiunte: addTranscript, deleteTranscript in actions.ts +- TypeScript check: passed/failed + diff --git a/.planning/phases/20-knowledge-base-cliente/20-03-PLAN.md b/.planning/phases/20-knowledge-base-cliente/20-03-PLAN.md new file mode 100644 index 0000000..d2404f2 --- /dev/null +++ b/.planning/phases/20-knowledge-base-cliente/20-03-PLAN.md @@ -0,0 +1,626 @@ +--- +phase: 20-knowledge-base-cliente +plan: 03 +type: execute +wave: 3 +depends_on: + - 20-01 + - 20-02 +files_modified: + - src/components/admin/leads/TranscriptModal.tsx + - src/components/admin/leads/LeadDetail.tsx + - src/app/admin/leads/[id]/page.tsx +autonomous: true +requirements: + - KB-02 +must_haves: + truths: + - "L'admin può aggiungere un transcript dalla pagina dettaglio lead via modal" + - "I transcript sono elencati in call_date DESC con titolo, data formattata e anteprima del testo" + - "Il testo completo di ogni transcript è espandibile/collassabile" + - "L'admin può eliminare un transcript con un bottone 'Elimina'" + - "La sezione Transcript appare dopo lo Storico Attività nel LeadDetail" + artifacts: + - path: "src/components/admin/leads/TranscriptModal.tsx" + provides: "Modal form per aggiungere transcript (call_date, title opzionale, content textarea)" + exports: ["TranscriptModal"] + - path: "src/components/admin/leads/LeadDetail.tsx" + provides: "Sezione Transcript con lista collassabile e azione Elimina" + contains: "Transcript" + - path: "src/app/admin/leads/[id]/page.tsx" + provides: "getTranscripts(id) nel Promise.all + prop transcripts passato a LeadDetail" + contains: "getTranscripts" + key_links: + - from: "src/components/admin/leads/TranscriptModal.tsx" + to: "src/app/admin/leads/actions.ts" + via: "import addTranscript" + pattern: "addTranscript" + - from: "src/components/admin/leads/LeadDetail.tsx" + to: "src/app/admin/leads/actions.ts" + via: "import deleteTranscript" + pattern: "deleteTranscript" + - from: "src/app/admin/leads/[id]/page.tsx" + to: "src/lib/lead-service.ts" + via: "import getTranscripts" + pattern: "getTranscripts" +--- + + +Creare il componente `TranscriptModal` (form per aggiungere transcript) e integrare la sezione Transcript in `LeadDetail`, con wiring nella page server. + +Purpose: Completa il loop KB-02 — l'admin può incollare transcript datati e vederli elencati in ordine cronologico nel profilo lead. I dati sono pronti per essere letti dall'agente AI in Phase 21. + +Output: UI completa funzionante — modal di aggiunta + lista con expand/collapse + elimina. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/20-knowledge-base-cliente/20-CONTEXT.md +@.planning/phases/20-knowledge-base-cliente/20-02-SUMMARY.md + + + + +Da LogActivityModal.tsx — pattern modal con react-hook-form + zod + shadcn Dialog: +```typescript +"use client"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +// ... action import ... +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; + +export function LogActivityModal({ leadId }: { leadId: string }) { + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { lead_id: leadId, activity_date: new Date().toISOString().split("T")[0], ... }, + }); + async function onSubmit(data) { + setLoading(true); + try { + const result = await logActivity(data); + if (result.success) { setOpen(false); form.reset({...}); } + } finally { setLoading(false); } + } + return ( + + + + ... + +