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>
15 KiB
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 campocommentsinProjectFullDetaildiventa 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_idsutime_entries, entrambeON DELETE SET NULL. Cancellare un task non deve cancellare il tempo tracciato: è storico fatturabile, l'entry ricade a livello progetto.project_idresta obbligatoria, quindi ogni entry è sempre attribuita. - Server actions in timer-actions.ts:
startTimerprendephaseId/taskIdopzionali; stessa cosa peraddManualTimeEntry. 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.TimerTabcontinua 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 mese —
count(project_offers)constart_datenel mese corrente. - Totale anno —
count+ sommaaccepted_totalconstart_datenell'anno. - Incassato —
paymentsin statosaldatoconpaid_atnell'anno.
L'incasso ha un problema di attribuzione da risolvere esplicitamente: i pagamenti stanno sul progetto, non sull'offerta. La regola:
- progetto con una offerta → tutto l'incasso va a quella categoria;
- progetto con più offerte → ripartito in proporzione ai rispettivi
accepted_total; - 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_macrocon categoriaEntry 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_datese valorizzata, altrimentiproject_offers.start_date + offer_micros.duration_months. La colonnadue_datearriva 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 lineadentro la banda,in ritardose il completamento è sotto,in anticipose è sopra. Un progetto oltre la data di consegna e non completo èin ritardoa 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à
(
lastMessageAtelastEntityLabelsono già inConversationSummary, 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-secretconfrontato 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.tsnon intercetta/api/*(matcher a riga 120), quindi la difesa è tutta dentro la route. - Payload validato con Zod:
nameobbligatorio,email/phone/company/notesopzionali, più unsourcelibero. Mappatura campi in un modulo a parte così aggiungere una sorgente non tocca la route. - Effetto: crea un
leadin statocontactedriusando le funzioni di lead-service.ts. Se l'email esiste già, aggiornalast_contact_dateinvece 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 inTIDYCAL_ACCESS_TOKEN, che chiede le prenotazioni create dall'ultimo giro.GET /api/internal/tidycal-sync— protetta daINTERNAL_SECRET, come le due internal esistenti. Per ogni prenotazione nuova crea un lead con nome, email e la data della call innext_action_date, e registra un'activitydi tipomeeting. Idempotente sull'id TidyCal, così rilanciarla non duplica.- Schedulazione: Coolify ha gli Scheduled Tasks. Un
curlogni 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
- A1 — le due rimozioni (nessuna dipendenza, effetto immediato)
- Migration 0018 in produzione
- A3 timer su task/fase · A2 mini-dashboard progetto
- B3 inbox (piccola) → B1 analytics prodotto → B2 timeline consegne
- C1 endpoint lead → C2 TidyCal, dopo aver letto l'API vera
- 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/conversazionicon la loro etichetta d'entità; la lista progetti non ha più la colonna Timer. - A3 — timer avviato su un task:
time_entrieshatask_idvalorizzato; il totale della fase è la somma dei suoi task; il totale progetto non cambia. Poi cancellare quel task e verificare che l'entry sopravviva contask_ida 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_datemanuale su un progetto e verificare che vinca sulla data derivata dall'offerta. - C1 —
curlcon 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.