Dhonko — specifica di prodotto completa

Fai match
con la vita.

"Sogna, crea, pianifica, parti, VIVI. Il mondo è lì fuori che ti aspetta!" — così Dhonko si presenta a chi apre l'app per la prima volta.

Cos'è
App di travel-matching + esperienze locali
Community
I "Dhonkers"
Due percorsi
Viaggiatore · Local
Boarding ✓
Il problema che risolve

Due bisogni diversi, una piattaforma.

Dhonko nasce dall'incrocio di due esigenze che di solito vivono su app separate: trovare le persone giuste con cui partire, e trovare le esperienze giuste una volta arrivati.

Da una parte c'è chi vuole viaggiare ma non ha compagni disponibili, o vuole conoscere persone compatibili prima di legarsi a un itinerario condiviso — la logica è quella di un matching per affinità, non di un semplice annuncio "cerco compagno di viaggio". Dall'altra ci sono le persone del posto — i "Local" — che conoscono davvero una destinazione e vogliono trasformare quella conoscenza in esperienze prenotabili, invece di lasciarla in una chat o in un consiglio informale.

È una compatibilità! 🎉

È il messaggio che l'app mostra quando due profili risultano affini dopo il questionario di compatibilità — la frase che, più di ogni altra, riassume il meccanismo centrale di Dhonko.

Due percorsi, dal momento dell'iscrizione

"Come vuoi usare Dhonko?"

È la prima vera domanda che l'app fa dopo la registrazione. Non è retorica: determina quale delle due metà del prodotto la persona vedrà da quel momento in poi.

Viaggiatore

Parti, non da solo

"Scopri nuove destinazioni, trova compagni di viaggio e vivi esperienze uniche." Crea un viaggio o si unisce a uno in costruzione, fa match con altri Dhonkers in base a preferenze e personalità, poi pianifica insieme al gruppo — spostamenti, alloggi, attività — con una road map condivisa e votata.

  • Fai match con i Dhonkers più adatti a te
  • Esplora i viaggi in costruzione e unisciti ai gruppi
  • Pianifica il viaggio con l'aiuto della road map
  • Colleziona ricordi sul profilo, in album
Local

Condividi il tuo posto

"Crea esperienze locali, accogli i viaggiatori e condividi la tua cultura." Pubblica attività prenotabili con orari ricorrenti, prezzi per fascia d'età e sconti gruppo, e le gestisce come un vero micro-business dentro l'app — pagamento incluso.

  • Crea esperienze per i Dhonkers intorno a te
  • Tieni traccia delle esperienze in corso
  • Pagamento veloce in app
  • Richiede una verifica: attività, P.IVA/CF, certificati
Dalla prima apertura al primo viaggio

Il percorso di un Dhonker

1

Registrazione e verifica

Email + telefono, ciascuno confermato via OTP a 6 cifre inviato in parallelo su entrambi i canali. Finché non è completa, l'account resta nello stato UNKNOWN.

2

Profile setup wizard

16+ step: foto, biografia guidata ("è importante affinché gli altri viaggiatori decidano di partire con te"), lingue, personalità, hobby, destinazioni preferite, abitudini di viaggio.

3

Questionario di compatibilità

Facoltativo (si può saltare): personalità, abitudini, tipo di viaggiatore — la base di ogni match futuro.

4

Scelta del percorso

Viaggiatore o Local. Un Local passa per una revisione manuale (1-3 giorni lavorativi) prima di poter pubblicare esperienze.

5

Match, viaggio, esperienza

Da qui in poi l'app diventa quotidiana: nuovi match, gruppi di viaggio da pianificare insieme, esperienze locali da prenotare o accogliere.

Chi può fare cosa

Ruoli e stati account

Ogni utente attraversa una progressione di stati man mano che completa passaggi o riceve approvazioni.

UNKNOWN

Appena registrato

Email e telefono non ancora entrambi verificati via OTP. Accesso limitato al solo completamento della verifica.

REGISTRED

Verificato

Email e telefono confermati. Può completare profilo, questionario di compatibilità e scegliere il proprio percorso.

TRAVELER

Viaggiatore

Crea e si unisce a viaggi, fa match, partecipa alla pianificazione condivisa, scrive recensioni.

PENDING_LOCAL → LOCAL

Local, in attesa e poi attivo

Invia bio, dati fiscali (persona fisica o azienda, P.IVA/CF) e certificati. Un admin approva o rifiuta con motivazione; solo dopo l'approvazione può pubblicare esperienze prenotabili.

ADMIN

Staff Dhonko

Ha una vera sezione amministrativa dentro l'app stessa: dashboard, moderazione, pagamenti — dettagli nel catalogo, gruppo 09.

DHONKO

Contenuto ufficiale

Viaggi ed esperienze creati direttamente dalla piattaforma, riconoscibili dal badge "Verificato Dhonko".

Cosa succede da solo, senza che nessuno clicchi nulla

Scheduler automatici e stati del viaggio

Il backend fa girare dei job periodici (NestJS Schedule) che spostano automaticamente lo stato dei viaggi in base alle date, e mandano email + notifiche push di conseguenza.

ogni minuto
In corso (TravelInProgressScheduler)
Cerca viaggi SCHEDULED la cui data di inizio è passata → li sposta a IN_PROGRESS, invia email "buona fortuna" + push a tutti i partecipanti confermati (con controllo per non notificare due volte).
ogni minuto
Completato (TravelCompleteScheduler)
Cerca viaggi IN_PROGRESS la cui data di fine è passata → li sposta a COMPLETED, invia email + push di completamento a creatore e partecipanti confermati.
ogni minuto
Archiviazione (TravelArchiveScheduler)
Cerca viaggi ancora PENDING o APPROVED (mai partiti) la cui data di fine è comunque passata → li sposta direttamente a ARCHIVED, senza notifiche.
ogni ora disattivato
In partenza domani (TravelStartingSoonScheduler)
Scritto per intero ma commentato e non registrato: avviserebbe i partecipanti confermati 23-25 ore prima della partenza. Oggi non viene eseguito.

Il flusso di stato che questi job disegnano nella pratica:

PENDING→ admin approva →APPROVED SCHEDULEDIN_PROGRESS COMPLETED
PENDING / APPROVED→ fine data senza essere mai partito →ARCHIVED
Nessun punto del codice imposta mai lo stato SCHEDULED su un viaggio (verificato: zero occorrenze di scrittura di quel valore). Il risultato pratico è che lo scheduler "In corso" non trova oggi mai nulla da processare: un viaggio approvato resta su APPROVED finché non scade, punto in cui lo scheduler di archiviazione lo porta direttamente ad ARCHIVED — saltando IN_PROGRESS e COMPLETED, e le relative email di viaggio-in-corso/completato non partono mai. Manca solo il passaggio che dovrebbe scrivere SCHEDULED (presumibilmente al termine della pianificazione/match) perché l'intera catena torni a funzionare.

Gli altri stati dell'enum — REJECTED (rifiuto admin), CANCELLED, DELETED — sono impostati da azioni manuali (rispettivamente: rifiuto in fase di revisione, cancellazione da parte del creatore, eliminazione) e non da questi scheduler.

Come Dhonko protegge davvero un utente da un altro

Blocco, segnalazione e ban

Tre meccanismi diversi, con maturità molto diversa tra loro — vale la pena distinguerli invece di trattarli come un unico "sistema di sicurezza".

Blocco utente (tra Dhonkers)
Impedire a un altro utente di interagire con te
Cosa compili
  • Un solo tap: "Blocca" sul profilo dell'utente
  • Nessun motivo richiesto dal backend (il campo esiste nel database ma l'endpoint di blocco non lo accetta né lo scrive mai)
Cosa vedi
  • Dialog con le conseguenze spiegate prima di confermare
  • Lista "Utenti bloccati" in impostazioni, con sblocco
  • Un secondo tap sullo stesso utente lo sblocca (è un toggle, non due azioni separate)
Il blocco è verificato nella visualizzazione del profilo (flag blocked) e nei risultati di ricerca. Sul database due trigger (public/sql/trigger_blocker.sql) lo applicano anche ai follow: al blocco vengono cancellati i follow in entrambe le direzioni, con le notifiche "Nuovo follower!" tra i due, e finché il blocco esiste nessuno dei due può tornare a seguire l'altro. Il file va eseguito a mano sul database, perché Prisma non gestisce i trigger. Nelle chat dirette il blocco è verificato in entrambe le direzioni sia all'apertura (chat-create-chat.use-case.ts) sia a ogni invio (chat-send-message.use-case.ts, che copre REST e socket): rifiuto 403 USER_BLOCKED. Le chat di gruppo non escludono nessuno. Resta un limite: un follow rifiutato dal trigger arriva al client come errore generico, non come 403 dedicato.
Segnalazione di chat e messaggi
Segnalare un messaggio o un'intera conversazione, senza per forza bloccare
Cosa compili (client web)
  • Motivo: Spam o pubblicità, Molestie o insulti, Contenuto inappropriato, Truffa o richiesta di denaro, Altro (con testo obbligatorio)
  • Dettagli facoltativi
Cosa vedi
  • Conferma "Segnalazione inviata", oppure "L'avevi già segnalato" se c'è già una tua segnalazione in attesa sullo stesso messaggio
POST /user/chat/:chatId/report crea un ChatReport: chi segnala deve essere membro della chat, il messaggio deve appartenere a quella chat e non essere suo. Prima nessuna rotta lato utente creava un ChatReport, quindi la coda admin "Moderazione chat" restava sempre vuota. Su Flutter la segnalazione di chat non esiste ancora.
Privacy dei messaggi
Scegliere da chi ricevere messaggi diretti e proposte dai local
Cosa compili
  • "Chi può scrivermi": Tutti (predefinito) o Solo chi seguo
  • "Proposte di esperienze dai local": sì (predefinito) o no
Cosa succede
  • Con "Solo chi seguo", chi non segui non apre una chat diretta con te né ti scrive (403 MESSAGES_FOLLOWERS_ONLY); nelle conversazioni in cui hai già risposto può continuare
  • Con le proposte spente, un local non può mandarti le sue date (403 PROPOSALS_DISABLED)
  • GET /user/users/:id dice in anticipo a chi guarda canMessage, messagingRestriction e acceptsProposals, così i pulsanti si spengono prima di un errore
Segnalazione utente
Avvisare la piattaforma di un comportamento scorretto di un altro utente
Cosa compili (lato app)
  • Motivo: Spam, Molestie, Contenuti inappropriati, Profilo falso, Altro
  • Descrizione libera del problema
  • Opzione combinata "Blocca e segnala"
Cosa vedi
  • Conferma "Segnalazione inviata" con nota sull'anonimato
  • Invito a contattare i servizi di emergenza in caso di pericolo reale
  • Client web, Impostazioni → "Le mie segnalazioni": elenco con utente segnalato, motivo, data e stato (In revisione, Accolta, Respinta, Annullata); al clic il dettaglio con descrizione, spiegazione dello stato e data dell'esito
Le segnalazioni utente sono salvate in UserWarn (userId = chi è segnalato, warnedById = chi segnala) e finiscono nella coda admin "Segnalazioni (warn)", dove vengono approvate, rifiutate o revocate. Ci sono due ingressi: POST /user/profile/report/:userId (client web) e POST /user/warn/:userId (Flutter). Il secondo leggeva campi che Flutter non manda, e aveva invertiti chi segnala e chi è segnalato: ora è corretto. Con GET /user/warn chi segnala vede solo stato e date, mai l'admin che ha deciso. Su Flutter la schermata "Le mie segnalazioni" non esiste ancora.
Ban (solo admin)
Sospendere l'accesso di un utente all'intera piattaforma
Cosa compila l'admin
  • Motivo (obbligatorio, testo libero)
  • Scadenza opzionale in data — se lasciata vuota, il ban è permanente
Cosa vede l'utente bannato
  • Pagina dedicata "Account Sospeso" con motivo, scadenza (o "Permanente"), chi ha applicato il ban, data
  • Il login riesce comunque, ma ogni chiamata /user/* o /admin/* risponde HTTP 450 USER_BANNED con i dettagli del ban; anche il socket della chat rifiuta l'utente bannato alla connessione
  • In tempo reale: quando l'admin applica il ban, il gateway /session emette user_banned e le connessioni chat aperte vengono chiuse; alla revoca emette user_unbanned e chi è sulla pagina di ban rientra senza ricaricare (client web — Flutter se ne accorge alla chiamata successiva)
  • Nel profilo visto dagli altri: vista "utente sospeso", ban permanenti inclusi
  • Un solo ban attivo possibile per utente alla volta (vincolo del database)
  • Pulsante "Contatta supporto" — su Flutter ancora non funzionante; sul client web apre il Centro assistenza
Onestà sullo stato del codice

Cosa è rifinito, cosa è ancora un cantiere

Non tutto nel catalogo sopra è ugualmente maturo. Questo si vede direttamente nel codice: alcuni flussi hanno gestione completa di caricamento/errore/successo e validazioni puntuali; altri mostrano ancora messaggi espliciti tipo "funzionalità in arrivo", o — come lo scheduler sopra — un anello mancante che si può isolare con precisione.

Rifiniti e completi

  • Registrazione con doppio OTP
  • Recupero password (6 step)
  • Profile setup wizard
  • Creazione esperienza Local (14 step, validazioni puntuali)
  • Creazione/modifica viaggio
  • Road map con votazione
  • Verifica identità e relativo storico
  • Pannello Admin (11 aree)
  • Ban/unban con vincoli corretti
  • Pagamenti Stripe con rimborsi proporzionali
  • Header di autenticazione su tutti i media Flutter 25/25 file, verificato
  • Avvio nuova chat: scelta reale tra le persone seguite era "in arrivo", ora funzionante
  • Chiusura/eliminazione di un viaggio rispondeva sempre 500, ora funzionante
  • Blocco utente in chat diretta prima ignorato, ora verificato all'apertura e a ogni invio

Verificati come incompleti

  • Upload foto da "Modifica Profilo" "in arrivo"
  • Contatta supporto da pagina ban "in sviluppo"
  • Log dal profilo Admin "non implementata" (esiste già altrove)
  • Scheduler "in corso" stato SCHEDULED mai scritto
  • "Le mie segnalazioni" su Flutter esiste solo sul client web
  • Richiesta di partecipazione a una data (POST /user/local/:eventId/calendar/details/request) conferma senza pagamento, controllo dei posti commentato
  • Segnalazione di chat e privacy dei messaggi su Flutter esistono solo sul client web
  • Impostazioni Match su Flutter mostrano "aggiornato" senza chiamare nessuna API (sul web salvano davvero)
  • Modifica viaggio su Flutter tipi di viaggio e compagni inviati con nomi che il backend non legge
  • Modifica di pernottamenti e attività su Flutter chiama rotte inesistenti (404)
  • Cambio stato manuale del viaggio POST /:id/change-status lancia "Not implemented"
Non un'interpretazione — i token presi dal tema reale

Identità visiva: logo, colori, font

Preso da lib/core/constants/colors.dart e lib/app/theme/dhonko_theme.dart — gli stessi valori che Flutter compila nell'app, non una ricostruzione.

Logo Dhonko: pin di localizzazione stilizzato con cima di montagna, in gradiente rosso-arancio
Il logo (assets/images/logo.png) è un pin di geolocalizzazione che racchiude una cima a doppio picco — la lettura è "un luogo da raggiungere" più "una montagna da scalare", coerente col posizionamento travel. È l'unico punto del brand dove compare il rosso: da solo, in gradiente verso l'arancio. Sullo splash screen nativo appare su sfondo #132742 (il navy "tertiary").
primary
#00C1B3
bottoni, tab attivi, focus input
secondary
#FF7900
accenti, gradiente secondario
tertiary
#132742
testo primario, splash, snackbar
textSecondary
#6B7280
sottotitoli, label
success
#10B981
conferme, stati approvati
warning
#F59E0B
in attesa, avvisi
error
#EF4444
errori, rifiuti, ban
border
#E5E7EB
divisori, contorni
Nel ColorScheme di Flutter, primary è mappato sul navy tertiary (#132742), non sul teal DhonkoColors.primary — sono due sistemi paralleli: la costante "primary" guida i componenti (bottoni, tab), il ColorScheme di Material guida ciò che Flutter colora automaticamente. Nella pratica il teal è il colore interattivo dominante che si vede, il navy è il colore del testo.
display / lato 32 · 700
Fai match con la vita
headline / lato 22 · 600
Come vuoi usare Dhonko?
title / lato 16 · 600
Registrati e condividi le tue vibrazioni
body / lato 14 · 400
È importante affinché gli altri viaggiatori decidano di partire con te.
label / lato 12 · 600
Completa profilo

Font unico in tutta l'app: Lato (Google Fonts), nessun font secondario — nemmeno per numeri o codici.

🏔️ Montagna
Weekend a Napoli
4 Dhonkers · 3 giorni

Ricostruito 1:1 dai valori in dhonko_theme.dart — stesso raggio, stesso colore, stesso peso font dei componenti reali.

100px — raggio bottoni e FAB: sempre a pillola, mai squadrati
20px — raggio input, bordo teal 2px sempre visibile (non solo al focus)
16px — raggio card e chip
8px — raggio snackbar, l'unico elemento meno arrotondato
elevation 0 — AppBar sempre piatta, mai un'ombra
scaffold trasparente — lo sfondo lo dipinge ogni singola pagina, non il tema
Stile: Material Design (Flutter ThemeData) pesantemente ricustomizzato — pillola/soft, non Material puro

Non è Material 3 "di fabbrica" né uno stile neumorfico/glassmorfico: è un tema Material classico (usa ColorScheme, CardThemeData, ecc.) dove ogni componente è stato riscritto a mano per essere più arrotondato e più "morbido" del default — l'effetto complessivo è quello tipico delle app travel/social consumer (bottoni a pillola, input sempre bordati, card con ombra leggera). Non esiste un tema scuro: in tutto il codice è definito solo DhonkoTheme.lightTheme, nessun darkTheme collegato alla MaterialApp.

Sfondo globale — due varianti, confrontate con i mockup Figma reali
Sfondo chiaro Dhonko: linee di contorno topografiche teal pallido su bianco
Sfondo scuro Dhonko: linee di contorno topografiche su blu navy, per le schermate a tutto schermo con testo bianco
Non è un colore, è un'immagine in stile mappa topografica (linee di contorno), generata direttamente dal sorgente vettoriale del brand (assets/svg/background.svg, linee #00C1B3 al 10% di opacità) — con le stesse identiche curve in due varianti tonali. La versione chiara (assets/images/background.png) è montata una sola volta alla radice dell'app (app.dartDhonkoBackgroundWidget), dentro uno Stack dietro ogni schermata — per questo il tema imposta scaffoldBackgroundColor: Colors.transparent. Piastrellata alla sua scala reale (ImageRepeat.repeat), non stirata: la densità del pattern resta identica su qualsiasi larghezza di finestra, web incluso.

La versione scura (assets/images/background_dark.png, linee sottili su blu navy) è usata da DhonkoScaffoldBlue in ~18 schermate a piena pagina con testo bianco sopra — verifica identità, completamento profilo, travel match, pagine di successo creazione/modifica/eliminazione viaggio e proposta.

Bug reale trovato e corretto: il file background.png era una versione invertita — sfondo nero, linee teal spesse a piena opacità — l'esatto negativo cromatico di come appare nei mockup Figma. E DhonkoScaffoldBlue montava per errore il widget dello sfondo chiaro invece di quello scuro dedicato (DhonkoBackgroundBlueWidget, che esisteva già ma non era collegato a nulla): sulle ~18 schermate sopra, il testo bianco sarebbe stato scritto su uno sfondo poi diventato bianco anch'esso — illeggibile. Rigenerate entrambe le varianti dall'arte vettoriale originale e ricollegato correttamente il widget.
Lockup del logo — icona + wordmark + tagline

Nei mockup, la schermata di benvenuto e i tutorial mostrano sempre l'icona a goccia accompagnata dalla scritta "DHONKO" e dal claim "FEEL YOUR VIBES" in teal. L'unico asset immagine del logo nel progetto (assets/images/logo.png) contiene solo l'icona — il wordmark non esisteva da nessuna parte, quindi quelle due schermate mostravano solo il simbolo, mai il nome del brand per esteso. Aggiunto DhonkoLogoLockupWidget (icona + testo, componibile a qualunque dimensione) e collegato alla schermata di benvenuto e ai due tutorial (Traveler/Local); l'icona da sola resta invariata ovunque altro (barre superiori, ~40 punti) dove il mockup mostra solo il simbolo.

Copertine profilo — 20 foto, 4 temi (5 ciascuno)
Controllando i file reali (CoverUtils, 4 categorie × 5 immagini): la prima foto della categoria "Città" (cover/6.jpeg) è ancora la preview con filigrana "Unsplash+" ripetuta su tutta l'immagine, non la versione licenziata — visibile anche agli utenti finali che la scelgono come copertina profilo. Vale la pena controllare se anche le altre 19 sono a posto prima del rilascio.
Dietro le quinte

Con cosa è costruito

Un backend NestJS/PostgreSQL (Prisma) espone le API consumate dal client mobile Flutter; Firebase gestisce lo storage privato dei file e le notifiche push, Stripe i pagamenti delle esperienze.

Backend NestJS + PostgreSQL
Client Flutter (iOS/Android)
File Firebase Storage (privato)
Push Firebase Cloud Messaging
Pagamenti Stripe
Manuale tecnico — lato client

Come è organizzato il frontend Flutter

Un'app Flutter unica per web e mobile (dhonko/), organizzata in tre livelli — schermate, infrastruttura condivisa, logica di dominio — con gestione dello stato a Bloc/Cubit, routing dichiarativo e 7 lingue.

FE.1

I tre livelli del progetto

  • lib/app/livello di presentazione: ogni schermata, il routing, il tema, i widget condivisi tra più schermate.
  • lib/core/infrastruttura trasversale usata da tutte le feature: configurazione, costanti, Dependency Injection, setup Firebase, notifiche push, modelli dati generici, servizi (rete, storage, socket…), traduzioni, utility.
  • lib/features/livello di dominio: ogni feature (auth, chat, viaggi, profilo…) possiede il proprio datasource remoto, repository e contenitori di stato (Bloc/Cubit).
  • lib/main.dartpunto di ingresso dell'app.
FE.2

Gestione dello stato

Confermato flutter_bloc (Bloc + Cubit) ovunque, con freezed per gli union-state in molte feature (non tutte — alcune usano ancora classi Equatable con copyWith a mano). Convenzione di nomenclatura costante in tutto il progetto: x_bloc.dart/x_cubit.dart + state/x_state.dart + event/x_event.dart (per i Bloc), con gli stati spesso come union initial/loading/loaded (o success)/error. Anche i DTO seguono lo stesso trattamento quasi ovunque: *_dto.dart + generato *_dto.freezed.dart + *_dto.g.dart (JSON serialization).

FE.3

Moduli di dominio — lib/features/

  • authlogin/registrazione/OTP, accesso social (Apple/Google), passi di setup profilo, recupero password.
  • profileprofilo dell'utente loggato: bloc principale, blocco utenti, follower/following, foto, recensioni, storico viaggi, impostazioni.
  • travelsil dominio più grande: creazione/modifica/dettaglio/feed/match/richieste/scheduling/salvataggi/lista propria.
  • chatbloc di messaggistica, datasource/repository remoto, caso d'uso per upload file.
  • localsesperienze "Local": calendario, creazione, dettaglio, lista propria.
  • bookingcubit di prenotazione viaggio/esperienza, elenco paesi, DTO di prenotazione.
  • paymentflussi di pagamento stile Stripe: creazione/conferma/rimborso, bloc pagamento evento, cubit lista pagamenti.
  • notificationcubit notifiche più impostazioni di preferenza per utente.
  • searchcubit/repository di ricerca utenti.
  • verifybloc di verifica identità (upload documenti, polling dello stato).
  • gate_configconfigurazione delle regole di accesso "gate" (matching viaggi).
  • homecontiene la sotto-feature di verifica email/telefono.
  • adminun intero set di feature solo-admin che rispecchia quasi tutto il resto (utenti, locals, lista viaggi, pagamenti, recensioni, warn, ban, log attività, moderazione chat, gate, statistiche, notifiche) — una vera console di back-office sopra gli stessi domini.
FE.4

Aree di schermata — lib/app/pages/

  • splash / welcomeavvio app e ingresso all'onboarding.
  • authlogin, registrazione, setup profilo, recupero password.
  • banschermata mostrata agli utenti bannati.
  • be_localpercorso per diventare un host "Local".
  • homedashboard/landing principale, diversa per ruolo (traveler/local/admin).
  • travelsl'area più grande: creazione, modifica, dettaglio, feed, filtri, gate, inviti, lista utenti, match, richieste, scheduling, biglietti, eliminazione.
  • localsconsultazione/visualizzazione delle esperienze Local.
  • searchinterfaccia di ricerca utenti/viaggi.
  • chatlista conversazioni e schermata di conversazione.
  • notificationcentro notifiche.
  • profiledettaglio, modifica, follower/seguiti.
  • bookingwidget di prenotazione (nessuna sottocartella pages dedicata).
  • verifyinterfaccia di verifica identità.
  • adminintero back-office: dashboard, lista/dettaglio utenti, dettaglio/richieste viaggio, locals/locals in attesa, pagamenti, recensioni, warn, verifiche, config gate/contenuti, moderazione chat, log attività, notifiche, profilo admin, più i layout condivisi.
FE.5

Routing, Dependency Injection, servizi trasversali

  • Routinggo_router. Tabella delle rotte in lib/app/router/router.dart, logica di redirect divisa in redirect_home.dart/redirect_splash.dart/cartella redirects/. Circa 80 rotte registrate, incluse quelle annidate (es. /profile/:id/follow).
  • Dependency InjectionGetIt (sl<T>()). Wiring centrale in lib/core/di/_injection_container.dart, diviso per part file (injection_providers/repositories/data_source/service.dart + una cartella bloc/); ogni feature espone un proprio <feature>_injection.dart importato e richiamato dal contenitore centrale.
  • Servizi corelib/core/services/: token storage, client HTTP (Dio), socket real-time, upload file, picker immagini, cache offline, stato di connettività, ruolo utente, logging.
  • Utility corelib/core/utils/: breakpoint responsive (web+mobile), header di autenticazione per media protetti, copertine profilo, normalizzazione messaggi d'errore.
  • Localizzazioneeasy_localization, 7 lingue (IT/EN/DE/ES/FR/RU/ZH) in assets/translations/*.json — circa 2.800 chiavi per lingua.
Sul supporto web+mobile: l'app è genuinamente costruita per entrambi (dipendenza flutter_stripe_web, cartella web/ reale), ma la protezione non è uniforme — kIsWeb compare in un solo punto (il generato firebase_options.dart), mentre 13 file importano dart:io direttamente (in particolare i picker immagine/file di chat, verifica identità, foto profilo), senza guardia condizionale. Un'area da tenere d'occhio quando si tocca l'upload di file.
Manuale tecnico — lato server

Mappa completa delle API

NestJS + PostgreSQL (Prisma). Nessun prefisso globale: AdminModule e UserModule montano i propri sotto-moduli sotto /admin/<modulo> e /user/<modulo>, mentre AuthModule e TestModule dichiarano il proprio prefisso (/auth, /test). Circa 200 endpoint REST in totale, elencati qui uno per uno.

BE.1

Guardie di accesso

  • SmartGlobalGuardglobale (APP_GUARD), instrada per prefisso URL: /auth/* e rotte @Public() → pubbliche, /admin/*AdminGuard, /user/*AuthGuard; qualsiasi percorso non riconosciuto fallisce in chiuso su AuthGuard.
  • AuthGuardvalida l'access token JWT, verifica che l'utente non sia bannato, aggancia request.user. Copre tutte le rotte /user/*.
  • AdminGuardvalida il JWT, richiede role === 'ADMIN', verifica il ban, aggancia request.user/request.userRole. Copre tutte le rotte /admin/*.
  • ThrottlerGuardglobale, rate limit 500 richieste/60s.
  • @Public()decoratore per escludere esplicitamente una rotta dalle guardie sopra.
BE.2

Auth — /auth

  • POST /auth/registercrea un account
  • POST /auth/loginautentica, emette i token
  • POST /auth/logoutinvalida sessione/token
  • POST /auth/refreshrinnova l'access token
  • POST /auth/refresh-tokenrinnova l'access token (controller alternativo)
  • POST /auth/send-otp-emailinvia OTP via email
  • POST /auth/send-otp-phoneinvia OTP via SMS
  • POST /auth/verify-otp-emailverifica OTP email
  • POST /auth/verify-otp-phoneverifica OTP SMS
  • POST /auth/recovery-passwordavvia il recupero password
  • POST /auth/send-recovery-otp-emailinvia OTP di recupero via email
  • POST /auth/send-recovery-otp-phoneinvia OTP di recupero via SMS
  • POST /auth/verify-recovery-otp-emailverifica OTP di recupero (email)
  • POST /auth/verify-recovery-otp-phoneverifica OTP di recupero (telefono)
  • POST /auth/complete-recovery-verificationfinalizza il reset password
BE.3

Profilo — /user/profile

  • POST /user/profile/change_passwordcambia password
  • POST /user/profile/verification/completecompleta la verifica profilo
  • GET/PUT /user/profile/notification_settingspreferenze notifiche
  • GET/PUT /user/profile/info_settingspreferenze di visibilità profilo
  • GET/PUT /user/profile/privacy_settingsprivacy dei messaggi: whoCanMessage (EVERYONE/FOLLOWING) e acceptLocalProposals; senza riga valgono i predefiniti
  • GET/POST /user/profile/albumalbum fotografico
  • POST /user/profile/follow/:idsegui un utente
  • GET /user/profile/my-photofoto profilo propria
  • POST /user/profile/photocarica foto profilo
  • GET /user/profile/notificationelenco notifiche
  • GET /user/profile/notification/travel-dialogsdialoghi notifica legati ai viaggi
  • PATCH /user/profile/notification/read-allsegna tutte come lette
  • PATCH /user/profile/notification/:id/readsegna una notifica come letta
  • GET /user/profile/prefspreferenze utente
  • GET /user/profile/roleruolo proprio
  • GET /user/profile/verify_statusstato verifica identità
  • POST /user/profile/verify_profileinvia il profilo per la verifica
  • POST /user/profile/report/:userIdsegnala un utente
  • POST /user/profile/set_localimposta profilo/flag Local
  • POST /user/profile/set_registersegna completato il passo di registrazione
  • POST /user/profile/set_tokenregistra il token push/dispositivo
  • POST /user/profile/set_travelerimposta profilo/flag Traveler
  • GET /user/profile/travel-listelenco viaggi propri (vista profilo)
  • PUT /user/profile/updateaggiorna il profilo
  • POST /user/profile/otp/send-emailOTP per cambio email
  • POST /user/profile/otp/send-phoneOTP per cambio telefono
  • POST /user/profile/otp/verify-emailverifica OTP cambio email
  • POST /user/profile/otp/verify-phoneverifica OTP cambio telefono
BE.4

Utenti — /user/users

  • GET /user/userselenco utenti
  • GET/PUT/DELETE /user/users/meprofilo proprio: lettura, aggiornamento, cancellazione account
  • GET /user/users/:idprofilo di un utente, con canMessage, messagingRestriction e acceptsProposals calcolati per chi guarda
  • GET /user/users/:id/travelsviaggi di un utente
  • GET /user/users/:id/followingchi segue quell'utente
  • GET /user/users/:id/followerschi è seguito da quell'utente
  • GET /user/users/:id/experiencesesperienze svolte da un utente
BE.5

Chat — /user/chat

  • POST /user/chat/createcrea una chat; se è diretta, rifiuta con 403 in caso di blocco (USER_BLOCKED) o privacy (MESSAGES_FOLLOWERS_ONLY). Stesso controllo a ogni invio in chat diretta, via REST e socket
  • POST /user/chat/:chatId/reportsegnala una conversazione o un messaggio (messageId facoltativo, reason); crea un ChatReport per la moderazione admin
  • GET /user/chat/my-chatselenco chat proprie
  • GET /user/chat/travel/:travelIdchat di gruppo di un viaggio (creata la prima volta dal coordinatore)
  • GET /user/chat/event/:eventIdchat di gruppo di un'esperienza (idem)
  • GET /user/chat/messages/:chatIdmessaggi di una chat, paginati
  • POST /user/chat/send-messageinvia un messaggio (fallback REST, il percorso live è il socket)
  • POST /user/chat/uploadcarica un allegato in chat
  • POST /user/chat/reaction/addaggiungi una reazione a un messaggio
  • POST /user/chat/reaction/removerimuovi una reazione
BE.6

Viaggi — /user/travel

  • POST/PUT/GET /user/travelcrea, aggiorna (solo creatore o coordinatore; keepPhotos dice quali foto esistenti restano), elenca/ottiene un viaggio
  • GET /user/travel/dhonkoviaggi curati "Dhonko"
  • GET /user/travel/usersutenti nel contesto viaggi
  • GET /user/travel/users/italiautenti per viaggi domestici
  • GET /user/travel/users/esteroutenti per viaggi internazionali
  • GET /user/travel/my-listviaggi propri
  • GET/DELETE /user/travel/:iddettaglio o eliminazione (solo creatore; elimina anche programma e voti, partecipanti, salvataggi e revisioni, e scollega la chat di gruppo)
  • GET /user/travel/:id/statusstato della propria partecipazione
  • POST /user/travel/:id/change-statusnon implementato: lancia "Not implemented"
  • GET /user/travel/:id/requestsrichieste di partecipazione
  • POST /user/travel/request/:idrichiedi di partecipare (solo traveler e Dhonko: come per invito, conferma dell'invito e accettazione, un local riceve 403 ROLE_CANNOT_JOIN_TRAVELS)
  • GET /user/travel/:id/user/pendingspartecipanti in attesa
  • POST /user/travel/:id/user-addaggiungi un utente direttamente
  • POST /user/travel/:id/user/:idUser/inviteinvita un utente
  • POST /user/travel/:id/user/:idUser/confirmaccetta un partecipante
  • POST /user/travel/:id/user/:idUser/rejectrifiuta un partecipante
  • DELETE /user/travel/:id/user/:idUserrimuovi un partecipante
  • POST /user/travel/:id/user/invite/match/confirm|rejectconferma/rifiuta un invito nato da un match
  • GET /user/travel/:travelId/participant/:participantId/statusstato di un partecipante
  • GET/POST /user/travel/save/:idverifica/aggiunge un salvataggio (bookmark)
  • GET /user/travel/ticketsbiglietti dei propri viaggi
BE.7

Roadmap del viaggio — spostamenti, pernottamenti, attività

Tre sotto-moduli simili (crea, dettaglio, modifica, elimina, cambia stato, vota), uno per tipo di tappa, più le esperienze locali. Le rotte non sono uniformi tra i moduli (la modifica è PUT /:idMovement, PUT /:id/details/:idOvernight, PUT /:id/:idTodo). Regole comuni in schedule/schedule-access.ts: leggere, proporre e votare solo creatore e partecipanti confermati; modificare ed eliminare chi ha proposto o un coordinatore; cambiare stato solo un coordinatore.

  • /user/schedule-movement/*spostamenti (treno/auto/aereo/bus/camper) — CRUD + voto + stato; orari "HH:MM" salvati in UTC
  • /user/schedule-overnight/*pernottamenti — CRUD + voto + stato
  • /user/schedule-todo/*attività/cose da fare — CRUD + voto + stato, orari facoltativi
  • /user/schedule-local/*esperienze locali: eventi disponibili entro 50 km (GET available/:travelId), proposta, dettaglio, voto, stato, eliminazione. POST /:travelId è aperta anche all'host della data proposta, con le stesse regole della lista (errori TRAVEL_NOT_OPEN, EVENT_OUTSIDE_TRAVEL_DATES, EVENT_TOO_FAR…)
  • GET /user/schedule/:idprogramma completo in lettura (solo partecipanti)
  • GET/PUT /user/match/:idcompagni compatibili da invitare / criteri di abbinamento del viaggio (modifica solo creatore o coordinatore)
BE.8

Local / Esperienze — /user/local

  • POST/GET /user/localcrea/elenca esperienze. In creazione schedule ha due formati: con kind (SINGLE, DATES, WEEKLY — validato e trasformato in regole e date da local/schedule/schedule-plan.ts, errori con codice e messaggio) oppure quello di Flutter (weekdaySlots, date per 90 giorni)
  • GET/POST /user/local/:localId/schedulesprogrammazioni di un'esperienza (solo il creatore): elenco con date in programma e prenotate; aggiunta di una programmazione con lo stesso formato della creazione
  • PATCH/DELETE /user/local/schedules/:scheduleIdcambia (ricorrente: giorni, orari, fine, giorni esclusi; date a scelta: aggiunge date; evento unico: data e orari se senza prenotazioni; tutte: posti e prezzi) o ferma una programmazione. Tocca solo le date future senza iscritti, pagamenti, proposte ai viaggi o chat, e dice quante altre restano
  • GET /user/local/dhonkoesperienze curate "Dhonko"
  • GET /user/local/usersutenti nel contesto esperienze
  • GET /user/local/users/specialutenti "speciali" per esperienze
  • GET /user/local/myesperienze ospitate da me
  • GET /user/local/my/partecipantspartecipanti alle mie esperienze
  • GET /user/local/my/dashboardhome dell'host: riepilogo (esperienze per stato, date e iscritti a 30 giorni, incassi netti a 30 giorni e nei 30 precedenti, incassi attesi, valutazione), esperienze con motivo del rifiuto, prossime date con iscritti e incasso, proposte ai viaggi con esito
  • GET /user/local/my/eventstutte le mie date in un intervallo (from/to, predefinito il mese corrente, massimo 100 giorni)
  • GET /user/local/my/nearby-travelsviaggi aperti con le mie date compatibili (dentro il periodo, entro 50 km) e lo stato di ogni proposta
  • POST /user/local/event/:eventId/propose-to/:userIdl'host propone una sua data a un traveler o Dhonko: controlla data, blocchi e privacy, apre o riusa la chat diretta e ci scrive il messaggio con il percorso dell'evento; restituisce chatId
  • GET/PATCH /user/local/:localIddettaglio/aggiornamento esperienza
  • GET /user/local/:localId/calendarcalendario delle occorrenze
  • GET /user/local/:eventId/calendar/detailsdettaglio di una singola occorrenza
  • POST /user/local/:eventId/calendar/details/requestrichiedi di partecipare a un'occorrenza
  • PATCH /user/local/event/:eventIdaggiorna un'occorrenza
  • PATCH /user/local/events/batchaggiornamento massivo di occorrenze
  • DELETE /user/local/event/:eventId/participant/:userIdrimuovi/espelli un partecipante
  • GET /user/local/events/discoverscoperta eventi per il traveler — lat/lng opzionali (assenti = modalità "sfoglia tutto"); restituisce solo occorrenze disponibili, escludendo quelle al completo o già richieste/prenotate dal chiamante
  • GET /user/local/events/search-placecerca un luogo per nome (geocoding) per scegliere manualmente dove cercare eventi vicini
BE.9

Relazioni sociali, pagamenti, ricerca, altro

  • GET/POST /user/follow/:idstato/toggle di un follow
  • GET /user/blockelenco utenti bloccati
  • POST /user/block/:idblocca/sblocca un utente
  • POST /user/warn/:userIdsegnala un utente (usato da Flutter; stessi campi di /user/profile/report/:userId)
  • GET /user/warnle segnalazioni inviate da me, con stato e date (senza l'admin che le ha gestite)
  • POST /user/payment/createcrea un pagamento (Stripe)
  • POST /user/payment/:paymentId/confirmconferma un pagamento
  • GET /user/paymentelenco pagamenti propri
  • POST /user/payment/refundrichiedi un rimborso
  • GET /user/gate/configconfigurazione feature-flag lato utente
  • GET /user/search/usersricerca utenti
  • GET/DELETE /user/search/historystorico ricerche: lettura/pulizia
  • POST/DELETE /user/search/history/:userIdsalva/elimina una voce di storico
  • GET /user/assetsrecupero file/asset autenticato (foto, documenti…)
  • GET/POST /user/reviews/:idrecensioni di un target: lettura/invio
  • GET /user/reviews/status/:idstato delle recensioni
BE.10

Admin — utenti, viaggi, ban/warn, locals

  • /admin/users/*elenco/dettaglio/modifica/cancellazione utenti, cambio ruolo, verifiche in attesa (approva/rifiuta), profili Local in attesa (approva/rifiuta)
  • /admin/travels/*elenco (anche solo in attesa), dettaglio, cancellazione, approvazione/rifiuto viaggio
  • /admin/bans/*banna, elenca, ottieni, rimuovi un ban
  • /admin/warns/*elenca, ottieni, approva/rifiuta/revoca un avviso
  • /admin/locals/*elenco/dettaglio/aggiornamento/cancellazione, approvazione/rifiuto esperienza
BE.11

Admin — statistiche, moderazione, configurazione, pagamenti

  • /admin/stats/*panoramica, serie temporali, top utenti, top destinazioni
  • GET /admin/activity-logregistro di ogni azione admin (32 tipi di azione tracciati)
  • /admin/gate/*config sezioni app (lettura/aggiornamento/seed) + voci gate per locals/viaggi curati "Dhonko"
  • /admin/chat/*chat segnalate, risolvi una segnalazione (POST reported/:reportId/resolve), chiudi una chat, silenzia un utente, elimina un messaggio
  • /admin/payments/*elenco, statistiche, dettaglio, rimborso pagamento (eseguito su Stripe)
  • /admin/reviews/*elenco, segnalazione (flag), cancellazione recensione
  • /admin/notifications/*invio broadcast/mirato, storico invii
BE.12

Canale realtime — Socket.IO

Un solo gateway (ChatGateway) gestisce tutta la messaggistica live.

  • In entratajoin_chat, leave_chat, send_message, typing_start, typing_stop, get_chat_messages, add_reaction, remove_reaction
  • In uscitachats_updated, joined_chat, left_chat, new_message, message_sent, user_typing, chat_messages, reaction_added, reaction_removed, error
BE.13

Servizi condivisi e lavori pianificati

  • Prismaunico strato di accesso al database, usato da guardie e moduli
  • JWTemissione/verifica access e refresh token
  • Bcrypthashing/verifica password
  • Email + Twilioinvio email (Nodemailer/Handlebars) e SMS OTP, entrambi in coda via BullMQ
  • Firebase AdminAuth, notifiche push (device/multi/topic), Storage (immagini/audio/file)
  • Geocodinglookup diretto/inverso (usato dagli endpoint /test/geocode*)
  • Stripepayment intent e rimborsi dietro pagamenti utente e admin
  • Rediscache, in particolare per lo storage degli OTP
  • Job pianificatitre cron ogni minuto (src/schedulers/): archiviazione viaggi conclusi, completamento viaggi terminati, passaggio a "in corso" all'orario di partenza. Un quarto job (notifica "parte a breve") esiste ma è interamente commentato, non attivo. Ogni 10 minuti LocalEventsScheduler porta le date delle esperienze in "in corso" e "conclusa" e tiene pronte le date dei prossimi 120 giorni delle programmazioni ricorrenti, anche senza data di fine.
Manuale tecnico — dati

Cosa memorizza il database (35 modelli Prisma)

PostgreSQL via Prisma. Di seguito ogni modello raggruppato per dominio, con lo scopo e i campi che contano davvero per capire cosa rappresenta — non ogni singola colonna.

DB.1

Identità e utente

  • Userl'account: profilo, l'intero blocco di preferenze di matching viaggio, ruolo, campi di verifica identità (foto selfie/fronte/retro documento, stato, motivo di rifiuto). Nodo centrale collegato a quasi ogni altro dominio.
  • UserFirebasecollegamento 1:1 tra un utente e il suo ID Firebase Auth.
  • UserFirebaseTokentoken dispositivo per le notifiche push, più per utente, per piattaforma.
  • RefreshTokenSessiontraccia i refresh token emessi (con hash) per dispositivo/sessione, per poterli revocare singolarmente o tutti insieme e rilevare il riutilizzo di un token già ruotato.
  • UserSearchHistorystorico "chi ha cercato chi", stile Instagram.
  • VerificationAttemptstorico completo delle richieste di verifica identità (foto, stato, motivo di rifiuto); relazioni separate per chi invia e per l'admin che revisiona.
  • UserWarnavviso di moderazione, con chi lo ha emesso e chi (admin) l'ha eventualmente validato.
  • UserBanban (con scadenza opzionale), utente bannato + admin che lo ha emesso.
  • UserReviewsrecensioni tra utenti (voto + commento), con flag di moderazione flagged.
  • UserNotificationSettings / UserPrefsSettingstabelle 1:1 di interruttori per utente — quali tipi di notifica sono attivi e quali categorie di preferenza sono visibili agli altri.
  • UserPrivacySettings1:1 per utente: whoCanMessage (enum MessagePermission: EVERYONE/FOLLOWING) e acceptLocalProposals. Nessuna riga equivale ai predefiniti (tutti, sì). Migrazione 20260915090000_add_user_privacy_settings.
  • FollowRelation / BlockRelationgrafo sociale classico: chi segue chi, chi ha bloccato chi.
DB.2

Notifiche

  • UserNotificationfeed di notifiche in-app; il type copre quasi ogni evento cross-dominio (follow, recensione, like, match viaggio, messaggio chat, verifica approvata…) con un payload JSON libero.
DB.3

Chat

  • Chatcontenitore di conversazione condiviso tra tre contesti diversi via type (DIRECT/TRAVEL/EVENT) — non un modello polimorfico, ma due chiavi opzionali @unique verso Travel e LocalEvent.
  • ChatMemberappartenenza/ruolo (ADMIN/MEMBER) per utente per chat; leftAt segna l'uscita senza cancellare la cronologia.
  • Messagemessaggi (TEXT/IMAGE/AUDIO/COMBO), con auto-relazione per le risposte in thread.
  • Reactionreazione emoji a un messaggio, per utente.
  • ChatReportsegnalazione di una chat/messaggio, con stato PENDING/RESOLVED e chi ha segnalato/risolto — alimenta la moderazione admin.
DB.4

Viaggi

  • Travelproposta di viaggio: date, destinazione/coordinate, stato (PENDING→APPROVED/SCHEDULED/IN_PROGRESS/COMPLETED, più REJECTED/CANCELLED/ARCHIVED/DELETED), budget, fascia d'età, numero massimo utenti, filtri di matching. Chat 1:1 opzionale.
  • TravelPartecipanttabella di join utente↔viaggio per le richieste di partecipazione, con stato (WAITING/INVITED/REQUESTED/CONFIRMED/REJECTED/INVITE_REJECTED) e flag coordinator.
  • TravelSavesalvataggio/preferito di un viaggio da parte di un utente.
  • TravelMovement / TravelOvernight / TravelTodotre modelli paralleli di "tappa itinerario" (spostamenti, pernottamenti, attività), ciascuno con un utente proponente, un proprio stato (WAITING/ACCEPTED/REJECTED) e un proprio modello di voto per-utente (TravelerMovementVote/TravelOvernightVote/TravelTodoVote) — lo stesso schema di decisione di gruppo ripetuto tre volte.
  • TravelReviewdecisione admin di approvazione/rifiuto di un viaggio (diverso da UserReviews).
DB.5

Local / Esperienze

  • Localannuncio di un'esperienza offerta da un host: destinazione, tipi di esperienza, limiti età/lingua, politiche di cancellazione/termine prenotazione, accessibilità, stato. Soft-delete via deletedAt.
  • LocalPhoto / LocalProvision / LocalGroupDiscounttabelle di dettaglio: foto, forniture incluse (equipaggiamento/cibo/trasporto/bevande), sconti a scaglioni per dimensione del gruppo.
  • LocalScheduleregola che genera le occorrenze, con prezzi e capacità. kind (enum LocalScheduleKind): SINGLE (evento unico), DATES (date a scelta), WEEKLY (ricorrente). Una regola è una fascia oraria (startTimeendTime): una scuola con quattro fasce il lunedì ha quattro regole. Per le ricorrenti anche daysOfWeek, periodo, excludedDates e generatedUntil (fin dove le date sono state create; null nelle regole della vecchia procedura, che il job non estende). Orari nel fuso di Roma. Migrazione 20260916090000_local_schedule_kinds.
  • LocalEventuna singola occorrenza generata da uno schedule, con proprio stato e override opzionali di prezzo/capacità per occorrenza. Come Travel, ha una chat 1:1 opzionale, ed è il target dei pagamenti.
  • LocalEventPartecipanttabella di join per la prenotazione a una specifica occorrenza, stesso schema di stato di TravelPartecipant.
  • LocalProfileprofilo business/verifica dell'host (P.IVA, codice fiscale, certificati, stato) — il record di onboarding "diventa host Local", separato dalla verifica identità.
DB.6

Pagamenti

  • Paymentpagamento via Stripe legato a un utente e a un'occorrenza (LocalEvent), con stripePaymentIntentId, importo in centesimi, stato (PENDING/SUCCEEDED/FAILED/REFUNDED/PARTIALLY_REFUNDED/CANCELLED), importo rimborsato.
DB.7

Admin, moderazione, configurazione

  • AdminActivityLoglog generico delle azioni admin (~30 tipi), contro una coppia polimorfica tipo/id di target.
  • GateConfigconfig tipo feature-flag per mostrare/nascondere/ordinare sezioni dell'app, con filtri JSON e chi l'ha aggiornata per ultimo.
  • AdminNotificationnotifica broadcast/mirata inviata da un admin — target (ALL/ROLE/USER) determina se conta targetRole o targetUserId.
DB.8

Relazioni degne di nota

  • Chat multi-contestocondivisa tra DIRECT/TRAVEL/EVENT via due chiavi esterne opzionali uniche, non una chiave polimorfica.
  • Message auto-relazionatoper le risposte in thread.
  • User iper-relazionatoun numero inusuale di relazioni nominate verso se stesso, per moderazione (chi avvisa/chi è avvisato/admin), grafo sociale (segui/blocca), recensioni, storico ricerche.
  • Attore vs revisoreVerificationAttempt e ChatReport separano entrambi in due chiavi esterne distinte chi ha agito e chi ha revisionato/risolto.
  • Regola → occorrenzaLocal → LocalSchedule → LocalEvent è uno schema a tre livelli regola/occorrenza; pagamenti e chat si attaccano al livello occorrenza, non al livello annuncio.
  • Lo stesso pattern, tre volteil trio Movement/Overnight/Todo sotto Travel ripete identico lo schema "tappa + voto per utente".
Cosa costa davvero (o potrebbe costare)

Servizi esterni: cosa stai usando, cosa stai pagando

Ricavato leggendo .env, dipendenze (package.json/pubspec.yaml) e i file di configurazione reali dei due repository — non le dashboard di fatturazione dei fornitori, che nessun file di codice può mostrare. Dove il codice non basta a dirlo con certezza, è segnalato esplicitamente sotto.

SVC.1

Servizi collegati, uno per uno

  • FirebaseAuth, Cloud Storage (foto/documenti/audio), Cloud Messaging (notifiche push), Analytics. Credenziali reali, non un placeholder (serviceAccountKey.json valido, google-services.json reale sul lato Flutter). Ha un piano gratuito (Spark), ma Storage e Cloud Messaging a un volume d'uso reale richiedono tipicamente il piano a consumo (Blaze) — va confermato sulla console Firebase, non deducibile dal codice.
  • Stripepagamenti delle esperienze Local. Chiavi in modalità test (sk_test_…/pk_test_…) sia lato backend che Flutter — nessun euro reale viene mosso con la configurazione attuale.
  • TwilioOTP via SMS. Il pacchetto è installato ma TWILIO_ENABLED=false nel .env attivo — disattivato, nessun costo in corso.
  • Email (Aruba)invio email transazionali via SMTP Aruba (smtps.aruba.it, mittente piattaforma@dhonko.it) tramite Nodemailer — non Gmail/SendGrid/Mailgun. Presumibilmente già incluso in un piano hosting/email Aruba esistente, non un servizio SaaS aggiuntivo separato.
  • Redisnella configurazione attuale è autogestito in locale (container Docker, localhost:6380). Il file .env.example documenta un formato per un'istanza Redis ospitata su Render per la produzione, ma non risulta ancora attiva nella configurazione effettivamente in uso.
  • PostgreSQLnella configurazione attuale è autogestito in locale (container Docker, postgres:16-alpine). Nessun database gestito (Render/Neon/Supabase/RDS) risulta configurato nell'ambiente corrente.
  • Hosting/deploynessun Dockerfile/render.yaml/pipeline CI trovato in nessuno dei due repository. Un indizio forte ma non una conferma: l'URL di default (commentato) nel .env del frontend punta a onrender.com, e lo stesso vale per gli esempi nel backend — Render è il bersaglio previsto, ma niente nel codice conferma che sia già l'ambiente live in uso.
  • GitHubentrambi i repository sono su github.com (backend e frontend separati). Nessuna GitHub Action configurata in nessuno dei due — quindi nessun costo di CI, ma anche nessuna pipeline automatica di test/build/deploy oggi.
  • Geocodingusa Google Maps Geocoding API se GOOGLE_MAPS_API_KEY è impostata, altrimenti ricade automaticamente su OpenStreetMap Nominatim (gratuito) quando la chiave è assente — il costo qui dipende interamente dal fatto che quella variabile sia popolata o no nell'ambiente in uso.
  • Non trovatinessun servizio di analytics/error-tracking a pagamento (Sentry, Mixpanel, Amplitude), nessun CDN/storage oltre Firebase (niente Cloudinary/S3), nessun servizio push oltre Firebase.
Trovata un'inconsistenza degna di verifica: il file serviceAccountKey.json committato appartiene al progetto Firebase dhonko, mentre il .env attivo del backend dichiara FIREBASE_PROJECT_ID=dhonko-cfdbe (lo stesso ID usato anche lato Flutter, in google-services.json) — due progetti Firebase diversi citati in due punti diversi della configurazione. Vale la pena controllare quale dei due sia davvero quello attivo/fatturato, prima di dare per scontato "un solo progetto Firebase".
Il codice può dire solo "cosa è configurato", non "cosa è fatturato". Per sapere con certezza cosa stai davvero pagando in questo momento vanno controllate le dashboard dei singoli fornitori — piano/fatturazione su console.firebase.google.com, modalità test/live e importi su dashboard.stripe.com, l'account Render (se e quando l'ambiente di produzione viene effettivamente collegato lì), l'account Twilio (comunque disattivato oggi), e il pannello di controllo Aruba per l'email — nessuno di questi dati vive nel repository.
Cosa è stato fatto, sessioni dall'11 al 15 settembre 2026

Registro attività

Non un changelog di prodotto: un resoconto preciso del lavoro tecnico svolto su mock_server e su dhonko (Flutter), in ordine.

sicurezza

Audit completo del backend

Lettura di tutto il codice sorgente di mock_server (auth, common, moduli user/admin) e produzione di un audit con criticità classificate per severità.

8 critiche8 alte6 medie/basse11 architetturali
sicurezza

Autenticazione e OTP

Corretto il bug che rendeva l'OTP sempre "000000" e quello che forzava is_verified = true al login (causa reale: mappato sul campo KYC sbagliato invece che sul completamento registrazione). L'OTP ora è casuale per davvero, con un bypass opzionale via .env (OTP_BYPASS_ENABLED), disattivato in modo incondizionato se NODE_ENV=production, come richiesto.

sicurezza

Segreti e sessioni

Chiave privata Firebase (serviceAccountKey.json) tolta dal tracking git. JWT_SECRET ruotato con un valore casuale a 384 bit. Access token accorciato da 30 giorni a 15 minuti. Creata una vera tabella di sessioni (RefreshTokenSession) con rotazione ad ogni refresh, rilevamento del riuso di un token già ruotato (revoca precauzionale di tutte le sessioni) ed endpoint /auth/logout nuovo.

sicurezza

Accesso alle API

Collegato il rate limiting (era configurato ma mai agganciato) con limiti stretti su login/OTP/registrazione. CORS ristretto a allow-list, aggiunto Helmet. Email e telefono non sono più visibili ad altri utenti tramite GET /user/users/:id o la lista utenti. Filtro globale che impedisce a qualunque errore di esporre stack trace o dettagli interni.

sicurezza

Storage file — la scoperta più grave

Ogni file caricato (chat, profilo, viaggi, local, documenti di verifica identità inclusi) era pubblico su Firebase Storage con un URL firmato valido fino al 2500, senza autenticazione. Creato GET /user/assets, endpoint autenticato con regole per categoria (chat e verifica privati, resto visibile a chi è loggato, admin sempre). Corretta in 5 punti di upload una vulnerabilità di path traversal (nomi file del client usati senza sanificazione in scrittura su disco). Trovate e tolte dal tracking 48 foto già committate su GitHub in tmp/photo/verify/ — confermate foto di test, non documenti reali.

infrastruttura

Docker Compose locale

Aggiunti Postgres e pgAdmin accanto al Redis già presente, per non dover più usare il database di produzione in locale. Risolti due conflitti di porta reali con servizi Windows nativi già in ascolto sulla macchina (un Postgres nativo su 5432, non Docker — causa di un'ora di autenticazioni fallite in modo random prima di essere isolata). Migration applicate e verificate sul DB locale: scoperto per l'occasione che la cronologia migration tracciata creava solo 8 tabelle su 40+, segno che il DB di produzione è stato allineato a mano con db push senza mai generare le migration corrispondenti.

documentazione

Swagger / OpenAPI

Era una dipendenza installata ma mai collegata. Attivato con generazione automatica di tag e requisito di autenticazione dedotti dal path della route (nessun @ApiTags da mantenere a mano su 200+ controller). Verificato dal vivo, non solo a compile-time.

180 endpoint31 tag192 protetti / 14 pubblici
analisi

Scheduler e stati del viaggio

Analizzati i 4 job automatici (3 attivi ogni minuto, 1 scritto ma disattivato). Trovato un anello mancante: nessun punto del codice scrive mai lo stato SCHEDULED, quindi lo scheduler che dovrebbe far partire "ufficialmente" un viaggio non trova oggi mai nulla da processare — un viaggio approvato salta direttamente da APPROVED ad ARCHIVED allo scadere, senza mai passare da in-corso/completato. Solo analisi, non ancora corretto.

frontend

Client Flutter aggiornato di conseguenza

Con lo storage reso privato, ogni immagine/audio caricato dall'app doveva iniziare ad allegare l'access token o avrebbe ricevuto 401. Aggiunta una cache sincrona del token in TokenStorageService e un helper condiviso (authMediaHeaders) che allega l'header solo alle URL del nostro backend, mai a host esterni. Applicato a ogni punto reale dell'app che carica media da rete.

25/25 file coperti2 widget condivisi + 23 punti direttiflutter analyze: 0 errori introdotti
infrastruttura

Questa pagina servita davvero dal backend

public/ non era servito staticamente da nessuna parte (nemmeno il file chat-test.html già presente ci era mai arrivato). Aggiunto app.useStaticAssets() in main.ts; questa pagina ora vive anche come file reale in public/overview.html, raggiungibile dal server, non solo come artifact.

frontend

Privacy, FAQ, Centro assistenza — da placeholder a pagine vere

Le tre voci del menu impostazioni Flutter (Privacy / Policy, Centro assistenza, Faq) aprivano tutte, letteralmente, https://www.google.com — mai completate. Scritte tre pagine reali in public/ (privacy + termini d'uso, FAQ con 12 domande sulle funzioni reali dell'app, centro assistenza con contatti per area), con lo stile del tema Flutter reale. main_menu.dart ora punta lì.

infrastruttura

Home page del backend e cancellazione dati

Aggiunta public/index.html come pagina di default su / (prima il root non serviva nulla). Aggiunta anche data-deletion.html: pagina pubblica separata dalla privacy policy per la richiesta di cancellazione account/dati, collegata alla funzione "Elimina account" già esistente — utile in vista di una pubblicazione reale su Google Play/App Store, che la richiedono come URL a sé stante.

documentazione

Questa pagina

Costruita in più passaggi: prima panoramica di prodotto, poi catalogo funzionalità completo dal client Flutter, poi scheduler/blocco/ban dal backend, poi identità visiva reale (logo, colori, font, sfondi) dai file del tema, poi questo registro — aggiornato di nuovo qui, come da pratica concordata: ad ogni sessione di lavoro, sia Swagger che questa pagina restano allineati a cosa esiste davvero.

frontend

Audit completo delle stringhe hardcoded (i18n)

Passata sistematica su tutto dhonko/lib per trovare testo non passato da easy_localization: circa 40 file tra widget, bloc/cubit e livello dati. Corretti tutti, con chiavi nuove tradotte davvero (non solo in italiano) in tutte le 7 lingue supportate (it/en/de/es/fr/ru/zh) — profilo generico e ruoli utente, pulsante segui, recupero password, avanzamento/timeout/successo verifica identità, campi di ricerca viaggio, card richieste di viaggio, pagine di successo creazione/modifica/eliminazione proposta, editor evento local (mesi del calendario ora seguono la lingua dell'app invece di essere fissi in italiano), selettore foto, stato "nessuna proposta di trasporto", calendario a intervallo.

~40 file Dart7 lingueflutter analyze: 0 errori introdotti
bug reale

Il pulsante di login mostrava la chiave grezza, non il testo

Durante l'audit, scoperto che il bottone "Accedi" della schermata di login usava una chiave di traduzione (auth.login) che nel file JSON puntava a un oggetto annidato, non a una stringa: easy_localization non riusciva a risolverla e mostrava letteralmente il testo auth.login a schermo, non "Accedi"/"Log in". Corretto con una chiave dedicata (auth.login_button) in tutte le lingue. Trovati e corretti anche due copia-incolla: la pagina di "viaggio eliminato" mostrava il testo della pagina di recupero password ("Hai cambiato la tua Password!") con un pulsante "ACCEDI" senza senso in quel contesto.

i18n backend

Messaggi di errore lato client, anche loro tradotti

Non solo l'interfaccia: anche i messaggi d'eccezione generati dal livello dati Flutter (chiamate API fallite, upload file, verifica OTP, permessi ban/unban, riconnessione socket) erano stringhe fisse in italiano o inglese, mostrate all'utente tramite SnackBar/stato d'errore. Unificati su un set di chiavi errors.* riutilizzabili e tradotte in tutte le 7 lingue, applicate su circa 60 punti di lancio eccezione tra repository, use-case e bloc.

~60 messaggi d'errore1 bug di login corretto2 bug di copia-incolla corretti
design

Studio dei mockup Figma reali e correzione dello sfondo globale

Caricati in assets/figma/ 201 mockup PNG del design reale dell'app: usati come riferimento definitivo di stile per confrontare ogni schermata con quello che il codice produce davvero. Prima scoperta, la più grande: background.png — montato dietro ogni schermata di tutta l'app — era il negativo cromatico del mockup: sfondo nero con linee teal spesse a piena opacità, invece di sfondo bianco con linee teal pallidissime. Rigenerata l'immagine dall'arte vettoriale sorgente in due varianti corrette (chiara per l'app, blu navy per le schermate a piena pagina con testo bianco) — vedi dettagli nella sezione 09 "Identità visiva", ora aggiornata di conseguenza. Trovato nello stesso punto un secondo bug reale: DhonkoScaffoldBlue montava lo sfondo chiaro invece di quello scuro dedicato, rendendo a rischio illeggibilità il testo bianco su ~18 schermate (verifica identità, completamento profilo, travel match, pagine di successo viaggio/proposta). Aggiunto anche il lockup di logo (icona + "DHONKO" + "FEEL YOUR VIBES") mancante su benvenuto e tutorial. Il resto dei 201 mockup (form di viaggio, pianificazione condivisa, home, profilo) è stato campionato ma non ancora passato in rassegna schermata per schermata.

201 mockup caricati2 bug reali di sfondo corretti~18 schermate impattate1 lockup logo aggiunto
bug reale

Le 14 schermate di preferenze dell'onboarding non erano mai state tradotte

Riaudit completo di tutto dhonko/lib per stringhe hardcoded residue. Trovato il caso più grave della sessione: profile_setup_steps.dart — lo step che genera le 14 schermate di preferenze mostrate a ogni singolo nuovo utente durante la registrazione (stato civile, lingue, destinazioni, hobby, personalità, compagni di viaggio, animali, abitudini, vita notturna, alloggio, alimentazione, tipo di viaggiatore, mezzi di trasporto, itinerari) — passava titoli e sottotitoli fissi in italiano invece di richiamare .tr(), nonostante le chiavi corrette (auth.profile_setup.steps.*) esistessero già, tradotte in tutte le 7 lingue, semplicemente mai collegate. Un utente non italiano vedeva quindi metà della registrazione in italiano puro. Collegate tutte le 28 chiavi mancanti. Corretti inoltre: un'etichetta "Rispondi a" in chat, i motivi di rifiuto della verifica identità, i messaggi d'errore del recupero password e del caricamento profilo (~15 varianti), il nome di fallback delle chat senza nome, un messaggio di errore offline in chat, il valore di default della biografia utente (mai traducibile perché hardcoded in un parametro costante — sostituito con stringa vuota), e 7 messaggi di conferma nel pannello admin (creazione/modifica/eliminazione contenuti Gate, eliminazione utente).

14 schermate onboarding corrette28 chiavi ricollegate~25 stringhe aggiuntive corretteflutter analyze: 0 errori introdotti
design

Sfondo rigenerato dal vero sorgente vettoriale, e piastrellato invece di stirato

Test in web (finestra larga) hanno mostrato il problema vero: lo sfondo era un'unica immagine bitmap in formato verticale-telefono, forzata a coprire finestre larghissime con BoxFit.cover — il pattern veniva ingrandito a dismisura, le linee diventavano rade e sfocate, quasi illeggibili. Trovato il file sorgente reale (assets/svg/background.svg, 430×932, linee #00C1B3 al 10% di opacità): rigenerata l'immagine direttamente da quel vettoriale (non più una ricostruzione approssimata) e cambiato il meccanismo da "un'immagine stirata" a un tassello piastrellato alla sua scala reale (DecorationImage con ImageRepeat.repeat), così la densità del pattern resta identica indipendentemente dalla larghezza della finestra — desktop, web o telefono che sia.

sorgente: assets/svg/background.svgda stirato a piastrellato2 varianti (chiara/scura) rigenerate
design

Welcome page — layout desktop rivisto

Il layout a due colonne per schermi larghi (≥1200px) aveva due problemi: l'illustrazione usava BoxFit.cover e veniva tagliata male (testa/piedi fuori inquadratura), e il blocco di testo a sinistra era centrato dentro una colonna allineata a sinistra — un disallineamento visivo tipico dei layout "riadattati" male. Illustrazione ora in contain dentro un pannello con alone sfumato teal/arancio coerente col brand; titolo/sottotitolo/CTA allineati a sinistra su desktop, centrati su mobile/tablet come da mockup.

ruolo admin

Restyling completo del ruolo Admin — richiesto come "prodotto professionale, stile SaaS, web e mobile distinti"

Creato un design-kit condiviso in lib/app/pages/admin/widgets/ (AdminPageScaffold, AdminSectionCard, AdminKpiCard, AdminEntityCard, AdminStatusBadge, AdminSearchField, AdminFilterChips, stati vuoti/errore, dialog di conferma) invece di ridisegnare ogni schermata a mano. Bug strutturale trovato su quasi tutte le pagine admin: ognuna aveva la propria Scaffold+AppBar, mostrata IN PIÙ rispetto a quella già presente nella shell di AdminHomePage quando usate come tab — due barre sovrapposte. Risolto con una modalità embedded nello scaffold condiviso. Dashboard principale ricostruita da zero (griglia vera su desktop, colonna singola su mobile, nuova sezione "Azioni rapide"). Restyle delle 13 schermate rimanenti con lo stesso stile piatto/bordato e badge di stato coerenti. Aggiunte 15 chiavi di traduzione mancanti in tutte le 7 lingue.

1 design-kit condiviso14 schermate ristilizzatebug doppia AppBar risolto15 chiavi i18n aggiunte × 7 lingue
ruolo admin

Dettaglio ovunque, sul modello della pagina Viaggi — ogni schermata deve avere "il tasto dettagli" e i dettagli devono rimandarsi a catena

Secondo design-kit condiviso, estratto dallo stile della pagina Viaggi presa come riferimento esplicito: admin_list_kit.dart (bottoni azione uniformi, striscia KPI, tab a pillola, toolbar ricerca+filtri+ordinamento, tabella con intestazioni ordinabili, paginazione) e admin_detail_kit.dart (sezioni di dettaglio, righe etichetta/valore, chip informativi, tile utente collegabile al profilo, tile record correlato con bottone "Dettagli", viewer JSON, galleria foto con zoom). La pagina "Esperienze Local" è stata rifatta completamente sul modello Viaggi, sia lista che dettaglio (prima il dettaglio mostrava solo titolo/destinazione/stato/gestore — ora galleria foto, tutti i campi pubblicati, logistica, regole di prenotazione, host con profilo operatore completo, ogni ricorrenza con le sue occorrenze generate). Il Log Attività copriva solo 10 azioni su 32 possibili e 5 target su 10 — ricostruito con mappatura completa, dialog di dettaglio per ogni voce con JSON grezzo ispezionabile. Bug trovato di passaggio: un campo competenze dichiarato come lista in un DTO nuovo, ma sul backend è testo libero — crash TypeError non appena un profilo Local aveva quel campo compilato. Bug di traduzione preesistente (non introdotto qui) anche trovato: admin.sidebar.*/admin.topbar.* esistevano solo in italiano e inglese.

2° design-kit (list+detail)Locali: lista e dettaglio da zeroActivity Log: 10→32 azioni mappate1 crash TypeError risolto
bug reale

Foto profilo assenti in tutto l'admin — "non ci sono le foto degli utenti"

Bug sistemico in due varianti su tutto il pannello admin: (1) molte pagine mostravano solo le iniziali senza nemmeno provare a caricare la foto, pur avendo l'URL disponibile; (2) le poche che ci provavano usavano NetworkImage/CircleAvatar.backgroundImage senza il Bearer token richiesto dal nostro endpoint autenticato /user/assets — la richiesta tornava 401 e, siccome il fallback iniziali veniva passato solo quando la foto era assente (non quando falliva), l'avatar restava vuoto del tutto. Creato AdminAvatar, un solo widget con header di autenticazione + fallback iniziali vero via errorBuilder (non onBackgroundImageError, che non riesce a ripristinare un fallback in modo affidabile), sostituito in tutte le 7 sezioni segnalate più altre 3 trovate con lo stesso bug. Bug di backend trovato in coppia: l'endpoint /admin/users non selezionava affatto il campo photo nella query Prisma.

1 widget AdminAvatar per tutto l'admin10 sezioni corrette1 campo Prisma mancante aggiunto0 chiavi i18n mancanti su 310 verificate
viaggi

"Roadmap" del viaggio: proponente e voti sulle tappe, viaggi salvati, preferenze di matching

Rinominato lo scheduling utente in "Roadmap" (richiesto esplicitamente). Backend: l'endpoint /admin/travels/:id non includeva affatto chi avesse proposto ogni spostamento/pernottamento/attività, né i voti dei partecipanti, né chi avesse salvato il viaggio — tutti aggiunti all'include Prisma. Frontend: la vecchia sezione "Itinerario" (tipo/data/prezzo in una riga piatta) rifatta come sezione "Roadmap del viaggio" con icona per tipo specifico, stato di conferma, chi l'ha proposta (link al profilo), e il conteggio dei voti pro/contro con fila di avatar cliccabili. Aggiunte due sezioni nuove: "Salvato da" (chip con avatar+data) e "Preferenze di matching" (i campi che alimentano i suggerimenti di compatibilità, con etichette leggibili). Bug trovato di passaggio, non collegato al restyling: il bottone "Trova compagni di viaggio", mostrato dall'app a qualsiasi coordinatore, veniva rifiutato dal backend con un 400 perché autorizzava solo il creatore esatto del viaggio — corretto in entrambi gli use-case coinvolti.

3 campi Prisma include aggiunti2 sezioni nuove (salvati, matching)1 bug 400 "solo il creatore" corretto
design

Orologio più semplice per le proposte Movement/Todo

Sostituito showTimePicker (l'orologio Material con lancetta da trascinare, giudicato scomodo) con un nuovo showDhonkoTimePicker — una rondella scorrevole ore:minuti in stile bottom sheet coerente col brand — su tutti e 4 i punti in cui si sceglie un orario: creazione/modifica proposta di spostamento e di attività (i pernottamenti hanno solo una data, mai avuto un orario).

4 schermate aggiornate
impostazioni

Impostazioni Traveler: coerenza grafica totale, navigazione avanti/indietro corretta, privacy/help/faq verso il backend, cambio lingua

Creato SettingsSectionScaffold: un'unica intestazione (bottone indietro + titolo centrato) e corpo scrollabile condiviso da tutte le schermate del bottom sheet impostazioni. Tre bug trovati e corretti: (1) il bottom sheet non aveva altezza vincolata — la sezione "Viaggi salvati" usava Expanded dentro una Column senza altezza definita, rischio di crash reale a runtime; (2) SettingsTile aveva l'icona commentata (//leading: icon) — nessuna icona è mai stata visibile in nessun menu impostazioni; (3) "Verifica Account" veniva renderizzato dentro il bottom sheet come uno Scaffold annidato senza alcun bottone indietro. La navigazione da singola stringa _selectedSection (con salti sbagliati per due sezioni) è diventata un vero stack push/pop — ogni sezione torna indietro esattamente da dove si è entrati. "Informazioni personali" mostrava tre campi sempre vuoti (mai valorizzati) — ora popolati dai dati reali. Privacy/Help/FAQ spostate dal menu principale dentro Impostazioni, insieme alla nuova sezione Lingua (le 7 lingue già tradotte, bandiera+nome nativo, cambio immediato).

1 scaffold condiviso per 10 sezioni1 crash potenziale risoltoicone mai viste ora visibilinuova sezione Lingua
sicurezza

Profilo Traveler: la visibilità dei dati era salvata ma non applicava davvero nulla

Bug di backend importante: le 13 leve "Informazioni visibili nel profilo" (UserPrefsSettings) venivano salvate correttamente ma non erano mai lette — GET /user/users/:id restituiva sempre tutti i campi a chiunque, indipendentemente dalle preferenze. Corretto in users-get-by-id.use-case.ts. Aggiunte 3 leve mancanti dall'interfaccia (itineraries, travelUserType, travelType). TabUserProfileWidget riscritto nello stesso stile a card del dettaglio viaggio, con un interruttore occhio/occhio-barrato per campo (solo per il proprietario) che nasconde/mostra il dato agli altri all'istante. Corpo della pagina trasformato in vere tab stile Instagram (SliverPersistentHeader+TabBar, pattern già esistente in LocalProfile ma mai attivato per il Traveler — c'era perfino il codice commentato che lo preannunciava): Info, Viaggi, Esperienze, più Salvati visibile solo al proprietario. Creati due endpoint backend prima inesistenti (GET /user/users/:id/travels e /experiences) perché quelli esistenti leggevano sempre solo l'utente autenticato. Pagina Follower/Seguiti ristilizzata in coerenza.

13 leve di visibilità: da no-op a funzionanti4 tab stile Instagram2 endpoint backend nuovi
design

Modifica profilo — restyling grafico

Tutti i widget di campo (testo, dropdown, selezione multipla, interruttore, selettore data) passati da Colors.grey[...]/Colors.black87 grezzi a DhonkoColors, campi ora riempiti (filled) invece che solo bordati. Contenuto raggruppato in card (EditProfileCard, stesso stile bianco/bordato/ombra soft del dettaglio viaggi) al posto del vecchio layout a colonna piatta con solo titoli di sezione: Informazioni base, Biografia, Preferenze personali, Preferenze di viaggio (solo Traveler), Profilo Local (solo Local). Nessuna modifica di logica o di salvataggio, solo styling.

5 widget di campo ricoloraticontenuto raggruppato in 5 card
bug reale

Salvataggio modifica profilo: la barra di caricamento restava bloccata per sempre

Il gestore dell'evento di salvataggio (auth_bloc.dart) impostava isLoading: true prima della chiamata, ma dopo l'attesa emetteva solo status: authenticated senza mai riportare isLoading a false — e il metodo copyWith dello stato mantiene il valore precedente quando non viene passato esplicitamente. La pagina restava quindi bloccata per sempre nel ramo "sto caricando" (vuoto), niente pop né messaggio di successo, e nessun try/catch — un errore di rete avrebbe prodotto lo stesso blocco. Corretto aggiungendo isLoading: false su entrambi i rami (successo/errore) più un vero try/catch con messaggio d'errore leggibile. Stesso identico bug corretto anche nel gestore di caricamento foto profilo, che aveva lo stesso pattern.

2 gestori con lo stesso bug corretti
chat

Lista chat in stile WhatsApp, "nuova chat" reale (prima un placeholder), fix di uno spinner infinito

Bug reale trovato: aprire una chat con un utente per la prima volta poteva restare bloccato su uno spinner per sempre. Causa: la pagina creava la chat sparando un evento sul bloc e poi ascoltava lo stream sperando in un futuro stato che la contenesse — se la creazione andava a buon fine mentre il bloc non era ancora nello stato giusto (es. subito dopo l'avvio app), nessuno stato veniva più emesso e lo spinner restava acceso all'infinito. Risolto chiamando l'use-case di creazione chat direttamente, in modo deterministico. La riga della lista chat (prima una card fluttuante con gradiente/ombra/animazioni) è stata riscritta in righe piatte edge-to-edge stile WhatsApp: avatar 56px, nome+orario in alto, anteprima messaggio+puntino verde non-letto in basso, divisore sottile rientrato dopo l'avatar. Tab dei filtri passate da pillole colorate a una sottolineatura stile segmented-tab. "Nuova chat" — prima un placeholder "funzionalità in arrivo" — apre ora un bottom sheet che carica le persone seguite e, alla selezione, apre/crea la chat con quell'utente; aggiunto anche un FAB verde sempre visibile (prima l'unico ingresso era la sola schermata vuota).

1 spinner infinito risoltorighe chat: da card a stile WhatsApp"nuova chat": da placeholder a funzione reale
design

Pagina Follower/Seguiti: le tab non si intonavano al resto

L'header usava il logo del brand come titolo con una AppBar alta 80px, e la TabBar di Material di default sotto — una combinazione pensata per la home, fuori posto su una sotto-pagina. Sostituito con lo stesso pattern back-button + titolo testuale usato ovunque nelle altre sotto-pagine di questa sessione, e la TabBar spostata in una barra più sottile (44px) con un bordo superiore leggero e un indicatore a misura di etichetta invece di quello a tutta larghezza.

AppBar: da 80px con logo a titolo testuale standard
design

Barra di navigazione Traveler: rimosso il pulsante "+" flottante che stonava

Il pulsante "+" per creare un viaggio era un FloatingActionButton agganciato al centro (centerDocked) ma senza una vera tacca (CircularNotchedRectangle) ritagliata nella barra sottostante — restava semplicemente appoggiato sopra, staccato dal resto. Rimosso il FAB e ricostruita la barra con 6 posizioni equidistanti: viaggi, biglietti, "+" (ora una posizione normale della barra, un cerchio verde con ombra propria, non più flottante), gate, messaggi, ricerca.

FAB flottante → posizione normale nella barra
design

Bolla chat flottante: visibile anche su telefono, dove non serviva

La bolla di scorciatoia rapida alla chat compariva anche nel layout mobile, dove la barra di navigazione ha già una voce "Messaggi" a portata di pollice — un elemento in più senza motivo, oltre a sovrapporsi visivamente con altri controlli. Rimossa dal ramo mobile; resta visibile solo nei layout tablet/desktop, dove effettivamente non esiste un'alternativa già a portata di clic.

bolla chat: solo tablet/desktop
sicurezza

Durata dei token rivista: 15 minuti era troppo aggressivo, il refresh sul client non esisteva davvero

Access token portato da 15 minuti a 60 minuti, refresh token da 7 a 30 giorni. Le due durate erano sparse e duplicate a mano in 4 punti diversi (jwt.service.ts, refresh-session.service.ts, refresh-token.use-case.ts, login.use-case.ts) col rischio concreto di andare fuori sincrono — login.use-case.ts infatti dichiarava già una scadenza (expires_at) di 24 ore, completamente sbagliata rispetto ai 15 minuti reali del token. Ora c'è un'unica fonte di verità (token-ttl.constants.ts), sovrascrivibile via env per ambiente. Controllato anche il client Flutter: il flusso di refresh automatico era codice morto, commentato per intero da mesi (l'handler del 401 non puliva nemmeno i token salvati prima di rimandare al login, e l'handler del 403 — che comunque non è mai il segnale giusto per un token scaduto, quello arriva sempre come 401 — non chiamava né resolvenext, quindi la richiesta restava sospesa per sempre). Riscritto dio_client.dart: sul 401 il client ora chiama davvero POST /auth/refresh, con un solo refresh in volo anche se più richieste scadono nello stesso istante (le altre attendono lo stesso risultato), riprova la richiesta originale col nuovo access token, e solo se il refresh token stesso non è più valido pulisce tutto e manda l'utente al login.

access token: 15m → 60mrefresh token: 7d → 30d1 fonte di verità per le duraterefresh automatico sul client: da morto a funzionante
sicurezza

Chat di gruppo viaggio/esperienza: chiunque poteva crearla e chi ne usciva continuava a ricevere messaggi live

GET /travel/:travelId e GET /event/:eventId (risoluzione della chat di gruppo) non avevano nessun controllo di autorizzazione: qualunque utente autenticato, anche mai stato nel viaggio/evento, poteva farsi restituire l'id della chat — e se non esisteva ancora, la creava lui stesso con tutti i partecipanti come membri. Stesso buco, indipendente, in chat-get-my-chats.use-case.ts (syncMissingTravelChats/syncMissingEventChats): la chat veniva creata automaticamente al primo utente qualsiasi che apriva la sua lista chat. Ora la creazione è riservata al coordinatore del viaggio/evento (chi lo ha creato, o un partecipante confermato con coordinator: true) in entrambi i punti; se la chat esiste già, richiederne l'id richiede di essere (stato) un membro — altrimenti 403. Corretto anche un buco lato socket: al reconnect il gateway univa l'utente a tutte le room delle chat di cui è mai stato membro, incluse quelle abbandonate — un utente uscito o rimosso da un viaggio continuava a ricevere in tempo reale i nuovi messaggi del gruppo. Ora si uniscono solo le chat di cui si è membro attivo (leftAt IS NULL); chi ha lasciato/è stato rimosso resta comunque in grado di aprire la conversazione e leggere la cronologia fino alla propria leftAt (già filtrata correttamente lato REST), ma non riceve più nulla dopo quella data — né via socket né via API.

2 endpoint REST senza alcun controllo → coordinatore-only2 auto-sync non gated → coordinatore-onlysocket: join solo su membership attivachi lascia: cronologia sì, live no
bug reale

Eventi Local del traveler: "non vedo nessun evento" — le coordinate seedate avevano gli assi invertiti

Diagnosticata la causa reale: la quasi totalità dei local seedati aveva coordX/coordY scambiati rispetto alla convenzione del codice (coordX=longitudine, coordY=latitudine) — verificato concretamente: un local chiamato letteralmente "Roma" risultava a 203km dalla vera Roma, ben oltre il raggio di ricerca di default (50km). Corretti gli script sorgente del bug (seed-locals-mock.ts, enrich-travels-full.ts) e i 213 eventi già presenti nel database di sviluppo (50 righe con lo scambio esatto invertite, 7 righe "Tokyo" corrotte diversamente riportate alle coordinate reali). Aggiunto anche fix-local-coords-geocode.ts, un nuovo script permanente che geocodifica per davvero ogni destinazione (stesso GeocodingService già usato in creazione/modifica esperienza, Google Maps con fallback OpenStreetMap) — utile dopo un futuro reseed. Verificato dopo il fix: una ricerca dalla vera posizione di Roma trova ora eventi entro 50km, prima ne trovava zero. Approfittandone: GET /user/local/events/discover ora accetta lat/lng opzionali (nessun filtro di distanza se assenti) e nuovo endpoint GET /user/local/events/search-place per cercare un luogo per nome — la schermata di scoperta eventi lato Flutter è stata ristilizzata da zero (era in tema scuro/gradiente con testi hardcoded in italiano, stile estraneo al resto dell'app) con due modalità: "Vicino a te" (GPS o luogo scelto tramite ricerca) e "Tutti" (nessun filtro di posizione, eventi raggruppati per destinazione — un vero drill-down nazione/regione/provincia non è possibile con i dati attuali, solo testo libero, nessuna struttura geografica nello schema).

213 eventi con coordinate corretteentro 50km da Roma: 0 → 71 endpoint nuovo (ricerca luogo)schermata ristilizzata + 2 modalità di ricerca
bug reale

Eventi Local: seguito — spinner infinito su "Vicino a te", solo occorrenze disponibili, tap diretto all'evento

loadWithGps() non aveva alcun timeout esplicito attorno alle chiamate di Geolocator — sul web, se il prompt del browser per il permesso di localizzazione non riceve mai una risposta definitiva, la Future resta sospesa a tempo indefinito (il timeLimit di LocationSettings non è garantito su tutte le piattaforme). Aggiunto un .timeout(...) esplicito a ogni chiamata (stato del servizio, permesso, posizione), con fallback su uno stato d'errore invece dello spinner perenne. Backend: discover_local_events ora filtra davvero "solo disponibili" — esclude le occorrenze già al completo (partecipanti confermati ≥ posti massimi) e quelle su cui il chiamante ha già una richiesta/prenotazione attiva; il campo isAlreadyProposed del DTO era dichiarato ma hardcoded a false e mai calcolato — ora la logica esiste per davvero, applicata come filtro. Corretto anche un bug di navigazione: il tap su una card portava alla pagina principale del Local, da cui bisognava ripescare a mano la stessa data nel calendario — ora va dritto al dettaglio della singola occorrenza (LocalCalendarDetailPage, già esistente altrove ma mai collegata qui), con evento e data già fissati.

GPS: timeout espliciti su ogni chiamatafiltro "solo disponibili" attivato lato backendtap card: 2 passaggi → 1
bug reale

Email transazionali: il codice OTP non compariva nell'email, il layout condiviso non ha mai funzionato — avviata la conversione a React Email

Controllando l'infrastruttura email per convertirla a React Email (richiesto esplicitamente, per template più professionali), trovati due bug reali indipendenti dalla richiesta iniziale. Il primo, grave: il template otp-email-verification.hbs non conteneva da nessuna parte il codice OTP (verificato: zero occorrenze di "otp"/"code" in tutto il file) — chi riceveva quell'email non vedeva il codice. Altri template come welcome.hbs ignoravano completamente il nome utente, e welcome_back.hbs usava {{username}} minuscolo mentre il codice passa userName — Handlebars è case-sensitive, quindi si renderizzava vuoto. Il secondo bug, strutturale: la configurazione di layoutsDir/defaultLayout/partialsDir in app.module.ts (un layout condiviso mai completato, letteralmente "HELLO WORLD" come contenuto) non ha mai avuto effetto — verificato leggendo il sorgente di HandlebarsAdapter.compile(): compila il file .hbs direttamente con la libreria handlebars grezza, ignorando del tutto quella configurazione. Ogni email è quindi sempre stata, di fatto, un file autonomo — da cui il motivo per cui tutti e 27 i template erano ciascuno una copia-incolla completa da Gmail invece di un contenuto agganciato a un layout condiviso. Rimossa la configurazione morta e i file collegati (layout stub, partial mai referenziati, 3 template orfani senza alcun metodo che li richiami).

Installato react-email/@react-email/components come devDependency, in una cartella emails/ separata da src/ (esclusa esplicitamente dalla build NestJS — zero rischio sul bundle di produzione). I template si scrivono in JSX con componenti condivisi (EmailLayout con il vero logo incorporato come data URI invece dell'URL rotta via proxy Gmail, colori brand, footer con i link legali reali), poi npm run build:emails li compila in .hbs statici con i segnaposto {{mustache}} intatti — la pipeline di invio esistente (BullMQ, override di test, rotazione oggetto) resta identica, senza alcun runtime React nell'app in produzione. Convertiti i primi 4 template come campione (benvenuto, bentornato, verifica OTP, richiesta di viaggio accettata) end-to-end verificato contro il vero HandlebarsAdapter; gli altri 23 template restano da convertire. Corretto anche l'oggetto duplicato di sendWelcome/sendWelcomeBack (stesso identico testo per registrazione e login).

1 bug critico: OTP assente dall'emaillayout condiviso: mai stato attivo, ora rimosso4/27 template convertiti a React Email0 impatto sul bundle di produzione
design

Email transazionali: completata la conversione a React Email — tutti e 28 i template, stesso stile ovunque

Convertiti gli ultimi 24 template rimasti (i primi 4 erano già stati fatti come campione ed approvati). Estratti due componenti condivisi in più per tenere lo stile coerente senza ricostruirlo ogni volta: InfoCard (il box con titolo+dettagli di viaggio/local/evento, usato in metà dei template) e ReasonBox (box ambra per le motivazioni di rifiuto — verifica, esperienza Local). Copertura completa: autenticazione (OTP email/telefono/reset password, verifica rifiutata), viaggi (creazione/modifica/completamento/cancellazione, richieste e inviti in entrata/uscita, "parte domani"/"parte oggi" degli scheduler), roadmap (elemento aggiunto/aggiornato), Local/esperienze (creata/approvata/rifiutata, prenotazione confermata, rimosso dall'evento, nuova recensione), più l'email interna di test SMTP. Verificati 4 template extra a campione (oltre ai 4 già verificati in precedenza) end-to-end contro il vero HandlebarsAdapter: nessun segnaposto rimasto non sostituito, tutti i valori dinamici presenti. Rimossa anche public/email/templates/html/, una seconda cartella di bozze morte da 18 file .html (non .hbs, mai referenziata da nessun codice) trovata ripulendo la prima.

28/28 template convertiti2 componenti condivisi in più8/28 verificati end-to-end contro l'adapter reale18 file di bozze morte rimossi
bug reale

Utente bannato: i ban permanenti sparivano dal profilo, la pagina ban Flutter non mostrava i dettagli, la chat ignorava il ban — e porting sul client web

Analizzata la gestione del ban lato backend e Flutter per portarla sul client web Next.js. Il meccanismo di base era corretto: AuthGuard/AdminGuard rileggono il ban a ogni richiesta e rispondono HTTP 450 con i dettagli, e l'interceptor Dio porta a /ban. Trovati però cinque difetti reali. (1) users-get-by-id.use-case.ts cercava il ban con expiresAt > now, un confronto che esclude le righe con expiresAt null — cioè proprio i ban permanenti: il profilo di chi era bannato per sempre appariva normale. Estratta una regola unica (isBanActive/activeBanWhere in user-banned.exception.ts) e corretto il DTO (expiresAt: string | null); lato Flutter UserBanDto.expiresAt è diventato nullable, altrimenti il primo ban permanente avrebbe fatto fallire il parsing del profilo. (2) La pagina ban Flutter leggeva motivo e date dalla radice del body 450 invece che dal campo ban: mostrava sempre "Non specificato" e date vuote. (3) Il gateway /chat non controllava il ban: un utente bannato con il socket aperto poteva continuare a scrivere. Ora viene rifiutato alla connessione. (4) Ban e revoca non avevano effetto finché il client non faceva un'altra chiamata HTTP. (5) Trovato provando il ban dal vivo, ed era la causa più a monte: il filtro globale AllExceptionsFilter ricostruiva ogni risposta d'errore con soli messaggio e codice, scartando il campo ban — nessun client aveva mai ricevuto i dettagli del ban, e il client web non poteva riconoscere il 450. Ora il filtro li inoltra (senza l'email dell'admin, che non va esposta) e registra il 450 come warning su una riga invece che come errore con stack trace a ogni chiamata.

Per il punto 4, SessionGateway emette user_banned (stesso payload della risposta 450) e user_unbanned; al ban le connessioni chat dell'utente vengono chiuse solo sul namespace /chat e non sull'intera connessione, che il client condivide con /session, da cui deve poi arrivare la revoca. Sul client web: il middleware di api-client intercetta ogni 450, il ban vive nello store di sessione (persistito, sopravvive al ricaricamento), un unico BanGuard porta a /ban da qualunque pagina, e la sessione resta aperta — il login non controlla il ban, quindi un logout forzato non proteggerebbe nulla — con la pagina che si chiude da sola quando il ban viene revocato o scade. Lì "Contatta supporto" apre il Centro assistenza; su Flutter resta un segnaposto.

5 bug reali corretti2 eventi socket nuovi0 endpoint nuovi (Swagger invariato)tsc nest: 0 errori · dart analyze: 0 issue
bug reale

Blocco e follow, segnalazioni con stato, verifica account e "diventa local" sul client web

Blocco. Si poteva continuare a seguire un utente bloccato. Il trigger che avrebbe dovuto evitarlo (prisma/trigger_blocker.sql) esisteva, ma nessuno lo applicava al database. Il nuovo public/sql/trigger_blocker.sql, rieseguibile senza effetti collaterali, fa tre cose: al blocco cancella i follow in entrambe le direzioni e le notifiche di follow tra i due; impedisce un nuovo follow finché il blocco esiste (così non nasce nemmeno la chat privata creata dal trigger del follow); ripulisce i follow già rimasti tra utenti bloccati. Va eseguito a mano, perché Prisma non gestisce i trigger.

Segnalazioni. Le segnalazioni utente finivano già in UserWarn, con lo stato gestito dagli admin, ma chi segnalava non poteva vederle. Nuovo GET /user/warn: restituisce le proprie segnalazioni con stato e date, senza l'admin che le ha gestite. Sul client web, Impostazioni → "Le mie segnalazioni" mostra l'elenco e il dettaglio al clic. Bug trovato di passaggio: POST /user/warn/:userId, l'endpoint che usa Flutter, leggeva title/reason mentre Flutter manda reason/description, quindi l'inserimento falliva. In più aveva invertiti chi segnala e chi è segnalato: una volta corretti i campi, il segnalato avrebbe visto nella sua lista il nome di chi l'aveva segnalato. Corretti entrambi.

Verifica account e diventa local. Sul client web mancavano entrambe le voci del menu. La verifica d'identità non esisteva proprio: /verify è la verifica OTP dell'onboarding. Nuova pagina /profile/verify con stato, motivo del rifiuto, storico e i 5 passi di Flutter (tipo documento, fronte, retro, selfie, riepilogo). Formato e peso delle foto sono controllati nel client, perché il backend non li filtra. Una foto mancante restituiva 200 con un messaggio e sembrava un invio riuscito: ora il client la tratta come errore. "Diventa local" apre ora la candidatura anche al TRAVELER: non chiede di nuovo i dati personali e avvisa prima dell'invio che il ruolo diventa PENDING_LOCAL, con l'app da traveler bloccata fino all'approvazione.

2 trigger SQL1 endpoint nuovo (GET /user/warn)1 endpoint Flutter corretto2 pagine/sezioni web nuovetsc nest · tsc web · eslint: 0 errori
frontend

Scheda di un'esperienza Local sul client web: era un segnaposto "in arrivo"

Le card delle esperienze nel Gate e sul profilo portano a /locals/:id, ma quella rotta mostrava solo "in arrivo". Ora è la scheda vera, portata da LocalDetailsPage e LocalCalendarPage di Flutter. Contiene intestazione (targa DHONKO o SPECIAL, destinazione, durata, livelli, dimensione del gruppo), racconto, cosa è incluso, tipo di esperienza e lingue, prezzi della programmazione con sconti di gruppo, requisiti, termine di prenotazione e politica di cancellazione, e l'organizzatore. In Flutter il calendario è una seconda pagina; qui sta nella scheda: griglia del mese con i giorni che hanno date, e ogni orario apre il dettaglio della singola data (/gate/local/events/:id, già portato). Nessun endpoint nuovo: usa GET /user/local/:localId e /calendar. Un'esperienza non approvata (403) o inesistente (404) mostra un messaggio dedicato. Restano da portare le due azioni dell'organizzatore che in Flutter partono da qui: modifica dell'esperienza e annullamento di più date insieme.

1 pagina web da segnaposto a funzionante0 endpoint nuovi (Swagger invariato)tsc web · eslint: 0 errori
infrastruttura

Seed di simulazione: 100 utenti che possono fare login e almeno 100 righe in ogni tabella

Nuovo script npm run seed:simulation (src/scripts/seed-simulation/) che popola il database locale con dati coerenti su cui simulare l'uso reale dell'app. Crea 100 utenti con tutti i ruoli (5 admin, 2 Dhonko, 25 local, 5 local in attesa, registrati, non verificati e 58 traveler). Sono registrati anche su Firebase Auth, perché il login del backend chiede a Firebase un custom token e senza quello il client web blocca l'accesso. Hanno tutti la stessa password e un'email su un dominio riservato (@dhonko-seed.example.com), così le email automatiche del login non arrivano a caselle reali.

Intorno agli utenti: follow e amicizie, blocchi, recensioni, segnalazioni, un ban per utente (16 attivi, permanenti o temporanei, gli altri scaduti), cronologia ricerche, impostazioni, verifiche e candidature local. Poi 100 esperienze local, il 40% intorno a Roma così "Vicino a te" trova eventi, con 400 date, partecipanti e pagamenti; 100 viaggi in tutti gli stati con partecipanti, programma votato, eventi proposti e Book fotografico; chat di viaggio, di evento e dirette con messaggi, risposte, reazioni e segnalazioni; notifiche di ogni tipo, log e broadcast admin. Una parte dei dati coinvolge anche gli utenti reali già presenti (follower, chat, notifiche).

Il seed tocca solo i propri dati e si può rilanciare: prima cancella il seed precedente, riconoscendolo dagli utenti di quel dominio e da un marcatore nei campi JSON. Con --purge cancella soltanto, account Firebase compresi; con --no-firebase non tocca Firebase. Si rifiuta di girare se DATABASE_URL non punta a un database locale. Limiti noti: i pagamenti hanno PaymentIntent Stripe finti, quindi un rimborso reale su Stripe fallisce; i token FCM sono finti e il processor li elimina al primo invio.

100 utenti anche su Firebase Auth44 tabelle ≥ 100 righelogin verificato via APIrilanciabile e rimovibile con --purge
bug reale

Dettaglio viaggio completo sul client web: gestione del gruppo, Travel Match, programma — e i bug trovati nel backend

Gestione del gruppo. Sul client web sono arrivate le pagine partecipanti (ricerca, filtri per ruolo, rimozione, "Chiudi stato viaggio"), richieste di partecipazione, inviti con selezione multipla, Travel Match con le carte da scorrere, impostazioni del match e modifica del viaggio. Nel backend: la modifica del viaggio ricostruiva le foto solo da quelle caricate nella richiesta, quindi ogni modifica senza nuove foto le cancellava tutte; rispondeva sempre { success: true } anche quando falliva; e non controllava chi la chiedeva — chiunque poteva riscrivere qualsiasi viaggio. Stessa assenza di controllo su PUT /user/match/:id. Ora solo creatore o coordinatore, gli errori arrivano al client e le foto esistenti restano (con keepPhotos per scegliere quali). Eliminare un viaggio rispondeva sempre 500, anche da Flutter: le relazioni verso Travel non sono in cascata e c'è sempre almeno il creatore tra i partecipanti; ora programma, voti, partecipanti, salvataggi e revisioni si eliminano nella stessa transazione, e la chat di gruppo resta ma scollegata. Su Flutter restano tre difetti: le Impostazioni Match mostrano "aggiornato" senza chiamare nessuna API, la modifica manda tipi di viaggio e compagni con nomi che il backend non legge, e i posti cercati ripartono da quelli liberi.

Programma del viaggio. Portati dettaglio, voto, elenco dei votanti, creazione e modifica di spostamenti, pernottamenti e attività, e la scelta delle esperienze locali da proporre. Bug del backend corretti: modificare uno spostamento falliva sempre (gli orari "HH:MM" diventavano una data non valida); gli orari si salvavano nel fuso del server e tornavano spostati; una proposta senza data (facoltativa nel modulo, obbligatoria nel database) e un'attività senza orari fallivano con 500; il link di uno spostamento veniva ignorato in creazione; il dettaglio di un'attività non restituiva gli orari. Nessun permesso su voti, modifiche e dettagli — bastava conoscere un id — e le eliminazioni cancellavano i voti prima ancora di controllare chi chiedeva. Ora le regole sono in un punto solo (schedule/schedule-access.ts): leggere, proporre e votare solo creatore e partecipanti confermati, modificare ed eliminare chi ha proposto o un coordinatore. Su Flutter la modifica di pernottamenti e attività chiama rotte che non esistono, e le traduzioni dei tipi di attività usano ancora i valori di un enum precedente. Sul web, quando il programma è vuoto le sezioni restano visibili con "Proponi": in Flutter la prima proposta si poteva fare solo scegliendo prima un filtro di sezione.

11 pagine web nuove2 bug che rendevano inutilizzabili modifica spostamento ed eliminazione viaggio17 endpoint senza controllo dei permessi, ora protetti (403 verificato su 6)0 endpoint nuovi (Swagger invariato)tsc nest · tsc web · eslint: 0 errori · E2E Playwright
ruolo admin

Pannello admin sul client web

Portate sotto /admin le aree del pannello di Flutter, ciascuna con la sua pagina di dettaglio: utenti, verifiche identità, local in attesa, viaggi, esperienze, segnalazioni, pagamenti, recensioni, moderazione chat, log attività, notifiche broadcast, configurazione e contenuti Gate. Nel backend, a supporto: il rimborso admin passa davvero da Stripe; l'eliminazione di un viaggio moderato elimina in una transazione anche le righe collegate, TravelReview compresa; nuova POST /admin/chat/reported/:reportId/resolve per chiudere una segnalazione di chat; il dettaglio utente porta lo storico delle verifiche identità e quello del viaggio le decisioni di revisione.

13 aree + dettagli1 endpoint admin nuovotsc nest · tsc web · eslint: 0 errori
sicurezza

Home dell'host sul web, proposte di esperienze — e una chat da cui ci si può difendere

Home dell'host. Un local, entrando, vedeva il feed dei viaggi del traveler (anche su Flutter), pur non potendo partecipare a nessun viaggio. Ora la sua navigazione è Dashboard, Calendario, Viaggi, Gate, Chat. La dashboard mette insieme incassi, cose da gestire, prossime date ed esperienze (GET /user/local/my/dashboard); il calendario mostra tutte le date di tutte le esperienze (GET /user/local/my/events) e ne annulla più insieme; "Viaggi" elenca quelli aperti che passano vicino alle date dell'host (GET /user/local/my/nearby-travels). Le regole "entro 50 km e nei giorni del viaggio" stanno ora in un punto solo (common/utils/distance.ts), usato sia dalla lista delle esperienze proponibili sia dal controllo della proposta.

Proposte. L'host può proporre una sua data al programma di un viaggio senza farne parte (prima potevano solo partecipanti e creatore), o a una persona in chat con POST /user/local/event/:eventId/propose-to/:userId: il messaggio resta testo leggibile da qualunque client, con il percorso dell'evento, e il web lo mostra come card. I local non possono più essere invitati, chiedere di partecipare, confermare un invito o essere accettati: travel-invite.use-case.ts accettava qualunque utente e la ricerca "Invita compagni" mostrava anche i local.

Sicurezza della chat. Tre buchi trovati progettando le proposte, perché un host può scrivere a chiunque. (1) Il blocco non fermava la chat: un utente bloccato poteva ancora aprire una chat diretta e scrivere. Ora apertura e invio (REST e socket) rispondono 403 USER_BLOCKED, e l'errore del socket porta il codice. (2) Nessuna rotta lato utente creava un ChatReport, quindi la coda admin di moderazione chat restava vuota: nuova POST /user/chat/:chatId/report, con "Segnala messaggio" e "Segnala conversazione" sul web. (3) Non c'era modo di scegliere da chi ricevere messaggi: nuovo modello UserPrivacySettings (unica modifica allo schema, migrazione 20260915090000_add_user_privacy_settings) con "Chi può scrivermi" e "Proposte dai local", in Impostazioni. Nel dettaglio viaggio, su web e Flutter, l'ingresso alla chat di gruppo (card e icona) si vede solo a chi partecipa o coordina, e su Flutter il local non vede più "Chiedi di partecipare". Visto ma non corretto: la richiesta diretta di partecipazione a una data conferma senza pagamento e ha il controllo dei posti commentato.

6 endpoint nuovi1 modello + 1 enum + 1 migrazione4 use-case viaggio con controllo del ruolo3 buchi di sicurezza in chat chiusitsc nest · tsc core · tsc web · eslint: 0 errori
infrastruttura

Account local di prova per la home dell'host

Nuovo npm run seed:local-demo (src/scripts/seed-simulation/local-demo.ts): crea sul database locale un local con cui fare login (demo.local@dhonko-seed.example.com) e un traveler (demo.traveler@…), stessa password del seed di simulazione, più 24 ospiti. Il local ha 13 esperienze: approvate tra Roma, Castelli Romani, Tivoli, Ostia, Firenze e Napoli, una in revisione, una rifiutata con il motivo nel registro admin, una approvata senza date e una rimossa. Le date coprono gli ultimi 75 giorni e i prossimi 150 (circa 950), con iscritti, pagamenti anche rimborsati, alcune date imminenti sotto il minimo, una in corso e 14 recensioni. Ci sono 11 viaggi: 7 passano vicino alle sue date (su 3 c'è già una sua proposta in attesa, accettata o rifiutata), 4 servono a verificare le esclusioni (fuori raggio, in revisione, oltre le date, concluso).

Tocca solo gli utenti demo.*: purgeSeedData accetta ora un prefisso email, e la pulizia elimina anche le voci del registro admin legate a esperienze e viaggi del seed. Con --purge cancella soltanto; il --purge del seed di simulazione, che usa lo stesso dominio, cancella anche questi account.

2 account con login + 24 ospiti13 esperienze · ~950 date11 viaggi, 7 vicinirilanciabile e rimovibile con --purge
esperienze

Creare un'esperienza e le sue date: evento unico, date a scelta, ricorrente

Il problema. Esisteva un solo modo di programmare: una settimana tipo, con un unico orario di riferimento per regola. "Evento speciale" era solo un'etichetta, e il concerto di una sera veniva registrato come una ricorrenza. Le date nascevano per 90 giorni e poi nessuno ne creava altre: un lido "tutta la stagione" si fermava lì. Alla creazione nasceva anche una chat di gruppo per ogni data, centinaia in una volta. Gli orari erano calcolati nel fuso del server, e lo stato delle date ("in corso", "conclusa") non lo aggiornava nessun job.

Adesso. LocalSchedule ha un tipo (LocalScheduleKind: SINGLE, DATES, WEEKLY), l'orario di fine, i giorni esclusi e generatedUntil. Ogni regola è una fascia oraria. La logica sta in un punto solo, puro e senza database (local/schedule/schedule-plan.ts): la usano la creazione, le nuove rotte /user/local/:localId/schedules e /user/local/schedules/:scheduleId e il job LocalEventsScheduler. Il job ogni 10 minuti aggiorna gli stati e tiene pronte le date dei prossimi 120 giorni, così una ricorrenza può non avere fine. Giorni e orari sono sempre nel fuso di Roma (common/utils/zoned-time.ts). La logica è stata provata sui casi reali, cambio dell'ora legale compreso: concerto (1 data), partite in casa (3 date), bar ogni venerdì di ottobre dalle 21 alle 2 (5 date che finiscono il giorno dopo), lido lun–ven senza fine con un festivo escluso (85 date), scuola il lunedì in quattro fasce (4 regole da 14 date). Modificare o fermare una programmazione tocca solo le date senza iscritti, pagamenti, proposte o chat. La chat di una data nasce quando serve. Il formato di Flutter resta accettato com'era.

Client web. Nuova pagina /locals/create, da "Crea esperienza" nella dashboard: le sezioni della procedura Flutter su una pagina, con l'editor delle tre modalità e l'anteprima delle date. Nella scheda dell'esperienza il creatore gestisce la programmazione: aggiunge date, cambia una ricorrenza, ferma, cambia posti e prezzi. Da applicare: npx prisma migrate deploy e npx prisma generate a server fermo.

1 enum + 4 campi + 1 migrazione4 rotte nuove1 job nuovo5 casi reali verificati