Files
vuelato/plans/vuelato-plan.md
Alejandro Martinez b8906efc80
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Initial commit: Vuelato - buscador de vuelos
Nuxt 4 + Supabase + Flightics API. Incluye búsqueda de vuelos,
inspiraciones, watchlist, tracking de precios y mapa interactivo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:37:06 +02:00

14 KiB

Vuelato - Frontend + Backend Supabase

Contexto

Tenemos un buscador de vuelos con un cliente API completo de Flightics (9 endpoints REST funcionando). El frontend actual tiene 3 paginas basicas. Queremos:

  1. Un backend Supabase/PostgreSQL que wrapee Flightics y persista datos
  2. Un frontend ambicioso que explote toda la API

Por que Supabase

  • Persistencia: watchlists, preferencias, busquedas recientes -> entre dispositivos
  • Cache inteligente: airports, countries, inspirations en Postgres (no llamar a Flightics cada vez)
  • Auth: usuarios con sus datos
  • Cron/Edge Functions: verificar precios de watchlist automaticamente
  • Realtime: notificar bajadas de precio
  • Row Level Security: cada usuario ve solo sus datos

Stack

  • Nuxt 4 + @nuxt/ui v4 + Tailwind CSS v4 (ya instalado)
  • @nuxtjs/supabase v2 - modulo oficial, composables auto-importados
  • Supabase PostgreSQL - tablas, RLS, funciones
  • Leaflet - mapa explorador
  • Server routes Nitro wrapeando Flightics -> Supabase cache

Esquema de base de datos (Supabase/PostgreSQL)

Tablas de cache (datos de Flightics, publicas, sin RLS)

-- Aeropuertos, ciudades, paises, regiones (de getLocations + getCountries)
-- Se sincroniza via cron o al primer request, TTL 24h
CREATE TABLE airports (
  iata TEXT PRIMARY KEY,
  icao TEXT,
  name TEXT NOT NULL,
  lat DOUBLE PRECISION,
  lon DOUBLE PRECISION,
  city_id TEXT,
  city_name TEXT,
  country_code TEXT,
  country_name TEXT,
  region_slug TEXT,
  updated_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE countries (
  iso_code2 TEXT PRIMARY KEY,
  iso_code3 TEXT,
  name_eng TEXT,
  name_native TEXT,
  phone_prefix TEXT,
  updated_at TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE regions (
  slug TEXT PRIMARY KEY,
  name TEXT,
  name_localized TEXT,
  airport_codes TEXT[], -- array de IATA codes
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- Cache de inspiraciones (por aeropuerto origen, TTL 1h)
CREATE TABLE inspirations_cache (
  id SERIAL PRIMARY KEY,
  from_airport TEXT NOT NULL,
  to_airports TEXT[] NOT NULL,
  min_price NUMERIC(10,2),
  min_stops INTEGER,
  fetched_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(from_airport, to_airports)
);

-- Cache de multi-city inspirations (TTL 1h)
CREATE TABLE multi_city_cache (
  id SERIAL PRIMARY KEY,
  origin_codes TEXT[] NOT NULL,
  from_airport TEXT,
  stops TEXT[] NOT NULL,
  min_price NUMERIC(10,2),
  fetched_at TIMESTAMPTZ DEFAULT now()
);

Tablas de usuario (con RLS)

-- Perfiles de usuario (extends auth.users)
CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  home_airports TEXT[] DEFAULT '{}',
  default_adults INTEGER DEFAULT 1,
  default_children INTEGER DEFAULT 0,
  default_infants INTEGER DEFAULT 0,
  locale TEXT DEFAULT 'es',
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Watchlist: vuelos guardados para monitorizar precio
CREATE TABLE watchlist (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  booking_token TEXT NOT NULL,
  route_summary TEXT NOT NULL,        -- "MAD > NTE > MAD"
  departure_code TEXT,
  arrival_code TEXT,
  departure_date TEXT,
  original_price NUMERIC(10,2) NOT NULL,
  current_price NUMERIC(10,2),
  price_status TEXT DEFAULT 'saved',  -- saved, available, price_up, price_down, unavailable
  passengers_adult INTEGER DEFAULT 1,
  passengers_child INTEGER DEFAULT 0,
  passengers_infant INTEGER DEFAULT 0,
  last_checked_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Busquedas recientes
CREATE TABLE recent_searches (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  search_params JSONB NOT NULL,       -- el payload completo de busqueda
  route_summary TEXT,                  -- "AGP,GRX > NTE"
  search_mode TEXT DEFAULT 'roundtrip',
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Alertas de precio (futuro: cron verifica y notifica)
CREATE TABLE price_alerts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  watchlist_id UUID REFERENCES watchlist(id) ON DELETE CASCADE,
  target_price NUMERIC(10,2),         -- notificar si baja de este precio
  is_active BOOLEAN DEFAULT true,
  last_notified_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- RLS policies
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE watchlist ENABLE ROW LEVEL SECURITY;
ALTER TABLE recent_searches ENABLE ROW LEVEL SECURITY;
ALTER TABLE price_alerts ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users see own profile" ON profiles FOR ALL USING (auth.uid() = id);
CREATE POLICY "Users see own watchlist" ON watchlist FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "Users see own searches" ON recent_searches FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "Users see own alerts" ON price_alerts FOR ALL USING (auth.uid() = user_id);

Arquitectura server routes (Nitro)

Los server routes actuan como proxy inteligente: Flightics API -> cache en Supabase -> respuesta al frontend.

Rutas existentes (refactored con cache)

Ruta Logica
GET /api/locations Lee de tabla airports. Si vacia o >24h, llama a Flightics getLocations(), parsea y guarda.
GET /api/countries Lee de tabla countries. Si vacia o >24h, sincroniza.
GET /api/inspirations Lee de inspirations_cache si <1h. Si no, llama a Flightics, guarda en cache, retorna.
POST /api/search Proxy directo a Flightics searchTrips. Sin cache (resultados son efimeros).
POST /api/detail Proxy directo a Flightics.
POST /api/check Proxy directo a Flightics.
POST /api/weekend-search Proxy directo.
POST /api/route-flights Proxy directo (datos cambian frecuentemente).
POST /api/multi-city-inspirations Cache 1h en multi_city_cache, sino Flightics.

Rutas nuevas (usuario/Supabase)

Ruta Logica
GET /api/user/profile Lee profile del usuario autenticado
PUT /api/user/profile Actualiza home_airports, passengers, locale
GET /api/user/watchlist Lista watchlist del usuario
POST /api/user/watchlist Anadir vuelo a watchlist
DELETE /api/user/watchlist/[id] Eliminar de watchlist
POST /api/user/watchlist/[id]/check Verificar precio: llama checkTrip, actualiza en DB
POST /api/user/watchlist/check-all Verificar todos los precios del usuario
GET /api/user/recent-searches Ultimas 10 busquedas
POST /api/user/recent-searches Guardar busqueda reciente
POST /api/sync/locations Fuerza sync de airports/countries/regions desde Flightics a Supabase

Paginas (9)

/ - Home (discovery hub)

  • Hero con SearchBar compacto inline
  • Tabs rapidos: Ida y vuelta | Solo ida | Multi-ciudad | Finde | Explorar
  • Inspiraciones desde home airports del usuario (profile o localStorage si no logueado)
  • Carrusel multi-city inspirations
  • Widget "Donde por X€?" con slider presupuesto
  • Busquedas recientes del usuario (si logueado)

/auth - Login/Register

  • Login con email/password (Supabase Auth)
  • OAuth con Google
  • Registro
  • Redirect post-login

/search - Busqueda completa

  • 5 modos: standard, solo ida, multi-city, finde, explorar
  • AirportInput con autocomplete (datos de Supabase airports table)
  • Al buscar, guarda en recent_searches si logueado

/results - Resultados

  • Sort: precio, duracion, salida, escalas
  • Filtros: precio max, escalas, hora, aerolineas
  • Polling progresivo
  • Vista lista + compacta
  • Estrella watchlist (requiere login, o prompt to login)

/detail/[token] - Detalle

  • Timeline itinerario
  • Verificar precio
  • Vuelos relacionados (getRouteFlights)
  • Compartir
  • Watchlist toggle
  • CTA booking

/explore - Mapa explorador

  • Mapa Leaflet con aeropuertos de tabla airports (lat/lon)
  • Seleccionar origen -> inspiraciones como arcos
  • Slider presupuesto
  • Click destino -> popup
  • Toggle directos

/route/[from]-[to] - Explorador de ruta

  • getRouteFlights sin fechas
  • Todas las opciones de vuelo
  • Comparativa aerolineas

/multi-city - Inspiracion multi-ciudad

  • Input origenes -> getMultiCityInspirations
  • Tarjetas de itinerarios
  • Click -> busqueda multi-city

/watchlist - Lista seguimiento (requiere auth)

  • Tabla desde Supabase watchlist
  • Verificar precios individual / bulk
  • Badges de estado: precio bajo (verde), subio (rojo), no disponible (gris)
  • Alertas de precio (target price)
  • Busquedas recientes

Componentes (mismos del plan anterior + nuevos auth)

Auth

Componente Proposito
auth/LoginForm.vue Email/password + Google OAuth
auth/UserMenu.vue Avatar + dropdown: perfil, watchlist, logout. O boton login si no auth.
auth/AuthGuard.vue Wrapper que redirige a /auth si no logueado

Busqueda (sin cambios del plan anterior)

search/SearchBar, search/AirportInput, search/SearchModeTabs, search/StopsConfigurator, search/MaxStopsFilter, search/BudgetSlider, search/DateRangePicker, search/StayDurationPicker, search/PassengerPicker (en popover)

Resultados

results/TripCard, results/TripCardCompact, results/ResultsToolbar, results/ResultsFilters, results/LoadingMore

Detalle

detail/ItineraryTimeline, detail/SegmentCard, detail/PriceVerifier, detail/RelatedFlights, detail/ShareButton

Mapa

map/FlightMap, map/MapControls, map/MapDestinationPopup

Inspiracion

inspiration/InspirationGrid, inspiration/MultiCityCard, inspiration/BudgetExplorer

Watchlist

watchlist/WatchlistItem, watchlist/PriceAlertSetter


Composables

Existentes (refactored)

  • useFlightSearch - search con polling, sort, filter, weekend
  • useLocations - ahora lee de Supabase (tabla airports), con fallback a API
  • useInspirations - lee de cache Supabase, refresh si stale

Nuevos

  • useAuth - wrapper de useSupabaseUser() + useSupabaseClient(). Login, logout, register, profile CRUD.
  • useWatchlist - CRUD contra Supabase watchlist table. add, remove, checkPrice, checkAll, isWatched.
  • useRecentSearches - ultimas busquedas del usuario desde Supabase
  • useUserPreferences - lee/escribe profiles table. Home airports, default passengers.
  • useRouteFlights - fetch route flights
  • useResultFilters - sort/filter computeds client-side

Dependencias a instalar

pnpm add @nuxtjs/supabase leaflet @vue-leaflet/vue-leaflet

Orden de implementacion

Fase 0: Setup Supabase

  1. Crear proyecto Supabase (o usar existente)
  2. pnpm add @nuxtjs/supabase
  3. Configurar modulo en nuxt.config.ts + env vars
  4. Ejecutar SQL para crear tablas + RLS policies
  5. Crear server route POST /api/sync/locations para poblar airports/countries/regions

Fase 1: Auth + Infraestructura

  1. auth/LoginForm.vue + /auth page
  2. auth/UserMenu.vue en app.vue header
  3. useAuth composable
  4. useUserPreferences composable (lee/escribe profiles)
  5. useLocations composable (lee de Supabase airports table)
  6. search/AirportInput.vue con autocomplete

Fase 2: Busqueda completa

  1. SearchModeTabs + refactor SearchForm para 5 modos
  2. StopsConfigurator, MaxStopsFilter, BudgetSlider, DateRangePicker, StayDurationPicker
  3. PassengerPicker en popover
  4. /search page
  5. useRecentSearches + guardar busquedas en Supabase

Fase 3: Resultados mejorados

  1. useResultFilters composable
  2. Refactor useFlightSearch con polling + sort/filter
  3. ResultsToolbar + ResultsFilters
  4. TripCard mejorado + TripCardCompact
  5. LoadingMore polling indicator

Fase 4: Detalle + Watchlist

  1. /detail/[token] ruta dinamica
  2. ItineraryTimeline + SegmentCard
  3. PriceVerifier
  4. useWatchlist composable (Supabase CRUD)
  5. Watchlist toggle en TripCard y detail
  6. /watchlist page con verificacion de precios
  7. Server routes: /api/user/watchlist/*

Fase 5: Discovery

  1. pnpm add leaflet @vue-leaflet/vue-leaflet
  2. /explore + FlightMap + controles
  3. /route/[from]-[to] + useRouteFlights
  4. /multi-city + MultiCityCard
  5. RelatedFlights en detail page

Fase 6: Home + Polish

  1. Home refactored: SearchBar compacto, inspiraciones, multi-city carousel, BudgetExplorer, busquedas recientes
  2. Navegacion actualizada en app.vue (Buscar, Explorar, Multi-city, Watchlist)
  3. Cache de inspiraciones en Supabase
  4. Responsive polish

Configuracion Nuxt

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxt/eslint', '@nuxt/ui', '@nuxtjs/supabase'],
  supabase: {
    redirectOptions: {
      login: '/auth',
      callback: '/auth/confirm',
      exclude: ['/', '/search', '/results', '/explore', '/route/*', '/multi-city'],
    }
  },
  // ...
})

Las paginas publicas (home, search, results, explore, route, multi-city) funcionan sin auth. Watchlist y profile requieren login.


Verificacion

  • pnpm dev compila sin errores
  • Supabase: tablas creadas, sync de locations funciona
  • Auth: registro, login, logout, profile update
  • Home: inspiraciones, multi-city carousel, budget explorer, busquedas recientes
  • /search: 5 modos, autocomplete de aeropuerto lee de Supabase
  • /results: polling, sort/filter, watchlist star (pide login si no auth)
  • /detail/[token]: timeline, verificar precio, related flights, compartir
  • /explore: mapa con aeropuertos, arcos de precio, slider presupuesto
  • /route/MAD-NTE: todos los vuelos sin fechas
  • /multi-city: itinerarios sugeridos
  • /watchlist: CRUD, verificar precios, badges estado
  • Mobile responsive en todas las paginas