fix: handle clear_lobby event to kick players and reset lobby

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 02:11:59 +00:00
co-authored by Claude Sonnet 4.6
parent b88f47b72d
commit 3066e1e460
+109 -60
View File
@@ -2,7 +2,7 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse
import json, time, random, sqlite3, os, asyncio
import json, time, random, sqlite3, os
from datetime import datetime
DB_PATH = os.environ.get("DB_PATH", "/data/quiz.db")
@@ -65,59 +65,114 @@ templates = Jinja2Templates(directory="templates")
# ── QUESTIONS ─────────────────────────────────────────────────────────────────
QUESTIONS = [
{
"q": "¿Cuál es el enfoque principal del equipo de Network Intelligence?",
"q": "NI construye herramientas propias en vez de depender de licencias comerciales. ¿Por qué?\n\n[EN] NI builds its own tools instead of relying on commercial licenses. Why?",
"options": [
"Vender licencias de software a otras empresas",
"Construir herramientas propias que empoderan a los equipos operacionales",
"Administrar el presupuesto de TI de LCPR",
"Instalar equipos físicos en planta externa"
"Para tener control total, escalar sin costo por usuario y adaptarse a nuestros flujos reales / Full control, scale without per-user cost, adapt to real workflows",
"Para ahorrar en hardware de servidores / To save on server hardware",
"Porque las herramientas comerciales no existen para redes / Commercial tools don't exist for networks",
"Es un requisito del contrato de Liberty / It's a Liberty contract requirement"
],
"correct": 1,
"fun_fact": "¡Exacto! Construimos para quienes operan la red día a día — no solo dashboards bonitos."
"correct": 0,
"fun_fact": "Código propio = sin techo de plataforma. Podemos extender, integrar y mejorar sin pedir permiso ni pagar más. / Our code = no platform ceiling. We extend, integrate, and improve without asking permission or paying more."
},
{
"q": "¿Qué problema resolvió el equipo al reemplazar Power BI con frontend propio?",
"q": "¿Para quién construye herramientas el equipo de Network Intelligence?\n\n[EN] Who does the Network Intelligence team build tools for?",
"options": [
"La pantalla se veía más bonita",
"Eliminamos la dependencia de licencias costosas y ganamos control total",
"Fue más fácil de instalar en los servidores",
"Los colores del dashboard coincidían con los de Liberty"
],
"correct": 1,
"fun_fact": "$0 en licencias de visualización. El código es nuestro y lo extendemos como queremos."
},
{
"q": "¿Qué es Pathfinder?",
"options": [
"Una aplicación para rastrear camiones de campo",
"Un sistema de GPS para la red de fibra",
"Una plataforma web que centraliza herramientas operacionales de red en un solo lugar",
"Un reporte mensual de disponibilidad de red"
"Solo para gerencia ejecutiva / Only for executive management",
"Para clientes residenciales de Liberty / For Liberty residential customers",
"Para los equipos de NOC, ingeniería, operaciones y técnicos de campo / For NOC, engineering, operations, and field technician teams",
"Para el equipo de ventas B2B / For the B2B sales team"
],
"correct": 2,
"fun_fact": "Pathfinder es el 'panel de control' propio de LCPR — construido exactamente para nuestros flujos de trabajo."
"fun_fact": "El objetivo no es reportar — es que quien opera la red pueda decidir y actuar más rápido. / The goal isn't reporting — it's enabling the people running the network to decide and act faster."
},
{
"q": "¿Qué logró el proyecto de Firmware Upgrade Automation?",
"q": "Pathfinder junta monitoreo, rutas, topología y análisis operacional en un solo lugar. ¿Cuál es su propósito principal?\n\n[EN] Pathfinder brings together monitoring, routes, topology, and operational analysis in one place. What is its main purpose?",
"options": [
"Actualizar manualmente 10 modems por semana",
"Contratar más técnicos para visitas a domicilio",
"Automatizar la actualización masiva de modems sin intervención humana",
"Comprar equipos nuevos para reemplazar los modems viejos"
"Gestionar el presupuesto de proyectos de red / Manage the network project budget",
"Reemplazar el sistema de ticketing de la empresa / Replace the company ticketing system",
"Automatizar facturación a clientes B2B / Automate billing for B2B customers",
"Ayudar a ingeniería y NOC a ver el mismo contexto de red para decidir y actuar más rápido / Help engineering and NOC see the same network context to decide and act faster"
],
"correct": 2,
"fun_fact": "400+ modems actualizados en batches automáticos. Antes era trabajo manual uno por uno."
"correct": 3,
"fun_fact": "Pathfinder no es solo un dashboard — es un centro operativo. Monitoreo, rutas, topología, backbone e interfaces en una sola experiencia. / Pathfinder isn't just a dashboard — it's an operations center. Monitoring, routes, topology, backbone, and interfaces in one experience."
},
{
"q": "¿Hacia dónde se dirige el equipo en su visión AIOps?",
"q": "MetricFlow es la plataforma KPI del equipo de NI. ¿Qué problema resuelve?\n\n[EN] MetricFlow is NI's KPI platform. What problem does it solve?",
"options": [
"Depender más de herramientas comerciales como ServiceNow",
"Detectar y predecir fallas antes de que el cliente las reporte",
"Reducir el equipo de ingeniería a la mitad",
"Migrar toda la red a la nube pública"
"Reemplaza los emails internos del equipo / Replaces internal team emails",
"Centraliza métricas de desempeño, capacidad y tendencias para que los dominios puedan medir cumplimiento / Centralizes performance, capacity, and trend metrics so domains can measure compliance",
"Administra las cuentas de acceso de los empleados / Manages employee access accounts",
"Genera facturas automáticas para clientes / Generates automatic invoices for customers"
],
"correct": 1,
"fun_fact": "La meta: que el técnico salga a campo con el diagnóstico ya hecho — no a diagnosticar."
"fun_fact": "MetricFlow convierte datos dispersos en KPIs por dominio — desempeño, capacidad, tendencia y cumplimiento en un solo lugar. / MetricFlow turns scattered data into domain KPIs — performance, capacity, trend, and compliance in one place."
},
{
"q": "Fiber Admin organiza el inventario físico de fibra de LCPR. ¿Qué información centraliza?\n\n[EN] Fiber Admin organizes LCPR's physical fiber inventory. What information does it centralize?",
"options": [
"Contratos de proveedores y facturas de mantenimiento / Vendor contracts and maintenance invoices",
"Horarios del personal de planta externa / External plant staff schedules",
"Hubs, puertos, conexiones y empalmes de fibra para coordinar trabajo entre dominios / Hubs, ports, connections, and fiber splices to coordinate work across domains",
"Configuraciones de routers y switches de core / Core router and switch configurations"
],
"correct": 2,
"fun_fact": "Fiber Admin convierte inventario físico en información confiable. Sin eso, coordinar cambios entre dominios depende de quién recuerda qué. / Fiber Admin turns physical inventory into reliable information. Without it, coordinating changes across domains depends on who remembers what."
},
{
"q": "Los Reportes Automatizados con AI transforman datos crudos en algo más útil. ¿Qué produce exactamente?\n\n[EN] Automated AI Reports transform raw data into something more useful. What exactly does it produce?",
"options": [
"Alertas de red en tiempo real para el NOC / Real-time network alerts for NOC",
"Backups automáticos de bases de datos / Automatic database backups",
"Resúmenes, narrativas y reportes ejecutivos listos para compartir — no solo tablas de números / Summaries, narratives, and executive reports ready to share — not just number tables",
"Configuraciones automáticas de equipos de red / Automatic network equipment configurations"
],
"correct": 2,
"fun_fact": "Un reporte que solo muestra números no explica qué cambió ni qué hacer. AI convierte métricas en comunicación accionable para cualquier audiencia. / A report that only shows numbers doesn't explain what changed or what to do. AI turns metrics into actionable communication for any audience."
},
{
"q": "Znuny es la pieza que cierra el ciclo operacional del pipeline de NI. ¿Cuál es su rol clave?\n\n[EN] Znuny is the piece that closes the operational cycle in NI's pipeline. What is its key role?",
"options": [
"Conectar detección, notificación, tarea, seguimiento y resolución — que cada señal tenga dueño y cierre / Connect detection, notification, task, follow-up, and resolution — every signal has an owner and a close",
"Almacenar backups de configuración de red / Store network configuration backups",
"Procesar pagos de nómina del equipo / Process team payroll payments",
"Monitorear el consumo eléctrico del datacenter / Monitor datacenter power consumption"
],
"correct": 0,
"fun_fact": "Znuny no es solo un sistema de tickets. Es la pieza que ata el pipeline completo: lo que se detecta termina resuelto, con historial y evidencia. / Znuny isn't just a ticketing system. It's the piece that ties the whole pipeline together: what gets detected ends up resolved, with history and evidence."
},
{
"q": "El equipo de NI también apoya dominios fuera de la red pura. ¿Cuál de estas áreas es un ejemplo de eso?\n\n[EN] NI also supports domains outside pure network work. Which of these is an example?",
"options": [
"Administración de torres celulares / Cell tower administration",
"Gestión de inventario de almacén / Warehouse inventory management",
"Soporte técnico a clientes de cable / Technical support for cable customers",
"Dashboards de BI y automatización de flujos para Construcción y PNM en Power BI y Excel / BI dashboards and workflow automation for Construction and PNM in Power BI and Excel"
],
"correct": 3,
"fun_fact": "NI no es solo red — construimos soluciones analíticas para otros dominios VPTO usando las herramientas correctas para cada caso. / NI isn't just network — we build analytical solutions for other VPTO domains using the right tools for each case."
},
{
"q": "¿Cuál es la visión del equipo para el próximo capítulo — AIOps?\n\n[EN] What is the team's vision for the next chapter — AIOps?",
"options": [
"Reemplazar a todos los ingenieros con inteligencia artificial / Replace all engineers with artificial intelligence",
"Comprar una plataforma comercial de AIOps / Purchase a commercial AIOps platform",
"Detectar y predecir fallas de red antes de que el cliente las reporte, con recomendaciones automatizadas / Detect and predict network failures before the customer reports them, with automated recommendations",
"Migrar toda la infraestructura a la nube pública / Migrate all infrastructure to public cloud"
],
"correct": 2,
"fun_fact": "La meta: el técnico sale a campo con el diagnóstico ya hecho — no a diagnosticar. La red se auto-diagnostica. / The goal: the technician goes to the field with the diagnosis already done — not to diagnose. The network self-diagnoses."
},
{
"q": "¿Qué diferencia al equipo de NI de un equipo de soporte TI tradicional?\n\n[EN] What sets the NI team apart from a traditional IT support team?",
"options": [
"NI solo atiende tickets de soporte / NI only handles support tickets",
"NI construye producto interno — herramientas, plataformas y automatización que transforman cómo opera la red / NI builds internal product — tools, platforms, and automation that transform how the network operates",
"NI administra el presupuesto de licencias de software / NI manages the software license budget",
"NI hace instalaciones físicas de equipos en campo / NI does physical equipment installations in the field"
],
"correct": 1,
"fun_fact": "El backlog de NI no es soporte — es producto interno. Cada tarea construye capacidad operacional real. / The NI backlog isn't support — it's internal product. Every task builds real operational capability."
}
]
@@ -132,8 +187,12 @@ class GameState:
self.current_q = -1
self.q_start_time = 0
self.answers_this_round = []
self.question_order = list(range(len(QUESTIONS)))
random.shuffle(self.question_order)
# 4 generals (indices 0-4) + 1 tool (indices 5-9), shuffled together
generals = random.sample(range(5), 4)
tool = random.sample(range(5, 10), 1)
combined = generals + tool
random.shuffle(combined)
self.question_order = combined
game = GameState()
@@ -142,7 +201,6 @@ class ConnectionManager:
def __init__(self):
self.players: dict = {}
self.hosts: list = []
self._reset_task: asyncio.Task = None
async def connect_player(self, ws_id: str, ws: WebSocket):
await ws.accept()
@@ -151,10 +209,6 @@ class ConnectionManager:
async def connect_host(self, ws: WebSocket):
await ws.accept()
self.hosts.append(ws)
# Cancel pending auto-reset if host reconnected in time
if self._reset_task and not self._reset_task.done():
self._reset_task.cancel()
self._reset_task = None
def disconnect(self, ws_id: str):
self.players.pop(ws_id, None)
@@ -163,16 +217,6 @@ class ConnectionManager:
def disconnect_host(self, ws: WebSocket):
if ws in self.hosts:
self.hosts.remove(ws)
# If no hosts remain and game is not in lobby, schedule auto-reset
if not self.hosts and game.phase != "lobby":
self._reset_task = asyncio.create_task(self._auto_reset(delay=300))
async def _auto_reset(self, delay: int):
"""Wait `delay` seconds, then reset the game if no host has reconnected."""
await asyncio.sleep(delay)
if not self.hosts:
game.reset()
await self.broadcast_players({"type": "phase", "phase": "lobby"})
async def send_to(self, ws_id: str, data: dict):
ws = self.players.get(ws_id)
@@ -283,7 +327,7 @@ async def player_ws(ws: WebSocket, ws_id: str):
if not player or player["answered"]:
continue
elapsed = time.time() - game.q_start_time
TIME_LIMIT = 20
TIME_LIMIT = 30
if elapsed > TIME_LIMIT:
continue
@@ -353,7 +397,7 @@ async def host_ws(ws: WebSocket):
elif msg["type"] == "next_question":
game.current_q += 1
if game.current_q >= len(QUESTIONS):
if game.current_q >= len(game.question_order):
lb = get_leaderboard()
save_session(list(game.players.values())) # ← persist to SQLite
game.phase = "podium"
@@ -371,11 +415,11 @@ async def host_ws(ws: WebSocket):
await mgr.broadcast_all({
"type": "question",
"number": game.current_q + 1,
"total": len(QUESTIONS),
"total": len(game.question_order),
"q": q["q"],
"options": q["options"],
"time_limit": 20,
"correct_idx_hint": -1 # not revealed until show_results
"time_limit": 30,
"correct_idx_hint": q["correct"]
})
elif msg["type"] == "show_results":
@@ -393,5 +437,10 @@ async def host_ws(ws: WebSocket):
game.reset()
await mgr.broadcast_all({"type": "phase", "phase": "lobby"})
elif msg["type"] == "clear_lobby":
await mgr.broadcast_players({"type": "kicked"})
game.reset()
await mgr.broadcast_hosts({"type": "lobby_update", "players": [], "count": 0})
except WebSocketDisconnect:
mgr.disconnect_host(ws)