Documentazione Tecnica · AMV Idealab

Gioiosa Marea
Guida Turistica Digitale

Piattaforma web AI per la valorizzazione turistica di Gioiosa Marea, costa tirrenica messinese. Powered by TTATTÀ GO.

v2.0 · Giugno 2026 · Rails 8.1.2 · Ruby 3.4.7 · Claude Haiku 4.5
Changelog

Tutte le versioni e gli aggiornamenti rilasciati in produzione.

v2.0 Giugno 2026 Piattaforma completa — Sync API, SEO avanzato, PWA, Performance
  • new Sincronizzazione automatica da TTATTÀ GO API: eventi, esperienze, vendor, luoghi via SolidQueue background jobs con fallback graceful se API non raggiungibile
  • new Sezione Vendor completa: 6 categorie (food, sleep, wellness, producers, nightlife, tourism) con slug URL SEO-friendly, mappa Leaflet, photo gallery, orari strutturati
  • new Sezione Eventi: sync da API, badge "tra X giorni / oggi / concluso", filtro attivi/passati, mappa, structured data Event schema.org
  • new Sezione Esperienze: sync da API con difficulty, duration, price_label, season_label, days_label, organizer (organization_name/slug), mappa, badge colorati
  • new Guida Turistica Interattiva (/esplora/guida-turistica): mappa Leaflet con tutti i POI numerati, filtri per categoria, sheet detail mobile swipe-up + desktop panel fisso, zoom auto al click, versione stampabile PDF
  • new Riconoscimento vocale Whisper: input voce nella chat AI trascritto via OpenAI Whisper API, compatibile iOS/Android
  • new Ricerca per prossimità "Vicino a me": geolocalizzazione browser, calcolo distanza Haversine, ordinamento e filtro luoghi nelle vicinanze
  • new Structured data schema.org completo: WebSite, TouristAttraction, Event, TouristTrip, LocalBusiness (con sottotipi per categoria vendor), ItemList su tutte le index, Map + TouristMap sulla guida turistica
  • new PWA completa: service worker, web manifest, offline page, installabile da browser su iOS e Android
  • new Banner "Altri eventi in zona" linkato a ttattago.com/it/events per cross-promozione piattaforma
  • new Sezione eventi passati: archivio storico degli eventi terminati, accessibile dalla pagina eventi
  • perf Google Fonts caricato async con media="print" trick — eliminato blocco render da ~800ms su mobile
  • perf marked.min.js caricato con defer — non blocca più il render sulla homepage
  • perf isolation: isolate su tutti i container Leaflet — z-index interni Leaflet (fino a 1000) confinati nel proprio stacking context, bottom nav e overlay non più bucati dalla mappa
  • fix Guida turistica desktop: sheet POI non copriva più filtri, aggiunto bottone ✕ + listener ESC, pills categoria non più schiacciate da flexbox overflow
  • fix Zoom automatico a livello 17 al click su POI nella guida turistica (15 su mobile)
  • fix Crediti fotografici (cover_image_caption) mostrati nella sheet della guida turistica
  • seo Accessibilità WCAG AA: tutti i colori portati a contrasto ≥ 4.5:1 — #F4A261 → #92400E su sfondi chiari, text-gray-400 → text-gray-600, #00A896 → #00796B su bianco
  • seo PageSpeed Insights mobile: Accessibilità 92 → 100, Performance FCP migliorato di ~800ms
v1.2 Aprile 2026 Multi-lingua, QR code, admin sync, mobile UX
  • new Bottom navigation mobile a 4 tab (Esplora, Mappa, Chat, Menu) con stato attivo e safe area iOS
  • new Supporto multi-lingua: italiano, inglese, francese, tedesco nella chat AI (risponde nella lingua della domanda)
  • new Admin panel per trigger manuale sync da API TTATTÀ GO
  • new Pagine vendor con route per sezione: /mangia/:slug, /dormi/:slug, ecc.
  • fix Sync eventi interrotto gracefully se API non raggiungibile (fallback con log)
  • fix Mappa esplora: marker clustering con leaflet.markercluster
v1.0 Marzo 2026 Release iniziale — MVP funzionante
  • new Homepage con statistiche dinamiche, preview luoghi/vendor/eventi/esperienze
  • new Esplora: mappa Leaflet + griglia luoghi con filtri categoria
  • new Chat AI con Claude Haiku, streaming real-time via ActionCable, contesto Gioiosa Marea iniettato
  • new Itinerario AI: form personalizzazione + streaming + mappa risultato
  • new Meteo in tempo reale via Open-Meteo (gratuito, no API key)
  • new Orari strutturati JSON per ogni luogo con open_now? real-time
  • new Deploy su server VPS Linux custom, configurato ad hoc con Puma + Nginx

Panoramica

Piattaforma web Rails che funge da guida turistica AI per Gioiosa Marea. Gli utenti accedono tramite QR code — nessuna installazione richiesta. I dati di luoghi, eventi, esperienze e vendor sono sincronizzati automaticamente dalla piattaforma TTATTÀ GO.

📱

QR Code Entry

Ogni punto del paese ha il suo QR code. L'utente inquadra e accede istantaneamente all'app senza installare nulla. Installabile come PWA da browser.

Web App + PWA
🗺️

Guida Turistica Interattiva

Mappa Leaflet con tutti i POI numerati, filtrabili per categoria. Sheet detail con foto, orari, indicazioni. Stampabile come PDF in 3 sezioni geografiche.

Leaflet.js
🤖

Assistente AI + Voce

Chat con Claude Haiku che conosce tutti i luoghi, il meteo attuale, gli orari e la storia del territorio. Input testuale e vocale (Whisper). Streaming real-time.

Claude + Whisper

Itinerario AI Personalizzato

Durata, interessi, tipo di gruppo → Claude genera un itinerario su misura verificando orari apertura, meteo e preferenze. Output con mappa Leaflet integrata.

AI Personalized
🔄

Sincronizzazione TTATTÀ GO

SolidQueue jobs sincronizzano automaticamente eventi, esperienze, vendor e luoghi dalla piattaforma madre. Fallback graceful se API non raggiungibile.

SolidQueue + API
📍

Vicino a Me

Geolocalizzazione browser con calcolo distanza Haversine. Ordina e filtra luoghi, vendor ed esperienze per prossimità rispetto alla posizione corrente dell'utente.

Geolocation API

Tech Stack

Ogni componente scelto per leggerezza, velocità di sviluppo e minima dipendenza da servizi esterni.

LayerTecnologia
FrameworkRuby on Rails 8.1.2
LinguaggioRuby 3.4.7
DatabaseSQLite3 via Active Record (3 DB separati: main, cache, queue)
FrontendHotwire (Turbo Drive + Turbo Streams + Stimulus) + Tailwind CSS
Asset pipelinePropshaft + Importmap (no Webpack/bundler)
MappeLeaflet.js 1.9.4 + leaflet.markercluster via CDN
AI — Chat & ItinerarioClaude Haiku 4.5 (claude-haiku-4-5-20251001) via ruby_llm gem
AI — VoceOpenAI Whisper API (trascrizione audio → testo)
StreamingActionCable + Turbo::StreamsChannel broadcast
Job QueueSolid Queue (integrato in Puma, SQLite-backed)
CacheSolid Cache (SQLite-backed, configurato in config/cache.yml)
MeteoOpen-Meteo API — gratuita, no API key, coordinate fisse Gioiosa Marea
Dati esterniTTATTÀ GO REST API — sync eventi/esperienze/vendor/luoghi
Markdown renderingmarked.js 9 via CDN (con defer — non blocca render)
DeployVPS Linux — server dedicato configurato ad hoc, Puma + Nginx reverse proxy
Static files cachepublic_file_server.headers → Cache-Control: public, max-age=31536000

Architettura Data Sync

I contenuti (eventi, esperienze, vendor, luoghi) vengono sincronizzati dalla piattaforma TTATTÀ GO tramite la sua REST API. Ogni sync service è idempotente e gestisce gracefully i fallback.

🔄

Flusso di Sincronizzazione

1
Trigger — SolidQueue Job EventSyncJob / ExperienceSyncJob / VendorSyncJob schedulati o manuali da admin
2
TtattaApi.call(endpoint) HTTP GET con Bearer token verso API TTATTÀ GO. Timeout 10s. Ritorna nil se non raggiungibile. TtattaApi.events(place_slug: "gioiosa-marea")
3
Sync Service — upsert per slug find_or_initialize_by(external_slug). Aggiorna tutti i campi. Elimina i record non più presenti in API.
Result struct Ritorna { created:, updated:, skipped:, deleted:, errors: [] } per logging e admin panel

Sync Services implementati

📅EventSyncServiceeventi, date, location, immagini
ExperienceSyncServiceesperienze, difficulty, price_label, organizer
🏪VendorSyncServicevendor per sezione, orari, immagini, contatti
📍PlaceSyncServiceluoghi, categorie, foto, coordinate
🖼️PlaceImageSyncServicegallery immagini con caption e posizione

Campi chiave sincronizzati da API

🔑external_slugchiave univoca per upsert idempotente
🏢organization_name/slugorganizzatore esperienze (da API v2)
🎯activity_slugvendor collegato all'esperienza
Robustezza: Se TtattaApi ritorna nil (timeout, errore di rete, API down), il sync service aggiunge un entry in result.errors e interrompe senza crashare. I dati esistenti rimangono intatti. Il job viene loggato su stdout per monitoring.

Database Schema

SQLite3 con Active Record. Schema evoluto da 3 a 9+ tabelle con l'aggiunta di vendor, eventi, esperienze e sincronizzazione API.

TABLE places luoghi di interesse
idintegerPK
external_slugstringslug univoco TTATTÀ GO (upsert key)
namestringnome del luogo
descriptiontextdescrizione HTML estesa
lat / lngfloatcoordinate GPS
distance_from_parkfloatkm dal centro (ordinamento)
opening_hoursstringtesto leggibile orari
hours_jsontextJSON orari strutturati → open_now?
cover_image_urlstringimmagine di copertina
cover_image_captionstringcrediti fotografici copertina
image_emojistringemoji fallback senza foto
addressstringindirizzo testuale
pricestringfascia prezzo
featuredbooleanmostrato in homepage
TABLE events eventi sincronizzati da API
idintegerPK
external_slugstringupsert key
namestring
descriptiontextHTML sanitizzato
start_date / end_datedaterange evento
start_timestringorario inizio (HH:MM)
location_namestringnome del luogo evento
latitude / longitudefloat
cover_image_urlstring
image_emojistring
featuredboolean
remote_idintegerID originale TTATTÀ GO
TABLE experiences esperienze/attività
external_slugstringupsert key
namestring
descriptiontext
difficultystringfacile / medio / difficile
durationstringes. "2 ore"
price_labelstringes. "€18" o "Gratuito"
season_labelstringes. "Tutto l'anno"
days_labelstringes. "Tutti i giorni"
organization_namestringnome organizzatore (API v2)
organization_slugstringslug organizzatore su TTATTÀ GO
vendor_slugstringactivity_slug collegato (API v2)
latitude / longitudefloat
cover_image_urlstring
featuredboolean
TABLE vendors ristoranti, hotel, wellness...
external_slugstringupsert key
namestring
sectionstringfood / sleep / wellness / producers / nightlife / tourism
descriptiontextHTML sanitizzato
addressstring
telephonestring
website_urlstring
price_rangestringes. "€€" o "10–30 EUR"
cover_image_urlstring
latitude / longitudefloat
opening_hoursstring
type_label / type_emojistringsottocategoria leggibile
TABLE place_images gallery fotografica luoghi
place_idintegerFK → places
photo_urlstringURL immagine piena
photo_thumb_urlstringURL thumbnail
captionstringcrediti fotografici
positionintegerordine in gallery
remote_idintegerID originale TTATTÀ GO
TABLE chats + messages sessioni chat AI
session_idstringID sessione browser (unique)
rolestringuser / assistant
contenttexttesto messaggio
sourcestringclaude / fallback / whisper
input_tokensintegertoken monitoraggio costi
output_tokensinteger
Formato hours_json — chiavi giorno (mon–sun), array di slot [apertura, chiusura]:
{ "mon": [["09:00", "13:00"], ["16:00", "20:00"]], "sun": [] }

Route

Routing completo dell'applicazione. Tutte le URL sono SEO-friendly con slug leggibili.

MetodoPathDescrizione
GET/Homepage con preview dinamica di tutti i contenuti
GET/esploraMappa Leaflet + griglia luoghi con filtri categoria
GET/esplora/:slugDettaglio luogo con gallery, mappa, orari, indicazioni
GET/esplora/guida-turisticaGuida turistica interattiva — mappa POI + list + sheet detail + print
GET/eventiGriglia eventi con badge stato, filtro attivi/passati
GET/eventi/:slugDettaglio evento con mappa e link prenotazione
GET/esperienzeGriglia esperienze con badge difficulty, price, season
GET/esperienze/:slugDettaglio esperienza con link prenotazione TTATTÀ GO
GET/mangia / /mangia/:slugVendor food — ristoranti, bar, locali
GET/dormi / /dormi/:slugVendor sleep — hotel, agriturismi, B&B
GET/benessere / /benessere/:slugVendor wellness — lidi, spa, centri benessere
GET/produttori / /produttori/:slugVendor producers — artigiani, tipicità
GET/vita-notturna / /vita-notturna/:slugVendor nightlife — discoteche, locali serali
GET/servizi-turistici / /servizi-turistici/:slugVendor tourism — guide, noleggi, servizi
GET/assistenteUI chat AI con input testuale e vocale (Whisper)
POST/assistenteInvia messaggio → avvia ChatStreamJob
POST/assistente/voiceUpload audio → Whisper → risposta AI
DEL/assistenteReset conversazione
GET/itinerarioForm generatore + widget meteo
POST/itinerarioAvvia ItineraryStreamJob
GET/upHealth check ping (monitoraggio server)
GET/manifestWeb App Manifest JSON per PWA
GET/docsQuesta documentazione (static HTML)

Flusso AI

Chat, itinerario e voce: architettura streaming basata su ActionCable + SolidQueue.

💬

Chat Assistente + Voce

1
Input testo o audio Testo via form Turbo. Audio via MediaRecorder API → POST /assistente/voice con blob audio
2
Whisper (solo voce) OpenAI Whisper API trascrive l'audio in testo. Il testo trascritto entra nel flusso standard. OpenAI::Client.audio.transcribe()
3
Controller → ChatStreamJob Risponde subito con bolla utente. Avvia job in background. ChatStreamJob.perform_later(chat_id)
4
Contesto iniettato with_gioiosa_marea_context() → meteo + tutti i luoghi con orari + data/ora Europe/Rome
Streaming via ActionCable Ogni chunk Claude broadcastato in Turbo Stream. Fine: rimuove cursore, salva token.
🗺️

Itinerario AI Personalizzato

1
Form parametri Durata (mezz. / giornata), interessi (natura, storia, cibo...), tipo gruppo (solo, coppia, famiglia)
2
Controller → ItineraryStreamJob Rimpiazza il form con loading state. Job in background. ItineraryStreamJob.perform_later(params)
3
Contesto arricchito WeatherService → outdoor_suitable. Luoghi filtrati per interessi. open_now? per ogni posto.
4
Claude genera itinerario Streaming chunk per chunk. Markdown renderizzato da marked.js.
Broadcast mappa Leaflet Mappa con i luoghi dell'itinerario + pulsante "Nuovo itinerario"

SEO & Structured Data

Ogni pagina ha dati strutturati schema.org specifici per il tipo di contenuto. Copertura completa per Google Search e Google Maps.

PaginaSchema TypeDati inclusi
/WebSitenome, url, potentialAction SearchAction, sameAs
/esplora/:slugTouristAttractionname, description, geo, address, openingHours, image, url
/esploraItemListlista luoghi con position, name, url
/esplora/guida-turisticaMap + TouristMap + ItemListmappa turistica con tutti i POI numerati
/eventi/:slugEventname, startDate, endDate, location, image, organizer, eventStatus
/eventiItemListlista eventi con position, name, url
/esperienze/:slugTouristTripname, description, touristType, offers, provider
/esperienzeItemListlista esperienze
/mangia/:slugFoodEstablishmentname, servesCuisine, priceRange, telephone, geo, sameAs
/dormi/:slugLodgingBusinessname, priceRange, telephone, geo
/benessere/:slugHealthAndBeautyBusinessname, telephone, geo, openingHours
/vita-notturna/:slugNightClubname, telephone, geo
tutte le index vendorItemListlista strutture per sezione
Implementazione: Tutti i dati strutturati sono generati dall'helper StructuredDataHelper in Ruby come JSON-LD e iniettati nell'<head> via content_for :structured_data. Nessun JavaScript necessario — markup statico nell'HTML.

Progressive Web App

L'app è installabile come PWA su iOS e Android direttamente dal browser, senza App Store.

Funzionalità PWA

📲InstallabileiOS Safari + Android Chrome
📴Offline page/offline.html — servita dal SW
🎨Theme color#0B3D6E (barra browser colorata)
🖼️Iconaicon.png 512×512 maskable
📐Displaystandalone (no browser chrome)
🔤App nameTTATTÀ GO · Gioiosa Marea

Service Worker

Cache strategyNetwork-first, offline fallback
🗃️Cache staticaCSS, JS, icone, font
🌐Offlinepagina branded con reload automatico
📋Manifest routeGET /manifest → JSON dinamico Rails
🍎iOS metaapple-mobile-web-app-capable, status-bar-style

Performance & Accessibilità

Ottimizzazioni applicate per migliorare PageSpeed Insights mobile (target: score > 90 su tutte le metriche).

Performance — fix applicati

🔤Google Fonts asyncmedia="print" onload trick — eliminato blocco render ~800ms
⏱️marked.js defercaricamento posticipato — non blocca più il render
🔗preconnectfonts.googleapis.com + fonts.gstatic.com
🗺️isolation: isolatetutti i [data-controller="map"] — z-index Leaflet confinati, non bucano più bottom nav o sheet
💾Cache-Controlmax-age=31536000 sugli asset statici fingerprinted da Propshaft
🖼️loading="lazy"su tutte le immagini sotto la piega

Accessibilità WCAG AA — fix applicati

🎨#F4A261 su sfondo chiaro→ #92400E (ratio 6:1 su bianco/crema)
🎨text-gray-400/500 su bianco→ text-gray-600 (ratio 6.9:1)
🎨#00A896 su bianco→ #00796B (ratio 5.2:1)
🎨#A7F3D0 su #00796B→ #ffffff (ratio 5.3:1)
🎨opacity-70 su card scure→ rimosso opacity, testo bianco pieno
Risultato PageSpeedAccessibilità: 92 → 100

Weather Service

Integrazione Open-Meteo — completamente gratuita, nessuna API key richiesta.

Parametri chiamata

📍Coordinate fisse38.1748, 14.8965
🌡️current_weathertemperatura, windspeed, codice WMO
🌧️hourlyprecipitation_probability
🕐timezoneEurope/Rome
📅forecast_days1 (solo oggi)

Dove viene usato

💬Chat AIsystem prompt arricchito con meteo corrente
🗺️ItineraryStreamJoboutdoor_suitable filtra luoghi indoor/outdoor
📋Form itinerariowidget meteo visibile prima di generare
outdoor_suitabletrue se codice WMO ≤ 2 AND precip < 40%

Deploy — VPS Linux

Server VPS Linux dedicato, configurato ad hoc. Puma come application server, Nginx come reverse proxy. Deploy via SSH con git pull e restart.

Stack infrastruttura

🖥️ServerVPS Linux — dedicato e configurato ad hoc
🌐Reverse proxyNginx con SSL termination
⚙️App serverPuma (multi-thread, configurato per Rails)
💾StorageSQLite3 su volume persistente
🔒SSLLet's Encrypt via Certbot
🔄Process managersystemd — Puma + SolidQueue come servizi

Secrets richiesti

🔑RAILS_MASTER_KEYconfig/master.key
🤖ANTHROPIC_API_KEYClaude Haiku 4.5
🎤OPENAI_API_KEYWhisper voice transcription
🔗TTATTA_API_KEYsync da piattaforma TTATTÀ GO
🌐TTATTA_API_BASE_URLendpoint REST TTATTÀ GO
📧RAILS_SERVE_STATIC_FILEStrue
# Deploy aggiornamento (via SSH)
git pull origin main
bundle install --without development test
bin/rails assets:precompile
bin/rails db:migrate
sudo systemctl restart puma gioiosa-marea

# Verifica stato
sudo systemctl status puma
sudo journalctl -u puma -f

Struttura File Chiave

File principali dell'applicazione organizzati per layer.

GioiosaMarea/
├── app/controllers/
│ ├── home_controller.rb # homepage + preview dinamica
│ ├── explore_controller.rb # mappa + guida turistica
│ ├── events_controller.rb # eventi attivi/passati
│ ├── experiences_controller.rb # esperienze con organizer
│ ├── vendors_controller.rb # 6 sezioni vendor
│ ├── chat_controller.rb # chat AI + voice upload Whisper
│ └── itinerary_controller.rb # form + avvio job
├── app/models/
│ ├── place.rb # open_now?, all_category_names
│ ├── event.rb # starts_in_label, date_label, active?
│ ├── experience.rb # difficulty_style, distance_from_center
│ ├── vendor.rb # type_label, type_emoji, section routing
│ └── chat.rb # with_gioiosa_marea_context()
├── app/services/
│ ├── ttatta_api.rb # client HTTP TTATTÀ GO API
│ ├── event_sync_service.rb # upsert eventi da API
│ ├── experience_sync_service.rb # upsert esperienze + organizer
│ ├── vendor_sync_service.rb # upsert vendor per sezione
│ ├── place_sync_service.rb # upsert luoghi + immagini
│ └── weather_service.rb # Open-Meteo API
├── app/jobs/
│ ├── chat_stream_job.rb # streaming chat Claude
│ ├── itinerary_stream_job.rb # streaming itinerario + mappa
│ ├── event_sync_job.rb # scheduled sync eventi
│ └── experience_sync_job.rb # scheduled sync esperienze
├── app/helpers/
│ └── structured_data_helper.rb # schema.org JSON-LD per ogni tipo
├── app/views/explore/
│ ├── index.html.erb # mappa + griglia + filtri
│ ├── show.html.erb # dettaglio POI + gallery
│ └── tourist_guide.html.erb # guida turistica interattiva + print
├── app/javascript/controllers/
│ ├── chat_controller.js # submit, auto-scroll, quick replies, voice
│ └── map_controller.js # Leaflet overview + singolo luogo + cluster
├── app/assets/stylesheets/
│ └── application.css # isolation:isolate su [data-controller="map"]
├── public/
│ ├── offline.html # pagina offline PWA
│ ├── manifest → /manifest route # Web App Manifest dinamico
│ └── docs/index.html # questa documentazione
├── config/
│ ├── routes.rb # 30+ route con slug SEO-friendly
│ ├── initializers/ruby_llm.rb # config Claude Haiku
│ └── cache.yml # Solid Cache SQLite
└── db/
    ├── schema.rb # 9+ tabelle: places, events, experiences, vendors, place_images...
    └── seeds.rb # dati iniziali idempotenti