Compare commits

...

2 Commits

Author SHA1 Message Date
rafa bda4a753c9 Report TTS WordPress read failures instead of false zero backlog 2026-08-28 04:20:15 -04:00
rafa de91485854 sync audio: el recuento final contaba las subidas buenas como saltadas
sync_one devuelve "ok prod#<id>" desde que el id de prod puede diferir del
local, pero el contador seguia comparando con la igualdad exacta "ok". Resultado:
el 8-ago la subida de los 90 audios de Fray Marcos, sin un solo fallo, cerro con
"ok=0 skip=90". El susto es lo de menos; lo malo es que un fallo real se camufla
en ese ruido.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:57:04 -04:00
2 changed files with 59 additions and 30 deletions
+52 -29
View File
@@ -39,18 +39,21 @@ AUTORES = {
386: ("Arregi", "ArregiFeadulta2026"), 386: ("Arregi", "ArregiFeadulta2026"),
} }
# Autor y rango que está locutando el cron ahora mismo (los valores por defecto # Autor y rango que está locutando el cron ahora mismo. Debe seguir el crontab:
# de tts_backlog_cron.sh). Solo afecta a qué línea se marca como "en curso". # solo afecta a qué línea se marca como «en curso» en el informe diario.
ACTIVO = (382, 2025, 2026) ACTIVO = (774, 2000, 2026)
def php(*args: str) -> str: def php(*args: str) -> str:
"""fea_post_io.php dentro del contenedor. stderr fuera: WP escupe warnings.""" """Llama al helper WP sin convertir un fallo de lectura en una cola vacía."""
r = subprocess.run( r = subprocess.run(
["docker", "exec", CONTAINER, "php", "/tmp/fea_post_io.php", *args], ["docker", "exec", CONTAINER, "php", "/tmp/fea_post_io.php", *args],
capture_output=True, text=True, capture_output=True, text=True,
) )
return r.stdout if r.returncode == 0 else "" if r.returncode != 0:
detail = (r.stderr or r.stdout).strip().replace("\n", " ")[:300]
raise RuntimeError(f"consulta WP {' '.join(args)} falló (rc={r.returncode}): {detail}")
return r.stdout
def pendientes(autor: int, desde: int, hasta: int, voz: str) -> int: def pendientes(autor: int, desde: int, hasta: int, voz: str) -> int:
@@ -143,41 +146,62 @@ def _campo_cron(campo: str, valores: range) -> set[int]:
def previstas_24h(ahora: datetime | None = None) -> int | None: def previstas_24h(ahora: datetime | None = None) -> int | None:
"""Cuántas ventanas TENÍA que haber corrido el cron en las últimas 24 h. """Cuántas ventanas tenían que haber corrido en las últimas 24 horas.
Sin esto, los martes/miércoles/jueves (días de carta, sin cron) el informe El backlog usa varias líneas de crontab (ritmo normal + sábado + domingo).
daba la alarma de 'ninguna ventana dejó rastro' estando todo correcto. La Se calcula la unión de sus instantes previstos, sin contar dos veces una
verdad está en el crontab, no aquí: si Rafa cambia los días, esto le sigue. coincidencia entre líneas.
""" """
try: try:
linea = next( lineas = [
l for l in subprocess.run(["crontab", "-l"], text=True, capture_output=True, l for l in subprocess.run(["crontab", "-l"], text=True, capture_output=True,
check=True).stdout.splitlines() check=True).stdout.splitlines()
if "tts_backlog_cron.sh" in l and not l.lstrip().startswith("#")) if "tts_backlog_cron.sh" in l and not l.lstrip().startswith("#")
]
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
return None return None
campos = linea.split(None, 5) if not lineas:
if len(campos) < 5:
return None return None
minutos = _campo_cron(campos[0], range(60))
horas = _campo_cron(campos[1], range(24))
dows = {d % 7 for d in _campo_cron(campos[4], range(7))} # cron: 0 y 7 = domingo
ahora = ahora or datetime.now() ahora = ahora or datetime.now()
n = 0 inicio = ahora - timedelta(hours=24)
for h in range(25): previstos: set[datetime] = set()
t = (ahora - timedelta(hours=h)).replace(second=0, microsecond=0) for linea in lineas:
for m in minutos: campos = linea.split(None, 5)
cand = t.replace(minute=m) if len(campos) < 5:
if not (ahora - timedelta(hours=24) < cand <= ahora): continue
continue try:
if cand.hour in horas and (cand.weekday() + 1) % 7 in dows: minutos = _campo_cron(campos[0], range(60))
n += 1 horas = _campo_cron(campos[1], range(24))
return n dows = {d % 7 for d in _campo_cron(campos[4], range(7))}
except (TypeError, ValueError):
continue
for h in range(25):
t = (ahora - timedelta(hours=h)).replace(second=0, microsecond=0)
for m in minutos:
cand = t.replace(minute=m)
if inicio < cand <= ahora and cand.hour in horas and (cand.weekday() + 1) % 7 in dows:
previstos.add(cand)
return len(previstos)
def main() -> int: def main() -> int:
hechos = hechos_24h() # Nunca informar «0 pendientes» cuando la lectura de WordPress ha fallado.
# El cron no-agent entrega stdout/errores tal cual: un aviso explícito permite
# arreglar Docker/helper sin que parezca que el backlog está terminado.
try:
hechos = hechos_24h()
faltantes = {
aid: (pendientes(aid, 0, 9999, voz),
pendientes(aid, ACTIVO[1], ACTIVO[2], voz) if aid == ACTIVO[0] else None)
for aid, (_nombre, voz) in AUTORES.items()
}
except RuntimeError as exc:
print("⚠️ Fe Adulta — informe TTS inválido: no se pudo leer WordPress.")
print(f" {exc}")
print(" Los ceros no son datos reales; revisar wordpress-web y /tmp/fea_post_io.php.")
return 1
total = sum(len(v) for v in hechos.values()) total = sum(len(v) for v in hechos.values())
corridas, saltadas, fallos = logs_24h() corridas, saltadas, fallos = logs_24h()
p5, pw = cuota() p5, pw = cuota()
@@ -194,10 +218,9 @@ def main() -> int:
lineas.append("") lineas.append("")
lineas.append("Pendientes:") lineas.append("Pendientes:")
for aid, (nombre, voz) in AUTORES.items(): for aid, (nombre, voz) in AUTORES.items():
falta_todo = pendientes(aid, 0, 9999, voz) falta_todo, falta_lote = faltantes[aid]
marca = "" marca = ""
if aid == ACTIVO[0]: if aid == ACTIVO[0]:
falta_lote = pendientes(aid, ACTIVO[1], ACTIVO[2], voz)
marca = f" ← en curso, {falta_lote} del lote {ACTIVO[1]}-{ACTIVO[2]}" marca = f" ← en curso, {falta_lote} del lote {ACTIVO[1]}-{ACTIVO[2]}"
lineas.append(f" {nombre}: {falta_todo}{marca}") lineas.append(f" {nombre}: {falta_todo}{marca}")
+7 -1
View File
@@ -258,7 +258,13 @@ def main() -> int:
ok += 1 ok += 1
else: else:
res = sync_one(pid, state, dry_run=args.dry_run) res = sync_one(pid, state, dry_run=args.dry_run)
if res == "ok" or res.startswith("PLAN"): # `startswith`, no `==`: sync_one devuelve "ok prod#<id>" desde que
# el id de prod puede diferir del local. Con la igualdad exacta, una
# subida perfecta se contaba entera como saltada — el 8-ago-2026 el
# pie del log dijo "ok=0 skip=90" tras subir los 90 audios de Fray
# Marcos sin un solo fallo. Peor que el susto: así un fallo real se
# confunde con este ruido y pasa desapercibido.
if res.startswith(("ok", "PLAN")):
ok += 1 ok += 1
else: else:
skip += 1 skip += 1