Historial de cambios de la aplicación
Aplicación de cartas natales
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