- sync_translations_to_prod.py / sync_audio_to_prod.py / sync_carta_from_prod.py: migran de FEA_PROD_HOST/PASS (CDMON, password) a FEA_PROD_SSH_HOST/PASS + FEA_PROD_DOCKER_CONTAINER (Hetzner/Coolify, auth por clave), con wrapping docker exec y el fix del bug de redirecciones (wc -c/cat) resuelto en el host en vez de dentro del contenedor. - fea_translate_helper.php: subcomando clone_new para clonar en ID local nuevo cuando el ID de prod ya está ocupado localmente. - translate_post.py: --dry-run. - tts_produce.py: --allow-default-voice (voz Nico solo si se permite explícitamente para autores sin voz clonada) + fix voice_for_author. - minimax_tts.py: parámetro speed en t2a/_synth_chunk. - mirror-antiguo/deploy/nginx-mirror.conf: redirects de URLs históricas de catálogo y vista imprimible EFFA. - Importaciones humanas de Pagola (carta 738) + scripts/manifest de la fase A Enrique, y release_issue181_prod.sh / cdmon-retire-fewp1.sh. - .gitignore: excluye docs/backups/ (dumps SQL, mismo motivo que backups/*). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mirror-antiguo — mirror estático del Joomla legacy de feadulta
Construcción del mirror HTML read-only de antiguo.feadulta.com (issue
rafa/feadulta#180, remediación de #183).
Regla de diseño: el mirror se genera solo por HTTP, nunca copiando el filesystem. Así es
imposible arrastrar los .php comprometidos del incidente. Lo que se captura es lo que Joomla
renderiza.
Origen del crawl
No es producción: es el Joomla legacy restaurado en local.
| Contenedor | joomla-mirror-web (php:7.4-apache), red joomla-migration_joomla-net, IP 172.20.0.5 |
| Acceso | http://127.0.0.1:8086 o http://antiguo.feadulta.com (entrada en /etc/hosts → 172.20.0.5) |
| BD | joomla_mirror dentro del contenedor joomla-mysql |
| Snapshot fuente | source/antiguo-20260729.tar.gz + source/fejoomla3-20260729.sql.gz, hashes en MANIFEST-source.sha256 |
⚠️ La imagen base necesitó mysqli, pdo_mysql, gd, zip y mod_rewrite instalados en caliente:
se pierden si el contenedor se recrea, no si solo se para/arranca. Para cambiar memoria o política
de reinicio usar docker update, nunca docker rm + docker run.
⚠️ WSL2 se apaga cuando no queda ninguna sesión abierta desde Windows, y al apagarse para los contenedores. Antes de un proceso de horas, dejar un proceso ancla vivo en WSL.
Datos del sitio
- SEF:
sef=1,sef_rewrite=1,sef_suffix=1→ las URLs terminan en.html. - Un solo idioma de contenido publicado:
es-ES(sefes); todo el contenido eslanguage='*'. El sitio hace 301 de/x→/es/x, así que el inventario se genera ya con/es/. - Solo 2 categorías K2 y un único ítem de menú de K2:
buscadoravanzado(Itemid 138). Las URLs de ítem son/es/buscadoravanzado/item/<id>-<alias>.html. - 16.269 ítems K2 publicados · 9.079 artículos com_content · 216 ítems de menú · 68 categorías.
Método
El inventario no se reconstruye a mano: lo genera el propio router de Joomla. _genurls.php
(en la raíz del sitio restaurado) arranca el framework por HTTP y llama a JRoute::_() y
K2HelperRoute::getItemRoute(), así que las URLs son idénticas a las que el sitio imprime.
Cuatro pases, todos acotados por listas — nunca por recursión libre (ver post-mortem del 2026-07-29 en #180 comment-497):
| Pase | Qué captura | Script |
|---|---|---|
| A | Las 25.437 URLs del inventario | 21-crawl-html.sh |
| B | Recursos (css/js/img/mp3/pdf) referenciados por el HTML capturado | 30-extract-links.py + 31-fetch-assets.sh |
| C | Huecos: rutas de menú alternativas, páginas de autor de K2, /anterior, /ediciones |
43-pendientes.sh |
| D | /anterior, la web estática anterior a Joomla (HTML plano, recursión finita) |
37-pase-d-anterior.sh |
raw/ es inmutable. site/ es el derivado servible (45-normalize-links.py).
Trampas encontradas (y cómo se resuelven)
- Paginación de K2: los enlaces acumulan
&start=en vez de reemplazarlo → espacio de URLs infinito. Es lo que tumbó la VM el 29-jul. Se evita capturando por inventario, y el--reject-regexincluyestart|limitstart|limit|print|tmpl|format|searchword|task|orderby|filter. - Alias con
?literal: ~198 artículos tienen el signo de interrogación dentro del alias (...-dios-nos-ama?.html). Para cualquier cliente HTTP eso es el separador de query, así que la ruta real es la parte anterior al?y wget guarda un fichero sin extensión. Es correcto para servirlo estáticamente, pero nginx necesitadefault_type text/htmlen esa ubicación o el navegador se lo descargará en vez de mostrarlo. - Assets con cache-busting (
core.js?a32fb…): wget mete la query en el nombre del fichero. El paso de normalización deja también una copia con el nombre limpio, que es la que pedirá el servidor estático. - Menús cuyo componente ya no existe (
com_surveys,com_breezingforms):JRouteno les construye ruta y devuelven 404. Ya son 404 en producción; para el resto de ítems de menú la ruta autoritativa es la columnapathde#__menu(41-menu-paths.sh).
Scripts
Numerados por orden de ejecución en scripts/. 23-estado.sh da el estado en cualquier momento;
22-monitor.sh vigila el crawl y lo aborta si el servidor pasa de 100 respuestas 500 o si la RAM
libre baja de 800 MB. 60-restaurar-entorno.sh rearranca los contenedores parados durante el crawl.