Apunta los scripts de sync prod al Hetzner nuevo y suma trabajo pendiente
- 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>
This commit is contained in:
@@ -16,8 +16,10 @@ coincidencia local↔prod cuando prod va por detrás.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -29,12 +31,20 @@ DB_NAME = os.environ.get("FEA_DB_NAME", "wordpress_db")
|
||||
DB_USER = os.environ.get("FEA_DB_USER", "wordpress_user")
|
||||
DB_PASS = os.environ.get("FEA_DB_PASS", "wordpress_pass")
|
||||
|
||||
PROD_HOST = os.environ.get("FEA_PROD_HOST", "feadulta@134.0.10.170")
|
||||
PROD_PASS = os.environ.get("FEA_PROD_PASS", "C6c2A!mAl3Wj.BQF")
|
||||
PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/web/wp-load.php")
|
||||
PROD_HELPER = "/tmp/fea_translate_helper.php"
|
||||
PROD_HOST = os.environ.get("FEA_PROD_SSH_HOST", "")
|
||||
PROD_PASS = os.environ.get("FEA_PROD_SSH_PASS", "")
|
||||
PROD_WPLOAD = os.environ.get("FEA_PROD_WPLOAD", "/var/www/html/wp-load.php")
|
||||
# Desde el cutover a Hetzner, WordPress vive dentro de Coolify/Docker. Si se
|
||||
# define, el helper se ejecuta en memoria dentro del contenedor; no se escribe
|
||||
# ningún fichero temporal en prod. Espejo de sync_carta_from_prod.py.
|
||||
PROD_DOCKER_CONTAINER = os.environ.get("FEA_PROD_DOCKER_CONTAINER", "")
|
||||
|
||||
HELPER_SRC = Path(__file__).resolve().parent / "fea_translate_helper.php"
|
||||
# El helper se ejecuta en memoria con `php -r` en prod. Así no se copia ningún
|
||||
# fichero a /tmp remoto y stdin queda disponible para el payload JSON.
|
||||
HELPER_EVAL_CODE = "eval(base64_decode(" + repr(
|
||||
base64.b64encode(HELPER_SRC.read_text(encoding="utf-8").removeprefix("<?php").encode("utf-8")).decode("ascii")
|
||||
) + "));"
|
||||
LOCAL_HELPER_DST = "/tmp/fea_translate_helper.php"
|
||||
STATE_FILE = Path(os.environ.get("FEA_SYNC_STATE", "/tmp/feadulta-sync-state.json"))
|
||||
LOG_FILE = Path(os.environ.get("FEA_SYNC_LOG", "/tmp/feadulta-sync.log"))
|
||||
@@ -107,8 +117,20 @@ def local_read_full(post_id: int) -> dict:
|
||||
|
||||
|
||||
def local_translation_pairs() -> list[tuple[int, int]]:
|
||||
q = ("SELECT post_id, meta_value FROM wp_postmeta "
|
||||
"WHERE meta_key='traduccion_origen' ORDER BY CAST(meta_value AS UNSIGNED), post_id;")
|
||||
"""Devuelve (traducción_local, origen_ES_en_prod).
|
||||
|
||||
Normalmente ambos entornos compartían IDs. Para importaciones remapeadas,
|
||||
la carta ES local guarda `fea_prod_source_id`; esa referencia remota tiene
|
||||
prioridad y evita enlazar una traducción con un post distinto de prod.
|
||||
"""
|
||||
q = (
|
||||
"SELECT tr.post_id, COALESCE(NULLIF(src.meta_value,''), tr.meta_value) "
|
||||
"FROM wp_postmeta tr "
|
||||
"LEFT JOIN wp_postmeta src ON src.post_id=CAST(tr.meta_value AS UNSIGNED) "
|
||||
"AND src.meta_key='fea_prod_source_id' "
|
||||
"WHERE tr.meta_key='traduccion_origen' "
|
||||
"ORDER BY CAST(COALESCE(NULLIF(src.meta_value,''), tr.meta_value) AS UNSIGNED), tr.post_id;"
|
||||
)
|
||||
out = sh(["docker", "exec", DB_CONTAINER, "mysql", f"-u{DB_USER}", f"-p{DB_PASS}",
|
||||
DB_NAME, "-N", "-e", q])
|
||||
pairs = []
|
||||
@@ -155,21 +177,31 @@ def collect_related_posts(seed_ids: list[int]) -> tuple[dict[int, dict], list[di
|
||||
|
||||
|
||||
# ── Prod ─────────────────────────────────────────────────────────────────────
|
||||
_prod_ready = False
|
||||
|
||||
|
||||
def _ssh(remote_cmd: str, *, stdin: str | None = None, timeout: int = 120) -> str:
|
||||
cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=20", PROD_HOST, remote_cmd]
|
||||
# El Hetzner nuevo usa auth por clave (ed25519 claude-code@feadulta); sshpass
|
||||
# solo se antepone si hay contraseña configurada (servidor viejo CDMON).
|
||||
if PROD_PASS:
|
||||
cmd = ["sshpass", "-p", PROD_PASS, "ssh", "-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=20", PROD_HOST, remote_cmd]
|
||||
else:
|
||||
cmd = ["ssh", "-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=20", PROD_HOST, remote_cmd]
|
||||
return sh(cmd, stdin=stdin, timeout=timeout)
|
||||
|
||||
|
||||
def prod_helper(subcmd: str, *args: str, stdin: str | None = None) -> str:
|
||||
global _prod_ready
|
||||
if not _prod_ready:
|
||||
_ssh(f"cat > {PROD_HELPER}", stdin=HELPER_SRC.read_text(encoding="utf-8"))
|
||||
_prod_ready = True
|
||||
inner = f"FEA_WP_LOAD={PROD_WPLOAD} php {PROD_HELPER} {subcmd} " + " ".join(args)
|
||||
"""Ejecuta el helper PHP en memoria; no crea ficheros temporales en prod."""
|
||||
argv = " ".join(shlex.quote(x) for x in (subcmd, *args))
|
||||
if PROD_DOCKER_CONTAINER:
|
||||
inner = (
|
||||
f"docker exec -i -e FEA_WP_LOAD={shlex.quote(PROD_WPLOAD)} "
|
||||
f"{shlex.quote(PROD_DOCKER_CONTAINER)} php -r {shlex.quote(HELPER_EVAL_CODE)} {argv}"
|
||||
)
|
||||
else:
|
||||
inner = (
|
||||
f"FEA_WP_LOAD={shlex.quote(PROD_WPLOAD)} "
|
||||
f"php -r {shlex.quote(HELPER_EVAL_CODE)} {argv}"
|
||||
)
|
||||
return _ssh(inner, stdin=stdin, timeout=180)
|
||||
|
||||
|
||||
@@ -256,7 +288,12 @@ def deploy_fixed_ids(seed_ids: list[int], *, keep_existing: set[int], dry_run: b
|
||||
|
||||
|
||||
# ── Main legado ──────────────────────────────────────────────────────────────
|
||||
def legacy_sync(limit: int, origin: int) -> int:
|
||||
def legacy_sync(limit: int, origin: int, *, dry_run: bool = False) -> int:
|
||||
"""Sincroniza por origen ES+idioma, sin reutilizar IDs locales en prod.
|
||||
|
||||
Este es el modo seguro si producción ha avanzado y sus IDs ya pueden
|
||||
colisionar con traducciones creadas en el entorno local.
|
||||
"""
|
||||
state = load_state()
|
||||
pairs = local_translation_pairs()
|
||||
if origin:
|
||||
@@ -280,6 +317,10 @@ def legacy_sync(limit: int, origin: int) -> int:
|
||||
if key in state["done"]:
|
||||
n_skip += 1
|
||||
continue
|
||||
if dry_run:
|
||||
log(f" PLAN {key}: crearía traducción [{lang}] «{t['title'][:45]}»")
|
||||
n_ok += 1
|
||||
continue
|
||||
try:
|
||||
new_id = prod_create(src_origin, lang, t["title"], t["content"])
|
||||
state["done"][key] = new_id
|
||||
@@ -292,8 +333,10 @@ def legacy_sync(limit: int, origin: int) -> int:
|
||||
n_err += 1
|
||||
log(f" {key} ERROR: {exc}")
|
||||
|
||||
save_state(state)
|
||||
log(f"FIN sync legado. nuevos={n_ok} saltados={n_skip} errores={n_err}. Estado: {STATE_FILE}")
|
||||
if not dry_run:
|
||||
save_state(state)
|
||||
mode = "DRY-RUN" if dry_run else "SYNC"
|
||||
log(f"FIN sync legado {mode}. nuevos/plan={n_ok} saltados={n_skip} errores={n_err}. Estado: {STATE_FILE}")
|
||||
log("Recuerda en prod: ejecutar remap_translation_cats.php si alguna quedó sin categoría traducida.")
|
||||
return 0
|
||||
|
||||
@@ -318,7 +361,7 @@ def main() -> int:
|
||||
keep_existing = set(parse_csv_ints(args.keep_existing))
|
||||
return deploy_fixed_ids(seed_ids, keep_existing=keep_existing, dry_run=args.dry_run)
|
||||
|
||||
return legacy_sync(args.limit, args.origin)
|
||||
return legacy_sync(args.limit, args.origin, dry_run=args.dry_run)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user