Wiki: producción Hetzner, trampas del entorno y mapa del conocimiento heredado

Tres páginas nuevas escritas por Opix, verificadas contra el servidor el
2026-08-12 en solo lectura:

- Produccion-Hetzner-acceso: clave SSH correcta (id_ed25519, no
  feadulta_ed25519), contenedor de Coolify, y el hecho de que no hay WP-CLI
  (se usa php -r con wp-load.php).
- Trampas-del-entorno: las cuatro limitaciones del jail de CDMON que ya no
  aplican, más las que siguen vigentes, marcando qué está verificado.
- Conocimiento-heredado: mapa de las skills de Hermes, la memoria de Claude
  Code y los scripts, con su estado de vigencia.

Home actualizado con aviso de vigencia y enlaces al Gitea vivo (apuntaba a
localhost:3000). Ninguna credencial en el contenido.
2026-08-12 11:13:21 -04:00
parent ca83b3e11f
commit a3611a3245
4 changed files with 339 additions and 10 deletions
+99
@@ -0,0 +1,99 @@
# Conocimiento heredado — dónde está todo
> Levantado por Opix el **2026-08-12**. Mapa de lo que Hermes sabía hacer como
> webmaster de feadulta, con el estado de vigencia de cada fuente.
**Ninguna de estas fuentes es autoritativa por sí sola.** Varias describen el
servidor cPanel/CDMON, retirado en el cutover del 2026-08-03. Verifica contra la
máquina antes de operar. Ver [[Produccion-Hetzner-acceso]] y [[Trampas-del-entorno]].
## 1. Skills de Hermes — `~/.hermes/skills/` (WSL)
Lo más denso y lo más cercano a runbook. ~2.200 líneas.
### `feadulta/feadulta-webmaster/` — la principal (192 líneas + 10 referencias)
Principio operativo: **diagnóstico + preparación + dry-run**; no tocar producción sin
instrucción explícita. Cubre:
- Endpoints REST acotados para operaciones editoriales (avatar de autor).
- Hotfix de contenido con TTS accidental: despublicación reversible del audio.
- Cron diario de feedback `fea_feedback` y su diagnóstico.
- Triage y contención de incidentes de seguridad (backdoors en mu-plugins).
- Retirada segura de bases de datos.
- Releases de contenido con carta ya publicada: serialización con lock, snapshot del
cluster Polylang completo, doble verificador draft/publish, rollback.
Referencias: `translation-hotfixes`, `wordpress-compromise-first-response`,
`local-docx-carta-preview`, `feedback-beta`, `mysql-three-connection-release-gate`,
`rest-editorial-endpoints`, `guarded-release-handoff`,
`release-serialization-and-verification`, `access-log-forensics`,
`guarded-production-content-releases`, `production-mu-plugin-deploy`,
`feedback-cron-dedup`, `wordpress-author-avatar-api`.
### `feadulta/webmaster/` — skill anterior (71 líneas + 3 referencias)
Más concreta y más obsoleta. `references/gotchas.md` y `references/procedures.md`
siguen siendo buenos resúmenes; `references/environment.md` describe el prod
**anterior**. Su contenido vigente está volcado en [[Trampas-del-entorno]].
### `feadulta/feadulta-carta-derivados/` (477 líneas + 7 referencias)
Traducción + TTS + rotación de la carta semanal. **La skill tiene desviaciones
respecto al código** (argparse de `tts_produce.py`, flags `--allow-default-voice` y
`--remap-conflicts` que no existen). Usar la versión contrastada, no la skill.
### `feadulta/feadulta-editorial-workflows/` (100 líneas + 4 referencias)
Coordinación semanal con Inma/Mixbot, DOCX omitidos, evangelio diario A/B/C.
### `feadulta-analytics/` (117 líneas + 1 referencia)
GA4: informes y renovación de OAuth. Scripts en `~/.hermes/scripts/feadulta_ga4_*.py`.
### Fuera de feadulta pero aplicables
`devops/self-hosted-service-exposure/`, `devops/caddy-routing-recipes/`,
`devops/self-hosted-saas-operations/`.
## 2. Wiki del servidor — `rafa/server`
4 páginas. `Backups` es **la mejor escrita y la más actual** de las dos wikis:
diseño pull en dos capas, `~/backup-hetzner/pull-hetzner.sh`, verificación real de
restauración del 2026-08-01, y trampas de NTFS/rsync/Coolify.
## 3. Memoria de Claude Code (WSL)
`~/.claude/projects/-mnt-c-Users-Chia/memory/` — 88 ficheros, índice en `MEMORY.md`.
| Fichero | Contenido |
|---|---|
| `master-feadulta.md` (20 KB) | Arquitectura WP, term_ids, Polylang, carta, EFFA, avatares, scripts delta |
| `master-infra.md` (17 KB) | Hardware, Tailscale, Docker, LLMs locales, Hetzner, APIs |
| `master-gitea.md` (8,7 KB) | Setup, quirks de la wiki, convenciones, comandos API |
| `feadulta-migracion-cdmon-hetzner-202607.md` (39 KB) | **El cutover del 2026-08-03** — fuente real del estado actual de prod |
Y ~35 ficheros `feadulta-*.md` de incidentes y proyectos concretos.
## 4. Scripts y perfiles
- `~/.hermes/scripts/``fea_feedback_last24h.py`, `fea_tts_backlog_report.py`,
`feadulta_ga4_daily.py`, `feadulta_ga4_weekly.py`, `cron_scan.py`.
- `~/.hermes/profiles/feadulta/.env` — credenciales de producción.
- `~/.env` (permisos 600) — tokens de Gitea.
- `~/backup-hetzner/` — pull de backups + alerta Telegram + systemd.
- `~/.hermes/SOUL.md` — personalidad general de Hermes ("Luigi"). Sin contenido
operativo de feadulta; no hace falta migrarlo.
## 5. Política de secretos
**Ninguna credencial en esta wiki.** Solo punteros a los `.env` y a `~/.ssh/`.
Decisión de Rafa, 2026-08-12.
⚠️ La página [[Credenciales-y-accesos]] (junio 2026) **contiene contraseñas en
claro** y contradice esta política. Pendiente de decidir su limpieza — ten en cuenta
que borrarlas del texto no las quita del historial git de la wiki.
## 6. Estado de la migración de este conocimiento
| Bloque | Estado |
|---|---|
| Acceso a producción | ✅ [[Produccion-Hetzner-acceso]] |
| Trampas del entorno | ✅ [[Trampas-del-entorno]] |
| Ciclo de la carta (traducción + TTS) | ✅ contrastado con el código, fuera de la wiki |
| Áreas funcionales (portada, Polylang, autores, EFFA, buscador) | ⏳ en la wiki desde junio, sin re-verificar contra el código |
| Seguridad e incidentes | ⏳ sigue solo en la skill de Hermes |
| Despliegue de mu-plugins, backups y rollback | ⏳ pendiente de consolidar |
+25 -10
@@ -1,22 +1,37 @@
> **Este es el repo activo** — migrado desde el Gitea local de Tailscale el 2026-06-28. > **Este es el repo activo** — migrado desde el Gitea local de Tailscale el 2026-06-28.
> Servidor: **gitea.feadulta.com** (Hetzner FSN1) > Servidor: **gitea.feadulta.com** (Hetzner FSN1)
# feadulta.org — Wiki del proyecto # feadulta.com — Wiki del proyecto
Portal cristiano español de renovación de la fe. Documentación operativa del proyecto de migración Joomla → WordPress y de su operación. Portal cristiano español de renovación de la fe. Documentación operativa del proyecto
de migración Joomla → WordPress y de su operación.
**Estado:** migración completa, web protegida con HTTP Basic, cutover DNS previsto ~junio 2026. **Estado:** migración completa. Cutover Joomla→WordPress cerrado el 2026-07-07/08 y
cutover de servidor CDMON→Hetzner cerrado el 2026-08-03.
> ⚠️ **Aviso de vigencia.** Las páginas de operación y arquitectura se escribieron en
> junio de 2026, antes del cambio de servidor. Donde describan cPanel/CDMON
> (`134.0.10.170`, `/web/wp-nuevo`, `wp-nuevo.feadulta.com`) o el Gitea en
> `localhost:3000`, están **obsoletas**. Empieza por
> [[Produccion-Hetzner-acceso]] y [[Trampas-del-entorno]].
>
> **Ninguna credencial debe vivir en esta wiki** (decisión de Rafa, 2026-08-12).
--- ---
## Índice ## Índice
### Operación ### Operación — empieza por aquí
- [Credenciales y accesos](Credenciales-y-accesos) - [Producción Hetzner: acceso y ejecución](Produccion-Hetzner-acceso) ✅ *verificado 2026-08-12*
- [Infraestructura](Infraestructura) - [Trampas del entorno](Trampas-del-entorno) ✅ *verificado 2026-08-12*
- [Limitaciones del servidor de producción](Limitaciones-servidor-prod) - [Conocimiento heredado: dónde está todo](Conocimiento-heredado)
- [Sincronización local → producción](Sincronizacion-local-prod) - [Sincronización local → producción](Sincronizacion-local-prod)
### Operación — anterior al cutover de servidor ⚠️
- [Infraestructura](Infraestructura) — la sección de producción describe CDMON
- [Limitaciones del servidor de producción](Limitaciones-servidor-prod) — **ya no aplican**
- [Credenciales y accesos](Credenciales-y-accesos) — ⚠️ contiene contraseñas, pendiente de limpiar
### Arquitectura WordPress ### Arquitectura WordPress
- [Arquitectura WordPress](Arquitectura-WordPress) - [Arquitectura WordPress](Arquitectura-WordPress)
- [Portada y shortcodes](Portada-y-shortcodes) - [Portada y shortcodes](Portada-y-shortcodes)
@@ -45,7 +60,7 @@ Portal cristiano español de renovación de la fe. Documentación operativa del
## Repo ## Repo
- **Código:** http://localhost:3000/rafa/feadulta · https://farmer.taild3aaf6.ts.net/git/rafa/feadulta - **Código:** https://gitea.feadulta.com/rafa/feadulta
- **Issues:** https://gitea.feadulta.com/rafa/feadulta/issues
- **Directorio local:** `/home/rafa/joomla-migration/` - **Directorio local:** `/home/rafa/joomla-migration/`
- **Issues:** http://localhost:3000/rafa/feadulta/issues - **Wiki del servidor:** https://gitea.feadulta.com/rafa/server/wiki
- **Milestones:** `v1: Pre-cutover` · `v2: Post-launch`
+105
@@ -0,0 +1,105 @@
# Producción (Hetzner + Coolify) — acceso y ejecución
> Verificado contra el servidor el **2026-08-12** en solo lectura. — Opix
>
> Esta página **sustituye** a lo que dicen [[Infraestructura]] y
> [[Limitaciones-servidor-prod]] sobre producción: ambas describen el servidor
> anterior (cPanel/CDMON, `134.0.10.170`, `/web/wp-nuevo`), retirado en el cutover
> a Hetzner del **2026-08-03**.
## SSH
```bash
ssh -i ~/.ssh/id_ed25519 rafa@188.40.120.157
# también por Tailscale:
ssh rafa@srv1.taild3aaf6.ts.net
```
⚠️ **La clave es `~/.ssh/id_ed25519`** (comentario `claude-code@feadulta`).
La otra clave del directorio, `~/.ssh/feadulta_ed25519`
(`claude-code@wsl-feadulta-deploy`, mayo 2026), **da `Permission denied`**: es de la
etapa CDMON. Es un error fácil de cometer porque el nombre parece el correcto.
`FEA_PROD_SSH_PASS` en el perfil de Hermes está **vacío a propósito**: la
autenticación es por clave. Los scripts `sync_*_to_prod.py` caen a `ssh` normal
(sin `sshpass`) cuando esa variable está vacía. No es un valor que falte por rellenar.
## El contenedor de WordPress
| | |
|---|---|
| Contenedor | `wordpress-r2ssjifwj0r0ghyoqd528uaa` |
| Imagen | `wordpress:latest` — WordPress **7.0.3** |
| PHP | 8.3.31 (CLI) en `/usr/local/bin/php` |
| Docroot | `/var/www/html` (volumen persistente `r2ssjifwj0r0ghyoqd528uaa_wordpress-files`) |
| BD | contenedor `mysql-r2ssjifwj0r0ghyoqd528uaa` (MySQL 8) |
| Servicio Coolify | `feadulta-wp`, uuid `r2ssjifwj0r0ghyoqd528uaa` |
En el mismo host conviven **tres WordPress** (feadulta, summaraise, aqtalent), el
relay de Buzz, el CRM Relaticle, Beszel y el mirror estático de Joomla. Comprueba
siempre el nombre completo del contenedor antes de un `docker exec`.
## ⚠️ No hay WP-CLI en el contenedor
`command -v wp` → nada. La imagen oficial `wordpress:latest` no lo incluye; no es un
fallo de configuración ni algo que se haya perdido.
**La vía correcta es `php -r` cargando `wp-load.php`** — es lo que describe
`FEA_PROD_WPLOAD` del perfil de Hermes.
```bash
ssh -i ~/.ssh/id_ed25519 rafa@188.40.120.157 \
"docker exec wordpress-r2ssjifwj0r0ghyoqd528uaa php -r \
'require \"/var/www/html/wp-load.php\"; echo home_url(),\"|\",get_bloginfo(\"version\"),\"|\",wp_count_posts()->publish;'"
```
Salida real del 2026-08-12:
```
https://www.feadulta.com|7.0.3|29435
```
### Scripts largos
```bash
scp -i ~/.ssh/id_ed25519 script.php rafa@188.40.120.157:/tmp/script.php
ssh -i ~/.ssh/id_ed25519 rafa@188.40.120.157 \
'docker cp /tmp/script.php wordpress-r2ssjifwj0r0ghyoqd528uaa:/tmp/script.php && \
docker exec wordpress-r2ssjifwj0r0ghyoqd528uaa php /tmp/script.php'
```
⚠️ Para copiar **directorios**, usa `docker cp origen/. <c>:/destino/` con el `/.`
final. Sin él, si el destino ya existe, Docker crea `destino/origen` anidado — y
WordPress no carga mu-plugins desde subdirectorios, así que el despliegue queda
silenciosamente inactivo. Ver [[Trampas-del-entorno]].
## Verificación: Cloudflare sigue delante
`curl -sI https://www.feadulta.com` devuelve **403** con `cf-mitigated: challenge`,
incluso desde el propio servidor. No se puede certificar nada con curl, Playwright,
headless ni `wp_remote_get`. La verificación de producción es **server-side**
(`docker exec … php -r`) o navegación humana real.
Desde dentro del servidor, `curl -H 'Host: www.feadulta.com' http://127.0.0.1/`
sí llega al origen (302), saltándose Cloudflare.
## Credenciales
**No están en esta wiki y no deben estarlo.** Viven en:
- `~/.hermes/profiles/feadulta/.env` (WSL) — host SSH, contenedor, wp-load.
- `~/.env` (WSL, permisos 600) — tokens de Gitea.
- `~/.ssh/id_ed25519` — clave de acceso al servidor.
## Local, para contraste
```bash
docker exec wordpress-web php -r 'require "/var/www/html/wp-load.php"; echo home_url();'
docker cp /tmp/x.php wordpress-web:/tmp/x.php && \
docker exec wordpress-web wp eval-file /tmp/x.php --allow-root --path=/var/www/html
```
El local **sí** tiene wp-cli pero **no** cliente `mysql`: `wp db query` falla, hay que
usar `wp eval-file`. QA visual siempre contra `https://farmer.taild3aaf6.ts.net/fea/`,
que es el `siteurl` real — `localhost:8081` sirve desde la raíz y las `@font-face`
caen por CORS.
+110
@@ -0,0 +1,110 @@
# Trampas del entorno
> Consolidado por Opix el **2026-08-12** desde las skills de Hermes, filtrado contra
> el estado real del servidor tras el cutover a Hetzner del 2026-08-03.
>
> Lo marcado **[verificado]** se comprobó contra la máquina ese día. El resto es
> documentación heredada **sin re-verificar**: trátalo como pista, no como hecho.
## Ya NO aplican — eran del jail de CDMON [verificado]
Estas cuatro condicionaban buena parte de los procedimientos antiguos y **hoy son
falsas**. Si las lees en [[Limitaciones-servidor-prod]] o en las skills de Hermes,
están obsoletas.
| Trampa vieja | Realidad en Hetzner |
|---|---|
| `proc_open()` deshabilitado | `disable_functions` vacío, funciona |
| No hay `mysqldump` ni `stat` | `stat` en el contenedor WP; `mysqldump 8.4.10` en el de MySQL |
| `php` y `grep` rotos (GLIBC) | PHP 8.3.31 y `grep` correctos |
| `scp`/`sftp` no funcionan | `scp` y `docker cp` normales |
Importa más de lo que parece: varios procedimientos daban rodeos (subir ficheros con
`ssh 'cat > ruta'`, backups selectivos post a post, usar `wp` para todo porque `php`
estaba roto) que hoy son innecesarios. Copiarlos tal cual hereda los rodeos.
Lo que **sí** falta hoy dentro del contenedor de WordPress: `wp`, `mysql`,
`mysqldump`, `git`, `python3`. Sí hay: `php`, `curl`, `grep`, `stat`, `tar`.
## Siguen vigentes
### Cloudflare bloquea curl y headless [verificado]
403 con `cf-mitigated: challenge`, también desde el propio servidor. Verificación
de prod siempre server-side. Ver [[Produccion-Hetzner-acceso]].
### `docker cp` anida directorios en silencio [verificado]
Si el destino ya existe como directorio, `docker cp origen <c>:/ruta/destino` crea
`destino/origen`. Pasó desplegando mu-plugins el 2026-08-12: acabaron en
`wp-content/mu-plugins/mu-plugins/` y el lint posterior los dio por desaparecidos,
con la conclusión equivocada de que el volumen no persistía.
Dos consecuencias:
1. **WordPress no carga mu-plugins desde subdirectorios** — solo los `.php` del
primer nivel. Un despliegue anidado no falla: queda inactivo sin avisar.
2. Antes de concluir "no persiste", mira el mount real:
```bash
docker inspect --format '{{range .Mounts}}{{.Type}} {{.Destination}} <- {{.Source}}{{println}}{{end}}' <container>
docker inspect --format 'Created={{.Created}} StartedAt={{.State.StartedAt}} RestartCount={{.RestartCount}}' <container>
```
En feadulta, `/var/www/html` es el volumen `r2ssjifwj0r0ghyoqd528uaa_wordpress-files`
y el contenedor no se recrea desde el 2026-08-03 (`RestartCount=0`).
Usa `docker cp origen/. <c>:/destino/` para volcar contenido sin anidar.
### hub-proxy nunca devuelve 404
El nginx del hub responde la portada del Hub con **200 + text/html (20.597 bytes)**
para cualquier ruta desconocida. Comprueba `content_type` y tamaño, no solo el código.
### Charset: el CLI de MySQL conecta en latin1
Columnas en utf8mb4. Siempre `mysql --default-character-set=utf8mb4`, o corrompe acentos.
### La categoría de la carta está partida en tres
6 `cartasemana` → 22 `carta-semana-pasada` → 21 `cartas-de-otras-semanas`.
Buscar siempre en las tres. Ver [[Carta-de-la-semana]].
### Heredadas, sin re-verificar
- **wp-cli y categorías**: `wp post term set <ID> category 28` asigna el término *de
nombre* "28", no el term_id. Usar el slug.
- **WP_Tax_Query + Polylang**: `get_posts(['lang'=>$x,'category__in'=>[$c]])` genera
`AND 0=1`. No pasar `lang` explícito. `get_term_by('slug',…)` en idioma ≠ ES
devuelve `false`: usar `$wpdb->get_var()`.
- **Títulos en MAYÚSCULAS** en la BD (legacy Joomla); `fea_title()` los normaliza en
el front. Es intencional.
- **Single posts usan template FSE** (Twenty Twenty-Five, post 42359), no Astra.
- **Páginas desde CLI**: HTML plano con shortcodes, nunca bloques Gutenberg.
- **URLs entre entornos**: local cuelga de `/fea`, prod de la raíz. No copiar
`post_content` sin reescribir.
- **IDs**: los de posts ES coinciden local↔prod; los de attachments y user_meta **no**.
## Trampas de herramienta (agentes en WSL)
### `wsl -d Ubuntu -- bash -lc '…'` rompe bucles y rutas
Las variables se pierden, los `for … in *.md` se comen los argumentos y las rutas
`/tmp/x.sh` se traducen a rutas de Windows. Escribe el `.sh` por UNC
(`\\wsl.localhost\Ubuntu\tmp\…`) y lánzalo con `bash -lc 'cd /tmp; bash x.sh'`.
### `wp eval-file` y las globales
Dentro de una función, `global $X` **no** ve las variables del script: pásalas con `use`.
### Regex en PHP sobre texto español: siempre `/u`
`m[áa]s` o `eucarist[ií]as` **sin** el modificador `u` no casan: las vocales
acentuadas ocupan 2 bytes y la clase de caracteres parte el UTF-8. Costó un
diagnóstico entero equivocado el 2026-08-12.
### La API de wiki de Gitea
Devuelve `content_base64`, no `content`, y da **404 con títulos acentuados** aunque
el `sub_url` sea el que ella misma publica. Clona `<repo>.wiki.git` en su lugar.
(La wiki vive en el branch `main`; documentación antigua dice `master`.)
## Trampas de método
- **Contar en la BD antes de proponer un comando que muta.** No extrapolar el alcance
de un fix desde una página renderizada.
- **Seguir el camino completo del dato** antes de gritar "fuga a producción".
- **"El dato no existe" solo vale tras seguir los enlaces**, no solo los metas.
- **Antes de agrupar por un meta de origen, comprobar que no haya varios**: conviven
`_fgj2wp_old_k2_id` (K2, 18.121) y `_fgj2wp_old_id` (Joomla core, 9.022).
- **Verificar las dos mitades de una rotación**: que la carta nueva entre *y* que la
anterior salga.
- **Un issue no es un diario.** Si te retractas, se reescribe entero; el rastro va al
canal, no al ticket.