Files
clienthub/.claude/plans/v2.5-modifiche-hub.md
simone 817a8cd5d1 chore(claude): architettura base .claude — skill preventivo e audit, hook di guardia, piani nel repo
La cartella aveva dentro solo rules/ e i settings: nessun posto dove mettere una
skill, un hook o un piano. Ora ha lo scheletro completo e un .claude/CLAUDE.md che
spiega cosa va dove — non duplica CLAUDE.md di progetto, che resta quello che comanda.

Due skill locali (le altre restano globali in ~/.claude/skills/):
- /preventivo — la catena agent.ts → schema.ts → assemble.ts → ProposalDeck e i tre
  modi di romperla, di cui uno solo fa rumore. Nessun prompt di generazione qui
  dentro: quello vive in agent.ts ed e' l'unico. Porta check-profilo.sh.
- /audit — guida scripts/audit-fonti.ts, nuovo, che mette in moto le cinque fonti di
  src/lib/audit/sources/, in prod dal 2026-08-19 ma mai chiamate da nessuno. Provate
  su giojello.com: 5 su 5, 42,7 s, PageSpeed mobile 58 / desktop 93.

Due hook, provati a mano (6 casi il primo, 5 il secondo):
- guardia-migration.sh BLOCCA l'SQL distruttivo sulle entita' protette — il vincolo
  Data Safety (LOCKED) fatto rispettare dalla macchina invece che dalla memoria.
- guardia-token.sh AVVISA sulle classi Tailwind grezze. Non blocca: con ~450
  occorrenze di debito, bloccare lo renderebbe un ostacolo da disattivare.

I tre piani di v2.5 entrano nel repo: stavano solo in ~/.claude/plans/ e STATE.md
avvertiva che senza quelli la milestone non era ricostruibile. Passati al setaccio
per credenziali prima di committarli.

Corretta in rules/memory-discipline.md la chiave della memoria persistente: e'
…-Vault-IAMCAVALLI-hub, non quella del workspace. Sedici file stavano nella prima,
la regola indicava la seconda.

Impeccable resta abilitato solo a livello globale: fuori da settings.json locale.

Nessun tocco al prodotto. Build e lint verdi, lint identico al baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:09:14 +02:00

15 KiB
Raw Permalink Blame History

Modifiche all'hub — dashboard, progetti, pipeline

Contesto

Il 2026-08-18 hai elencato una serie di modifiche all'hub divise per sezione (dashboard, progetti, pipeline). La sessione si è fermata sul limite settimanale subito dopo aver lanciato l'esplorazione, e quel lavoro non è mai stato ripreso. Questo piano lo raccoglie.

Il punto di partenza è che quasi tutto quello che chiedi è calcolabile con i dati che l'hub già ha: le categorie di offerta esistono, le fasi e i task hanno uno stato, i pagamenti hanno una data di incasso, le conversazioni sono già aggregate. Serve poco schema nuovo — una sola migration additiva — e molta query + UI.

L'eccezione è l'ingresso da Whop per gli audit: dipende dal motore di analisi della v2.5, che è fermo alle sole fonti. Hai scelto di fare prima le modifiche hub e poi il motore, quindi quel blocco resta documentato ma non costruito.

Decisioni prese

Domanda Scelta
Sequenza Hub prima, motore audit dopo
Fonte "Audit" negli analytics È la tua Entry Offer — si legge da offer_macros.category, come Signature e Retainer
Data di consegna attesa Derivata da offerta + durata, con override manuale opzionale
Ingresso lead TidyCal + form sito — vedi il blocco C, dove c'è un vincolo che cambia le carte

Blocco A — Progetti

Il blocco a rischio più basso e a resa più immediata. Da fare per primo.

A1 · Togliere il tab Commenti e il timer dalla lista

Due rimozioni chieste esplicitamente.

  • Tab "Commenti" in page.tsx:75 e page.tsx:118-120, più l'import a riga 8. Non si perde niente: ho verificato che buildEntityMap() in conversations-queries.ts:57 cammina clienti → progetti → fasi → task → deliverable e raccoglie tutti i commenti con la loro etichetta d'entità. /admin/conversazioni è un sovrainsieme stretto di quel tab. Il campo comments in ProjectFullDetail diventa morto: va tolto anche da admin-queries.ts:567 e dalla query.
  • Colonna "Timer" dalla lista progetti: <TimerCell> in ProjectRow.tsx:85 e l'intestazione in projects/page.tsx:47. Il timer resta dove ha senso, dentro il progetto.

A2 · Mini-dashboard nel singolo progetto

Una striscia sopra i tab, prima di <Tabs> in projects/[id]/page.tsx:69. Nessuna query nuova: getProjectFullDetail restituisce già tutto.

Finance — incassato / contrattualizzato / residuo, da payments + offersAcceptedTotal; ore tracciate e €/h reale da totalTrackedSeconds e targetHourlyRate (già passato alla pagina a riga 24).

Avanzamento — task done su totale, con barra. Stessa aritmetica del progress_pct già usato per le fasi.

Riusa il pattern MetricCard di admin/page.tsx:18: va estratto in src/components/admin/MetricCard.tsx e importato da entrambe. Scrivere la striscia a token semantici (bg-card, text-muted-foreground) — questa route è il cluster peggiore di DEBT-01 (~182 occorrenze di palette raw) e non va peggiorata.

A3 · Timer per task e fase

Oggi time_entries ha solo project_id (schema.ts:245). Servono due colonne nullable.

  • Migration 0018 (dettagli in fondo): phase_id, task_id su time_entries, entrambe ON DELETE SET NULL. Cancellare un task non deve cancellare il tempo tracciato: è storico fatturabile, l'entry ricade a livello progetto. project_id resta obbligatoria, quindi ogni entry è sempre attribuita.
  • Server actions in timer-actions.ts: startTimer prende phaseId/taskId opzionali; stessa cosa per addManualTimeEntry. La regola "un solo timer attivo alla volta" (righe 19-38) resta com'è — vale globalmente, non per task.
  • UI: <TimerCell> su ogni riga task dentro PhasesTab.tsx, con il totale della fase come somma dei suoi task. TimerTab continua a mostrare il totale progetto.

Blocco B — Dashboard

B1 · Analytics per linea di prodotto

Tre righe — Entry (Audit), Signature, Retainer — lette da offer_macros.category, che è già una taxonomy editabile da /admin/impostazioni (taxonomy.ts:37). In produzione oggi ci sono esattamente quelle tre categorie.

Per categoria:

  • Iniziati questo mesecount(project_offers) con start_date nel mese corrente.
  • Totale annocount + somma accepted_total con start_date nell'anno.
  • Incassatopayments in stato saldato con paid_at nell'anno.

L'incasso ha un problema di attribuzione da risolvere esplicitamente: i pagamenti stanno sul progetto, non sull'offerta. La regola:

  1. progetto con una offerta → tutto l'incasso va a quella categoria;
  2. progetto con più offerte → ripartito in proporzione ai rispettivi accepted_total;
  3. progetto senza offerta → riga separata "Senza offerta", mostrata a video.

Il terzo caso non va nascosto. Oggi in prod ci sono 5 progetti e 2 righe in project_offers: la maggior parte dell'incassato finirebbe lì, e vederlo è il modo per accorgersene.

Nuovo modulo src/lib/product-analytics.ts (non gonfiare analytics-queries.ts), nuovo componente src/components/admin/dashboard/ProductBreakdown.tsx.

Nota da verificare al primo giro: l'unica Entry Offer in produzione si chiama "Sblocca Business". Se l'audit è un prodotto diverso, va creata la sua offer_macro con categoria Entry Offer — è configurazione, non codice.

B2 · Timeline delle consegne

Riguarda i progetti che si consegnano — Entry e Signature. I retainer sono continuativi e non hanno una consegna: restano fuori, filtrando su offer_macros.offer_type = 'una_tantum'.

Per ogni progetto non archiviato:

  • Consegna attesa = projects.due_date se valorizzata, altrimenti project_offers.start_date + offer_micros.duration_months. La colonna due_date arriva con la migration 0018 ed è l'override manuale che hai chiesto.
  • % completamento = task done / task totali.
  • % tempo trascorso = (oggi inizio) / (consegna inizio).
  • Semaforo — soglia ±10 punti: in linea dentro la banda, in ritardo se il completamento è sotto, in anticipo se è sopra. Un progetto oltre la data di consegna e non completo è in ritardo a prescindere.

I progetti senza scadenza calcolabile vanno elencati sotto come "senza scadenza" invece di sparire — altrimenti un progetto senza offerta assegnata diventa invisibile proprio nella vista che dovrebbe segnalarlo.

Query in src/lib/delivery-queries.ts, componente src/components/admin/dashboard/DeliveryTimeline.tsx. StatusBadge per il semaforo, Geist Mono per date e percentuali.

B3 · Inbox

Questo esiste già. MessagesWidget.tsx mostra i clienti con messaggi non letti, l'anteprima dell'ultimo messaggio e il link "Rispondi →" verso /admin/conversazioni. È il widget stretto in colonna 1/3, in fondo: probabilmente non l'hai visto perché sta sotto la piega.

Quindi non si costruisce, si promuove:

  • fascia a piena larghezza in cima alla dashboard, sopra i KPI, e solo quando c'è almeno un non letto;
  • 6 righe invece di 4, con data relativa e etichetta dell'entità (lastMessageAt e lastEntityLabel sono già in ConversationSummary, conversations-queries.ts:8 — oggi non vengono usati);
  • anteprima più lunga del messaggio.

Nuovo layout in admin/page.tsx: Inbox → KPI → Timeline consegne → Prodotti → il resto.


Blocco C — Pipeline: ingresso lead

Il vincolo che cambia le carte

TidyCal non ha webhook. È scritto nella loro FAQ: nessun supporto nativo, e la strada suggerita è passare da Zapier / Make / Pabbly. Quello che TidyCal ha è una REST API con OAuth 2.0 e Personal Access Token disponibile su tutti i piani (si crea da tidycal.com/integrations/oauth), con endpoint sulle prenotazioni.

Elementor Pro, invece, ha un'azione Webhook nativa in "Actions After Submit": si incolla un URL e lui fa POST dei campi del form. Se invece andate su Astro, il POST lo scrivete voi. In entrambi i casi è una chiamata in ingresso.

Da qui la forma della soluzione: un endpoint solo, sia per il form che per TidyCal, e per TidyCal un pezzo in più che va a prendersi le prenotazioni.

C1 · Endpoint di ingresso — POST /api/webhooks/lead

Un solo endpoint, indipendente da chi chiama.

  • Guardia: header x-webhook-secret confrontato con una env nuova (LEAD_WEBHOOK_SECRET), stesso pattern di validate-slug/route.ts:7 — ma senza la scorciatoia "se non è configurato passa": questa route è esposta a internet, secret assente deve voler dire 403.
  • Rate limit con rateLimit() da rate-limit.ts, bucket per IP. src/proxy.ts non intercetta /api/* (matcher a riga 120), quindi la difesa è tutta dentro la route.
  • Payload validato con Zod: name obbligatorio, email/phone/company/ notes opzionali, più un source libero. Mappatura campi in un modulo a parte così aggiungere una sorgente non tocca la route.
  • Effetto: crea un lead in stato contacted riusando le funzioni di lead-service.ts. Se l'email esiste già, aggiorna last_contact_date invece di duplicare.

Questo è tutto ciò che serve per Elementor, ed è pronto in anticipo per Astro: la decisione Elementor-vs-Astro non blocca niente, perché il contratto è un POST JSON in entrambi i casi.

C2 · TidyCal — sincronizzazione a polling

Senza webhook, l'unico modo pulito senza terze parti è andare a leggere.

  • src/lib/tidycal.ts — client con Personal Access Token in TIDYCAL_ACCESS_TOKEN, che chiede le prenotazioni create dall'ultimo giro.
  • GET /api/internal/tidycal-sync — protetta da INTERNAL_SECRET, come le due internal esistenti. Per ogni prenotazione nuova crea un lead con nome, email e la data della call in next_action_date, e registra un'activity di tipo meeting. Idempotente sull'id TidyCal, così rilanciarla non duplica.
  • Schedulazione: Coolify ha gli Scheduled Tasks. Un curl ogni 15 minuti verso quella route. Non serve infrastruttura cron nell'app — e infatti nel repo non ce n'è.

Prima di scrivere il client va guardata la documentazione vera dell'API, che è dietro login (tidycal.com/integrations → API Keys) e non è indicizzata: forma esatta dell'endpoint prenotazioni, filtro per data, paginazione. È il primo passo del blocco, non un dettaglio: vale la regola già scritta in memoria — ogni estrazione da API di terzi va vista funzionare, non dedotta dai docs.

Se l'API risultasse inadatta, il ripiego è Zapier/Make che POSTa su C1, che a quel punto esiste già.

C3 · "Alleggerire l'hub"

Questa richiesta non ha ancora un perimetro. Non la pianifico al buio: quando i blocchi A e B sono in produzione ti porto l'elenco di cosa è davvero poco usato (/admin/quotes e /admin/preventivi convivono, service_catalog e offer_services sono legacy da DEBT-02) e decidi tu cosa togliere. Cancellare route è irreversibile e va fatto guardando, non indovinando.


Blocco D — Whop → audit (rinviato, dipende dal motore)

L'ingresso Whop è già predisposto nello schema: audits.origin accetta 'whop' e audits.external_ref tiene il riferimento esterno (schema.ts:688-690).

Ma "partono gli agent e fanno il lavoro" oggi non è possibile: in src/lib/audit/ ci sono solo le cinque fonti; agent, sintetizzatore e pipeline (AUD-06 → AUD-11) non sono scritti. Costruire il webhook adesso significa costruire un innesco che non innesca niente.

Quando il motore c'è, il blocco è: POST /api/webhooks/whop con verifica firma → crea cliente + progetto con la Entry Offer assegnata (così l'audit compare automaticamente in B1 e B2) → crea l'audit con origin='whop' → accoda la run.


Migration 0018 — additiva

src/db/migrations/0018_timer_scope_and_due_date.sql, in parallelo alle stesse modifiche in schema.ts (drizzle-kit generate è rotto, i due file si tengono in pari a mano).

ALTER TABLE time_entries ADD COLUMN phase_id text REFERENCES phases(id) ON DELETE SET NULL;
ALTER TABLE time_entries ADD COLUMN task_id  text REFERENCES tasks(id)  ON DELETE SET NULL;
CREATE INDEX time_entries_phase_idx ON time_entries(phase_id);
CREATE INDEX time_entries_task_idx  ON time_entries(task_id);
ALTER TABLE projects ADD COLUMN due_date timestamptz;

Solo ADD COLUMN e CREATE INDEX: nessun DROP, nessun TRUNCATE, nessuna colonna rimossa — conforme a Data Safety (LOCKED). Le righe esistenti di time_entries restano valide con le due colonne a NULL, cioè "tempo di progetto".

Ordine di applicazione, come da CLAUDE.md: prima a produzione via ssh root@178.104.27.55 + docker exec, poi il push del codice che le usa.


Ordine di esecuzione

  1. A1 — le due rimozioni (nessuna dipendenza, effetto immediato)
  2. Migration 0018 in produzione
  3. A3 timer su task/fase · A2 mini-dashboard progetto
  4. B3 inbox (piccola) → B1 analytics prodotto → B2 timeline consegne
  5. C1 endpoint lead → C2 TidyCal, dopo aver letto l'API vera
  6. C3 e D dopo, con le informazioni che oggi non abbiamo

Commit per unità logica, come negli ultimi quattro.

Verifica

Non c'è suite di test in questo progetto: npm run build è la verifica di riferimento (fa il typecheck), più npm run lint.

Controlli manuali, in ordine:

  • A1 — il progetto non mostra più il tab Commenti; gli stessi commenti si vedono ancora in /admin/conversazioni con la loro etichetta d'entità; la lista progetti non ha più la colonna Timer.
  • A3 — timer avviato su un task: time_entries ha task_id valorizzato; il totale della fase è la somma dei suoi task; il totale progetto non cambia. Poi cancellare quel task e verificare che l'entry sopravviva con task_id a NULL.
  • A2 / B1 / B2 — confrontare ogni numero a video con la stessa query lanciata a mano su prod via psql. Su un dataset di 5 progetti si controllano tutti a occhio; è l'unico momento in cui questo è ancora possibile.
  • B2 — mettere una due_date manuale su un progetto e verificare che vinca sulla data derivata dall'offerta.
  • C1curl con secret giusto (lead creato), con secret sbagliato (403), senza secret (403), e ripetuto per verificare che non duplichi.
  • Dual light/dark su ogni schermata nuova, e nessuna classe palette raw.

Il portale cliente non va toccato da nessuna di queste modifiche: client-view.ts non si tocca, quindi il vincolo LOCKED #2 non è in gioco.