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.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
import json, time, random, sqlite3, os, asyncio import json, time, random, sqlite3, os
from datetime import datetime from datetime import datetime
DB_PATH = os.environ.get("DB_PATH", "/data/quiz.db") DB_PATH = os.environ.get("DB_PATH", "/data/quiz.db")
@@ -65,59 +65,114 @@ templates = Jinja2Templates(directory="templates")
# ── QUESTIONS ───────────────────────────────────────────────────────────────── # ── QUESTIONS ─────────────────────────────────────────────────────────────────
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": [ "options": [
"Vender licencias de software a otras empresas", "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",
"Construir herramientas propias que empoderan a los equipos operacionales", "Para ahorrar en hardware de servidores / To save on server hardware",
"Administrar el presupuesto de TI de LCPR", "Porque las herramientas comerciales no existen para redes / Commercial tools don't exist for networks",
"Instalar equipos físicos en planta externa" "Es un requisito del contrato de Liberty / It's a Liberty contract requirement"
], ],
"correct": 1, "correct": 0,
"fun_fact": "¡Exacto! Construimos para quienes operan la red día a día — no solo dashboards bonitos." "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": [ "options": [
"La pantalla se veía más bonita", "Solo para gerencia ejecutiva / Only for executive management",
"Eliminamos la dependencia de licencias costosas y ganamos control total", "Para clientes residenciales de Liberty / For Liberty residential customers",
"Fue más fácil de instalar en los servidores", "Para los equipos de NOC, ingeniería, operaciones y técnicos de campo / For NOC, engineering, operations, and field technician teams",
"Los colores del dashboard coincidían con los de Liberty" "Para el equipo de ventas B2B / For the B2B sales team"
],
"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"
], ],
"correct": 2, "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": [ "options": [
"Actualizar manualmente 10 modems por semana", "Gestionar el presupuesto de proyectos de red / Manage the network project budget",
"Contratar más técnicos para visitas a domicilio", "Reemplazar el sistema de ticketing de la empresa / Replace the company ticketing system",
"Automatizar la actualización masiva de modems sin intervención humana", "Automatizar facturación a clientes B2B / Automate billing for B2B customers",
"Comprar equipos nuevos para reemplazar los modems viejos" "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, "correct": 3,
"fun_fact": "400+ modems actualizados en batches automáticos. Antes era trabajo manual uno por uno." "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": [ "options": [
"Depender más de herramientas comerciales como ServiceNow", "Reemplaza los emails internos del equipo / Replaces internal team emails",
"Detectar y predecir fallas antes de que el cliente las reporte", "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",
"Reducir el equipo de ingeniería a la mitad", "Administra las cuentas de acceso de los empleados / Manages employee access accounts",
"Migrar toda la red a la nube pública" "Genera facturas automáticas para clientes / Generates automatic invoices for customers"
], ],
"correct": 1, "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.current_q = -1
self.q_start_time = 0 self.q_start_time = 0
self.answers_this_round = [] self.answers_this_round = []
self.question_order = list(range(len(QUESTIONS))) # 4 generals (indices 0-4) + 1 tool (indices 5-9), shuffled together
random.shuffle(self.question_order) 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() game = GameState()
@@ -142,7 +201,6 @@ class ConnectionManager:
def __init__(self): def __init__(self):
self.players: dict = {} self.players: dict = {}
self.hosts: list = [] self.hosts: list = []
self._reset_task: asyncio.Task = None
async def connect_player(self, ws_id: str, ws: WebSocket): async def connect_player(self, ws_id: str, ws: WebSocket):
await ws.accept() await ws.accept()
@@ -151,10 +209,6 @@ class ConnectionManager:
async def connect_host(self, ws: WebSocket): async def connect_host(self, ws: WebSocket):
await ws.accept() await ws.accept()
self.hosts.append(ws) 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): def disconnect(self, ws_id: str):
self.players.pop(ws_id, None) self.players.pop(ws_id, None)
@@ -163,16 +217,6 @@ class ConnectionManager:
def disconnect_host(self, ws: WebSocket): def disconnect_host(self, ws: WebSocket):
if ws in self.hosts: if ws in self.hosts:
self.hosts.remove(ws) 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): async def send_to(self, ws_id: str, data: dict):
ws = self.players.get(ws_id) 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"]: if not player or player["answered"]:
continue continue
elapsed = time.time() - game.q_start_time elapsed = time.time() - game.q_start_time
TIME_LIMIT = 20 TIME_LIMIT = 30
if elapsed > TIME_LIMIT: if elapsed > TIME_LIMIT:
continue continue
@@ -353,7 +397,7 @@ async def host_ws(ws: WebSocket):
elif msg["type"] == "next_question": elif msg["type"] == "next_question":
game.current_q += 1 game.current_q += 1
if game.current_q >= len(QUESTIONS): if game.current_q >= len(game.question_order):
lb = get_leaderboard() lb = get_leaderboard()
save_session(list(game.players.values())) # ← persist to SQLite save_session(list(game.players.values())) # ← persist to SQLite
game.phase = "podium" game.phase = "podium"
@@ -371,11 +415,11 @@ async def host_ws(ws: WebSocket):
await mgr.broadcast_all({ await mgr.broadcast_all({
"type": "question", "type": "question",
"number": game.current_q + 1, "number": game.current_q + 1,
"total": len(QUESTIONS), "total": len(game.question_order),
"q": q["q"], "q": q["q"],
"options": q["options"], "options": q["options"],
"time_limit": 20, "time_limit": 30,
"correct_idx_hint": -1 # not revealed until show_results "correct_idx_hint": q["correct"]
}) })
elif msg["type"] == "show_results": elif msg["type"] == "show_results":
@@ -393,5 +437,10 @@ async def host_ws(ws: WebSocket):
game.reset() game.reset()
await mgr.broadcast_all({"type": "phase", "phase": "lobby"}) 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: except WebSocketDisconnect:
mgr.disconnect_host(ws) mgr.disconnect_host(ws)