#!/usr/bin/env python3 # -*- coding: utf-8 -*- """monitor_estado.py — Punto de entrada del Monitor de estado. Modos: ciclo Ejecuta un ciclo completo y termina (a demanda). daemon Bucle en segundo plano cada `intervalo_minutos` (defecto 30). estado Muestra el ultimo resumen y las rutas de los ficheros. login-telegram Autoriza la sesion dedicada de Telegram (una vez). El nucleo vive en el paquete `monitor_estado/`. """ from __future__ import annotations import argparse import sys import time from datetime import datetime from pathlib import Path # Permite ejecutar el script desde cualquier directorio. _SCRIPTS_DIR = Path(__file__).resolve().parent if str(_SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPTS_DIR)) import monitor_estado.config as config # noqa: E402 from monitor_estado import __version__, ciclo, lock # noqa: E402 # Salida UTF-8 en consola Windows (acentos y simbolos). for _stream in (sys.stdout, sys.stderr): try: _stream.reconfigure(encoding="utf-8", errors="replace") except Exception: pass class _NoJira: """Cliente Jira vacio (para el modo --solo que desactiva esta fuente).""" def search(self, jql, fields=None, max_results=50): return {"issues": []} def _deps_solo(solo: str | None) -> dict: """Dependencias que desactivan las fuentes no seleccionadas con --solo.""" deps: dict = {} if solo and solo != "telegram": deps["telegram_recoger"] = lambda cfg, hoy: ({}, []) if solo and solo != "correo": deps["correo_recoger"] = lambda horas=24, limite=50: [] if solo and solo != "jira": deps["jira_cliente"] = _NoJira() return deps def cmd_ciclo(args: argparse.Namespace) -> int: """Ejecuta un ciclo completo (o de una sola fuente con --solo).""" cfg = config.cargar() if not lock.adquirir(): print("[monitor_estado] Ya hay una instancia en marcha; no lanzo otra.") return 0 try: ciclo.ejecutar(cfg, deps=_deps_solo(getattr(args, "solo", None))) except Exception as e: print(f"[monitor_estado] Error en el ciclo: {e}", file=sys.stderr) return 1 finally: lock.liberar() print(ciclo.texto_estado()) return 0 def cmd_daemon(args: argparse.Namespace) -> int: """Bucle en segundo plano cada intervalo_minutos (RF-19).""" if not lock.adquirir(): print("[monitor_estado] Ya hay una instancia en marcha; salgo.") return 0 cfg = config.cargar() intervalo = max(1, int(cfg.get("intervalo_minutos", 30))) * 60 print(f"[monitor_estado] daemon iniciado (cada {intervalo // 60} min). Ctrl+C para salir.", flush=True) try: while True: try: res = ciclo.ejecutar(cfg) atencion = len(res.get("requiere_atencion", [])) print( f"[{datetime.now():%Y-%m-%d %H:%M:%S}] ciclo ok | fuentes={res.get('fuentes_con_novedad')} " f"| atencion={atencion} | aviso={res.get('aviso_emitido')}", flush=True, ) except Exception as e: # un ciclo fallido no debe matar el daemon print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] Error en ciclo: {e}", flush=True, file=sys.stderr) time.sleep(intervalo) except KeyboardInterrupt: pass finally: lock.liberar() return 0 def cmd_estado(args: argparse.Namespace) -> int: """Muestra el ultimo resumen y la configuracion (RF-18).""" print(f"[monitor_estado] v{__version__}") print(ciclo.texto_estado()) print("\n--- Configuracion ---") print(config.resumen_config(config.cargar())) return 0 def cmd_simular(args: argparse.Namespace) -> int: """Simula entradas ficticias (correo/Telegram) SIN enviar ni crear nada real. Ejecuta el pipeline completo en un directorio aislado (`estado/monitor/simulacion/`): no toca los datos reales, no crea correos y NUNCA envia mensajes a Telegram. """ from datetime import date import monitor_estado.archivo as archivo import monitor_estado.vistos as vistos base = config.MONITOR_DIR / "simulacion" archivo.MONITOR_DIR = base vistos.VISTO_DIR = base / "visto" hoy = date.today() texto = getattr(args, "texto", None) or "Confirmado el desarrollo" asunto = getattr(args, "asunto", None) or texto def tg_fake(cfg, h): return ({"simulacion_telegram": [{ "id": 9900001, "fecha": datetime.now().isoformat(), "quien": getattr(args, "de", None) or "DAX", "texto": texto, }]}, []) def correo_fake(horas=24, limite=50): remitente = getattr(args, "de", None) or "DAX" return [{ "id": "sim-mail-1", "fecha": datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0200"), "de": f"{remitente} <{remitente.lower()}@prolongo.es>", "asunto": asunto, "snippet": texto, }] res = ciclo.ejecutar( cfg=config.cargar(), hoy=hoy, deps={"telegram_recoger": tg_fake, "correo_recoger": correo_fake, "jira_cliente": _NoJira()}, ) print("[SIMULACION] No se ha enviado ni creado nada real (solo simulado, aislado en 'simulacion/').") print(f"Elementos de atencion: {len(res.get('requiere_atencion', []))} | aviso={res.get('aviso_emitido')}") print(ciclo.texto_estado()) return 0 def cmd_login_telegram(args: argparse.Namespace) -> int: """Autoriza la sesion dedicada de Telegram (interactivo, una sola vez).""" from monitor_estado.fuentes import telegram as tg if getattr(args, "qr", False): tg.login_qr(config.cargar()) else: tg.login(config.cargar(), force_sms=getattr(args, "sms", False)) return 0 def build_parser() -> argparse.ArgumentParser: """Construye el parser de argumentos del CLI.""" parser = argparse.ArgumentParser( prog="monitor_estado", description="Monitor de estado (Telegram + correo + Jira) con resumen IA y aviso local.", ) parser.add_argument("--version", action="version", version=f"monitor_estado {__version__}") subs = parser.add_subparsers(dest="comando") p_ciclo = subs.add_parser("ciclo", help="Ejecuta un ciclo completo y termina.") p_ciclo.add_argument("--solo", choices=["telegram", "correo", "jira"], help="Recoge solo esa fuente.") subs.add_parser("daemon", help="Bucle en segundo plano cada intervalo_minutos.") subs.add_parser("estado", help="Muestra el ultimo resumen y las rutas.") p_sim = subs.add_parser("simular", help="Simula entradas ficticias sin enviar ni crear nada real.") p_sim.add_argument("--texto", help="Texto simulado (defecto: 'Confirmado el desarrollo').") p_sim.add_argument("--asunto", help="Asunto del correo simulado.") p_sim.add_argument("--de", help="Remitente simulado (defecto: DAX).") p_login = subs.add_parser("login-telegram", help="Autoriza la sesion dedicada de Telegram (una vez).") p_login.add_argument("--sms", action="store_true", help="Forzar que el codigo llegue por SMS.") p_login.add_argument("--qr", action="store_true", help="Autorizar escaneando un codigo QR.") return parser def main(argv: list[str] | None = None) -> int: """Punto de entrada: despacha el subcomando o muestra ayuda + config.""" parser = build_parser() args = parser.parse_args(argv) if args.comando == "ciclo": return cmd_ciclo(args) if args.comando == "daemon": return cmd_daemon(args) if args.comando == "estado": return cmd_estado(args) if args.comando == "simular": return cmd_simular(args) if args.comando == "login-telegram": return cmd_login_telegram(args) # Sin subcomando: ayuda + resumen de configuracion. parser.print_help() print("\n--- Configuracion actual ---") print(config.resumen_config(config.cargar())) return 0 if __name__ == "__main__": raise SystemExit(main())