Historial de cambios de la aplicación
Aplicación de cartas natales
/personalizar, antes «Orbes» en el menú): pestanyes verticals Orbes (contingut actual extret a partes/_orbes_form.php; /orbes segueix funcionant) + Colors (pickers per aspecte amb sÃmbol, roda i taules, presets i desat). Traduït a ca/es/en.UserColorManager, tabla user_colors, migración incluida): se aplican a las tablas (vars CSS :root por usuario) y a la rueda SVG (WheelRenderer usa wheelColor() con contexto de RequireLogin/ajax/main). Endpoint ajax/save-colors.php..chart-person-name); eliminado el subtÃtulo «Nacimiento: …» del header..filter-header, variante --plain) entre pestañas y contenido en Carta y Resumen.onclick en vez de por texto (antes solo funcionaba en español); 1.425rem base y 2rem en tabs globales, en negrita.angle_conjunction) no cambia: se conservan valores guardados.--prog-asp-*; arrastra a direccions i lunacions). transitos.css mantiene su paleta propia.Database::delete() (espera array, recibÃa string) que lanzaba excepción; ahora query() directo y verificado (vuelve al defecto). Mismo patrón detectado sin corregir en admin/coupons.php:65 y admin/treasury.php:30..informe-planet-position (1rem en móvil).ajax/change_group.php + ruta registrada). Banner visible «Estás probando el perfil» con botón «Volver a mi perfil». LÃmites de cartas, banners de cuota y cabecera respetan el grupo efectivo (AuthManager::getEffectiveGroup(), inRealGroup()).display_name de GeoNames). Nuevas filas PaÃs y Hora local (residencia) (instante natal expresado en la zona de residencia)..filter-header, variante --plain sin engranaje) entre pestañas y contenido en Carta (dibujo) y Resumen; switchRelocGlobal los sincroniza en todos los idiomas.AGENTS.md (texto → __() → .po ca/es/en → msgfmt → reload PHP-FPM → verificar).partes/header.php; respeta el override para simular fielmente).deploy:restart_services condicional — omite el restart de PHP-FPM/Nginx si la release es idéntica a la anterior (diff excluyendo shared); garantiza catálogo gettext fresco cuando cambian los .mo.renderFaseLuna() reutilizada también en la pestaña de residencia; bloque de Quirón solo con permiso de sÃntesis.onclick en vez de por texto (antes solo funcionaba en español).#prog-btn-calcular).obtenerFasesLluna() devolvÃa el JSON en castellano sin pasar por __(); ahora se traduce al consumir (mismo patrón que Quirón). «Casa X» del listado detallado de Direcciones (2 puntos), «orbe de» de Lunaciones (2 puntos) y textos de TooltipData (helper recursivo traducir() en los 4 métodos) ahora usan __().lado/IDs dejados idénticos a propósito; +12 nombres de elementos/modalidades/polaridades en ca). en_US reparado (lÃnea basura + 60 comillas sin escapar). Cadenas del simulador de perfiles traducidas a de/fr/it/pt; .pot regenerado.renderSeccionPlanetas() y ahora aparece tanto en la carta natal como en la residencia (antes solo existÃa en la pestaña natal, y al mostrar Residencia por defecto parecÃa que habÃan desaparecido).view_help_guides): ahora controla también los subtÃtulos-pregunta del Balance Astral («¿Cuál es la naturaleza predominante…?») y de Hemisferios y cuadrantes («¿Dónde se manifiesta esa energÃa…?»), además de las guÃas de lectura. Sin el permiso, no aparecen.view_help_guides): nuevo permiso en *Permisos avanzados*. Controla la visibilidad de la guÃa de lectura (tablas de coherencia) del Balance Astral y de Hemisferios y cuadrantes en el informe. Migración add_help_guides_permission.sql (asignado a Administrators, Rei do Mambo, Usuario registrado y Aula).CalculadorInfluenciaPlanetas::calcular() ya no se invoca y sus llamadas en charts/view.php y el método ProgressionsCalculator::generarEvolucionBalance() han sido retirados. La clase CalculadorInfluenciaPlanetas se conserva (sin uso).messages.pot regenerado con xgettext: faltaban ~200 msgids que no estaban en ningún catálogo.msgmerge aplicado a los 7 idiomas.fuzzy eliminadas (~1.400 que msgfmt ignoraba y quedaban sin traducir).%d carta de %d disponible).en_US (29 cadenas traducidas erróneamente con «lunations»).qwen3.6:27b); pendiente de revisión humana en detalles finos.SR/SD/S).$ y la aproximación USD→EUR). En BD, subscription_prices pasa a 8 €/mes y 60 €/año; STRIPE_CURRENCY=EUR.dashboard.php), con las traducciones actualizadas.update_free_group_max_charts.sql — lÃmite de cartas de los grupos gratuitos a 5.update_subscription_prices_eur.sql — precios a 800 / 6000 céntimos (8 € / 60 €).admin/*, auth/*, dashboard.php, charts/*, includes/*, partes/*). El catálogo ca_ES pasa a mostrar catalán en todas esas pantallas.__(): admin/textos.php (Tabla de Coherencia, Balance, Hemisferios, Nombre, Pregunta) y admin/email-logs.php (De:, Para:, Fecha:, Asunto:).Hemisferios, De:, Para:, Asunto:, Balance, Tabla de Coherencia. Todos los .mo recompilados con msgfmt.includes/Symbols.php, css/transitos.css, css/progressions.css.access_ia_export («Exportar para IA»). Al no tenerlo, tampoco se genera el texto plano (renderTextoPlano). renderPestanyes() recibe ahora bool $canViewTextTab y construye el botón y el panel de texto solo si se permite.renderTextoPlano() rep ara $results (opcional) i per això pot mostrar les cases de les tres ubicacions.PLANETAS_Y_QUIRON_CARTA).distAngular) con el ángulo del aspecto. Antes solo se probaba nodo + ángulo, por lo que se perdÃan todos los aspectos del lado contrario (p. ej. la sesquicuadratura al Nodo Sur). Cada nodo recibe su aspecto correcto: un planeta a 45° del Nodo Norte está a 135° del Nodo Sur.documentation/maia_ai_local_module.md reescrito: describÃa el proyecto Python externo obsoleto; ahora documenta el pipeline PHP real (4 pasadas, porta de cobertura, RAG, traces, xat, aturada, QA).documentation/revolucio_solar_module.md ampliado: constructor con $casasResidencia, DES/FC en angles natals, posició de l'ASC de RS (carta natal i residència), criteri de cases carregades, contactes amb nodes per qualsevol aspecte..env.example: MAIA_SYNTHESIS_MODEL, MAIA_DIGEST_MODEL, MAIA_OLLAMA_MODEL (legacy).Casa 2: ) porque leÃa los planetas con array_column(...,'planeta') sobre una lista de strings. Ahora lista los nombres correctamente.includes/MaiaRagStore.php, scripts/maia_rag_index.php (Ãndex local d'embeddings), includes/MaiaChatSession.php (sessions de conversa en disc amb TTL), corpus astrològic local (gitignored).MaiaRagStore::trocear() parteix per frases els parà grafs gegants; tolerà ncia a fragments OCR invà lids; .env.example amb MAIA_OLLAMA_URL/MODEL/MAIA_RAG_MODEL.ajax/ia_export.php); "Angles natals a cases RS" amb les 4 xifres (ASC/MC derivats de DES/FC); ASC de RS amb posició a la carta natal i de residència, à ngle (ASC/FC/DES/MC) i distà ncia a les dues cúspides; etiquetes Regent/Natal/Residencia en negreta.MaiaPipeline::verificarCobertura() amb reintent, lineaNodal()/miembrosEnfasis()/cuspidesAngulares() deterministes; model de cada passada a les traces.devstral-small-2:24b; falsos negatius del QA nodal i de dates amb guions.<strong>).posicioNatal() devuelve orbe_ini/orbe_fin, casa_siguiente y textos; fmtOrbe() compartido. Envoltorio i18n único traducido a los 7 idiomas (el msgid anterior de la cúspide queda retirado).relocation_access, se muestra también respecto a sus casas relocalizadas. Nuevo RevolucioSolarCalculator::posicioNatal(), etiquetaAngular(), textPosicio(); constructor acepta las casas de residencia (pasadas desde ajax/revolucio_solar.php). Nuevo msgid de la cúspide en 7 idiomas (.po recompilados). Verificado: ASC RS 26°36' con cúspide natal a 26°43' → "MC · 7' de la cúspide"; punto en casa 4 → "FC".generarAnglesNatalsACasesRS() descartaba Descendente y Fondo del Cielo (el render y las traducciones ya los soportaban). Ahora DSC = ASC+180° e IC = MC+180°. Verificado: ASC 14.28° → DES 194.28°, MC 144.28° → FC 324.28°.MaiaExport::miembrosEnfasis() (cruce ÉNFASIS × POSICIONES → "CASA 1 (4): Sol, Mercurio, Venus, Neptuno", verificado determinista) inyectado en DATOS CLAVE + regla anti-completado en la sÃntesis. Verificado e2e en carta real: "Casa 1 (4 planetas: Sol, Mercurio, Venus, Neptuno)", QA 5/5.devstral-small-2:24b (vÃa MAIA_DIGEST_MODEL; 1.7b elegible por env). Verificado en la misma carta: stellium, posiciones, nodos y Aire/Agua correctos, QA 5/5. Run total ~2 min.think:false no lo obedece el qwen3-vl:30b-a3b-thinking en prompts complejos (think 10k+ chars, done:length, content 0 en los 2 intentos). Probe de modelos: devstral-small-2:24b redacta directo en español (1072 chars, 29 s), qwen3.6:27b obedece think:false, qwen3:4b filtra thinking en inglés dentro del content (descartado). Nuevo modelo de sÃntesis por defecto: devstral-small-2:24b (vÃa MAIA_SYNTHESIS_MODEL; se respeta MAIA_OLLAMA_MODEL por compatibilidad; el 30B sigue elegible por env). Verificado e2e: QA 5/5.ajax/ia_export.php, tras ÃNGULOS): las 12 cúspides con grado+signo desde $datos['casas']. Ni el lector ni la IA podÃan saber el signo en cúspide; la sÃntesis confundÃa "punto situado en la casa" (Nodo Sur en Virgo en casa 10) con "cúspide" (casa 10 = Leo). MaiaExport::cuspidesCasas() la parsea, estructura() la incluye en la pasada 1 y cuspidesAngulares() la prefiere (fallback a ÃNGULOS para exports antiguos, verificado en ambos formatos).num_predict pensando sin redactar (traza: think 12k chars, content 0, done:length, 2 intentos). Verificado que think:false sà lo desactiva (test directo: content correcto). Digest y sÃntesis van sin thinking; la sÃntesis conserva num_predict: 3000 para redactar. Run e2e 30B (digest 1.7b): 80 s totales, contenido completo.eje_nodal: la regex exigÃa literalmente "Sur en Leo" y el 30B escribió el dato correcto como "Sur: Leo, casa 10". Acepta ahora Norte:, (, - además de en (misma frase, signo y casa reales). La sÃntesis 30B reevaluada pasa a QA 5/5.MaiaPipeline::verificarCobertura() (espejo ligero del QA: casa, nodo, orbe, fecha) tras la sÃntesis; si falta algo, reintento único con nudge explÃcito de lo omitido (máx. 2 intentos, campo intento en traza). El 4/5 intermitente del 1.7b era ruido de muestreo con prompts idénticos (dato nodal presente 2x y aun asà omitido 2 veces seguidas). Verificado: intento 1 falló cobertura, intento 2 pasó → QA 5/5.partes/ia_export.php mostra l'aturada mentre corre l'anà lisi; avorta el fetch (AbortController) i crida la nova acció cancel de ajax/ia_maia.php, que descarrega el model (ollama stop, nom validat + escapeshellarg, mateixos guards auth/CSRF). Allibera la GPU al moment; les passades completades queden a la traça.JSON.parse... sobre el HTML del 504. Digest (estructura/aspectos/temporal) ahora sin thinking (think: false, Ollama ≥0.12) y con num_predict: 600; sÃntesis con num_predict: 2000. Las opciones quedan registradas en cada traza. Verificado: mismo export + qwen3:1.7b sigue en QA 5/5._maiaPost() (partes/ia_export.php) ya no muestra el JSON.parse crudo; explica timeout y sugiere modelo más rápido.ajax/ia_export.php archiva cada export generado en logs/maia_ai_exports/ (protegido por .htaccess, gitignored; se conservan los 10 recientes) y devuelve export_file en el JSON. Permite QA estricto (--texto + --carta) sin copiar/pegar.datosClaves()); casa y aspectos llegaban reforzados desde los resúmenes. Nuevo MaiaExport::lineaNodal() (reutilizado en datosClaves()) y refuerzo determinista al final del resumen estructural en MaiaPipeline::analizarCartaNatal(). Verificado con qwen3:1.7b sobre el mismo export: 5/5 sin necesidad del 30B.modelo (nuevo OllamaMaiaClient::modelo()). Sin esto, dos runs con modelos distintos no son comparables (detectado al revisar trazas sin thinking ni duraciones de 30B).data/maia_rag_corpus/greene_sasportas_luminares.md (libro completo: Luna/madre/matriarcado, Luna en relaciones, héroe solar, Sol/padre/yo, coniunctio, lunaciones y nodos; 332 fragmentos, tags luminares-sol-luna; Ãndice total: 648).MaiaRagStore::trocear() parteix per frases els parà grafs que superen el pressupost (text amb salts de lÃnia durs, sense lÃnies en blanc). Abans, el llibre sencer col·lapsava en 1 chunk monstruós que Ollama rebutjava (HTTP 500).sasportas_transitos_neptuno_pluton_ocr.md (final cap. 6 + cap. 7 Neptuno por planetas/casas + inicio Plutón; 73 fragmentos, tags sasportas-neptuno-pluton), sasportas_crisis_plutonianas_cap8_ocr.md (mito Inanna/Ereshkigal; 18 fragmentos, tags sasportas-pluton-crisis) i sasportas_transitos_pluton_cap9_ocr.md (Plutón por planetas/casas; 32 fragmentos, tags sasportas-pluton-transitos; Ãndice total: 316). Limpieza ligera de OCR aplicada (guiones de lÃnea, marcas inequÃvocas, números de página).MaiaRagStore::indexarFichero() omite (con aviso a STDERR) los fragmentos que Ollama rechaza (HTTP 500 con basura de escaneo) en vez de abortar toda la indexación. 36 fragmentos omitidos en los 3 capÃtulos.data/maia_rag_corpus/ commiteado con git add -f (sigue gitignored: los ficheros nuevos futuros requieren -f). El Ãndice data/maia_rag_index.json NO se commitea (regenerable con php scripts/maia_rag_index.php).deploy:purge_ai_docs (tras deploy:update_code) que borra data/maia_rag_corpus/, data/maia_rag_index.json y todos los *.md del release excepto CHANGELOG.md, ROADMAP.md y prompt-astrologia-psicologica.md (este último lo lee ajax/ia_chat.php en runtime para el botón DeepSeek). El próximo deploy elimina los docs IA ya publicados.data/maia_rag_corpus/transito_neptuno_12_casas.md (disolución, idealización, sacrificio, marco de 7 preguntas + ejemplo casa 7; 8 fragmentos, tags transitos-neptuno).data/maia_rag_corpus/transito_pluton_12_casas.md (sombra, poder, ciclo plutoniano + ejemplo casa 10 + tabla Urano/Neptuno/Plutón; 9 fragmentos, tags transitos-pluton; Ãndice total: 193).data/maia_rag_corpus/transito_urano_12_casas.md (lectura psicológica por casas + marco de 7 pasos + ejemplo casa 7; 17 fragmentos, tags transitos-urano; Ãndice total: 176).data/maia_rag_corpus/transito_urano_casa7.md (lectura psicológica Sasportas/Greene: proyección, individuación, eje Yo–Tú; 4 fragmentos, tags transitos-urano-casa7; Ãndice total: 159).data/maia_rag_corpus/greene_doce_casas.md (Sasportas, planetas por casas, 56 fragmentos, tags greene-casas; Ãndice total: 69). Corpus gitignored (data/maia_rag_corpus/). Escuela de referencia: Aula Astrològica de Catalunya.aula_fonaments_01.md (Jung, 7 principis hermètics, esfera/eclÃptica, zodÃac, polaritats, 4 elements + funcions junguianes; tags aula-fonaments) i aula_modalitats_02.md (cardinal/fixa/mutable + taula tècnica del zodÃac; tags aula-modalitats). Ãndex total: 73 fragments.apunts_per_ia extrets a data/maia_rag_corpus/aula_nivell1/ (pypdf/pandoc; pà gines només-diagrama descartades) i indexats amb tags per tema. Ãndex total: 155 fragments.MaiaExport::datosClaves() (eje nodal + casa con énfasis + top-3 aspectos verificados por cálculo) inyectada en el prompt de sÃntesis con orden de no contradecirla. Segunda pasada con el fix: nodos correctos.--carta, el check nodal exige signo+casa REALES (caza la alucinación anterior) y acepta la forma corta "Norte en Piscis"; equilibrio acepta cabeceras negrita además de # y numeradas.CURLOPT_TIMEOUT 300 → 1800 en OllamaMaiaClient y set_time_limit(0) en ajax/ia_maia.php durante el pipeline.scripts/qa_maia.php, Fase 6 del plan): 5 comprobaciones deterministas sobre la sÃntesis (casa con énfasis, NN signo+casa, orbe numérico, fecha tránsito/progresión, equilibrio de secciones). Usos: --texto + --carta, stdin, o --carta (ejecuta el pipeline). Exit 0/1 para CI. Referencia actual con qwen3:1.7b: 4/5 (falla eje nodal; el 30B debe clavar 5/5).includes/MaiaRagStore.php, Fase 4): Ãndice JSON en data/maia_rag_index.json (gitignored, regenerable), chunks ~350 palabras con solape, embeddings nomic-embed-text vÃa Ollama, coseno en PHP (sin dependencias: sqlite-vec/Chroma innecesarios para este volumen). Indexador scripts/maia_rag_index.php (corpus inicial: prompt-astrologia-psicologica.md, 13 fragmentos). MaiaPipeline acepta RAG opcional e inyecta 2 fragmentos por pasada temática; ajax/ia_maia.php lo activa solo si hay Ãndice. Sin Ãndice, comportamiento idéntico al anterior.MaiaExport::progresionEdadActual() extrae del bloque PROGRESIONES solo el año de vida actual; transitos() ordena lentos primero y topa a 40 lÃneas. Nueva MaiaPipeline::pasadaTemporal() (ventana hoy−1m/hoy+6m + progresión actual, máx 200 palabras) y la sÃntesis integra 3 resúmenes (natal + aspectos + temporal) con punto 5 "cómo el momento actual dialoga con el tema central". Detección automática de la fecha de nacimiento desde la cabecera del export (MaiaExport::fechaNacimiento()); MaiaChatSession guarda también el contexto temporal. Si no hay material temporal, la pasada se omite y todo sigue como antes.num_ctx 16384 → 32768.MaiaExport se quedaba con las subsecciones de residencia (last-wins). Ahora first-wins (nacimiento).datos. Nuevo pipeline en 3 pasadas (estructura → aspectos → sÃntesis psicológica) con trazas JSONL para auditorÃa. Nuevas clases en includes/: OllamaMaiaClient (con disponible() y config vÃa MAIA_OLLAMA_URL/MAIA_OLLAMA_MODEL), MaiaExport (parseo del texto + conteo determinista de aspectos por punto), MaiaPipeline (orquestación + trazas), MaiaChatSession (chat de seguimiento en disco con TTL 4h). ajax/ia_maia.php reescrito sobre este motor (auth/CSRF con el patrón de ia_export.php, sesiones y trazas en logs/, guard "solo en local" si Ollama no responde). El endpoint antiguo (datos → scripts/maia_server.py, inexistente) queda sustituido. Nota: marcar a la vez "natal (naixement)" y "natal (residència)" genera dos bloques con las mismas subsecciones y el parser se queda con el último — para MAIA AI, generar solo con "naixement" de momento.Cuadrantes y PLANETAS MÃS INFLUYENTES (y lo mismo en ÉNFASIS/FASE LUNAR cuando no hay planetas influyentes) — ├── quedaba pegado al final de la lÃnea anterior y el parser lo tragaba dentro de la sección previa. Añadido \n inicial en ajax/ia_export.php./api/v1/geo/search) lentÃsima / MySQL al 100% CPU: la query usaba LIKE '%...%' con comodÃn inicial sobre name, asciiname, alternatenames (+ city_translations) → full scan de ~5M de filas en cada pulsación (8-12s por búsqueda). Reescrita a UNION por prefijo (name LIKE 'q%', asciiname LIKE 'q%', city_translations LIKE 'q%' + rama FULLTEXT BOOLEAN solo con 4+ caracteres), cada rama ORDER BY population DESC LIMIT 8 y enriquecimiento (admin1/paÃs/traducción) solo sobre ≤32 candidatos. Resultado: 2-100ms. El fallback "ciudad más cercana" (modo edición) usaba ORDER BY dist sobre todas las ciudades con population > 5000 → ahora caja envolvente ±1° (±5° si vacÃa) con USE INDEX (idx_lat_lon). Nueva migración 024_geonames_search_perf.sql con el Ãndice idx_lat_lon (latitude, longitude). Formato de respuesta JSON sin cambios.AuthManager::register(), cubre registro normal y SSO) se envÃa un email a cada usuario no bloqueado del grupo "Rei do Mambo" con nombre, email, ID y fecha de registro del nuevo usuario. EnvÃo no bloqueante (fallos SMTP solo se loguean). Nuevo método EmailService::sendNewUserNotification().AstrologiaHelper::getTranslate() no resolvÃa las claves de ángulo del AspectosCalculator (Ascendant, Midheaven, Descendant, IC) → añadidas las constantes TRANSLATE_ASCENDANT/MIDHEAVEN/DESCENDANT/IC. Ahora se muestran como "Ascendente", "Medio Cielo", "Descendente", "Fondo del Cielo". El fix es central: aplica también al grid de aspectos y a la rueda.includes/FuncionInferiorCalculator.php (doc 03 §01–§03): detecta funciones inferiores por los criterios de elemento único, modalidad única y grado 29°, con la excepción transpersonal (Urano/Neptuno/Plutón/Quirón solo cuentan como representantes si aspectan a un planeta personal/social). Universo: Sun–Quirón./admin/roadmap): el renderizador Markdown propio no soportaba ~~strikethrough~~ → se mostraba ~~texto~~ literal en vez de tachado. Ahora ~~ → <del> (inline y en items de lista, conservando numeración/viñetas).parseRoadmap solo reconocÃa pending|done → las secciones in_progress (cobro y suscripción, gestor de email) se incrustaban dentro del item "Realizado" anterior. Ahora se parsean como tarjetas propias con badge "🔨 En curso" e incluidas en la pestaña Pendientes (contador + Ãndice).<!-- status: pending -->, visible en la pestaña Pendientes) con los pendientes reales movidos desde las secciones en proceso (promotores, IVA, prorrateo, audit log, hardcodeos, emails) y todo lo ya realizado tachado en cada sección..modal-content legacy de styles.css capaba el ancho a 500px; override en .modal-dialog .modal-content (max-width: none, width: 100%, font-size: 16px). El modal large passa a width: 95% (més ample, sense cap de 900px).admin/email-config.php no definia $user (email de prova buit) i EmailService::send() referenciaba $isHTML no inicialitzada al catch. Corregit..env → els canvis guardats des del panel admin (email, Stripe…) tenen efecte immediat encara que la variable ja existeixi a l'entorn del procés PHP-FPM./admin/email-config): host, port, xifrat SSL/TLS, usuari, password, email i nom d'enviament. Guarda a .env + botó "Guardar i provar connexió" (envia email de prova).email_logs): data/hora, destinatari, assumpte, cos HTML/pla, estat (sent/failed) i missatge d'error. Central a EmailService::send() (no bloquejant)./admin/email-logs): paginat, amb accions Veure (popup 90% amb format de mail), Reenviar (tal qual) i Borrar.migrations/add_email_logs_table.sql.cs_...) en lloc del PaymentIntent (pi_...). Ara resolvePaymentIntent() resol el pi_ real (pago inicial del client) tant al reemborsar com al guardar la transacció i a la cancel·lació amb reembors.my-transactions: el modal de cancel·lar subscripció sense l'emoji â›” al tÃtol i amb size: large.view_sintesis_balance).SintesisHemisferios) al final del bloc Hemisferis i quadrants (gated pel mateix permÃs): dominants vertical/horizontal + quadrants amb frases./my-subscription: estat unificat de la subscripció (pla, caducitat, ús de cartes amb barra de progrés, cancel·lar subscripció, enllaç a historial de pagos i upgrade).mode=subscription (preus monthly/annual). Stripe guarda la targeta (Customer + PaymentMethod) i cobra automà ticament; l'app no toca mai dades de targeta.ajax/stripe-webhook.php: invoice.paid (renova i estén la caducitat), invoice.payment_failed (email d'avÃs), customer.subscription.deleted (downgrade al grup fallback)./my-transactions. Mensual → cancel·la al final del perÃode ("activa fins al X"). Anual → 2 opcions: cancel·lar al final o cancel·lar ara i reemborsar (reembors del darrer pagament + downgrade).once).PaymentEngine::extendExpiration() suma la durada amb la unitat correcta (dies/mesos/anys); abans INTERVAL ? MONTH no funcionava per al pla anual (1 any).migrations/add_stripe_subscription_columns.sql — stripe_customer_id, stripe_subscription_id a usuarios.SquarePayment.php, ajax/square-webhook.php, ruta i variables SQUARE_* del .env.example. El motor comú passa a includes/PaymentEngine.php (usat per Stripe).admin/group_edit.php: Permisos Asignados, de Administrador, Avanzados i para IA.is_free) amb preus USD/EUR (canvi actual, anual ÷12) i llista de funcions desbloquejades per permisos./upgrade)./upgrade: preus en euros al canvi actual + llista de diferències vs pla actual + "≈ N mesos gratis" al pla anual..php fÃsics (404 en producció perquè el servidor enruta tot pel front controller): corregits a admin/*, charts/*, partes/header.php per usar route().maia: migracions aplicades correctament (columna filename, comentaris --).admin/treasury.php. Nou endpoint ajax/refund-payment.php (admin + CSRF) que reemborsa via Stripe API (Refund::create amb el payment_intent).SquarePayment::rollbackSubscription() (extret de processRefund) baixa l'usuari al grup fallback si segueix al grup del pla; StripePayment::refundTransaction() el crida automà ticament.Router::post('/ajax/refund-payment', ...).subscription_prices) tenÃan group_id = NULL y no existÃa grupo Premium en user_groups, por lo que aplicarPlanYCupon() no asignaba grupo. Nueva migración add_premium_groups.sql que crea los grupos Premium Mensual (1 mes) y Premium Anual (1 año), con fallback a Users, y los vincula a los planes monthly/annual..php. Añadidas las rutas Router::post('/ajax/stripe-webhook', ...) y Router::post('/ajax/stripe-webhook.php', ...).https://astro.omda.es/ajax/stripe-webhook (la app corre en astro.omda.es, no en omda.es). Actualizar la URL del endpoint en el dashboard de Stripe (el whsec_ no cambia).includes/StripePayment.php (SDK stripe/stripe-php v21) que crea sesiones de checkout alojado con tarjeta. ajax/create-checkout.php ahora redirige a Stripe; ajax/stripe-webhook.php procesa checkout.session.completed (firma verificada, fail-closed).handleCheckoutCompleted() usa aplicarPlanYCupon() (idempotente, update atómico condicional) + emails de confirmación/bienvenida ya existentes.STRIPE_CURRENCY en .env. Visualización de upgrade.php y my-transactions.php en $.migrations/add_stripe_provider.sql — columnas provider y provider_payment_id en payment_transactions + actualización de precios a USD.refunded en el mapa.dashboard.php muestra confirmación al volver de Stripe (?paid=stripe); la activación real la hace el webhook..env → STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_CURRENCY=USD. Crear endpoint de webhook en Stripe apuntando a /ajax/stripe-webhook.php (evento checkout.session.completed).maia corregido: usaba la columna migration (inexistente; la tabla usa filename) y descartaba bloques SQL cuyo primer bloque empezaba por comentario --. Ahora php maia migrate funciona con los .sql existentes.completePayment() no reprocesa transacciones no-pendientes y usa update atómico condicional (WHERE status='pending'). completeFromRedirect() y completePayment() comparten aplicarPlanYCupon().completeFromRedirect() ahora aplica cupones extension (antes solo el webhook).EmailService::sendPaymentConfirmation() (plan, importe, caducidad, referencia). Se envÃa al completar el pago (redirect o webhook).EmailService::sendPremiumWelcome().EmailService::sendExpiryWarning() disparado desde AuthManager::checkGroupExpiration() con notice_days del plan y throttling de 1/dÃa (columna expiry_warning_sent_at).SquarePayment::cleanupStaleTransactions() (pending → abandoned), ejecutada en dashboard.php máx. 1 vez/hora.refund.updated → processRefund() marca refunded y revierte el grupo al fallback.admin/treasury.php con filtro de estado aplicado.migrations/add_expiry_warning_sent_at.sql (expiry_warning_sent_at en usuarios). Aplicar con php maia migrate o panel admin.~/Proyectos/astrologia-ai) i mostra la interpretació (Part 1 tècnica + Part 2 psicològica) sense dependre de DeepSeek.ajax/ia_maia.php: proxy que reenvia el JSON al servidor local. URL configurable amb MAIA_AI_URL (per defecte http://127.0.0.1:8765/chat).partes/ia_export.php: botó nou «Enviar a MAIA AI» + missatges d'estat.POST /chat amb {datos} o {session_id, mensaje}) i la pestanya IA mostra un xat amb historial, camp de pregunta i botó «Nova conversa». L'usuari pot fer preguntes de seguiment sobre la carta enviada.datos JSON a l'export IA: ajax/ia_export.php ara retorna {success, texto, datos} amb la carta estructurada (planetes amb casa, casas, ángles, aspectes ordenats per orbe, balanç en espanyol, patrons). Botó «💾 Exportar JSON» per descarregar-lo (maia_carta_<id>.json).documentation/maia_ai_local_module.md amb el contracte de dades, el flux i les instruccions d'arrencada del servidor local.admin/users.php ara només permet assignar el grup "Rei do Mambo" si l'actor autenticat en forma part.logs/index.php escapa view a l'href; api/v1/update/check.php serveix JSON amb Content-Type correcte i JSON_HEX_*.js/share-manager.js nou helper jsAttrEscape() per onclick + escapeHtml() als sinks innerHTML del modal.SquarePayment::completeFromRedirect() ara verifica amb Square que el payment és COMPLETED abans d'activar premium (no confia en ?paid=1).csrf_token a admin/migrations.php, ajax/translations.php i ajax/create-checkout.php (+ forms/JS)..env i dotfiles: .htaccess raÃz denega dotfiles i composer/package; vendor/.htaccess Require all denied.api/v1/chart/logs.php limitat per API key+IP (60/10min)..env.example.sso_secret es reutilitzava com a X-API-Key del plugin carta-natal-maia (exposat al client JS) i com a secret de signatura SSO. Ara els endpoints SSO (auth/sso_login.php, api/sso_generate.php) i el consumidor de sso_token (partes/RequireLogin.php) només accepten llocs amb plugin_type = 'maia' (secret només al servidor), blocant el forjat de tokens amb el key exposat.league/commonmark actualitzat de 2.7.1 → 2.10.0, resolent 4 CVEs (DoS per complexitat algorÃtmica + 3 XSS/bypass). Verificat amb composer (sense advisories restants).Host (CWE-640): generateResetLink() ara construeix l'enllaç des de APP_URL (config) en lloc del header Host, i apunta a /reset-password.requestPasswordReset() ja no retorna el reset_link; eliminat el bloc "mode desenvolupament" que el mostrava.auth/reset.php usava $pdo global indefinit i comparava el token cru contra el hash SHA-256. Ara usa findByResetToken()/resetPassword() del repositori.includes/RateLimiter.php (contadors per IP+email en logs/rate_limits/). AuthManager::login() limita a 5 intents fallits per IP+email en 15 min i reseteja en loguejar.requestPasswordReset() limita a 5 peticions per IP+email en 15 min.partes/transitos.php volcava $_GET['id'] sense escapar; ara usa (int)($_GET['id'] ?? 0).charts/share.php get_shared llistava usuaris compartits sense comprovar propietat; ara comprova esPropietario() abans.charts/share.php ara verifica csrf_token abans de cada acció; js/share-manager.js envia el token (meta csrf-token) a totes les peticions.charts/view.php:563 usava addslashes() en context JS → ara json_encode(). Corregits també els vectors a dashboard.php (onclick de compartir/eliminar) i els sinks innerHTML del modal (share-manager.js, confirmDeleteCarta).ajax/square-webhook.php ara és fail-closed: sense SQUARE_WEBHOOK_SIGNATURE_KEY configurada retorna 503 i no processa; amb clau, la verificació de firma és obligatòria.csrf_token + camp ocult als formularis de admin/groups.php, admin/group_edit.php, admin/treasury.php i admin/coupons.php.Require all denied a scripts/, config/ i sql/; gate CLI a scripts/__install.php i scripts/generador_ephemeris.php; logs/index.php ara requereix access_admin_panel.CalculadorInfluenciaPlanetas) amb puntuació i raons.includes/DireccionesCalculator.php): ara els resultats es mostren en dues pestanyes, com a progressions i trà nsits:renderPestanyes(), renderTextoPlano() i formatEdat().partes/ia_export.php + ajax/ia_export.php): nou checkbox amb rang "des de / fins a" (anys) situat abans de "Revolucions solars". Genera una secció "DIRECCIONES DE ARCO SOLAR" amb el text pla de les direccions (conjunció+oposició, 12 cases i 12 signes). renderTextoPlano() passa a ser públic.ajax/ia_export.php: hemisferios/cuadrantes usaven claus errònies (Norte, Cuadrante I) i no imprimien res; ara usen les claus numèriques correctes. També corregit el llistat de "Planetes angulares" i afegits els "Signes amb èmfasi" a la secció Informe.ajax/ia_export.php: fix bug de variable sobrescrida. El foreach ($angPlanetas as $angulo => $datos) de "Planetes angulares" reemplaçava la variable global $datos (les dades de la carta) per l'últim angle (IC). Això trencava les seccions Trà nsits (warning "Undefined array key planets") i Progressions (warning "array offset on float" a AspectosCalculator) i embrutava la resposta JSON amb HTML de warnings. Renombrada la variable del bucle a $angDatos.admin/translations.php): pà gina per revisar, editar i gestionar traduccions sense tocar fitxers .po manualment..mo.includes/TranslationManager.php: parser/editor de .po + compilador msgfmt.ajax/translations.php amb accions list, save, stats, approve_fuzzy, recompile.manage_translations (grup "Rei do Mambo").add_edit_content_texts_permission.sql i add_ia_export_permission.sql canvien INSERT per INSERT IGNORE.city_translations (geoname_id, lang, name) amb 159 ciutats × 7 idiomes. api/v1/geo/search.php fa JOIN amb city_translations segons el lang de la petició. Scoring simplificat: exact match (original o traduït) > té traducció > sense traducció, ordenat per població. Resultats:display_name i address.city en l'idioma de l'usuari (ex: "Roma, Lazio, Italia" per a lang=es)name amb el nom original (ex: "Rome")script.js (cerca naixement i residència) i revolucio-solar-js.php llegeixen document.documentElement.lang i l'afegeixen com a &lang= a la petició.changeLanguage() definida a js/script.js sobrescrivia la del header.php al carregar-se després (al footer). La de script.js només posava una cookie i recarregava sense canviar el path, fent que el Router tornés a l'idioma per defecte. Eliminada la duplicada de script.js.es) a català (ca) a .env (DEFAULT_LANGUAGE=ca) i al fallback de header.php.$currentLang !== $defaultLang per mantenir consistència amb changeLanguage().Router.url() i changeLanguage(). Les URLs ara sempre inclouen prefix d'idioma. També corregides les redireccions antigues (/auth/login.php, /UserLogin.php) per route('/login') a RequireLogin.php, AuthManager.php, logout.php, forgot-password.php i reset.php.migrations/023_create_city_translations.sql: taula city_translationsdata/city_translations.json: 159 ciutats amb traduccions en 7 idiomesscripts/import_city_translations.php: script d'importació per lot__() o _e().messages.pot con 1504 cadenas extraÃdas de todo el código. Los 7 archivos .po actualizados con msgmerge y compilados a .mo.auth/reset.php: todos los textos a catalán + __()auth/sso_login.php: mensajes die() a catalán + __()auth/change-password.php: placeholders a catalánauth/register.php: placeholders a cataláncharts/edit.php: página completa a catalán + __()charts/process.php: mensajes de error a cataláncharts/share.php: mensaje de error a catalánpartes/ia_export.php: 17 cadenas a catalán + __(), JS usa variables PHPpartes/header.php: fallback idioma es → caadmin/ai-config.php: 6 etiquetas a catalán + __()admin/index.php: 4 tÃtulos con emojis a __(), 2 descripciones a catalánadmin/integrations.php: opciones <select> a catalánadmin/roadmap.php: 3 etiquetas a catalánadmin/textos.php: 3 cadenas JS a catalán vÃa <?= __() ?>includes/Router.php: 404 a catalán, prefijo idioma siempre presenteincludes/TransitosCalculator.php: 2 mensajes vacÃos a catalánincludes/AspectosCalculator.php: 3 etiquetas a catalánincludes/AspectosGridGenerator.php: tÃtulo a catalánincludes/RevolucioSolarCalculator.php: prefijo Error: a __()ajax/main.php: 10 mensajes de error a catalán + __()ajax/user-transactions.php: 2 mensajes a catalánajax/transitos.php: mensaje de error a catalánajax/ia_export.php: mensaje de error a catalánajax/composite.php: 9 title de tabla a catalánapi/v1/chart/calculate.php: 3 etiquetas a catalánjs/script.js: eliminada función changeLanguage() duplicada.env: DEFAULT_LANGUAGE=es → caCalculadorInfluenciaPlanetas con los datos relocalizados (ASC, casas y regentes distintos). Antes solo se calculaban para la carta natal, ignorando que la relocalización cambia el regente del ASC. Nueva sección â Planetas más influyentes (Residencia) en la pestaña ðŸ“.CartaNatalCalculator::procesarCasas() anteponÃa el ASC (duplicando H1) y perdÃa la clave absoluto, provocando que todos los planetas se asignaran a casas incorrectas. También se corrigió la ordenación de casas en Functions.php (uksort → ksort SORT_NATURAL) y en el script Python (que las devolvÃa en orden H1,H10,H2...).InformeHemisferios::dibujarCirculo() usaba ángulos que no compensaban la resta de 90° de polarToCartesian. Corregidos los 4 tipos de gráfico (Interior/Exterior, AutonomÃa/Relación, Cuadrantes, Viaje Conciencia). Las lÃneas divisorias también estaban intercambiadas (horizontal ↔ vertical).v4 para evitar datos antiguos. view.php detecta datos estancados (>12 casas o sin absoluto) y fuerza recálculo.access_ia_export: asignado a 4 grupos (Administrators, Rei do Mambo, Usuario registrado, Aula Astrológica).admin/ai-config.php): formulario para configurar la API Key de DeepSeek. Se guarda en .env como DEEPSEEK_API_KEY. Visible para todos los admins. Nueva card en panel admin.🤖 Enviar a la IA en la pestaña IA de carta natal. Toma el prompt del sistema desde prompt-astrologia-psicologica.md, adjunta el texto generado, y lo envÃa a DeepSeek (deepseek-chat). La respuesta se renderiza con formato MD (negritas, itálicas, listas, encabezados). Nuevo endpoint ajax/ia_chat.php.access_ia_export). Genera texto estructurado para enviar a un GPT especializado en astrologÃa psicológica. 6 secciones configurables con selectores de perÃodo:(casa X).(casa N) en la lÃnea de texto.TransitosCalculator::adaptarResultadoPython(): añadido transito_longitud al array de salida para detectar cambios de casa.TransitosCalculator::renderTextoPlano(): ahora incluye (casa N) cuando el planeta transitante cambia de casa.ajax/composite.php (tránsitos compuestos): tabla HTML con sÃmbolos planetarios, signos zodiacales, pestañas Tabla/Texto, botón copiar, cambios de casa.🔄 Tránsitos dentro de la pestaña Compuesta. Selector de fecha inicio, duración y planetas en tránsito. Reutiliza TransitosCalculator y scripts/calculadora_transitos.py sin modificaciones — los datos del composite se pasan como si fueran una carta natal. Cache en sesión por combinación de parámetros. Nueva acción calculate_transits en ajax/composite.php.â³ Progresiones. Calcula progresiones secundarias a partir de la fecha virtual del composite (midpoint de las fechas natales). Selector de fecha de referencia y duración. Reutiliza ProgressionsCalculator y scripts/calculadora_sweph_batch.py. Nueva acción calculate_progressions en ajax/composite.php.$_SESSION["composite_raw_{id1}_{id2}"] los $datos crudos (no solo el HTML renderizado), permitiendo que tránsitos y progresiones puedan usar la carta compuesta como input natal.ajax/composite.php: soporta 3 acciones (calculate_composite, calculate_transits, calculate_progressions). +100 lÃneas.partes/composite.php: añadidas sub-pestañas y formularios de selección para tránsitos y progresiones.js/composite.js: nuevas funciones calcularTransitosCompuesta() y calcularProgresionesCompuesta(). Limpieza de resultados anteriores al cambiar de carta2.css/composite.css: estilos para los formularios de selección de fecha/duración.admin/textos.php): editor visual por campos con previsualización en vivo (panel derecho). Cada sección tiene formularios adaptados a su estructura: inputs para textos cortos, textareas para descripciones, tags editables (contenteditable) para arrays de palabras clave/frases. Soporta 6 tipos de datos: Balance Astral, Hemisferios, Tablas de Coherencia, Fases Lunares, Quirón por Signo, y SÃntesis del Balance. Permiso edit_content_texts. Guardado vÃa JSON a data/.JsonDataLoader.php con caché estática por request. Las clases consumidoras (TooltipData, InfoAstrologica, SintesisBalance) leen de archivos JSON en data/ con fallback transparente a los valores hardcodeados originales si el archivo no existe o está corrupto. 6 archivos JSON creados desde los datos originales.edit_content_texts: nueva migración SQL. Asignado al grupo 1 (Rei do Mambo).TooltipData: 4 constantes (BALANCE_ASTRAL, HEMISFERIOS_CUADRANTES, TABLA_BALANCE, TABLA_HEMISFERIOS) → 4 métodos estáticos (balanceAstral(), hemisferiosCuadrantes(), tablaBalance(), tablaHemisferios()).InfoAstrologica::obtenerFasesLluna() y obtenerInfoQuiron(): leen de JSON con fallback.SintesisBalance::get(): carga desde JSON vÃa JsonDataLoader.InformeAstrologico y partes/informe_carta.php: actualizados los 23 call sites de ::CONST a ::metodo().=== ASPECTOS === con todos los aspectos entre planetas de la compuesta, indicando orbe, tipo (A=plicativo/S=eparativo) y nombre traducido (Conjunción, Oposición, TrÃgono, etc.).=== DISTRIBUCIÓN POR ELEMENTOS ===, === DISTRIBUCIÓN POR MODALIDADES ===, === POLARIDADES === con conteos, porcentajes y dominante de cada categorÃa. Usa BalanceAstrologico y getDominante().partes/composite.php envolvÃa su contenido en un segundo <div class="chart-pane">, lo que causaba que el contenido quedara oculto aunque el panel externo se activara. Eliminado el wrapper duplicado.js/composite.js usaba <?= $carta['id'] ?? 0 ?> que no se procesaba al ser archivo .js. Cambiado a document.getElementById('tab-composite')?.dataset.cartaId.ord('P') → b'P': pyswisseph 2.10.x requiere byte string para se.houses(), no int.access_composite_chart. Cache en sesión. Nuevos archivos: scripts/calculadora_composite.py, ajax/composite.php, partes/composite.php, js/composite.js, css/composite.css. Documentación: documentation/composite_chart.md.Signo GG°MM'$auth en scope🌟 Natal / 📠Residencia / 🌠Elegida) con cálculo independiente para cada ubicación. Las pestañas son sticky y persisten el sub-tab activo al cambiar de ubicación. Placeholder del campo de ciudad: "Escriu la ciutat (deixar buit = residència ó natal)", con auto-precarga de residencia si existe.📋 Texto con resumen completo en texto plano de toda la información RS (balance, posicionamiento, superposición, contactes, regentes). Incluye el tipo de ubicación en el tÃtulo (Natal), (Residencia) o (Elegida).lugar contenÃa "Rome, Lazio, Italia" y fallaba. Columnas ciudad y ciudad_residencia guardan solo el nombre. Fallback por coordenadas en API. Badge ✅ Coordenadas cargadas.Signo GG°MM' en lugar de grados absolutos decimales (176.83°).(new AuthManager())->hasPermission().$user['id'] → $userId en lÃnea 243.scipy (faltaba para tránsitos).favicon.svg (cÃrculo verde con M).relocation_access, se muestra la sección "ðŸ Lugar de Residencia" con autocomplete de ciudades. En la vista de carta, pestañas globales "🌟 Carta Natal Nacimiento" / "📠Carta Natal Residencia" que cambian todo el informe de una vez (SVG, hemisferios, ángulos, casas con énfasis). Pestañas sticky al hacer scroll. Widget "ðŸ " en dashboard. Nuevos archivos/métodos: calcularRelocalizada() en CartaNatalCalculator, guardarDatosRelocalizada() en CartaNatalRepository, columnas lugar_residencia, lat_residencia, lon_residencia, timezone_residencia, datos_relocalizada en cartas_natales, migración add_relocation_fields.sql, permiso relocation_access.(new AuthManager())->hasPermission() en lugar de $auth que no estaba en scope dentro de renderCartaCard().$user['id'] corregido a $userId en ajax/transitos.php:243.scipy 1.17.1 (faltaba para el cálculo de tránsitos).relocation_access), la RS se calcula para hasta 3 ubicaciones: Natal, Residencia y Elegida. Pestañas sticky entre el placeholder y los resultados. El placeholder del campo de ciudad ahora dice "Escriu la ciutat (deixar buit = residència ó natal)" y se auto-precarga con la ciudad de residencia si existe. Nuevo método renderResultatsMulti() y renderResultatsIntern() en RevolucioSolarCalculator. JS añade initLocationTabs() + sticky via scroll.includes/SintesisBalance.php (24 frases), método getDominante() en BalanceAstrologico, método renderSintesis() en InformeAstrologico.renderItem(), buildContent() acepta itemIndex, handlers leen data-tooltip-item.admin/roadmap.php que renderiza documentation/ROADMAP.md. Accesible desde los cards del panel de administración.cartas_natales (lugar_residencia, lat_residencia, lon_residencia, timezone_residencia, datos_relocalizada) y nuevo permiso relocation_access asignado al grupo 1 (Rei do Mambo). Primera fase de la feature de relocalización de carta natal.pending/done) con trazabilidad de versión y fecha.admin/roadmap.php con pestañas Pendientes/Realizados/Todos, Ãndices de navegación, parser Markdown propio (headers, bullets anidados, código inline, blockquotes). Los items se marcan como done editando el comentario <!-- status: ... --> en ROADMAP.md..env relacionadas con debug (NO_DEBUG, DEBUG_ALLOWED_IPS, SAVEQUERIES, DEBUG_SHOW_WARNING, etc.) ahora se definen como constantes PHP. NO_DEBUG=true funciona.pyswisseph 2.10.3.2 + timezonefinder 8.2.5. PYTHON_PATH comentado en .env para usar detección automática del venv.🎯 Orbes al menú. Inclou:user_orbs), restabliment per tipus o global.includes/UserOrbManager.php.orbes.php, ajax/save-orbes.php, js/orbes-js.php.migrations/add_user_orbs_table.sql.ANGULOS_Y_ORBES_TRANSITOS (3° conjunció/oposició, 2° trÃgon/quadratura/sextil, 1° menors) enlloc de reutilitzar els 12° natals. Comparació Python dev > orb → dev >= orb./api/v1/geo/search) en lloc de demanar lat/lon.--accent-light sobre fons blanc → text #333).se.solcross())includes/RevolucioSolarCalculator.phpscripts/calculadora_revolucio_solar.py (flags Swiss Ephemeris correctes, timezone robust)migrations/add_revolucio_solar_permission.sqlajax/revolucio_solar.php, partes/revolucio_solar.php, js/revolucio-solar-js.php, css/revolucio-solar.cssscripts/test_revolucio_solar.php🎯 Orbes al menú. Inclou:user_orbs), restabliment per tipus o global.includes/UserOrbManager.php.orbes.php, ajax/save-orbes.php, js/orbes.js.migrations/add_user_orbs_table.sql.ANGULOS_Y_ORBES_TRANSITOS (3° conjunció/oposició) enlloc dels 12° natals. Comparació Python dev > orb → dev >= orb.data/saros_series.jsonincludes/SarosSeries.phpscripts/test_saros.phplinked_user_id en wordpress_sites. Al vincular un usuario, su suscripción Premium controla automáticamente el flag is_premium del sitio.carta-natal-maia muestra un warning âš ï¸ en su panel de configuración cuando la suscripción del usuario vinculado está a 30 dÃas o menos de caducar.admin/users.php permite al admin cambiar la contraseña de cualquier usuario (UserRepository::forcePassword()).019_add_promoter_fields.sql: promoter_id en usuarios y coupons. Campo promotor en admin/users.php y admin/coupons.php.020_add_linked_user_to_wp_sites.sql: linked_user_id en wordpress_sites. Dropdown en admin/integrations.php.php maia: Herramienta para ejecutar migraciones desde terminal.data/saros_series.json.scripts/test_saros.php).includes/SarosSeries.php.includes/LunacionesCalculator.php, ajax/lunaciones.php, partes/lunaciones.php, js/lunaciones-js.php, css/lunaciones.css.se.solcross()).includes/RevolucioSolarCalculator.php.scripts/calculadora_revolucio_solar.py.migrations/add_revolucio_solar_permission.sql.ajax/revolucio_solar.php, partes/revolucio_solar.php, js/revolucio-solar-js.php, css/revolucio-solar.css.scripts/test_revolucio_solar.php.<form> (antes un solo form para toda la tabla causaba pérdida de datos al guardar).filtrarDatosCartaNatal() compara strtolower($aspecto['acronimo']) contra los acrónimos. id ya no se pierde al no estar en query string.id de window.location.pathname con regex /charts/(\d+) si no está en ?id=.highlightAspect() ahora busca la lÃnea SVG correspondiente por data-planet1/data-planet2 en vez de comparar por referencia. Al hacer hover en una celda de la tabla de aspectos, la lÃnea SVG se resalta correctamente.openChartFullscreen() ahora crea el modal dinámicamente si no existe en el DOM. Cierre con Escape arreglado.Plugin_Upgrader::upgrade() → descompresión manual. download_url() → wp_remote_get(). Transient 'no_update' → false. (int) get_option() en validate_license().<a> anidado dentro del copyright.carta-natal-maia v1.4.2, maia-connector v1.0.3.check.php + componentVersion() en Functions.php.sweepFlipped para SVG.switchChartTab(), display: grid, sizing SVG.maia-connector alias.charts/view.php se reemplazan por un panel lateral izquierdo con toggle ☰. Colapsado: solo iconos (52px). Expandido: icono + texto (220px). Estado persistente en localStorage. En móvil (<768px) vuelve a tabs horizontales.scripts/calculadora_transitos.py (~350 lÃneas) que sustituye las consultas SQL a ephemerides_planetas (~630M filas, ~8 GB) por cálculo dinámico con Swiss Ephemeris + interpolación cúbica (scipy.CubicSpline).swe.calc_ut() diario (0.5d para Luna).CubicSpline con numpy.unwrap para cruce del punto Aries.ANGULOS_Y_ORBES_ASPECTOS (antes ignorados, solo matching exacto).017_drop_ephemerides_planetas.sql: Elimina v_ephemerides_legible y ephemerides_planetas.True_South_Node: Derivado automáticamente del Nodo Norte (+180°).TransitosCalculator.php: Refactorizado calcularTransitosCompletos() para usar el motor Python vÃa proc_open() con JSON stdin/stdout. Añadidos ejecutarCalculoTransitos(), adaptarResultadoPython(), lonToRaw() y formatearGradosMinutos(). obtenerRangosTransitos() ahora lee del resultado Python en memoria.ajax/transitos.php: get_date_limits() usa rangos configurables vÃa .env (TRANSITOS_ANIO_MIN/MAX) en vez de SQL.routes.php: Limpieza de ruta innecesaria.scipy>=1.17 al Python del sistema ServBay 3.11.sweepFlipped para invertir el sweep-flag SVG cuando corresponde. Corrige arcos de ~320° (89% del zodÃaco) en planetas retrógrados y la posición del label (tSN, tNN).switchTab() renombrado a switchChartTab() para evitar colisión con la función global de script.js que se carga después vÃa footer.php.calcularPuntosDeAspectos() y procesarTransitos(): Marcados como @deprecated. Se eliminarán en v2.4.0.run_update() en ambos plugins. Ahora descarga el zip por separado con SSL relajado y download_url(), usa Automatic_Upgrader_Skin + archivo local en vez de WP_Ajax_Upgrader_Skin + URL remota, fuerza FS_METHOD=direct, verifica tanto $result como $skin->result, y comprueba el retorno de activate_plugin(). Esto resuelve el falso "actualizado correctamente" cuando el upgrader fallaba silenciosamente.carta-natal-maia y maia ahora invalidan el transient de actualización si la versión cacheada ya no es superior a la versión local. Esto evitaba que apareciese una falsa notificación de "actualización disponible" cuando servidor y plugin tenÃan la misma versión.wordpress_chart_logs para registrar cada cálculo realizado desde WordPress.api/v1/license/validate para verificar validez de API Key y estado Premium.is_premium en tabla wordpress_sites (Migración 014_add_is_premium_to_wp_sites.sql).custom_title y custom_copyright soportados (solo para sitios Premium).InfoAstrologica.php.admin/users.php: los 5 formularios (change_group, block, unblock, delete, impersonate) no incluÃan el campo csrf_token desde que se añadió la validación CSRF en v1.0.5. El intento de impersonar, bloquear, desbloquear, eliminar o cambiar grupo devolvÃa «Token de seguridad inválido». Añadido <input type="hidden" name="csrf_token"> a todos.default_group_id y force_group_sync faltantes en wordpress_sites. El INSERT de WordPressSiteRepository::create() fallaba con SQLSTATE[42S22]: Column not found: 1054 Unknown column 'default_group_id' al crear integraciones WordPress en producción.@import en el CSS..astronomicon, .planeta-header, .simbolo-aspecto, .cnm-summary-glyph, .cnm-pt-symbol escalados al mÃnimo de 30px establecido por convención.ChartImageRenderer): nueva clase GD+TTF que dibuja la carta natal completa como imagen PNG (1200×1200). Incluye rueda con signos, casas, planetas con grados/signo/minutos, lÃneas de aspecto con sÃmbolos, cabecera con datos personales y tabla de posiciones planetarias. Usa fuente Astronomicon (TTF) directamente — no depende de renderizado SVG del servidor.chart_image field: POST /api/v1/chart/calculate ahora devuelve data.chart_image con la imagen PNG en formato data URI base64 (además del SVG y HTML de aspectos).charts/view.php detecta datos incompletos (planets o casas vacÃos), fuerza recálculo vÃa calcularCartaNatal() y persiste los datos en BD con el nuevo método CartaNatalRepository::guardarDatosCalculados().guardarDatosCalculados(int $cartaId, array $datos) en CartaNatalRepository: actualiza solo la columna datos_calculados de una carta existente.renderAspectosList() no mostraba aspectos: la función usaba claves en español con acentos ('conjunción', 'oposición'…) pero AspectosCalculator devuelve las claves en inglés de ANGULOS_Y_ORBES_ASPECTOS ('conjunction', 'opposition'…). Ahora renderAspectosList() recibe directamente el resultado de AspectosCalculator::getAspectosUnicos() y mapea correctamente las claves.#cnm-wheel-container, #cnm-aspects-container) por tabulación sobrante en shortcode.php. Eliminados los divs cnm-tab-* no utilizados. Estructura HTML corregida. Layout de resultados reorganizado: imagen PNG principal + sección de aspectos.api/v1/chart/calculate.php: usa AspectosCalculator para generar el listado de aspectos y el contador de stats; genera imagen PNG vÃa ChartImageRenderer.integrations/wp-plugins/carta-natal-maia/: JS usa data.chart_image (PNG) como vista principal. CSS con clase .cnm-chart-png responsive.charts/view.php: detección de datos incompletos + recálculo + guardado en BD.#carta-natal-maia-app en todos los selectores + !important en botones para vencer temas WP.POST /api/v1/chart/calculate ahora acepta API keys de ambos tipos (maia y carta-natal-maia).OPTIONS) automático en cualquier ruta registrada.008_create_geonames_places.sql: tabla geonames_places con FULLTEXT en name + asciiname + alternatenames.scripts/import_geonames.php: importa allCountries.txt (solo clase P, ciudades/pueblos) en lotes de 1000. Uso: php scripts/import_geonames.php --file=/ruta/allCountries.txt.data/geonames/ para colocar el archivo allCountries.txt.GET /api/v1/geo/search ahora consulta la BD local (con fallback a API GeoNames si la tabla no existe).009_geonames_indexes.sql: Ãndices B-tree en name, asciiname y population para acelerar búsquedas LIKE + ORDER BY.ob_start() en index.php para que los endpoints API puedan emitir headers CORS limpios sin interferencia del debug output de init.php.file_get_contents → curl) para llamadas a GeoNames como último recurso.display_name, lon, address.city/state/country) y formato GeoNames (name, lng, countryCode).js/script.js usa /api/v1/geo/search (BD local) en vez de Nominatim. Búsqueda automática al escribir 3+ caracteres (con 400ms debounce). Enter para búsqueda manual de 1-2 caracteres.010_create_admin1_codes.sql + script import_admin1.php traducen códigos FIPS/ISO a nombres (ej: "56" → "Catalonia"). geo/search hace LEFT JOIN automático.?? 'maia' para plugin_type en sitios existentes sin la migración aplicada./integrations/wp-plugins/carta-natal-maia/):[carta_natal_maia] que muestra un formulario paso a paso (nombre → fecha → hora → lugar) y calcula la carta natal de forma anónima.POST /api/v1/chart/calculate):X-API-Key (clave secreta del sitio).chart_count en wordpress_sites).admin/integrations.php):007_add_plugin_type.sql: columna plugin_type y chart_count en tabla wordpress_sites.validateApiKey(), incrementChartCount(), getByType()./integrations/wordpress/ → /integrations/wp-plugins/maia/.obtenerPorId() ahora requiere userId en transitos, direcciones, lunaciones y progressions. Las cartas ajenas ya no son accesibles mediante enumeración de IDs./charts/save, /charts/delete, /admin/users. Formularios de creación/edición incluyen campo oculto csrf_token. El JS de eliminación envÃa el token desde el <meta>..env inaccesible vÃa web: añadido <FilesMatch "\.env$"> Require all denied </FilesMatch> en .htaccess.logs/.htaccess actualizado a sintaxis Require all denied (la sintaxis Apache 2.2 anterior era ignorada en servidores modernos).reset_token ahora se almacena como hash('sha256', $token) en BD, no en texto plano. findByResetToken() compara hashes.requestPasswordReset() ya no incluye el token en el array de retorno.DebugManager::returnAjaxError() ya no incluye file, line ni trace en respuestas JSON (solo success + error)./api/ ahora requiere partes/RequireLogin.php.nombre, lugar, relacion, notas de carta se escapan con htmlspecialchars() antes de codificarse a JSON para showCartaInfo() en el dashboard.obtenerUsuariosCompartidos() y obtenerUsuariosDisponibles() ya no devuelven email. share-manager.js actualizado para no mostrar emails.verificarLimiteCartas() usa SELECT ... FOR UPDATE para evitar race condition en creación concurrente de cartas.SameSite y Secure ahora dependen de si la conexión es HTTPS. En HTTP local usa Lax + Secure=0, evitando que falle la autenticación en desarrollo.calculateDirecciones(), calculateLunaciones() y calculateProgressions() usaban $user['id'] fuera de scope (la variable no es accesible desde funciones standalone). Ahora reciben int $userId como parámetro explÃcito desde el switch del scope global.js/script.js + charts/create.php + css/styles.css):.form-step / .form-step-hidden / .form-step-visible#timeAdjustment) movido a la sección de lugar, bajo los resultadosvalidarFecha() en CartaNatalRepository.php ahora exige 1800–2400js/script.js)min="1800-01-01" y max="2400-12-31" en charts/create.phpincludes/AuthManager.php:376): phone_country y form_load_time se pasaban a UserRepository::create() y se intentaban insertar en usuarios (columna inexistente → SQLSTATE[42S22]). Añadidos al unset().auth/forgot-password.php:118-120): /UserLogin.php → route('/login'), /UserRegister.php → route('/register').admin/version.php: ahora se lee dinámicamente del CHANGELOG.mdcss/login.css): clase .app-footer + regla .user-login-container > .app-footer para grid de loginappVersion() en includes/Functions.php: lee la versión del CHANGELOG.md (cache estática)copyrightFooter() en includes/Functions.php: combina APP_COPYRIGHT de .env con appVersion() + versión enlaza a /admin/versionAPP_COPYRIGHT en .env y .env.example para editar el copyright centralizadamentestyle="") de dashboard.php — extraÃdos a css/dashboard.css<style> inline de admin/version.php a css/version.csscss/direcciones.css a css/dashboard.cssfont-size mÃnimo de 16px en todas las nuevas clases CSSAGENTS.md documentado con reglas de estilo: sin inline CSS/JS, min 16px texto, min 30px sÃmboloscss/dashboard.css — estilos del dashboardcss/version.css — estilos de la página de versiónCall to undefined method Database::prepare() en dashboard.php y ajax/feature_chart.php$db->query() y $db->fetchOne() (métodos del singleton Database)SyntaxError: redeclaration of let Modal por carga duplicada de Modal.jsdashboard.php (footer.php ya incluye modal.js)filtro-carta-natal.js mostraba error en páginas sin botón "Aplicar Filtros"Utils.error → Utils.log para que solo sea informativochart_featured no existente al primer usoCREATE TABLE IF NOT EXISTS automático en dashboard.php y ajax/feature_chart.phpalert() en caso de error + deshabilitado temporal del botón durante la peticiónaccess_direcciones ("Ver Direcciones") añadido al sistema de permisos/admin/version)includes/DireccionesCalculator.php — clase calculadora de direcciones de arco solarpartes/direcciones.php — template de la página de direccionesajax/direcciones.php — endpoint AJAX para cálculo de direccionesjs/direcciones-js.php — JavaScript del módulo de direccionescss/direcciones.css — estilos del módulo de direccionesajax/feature_chart.php — endpoint AJAX para toggle de destacadomigrations/add_direcciones_permission.sql — migración del permiso de direccionesmigrations/add_chart_featured.sql — migración de la tabla de destacadosadmin/version.php — página de versión y changelogCHANGELOG.md — este archivocharts/view.php — añadida pestaña "Direccions" con verificación de permisopartes/header.php — añadido <link> para direcciones.csspartes/footer.php — añadido <script> para direcciones-js.phpdashboard.php — añadido icono de estrella para destacar cartas, ordenación por destacadas primeroadmin/index.php — añadida tarjeta "Versión y Log" en el panel de administraciónroutes.php — añadida ruta /admin/version