Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Pagina dedicada a viajes
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
• docs/administrar.md — cómo operar el sitio con seguridad: cuentas, claves, rutina y qué hacer si algo pasa. • docs/arquitectura.md — cómo está armado, para quien recibe el proyecto. • docs/datos.md — diccionario de datos y reglas de los datos maestros. • docs/seguridad.md — amenazas, controles y la auditoría del 2026-10-08. • docs/agregar-un-pais.md — sumar un país sin sorpresas.README 전문 보기 ↗
Documentación
docs/administrar.md— cómo operar el sitio con seguridad: cuentas, claves, rutina y qué hacer si algo pasa.docs/arquitectura.md— cómo está armado, para quien recibe el proyecto.docs/datos.md— diccionario de datos y reglas de los datos maestros.docs/seguridad.md— amenazas, controles y la auditoría del 2026-10-08.docs/agregar-un-pais.md— sumar un país sin sorpresas.
Aplicación web sin registro que resuelve dos cosas para viajar: qué empacar y cuánto vas a gastar. Nació para destinos con alta volatilidad económica y multiplicidad cambiaria —el MVP fue un solo corredor, Buenos Aires— y hoy cubre América Latina completa, casi toda Europa, casi toda Asia, casi toda África y Oceanía completa, con el clima de cada ciudad y el presupuesto en moneda local.
La especificación completa vive en spec-mvp-kit-viaje.md
y es la fuente de verdad del proyecto — se implementa por secciones, en orden
de dependencias.
MVP completo contra el spec. Las siete secciones implementadas y los siete criterios de aceptación de la sección 7 cumplidos.
| Sección | Qué es | Estado |
|---|---|---|
| 3 | Modelo de datos + RLS | ✅ |
| 4 | Motor de packing | ✅ |
| 5 | Motor de presupuesto | ✅ |
| 6 | Flujo de usuario y persistencia | ✅ |
| 7 | Stack, testing, deploy | ✅ |
| 8 | Guía de destino | ✅ |
| 9 | Página de preparación | ✅ |
Cuatro páginas, en este orden. Cada una responde una pregunta y recién después pide la siguiente decisión.
| # | Ruta | Qué responde | Cómo se renderiza |
|---|---|---|---|
| 1 | / | ¿Adónde? — el globo, sin hablar del país | Estática |
| 2 | /guia/[slug] | ¿Qué me espera? — números, cotizaciones en vivo, tablero, puntajes, destinos y mapa | Estática + <Suspense> para las cotizaciones |
| 3 | /guia/[slug]/preparar | ¿Cuándo conviene ir y qué llevo? — el año climático, mes a mes, y la checklist | Dinámica; las lecturas de la base, en caché 1 h |
| 4 | /guia/[slug]/planificar | ¿Qué necesito para MIS fechas? — el generador | Dinámica; las ciudades, en caché 1 h. El viaje se crea en la Server Action |
Las páginas 3 y 4 son dinámicas y no prerenderizadas a propósito: leen
Supabase, y con generateStaticParams un hipo de la base durante el build no
rompería una request, rompería el deploy entero. Lo que se cachea son las
lecturas (src/lib/supabase/cached.ts): una hora, y por deploy.
| # | Criterio | Cómo se verifica |
|---|---|---|
| 1 | Crear un viaje sin cuenta y llegar a las dos listas, con todo en cero | src/lib/trips/item-write.test.ts y el flujo en src/lib/trips/ |
| 2 | El presupuesto se recalcula con las 4 cotizaciones | src/lib/quotes/map.test.ts |
| 3 | El share_slug abre en solo lectura | Ruta /viaje/ver/[shareSlug] con isReadOnly |
| 4 | Exportar a PDF y CSV | src/lib/export/csv.test.ts y CSS de impresión |
| 5 | El dashboard privado avisa que el link es la única forma de volver | src/components/trip/share-controls.tsx |
| 6 | Los tests de los motores pasan en CI | Job web del workflow |
| 7 | Ninguna escritura sin edit_token válido | Job database: 38 aserciones en supabase/tests/rls_smoke.sql |
El criterio 5 reemplaza al original, que pedía un historial de "viajes
recientes" en localStorage. Esa función se eliminó entera —módulo, efecto y
UI— porque un historial en el navegador promete una permanencia que no puede
cumplir: se pierde al cambiar de dispositivo o limpiar el sitio, y quien confió
en él perdió el viaje. El aviso al lado del link dice la verdad en su lugar
(spec, 6C).
Fuera del MVP y sin planificar: cuentas, i18n y tracking de gastos reales. La sección 2 del spec explica el porqué de cada uno. La monetización entra como experimento, con los links de afiliado de la sección de vuelos.
Una sección nueva, /vuelos, para planear desde la ciudad de cada uno: qué está
volando, a dónde se llega y cuánto cuesta (spec, sección 17). Va por pasos, un
PR cada uno. Ya están:
Se enlaza desde la portada.
new-york, base neutral)edit_tokenwindow al evaluarse y rompe el prerender si se importa arribaLas migraciones son archivos versionados en supabase/migrations/, en el
formato del CLI de Supabase (<timestamp>_<nombre>.sql). Se aplican en orden
alfabético y no se editan una vez aplicadas: los cambios van en una migración
nueva. El diccionario de datos y las reglas de los datos maestros están en
docs/datos.md.
Las de nombre _corredor_<pais> siembran un país cada una (ciudades, clima y
precios) y se generan con herramientas/corredor/generar.py. Las demás son
estructurales:
| Migración | Contenido |
|---|---|
20260826120000_reference_tables.sql | destinations, climate_profiles, climate_thresholds, products, packing_catalog |
20260826120100_session_tables.sql | trips, trip_packing_items, trip_budget_items |
20260826120200_rls_policies.sql | RLS de los dos grupos de tablas |
20260826120300_seed_climate_thresholds.sql | Buckets de clima iniciales |
20260826140000_precip_probability_scale.sql | precip_probability acotada a 0-100 |
20260827100000_seed_reference_data.sql | Destino, clima, catálogo y precios de Buenos Aires |
20260831040000_ezeiza_por_tramo.sql | El traslado del aeropuerto se cobra por tramo, no por día |
20260902060000_qty_desde_cero.sql | qty >= 0 y default 0 en las tablas de sesión |
20260910100000_destinos_por_ciudad.sql | Varias ciudades por corredor, una base |
20260910140000_slug_de_ciudad.sql | La ciudad en la URL |
20261008120000_endurecer_permisos.sql | Solo SELECT para anon; lo nuevo nace cerrado |
20261008120100_cupo_de_creacion.sql | Fecha de alta, cupo de creación de viajes y purga manual |
20261008120200_moneda_coherente.sql | Precios siempre en la moneda de su ciudad |
Aplicarlas:
npx supabase db push # contra el proyecto remoto
npx supabase start # o levantar Supabase local con Docker
Los datos de referencia se leen desde cualquier lado y no se escriben desde el
cliente. Los datos de sesión no se tocan desde el cliente en absoluto: todo
pasa por Server Actions que validan el edit_token y usan la service role key.
Esto es más restrictivo que lo que pide la sección 3 del spec, y el porqué está
documentado arriba de todo en 20260826120200_rls_policies.sql.
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f supabase/tests/rls_smoke.sql
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f supabase/tests/integridad.sql
El smoke son 38 aserciones sobre RLS, privilegios, tokens, constraints, el cupo
de creación y los triggers. La de integridad verifica los datos maestros: doce
meses de clima por ciudad, una base por país, precios en la moneda de su
ciudad. Las dos corren dentro de una transacción y terminan con rollback, así
que se pueden correr contra una base con datos.
La lógica de negocio vive en funciones puras que no saben que Supabase existe: reciben los datos ya leídos y devuelven el resultado. Persistir es trabajo de la Server Action que las llama. Es lo que las hace testeables sin base de datos, y es donde el spec pone el foco de testing (sección 7).
src/lib/packing)import { generatePackingList } from "@/lib/packing";
const lista = generatePackingList({
trip: { startDate: "2026-04-16", endDate: "2026-05-15", tripType: "urbano" },
climateProfiles,
climateThresholds,
catalog,
});
Determinístico y sin LLM. Resuelve los meses que toca el viaje, mapea cada uno a buckets de clima, filtra el catálogo por clima × tipo de viaje y escala las cantidades por duración.
Devuelve además monthsWithoutClimateData: si a un mes del viaje le falta la
fila en climate_profiles, la lista sale más corta de lo que debería y quien
llama tiene que poder avisarlo en vez de mostrar una lista incompleta sin
explicación.
src/lib/budget)import { calculateBudget, generateBudgetList, selectQuote } from "@/lib/budget";
const lineas = generateBudgetList(trip, products); // una vez, al crear el viaje
const totales = calculateBudget(lineas, selectQuote(quotes, "blue")); // en cada render
Las cotizaciones entran como dato, no las va a buscar el motor: así el cálculo se testea con valores fijos y no depende de una API externa. El monto convertido no se persiste nunca — se recalcula contra la cotización vigente.
Dos detalles que no son obvios:
Cuatro rutas leen Supabase en cada request. Sin red de contención, cualquier hipo de la base cae en la pantalla por defecto de Next —"Application error: a server-side exception has occurred"—, en inglés y sin salida. Hay tres archivos para eso:
| Archivo | Cuándo aparece |
|---|---|
src/app/not-found.tsx | Un slug que no existe. Lo llaman las tres rutas de guía |
src/app/error.tsx | Falla el render de una ruta: Supabase caído, una lectura que revienta |
src/app/global-error.tsx | Falla el layout raíz. Reemplaza el <html> entero, así que no puede usar nada del layout — de ahí los estilos inline |
No se muestra error.message. En producción Next ya lo reemplaza por uno
genérico, pero en desarrollo llega entero, y los errores de lectura incluyen el
texto de Postgres: eso es para los logs del servidor, no para la pantalla de
alguien que solo quiere armar una valija. Lo que sí se muestra es el digest,
que es con lo que se encuentra el error real en los logs y no dice nada de la
base. Las escrituras de viajes muestran un mensaje fijo y, como mucho, el
código SQLSTATE.
Cada push y cada pull request corren dos jobs:
web — lint, tests, build y typecheck. El build va antes del typecheck
a propósito: next build genera los tipos de las rutas (PageProps,
LayoutProps) en .next/types, y sin ellos tsc --noEmit falla en un
checkout limpio por archivos que todavía no existen.
database — levanta un Postgres 16, aplica las migraciones en orden y
corre el smoke test de RLS y el chequeo de integridad de los datos maestros.
Cubre el criterio de aceptación 7 del spec —ninguna escritura a las tablas de
sesión es posible sin un edit_token válido— y además que ninguna tabla quede
sin RLS ni ninguna función ejecutable desde la API pública, verificado en cada
PR y no una sola vez a mano.
Las actions van fijadas por commit (SHA) y el CLI de Supabase con versión fija; Dependabot abre un PR cada semana con lo que haya que actualizar.
Los dos badges de arriba son dos preguntas distintas. El de CI dice si el código está sano; el de Migraciones, si la base de producción está al día. Se puede tener el primero en verde y el segundo en rojo durante semanas, y ahí la app queda pidiéndole a Postgres columnas que no existen. Pasó: el workflow de migraciones falló sus cuatro primeras corridas seguidas —le faltaban los tres secrets— y nadie lo miró, porque el badge del README solo mostraba CI.
Casi siempre es lo mismo: faltan los tres secrets. El paso de verificación imprime cuáles, así que el log del run lo dice sin que haya que adivinar.
Se cargan en Settings → Secrets and variables → Actions, en la pestaña Secrets y como Repository secrets. Los tres errores habituales son cargarlas en la pestaña "Variables" de al lado, cargarlas como Environment secrets de un environment que el workflow no declara, o un typo en el nombre.
Mientras tanto la base se puede poner al día desde una terminal, sin GitHub de por medio:
npx supabase link --project-ref <ref>
npx supabase db push
Un tercer workflow, migraciones.yml,
corre supabase db push cuando un merge a main toca
supabase/migrations/, y también a mano desde la pestaña Actions. Existe
porque una migración que vive en el repo pero no está aplicada es
indistinguible de un bug: la app pide una columna que no existe y el error
aparece en la cara del usuario, no en CI. Necesita tres secrets cargados en
GitHub y los verifica antes de intentar nada.
npm install
cp .env.example .env.local # completar con las claves de Supabase
npm run dev # http://localhost:3000
Otros comandos:
npm run lint # ESLint
npm run typecheck # tsc --noEmit
npm test # Vitest (una corrida)
npm run test:watch # Vitest en watch
npm run build # build de producción
docs/ # administración, arquitectura, datos, seguridad
herramientas/
├── corredor/ # generador de las migraciones de cada país
└── aeropuertos/ # generador de la lista de aeropuertos (OurAirports)
supabase/
├── migrations/ # historial versionado del esquema y los datos
└── tests/
├── rls_smoke.sql # permisos, RLS, constraints, cupo y triggers
└── integridad.sql # reglas de los datos maestros
src/
├── app/ # rutas del App Router
│ ├── layout.tsx # pie con el aviso de analytics y el sello de build
│ ├── error.tsx # red de contención de las rutas
│ ├── global-error.tsx # último recurso: falla el layout raíz
│ ├── not-found.tsx # 404 propio
│ ├── page.tsx # 1 · landing: el globo
│ ├── robots.ts # todo indexable menos /viaje/ y /api/
│ ├── sitemap.ts # las guías, desde el índice de contenido
│ ├── guia/[slug]/
│ │ ├── page.tsx # 2 · guía del país
│ │ ├── preparar/ # 3 · condiciones actuales
│ │ └── planificar/ # 4 · el generador
│ ├── viaje/
│ │ ├── [editToken]/ # dashboard privado
│ │ └── ver/[shareSlug]/ # vista compartida, solo lectura
│ ├── vuelos/ # el radar en vivo y "Salgo desde" (sección 17)
│ └── api/
│ ├── aeropuertos/ # la búsqueda del selector, cacheada en la CDN
│ └── vuelos/en-vivo/ # los aviones de una zona del radar
├── components/
│ ├── landing/ # globo y formulario de viaje nuevo
│ ├── guia/ # tablero, puntajes, cotizaciones, mosaico, mapa
│ ├── preparar/ # tira de meses, gráficos, checklists, tabla, FAQ
│ ├── trip/ # listas, totales, controles de compartir
│ ├── vuelos/ # el radar, el buscador y las opciones de aeropuerto
│ ├── analytics.tsx # Vercel Analytics sin los links privados
│ └── ui/ # componentes de Shadcn
├── content/guias/ # contenido editorial de las guías (sección 8.2)
└── lib/
├── packing/ # motor de packing (sección 4)
│ ├── dates.ts # meses cubiertos y duración, todo en UTC
│ ├── climate.ts # resolución de buckets de clima
│ └── engine.ts # generatePackingList()
├── budget/ # motor de presupuesto (sección 5)
│ ├── quotes.ts # selección de cotización y tasa
│ ├── money.ts # aritmética en centavos enteros
│ ├── freshness.ts # antigüedad de los precios
│ └── engine.ts # generateBudgetList() / calculateBudget()
├── prepare/ # el año climático de la página 3 (sección 9)
├── quotes/ # dolarapi: fetch, mapeo y spread
├── trips/ # Server Actions, validación, lectura, escrituras y cupo
├── vuelos/ # aeropuertos, búsqueda, cercanía, ubicación, cookie y aviones en vivo
├── supabase/ # clientes, lectura de referencia y su caché
├── seguridad/ # headers HTTP y redacción de URLs
├── sitio/ # dominio público, para robots y sitemap
├── export/ # CSV
├── antimeridiano.ts # pines del mapa de un país que cruza el meridiano 180
├── quantity.ts # escalado por duración, usado por los dos motores
├── version.ts # el commit que está corriendo
└── utils.ts # cn()
Shadcn no es una dependencia: copia el código del componente al repo. Para agregar uno nuevo:
npx shadcn@latest add select tabs checkbox
Ver .env.example. SUPABASE_SERVICE_ROLE_KEY es
server-only y nunca debe llevar el prefijo NEXT_PUBLIC_: es la que usan las
Server Actions para escribir, después de validar el edit_token, y el secreto
con el que se hashea la IP del cupo de creación. Las dos NEXT_PUBLIC_ también
se usan solo en el servidor: el navegador nunca habla con Supabase.
Dónde vive cada una y qué hacer si se filtra:
docs/administrar.md.