TTS backlog: no dar falsa alarma los dias sin cron (#188)

El informe avisaba de "ninguna ventana dejo rastro en 24 h" los miercoles y
jueves, que son dias de carta y no tienen cron por diseno. Ahora lee el crontab,
cuenta cuantas ventanas TENIAN que correr y solo avisa si de verdad tocaba.
Si Rafa cambia los dias del cron, el informe le sigue solo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 22:10:15 -04:00
parent e4d2073eeb
commit 159080f0c8
+62 -2
View File
@@ -121,6 +121,61 @@ def logs_24h() -> tuple[int, int, list[str]]:
return corridas, saltadas, fallos
def _campo_cron(campo: str, valores: range) -> set[int]:
"""Expande un campo de crontab ('*', '*/5', '1,5,6,0', '0-4') a un set."""
if campo == "*":
return set(valores)
out: set[int] = set()
for trozo in campo.split(","):
paso = 1
if "/" in trozo:
trozo, p = trozo.split("/", 1)
paso = int(p)
if trozo == "*":
base = list(valores)
elif "-" in trozo:
a, b = (int(x) for x in trozo.split("-", 1))
base = list(range(a, b + 1))
else:
base = [int(trozo)]
out.update(base[::paso] if paso > 1 else base)
return out
def previstas_24h(ahora: datetime | None = None) -> int | None:
"""Cuántas ventanas TENÍA que haber corrido el cron en las últimas 24 h.
Sin esto, los martes/miércoles/jueves (días de carta, sin cron) el informe
daba la alarma de 'ninguna ventana dejó rastro' estando todo correcto. La
verdad está en el crontab, no aquí: si Rafa cambia los días, esto le sigue.
"""
try:
linea = next(
l for l in subprocess.run(["crontab", "-l"], text=True, capture_output=True,
check=True).stdout.splitlines()
if "tts_backlog_cron.sh" in l and not l.lstrip().startswith("#"))
except Exception: # noqa: BLE001
return None
campos = linea.split(None, 5)
if len(campos) < 5:
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()
n = 0
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 not (ahora - timedelta(hours=24) < cand <= ahora):
continue
if cand.hour in horas and (cand.weekday() + 1) % 7 in dows:
n += 1
return n
def main() -> int:
hechos = hechos_24h()
total = sum(len(v) for v in hechos.values())
@@ -151,11 +206,16 @@ def main() -> int:
lineas.append("Cuota MiniMax: no se pudo leer")
else:
lineas.append(f"Cuota MiniMax: 5h {p5:.0f}% · semana {pw:.0f}%")
lineas.append(f"Ventanas 24 h: {corridas} ejecutadas, {saltadas} saltadas por cuota")
previstas = previstas_24h()
de = "" if previstas is None else f" de {previstas} previstas"
lineas.append(f"Ventanas 24 h: {corridas} ejecutadas{de}, {saltadas} saltadas por cuota")
if previstas == 0:
lineas.append(" (sin ventanas previstas: día sin cron, toca carta)")
# Una ventana que ni arranca ni se salta es un fallo mudo: el cron no llegó a
# correr (bit +x, WSL apagada...). Es exactamente lo que pasó el 2-ago.
if corridas == 0 and saltadas == 0:
# Solo es alarma si de verdad tocaba correr; si no, es martes.
if corridas == 0 and saltadas == 0 and previstas != 0:
lineas.append("")
lineas.append("⚠️ Ninguna ventana dejó rastro en 24 h. Si tocaba que corriera, "
f"comprobar: crontab -l | grep tts_backlog · ls -l {CRON}")