Revise toda la app a profundidad: codigo, servicios, pagos, PWA, UX. Separe todo en lo que SI hay que arreglar (bugs reales) y lo que podria mejorar (consideraciones del equipo).
Estos son defectos verificados en el codigo. Afectan directamente a los usuarios, la seguridad, o impiden que la app funcione correctamente. No son opiniones.
vite.config.ts referencia favicon/favicon.svg en includeAssets, pero ese archivo no existe en public/. Ademas pesaria 8MB, excediendo el limite de Workbox. Resultado: el Service Worker nunca se genera y la PWA no tiene offline.
El <script src="https://cdn.tailwindcss.com"> carga el compilador JIT en runtime (~400KB). Tailwind documenta que el Play CDN es solo para desarrollo. El Service Worker no cachea scripts externos. Resultado: sin internet = app completamente sin estilos.
El JS principal pesa 951KB minificado. En moviles con 3G esto son 5-15 segundos de pantalla blanca. La dependencia firebase (~400KB) esta en package.json pero ningun archivo de la app lo importa — solo se usa en functions/.
La key se carga con import.meta.env.VITE_GROQ_API_KEY. Cualquier variable VITE_ se embebe en el bundle del browser. El dangerouslyAllowBrowser: true lo confirma. Cualquiera puede abrir DevTools, extraer la key, y hacer llamadas ilimitadas a tu cuenta.
dangerouslySetInnerHTML={{ __html: wiki.snippet }} renderiza HTML crudo de la API de Wikipedia directamente en el DOM. Un atacante que manipule la respuesta o un snippet inesperado podria inyectar scripts.
chatHistory es una variable de modulo que persiste entre sesiones. Si usuario A cierra sesion y B inicia, B recibe el contexto de la conversacion de A (contenido de terapia/espiritual). Ademas, localStorage (obsidiana_dreams, obsidiana_bitacoras, obsidiana_agenda, etc.) NO se limpia en logout.
Es un componente funcional con useState. React requiere un class component con getDerivedStateFromError() para capturar errores de render. Si cualquier hijo crashea, la app entera muestra pantalla blanca. Ademas, el ErrorFallback llama useApp() que depende de AppProvider — si el error viene de ahi, el fallback tambien crashea (doble crash).
El useEffect depende de [user.name, loading]. Dentro llama setLoading(false), que cambia loading, que re-ejecuta el effect, que vuelve a llamar loadMessages()... Loop infinito que hace fetch a Supabase cientos de veces por segundo y re-suscribe canales de realtime en cada ciclo.
Los timestamps se formatean a strings como "2 may 2026, 10:30" y luego se intenta new Date("2 may 2026, 10:30").getTime() que retorna NaN en la mayoria de browsers. Ordenar con NaN produce orden inestable/aleatorio.
Ningun webhook handler escucha customer.subscription.deleted, subscription.updated, ni invoice.payment_failed. Una vez que is_premium = true, nunca regresa a false. Usuarios que cancelan o cuyo pago falla mantienen premium para siempre.
El handler de Firebase escribe en Firestore, el de Supabase escribe en la tabla profiles. Si Stripe apunta a uno solo, el otro nunca se actualiza. Ademas, el handler de Firebase da premium por defecto a cualquier checkout que no reconozca (isProPurchase = true como fallback).
Cuando el webhook no encuentra al usuario (ni por client_reference_id ni por email), retorna status 200. Stripe interpreta esto como exito y no reintenta. El usuario pago pero su cuenta nunca se actualiza a premium.
No van a tumbar la app, pero los usuarios los van a notar. Causan datos incorrectos, UI rota, o comportamiento inesperado.
new Date(event.date).getDate() + 1 — un evento guardado para el 15 se muestra como el 16. Todos los eventos del calendario estan desfasados un dia.
El operador % de JavaScript preserva el signo negativo. Para cualquier fecha antes de enero 2023, phaseCycle es negativo, produciendo un phaseIndex negativo que siempre cae en "Luna Nueva" sin importar la fase real.
getFullYear() - getFullYear() no considera si el cumpleanos ya paso este ano. Alguien nacido el 31 de diciembre se muestra un ano mayor. Afecta cyclesRemaining (ciclos fertiles) y la validacion de edad minima (menores podrian pasar).
Todos los queries de mensajes usan user.name (nombre visible) como identificador. Si dos usuarios tienen el mismo nombre, sus mensajes se mezclan. Si alguien cambia su nombre, pierde acceso a todas sus conversaciones anteriores.
handleUpdatePeriodStart actualiza el state local pero nunca escribe a la tabla profiles de Supabase. Si la usuaria cierra sesion antes de ir a "Editar Perfil", el dato se pierde del lado del servidor.
user.trialStartTime = now muta el objeto directamente en vez de usar setUser({...user, trialStartTime: now}). React no detecta el cambio, los componentes que dependen de trialStartTime no se re-renderizan, y el trial timer puede no funcionar correctamente.
setIsSaved(true) se ejecuta fuera del try, asi que siempre muestra "Sincronizacion sagrada completada" aunque la escritura a Supabase haya fallado. La usuaria cree que sus datos se guardaron cuando no fue asi.
validateUserProfile existe y se usa en Login, pero UserProfileEdit NO lo importa. Se pueden guardar nombres vacios, fechas de nacimiento futuras, o ciclos de 0 dias.
Cuando un usuario con perfil completo vuelve a la app, loadProfileAndCheck nunca llama onLogin. En vez de entrar directo al Dashboard, ve el formulario de perfil otra vez.
setIsLoading(true) se activa antes del redirect a Stripe, pero si el redirect falla (popup bloqueado, back del browser), isLoading nunca se resetea. Todos los botones quedan deshabilitados mostrando "Redirigiendo..." para siempre.
handleReaction hace optimistic update local y luego un write a Supabase, pero el write lee el valor viejo del state. El servidor y el cliente terminan con conteos diferentes. Lo mismo pasa con los comentarios.
setFormData({...formData, avatarUrl}) cierra sobre el formData de cuando empezo el upload. Si la usuaria edita su nombre mientras la imagen sube, el nombre vuelve al valor anterior cuando el upload termina.
Cuando la AI falla, el error solo va a console.error. La usuaria no ve ningun mensaje, ninguna opcion de reintentar. En el Chatbot esto es especialmente grave: escribe algo personal/emocional y simplemente no recibe nada.
JSON.parse(saved) se llama sin try/catch. Si los datos de localStorage estan corruptos (raro pero posible), todo el Dashboard crashea con una excepcion al montar.
prompt: 'consensus' no es un valor valido de Google OAuth. Los valores correctos son none, consent, select_account. Google ignora el parametro invalido.
h + 1 para hora 23 produce DTEND:T240000 que no es valido en iCal. Algunos calendarios rechazan el archivo completo.
site.webmanifest dice nombre "Osiris", tema dorado (#e8bf1a). VitePWA genera uno con "Obsidiana", tema rosa (#831843). Iconos apuntan a paths diferentes. Puede causar iconos rotos o colores incorrectos en la pantalla de splash.
La URL de "Membresia Premium" contiene /test_ indicando modo test de Stripe. Los links de libro y donacion son live. La usuaria paga en modo test y no recibe nada.
Estas no son cosas rotas — son oportunidades que encontramos y que, en nuestra experiencia, pueden elevar la calidad de la app. El equipo decide cuales valen la pena.
Los mensajes solo estan en React state. Si la usuaria navega a otra seccion y vuelve, toda la conversacion desaparece. Para una feature con tono terapeutico/espiritual, perder la continuidad rompe la experiencia.
ConsideracionChatbotsendMessageToOsiris(input) manda solo el ultimo mensaje. La AI no tiene contexto de lo que se discutio antes. Las conversaciones de varios mensajes suenan desconectadas porque la AI no recuerda nada.
handleSubmitMiracle llama a la AI sin verificar premium ni trial. Dreams, Bitacoras y Chatbot si tienen gate. Esto significa que usuarios gratis pueden consumir tokens de AI ilimitadamente desde el Dashboard.
PremiumUnlock.tsx nunca se renderiza pero sigue en el repo con precios distintos: libro $49.99 (vs $5.99 en BookLibrary), premium $19.99/mes (vs $9.99 en ProUpgrade). Si alguien lo importa por error, las usuarias ven precios incorrectos.
maximum-scale=1.0, user-scalable=no impide que las usuarias hagan pinch-to-zoom. Esto viola WCAG 2.1 (Criterio 1.4.4). Ya existe touch-action: manipulation en CSS que previene doble-tap zoom sin bloquear pinch.
user-select: none en el body impide seleccionar y copiar cualquier texto (excepto inputs). Las usuarias no pueden copiar respuestas del chatbot, definiciones del glosario, ni interpretaciones de suenos. Recomendamos aplicarlo solo a botones y navegacion.
El hamburger menu, boton de enviar chat, boton de perfil, botones de eliminar en Agenda... todos son iconos sin aria-label. Lectores de pantalla solo dicen "boton" sin descripcion.
translations.ts tiene traducciones completas en ingles y espanol, pero ninguna se usa excepto una linea en ErrorBoundary. Todos los componentes tienen strings hardcoded en espanol. No hay language switcher ni provider. Si la app solo sera en espanol, recomendamos eliminar el archivo para no confundir.
Wikipedia envia X-Frame-Options: DENY. El boton "Leer en Wikipedia sin salir de la app" muestra un rectangulo blanco para siempre. No hay deteccion del error ni link alternativo.
srsearch=${searchTerm} sin encodeURIComponent(). Buscar "VPH & cancer" o "utero #2" rompe el URL silenciosamente y da resultados vacios o incorrectos.
El modal de detalle del Glossary no tiene onClick en el backdrop. Las usuarias esperan poder cerrarlo tocando fuera del modal, pero solo funciona el boton X.
Se pide permiso de notificaciones y se guarda reminderEnabled: true, pero no existe logica de scheduling. La usuaria habilita recordatorios para un evento esperando que le avisen, pero el reminder nunca llega.
BookLibrary renderiza fuera del Layout, asi que no hay sidebar, header, ni navegacion. La unica salida es el boton "cerrar". Si ese boton falla o esta oculto en pantalla completa, la usuaria queda atrapada.
El sidebar de desktop dice "Cerrar Sesion" y el menu mobile dice "Finalizar Sesion". Son la misma accion con nombres diferentes, lo cual puede confundir a usuarias que usan ambos.
ConsideracionLayoutLas funciones callGroq y sendMessageToOsiris no tienen AbortController ni timeout. Si la API de Groq tarda o se cuelga, el spinner gira para siempre sin opcion de cancelar.
onKeyPress esta deprecado en React y no funciona correctamente en algunos browsers mobile. Afecta Chatbot, DreamJournal, Bitacoras, y Community. Deberia ser onKeyDown.
NavItem esta definido como componente dentro del body de Layout. React lo re-crea en cada render, causando unmount/remount de todos los items de navegacion y perdiendo cualquier estado interno.
InstallBanner guarda el dismiss en useState (memoria de sesion). Cada vez que la usuaria recarga la pagina, el banner vuelve a aparecer. El prompt de iOS si usa localStorage. Comportamiento inconsistente.
La deteccion de iOS usa /iPad|iPhone|iPod/.test(navigator.userAgent) pero iPads con iPadOS 13+ reportan user agent de desktop Safari. Las usuarias de iPad nunca ven las instrucciones de instalacion.
El README habla de "AI Studio", tiene link a ai.studio/apps, e instruye configurar GEMINI_API_KEY. La app usa Groq + Supabase. Cualquier developer nuevo que lea el repo se confunde.
Revisen lo que seleccionaron. El boton genera un mensaje con todos los puntos marcados.