Commit Graph

54 Commits

Author SHA1 Message Date
rafa d528fe45ae cutover: wp-nuevo verificado, y por que no hace falta esperar al cert
Traefik ya tiene los dos routers (302 a ambos, 404 a lo no declarado).

Detalle que quita presion manana: wp-nuevo va proxied, asi que Cloudflare
presenta su propio cert a los bots y hacia el origen, con el modo SSL en Full
(no strict), acepta cualquiera - incluido el autofirmado de Traefik. El hostname
sirve desde el primer segundo, sin esperar a Lets Encrypt.

El reverso, anotado: subir la zona a Full (strict) antes de que los certs esten
emitidos romperia exactamente eso.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 21:36:46 -04:00
rafa 603eb1ecb4 cutover: paso 0 para wp-nuevo, LLAR fuera y el modo SSL ya confirmado
Aviso de Mixbot (2-ago): los bots de la carta publican por wp-nuevo.feadulta.com,
no por www, porque el WAF de Cloudflare los bloquea por www (regla "bloquear bots
1"). Ese hostname no tiene registro propio, lo cubre el wildcard, y Traefik solo
enruta nuevo.feadulta.com: al mover el A del apex los bots dejarian de publicar.
La carta 738 se compone el martes.

Se anade el paso 0 al runbook. Hay que hacerlo en el PANEL: por API el PATCH de
fqdn da 422 y actualizar SERVICE_FQDN_WORDPRESS cambia la variable pero no
regenera los routers (comprobado hoy). Plan B documentado: los bots pueden pasar
a nuevo.feadulta.com, que es grey cloud y por tanto no pasa por Cloudflare ni le
aplica ninguna regla del WAF.

Decision de Rafa: el bloqueo de login lo lleva Wordfence y sale
limit-login-attempts-reloaded. Wordfence hace lo mismo y ademas firewall de
aplicacion, escaner de malware y 2FA; los dos juntos son dos contadores
compitiendo. Retirado del staging (7 plugins activos, sitio en pie) y anadido al
post-import-seguridad.sh (paso 3c) porque el dump lo revive en cada import, junto
con sus 12 opciones huerfanas. Nuevo paso 3d que verifica que Wordfence sigue
activo y lo reinstala si faltara.

Inma confirma que el modo SSL de la zona es Full, no Flexible: cae el riesgo de
bucle de redireccion. Marcado como resuelto en los verdes previos.

Anadida la seccion del correo: el cutover no lo toca (MX a otra IP), pero la
caducidad del 07/08 si, y ediciones@/contenido@ son de donde sale la carta.

Anotado tambien que Cloudflare devuelve 403 a la IP de Rafa para cualquier
hostname de feadulta: un 403 desde su maquina no prueba nada, la verificacion de
los bots la tienen que hacer ellos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 21:31:37 -04:00
rafa da3dfae0a3 e2e: targets del sitio de staging
El runner necesita el array 'urls'; el fichero solo tenia baseUrl. Se anaden 11
targets: portadas de los 5 idiomas, carta de la semana, un articulo, evangelio
del dia, buscador, wp-admin y wp-login.

Resultado de la corrida contra nuevo.feadulta.com: los 6 puntos en verde
(5 idiomas 200, carta de la semana con sus enlaces, permalink de articulo,
buscador con 7.344 resultados para "evangelio", wp-admin/wp-login, y CERO
errores PHP en el HTML). Los WARN son imagenes que aun no habian llegado por el
rsync en curso, no fallos de la migracion.

Nota para no volver a asustarse: 6.195 guid de adjuntos apuntan al WordPress
local (IP de Tailscale). Es cosmetico: el guid no sirve imagenes. Lo que las
sirve es _wp_attached_file, y las 7.755 son rutas relativas, 0 absolutas, con
upload_path y upload_url_path vacias.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:53:17 -04:00
rafa 5ed2eaf3fe cutover: Wordfence entra en el plan (son 8 plugins, no 7)
CDMON tiene ModSecurity a nivel de hosting: fue lo que detecto las subidas de
wp-cl.php y health-check.php durante el incidente #183. En Hetzner, con Traefik,
NO hay WAF ninguno. Migrar tal cual estrenaria servidor con una capa defensiva
menos que la que tenia el sitio comprometido.

Wordfence 8.2.2 instalado limpio desde wordpress.org en el staging y verificado:
portada 200, wp-login 200, sin fatales, y fea-cloudflare-realip.php presente (sin
el veria todas las visitas como si vinieran de Cloudflare).

Queda en proteccion basica: el extended protection exige auto_prepend_file desde
su asistente y eso se hace despues del cutover, no en la ventana.

Anotado tambien que Wordfence y limit-login-attempts-reloaded solapan en el
bloqueo de login; hay que decidir cual manda.

Y documentada en el paso 6 la retirada de wordfence_core_options, que pese al
nombre no es de Wordfence sino el payload de spam del atacante.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:44:57 -04:00
rafa 0ee9a2eef5 cutover: retirar en cada import la inyeccion de SEO spam del #183
Buscando si Wordfence habia que reinstalarlo limpio (#183 comment-492) resulta
que Wordfence NO esta instalado en produccion. Lo que si queda es una opcion
llamada 'wordfence_core_options' que no es suya: es el payload del atacante,
disfrazado con ese nombre para pasar por legitimo.

Cifrado con un cesar de una letra (ejw->div, tuzmf->style, isfg->href). Una vez
descifrado:

  <div style="position: absolute;margin-top: -110px;">
    <div style="position: absolute; left: -7585px;">
      <a href="https://1xbetjap.com/ja/casino/">

Un div fuera de pantalla con enlaces a un casino japones, con el campo output
apuntando a wp_footer. Esto explica los "dos POST a wp-admin/options.php" del
comment-492, que se habian atribuido a que el atacante toco la configuracion de
Wordfence: lo que hacia era crear esta opcion.

Hoy esta inerte porque quien la leia era el plugin wp-highlits, en cuarentena.
Pero viaja dentro del dump, asi que CADA import la reintroduce en el servidor
nuevo: por eso va aqui y no como limpieza manual de una vez.

Se preserva el valor en /data/feadulta-migracion/evidencia-183/ antes de borrar.

Barrido de la BD del destino: es la unica. 0 en post_content, 0 enlaces ocultos
fuera de pantalla en el contenido, 0 referencias a las IPs del atacante.

Nota sobre el escaneo previo del dump: lo di por limpio buscando <script, eval(,
base64_decode y gzinflate. Este payload no lleva ninguno de esos patrones y paso
el filtro. El escaneo por firmas conocidas no acredita limpieza.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:34:12 -04:00
rafa 508a6b4178 cutover: runbook del lunes, script de seguridad post-import y sitio E2E de staging
Con los tiempos REALES medidos en el ensayo del 31-jul, no estimados:
dump 25s + subida 74s + saneo 2s + import 107s = camino critico ~3min30s.

- docs/cutover/RUNBOOK-cutover-wordpress.md: los pasos en orden, copiables, con
  el rollback y los tres verdes obligatorios previos (freeze, CDMON vivo, modo
  SSL de la zona).
- scripts/cutover/post-import-seguridad.sh: rota la contrasena de los 6
  administradores (las lee del .env del perfil, nunca van en el repo), retira el
  rol a pabloarias/josek/andrey y limpia los cron huerfanos de Yoast y WP
  Profile Builder. Es un script y no una tarea manual porque el dump revive esas
  cuentas en CADA import.
- tools/e2e/sites/nuevo.json: la suite toma el host de un JSON, asi que apuntarla
  al staging es anadir un fichero.

Hallazgo del ensayo: el origen es MariaDB 11.8.6 y el destino MySQL 8.4.10; el
dump trae 20 tablas con collations uca1400_* que MySQL rechaza. El saneo las
mapea antes de importar. Sin ese paso, el import aborta.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:15:58 -04:00
rafa 5c5ea60348 cutover: manifiesto explicito de los mu-plugins a desplegar
La regla del #180 comment-516 dice "recrear los fea-* desde el repo", pero
carta-semana-plugin.php y stop-redirects.php estan vivos en produccion y no
empiezan por fea-: un glob los dejaria fuera. El despliegue copia esta lista.

28 ficheros con su sha256. Excluidos a proposito los .bak-*, el .disabled,
el health-check.php.quarantined-incident-183 (backdoor del #183) y
fea-support-campaign.php, que esta en el repo pero nunca llego a desplegarse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:01:18 -04:00
rafa 91a212250d mu-plugins: versionar los dos que solo vivian en produccion
Preparando el cutover a Hetzner (#180), el criterio de migracion limpia
(#180 comment-516) dice recrear los mu-plugins desde el repo. Comparados por
MD5 los 26 comunes coinciden byte a byte, pero estos dos estaban vivos en
produccion y en ninguna rama del repo:

- fea-security-blocked-users.php: es la contencion del incidente #183
  (comment-437). Bloquea wp_authenticate_user y allow_password_reset para
  los IDs 1047 (pabloarias), 1049 (josek) y 1087 (andrey). Sin este fichero,
  recrear "los fea-* desde el repo" habria revivido las tres cuentas en el
  servidor nuevo, porque sus hashes viajan dentro del dump.
- fea-subir-avatar-api.php: endpoint REST del #175.

Copiados por HTTP-less cat sobre SSH y verificados por sha256 contra el
origen; php -l limpio en ambos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:56:49 -04:00
rafa 69e849d38e Sync: guardar en el historial el trabajo de las ultimas semanas que solo vivia en el disco
Este repo local tenia origin apuntando al Gitea local (localhost:3000), que Rafa
declaro archivado el 2026-06-28 (commit 962f33a, desarrollo movido a
gitea.feadulta.com). Ese memo nunca llego a este checkout: main quedo congelado
y todo el trabajo real de las ultimas 3+ semanas se fue commiteando solo en la
rama fix/multiidioma-portada-132 (ya fusionada a main sin perdida, commit
2504666), mientras que ademas se acumulaban 78 cambios sin commitear en el
working tree que nunca llegaron a NINGUN historial de git.

Este commit consolida esos cambios sueltos: TTS multi-voz (tts_*.py), scripts
de traduccion (translate_haiku.py, pretranslate_en_haiku.py, sync_translations_to_prod.py),
mu-plugins nuevos desplegados a prod (fea-beta-feedback, fea-cloudflare-realip,
fea-legacy-redirect, fea-gsc-verification, fea-support-campaign, fea-ui, etc.),
scripts de mantenimiento de enlaces/cartas, capturas E2E (tools/e2e/shot_*.cjs)
y documentacion de sesiones recientes.

Excluido deliberadamente (no es codigo versionable): tts-voices/ (3.3GB de
muestras de audio para clonacion de voz, anadido a .gitignore), logs/ (logs de
ejecucion, anadido a .gitignore), y 2 ficheros vacios accidentales + 2 copias
duplicadas sueltas en la raiz que ya existen en su ubicacion correcta.
2026-07-15 20:03:21 -04:00
rafa 250466696b Buscador: AND obligatorio en FULLTEXT, excluir paginas-indice K2 y soportar frase exacta entre comillas
Tres bugs corregidos (issue gitea.feadulta.com#178):
- OR implicito en Boolean Mode devolvia casi todo el sitio (~24.700/24.780 posts)
  para cualquier busqueda de varias palabras; ahora cada palabra es AND obligatorio (+).
- Paginas-indice residuales K2 (categorias 37/38, listados por autor) puntuaban
  altisimo en relevancia por su tamano y tapaban el articulo real; excluidas de
  resultados de busqueda.
- Palabras <3 caracteres forzadas como termino obligatorio rompian el calculo de
  relevancia (score 0 en filas que si coincidian); se descartan antes de construir
  la query.
- Anadido soporte real de frase exacta: termino entre comillas dobles se envia a
  MySQL Boolean Mode como frase, en vez de descartarse silenciosamente.
2026-07-15 19:14:43 -04:00
rafa f747ec30c7 feat: sync_carta_from_prod.py para copiar el cluster de una carta prod->local (#174)
Con Mixbot publicando la carta ES directamente en prod, el WP local dejaba de
recibir el contenido nuevo y se desalineaba (visto con la carta 735, ID 54495
apuntando a otro post en local). El nuevo script descubre el cluster de la
carta parseando sus propios enlaces (fea_parse_carta_sections, sin depender de
_carta_id) y clona cada post a local preservando ID, reutilizando
fea_translate_helper.php en la dirección contraria a sync_translations_to_prod.py.

Añade el subcomando 'carta_sections' al helper (aditivo, no toca el resto).
2026-07-14 22:49:43 -04:00
rafa f059ed17be feat(api): endpoint fea/v1/crear-autor para alta de autores (#166)
POST /wp-json/fea/v1/crear-autor — permite a Mixbot/Inma dar de alta
autores nuevos sin depender de intervención manual, sin necesitar el
permiso sensible create_users de WP. Rol 'author' fijo en el código
(no lo decide quien llama). Auth: is_user_logged_in() + edit_others_posts
(nivel Editor+, ya lo cumple su cuenta actual). Idempotente por slug:
si ya existe, devuelve el usuario existente en vez de duplicar/errorar.

Ver gitea.feadulta.com#166 (protocolo entre Claudes, opción B elegida
sobre A porque WP no permite nativamente restringir create_users a un
solo rol, y sobre C porque evita el salto humano/agente en el bucle).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 21:52:41 -04:00
rafa 7188adf0d7 feat(analytics): eventos GA4 audio_play y share_click
fea-audio-player.php: el <audio> lleva data-post-id/data-voice; listener
en wp_footer dispara gtag('event','audio_play', {post_id, voice}) en el
primer play (no en cada resume).

fea-share.php: se sube también al repo (nunca se había comiteado). El
botón de Facebook lleva data-fea-share-track="facebook" + listener que
dispara gtag('event','share_click', {method, post_id}).

Ambos reusan el dataLayer/gtag que ya inicializa fea-analytics.php, sin
añadir JS de terceros. Verificado en local con Playwright (evento se
dispara correctamente en ambos casos).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 21:34:32 -04:00
rafa db0d0a03f8 feat: TTS multi-voz por autor (MiniMax voice cloning, gitea.feadulta.com #152)
Voz clonada por autor (Fray Marcos, Pagola, Sicre, Arregi) en vez de una
única voz genérica para todo el TTS. minimax_tts.py resuelve la voz por
post_author (AUTHOR_VOICES) y cae a Nico si no hay clon; fea_post_io.php
propaga autor/voz; tts_produce.py y sync_audio_to_prod.py (local→prod,
workaround glibc rota del servidor) usan la voz correcta en vez de forzar
Nico.

Incluye fix de trim_after_author_signature: la heurística de "firma final
del autor" confundía encabezados litúrgicos iniciales (p.ej. "CORPUS (A)")
con la firma, truncando el texto a sintetizar a un par de palabras. Ahora
solo cuenta como firma un párrafo con >200 caracteres de contenido real
por delante.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 21:34:20 -04:00
rafa 82ca122a4e Add verify_carta_lang_links.php QA script for multilingual carta links
Reports internal links whose language prefix/slug doesn't match the target
post's actual language (SIN_TRADUCIR / MISMATCH), so link bugs like the
silence/silence-2 cross-language mixup on carta 734 get caught before they
reach prod.
2026-07-08 09:28:53 -04:00
rafa e2f23ace9f feat(#8): buscador avanzado nativo (FULLTEXT + filtros)
Replica el «Buscador avanzado» del Joomla viejo sin servicios externos:
- fea-search-fulltext.php: motor MySQL FULLTEXT (MATCH AGAINST, Boolean Mode,
  orden por relevancia); comprueba que existe el índice fea_ft y degrada al
  buscador nativo si no, sin romper.
- fea-search-advanced.php: formulario con filtros por autor, categoría
  (lista curada de secciones), cita bíblica (meta _cita_evangelio por prefijo)
  y rango de fechas; pre_get_posts, chips de filtros, byline en tarjetas,
  i18n es/en/fr/it/pt. Excluye pseudo-autores «Testamento».
- create_buscar_page.php: crea la página /buscar + traducciones (idempotente).
- tools/e2e: suite Playwright de verificación.

Desplegado y verificado en prod (índice fea_ft + página /buscar + traducciones).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 18:24:53 -04:00
rafa d959331ed0 docs(#8): plan del buscador avanzado nativo (sin Typesense)
Plan ejecutado por Sonnet: replica el «Buscador avanzado» K2 con WP nativo
+ MySQL FULLTEXT (palabra, autor, categoría, cita bíblica, fecha).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 18:24:53 -04:00
rafa 493fd4d835 docs(#8): plan de implementación del buscador Typesense (fase 2)
Plan ejecutable (para Sonnet) de la fase 2: arquitectura, schema, indexador,
UI InstantSearch con faceta de autor, seguridad de claves y verificación.
La decisión de dónde corre Typesense en producción queda pendiente; todo lo
demás se puede construir y validar en local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:30:34 -04:00
rafa 63e0057fd5 chore(#143): avisar en el importer cuando un autor K2 cae a 'Fe Adulta'
Cuando created_by de Joomla no está en user_map, post_author cae a 1
('Fe Adulta'). Se añade un aviso por consola apuntando a fix_k2_authors.php
para corregirlo tras el delta. (El resto de cambios del fichero, de sesiones
previas, quedan sin commitear.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:21:51 -04:00
rafa 6c0e366b03 docs: guía de publicación de la carta semanal para Inma (REST API)
Guía para el asistente de Inma: conexión por REST API, modelo
carta->portada, categorías, flujo de publicación y qué es automático.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:18:50 -04:00
rafa 8ea4d51fd2 feat(#8): buscador visible (MVP nativo)
- fea-search.php: barra de búsqueda visible en móvil (donde el menú
  colapsa y oculta el buscador del menú); multiidioma por Polylang.
- set_search_template.php: template FSE 'search' con resultados en
  rejilla de tarjetas (reutiliza el layout de archive #63) en vez del
  patrón del tema que los mostraba 'todos seguidos'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:18:50 -04:00
rafa 84ec9163b2 fix(#143): corregir autoría 'Fe Adulta' y avatares de iniciales
Artículos K2 cuyo created_by de Joomla no tenía usuario WP caían a
post_author=1 ('Fe Adulta'). Herramientas:
- fix_k2_authors.php: crea el usuario (nombre real de ew4r_users) y
  reasigna post_author (idempotente, dry-run/APPLY).
- gen_avatars_initials.py: recrea el generador de avatares de iniciales
  del #62 (paleta determinista + DejaVuSans-Bold).
- import_avatars_143.php: alta de attachment + foto_perfil con backup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:18:50 -04:00
rafa 6d9afaaf60 fix(#142): player de audio visible en móvil vertical
El reproductor se inyecta en el grupo flex del autor (wp-block-group FSE).
El flex-wrap apuntaba a .fea-byline (solo existe en Astra), por lo que en
móvil vertical el <audio> se comprimía y no mostraba controles. Se dirige
el wrap también al grupo FSE del byline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:18:50 -04:00
rafa 3d3ca037f3 feat(i18n): traducir nombre del libro en títulos de lecturas EN/FR/IT/PT (#140)
Mapa de 73 libros bíblicos ES->{en,fr,it,pt} y reemplazo determinista del
token de libro en cada cita biblica del post_title (inicio, compuestos '/',
ordinales '2 Timoteo', prefijos de fiesta). Idempotente, title-only,
cotejo insensible a acentos. Aplicado: local 2103, prod wp-nuevo 1605.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:41:09 -04:00
rafa a2aefd8e0b scripts(homenajes): migración Mardones/Galarreta + sync prod #130/#131/#133/#137/#138
Pipeline de los 23 posts de Mardones (anterior/*.htm → posts), repunte de
presentaciones de Galarreta, y scripts de sync a prod de las 5 issues del día.
Incluye retoques de fecha (2012-10-10) y eliminación de target=_blank en el
homenaje 17990.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 09:06:39 -04:00
rafa e26b279e63 fix(avatares): traducciones de lecturas heredan el símbolo del ES (#135/#66)
fea_avatar_url: para posts no-ES resuelve el título del original ES (Polylang),
donde sí se reconoce el libro/evangelista. Así Matteo/Geremia/Romani heredan el
mismo icono que Mateo(ángel)/Jeremías/Romanos(biblia) en vez de identicon random.
También incluye el guard de Multimedia solo-ES (#135 #1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:08:15 -04:00
rafa d5475ede8f chore(sync): scripts ajustados para deploy a prod (staging wp-nuevo)
- autores_biblicos.php: 2ª pasada directa de reasignación de autor.
- quitar_multimedia.php: #4 como fichero reutilizable.
- trad_cats.php: quitado require_once con ruta local (wp eval-file ya carga WP).

Sync a prod ejecutado y verificado: código + 440 lecturas + grupo D + 8 usuarios
bíblicos + reasignación de autores y categorías. 0 fatales en las 5 portadas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:45:59 -04:00
rafa 70831f13f0 feat(i18n): #136 reasignación de categorías traducidas + autores bíblicos por idioma
- trad_cats.php: traduce categorías estructurales (Nuevo/Antiguo Testamento, etc.)
- fix_catnames.php: corrige nombres de términos traducidos
- autores_biblicos.php: crea usuarios NT/AT por idioma y reasigna autor de lecturas
- reasign_cats.php: reasigna a cada post no-ES las categorías del ES traducidas a
  su idioma (crea término espejo si falta). Idempotente/resumible (_cats_reasignadas).
  7977 posts procesados, 0 con categoría en idioma incorrecto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:10:27 -04:00
rafa 0bc58bfa96 feat(lecturas): lote masivo de lecturas bíblicas vía índice del leccionario
build_lectionary_index.py construye un índice por referencia bíblica
descargando un ciclo litúrgico de evangelizo.ws (combina idiomas por
reading_code, robusto a fiestas trasladadas por país). lecturas_apply.py
casa las lecturas ES por referencia y apply_lecturas_wp.php crea+asocia
las traducciones en Polylang. 440/501 lecturas cubiertas y publicadas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:38:45 -04:00
rafa 0da0c0eb59 feat(lecturas): descargador de lecturas bíblicas litúrgicas (evangelizo.org)
Descarga el texto litúrgico católico oficial en es/en/fr/it/pt por fecha
litúrgica (leccionario), para traducir las lecturas bíblicas de las cartas
sin LLM. Parsea las secciones (1ª lectura, salmo, 2ª lectura, evangelio).
Grupo C de #132.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:43:30 -04:00
rafa 9ac857b027 fix(portada): artículos por idioma y enlace evangelio del día (#132)
Bug 2: fea_carta_section_posts parsea SIEMPRE la carta ES (el parser solo
reconoce cabeceras en español) y mapea cada artículo a su traducción
Polylang del idioma de la portada. Antes EN/FR/IT/PT caían al fallback
'últimos por categoría' y mostraban artículos incorrectos.

Bug 1: el enlace 'evangelio del día' resuelve la traducción del idioma
actual con pll_get_post (get_posts usa suppress_filters=true y devolvía
siempre la página ES).

Desplegado y verificado en prod (wp-nuevo) 2026-06-22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:30:18 -04:00
rafa 66ee943da4 chore(verify): recuperar enum_urls.php y commitear tooling de verificación #121
El enumerador de URLs no se había commiteado y se perdió. Recuperado
reproduciendo la composición de la corrida previa (menús×5 idiomas +
categorías con contenido + contenido no-ES). Se añade también
classify_gaps.py (clasifica huecos: traducir vs descargar) y run_all.sh.
Outputs y .venv quedan en .gitignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:30:18 -04:00
rafa 863a3c6677 feat(analytics): GA4 (gtag) + banner de consentimiento de cookies (#93)
- fea-analytics.php: GA4 G-6RT9ZRS4LW vía gtag en mu-plugin (Consent Mode v2,
  default denied). Measurement ID portable: cutover wp-nuevo -> www indoloro.
- fea-cookie-consent.php: banner RGPD discreto, multiidioma ES/EN/FR/IT/PT,
  enlace a la politica resuelto por idioma (Polylang). Aceptar -> consent update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:49:59 -04:00
rafa 6791afdb42 Handoff carta 46956: traducciones Haiku + evangelios descargados + scripts; despliegue prod pendiente (issue #88, #87) 2026-06-17 21:40:56 -04:00
rafa 27aad22cd5 Merge remote-tracking branch 'origin/main' 2026-06-14 19:32:40 -04:00
rafa 378ff6510f Update tracked WordPress core files 2026-06-14 19:30:16 -04:00
rafa 3ddfd8d665 Add Joomla PHP 8.3 local service 2026-06-14 19:29:28 -04:00
rafa efff1b9350 Document Joomla production delta import 2026-06-14 19:28:58 -04:00
OpenClaw Agent fb7eec883c docs: add Gemma 4 benchmark plan for Feadulta 2026-06-04 09:19:27 -04:00
rafa e2ea90dd1e chore(wp): registrar borrado de scripts de raíz WP
Los ficheros archivados/borrados ya están descritos en archive/README.md
y el commit anterior. Este solo registra los deletes en git.
2026-05-26 08:54:49 -04:00
rafa 587d718fde chore(wp): limpieza wordpress/ — backdoor reset.php, scripts migración, updraft
- Borrar reset.php (utilidad de reset password admin que dejamos suelta — backdoor)
- Archivar 25 scripts de migración K2→WP a archive/wp-migration-scripts/
- Borrar wp-config-docker.php (plantilla original Docker, sin uso) y
  clasificacion_articulos.csv (duplicado del que ya está en archive/logs-migracion/)
- updraft/: retención (5 ficheros backup completo Feb-02 + Feb-28 + Mar-15 +
  Abr-30 + 7 últimos días May) — borrados 47 backups + 32 logs (2.1 GB)
- Borrar 4 themes no usados (generatepress, kadence, twentytwentythree,
  twentytwentyfour) — solo astra + twentytwentyfive en uso
- Borrar wp-content/plugins/hello.php (Hello Dolly demo) y
  delete-duplicate-posts.zip (instalador antiguo, 1.9 MB)
- Borrar dirs vacíos wp-content/{plugins-old,upgrade,upgrade-temp-backup}

WP local: 7.8 GB → 5.7 GB (-2.1 GB)
2026-05-26 08:54:32 -04:00
rafa 39431c98e3 chore: reorganizar carpetas de backups y archivar scripts/logs
- Unificar backup/ + backups/ + Akeeba sueltos en backups/ etiquetados por fecha
  (pre-incidente Enero, Marzo, INFECTED 25-mayo, clean 25-mayo, wp-local-dumps)
- Mover scripts de setup-inicial (Feb 2026) y logs de migración a archive/
- Mover kickstart.* a tools/akeeba-kickstart/
- Borrar joomla-php83-old-local-20260525/ (6.3 GB) y google-chrome.deb
- Conservar solo 2 de 8 dumps WP del 25-mayo (los hotfixes ya están consolidados)
- README de inventario con MD5 en backups/README.md y archive/README.md
- .gitignore: trackear los README dentro de carpetas gitignored
2026-05-26 08:38:22 -04:00
rafa 08d203249d script(cats): aplicar clasificacion_articulos.csv a wp_term_relationships
Toma el output de regenerar_clasificacion_csv.py y añade en BD las cats
1645-1650 que el CSV indique y no estén ya. Modo 'add' (default, conservador)
solo añade; modo 'replace' (opt-in) también borra cats sobrantes.

Recalcula wp_term_taxonomy.count tras los cambios.

Aplicado en local 2026-05-25 modo 'add': +183 atribuciones (1645 +1,
1646 +17, 1647 +29, 1648 +11, 1649 +93, 1650 +32). Cubre delta marzo-mayo
2026 + sub-clasificación posicional en evangelio.

Refs #42.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:49:55 -04:00
rafa 64672857fd script(cats): sub-clasifica evangelio por posición (lectura/editorial/comentario)
Regla del editor confirmada contra CSV histórico: dentro de "Evangelio y
comentarios al Evangelio" el orden es siempre:
  pos 1 → lectura del evangelio
  pos 2 → comentario editorial
  pos 3+ → comentarios

Mejora diff contra CSV marzo 2026: 8.655 → 9.286 mismas cats
(de 1.599 a 968 divergencias, -631 resueltas).

Residual mayoritario son posts que ya no aparecen en las cartas
actuales (links eliminados, k2 ids sin mapping) o granularidad fina
no implementada (lectura dentro de eucaristía, secciones especiales).

Refs #42.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:43:28 -04:00
rafa 89fcd10a5d script: regenerar clasificacion_articulos.csv desde las cartas
Reproduce el CSV histórico (marzo 2026) recorriendo todas las cartas
(cat 6+21+22) y extrayendo los links agrupados por encabezado. Paridad
con mu-plugins/fea-carta-portada.php (#38).

Cobertura: 5 secciones core (comentario, articulo, eucaristia,
multimedia, effa). NO cubre granularidad fina del CSV histórico
(lectura, comentario_editorial, otro, noticia) ni encabezados de
fiestas especiales — esos quedan agrupados en la cat más cercana.

Usa pymysql para no depender del CLI mysql (que rompe el parsing
cuando post_content tiene tabs/newlines). Modo --diff compara contra
un CSV de referencia y reporta divergencias.

Resuelve #42.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:39:08 -04:00
rafa d21e7f1e12 script(avatars): face-crop con OpenCV + regen thumbnails
Re-procesa los 724 avatares col_*.png (imágenes horizontales con foto+texto)
recortándolos cuadrados centrados en la cara antes de mostrarlos como avatar
circular. Resuelve la queja del #39 sobre las fotos "stretcheadas".

- face_crop_avatar.py: Haar cascade frontal+profile, padding 0.6×, fallback
  inteligente para imágenes horizontales (recorta lado izquierdo), reduce
  side cuando padding no cabe (no invade lado del texto)
- regen_avatars.php: borra thumbnails viejos + regenera con la nueva imagen
  para todos los attachments de uploads/autores/joomla/

Resultado: 100 caras detectadas + 624 fallbacks (mayoría placeholders sin
foto real) + 0 fail. Attachment IDs preservados.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:09:01 -04:00
rafa 54132b772a script(avatars): import bulk col_*.png de Joomla → user_meta foto_perfil
Usado para resolver issue #39 — 736 avatares de autor importados desde
joomla-php83/images/quienes_somos/col_*.png. Crea attachments WP con
wp_insert_attachment + wp_generate_attachment_metadata y asigna foto_perfil
a cada user_id según matching exacto (login==slug) o strong (login es token).

Preserva foto_perfil ya asignado. TSV de entrada esperado en
/tmp/avatar_assignments.tsv con columnas: aid, login, posts, source, filename.
Ficheros pre-copiados a uploads/autores/joomla/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:57:40 -04:00
rafa 382a6a6e87 feat(portada): modelo carta→portada + slider sync desde filesystem
- fea-carta-portada.php (nuevo, 1.0): parser de la carta semanal.
  Extrae secciones (Evangelio, Artículos, Eucaristía, Multimedia, EFFA)
  por encabezados y resuelve los href (WP slug o K2 legacy id) a
  wp_posts.ID. Cache en transient 15 min, invalidación en save_post.
  Cierra issue #38.

- fea-homepage.php: 4 shortcodes reescritos para usar
  fea_carta_section_posts() como fuente primaria, manteniendo el
  fallback existente (ACF + últimos por categoría).
  Afecta: [fea_articulos_semana], [fea_evangelio], [fea_eucaristia],
  [fea_multimedia]. El hero y noticia_centro sin cambios.

- fea-slider-sync.php (nuevo, 1.0): sincroniza Smart Slider 3 id 2
  con uploads/home/. Editor sube/borra imágenes en esa carpeta y el
  slider se actualiza automáticamente al primer pageview (modelo
  paridad con Joomla mod_ariimageslider). Cierra issue #43.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:26:01 -04:00
rafa 56ee1580b0 fix(content) + tools(e2e): rutas de imagen Joomla + suite E2E
- scripts/fix_image_paths.php: reescribe src/href "images/..." en wp_posts
  a "/fea/wp-content/uploads/..." cuando el fichero existe en uploads.
  Cubre comillas dobles/simples, urldecode antes de chequear filesystem.
  Resuelto contra issue #34 (458 posts, 465 refs) y completado con #36
  (124 posts, 128 refs tras recuperar 127 assets del backup producción).

- tools/e2e/: pipeline 3-tier para validar WP local con coste mínimo en
  tokens de Claude — Playwright (Tier 1, deterministas) + Gemma 4 vision
  en LM Studio (Tier 2, bajo demanda) + Claude solo lee report.md.
  Issue de diseño #37, suite inicial en sites/feadulta.json (13 URLs).

- .gitignore: excluir tools/e2e/node_modules y out/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:26:04 -04:00
rafa f857734130 docs: migrar documentación a la wiki del repo
Borrados:
- AUDITORIA_MIGRACION.md
- PLAN_MIGRACION.md
- WORDPRESS_CONFIG.md
- CARTAS_PARA_MIGRAR.md

README.md reducido a puntero a la wiki, que ahora contiene toda la
documentación operativa: credenciales, infraestructura, arquitectura WP,
áreas funcionales (cartas, evangelios, EFFA, autores, Brevo), scripts,
sincronización local→prod y roadmap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:12:17 -04:00