Files
clienthub/.claude/plans/v2.5-modifiche-hub.md
T
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

320 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](src/app/admin/projects/[id]/page.tsx#L75) e
[page.tsx:118-120](src/app/admin/projects/[id]/page.tsx#L118-L120), più l'import a riga 8.
**Non si perde niente**: ho verificato che `buildEntityMap()` in
[conversations-queries.ts:57](src/lib/conversations-queries.ts#L57) 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](src/lib/admin-queries.ts#L567) e dalla query.
- **Colonna "Timer"** dalla lista progetti: `<TimerCell>` in
[ProjectRow.tsx:85](src/components/admin/ProjectRow.tsx#L85) e l'intestazione in
[projects/page.tsx:47](src/app/admin/projects/page.tsx#L47). 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](src/app/admin/projects/[id]/page.tsx#L69). **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](src/app/admin/page.tsx#L18):
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](src/db/schema.ts#L245)). 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](src/app/admin/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](src/components/admin/tabs/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](src/lib/taxonomy.ts#L37)). In produzione
oggi ci sono esattamente quelle tre categorie.
Per categoria:
- **Iniziati questo mese** — `count(project_offers)` con `start_date` nel mese corrente.
- **Totale anno** — `count` + somma `accepted_total` con `start_date` nell'anno.
- **Incassato** — `payments` 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](src/components/admin/dashboard/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](src/lib/conversations-queries.ts#L8) — oggi non
vengono usati);
- anteprima più lunga del messaggio.
Nuovo layout in [admin/page.tsx](src/app/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](src/app/api/internal/validate-slug/route.ts#L7) —
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](src/lib/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](src/lib/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](src/db/schema.ts#L688-L690)).
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](src/db/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.
- **C1** — `curl` 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.