diff --git a/CONTEXT.md b/CONTEXT.md
index 525aa89..f38e01d 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -1,7 +1,7 @@
# Estado del Proyecto - TotalConnect 2.0
**Última actualización:** 2026-08-02
-**Versión actual:** v06.08.01
+**Versión actual:** v06.08.02
---
@@ -11,15 +11,20 @@ El proyecto **TotalConnect 2.0** es un panel web centralizado de alto rendimient
### 🛠️ Características Principales e Infraestructura
1. **Motor Híbrido Dinámico de Monitoreo (`index.py`)**:
- - **Grupo 0**: Prioridad absoluta para sucursales con alarmas activas (particiones, zonas y estado ALARMING) o eventos SOAP pendientes (`needs_full_update = True`).
+ - **Grupo 0**: Prioridad absoluta para sucursales con alarmas activas, eventos SOAP pendientes y sucursales desarmadas bajo el modo *"Desarmadas revisión constante"*.
+ - **Control de Protección por Límite**: Si el número de sucursales desarmadas supera la variable `max_disarmed_count` configurada en `credentials.json`, la función se deshabilita automáticamente para evitar cuellos de botella y *rate-limiting*.
- **Esquema 2:1 (SOAP + REST)**: 2 consultas ligeras vía SOAP por 1 consulta completa por REST para evitar el *rate-limiting* de Honeywell (máximo 35 consultas REST por ciclo).
- **Resiliencia de Sesión**: Purgado automático de tokens SOAP obsoletos y auto-recuperación ante errores de red (`ConnectionResetError`).
2. **Dashboard Web e Interfaz en Tiempo Real**:
+ - **Switch "Desarmadas revisión constante"**: Control checkbox interactivo en la cabecera del dashboard con sincronización en vivo.
+ - **Tooltips Flotantes Explicativos**: Ventanas emergentes al pasar el cursor (`mouseover`) sobre los controles de la cabecera (*Cuentas*, *Desarmadas revisión constante* y *Polling: Activo*), detallando sus funciones y parámetros técnicos.
+ - **Formato Compacto de Fecha/Hora**: Fecha de última actualización optimizada con días/meses abreviados a 3 letras, sin año y hora en formato de 24 horas (ej. `dom, 2 ago - 02:36:18`).
- **Badge de Escaneo en Vivo**: Muestra en tiempo real la sucursal actual en proceso de carga/sondeo (`Cargando sucursal: 6001`).
- **Visor de Bitácora Interactiva**: Modal visual en vivo para consultar las últimas 150 líneas del archivo de log (`activity.log`).
- **Hard Reset**: Mecanismo de purga de caché y reinicio por clic prolongado (3s) en el botón de actualización.
- **Kill-Switch / Polling Toggle**: Control de inicio/pausa de sondeo en segundo plano con *Circuit Breaker* ante errores `401`/`429`.
3. **Portabilidad y Parámetros**:
+ - Persistencia de `monitor_disarmed_enabled` y `max_disarmed_count` en `credentials.json`.
- Eliminación de rutas absolutas en duro (`Z:\`, `E:\`), operando de manera 100% relativa a la ruta de ejecución actual.
- Binario autónomo `totalconnect.exe` para Windows y soporte para servicio Systemd en Linux.
@@ -27,11 +32,13 @@ El proyecto **TotalConnect 2.0** es un panel web centralizado de alto rendimient
## 🏷️ Historial de Tags / Versiones
+* **`v06.08.02`**: Implementado switch "Desarmadas revisión constante" en UI con tooltip explicativo, persistencia en `credentials.json`, endpoint `/toggle-disarmed-monitor` y autodesactivación por límite `max_disarmed_count`.
* **`v06.08.01`**: Prioridad en Grupo 0 para alarmas y eventos SOAP, indicador de escaneo en vivo en UI, eliminación de rutas absolutas en duro.
---
## 📋 Tareas Pendientes / Próximos Pasos
-- [ ] Monitorear estabilidad del motor híbrido en producción tras despliegue de `v06.08.01`.
+- [ ] Monitorear estabilidad del motor híbrido y la función de desarmadas en producción tras despliegue de `v06.08.02`.
- [ ] Validar rendimiento en entornos con elevado número de cuentas Total Connect.
+
diff --git a/index.py b/index.py
index 7780f8c..d542f0f 100644
--- a/index.py
+++ b/index.py
@@ -177,7 +177,7 @@ CACHE_FILE = os.path.join(BASE_DIR, 'locations_cache.json')
LAST_STATE_FILE = os.path.join(BASE_DIR, 'last_state.json')
LAST_EVENT_IDS_FILE = os.path.join(BASE_DIR, 'last_event_ids.json')
LOG_FILE = os.path.join(BASE_DIR, 'activity.log')
-APP_VERSION = "06.08.01"
+APP_VERSION = "06.08.02"
polling_trigger_event = threading.Event()
soap_disabled_accounts = set()
soap_session_ids = {} # dict: username -> GUID SessionId para SOAP GetLiveEvents
@@ -228,7 +228,37 @@ def save_polling_state(enabled):
except Exception as e:
log_msg(f"⚠️ No se pudo actualizar polling_enabled en {CREDENTIALS_FILE}: {e}")
+def load_disarmed_monitor_config():
+ """Carga el estado del monitoreo de desarmadas y su límite máximo exclusivamente desde credentials.json."""
+ enabled = False
+ max_count = 50
+ if os.path.exists(CREDENTIALS_FILE):
+ try:
+ with open(CREDENTIALS_FILE, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ if isinstance(data, dict):
+ enabled = data.get("monitor_disarmed_enabled", False)
+ max_count = data.get("max_disarmed_count", 50)
+ except Exception:
+ pass
+ return enabled, max_count
+
+def save_disarmed_monitor_state(enabled):
+ """Guarda el estado del monitoreo de desarmadas exclusivamente en credentials.json de la ruta actual."""
+ if os.path.exists(CREDENTIALS_FILE):
+ try:
+ with open(CREDENTIALS_FILE, 'r', encoding='utf-8') as f:
+ creds_data = json.load(f)
+ if isinstance(creds_data, dict):
+ creds_data["monitor_disarmed_enabled"] = enabled
+ with open(CREDENTIALS_FILE, 'w', encoding='utf-8') as f:
+ json.dump(creds_data, f, ensure_ascii=False, indent=2)
+ log_msg(f"💾 Estado de monitoreo de desarmadas ({enabled}) guardado en: {CREDENTIALS_FILE}")
+ except Exception as e:
+ log_msg(f"⚠️ No se pudo actualizar monitor_disarmed_enabled en {CREDENTIALS_FILE}: {e}")
+
polling_enabled = load_polling_state()
+monitor_disarmed_enabled, max_disarmed_count = load_disarmed_monitor_config()
def log_msg(msg):
"""Escribe un mensaje de log con timestamp tanto en la consola (con flush) como en activity.log."""
@@ -579,10 +609,11 @@ def _get_location_sort_priority(loc_item):
def generate_html_report(data):
"""Genera un archivo HTML premium, limpio, claro y compacto con el estatus consolidado."""
now = datetime.datetime.now(ZoneInfo("America/Mexico_City"))
- dias = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]
- meses = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]
+ dias = ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"]
+ meses = ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"]
- timestamp = f"{dias[now.weekday()]}, {now.day} de {meses[now.month - 1]} de {now.year} - {now.strftime('%I:%M:%S %p')}"
+ timestamp = f"{dias[now.weekday()]}, {now.day} {meses[now.month - 1]} - {now.strftime('%H:%M:%S')}"
+ disarmed_checked_attr = "checked" if monitor_disarmed_enabled else ""
# Generar checkboxes dinámicos para las cuentas de credentials.json
checkboxes_html = ""
@@ -1233,6 +1264,64 @@ def generate_html_report(data):
.btn-log-refresh:hover {{
background: #2563eb;
}}
+ /* Tooltip Desarmadas Revisión Constante */
+ .disarmed-tooltip-wrapper {{
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ }}
+
+ .disarmed-tooltip-trigger {{
+ color: #475569;
+ font-weight: 600;
+ font-size: 0.75rem;
+ cursor: help;
+ text-decoration: underline dotted #94a3b8;
+ transition: color 0.15s;
+ }}
+
+ .disarmed-tooltip-trigger:hover {{
+ color: #1e293b;
+ text-decoration-color: #2563eb;
+ }}
+
+ .disarmed-tooltip-box {{
+ visibility: hidden;
+ opacity: 0;
+ position: absolute;
+ top: 130%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-4px);
+ width: 320px;
+ background: #0f172a;
+ color: #f8fafc;
+ padding: 12px 14px;
+ border-radius: 10px;
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
+ border: 1px solid #334155;
+ font-size: 0.75rem;
+ line-height: 1.45;
+ z-index: 1000;
+ transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
+ pointer-events: none;
+ }}
+
+ .disarmed-tooltip-box::after {{
+ content: '';
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border-width: 6px;
+ border-style: solid;
+ border-color: transparent transparent #0f172a transparent;
+ }}
+
+ .disarmed-tooltip-wrapper:hover .disarmed-tooltip-box {{
+ visibility: visible;
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }}
+ Permite elegir qué cuentas de usuario (configuradas en credentials.json) serán procesadas al presionar el botón "Actualizar" para consultar sus sucursales.
+
+
+
+
+
+
+
+ 🔄 Monitoreo de Desarmadas en Revisión Constante
+
+
+ Al activar esta casilla, las sucursales desarmadas se agregan al Grupo 0 de alta prioridad para detectar en tiempo real el instante exacto en que son armadas.
+
+
+ ⚡ Protección por Límite: Si el número total de desarmadas supera el límite configurado (max_disarmed_count:{max_disarmed_count} en credentials.json), esta función se desactivará automáticamente para evitar sobrecargar la API de Honeywell.
+
+
+
+
+
+
+
+ 🛑/🟢 Control de Polling Automático (Kill-Switch)
+
+
+ Activa o pausa las consultas automáticas en segundo plano (cada 30s).
+ - Activo: El motor monitorea eventos SOAP en tiempo real y realiza sondeos por lotes.
+ - Pausado: Detiene las peticiones automáticas a Honeywell para congelar el rastreo o conservar ancho de banda.
+