Aplicación offline-first en React Native / Expo para pilotos de aviación y operadores UAS (drones). No tiene backend: todos los datos se guardan localmente en SQLite, con lecturas externas de las APIs de NOAA y Google. El directorio mundial de aeropuertos proviene del dataset abierto de OurAirports (dominio público, CC0), sembrado en SQLite en el primer arranque.
- METAR — búsqueda con autocompletado sobre una base mundial de aeropuertos (~72k, OurAirports en SQLite), decodificación en español y cache offline de 6 horas (NOAA AWC).
- Índice Kp — gauge, gráfico de 24 h, pronóstico a 3 días y análisis de impacto (NOAA SWPC).
- Condiciones de vuelo (go / no-go) — banner resumen en Home y pantalla de detalle que combinan METAR + Kp en un nivel general (Buenas / Regulares / Malas) por el peor factor: viento, visibilidad, techo, índice Kp y fenómenos. Acceso al visor geográfico de zonas UAS de la Aerocivil (ArcGIS).
- Checklists — plantillas prearmadas (Cessna 172 VFR/post, genéricos, RPAS) y editor de checklists personalizados; la sección "Listas de chequeo" es solo catálogo (crear / clonar / ver). La ejecución (ok / N/A / omitido + razón) se hace únicamente al registrar un vuelo y queda vinculada a él.
- Bitácora tripulada — campos OACI con cálculo automático de tiempo de vuelo.
- Bitácora UAS / Drone — sitio de operación, captura GPS + geocodificación inversa, altitud y distancia máximas, VLOS/BVLOS, tipo de misión.
- Exportación / importación — JSON, CSV y PDF, con deduplicación al importar.
- i18n — español (fuente de verdad), inglés y portugués (pt-BR), con detección del idioma del sistema.
- Tema (Claro / Oscuro / Sistema) — selector en Configuración que fuerza el tema de toda la app o sigue la apariencia del teléfono; aplica al instante y persiste (SecureStore).
Expo SDK 54 · Expo Router (file-based) · TypeScript strict · Zustand (+ SecureStore) · TanStack Query · Expo SQLite (WAL) · NativeWind (Tailwind CSS) · date-fns · i18next.
yarn start # Servidor de desarrollo Expo (iOS / Android / web)
yarn ios # Simulador iOS
yarn android # Emulador Android
yarn web # Vista previa web
yarn lint # ESLint (flat config, eslint-config-expo) sobre todo el proyectoNo hay test runner configurado; la verificación se hace con yarn lint y yarn tsc --noEmit.
Requiere EXPO_PUBLIC_GOOGLE_MAPS_KEY en .env (con Maps Static API y Geocoding API habilitadas). Ver .env.example.
app/— rutas (Expo Router). Entrada enapp/_layout.tsx;app/index.tsxdecide entreapp/auth/yapp/(tabs)/.src/components/— componentes reutilizables (pickers, campos de rango de fecha/hora, etc.).src/db/— inicialización SQLite y acceso a datos.src/store/— estado Zustand (perfil, favoritos, locale, sesión).src/utils/— utilidades puras (validación de rangos, exportación/importación).src/i18n/— traducciones (es/en/pt).docs/— PRD, estrategia por ticket (docs/tickets/) y diseño.
Alias de import: @/* → ./src/*.
El producto se especifica en docs/PRD.md. Las estrategias de implementación por ticket viven en docs/tickets/.
- 2026-08-02 — PAR-18 (M1): se centraliza el sistema de diseño de color, que estaba duplicado (351+ literales
#RRGGBBrepartidos, colores de categoría de vuelo repetidos, y el objetoconst Cidéntico copiado en las pantallas de auth). Nueva fuente única:src/theme.ts(tipada, sinany) que exponebrand(paleta oscura fija de onboarding/auth),palette/getColors(scheme)(paleta light/dark "slate" para migración incremental y consumo por el hook de esquema de PAR-7) y los colores de categoríaflightCategoryColors/flightCategoryBgColors. Los tokens que además necesita Tailwind viven ensrc/theme.tokens.js(CommonJS, ya quetailwind.config.jscorre en Node y no puede importar TS);tailwind.config.jslosrequireysrc/theme.tslos re-exporta tipados, de modo que VFR/MVFR/IFR/LIFR y el fondo de marca tienen una sola definición.src/utils/metar.ts(categoryColor/categoryBg) pasa a referenciar esos tokens en vez de literales. Las 3 pantallas de auth (signup,signin,disclaimer) consumenbrand— se elimina elconst Cduplicado. Se corrige la inconsistencia histórica del fondo de marca (#0f172avs#0c1a2e) unificándolo a#0c1a2e(sky-950, según CLAUDE.md): el loader deapp/index.tsxahora usabrand.bg. Sin rediseño visual: los valores migrados son idénticos a los previos (Tailwind resuelve los mismos hex; único cambio perceptible es el fondo del loader, que era el objetivo). La migración del resto de pantallas (paleta "Twitter" de kp/metar-detail/logbook/flight, incl. su set divergente de colores de categoría#16A34A…) queda como lote incremental futuro, como prevé el ticket. Verificado:tsc --noEmitynpm run lintlimpios; conteo de hex en los archivos migrados baja de 53 a 9 (los restantes son one-offs semánticos: blancos, ámbar de advertencia, tinte de error). Estrategia:docs/tickets/PAR-18.md. - 2026-08-02 — PAR-37: se reemplaza la lista hardcodeada
COLOMBIA_AIRPORTS(~35 aeropuertos) por una base mundial de aeropuertos servida desde SQLite, manteniendo el enfoque offline-first (sin red en runtime). Fuente: dataset abierto de OurAirports (dominio público / CC0). Un script de build (scripts/build-airports-seed.mjs) filtra el CSV (assets/airports.csv) — descartaclosed/balloonport, conserva large/medium/small_airport, heliport y seaplane_base — y genera un JSON compacto array-of-arrays (assets/airports.dat, ~72k filas, ~6.3 MB) empaquetado como asset de Metro (extensióndatañadida enmetro.config.js, no se inlinea en el bundle JS). Nueva tablaairport+ índices (icao/iata/country/name) enrunMigrations()(src/db/database.ts); su PK es elidentde OurAirports (excepción documentada al patrón UUID del resto del esquema). El seed corre una sola vez en el primer arranque dentro de una transacción WAL por lotes (100 filas), es idempotente y versionado víaapp_settings.airport_seed_version(reinstalar/reabrir no duplica ni re-siembra; subirAIRPORT_SEED_VERSIONfuerza reseed limpio). Nuevo módulosrc/db/airports.ts(getAirportByCode,searchAirports,getAirportsByCountry) reemplaza los.find()/.filter()en memoria; consumidores migrados a consultas async víauseQueryenapp/(tabs)/metar.tsx,app/metar/[icao].tsxyapp/flight/[id].tsx. Se eliminasrc/data/airports.tsy el tipoAirportEntry. La búsqueda ahora sugiere aeropuertos de cualquier país por código o nombre (EGLL, RJTT, FAOR…), funcionando offline. Aviso: el dataset de OurAirports es solo referencia, no apto para navegación aeronáutica real. Verificado:tsc --noEmitynpm run lintlimpios. Estrategia:docs/tickets/PAR-37.md. - 2026-07-09 — PAR-25 (M8): se cierra el hueco de calidad de datos por el que un vuelo podía guardarse con 0.0 h sin aviso. En
app/flight/new.tsx, cuando el total de horas se deriva del intervalo de bloque (PAR-13) y el usuario dejablockOut/blockInvacíos, el total caía a 0 yhandleSavelo persistía en silencio. Ahora, tras las validaciones existentes (fecha, ruta/sitio, rango PAR-5),handleSavedetecta intervalo de bloque incompleto o total derivado igual a 0 y muestra unAlertde confirmación ("¿Guardar sin horas?") con dos acciones: Cancelar (permanece en el formulario) y Guardar de todos modos (persiste el registro). Se optó por confirmación en vez de bloqueo duro para no impedir casos legítimos, rompiendo el silencio. La lógica de guardado se extrajo apersistFlight()para invocarse directa o tras confirmar. El flujo normal (total > 0) no muestra fricción. Sin cambios de esquema; claves i18n nuevasflight.new.zeroHoursTitle/zeroHoursMsg/zeroHoursConfirmen es/en/pt. Verificado:tsc --noEmitlimpio. Estrategia:docs/tickets/PAR-25.md. - 2026-07-09 — PAR-28 (L3): se elimina la duplicación de
fetchWithTimeout, que estaba copiada idéntica ensrc/api/awc.tsysrc/api/swpc.ts. Ahora vive en un único módulosrc/utils/fetchWithTimeout.ts(función pura y tipada,fetch+AbortControllercon timeout en ms,clearTimeoutenfinally); ambos clientes de API la importan vía../utils/fetchWithTimeout. Sin cambios de comportamiento (mismo timeout de 8 s en METAR y Kp) ni de esquema ni i18n.grep -rn "function fetchWithTimeout" srcconfirma una sola definición ynpx tsc --noEmitqueda limpio. Base para PAR-22 (M5), que reutilizará el helper para las llamadas de geocoding/Wikipedia sin timeout. Estrategia:docs/tickets/PAR-28.md. - 2026-07-09 — PAR-15 (A2): se deja funcional la red de seguridad de lint. El script
npm run lint(eslint .) fallaba porque ESLint no estaba instalado ni configurado. Ahoraeslint@^9yeslint-config-expo@~10están endevDependenciescon una flat config (eslint.config.js) que extiendeeslint-config-expo/flate ignoradist/node_modules/.expo. Se corrigieron las violaciones triviales que afloraron: import muertoDownloadenapp/(tabs)/logbook.tsx(evidencia original del ticket), imports/variables sin usar (useEffectensignin,Animatedensignup,Platform/useMutation/Circle/statusColorenchecklist/[id]/execute,setPicTimeenflight/new,isDark+ su import enFlightConditionsBanner,forecastLoadingenkp) y las reglas de estilo autofixables (array-type→T[]). El único hallazgo restante (import/no-named-as-default-membersobrei18n.use(...), un falso positivo del patrón de encadenamiento de i18next) se silencia puntualmente con uneslint-disable-next-linejustificado. Resultado:npm run linttermina en 0 errores, 0 warnings ynpx tsc --noEmitsigue limpio. El comando queda documentado como paso de verificación (el proyecto no tiene test runner). Habilita PAR-26 (L1) y PAR-27 (L2). Estrategia:docs/tickets/PAR-15.md. - 2026-07-09 — PAR-35 (fix): el Home a veces quedaba en blanco al navegar desde Kp. El botón "← Home" de
app/(tabs)/kp.tsxhacíarouter.navigate('/'), y la ruta/esapp/index.tsx(EntryScreen), un gate de auth que solo muestra unActivityIndicatory hacía la redirección real dentro de unuseEffect(..., [])que corre únicamente al montar; si esa instancia de/seguía viva en el stack (Expo Router puede reusarla sin remontar), el efecto no volvía a ejecutarse y la pantalla se quedaba en el loader/blanco. Se corrigió el destino arouter.navigate('/(tabs)')(el tab Home ya montado) y, como defensa en profundidad, se endurecióEntryScreenpara que resuelva el estado de auth y renderice un<Redirect href=... />declarativo de expo-router en vez de navegar imperativamente en eluseEffect: aunque se vuelva a entrar a/estando ya montada, la redirección siempre ocurre y nunca queda un loader colgado. Sin cambios de esquema ni i18n. Verificado:tsc --noEmityeslintlimpios (solo warnings preexistentes ajenos al cambio), grep confirma que ya no hay otrosnavigate('/')/push('/'). Estrategia:docs/tickets/PAR-35.md. - 2026-07-08 — PAR-14 (fix): se activa la aplicación de claves foráneas en SQLite.
runMigrations()(src/db/database.ts) solo ejecutabaPRAGMA journal_mode = WAL, nuncaPRAGMA foreign_keys = ON, por lo que losON DELETE CASCADEdechecklist_section/checklist_item/checklist_item_resultno se aplicaban: borrar un vuelo (deleteExecutionsByFlight) o un checklist (deleteChecklist) dejaba filas huérfanas acumulándose. Ahora se ejecutaPRAGMA foreign_keys = ON;al inicio derunMigrations(fuera de transacción, conexión única reutilizada) y se añade una migración de limpieza idempotente (DELETE ... NOT IN, orden hijo→nieto) que elimina los huérfanos preexistentes y es segura de re-ejecutar. Sin cambios de esquema ni i18n; los flujos de INSERT ya insertan padre-primero, así que activar la aplicación no rompe la creación de checklists/ejecuciones. Verificado: cascadas y limpieza correctas (test SQLite),tsc --noEmitlimpio. Ajuste de code review: el barrido de huérfanos pasa a orden padre→hijo (sección antes que ítem); con el orden previo (hijo→padre) un ítem que solo queda huérfano porque su sección también lo está sobrevivía hasta el siguiente arranque, ahora se elimina en la misma pasada. Estrategia:docs/tickets/PAR-14.md. - 2026-07-08 — PAR-12: limpieza de claves i18n muertas.
checklists.list.executeychecklists.detail.executequedaron sin uso desde PAR-9 (que retiró el botón "Ejecutar" del catálogo de checklists) y se eliminaron dees.ts,en.tsypt.ts.checklists.execute.*(pantalla de ejecución) no se toca. Cambio cosmético, sin impacto funcional. Estrategia:docs/tickets/PAR-12.md. - 2026-07-08 — PAR-10 (fix): el anexo "Checklists ejecutados" del PDF exportado mostraba el UUID de
checklist_item.iden vez del título legible, rompiendo la trazabilidad checklist↔vuelo (O2 del PRD). Ensrc/utils/flightIO.ts,buildChecklistAnnexHtmlahora recibe un mapaitemId → title(nueva funciónbuildChecklistItemTitles, que resuelve cada checklist único víagetChecklistByIddesrc/db/checklists.ts) y lo usa para renderizar cada ítem, con fallback alitemIdsi el checklist original fue borrado.buildHtmlpasa a serasyncpara resolver ese mapa antes de armar el HTML;exportFlights(rama PDF) haceawaiten consecuencia. CSV y JSON no cambian (fuera de alcance — el JSON exponeitemIdcrudo, cruzable por el consumidor). Sin cambios de esquema ni i18n. Estrategia:docs/tickets/PAR-10.md. - 2026-07-08 — PAR-13: en "Registrar vuelo" (
app/flight/new.tsx), las horas de bloque se prellenan al crear un vuelo nuevo:blockOut= hora actual,blockIn= hora actual + 2h (vacío si cruza medianoche, es decir hora actual ≥ 22:00); el prellenado usainitialDraft?.blockOut ?? defaultBlockOutpara no pisar un borrador restaurado (PAR-9) ni datos ya diligenciados. El tiempo total de vuelo (totalTime) deja de ser editable a mano y pasa a derivarse siempre del intervaloblockOut→blockIn, recalculando en cada cambio (efecto sin la guarda previa de "solo si está vacío"); intervalo incompleto o inválido (fin < inicio, según PAR-5) → total = 0. Los campos de total en tripulado y UAS pasan aeditable={false}con opacidad reducida. Se simplificahandleSave: ya no recalcula el total de forma defensiva porque siempre viene derivado. ReutilizaminutesOfDay/isValidTimeRangedesrc/utils/dateTimeRange.ts(PAR-5); sin cambios de esquema SQLite ni claves i18n nuevas. Estrategia:docs/tickets/PAR-13.md. - 2026-07-03 — PAR-9: la ejecución de checklists se traslada a la creación de vuelo y queda vinculada a la bitácora. En
app/flight/new.tsxse agregó una sección para elegir y ejecutar uno o más checklists antes de guardar (app/checklist/[id]/execute.tsxahora soporta el parámetroreturnTopara volver al formulario tras completar); al guardar el vuelo,linkExecutionsToFlightvincula las ejecuciones completadas (checklist_execution.linked_flight_id), y si el usuario sale sin guardar, las ejecuciones huérfanas se borran. Se retiró el botón "Ejecutar" deapp/(tabs)/checklists.tsxyapp/checklist/[id]/index.tsx— el catálogo ya no ejecuta ni guarda resultados.app/flight/[id].tsxexpande "Checklists asociados" a detalle ítem por ítem (estado + motivo de omisión), no solo el conteo agregado. Nuevas funciones ensrc/db/checklists.ts:linkExecutionsToFlight,getExecutionsForFlights,deleteExecutionsByFlight,deleteExecution;saveItemResult/completeExecutionahora rechazan escribir sobre una ejecución ya completada (inmutabilidad post-cierre).deleteFlightensrc/db/flights.tsborra en cascada las ejecuciones/resultados vinculados al vuelo (el checklist del catálogo nunca se toca). Exportación (src/utils/flightIO.ts): JSON incluye el detalle de checklist por vuelo, CSV agrega columnas resumen (checklists_ejecutados,items_ok,items_na,items_omitidos) y PDF agrega un anexo por vuelo. Sin cambios de esquema SQLite. Claves i18n nuevas bajoflight.new.checklists*ydoc.checklist*en es/en/pt. Estrategia:docs/tickets/PAR-9.md. - 2026-06-25 — PAR-8: en la ejecución de checklist, el scroll vuelve al inicio al cambiar de sección. En
app/checklist/[id]/execute.tsxse añadió unreftipado alScrollViewy unuseEffectdependiente desectionIdxque llamascrollTo({ y: 0, animated: false }), evitando que la nueva sección herede el offset de la anterior. Arreglo de UI aislado, sin cambios de esquema ni i18n. - 2026-06-23 — PAR-7: selector de tema (Claro / Oscuro / Sistema) en Configuración. Nuevos:
src/store/themeStore.ts(preferencia persistida en SecureStore con clavepl-theme, espejo delocaleStore) ysrc/hooks/useAppColorScheme.ts(resuelve el esquema efectivo). El cambio se conduce vía NativeWind (setColorScheme) para mantener en sync los estilos inlineisDarky las clasesdark:. Se migraron los 18 consumidores deuseColorScheme()de React Native al hookuseAppColorScheme();app/_layout.tsxcarga la preferencia al arrancar y deriva laStatusBardel esquema efectivo. Nueva sección "Tema" enapp/settings.tsxy claves i18nsettings.theme*en es/en/pt. - 2026-06-17 — PAR-6: banner de condiciones de vuelo en Home + pantalla de detalle
flight-conditions. Evaluación go/no-go (peor factor) sobre datos METAR + Kp existentes. Nuevos:src/utils/flightConditions.ts,src/components/FlightConditionsBanner.tsx,app/flight-conditions.tsx; helpergetLastUasFlightLocationensrc/db/flights.tspara centrar el visor UAS de ArcGIS. Botón "Pronóstico" en Quick Actions y ruta stack registrada enapp/_layout.tsx. Claves i18nflightConditions+home.forecasten es/en/pt. Nota: el gradiente del banner se aproxima conViews para evitar agregar el módulo nativoexpo-linear-gradient. - 2026-06-17 — PAR-5: utilidad y componentes reutilizables de rango de fecha/hora que garantizan
inicio ≤ fin. Nuevos:src/utils/dateTimeRange.ts,src/components/DateRangeField.tsx,src/components/TimeRangeField.tsx. La validación de hora se aplica ablockOut/blockInen el formulario de vuelo (no se puede guardar con la llegada anterior a la salida; el autocálculo de tiempo total usa resta directa en lugar deMath.abs). Claves i18ndateTimeRangeen los tres idiomas.