1 Commits

Author SHA1 Message Date
cheveguerra 36be1779c3 feat(ui/polling): monitoreo de desarmadas en Grupo 0, tooltips explicativos y fecha compacta (v06.08.02)
- Monitoreo de Desarmadas & Grupo 0:^
2026-08-02 04:00:25 -06:00
4 changed files with 247 additions and 35 deletions
+10 -3
View File
@@ -1,7 +1,7 @@
# Estado del Proyecto - TotalConnect 2.0 # Estado del Proyecto - TotalConnect 2.0
**Última actualización:** 2026-08-02 **Ú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 ### 🛠️ Características Principales e Infraestructura
1. **Motor Híbrido Dinámico de Monitoreo (`index.py`)**: 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). - **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`). - **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**: 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`). - **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`). - **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. - **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`. - **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**: 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. - 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. - 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 ## 🏷️ 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. * **`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 ## 📋 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. - [ ] Validar rendimiento en entornos con elevado número de cuentas Total Connect.
+219 -10
View File
@@ -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_STATE_FILE = os.path.join(BASE_DIR, 'last_state.json')
LAST_EVENT_IDS_FILE = os.path.join(BASE_DIR, 'last_event_ids.json') LAST_EVENT_IDS_FILE = os.path.join(BASE_DIR, 'last_event_ids.json')
LOG_FILE = os.path.join(BASE_DIR, 'activity.log') LOG_FILE = os.path.join(BASE_DIR, 'activity.log')
APP_VERSION = "06.08.01" APP_VERSION = "06.08.02"
polling_trigger_event = threading.Event() polling_trigger_event = threading.Event()
soap_disabled_accounts = set() soap_disabled_accounts = set()
soap_session_ids = {} # dict: username -> GUID SessionId para SOAP GetLiveEvents soap_session_ids = {} # dict: username -> GUID SessionId para SOAP GetLiveEvents
@@ -228,7 +228,37 @@ def save_polling_state(enabled):
except Exception as e: except Exception as e:
log_msg(f"⚠️ No se pudo actualizar polling_enabled en {CREDENTIALS_FILE}: {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() polling_enabled = load_polling_state()
monitor_disarmed_enabled, max_disarmed_count = load_disarmed_monitor_config()
def log_msg(msg): def log_msg(msg):
"""Escribe un mensaje de log con timestamp tanto en la consola (con flush) como en activity.log.""" """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): def generate_html_report(data):
"""Genera un archivo HTML premium, limpio, claro y compacto con el estatus consolidado.""" """Genera un archivo HTML premium, limpio, claro y compacto con el estatus consolidado."""
now = datetime.datetime.now(ZoneInfo("America/Mexico_City")) now = datetime.datetime.now(ZoneInfo("America/Mexico_City"))
dias = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"] dias = ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"]
meses = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"] 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 # Generar checkboxes dinámicos para las cuentas de credentials.json
checkboxes_html = "" checkboxes_html = ""
@@ -1233,6 +1264,64 @@ def generate_html_report(data):
.btn-log-refresh:hover {{ .btn-log-refresh:hover {{
background: #2563eb; 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);
}}
</style> </style>
</head> </head>
<body> <body>
@@ -1249,14 +1338,51 @@ def generate_html_report(data):
</div> </div>
</div> </div>
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
<div class="accounts-selector" style="display: flex; gap: 10px; align-items: center; background: #f8fafc; padding: 6px 12px; border-radius: 8px; border: 1px solid #e2e8f0;"> <div class="disarmed-tooltip-wrapper accounts-selector" style="display: flex; gap: 10px; align-items: center; background: #f8fafc; padding: 6px 12px; border-radius: 8px; border: 1px solid #e2e8f0;">
<span style="font-size: 0.7rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.3px; margin-right: 4px;">Cuentas:</span> <span class="disarmed-tooltip-trigger" style="font-size: 0.7rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.3px; margin-right: 4px; text-decoration: underline dotted #94a3b8;">Cuentas:</span>
{checkboxes_html} {checkboxes_html}
<div class="disarmed-tooltip-box">
<div style="font-weight: 700; color: #38bdf8; margin-bottom: 4px; font-size: 0.8rem;">
👤 Selección de Cuentas TotalConnect
</div> </div>
<div>
Permite elegir qué cuentas de usuario (configuradas en <code>credentials.json</code>) serán procesadas al presionar el botón <b>"Actualizar"</b> para consultar sus sucursales.
</div>
</div>
</div>
<div class="disarmed-tooltip-wrapper" style="background: #f8fafc; padding: 6px 12px; border-radius: 8px; border: 1px solid #e2e8f0;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0; user-select: none;">
<input type="checkbox" id="disarmed-monitor-cb" {disarmed_checked_attr} onchange="toggleDisarmedMonitor(this)" style="cursor: pointer; width: 14px; height: 14px; accent-color: #2563eb;">
<span class="disarmed-tooltip-trigger">Desarmadas revisión constante</span>
</label>
<div class="disarmed-tooltip-box">
<div style="font-weight: 700; color: #38bdf8; margin-bottom: 4px; font-size: 0.8rem;">
🔄 Monitoreo de Desarmadas en Revisión Constante
</div>
<div>
Al activar esta casilla, las sucursales desarmadas se agregan al <b>Grupo 0</b> de alta prioridad para detectar en tiempo real el instante exacto en que son armadas.
</div>
<div style="margin-top: 8px; padding-top: 6px; border-top: 1px solid #334155; color: #fbbf24; font-size: 0.72rem;">
⚡ <b>Protección por Límite:</b> Si el número total de desarmadas supera el límite configurado (<code>max_disarmed_count:{max_disarmed_count}</code> en <code>credentials.json</code>), esta función se desactivará automáticamente para evitar sobrecargar la API de Honeywell.
</div>
</div>
</div>
<div class="disarmed-tooltip-wrapper" style="position: relative; display: inline-flex;">
<button id="toggle-polling-btn" onclick="togglePolling()" style="display: flex; align-items: center; gap: 6px; padding: 6px 12px; font-weight: 600; font-size: 0.75rem; border-radius: 8px; border: 1px solid #a7f3d0; background: #ecfdf5; color: #065f46; cursor: pointer; transition: all 0.2s;"> <button id="toggle-polling-btn" onclick="togglePolling()" style="display: flex; align-items: center; gap: 6px; padding: 6px 12px; font-weight: 600; font-size: 0.75rem; border-radius: 8px; border: 1px solid #a7f3d0; background: #ecfdf5; color: #065f46; cursor: pointer; transition: all 0.2s;">
<span id="polling-dot" style="width: 8px; height: 8px; border-radius: 50%; background: #10b981; display: inline-block;"></span> <span id="polling-dot" style="width: 8px; height: 8px; border-radius: 50%; background: #10b981; display: inline-block;"></span>
<span id="polling-text">Polling: Activo</span> <span id="polling-text">Polling: Activo</span>
</button> </button>
<div class="disarmed-tooltip-box">
<div style="font-weight: 700; color: #38bdf8; margin-bottom: 4px; font-size: 0.8rem;">
🛑/🟢 Control de Polling Automático (Kill-Switch)
</div>
<div>
Activa o pausa las consultas automáticas en segundo plano (cada 30s).<br><br>
- <b>Activo:</b> El motor monitorea eventos SOAP en tiempo real y realiza sondeos por lotes.<br>
- <b>Pausado:</b> Detiene las peticiones automáticas a Honeywell para congelar el rastreo o conservar ancho de banda.
</div>
</div>
</div>
<button id="refresh-btn"> <button id="refresh-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>
Actualizar Actualizar
@@ -1403,6 +1529,30 @@ def generate_html_report(data):
}} }}
}} }}
async function toggleDisarmedMonitor(checkbox) {{
let toggleUrl = '/toggle-disarmed-monitor';
if (window.location.protocol === 'file:') {{
if (window.location.pathname.indexOf(':') !== -1) {{
toggleUrl = 'http://localhost:8080/toggle-disarmed-monitor';
}} else {{
toggleUrl = 'http://192.168.100.50:8080/toggle-disarmed-monitor';
}}
}}
try {{
const res = await fetch(toggleUrl, {{ method: 'POST' }});
const data = await res.json();
if (data.status === 'success') {{
if (checkbox) checkbox.checked = data.enabled;
}} else if (data.message) {{
alert('Atención: ' + data.message);
if (checkbox) checkbox.checked = false;
}}
}} catch (e) {{
alert('No se pudo cambiar el monitoreo de desarmadas.');
if (checkbox) checkbox.checked = !checkbox.checked;
}}
}}
function updatePollingUI(enabled) {{ function updatePollingUI(enabled) {{
const dot = document.getElementById('polling-dot'); const dot = document.getElementById('polling-dot');
const text = document.getElementById('polling-text'); const text = document.getElementById('polling-text');
@@ -1441,6 +1591,10 @@ def generate_html_report(data):
if (data.enabled !== undefined) {{ if (data.enabled !== undefined) {{
updatePollingUI(data.enabled); updatePollingUI(data.enabled);
}} }}
if (data.disarmed_monitor_enabled !== undefined) {{
const cb = document.getElementById('disarmed-monitor-cb');
if (cb) cb.checked = data.disarmed_monitor_enabled;
}}
// Si se completó un nuevo ciclo de actualización en segundo plano, recargar automáticamente las tarjetas // Si se completó un nuevo ciclo de actualización en segundo plano, recargar automáticamente las tarjetas
if (data.last_poll) {{ if (data.last_poll) {{
if (currentLastPoll !== null && currentLastPoll !== data.last_poll) {{ if (currentLastPoll !== null && currentLastPoll !== data.last_poll) {{
@@ -1710,6 +1864,8 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
self.end_headers() self.end_headers()
data = { data = {
"enabled": polling_enabled, "enabled": polling_enabled,
"disarmed_monitor_enabled": monitor_disarmed_enabled,
"max_disarmed_count": max_disarmed_count,
"last_poll": last_poll_timestamp, "last_poll": last_poll_timestamp,
"loading_status": current_loading_status "loading_status": current_loading_status
} }
@@ -1827,6 +1983,25 @@ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
self.send_header('Access-Control-Allow-Headers', 'Content-Type') self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers() self.end_headers()
self.wfile.write(json.dumps({"status": "success", "enabled": polling_enabled}).encode('utf-8')) self.wfile.write(json.dumps({"status": "success", "enabled": polling_enabled}).encode('utf-8'))
elif self.path == '/toggle-disarmed-monitor':
global monitor_disarmed_enabled
monitor_disarmed_enabled = not monitor_disarmed_enabled
save_disarmed_monitor_state(monitor_disarmed_enabled)
state_str = "activado" if monitor_disarmed_enabled else "desactivado"
log_msg(f"🔄 Monitoreo de sucursales desarmadas {state_str} por el usuario.")
if monitor_disarmed_enabled:
polling_trigger_event.set()
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(json.dumps({
"status": "success",
"enabled": monitor_disarmed_enabled,
"max_count": max_disarmed_count
}).encode('utf-8'))
else: else:
self.send_response(404) self.send_response(404)
self.end_headers() self.end_headers()
@@ -2254,11 +2429,39 @@ def run_update(accounts=None, exclude=None, full_refresh=False):
active_locs_with_obj.sort(key=get_download_epoch) active_locs_with_obj.sort(key=get_download_epoch)
# 3. Construir Grupo 0 (Alarmas Activas y Triggers SOAP - SIEMPRE incluidas obligatoriamente) # 3. Construir Grupo 0 (Alarmas Activas, Triggers SOAP y Desarmadas en Revisión Constante)
# Se extraen ANTES de los demás grupos para garantizar presencia y prioridad en cada ciclo. global monitor_disarmed_enabled, max_disarmed_count
group_0 = [] group_0 = []
group_0_ids = set() group_0_ids = set()
non_alarm_locs = [] non_alarm_locs = []
# Pre-calcular sucursales desarmadas si la opción está activa
disarmed_candidates = []
if monitor_disarmed_enabled:
for loc_id, location in active_locs_with_obj:
loc_id_str = str(loc_id)
prev = last_loc_map.get(loc_id_str, {})
status_val = prev.get('status')
if isinstance(status_val, int):
try:
status_val = ArmingState(status_val)
except Exception:
pass
is_disarmed = (status_val == ArmingState.DISARMED or status_val == 10200 or str(status_val).upper() == "DISARMED")
if is_disarmed:
disarmed_candidates.append((loc_id, location))
# Auto-desactivar si excede el límite máximo configurado
if len(disarmed_candidates) > max_disarmed_count:
log_msg(f" ⚠️ Monitoreo de desarmadas desactivado automáticamente: hay {len(disarmed_candidates)} sucursales desarmadas (supera el límite configurado de {max_disarmed_count}).")
monitor_disarmed_enabled = False
save_disarmed_monitor_state(False)
disarmed_candidates = []
elif disarmed_candidates:
log_msg(f" 🟢 Monitoreo de desarmadas activo: {len(disarmed_candidates)} sucursales forzadas en Grupo 0 (Límite: {max_disarmed_count}).")
disarmed_ids = {x[0] for x in disarmed_candidates} if monitor_disarmed_enabled else set()
for loc_id, location in active_locs_with_obj: for loc_id, location in active_locs_with_obj:
loc_id_str = str(loc_id) loc_id_str = str(loc_id)
prev = last_loc_map.get(loc_id_str, {}) prev = last_loc_map.get(loc_id_str, {})
@@ -2282,11 +2485,17 @@ def run_update(accounts=None, exclude=None, full_refresh=False):
any(z.get('status') == 'Alarma' for z in prev.get('active_zones', [])) any(z.get('status') == 'Alarma' for z in prev.get('active_zones', []))
) )
has_soap_trigger = prev.get('needs_full_update', False) has_soap_trigger = prev.get('needs_full_update', False)
is_disarmed_monitored = loc_id in disarmed_ids
if is_alarm or has_soap_trigger: if is_alarm or has_soap_trigger or is_disarmed_monitored:
group_0.append((loc_id, location)) group_0.append((loc_id, location))
group_0_ids.add(loc_id) group_0_ids.add(loc_id)
reason_label = "Alarma" if is_alarm else "Evento SOAP" if is_alarm:
reason_label = "Alarma"
elif has_soap_trigger:
reason_label = "Evento SOAP"
else:
reason_label = "Desarmada - Revisión Constante"
log_msg(f" 🚨 Grupo 0 ({reason_label}): '{location.location_name}' forzado en este ciclo.") log_msg(f" 🚨 Grupo 0 ({reason_label}): '{location.location_name}' forzado en este ciclo.")
else: else:
non_alarm_locs.append((loc_id, location)) non_alarm_locs.append((loc_id, location))
BIN
View File
Binary file not shown.
+14 -18
View File
@@ -1,48 +1,44 @@
@echo off @echo off
echo ======================================================= echo =======================================================
echo Actualizando Repositorio Git y creando Tag v06.08.01 echo Actualizando Repositorio Git y creando Tag v06.08.02
echo ======================================================= echo =======================================================
git add . git add .
git commit -m "feat(polling): prioridad en Grupo 0 para alarmas y eventos SOAP e indicador de escaneo en vivo (v06.08.01)" -m "- Logica de Grupo 0:^ git commit -m "feat(ui/polling): monitoreo de desarmadas en Grupo 0, tooltips explicativos y fecha compacta (v06.08.02)" -m "- Monitoreo de Desarmadas & Grupo 0:^
* Se unificaron las alarmas activas (particiones, zonas y estado ALARMING) y los eventos pendientes de SOAP (needs_full_update = True) dentro del Grupo 0.^ * Agregado switch 'Desarmadas revision constante' para incluir sucursales desarmadas en Grupo 0.^
* Garantiza ejecucion al inicio del ciclo de polling en las primeras posiciones, evitando la degradacion por el limite de 35 consultas rutinarias.^ * Proteccion por Limite: Autodesactivacion automatica si el numero de desarmadas supera 'max_disarmed_count' en credentials.json.^
* Mantiene auto-limpieza dinamica: al recibir confirmacion de restablecimiento via REST, la sucursal sale del Grupo 0 automaticamente.^ * Nuevo endpoint HTTP /toggle-disarmed-monitor para cambio de estado en caliente.^
^ ^
- UI & Monitor en Tiempo Real:^ - UI & Tooltips Flotantes:^
* Agregado badge visual de escaneo en vivo en el encabezado del tablero.^ * Implementados tooltips explicativos con diseño oscuro emergentes (mouseover) en Cuentas, Desarmadas y Polling Activo.^
* Muestra la sucursal actual en formato compacto (Cargando sucursal: 6001).^ * Formato compacto de Fecha/Hora: dias/meses abreviados a 3 letras, sin año y hora en formato 24 horas (ej. dom, 2 ago - 02:36:18).^
* Implementacion en JS usando setTimeout recursivo, manipulacion directa de textContent y suspension en pestanas inactivas (Page Visibility API).^
^ ^
- Correcciones de Infraestructura:^ - Version & Infraestructura:^
* Se eliminaron rutas absolutas en duro (Z:\, E:\) en save_polling_state().^ * Persistencia de 'monitor_disarmed_enabled' y 'max_disarmed_count' en credentials.json.^
* La configuracion del estado del polling ahora escribe exclusivamente en credentials.json de la ruta de ejecucion actual.^ * Version actualizada a 06.08.02."
* Actualizada version a 06.08.01."
if %ERRORLEVEL% NEQ 0 ( if %ERRORLEVEL% NEQ 0 (
echo Error al realizar el commit o no hay cambios por commitear. echo Error al realizar el commit o no hay cambios por commitear.
) )
echo Creando Tag v06.08.01... echo Creando Tag v06.08.02...
git tag -a v06.08.01 -m "Version 06.08.01 - Prioridad Grupo 0 e indicador de escaneo" git tag -a v06.08.02 -m "Version 06.08.02 - Desarmadas en revision constante con tooltip y proteccion por limite"
echo Subiendo cambios y tag a GitHub... echo Subiendo cambios y tag a GitHub...
git push origin main git push origin main
git push origin v06.08.01 git push origin v06.08.02
echo ======================================================= echo =======================================================
echo Proceso completado exitosamente. echo Proceso completado exitosamente.