Changelog
Tutte le versioni rilasciate di Koru.
# Changelog
## [0.3.5] - 2026-07-13
### Changed
- Secret di deploy dal vault CloseYourIt (`SECRETS_JSON`) invece di Doppler→GitHub Sync. Transizione zero-downtime con fallback.
## [0.3.4] - 2026-07-12
### Changed
- Deploy SSH via certificato effimero step-ca (OIDC) invece della deploy-key statica (SSH-CA).
## [0.3.3] - 2026-07-12
### Fixed
- Il bundle minimo `KAMAL_SECRETS_JSON` sincronizzato negli environment GitHub staging e production
include ora anche `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` e `AWS_S3_BUCKET`, così Active
Storage può inizializzare il servizio S3 al boot del container.
## [0.3.2] - 2026-07-12
### Fixed
- I mapping dei secret AWS S3 sono ora in `.kamal/secrets-common`, caricato sia dalla destinazione
staging sia da quella predefinita. La release `v0.3.1` li dichiarava soltanto in
`.kamal/secrets`, che Kamal non legge durante un deploy con `-d staging`.
## [0.3.1] - 2026-07-12
### Fixed
- Il deploy materializza ora nel container le credenziali e il bucket AWS S3 già forniti dagli
environment secrets GitHub. `v0.3.0` completava build e test ma si fermava prima del boot staging
perché le variabili `AWS_*` non erano dichiarate in `.kamal/secrets`.
## [0.3.0] - 2026-07-12
### Added
- **Programmi settimanali dei pasti su WhatsApp**: koru raccoglie soltanto i requisiti essenziali,
riusa nucleo, gusti e vincoli alimentari già conosciuti e salva versioni immutabili del piano con
lista della spesa aggregata. Ogni versione propone esplicitamente la generazione del PDF.
- **PDF persistenti nello stile Koru**: dopo il consenso, un job genera il documento A4, lo conserva
in Active Storage su bucket AWS S3 privato per ambiente e lo invia come documento WhatsApp tramite
OpenWA. Generazione e invio sono idempotenti e ritentabili.
- **Infrastruttura AWS isolata**: account AWS dedicato, IAM applicativo a privilegi minimi e bucket
development/staging/production separati in `eu-central-1`, con versioning, cifratura SSE-S3,
blocco accesso pubblico e lifecycle delle versioni non correnti.
### Changed
- Lo staging conserva un solo container precedente e usa Solid Queue dentro Puma per ridurre il
consumo di memoria, mantenendo invariato il flusso di deploy tramite GitHub Actions.
- Aggiornate le dipendenze transitive `crass`, `css_parser` e `websocket-driver` alle versioni che
correggono gli advisory di sicurezza correnti.
## [0.2.1] - 2026-07-11
### Fixed
- Il follow-up su una proposta ristorante ("altri", fascia diversa, atmosfera o occasione speciale)
raffina ora la stessa richiesta senza il falso conflitto "prenotazione in corso". I locali già
mostrati vengono esclusi dalle proposte successive; fascia alta ed eleganza applicano filtri
effettivi, con Google Text Search per prezzo/rilevanza. Se nessun locale è compatibile, koru torna
in uno stato modificabile e avvisa l'utente invece di restare bloccato in ricerca.
## [0.2.0] - 2026-07-11
### Added
- **Preferenze strutturate per la ricerca ristoranti**: fascia di prezzo, cucina e atmosfera vengono
salvate nella richiesta e usate per filtrare e ordinare i candidati. Google Places fornisce ora
tipo primario, prezzo, stato operativo e volume recensioni; le proposte mostrano prezzo, rating e
recensioni quando disponibili.
### Fixed
- **Flusso di ricerca più coerente**: koru raccoglie tutti i requisiti prima dell'unico riepilogo,
non chiede più conferme parziali e sospende le domande di profilazione durante una prenotazione.
Il ranking esclude risultati incompatibili, come strutture ricettive, attività chiuse o fasce di
prezzo non richieste, e penalizza i locali informali quando viene chiesto un ambiente elegante.
## [0.1.1] - 2026-07-11
### Fixed
- **koru non resta più muto sulle richieste di prenotazione ristorante** (KORU-19). Quando l'utente
scriveva su WhatsApp una richiesta come «Volevo prenotare un ristorante, puoi aiutarmi?», koru non
rispondeva: su un turno in cui l'assistente eseguiva solo un'azione interna "silenziosa" (es. la
classificazione dell'intento prenotazione) senza produrre testo, non veniva inviato alcun messaggio
e — a differenza dei turni senza azioni — mancava un fallback. Il saluto «Ciao» funzionava perché ha
una risposta predefinita. Ora il turno viene risolto con un **loop agentico**: dopo l'esecuzione delle
azioni, se non c'è testo da inviare, koru ri-chiama il modello nello stesso turno con i risultati per
generare la continuazione naturale (chiede i dettagli mancanti della prenotazione). Con un tetto di
sicurezza sui passaggi e un messaggio di guida di fallback: **koru non resta mai muto su un turno
compreso**, senza loop infiniti. Nessun cambiamento sugli altri flussi.
### Changed
- Stabilizzati i confini dei test di rate-limiting (Rack::Attack) — nessun impatto funzionale.
## [0.1.0] - 2026-07-11
### Added
- **Dashboard member operativa**: `/member` mostra le liste dell'account con anteprima e stato vuoto
guidato, i prossimi tre promemoria e le prossime tre prenotazioni confermate. Ogni sezione senza
dati propone un'azione contestuale via WhatsApp con messaggio precompilato; il pannello di gestione
WhatsApp esistente resta disponibile in forma compatta.
- **Creazione e consultazione liste dal web**: nuovo flusso autenticato
`/member/lists/new` → `/member/lists/:id`, con nome, tipo ed elementi dinamici (nome, quantità,
nota). Lista ed elementi sono creati atomicamente riusando il dominio `Lists::`; il dettaglio è
read-only e ogni lookup è limitato a `Current.account`. UI responsive IT/EN, Stimulus e copertura
request/system inclusa.
## [0.0.45] - 2026-07-11
### Fixed
- Rilascio in **produzione** delle fix di v0.0.44 (robustezza click-to-chat: deep-link stabile, relink
protetto, LID→numero reale, telefono nel messaggio). Il deploy prod di v0.0.44 era stato skippato da
un bug della pipeline CI riutilizzabile (il gate di produzione ereditava lo skip di un job opzionale),
ora risolto lato `ci-templates`. **Nessuna modifica al codice applicativo** rispetto a v0.0.44.
## [0.0.44] - 2026-07-11
### Fixed
- **Robustezza collegamento WhatsApp click-to-chat** (emerso dal test dal vivo):
- Il codice del deep-link non viene più rigenerato a ogni refresh della pagina: un token valido
pendente viene riusato, così un codice appena ottenuto resta valido per tutta la sua finestra
(prima un refresh lo invalidava silenziosamente).
- Il tasto "Ricordami chi sei" / "Cambia numero" ora chiede **conferma** prima di scollegare il
numero, evitando lo scollegamento per un click accidentale.
- Riconoscimento affidabile quando WhatsApp identifica il mittente con un **LID** (`@lid`, modalità
privacy) invece che col numero: l'inbound risolve il LID al numero reale via l'API contatti OpenWA
(con cache e fallback), così il collegamento regge sia che il messaggio arrivi come `@lid` sia come
numero. Migrazione dei collegamenti esistenti (`bin/rails openwa:migrate_lid_phones`).
- Rimosso il numero dal testo precompilato del messaggio (era il numero *dichiarato*, opzionale e a
volte vuoto: "il mio numero è ⎵⎵"); il numero reale è quello del mittente.
## [0.0.43] - 2026-07-11
### Changed
- **Messaggio WhatsApp click-to-chat più amichevole**: il deep-link `wa.me` dell'onboarding (step
whatsapp) e del cambio numero (area Member) ora precompila un messaggio in prima persona
("Ciao Koru! Sono <nome>, il mio numero è <numero> e questo è il codice di sicurezza per collegarlo:
<codice>") invece del token grezzo. Il codice di verifica passa al formato leggibile
`KORU-XXXXX-XXXXX` (Crockford base32) e viene riconosciuto ovunque nel testo — anche in fondo alla
frase — grazie a un'estrazione mirata per pattern (prima un codice in coda a una frase lunga non
veniva riconosciuto). Encoding del deep-link corretto (`%20` invece di `+`). Verifica e collegamento
del numero invariati (il numero resta quello del mittente del messaggio).
## [0.0.42] - 2026-07-10
### Added
- **Footer nelle aree loggate** (member + valhalla): link Privacy/Cookie/Termini, bottone "Gestisci
cookie" (riapre le preferenze del consenso) e numero di versione del sistema. Cliccando la versione
si apre una modale con le ultime 5 versioni rilasciate, estratte dal CHANGELOG tramite il nuovo
parser `App::Changelog` (nessuna gem markdown). La versione mostrata è `App::Version.tag`, con
fallback all'ultima versione del CHANGELOG in dev/staging (dove il tag di build è `unknown`). Nuovo
`Ui::AppFooterComponent` + controller Stimulus `versions`, i18n IT/EN. Il controller `cookie-consent`
è ora montato anche nei layout loggati (necessario per "Gestisci cookie").
## [0.0.41] - 2026-07-10
### Changed
- Bump `closeyourit-ruby` 0.3.2 → 0.4.0 (#22) e `thruster` 0.1.21 → 0.1.22 (#23) — aggiornamenti Dependabot già mergiati su `main`, non inclusi in v0.0.40.
## [0.0.40] - 2026-07-10
### Added
- **Profilazione conversazionale**: dopo l'onboarding koru raccoglie in chat il profilo dell'utente
— genere, compleanno, interesse prenotazioni, allergie/dieta PROPRIE (chiuso il gap "self" di
HouseholdMember via `Connections::AccountDietaryConstraint`), gusti culinari, animali, note e
rubrica numeri utili (`Phonebook::Entry`, telefono cifrato; recupero in chat: "dammi il numero del
veterinario"). Due regie configurabili da Valhalla (`/valhalla/profiles/setting` + registro campi):
**intervista diretta** (una domanda per turno, skip rispettato per sempre) e **contestuale**
(prima prenotazione → chiede allergie/dieta) con **fallback proattivo a tempo**
(`Profiles::AskDueJob` orario: slot mai risposti oltre soglia → messaggio WhatsApp composto da
Gemini col giorno della settimana, fallback template i18n; finestra oraria e cap settimanale).
11 tool LLM nuovi. I vincoli del profilo alimentano il contesto LLM e le chiamate vocali
(`gluten_free`/`dietary_notes` ora includono i vincoli self e dei membri collegati).
- **Inviti famiglia (nucleo multi-account)**: tool `invite_family_member` → email con link
`/invitations/:token` (scadenza 14gg); alla registrazione l'account viene collegato al membro del
nucleo dell'invitante (`accounts_household_members.member_account_id`) e i suoi vincoli alimentari
si leggono dal suo profilo.
### Removed
- **Messaging WhatsApp: solo OpenWA** (Twilio e Meta rimossi). OpenWA era già il provider di default
(priority 1): ora è l'**unico** provider della categoria `messaging` — niente più fallback Twilio né
lo stub Meta Cloud. Rimossi `Providers::Messaging::TwilioAdapter`, le rotte webhook `whatsapp/twilio`
e `whatsapp/meta`, le firme `X-Twilio-Signature`/`X-Hub-Signature-256`, le ENV `TWILIO_*` e
`META_APP_SECRET`. La migration `RemoveObsoleteMessagingProviders` elimina dal DB le
`Providers::Configuration` messaging obsolete (Twilio/Meta/Vonage). Twilio **resta solo per la voce**
(numero importato in ElevenLabs; Rails non legge `TWILIO_*` per la voce). Messaging ora single-provider
→ nessun failover (SPOF consapevole, outbound best-effort). Follow-up ops: rimuovere `TWILIO_*` e
`META_APP_SECRET` da Doppler `koru-rails` (3 config).
## [0.0.39] - 2026-07-02
### Added
- **Liste generiche via WhatsApp (dominio `Lists::`)**: oltre alla spesa, koru gestisce liste di
faccende e liste personalizzate (regali, valigia…). Più liste per account, nome unico
case-insensitive, `kind` (shopping/chores/custom) che guida copy ("comprato" vs "fatto") ed
emoji (🛒/🧹/📋). 8 tool LLM (`create_list`, `show_lists`, `add_list_items`, `show_list`,
`check_list_item`, `remove_list_item`, `clear_list`, `delete_list`) → `ListsHandler` → service
`Lists::{Resolve,Create,AddItems,ListItems,MarkDone,RemoveItem,Clear,Delete,Overview}`.
Risoluzione lista per nome o tipo ("spesa"/"faccende"); spesa e faccende auto-create al primo
elemento. Su "crea una lista" senza tipo, koru chiede e propone spesa/faccende/custom (system
prompt); `welcome`/`help`/`guidance_after_failures` ora presentano prenotazioni + liste +
promemoria (prima citavano solo i ristoranti). Codici errore dominio `LISTS`, i18n IT/EN.
### Changed
- **Informative cookie/privacy aggiornate a CloseYourIt**: le pagine legali (IT+EN) e la modale
preferenze citavano ancora Plausible Analytics; ora descrivono CloseYourIt Analytics
(self-hosted, cookieless, hash giornaliero irreversibile). Nessun cambio di comportamento.
- **Lista della spesa migrata nel dominio generico**: i 5 tool `*_shopping_*` e il dominio
`Shopping::` sono rimossi; i dati esistenti migrano in `lists_lists`/`lists_items` (lista
"Spesa", kind `shopping`, status `bought`→`done`) con la migration `MigrateShoppingIntoLists`.
### Fixed
- **Turni Gemini a vuoto ("Non ho capito bene" a caso)**: gemini-2.5-flash a volte risponde con un
candidate senza parts (zero token, glitch upstream input-dipendente). `GeminiAdapter#complete`
ora imposta `temperature` (0.2 al primo colpo, tool calling ripetibile) e ritenta fino a 3 volte
a temperatura crescente (0.2→0.7→1.0) quando la risposta è vuota. Eval `bin/conversation_eval`:
8/8 scenari PASS (prima oscillava 4-7/8).
## [0.0.38] - 2026-07-02
### Changed
- **Web analytics: da Plausible a CloseYourIt** (gating consenso invariato): lo script gated del
banner cookie ora carica l'SDK `@bussolabs/closeyourit-js@0.2.0` da CDN (pinned + SRI) e su
consenso "analytics" chiama `init` con `trackPageviews: true` (pageview cookieless). Nuove ENV
`CLOSEYOURIT_BROWSER_TOKEN` (token ingest dedicato al browser) in deploy.yml/staging + Doppler;
rimosse `PLAUSIBLE_DOMAIN`/`PLAUSIBLE_SCRIPT_SRC`.
## [0.0.37] - 2026-07-01
### Fixed
- **Onboarding WhatsApp — webhook Twilio 500 in produzione**: `db/cable_schema.rb` era **vuoto** →
la tabella `solid_cable_messages` non veniva creata (prod usa `adapter: solid_cable` su SQLite; dev
usa `async` → bug invisibile in dev). Il broadcast Turbo Stream di verifica del link
(`Accounts::VerifyWhatsappLinkService`) falliva con `ArgumentError (No unique index found for id)` →
webhook 500 → onboarding bloccato ("in attesa"). Popolato `cable_schema.rb` con la tabella +
broadcast reso resiliente (un guasto ActionCable non fa più fallire la verifica del link).
## [0.0.36] - 2026-07-01
### Changed
- **Numero WhatsApp koru → `+15559113280`** (`KORU_WHATSAPP_NUMBER=15559113280`): il deep-link
onboarding (`/member/whatsapp_link`) ora punta al numero Twilio corrente (prima puntava al vecchio
numero OpenWA `393204916564`). Redeploy per ri-catturare l'env aggiornato su staging + produzione.
## [0.0.35] - 2026-07-01
### Changed
- **Twilio WhatsApp su staging + produzione**: credenziali + `TWILIO_WHATSAPP_FROM` configurate su
Doppler (staging/production) e cablate nel deploy (`.kamal/secrets-common`, `deploy.yml`,
`deploy.staging.yml`). Il registry seleziona `twilio` come provider messaging quando `openwa` non è
eleggibile.
## [0.0.34] - 2026-07-01
### Added
- **Lista della spesa conversazionale** (dominio `Shopping::`): 5 tool LLM
(`add_shopping_items`/`list_shopping_items`/`remove_shopping_item`/`mark_shopping_item_bought`/
`clear_shopping_list`) — l'utente gestisce la lista via chat.
- **Promemoria personali** (dominio `Reminders::`): 3 tool (`create_reminder`/`list_reminders`/
`cancel_reminder`), notifiche su **WhatsApp + email** (`Reminders::ReminderMailer`), job schedulato
`Reminders::DispatchDueJob` (ogni minuto) che consegna i promemoria scaduti.
- **Allergie**: conferma esplicita all'utente al salvataggio del vincolo (`save_household_constraint`).
### Changed
- **Function calling multi-turno**: `ContextBuilder` + `GeminiAdapter` ora passano a Gemini lo storico
con `functionCall`/`functionResponse` → nella stessa conversazione l'LLM non ripete più il primo tool.
- **RunLlmTurnService**: la reply deterministica del tool prevale sul testo dell'LLM (i tool "display"
mostrano il contenuto reale, es. la lista).
- **error_codes.yml**: registrati i domini `SHOPPING` e `REMINDERS`.
## [0.0.32] - 2026-06-29
### Changed
- **Monitoring**: sostituito Sentry/GlitchTip con CloseYourIt (`closeyourit-ruby`) — error tracking,
metriche (slow query/method) e log strutturati verso https://www.closeyour.it (progetto `KORA`).
PII scrubbing preservato (`ErrorTracking::PiiScrubber`).
- **Ruby**: bump da 3.3.10 a 4.0.5.
### Added
- **Onboarding step WhatsApp — numero dichiarato (fase 1)**: l'utente dichiara il proprio numero
nello step whatsapp (`Accounts::Account#phone` via migration `add_phone_to_accounts`,
`Onboarding::SavePhoneService`). Fase 1 non avanza lo step (resta al token-match del webhook); la
fase 2 (deep-link click-to-chat) parte quando il numero è presente. Àncora identità per account LID.
### Security
- **`Accounts::Account#phone` cifrato at-rest** (GDPR/PII): `encrypts :phone` non-deterministic,
parità col phone già cifrato su `accounts_whatsapp_links`. Test di parità in `pii_encryption_spec`.
## [0.0.31] - 2026-06-25
### Changed
- **Aggiornamento dipendenze**: sentry-ruby e sentry-rails 6.5.0 → 6.6.2, kamal 2.11.0 → 2.12.0,
view_component 4.11.0 → 4.12.0, tailwindcss-rails 4.4.0 → 4.6.0, resend 1.3.0 → 1.5.0,
bootsnap 1.24.5 → 1.24.6, sqlite3 2.9.4 → 2.9.5, selenium-webdriver 4.44.0 → 4.45.0 (dev).
### Security
- **net-imap 0.6.4 → 0.6.4.1**: corregge CVE-2026-47242 (command injection via ID command,
Medium) e CVE-2026-47241 (DoS, Low).
### Removed
- **Alias job legacy** (`config/initializers/legacy_job_aliases.rb`): rimossi gli alias
vecchio-nome → nuovo-nome dei job (convenzione VerbNoun), non più necessari dopo i cicli di
deploy successivi a v0.0.24 — in coda non restano job serializzati col vecchio nome.
### Fixed
- **CI**: rimosso `--ensure-latest` dal binstub brakeman (rendeva `scan_ruby` flaky).
## [0.0.30] - 2026-06-14
### Changed
- **Pulsante waitlist gated**: "Voglio provare Koru" resta disabilitato finché nome ed email
sono compilati e il consenso privacy è spuntato (Stimulus, in tempo reale). Progressive
enhancement: senza JS il pulsante resta cliccabile e il server valida comunque.
## [0.0.29] - 2026-06-14
### Added
- **Scorciatoia Valhalla** nella navbar dell'area member: link "Valhalla" con icona coroncina
(`fa-crown`) accanto a "Esci", visibile **solo agli account god**.
## [0.0.28] - 2026-06-14
### Fixed
- **Login god rotto**: il seed impostava la password del god account solo alla prima creazione
(`find_or_create_by!`), quindi le rotazioni di `GOD_PASSWORD` non arrivavano mai al DB. Ora il
seed **riallinea la password a `GOD_PASSWORD` a ogni run/deploy** (no-op se già combacia).
## [0.0.27] - 2026-06-14
### Fixed
- **Logo badge mancante** nelle pagine di login/registrazione, onboarding, area member e
nell'avatar del mockup chat della landing: era rimasta la vecchia spirale. Ora tutte usano
`koru-badge.svg`.
## [0.0.26] - 2026-06-14
### Added
- **Valhalla — lista d'attesa**: pagina god-only `/valhalla/waitlist_subscriptions` con elenco
read-only degli iscritti (nome, email, lingua, data) + **export CSV**; link in sidebar admin.
### Security
- **CSV formula injection (CWE-1236)** nell'export waitlist Valhalla: le celle da input pubblico
(nome/email) vengono neutralizzate con un apice se iniziano con `= + - @` (o tab/CR).
### Changed
- **Nuovo logo badge koru** (anello viola, centro arancione, "koru") al posto della spirale:
nav, footer, pannello Valhalla; rigenerati favicon, apple-touch-icon, icon PWA e og-image.
- **Copy success waitlist**: "seguici su **Instagram** @HeyKoru" (con link a Instagram), it + en.
## [0.0.25] - 2026-06-14
### Changed
- **Landing pubblica → waitlist pre-lancio**: la home (`/`) passa da pagina prodotto
(signup/pricing/testimonials) a landing di raccolta iscrizioni alla lista d'attesa, con nuovo
design e testi. Sezioni: hero + form lista d'attesa, servizi (4 card), come funziona, demo chat,
CTA, footer. Rimossi pricing, testimonials e i CTA login/signup dalla landing (rotte auth attive
ma non linkate).
### Added
- **Lista d'attesa**: model `Waitlist::Subscription` (email + nome + consenso GDPR, unicità email),
endpoint `POST /waitlist` (`Website::WaitlistSubscriptionsController`, risposta Turbo Stream),
mail di benvenuto `WaitlistMailer#welcome` (localizzata), throttle rack-attack `/waitlist`
(5/ora per IP ed email).
### CI
- **brakeman 8.0.4 → 8.0.5**: sblocca il job `scan_ruby` (`bin/brakeman` forza `--ensure-latest`).
- **Notifiche deploy**: le email di esito rilascio di koru includono ora anche `info@elianasalvi.com`
e `hello@bussolarialessio.me` (template CI condiviso reso multi-destinatario).
## [0.0.24] - 2026-06-12
### Security
- **God account**: `GOD_PASSWORD` obbligatoria in produzione — il seed fallisce se assente (prima:
fallback silenzioso `password123` a ogni deploy). Secret impostata su Doppler nei 3 config.
- **Webhook ElevenLabs**: la firma HMAC richiede `ELEVENLABS_WEBHOOK_SECRET` dedicato — rimosso il
fallback sulla API key (scope troppo ampio per firmare webhook); secret assente → 401.
- `filter_parameters`: aggiunti `phone`, `address`, `authorization` (PII WhatsApp/GDPR nei log).
- **PII cifrata at-rest** (AR Encryption): `email`/`address` dell'account e `phone`/`chat_id` del
link WhatsApp (deterministic dove servono lookup/unique index) + migration di backfill
(`support_unencrypted_data` attivo in transizione).
- Rinumerate collisioni SEQ dei codici errore: `R422-AUTH-001→008`, `R422-SYSTEM-001→002`,
`R422-PROVIDERS-016→017`, `R429-SYSTEM-001→003`.
### Added
- **`config/error_codes.yml`**: registry centrale dei ~60 codici `R{STATUS}-{DOMINIO}-{SEQ}` con
spec drift-guard (codice usato ma non registrato = test rosso).
- **Mission Control (dashboard Solid Queue)** su `/valhalla/jobs`, god-only via
`Valhalla::BaseController` (basic auth della engine disabilitata).
- **`/changelog`** pubblico (rende `CHANGELOG.md`) + versione deployata linkata nel footer; alias
301 `/privacy-policy` → `/privacy`, `/cookie-policy` → `/cookies`, `/tos` → `/terms`.
- **Richieste speciali sulla prenotazione**: campo `special_requests` su `Bookings::Request` +
tool conversazionale `save_special_requests` — passate all'agente voce nelle `dynamic_variables`.
- **Invito WhatsApp con riconnessione**: copy con i due rami (nuovo utente / pairing perso) e link
reali (`/member`, sito) da `MAIL_HOST`; bottone "Ricordami chi sei!" nella dashboard member per
il re-pairing one-click. Invio invito ora **asincrono** (`Conversations::SendRegistrationInviteJob`)
fuori dal ciclo di risposta del webhook.
- Job di retention `Providers::PurgeHealthChecksJob` (storico heartbeat oltre 30 giorni) +
N+1 detection in development (prosopite + `strict_loading` in modalità log).
### Changed
- **Indici hot path**: `calls_calls(status,updated_at)` (reaper 5'), `conversations_messages
(conversation_id,created_at)` (ogni turno LLM), `bookings_bookings(status,booked_at)`,
`restaurants_restaurants(gluten_free_source)`; rimossi 2 indici ridondanti.
- **`Providers::HttpClient`**: connessione keep-alive per host + retry (solo GET/DELETE, errori
transitori) — prima: handshake TCP+TLS per ogni chiamata provider, nessun retry.
- **Heartbeat provider**: il fan-out rispetta `heartbeat_interval_seconds` (smoke test Gemini a
pagamento: da ~96 a ~8 al giorno per provider).
- Cache contatori dashboard Valhalla (TTL 1') e suggerimenti Places per query normalizzata (TTL 10').
- **Refactor architetturale**: business logic estratta da controller e job in 12 service dedicati;
`Tools::Dispatcher` splittato in handler per dominio (355→120 righe) con memoizzazione della
request attiva; `params.expect` (Rails 8.1); login timing-safe via `Account.authenticate_by`;
enum con `validate: true` (garbage = 422, non 500); `belongs_to` con opzioni esplicite; discovery
ristoranti via `upsert_all` (~80→2 query); rename Verb-first di 6 job e 6 service (alias di
compatibilità Solid Queue per i job già in coda, da rimuovere al prossimo ciclo) + utility
spostate in `app/lib`.
- **`Bookings::BookingRequest` → `Bookings::Request`** (no-stuttering, tabella `bookings_requests`)
+ fixup del discriminator polimorfico `calls_calls.subject_type` sui record storici.
- `rack_attack`: risposta 429 via i18n.
### Removed
- 6 gem inutilizzate: `jbuilder`, `image_processing`, `alba`, `twilio-ruby`, `geocoder`,
`googleauth`; dead code (job/adapter/view/chiavi i18n orfane); `credentials.yml.enc`/`master.key`
(progetto Doppler-only).
## [0.0.23] - 2026-06-09
### Fixed
- `before_send` Sentry: i breadcrumbs venivano iterati con `event.breadcrumbs&.values`, ma
`Sentry::BreadcrumbBuffer` espone l'array via `#buffer` (non `#values`) — il `NoMethodError` dentro
`before_send` faceva scartare a sentry-ruby l'INTERO evento in silenzio ("Event capturing failed").
Ora `Array(event.breadcrumbs&.buffer)`.
### Added
- **Log bridge `Rails.logger` → GlitchTip structured logs** (`ErrorTracking::SentryLogBridge`,
`lib/error_tracking/`): i log dell'app vengono inoltrati automaticamente ai structured logs, livello
minimo **WARN** di default (override via `SENTRY_LOGS_MIN_LEVEL`; `info` solo per debug temporaneo —
il volume INFO in produzione è nell'ordine dei milioni di righe/giorno; DEBUG sempre scartato).
- `ErrorTracking::PiiScrubber.scrub_text`: redazione best-effort di PII/secrets in testo libero
(email, bearer token, coppie `token=...`/`password: ...`) — usato dal log bridge prima dell'invio.
## [0.0.22] - 2026-06-09
### Changed
- **CI/deploy migrati al reusable workflow condiviso** `bussolabs/ci-templates/.github/workflows/rails.yml`:
`ci.yml` è ora un caller thin. Comportamento invariato (qualsiasi tag → staging; tag stabile →
produzione con e2e gate). Cache build Docker su registry nostro, idle-sleep Sablier su staging,
smoke `/up`+`/version` dopo ogni deploy.
- `bin/docker-entrypoint`: `db:prepare` (Rails 8 connect-first) — rimosso il tentativo `solid:prepare`.
## [0.0.21] - 2026-06-02
### Added
- Endpoint `GET /version` (top-level, no auth): espone tag/sha/build_time/environment della build
deployata. Build identity iniettata via Docker ARG → Kamal `builder.args` → CI. Gli smoke-test
staging/production verificano che la versione live corrisponda al tag (fail su mismatch).
### Fixed
- **Deploy CI dopo lockdown SSH** (infra 2026-06-02): la porta 22 di tutti i server è stata chiusa alla
rete privata `10.0.0.0/16`. La CI ora passa per il gateway-bastion (`167.233.61.97`) via SSH `ProxyJump`,
con i server referenziati per IP privato (web `10.0.1.3`, worker `10.0.1.2`, staging `10.0.1.6`); il
builder resta locale sul runner. Nessun cambiamento di comportamento dell'applicazione.
## [0.0.20] - 2026-06-01
### Changed
- Hero della landing: rimosso il padding laterale su desktop (`md:px-12` → `md:px-0`), mantenuto
`px-6` su mobile. Il contenuto resta vincolato da `max-w-6xl mx-auto`.
## [0.0.19] - 2026-06-01
### Added
- Pagine legali pubbliche **Termini e Condizioni** (`/terms`), **Privacy Policy** (`/privacy`) e
**Cookie Policy** (`/cookies`), in italiano e inglese, con copy GDPR su misura per Koru (WhatsApp,
Gemini, dati salute, Maps, GlitchTip, Plausible, Hetzner). Guscio condiviso `_legal`, link dal footer.
I dati del titolare del trattamento sono segnaposto da compilare prima del go-live.
- **Cookie banner con consenso granulare** (Necessari/Analitici/Marketing): `Ui::CookieConsentComponent`
+ Stimulus `cookie_consent_controller`, consenso persistito nel cookie `koru_cookie_consent` (180 giorni),
link "Gestisci cookie" nel footer per riaprire le preferenze.
- **Plausible Analytics** (self-hosted, cookieless) caricato **solo previo consenso** "Analitici"
(script inerte `type="text/plain"` attivato dal controller). Nuova env `PLAUSIBLE_DOMAIN`
(`heykoru.ai` prod, `staging.heykoru.ai` staging) + `PLAUSIBLE_SCRIPT_SRC` opzionale.
### Fixed
- Evitata collisione: l'azione della pagina cookie è `cookie_policy`, non `cookies` (che shadowerebbe
il cookie jar del controller e romperebbe l'autenticazione). URL `/cookies` e helper `cookies_path` invariati.
## [0.0.18] - 2026-05-31
### Fixed
- Risposte WhatsApp non recapitate (silenzio dopo l'onboarding): WhatsApp può identificare il
mittente con un **LID** (`<id>@lid`) anziché col numero (`<digits>@c.us`). L'adapter OpenWA
scartava il suffisso e rispediva a un `@c.us` ricostruito dal LID — indirizzo **non consegnabile**
(OpenWA accetta con 201/500 ma non recapita) → le risposte del turno conversazionale morivano in
silenzio, senza errori. Inbound e onboarding non erano impattati (match sullo stesso valore
normalizzato). Ora si preserva il **chatId opaco** end-to-end e si risponde sempre allo stesso
indirizzo dell'inbound: nuova colonna `chat_id` su `accounts_whatsapp_links`,
`Providers::Messaging::InboundMessage#chat_id`, `OpenWaAdapter#to_chat_id` passthrough sugli
indirizzi con `@`, persistenza in `VerifyWhatsappLinkService`, invio via `chat_id` in
`Bookings::Notifier`, e **self-heal** del `chat_id` sui numeri già verificati. I link verificati
prima del fix (con `chat_id` NULL) si riparano da soli al primo inbound successivo.
## [0.0.17] - 2026-05-31
### Added
- `GOOGLE_MAPS_API_KEY` provisionata **server-side** (API key ristretta a Places API New + Geocoding,
nessun referrer) sui progetti GCP `koru-development` e `koru-production`, salvata su Doppler e cablata
nel deploy (`deploy.yml` `env.secret`, `.kamal/secrets*`, blocco `env:` del job `deploy-production`).
Sblocca i suggerimenti autocomplete indirizzo (e geocoding/discovery Google) in dev e **produzione**.
**Staging escluso**: billing GCP non disponibile (quota fatturazione esaurita) → su staging
l'autocomplete resta vuoto e il submit usa il fallback OSM.
### Fixed
- Deploy: `KORU_WHATSAPP_NUMBER` non veniva passato a Kamal nei blocchi `env:` dei job
`deploy-staging`/`deploy-production` del workflow (la v0.0.16 aveva sistemato solo i file
`.kamal/secrets*`) → il numero arrivava **vuoto** nel container. Aggiunto al workflow: il deep-link
WhatsApp dell'onboarding ora ha il numero reale in staging e produzione.
## [0.0.16] - 2026-05-31
### Fixed
- Deploy Kamal (staging **e** produzione): `KORU_WHATSAPP_NUMBER`, aggiunto in `env.secret` di
`deploy.yml`/`deploy.staging.yml` con la v0.0.14, non era mappato nei file `.kamal/secrets*` →
`Kamal::ConfigurationError: Secret 'KORU_WHATSAPP_NUMBER' not found in .kamal/secrets-common,
.kamal/secrets.staging` e deploy fallito (v0.0.14 e v0.0.15, CI verde ma step kamal in errore). Il
valore era già su Doppler (3 config) → mancava solo il mapping. Aggiunta la riga
`KORU_WHATSAPP_NUMBER=$KORU_WHATSAPP_NUMBER` in `.kamal/secrets`, `.kamal/secrets-common` e
`.kamal/secrets.staging`. Porta in produzione anche la v0.0.15 (autocomplete indirizzo).
## [0.0.15] - 2026-05-31
### Added
- Onboarding step "profile": il campo indirizzo ora ha **autocomplete as-you-type**. Nuovo
`Forms::AddressAutocompleteComponent` con dropdown di suggerimenti; lo Stimulus controller
interroga un endpoint Rails (`GET onboarding/profile/address-suggestions`) che proxy-a Google
Places **lato backend** (`Providers::Places::GoogleAutocompleteAdapter`, host
`places.googleapis.com`, `GOOGLE_MAPS_API_KEY` ristretta) via `Onboarding::SuggestAddressesService`.
Soglia minima 3 caratteri (`ADDRESS_SUGGEST_MIN_QUERY`), max 5 risultati (`ADDRESS_SUGGEST_LIMIT`).
Il backend ri-geocodifica al submit.
### Changed
- Brand mark koru **centrato** nel viewBox: la spirale era spostata a destra (centroide a ~49 su 80
invece di 40). Aggiunto wrapper `translate(-9,-1)` a tutti i mark inline (LogoComponent, layout
auth/member/onboarding/valhalla, landing footer/hero) e ai mockup `docs/design-system`. **Favicon**
rigenerata col mark centrato: `icon.svg` + tutti i raster (`favicon.ico` 16/32/48, png 16/32/180/192/512,
apple-touch-icon).
## [0.0.14] - 2026-05-31
### Fixed
- Onboarding "collega WhatsApp": il deep-link apriva `wa.me/?text=<token>` **senza numero** →
WhatsApp chiedeva di scegliere un contatto invece di aprire la chat con koru, e il token non
veniva inviato (pagina ferma su "in attesa del tuo messaggio"). Causa: `KORU_WHATSAPP_NUMBER`
mai configurato su Doppler. Aggiunto il numero koru (3 config) + alla lista `env.secret` di
`deploy.yml`/`deploy.staging.yml`; il deep-link ora è `wa.me/<numero>?text=<token>` e l'onboarding
si completa via webhook. Warn esplicito se il numero non è configurato.
### Added
- Task `openwa:unregister_webhook` (cleanup del webhook OpenWA dopo i test locali via tunnel) +
verbo `delete` in `Providers::HttpClient`.
## [0.0.13] - 2026-05-31
### Changed
- Navbar landing: aggiunta la voce **Accedi** (link a `/auth/sign-in`) — prima il login non era
raggiungibile da nessun punto del sito pubblico.
- Le CTA primarie "Inizia gratis" (navbar, hero, pricing) ora portano **direttamente** alla
registrazione (`/auth/sign-up`) invece di scrollare alla sezione `#cta` in fondo: eliminato il
doppio passaggio. La sezione CTA finale resta come chiusura della pagina (bottone → registrazione).
## [0.0.12] - 2026-05-31
### Fixed
- Login non portava da nessuna parte: dopo l'autenticazione (e per un utente già loggato che
apriva una pagina auth) il redirect andava a `root_path` (landing pubblica) invece che all'area
autenticata. Ora `Auth::SessionsController#create` e `redirect_if_authenticated` reindirizzano a
`member_root_path`, da cui il gate `Member::` porta l'utente all'onboarding (se incompleto) o alla
dashboard (se completato).
## [0.0.11] - 2026-05-31
### Fixed
- Landing CTA "Inizia gratis" non faceva nulla: il form della sezione finale (`_cta`) aveva
`action="#"` + input numero senza wiring (mockup mai collegato). Sostituito con
`Ui::ButtonComponent` che porta a `/auth/sign-up` (registrazione reale email/password; il numero
WhatsApp si collega nell'onboarding). Rimosso il campo numero fuorviante + chiave i18n
`cta.input_placeholder`; referenza `docs/design-system/examples/landing.html` allineata.
## [0.0.10] - 2026-05-31
### Added
- Gemini come provider LLM attivo per le risposte conversazionali/WhatsApp (`gemini-2.5-flash`,
pipeline esistente). Provisioning GCP: folder `koru` + progetti
`koru-{development,staging,production}`, Generative Language API abilitata, API key per env
ristretta a `generativelanguage.googleapis.com`, `GEMINI_API_KEY` su Doppler (3 config). Chiave
propagata al deploy: `env.secret` (`config/deploy.yml` + `deploy.staging.yml`), `.kamal/secrets*`,
job deploy del workflow CI. Doc servizio in `CLAUDE.md`.
## [0.0.9] - 2026-05-31
### Fixed
- Landing hero: le sfere decorative (viola top-right, fire bottom-left) non vengono più troncate a
metà schermo. La `<section>` accorpava `max-w-6xl mx-auto` + `overflow-hidden` sullo stesso elemento
→ i blob venivano clippati al bordo del box centrato. Separata in `<section>` full-width (solo
`overflow-hidden`, clip al viewport) + `<div max-w-6xl mx-auto>` interno centrato; sfere riposizionate
per sbordare oltre i bordi del viewport. Allineato alla referenza `docs/design-system/examples/landing.html`.
## [0.0.8] - 2026-05-31
### Added
- SEO + social: partial `layouts/_head_meta` (description, canonical, theme-color, Open Graph,
Twitter Card) sovrascrivibile per pagina via `content_for`; `og-image.png` (1200×630),
`public/robots.txt` (disallow aree autenticate + sitemap), `public/sitemap.xml`.
- PWA / app icons: `site.webmanifest` (icone 192/512 + maskable + svg), favicon multi-size
(`favicon.ico`/16/32), `apple-touch-icon.png`, `icon-192/512.png`; meta apple/mobile-web-app
nel layout. Initializer `mime_types.rb` per servire `.webmanifest` come `application/manifest+json`.
- Logo brand `koru-logo.png` in `Ui::LogoComponent` (variante immagine).
### Changed
- Layout (`application`/`auth`/`member`/`onboarding`) e sezioni landing (hero, footer, pricing,
services) allineati al brand/SEO; `Ui::CardComponent`/`Ui::LogoComponent` rifiniti.
## [0.0.7] - 2026-05-31
### Fixed
- Deploy: i file `.kamal/secrets*` dichiaravano solo 5 secret mentre `env.secret` ne lista 13 →
kamal falliva con `Secret 'OPENWA_BASE_URL' not found` e il container web restava *unhealthy*
(l'initializer AR encryption faceva `ENV.fetch` su chiavi mai iniettate). Aggiunti tutti i nomi
(`OPENWA_*`, `RESEND_API_KEY`, `MAIL_FROM`, 3 chiavi AR encryption) a `secrets-common`, `secrets`
e `secrets.staging` come passthrough `NAME=$NAME` (valori da ENV, safe for git).
## [0.0.6] - 2026-05-31
### Fixed
- Deploy: il build Docker falliva su `assets:precompile` con `KeyError` su
`ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY` (le chiavi non sono disponibili a build-time). Guard
`SECRET_KEY_BASE_DUMMY` nell'initializer AR encryption: chiavi placeholder a build, reali a runtime.
- Deploy: il workflow CI passava a kamal solo 3 secret; ora passa **tutti** gli `env.secret`
(SENTRY_DSN, OPENWA_*, RESEND_API_KEY, MAIL_FROM, chiavi AR encryption) dai GitHub Environments
staging/production. Aggiunte le 3 chiavi AR encryption a `env.secret` (`deploy.yml` + `deploy.staging.yml`)
per l'iniezione a runtime nel container.
## [0.0.5] - 2026-05-31
### Added
- Email transazionali via **Resend** (HTTP API): gem `resend` + initializer, delivery `:resend` in
produzione/staging, `letter_opener_web` in development (preview `/letter_opener`), `:test` in test.
Mittente dominio verificato `@notifications.heykoru.ai`. ENV `RESEND_API_KEY`/`MAIL_FROM` (Doppler) +
`MAIL_HOST` per-ambiente (`www.heykoru.ai` / `staging.heykoru.ai`).
- Dominio pubblico **heykoru.ai** ai `proxy.hosts` Kamal: `www.heykoru.ai` (prod) e
`staging.heykoru.ai` (staging); apex via URL-forwarding 301 → www.
### Fixed
- Lint CI: `Ui::IconComponent#aria_attrs` migrato alla hash syntax 1.9 (`Style/HashSyntax`) — il job
`lint` falliva su v0.0.4 bloccando il deploy. Con il fix l'intera pipeline (scan/lint/test) è verde.
## [0.0.4] - 2026-05-31
### Added
- Messaging WhatsApp via provider `openwa` agganciato al servizio OpenWA REST self-hosted
(auth `X-API-Key`): invio (`send-text`/`send-image`), heartbeat sessione, e ricezione inbound
via webhook (`message.received`, firma `X-OpenWA-Signature`). Rake `openwa:register_webhook`.
### Changed
- `Providers::Messaging::OpenWaAdapter` riscritto dal sidecar HMAC `/send` (mai deployato) al
servizio OpenWA REST session-based. Nuove ENV `OPENWA_BASE_URL`/`OPENWA_SESSION_ID`/
`OPENWA_API_KEY`/`OPENWA_SHARED_SECRET` (Doppler + `env.secret` deploy).
## [0.0.3] - 2026-05-30
### Added
- Design system koru: ViewComponent (`Ui::`/`Forms::`) — Logo, Button, Badge, Chip, Card,
Avatar, SectionHeading, TextField. Tailwind v4 `@theme` con 2 brand token (`fire`, `ink`) +
font Fraunces/DM Sans. Styleguide Lookbook a `/lookbook`. Doc `app/components/COMPONENTS.md`.
- Landing page pubblica (`/`) ricostruita con i componenti (i18n it/en in `config/locales/website`).
## [0.0.2] - 2026-05-29
### Added
- Error tracking + structured logs via GlitchTip (Sentry SDK): initializer con
`enable_logs`, scrubbing PII (`ErrorTracking::PiiScrubber`), `SENTRY_ENVIRONMENT`
per distinguere staging/production, route `/debug-glitchtip` (esclusa in produzione).
## [0.0.1] - 2026-05-29
### Added
- Initial release — scaffold Rails 8 (PostgreSQL primary + queue, SQLite cache + cable).
- Doppler secrets (development/staging/production), Kamal deploy (web + worker), CI/CD con
environment-gated deploy (staging/test/production), rack-attack, Sentry, RSpec.