Funcionalidaddraftcreado Mon Jul 13
fleet:view, el aviso degrada al confirm normal (fail-safe).\n\n## Motivación\n\nVisión de producto: “un solo Agente en un equipo 24/7 debería bastar”. El problema:\n- Un user instala el Agent en su portátil (primera máquina disponible).\n- Activa Sentinel para monitorización 24/7.\n- Se va a casa, el portátil se hiberna/apaga.\n- El monitoreo se detiene.\n- User culpa a CreaRack: “el monitoreo no funciona” (culpa es de la máquina, no del software).\n\nLa solución: avisar basándose en datos pasados. Si el historial muestra el patrón (portátil inestable), sugerir mover el agente a una máquina fija. En el momento exacto de la decisión, cuando el user está eligiendo el agente para Sentinel.\n\n## Arquitectura\n\n### 1. Contador de desconexiones (AgentInstance)\n\nVer [[entity—terminal—model—agentinstance]] para detalles completos. Resumen:\n\n- Campos nuevos en v1.49.0:\n - disconnect_count: entero, contador de desconexiones “largas” en la ventana vigente\n - disconnect_window_start: timestamp, inicio de la ventana rodante\n\n- Ventana rodante: ~7 días. Si now - disconnect_window_start > 7d, la próxima desconexión larga arranca una nueva ventana desde cero.\n\n- Property disconnects_7d: devuelve 0 si la ventana ha caducado, o el valor de disconnect_count en caso contrario.\n\n- Método note_reconnect_gap(): Registra una desconexión “larga” al reconectar. Solo cuentan huecos offline >10 min (DISCONNECT_GAP_MINUTES). Los micro-cortes (deploy del SaaS, blips de red) reconectan en segundos y NO cuentan.\n\n### 2. Ciclo de vida del contador\n\nEn terminal/consumers.py::_register_and_assign_role() (consumer WebSocket del Agent):\n\npython\ndef _db_register():\n # Detectar si venía de offline ANTES del upsert\n was_offline = AgentInstance.objects.filter(\n agent_id=self.agent_id, status=\"offline\"\n ).exists()\n\n # Upsert normal\n instance, created = AgentInstance.objects.update_or_create(\n agent_id=self.agent_id,\n defaults={\"status\": \"online\", \"connected_at\": now, ...},\n )\n\n # ANTI-DOBLE-CONTEO: solo si venía de offline, evalúa el hueco\n if was_offline and instance.note_reconnect_gap():\n instance.save(update_fields=[\"disconnect_count\", \"disconnect_window_start\", \"updated_at\"])\n\n\nFlujo:\n1. Agent offline → status = “offline”, disconnected_at = now\n2. Agent reconnecta → consulta _register_and_assign_role()\n3. Detecta: “era offline” + hueco > 10 min → llama note_reconnect_gap()\n4. Incrementa disconnect_count (o arranca nueva ventana si expiró)\n5. Guarda en DB\n\n### 3. Exposición en API\n\n**GET /api/agent/fleet** (endpoint existente en terminal/api/fleet.py):\n\npython\nclass AgentInstanceOut(Schema):\n # ... campos existentes ...\n disconnects_7d: int = 0 # NUEVO en v1.49.0\n\n\nCada agente en la respuesta ahora incluye disconnects_7d con el valor actual de la property.\n\n### 4. Lógica de aviso (frontend)\n\nEn static/js/pages/observatory/ObservatorySentinel.js::requestModeChange() (al activar/desactivar Sentinel):\n\njavascript\nexport async function requestModeChange(obs) {\n const current = obs._sentinelConfig?.mode || 'cloud';\n const newMode = current === 'sentinel' ? 'cloud' : 'sentinel';\n let msg = newMode === 'sentinel'\n ? t('Enable 24/7 Sentinel Mode?\\n\\nThe Local Agent will monitor continuously...')\n : t('Switch to Cloud Only mode?\\n\\nMonitoring will STOP when you close the browser.');\n \n // NUEVO: aviso de estabilidad al ACTIVAR Sentinel\n if (newMode === 'sentinel') {\n const warning = await _primaryStabilityWarning();\n if (warning) msg += '\\n\\n' + warning;\n }\n \n if (!confirm(msg)) return;\n _applySentinelMode(obs, newMode);\n}\n\nasync function _primaryStabilityWarning() {\n try {\n const fleet = await window.ApiService.get('/api/agent/fleet');\n if (!Array.isArray(fleet) || !fleet.length) return null;\n \n // Buscar Primary o usar el primero\n const primary = fleet.find(a => a.role === 'primary') || fleet[0];\n const gaps = primary?.disconnects_7d || 0;\n \n // UMBRAL: ≥3 desconexiones largas en 7 días\n if (gaps >= UNSTABLE_DISCONNECTS_7D) {\n return interpolate(\n t('Heads up: the agent that will run the monitoring lost connection %(n)s times in the last 7 days. For continuous 24/7 monitoring, install the Agent on a machine that stays on (a server or a mini-PC).'),\n { n: gaps }, true\n );\n }\n } catch { /* sin datos de fleet → confirm normal */ }\n return null;\n}\n\nconst UNSTABLE_DISCONNECTS_7D = 3; // Umbral\n\n\nFlujo:\n1. User hace click en “Enable 24/7 Sentinel”\n2. requestModeChange() con newMode = 'sentinel'\n3. Construye mensaje base\n4. Llama a _primaryStabilityWarning() (async)\n5. _primaryStabilityWarning() hace GET /api/agent/fleet\n6. Si Primary tiene disconnects_7d ≥ 3, añade advertencia al mensaje\n7. Muestra confirm con advertencia (o sin, si no hay datos)\n8. Si user confirma, aplica Sentinel\n\nFail-safe:\n- Sin permiso fleet:view → el GET falla → catch silencioso → confirm normal (como siempre)\n- Sin agentes en flota → confirm normal\n- Si Primary tiene <3 desconexiones → confirm normal\n\n## Testing\n\nFile: tests/api/test_agent_stability_signal.py (8 casos nuevos)\n\n### TestNoteReconnectGap\n\n1. test_short_gap_does_not_count: Hueco <10 min → return False\n2. test_long_gap_counts: Hueco >10 min → return True, disconnect_count = 1\n3. test_no_previous_disconnect_does_not_count: disconnected_at = None → return False\n4. test_gaps_accumulate_within_window: Contador acumula dentro de la ventana (2 → 3)\n5. test_expired_window_restarts_count: Ventana expirada (>7d atrás) → reinicia desde 1\n6. test_disconnects_7d_zero_when_window_expired: Property devuelve 0 si ventana caducó\n7. test_disconnects_7d_returns_count_in_window: Property devuelve el contador si ventana vigente\n\n### TestFleetExposesStabilitySignal\n\n8. test_fleet_returns_disconnects_7d: GET /api/agent/fleet serializa disconnects_7d correctamente\n\nEstado: 13/13 tests verdes (incluyendo tests de fleet existentes)\n\n## I18n\n\nStrings nuevos en djangojs.po:\n- \"Heads up: the agent that will run the monitoring lost connection %(n)s times in the last 7 days. For continuous 24/7 monitoring, install the Agent on a machine that stays on (a server or a mini-PC).\" → traducción ES larga\n- \"Where to Install It\", \"Where to install the Local Agent\" (reutilizados de la guía modal)\n\n## Límites y futuro\n\n1. Aprendizaje inicial: El contador empieza a 0 en la migración (v1.49.0). Los primeros días sin historial, no hay aviso (comportamiento correcto, esperamos datos). A los 3-7 días, el sistema “aprende”.\n\n2. Detección de portátil: La detección directa via batería (es portátil sí/no) queda apuntada para un future release del Agent. Aquí usamos observación pasada del patrón.\n\n3. Umbral configurable: UNSTABLE_DISCONNECTS_7D = 3 hardcoded en JS. Si en futuro queremos hacerlo configurable por tenant, migrar a una const en settings + endpoint.\n\n