"Quanto e' costata questa fase" non era una domanda che si potesse fare:
time_entries aveva la sola project_id.
Migration 0018 (gia' applicata a prod): phase_id e task_id su time_entries,
piu' due indici, piu' projects.due_date che serve al blocco successivo. Solo
ADD COLUMN e CREATE INDEX.
Due scelte che vale la pena spiegare:
- ON DELETE SET NULL, non CASCADE. Cancellare un task NON deve cancellare le
ore lavorate su di esso: sono storico fatturabile. L'entry ricade a livello
progetto e il totale del progetto non cambia mai. Con CASCADE, ripulire una
fase avrebbe silenziosamente abbassato il fatturato tracciato. Verificato
sul DB di produzione dentro una transazione con ROLLBACK: cancellato il
task, l'entry sopravvive con task_id NULL, phase_id intatto e i secondi
invariati.
- Il timer su un task scrive ENTRAMBE le colonne. Cosi' il totale di una fase
e' un group-by diretto su phase_id, senza risalire dai task, e comprende
anche il tempo imputato alla fase ma a nessun task in particolare.
Resta un solo timer attivo in tutto l'hub. Da qui una conseguenza in UI: se
sta girando su un task, il timer del tab "Timer" NON si mostra acceso —
mostrarlo acceso farebbe credere che siano due cronometri diversi. Il tab lo
dice a parole e avvisa che avviarlo fermerebbe l'altro.
Le 8 entry esistenti restano valide con entrambe le colonne a NULL, cioe'
"tempo di progetto": e' esattamente cio' che sono.
PhasesTab passa ai token semantici mentre lo si tocca. Non e' zelo: ci si
infila dentro una TimerCell che i token li usa gia', e in dark mode un badge a
token dentro una card bg-white si vede. Un pezzo di DEBT-01 in meno.
Cade startTimerForClient, senza chiamanti da quando la lista progetti non ha
piu' il timer.
Build pulito. L'avvio/arresto dal browser non e' ancora stato provato: si
verifica in produzione, che e' l'unico posto dove esiste il DB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>