diff --git a/README.md b/README.md new file mode 100644 index 0000000..5928690 --- /dev/null +++ b/README.md @@ -0,0 +1,344 @@ +# Trivy - Trivia Multiplayer en Tiempo Real + +
+
+
+ ¡Que gane el mejor equipo! 🏆 +
diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..a0c527f --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,45 @@ +# =========================================== +# Trivy Backend - Configuration +# =========================================== + +# Database +DATABASE_URL=postgresql+asyncpg://trivy:trivy@localhost:5432/trivy + +# Redis +REDIS_URL=redis://localhost:6379 + +# Anthropic API (Required for answer validation) +ANTHROPIC_API_KEY=sk-ant-api03-YOUR_API_KEY_HERE + +# =========================================== +# Game Settings +# =========================================== + +# Penalty multiplier when failing a steal (0.5 = lose 50% of question points) +STEAL_PENALTY_MULTIPLIER=0.5 + +# Time multiplier for steal attempts (0.5 = half the original time) +STEAL_TIME_MULTIPLIER=0.5 + +# How long rooms stay in Redis (hours) +ROOM_TTL_HOURS=3 + +# =========================================== +# Security +# =========================================== + +# CORS origins (comma-separated) +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 + +# Secret key for sessions (generate with: openssl rand -hex 32) +SECRET_KEY=your-secret-key-here + +# =========================================== +# Optional +# =========================================== + +# Log level (DEBUG, INFO, WARNING, ERROR) +LOG_LEVEL=INFO + +# Enable SQL echo (for debugging) +SQL_ECHO=false diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..fee6984 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,492 @@ +# API Reference - Trivy + +## Base URL + +``` +http://localhost:8000/api +``` + +## Autenticación + +El panel de administración usa autenticación básica HTTP. + +```bash +# Ejemplo con curl +curl -u admin:admin123 http://localhost:8000/api/admin/questions +``` + +--- + +## Endpoints REST + +### Game + +#### GET /game/categories + +Obtiene todas las categorías disponibles. + +**Response:** +```json +[ + { + "id": 1, + "name": "Nintendo", + "icon": "🍄", + "color": "#E60012" + }, + { + "id": 2, + "name": "Xbox", + "icon": "🎮", + "color": "#107C10" + } +] +``` + +#### GET /game/today-questions + +Obtiene las preguntas activas para hoy (sin respuestas). + +**Response:** +```json +{ + "1": [ + { + "id": 1, + "category_id": 1, + "question_text": "¿En qué año se lanzó la NES?", + "difficulty": 1, + "points": 100, + "time_seconds": 15 + } + ] +} +``` + +--- + +### Admin - Preguntas + +#### GET /admin/questions + +Lista todas las preguntas. + +**Query Parameters:** +- `status` (optional): `pending`, `approved`, `used` +- `category_id` (optional): ID de categoría + +**Response:** +```json +[ + { + "id": 1, + "category_id": 1, + "question_text": "¿En qué año se lanzó la NES?", + "correct_answer": "1983", + "alt_answers": ["1984"], + "difficulty": 1, + "points": 100, + "time_seconds": 15, + "date_active": "2024-01-26", + "status": "approved", + "fun_fact": "La NES salvó la industria del videojuego", + "created_at": "2024-01-25T10:00:00Z" + } +] +``` + +#### POST /admin/questions + +Crea una nueva pregunta. + +**Request Body:** +```json +{ + "category_id": 1, + "question_text": "¿Cuál es el nombre del fontanero de Nintendo?", + "correct_answer": "Mario", + "alt_answers": ["Super Mario"], + "difficulty": 1, + "points": 100, + "time_seconds": 15, + "date_active": "2024-01-26", + "status": "approved", + "fun_fact": "Mario apareció por primera vez en Donkey Kong" +} +``` + +**Response:** `201 Created` + +#### PUT /admin/questions/{id} + +Actualiza una pregunta existente. + +#### DELETE /admin/questions/{id} + +Elimina una pregunta. + +--- + +### Admin - Categorías + +#### GET /admin/categories + +Lista todas las categorías. + +#### POST /admin/categories + +Crea una nueva categoría. + +**Request Body:** +```json +{ + "name": "Ciencia", + "icon": "🔬", + "color": "#00BCD4" +} +``` + +--- + +### Admin - Salas + +#### GET /admin/rooms/active + +Obtiene las salas activas en Redis. + +**Response:** +```json +[ + { + "code": "ABC123", + "status": "playing", + "host": "Player1", + "teams": { + "A": [{"name": "Player1", "team": "A"}], + "B": [{"name": "Player2", "team": "B"}] + }, + "scores": {"A": 500, "B": 300}, + "current_team": "A" + } +] +``` + +--- + +## WebSocket Events + +### Conexión + +```javascript +import { io } from 'socket.io-client'; + +const socket = io('http://localhost:8000', { + transports: ['websocket', 'polling'] +}); +``` + +--- + +### Eventos Cliente → Servidor + +#### create_room + +Crea una nueva sala de juego. + +```javascript +socket.emit('create_room', { + player_name: 'MiNombre' +}); +``` + +#### join_room + +Unirse a una sala existente. + +```javascript +socket.emit('join_room', { + room_code: 'ABC123', + player_name: 'MiNombre', + team: 'A' // o 'B' +}); +``` + +#### change_team + +Cambiar de equipo en el lobby. + +```javascript +socket.emit('change_team', { + team: 'B' +}); +``` + +#### start_game + +Iniciar el juego (solo host). + +```javascript +socket.emit('start_game', {}); +``` + +#### select_question + +Seleccionar una pregunta del tablero. + +```javascript +socket.emit('select_question', { + question_id: 1, + category_id: 1 +}); +``` + +#### submit_answer + +Enviar respuesta a la pregunta actual. + +```javascript +socket.emit('submit_answer', { + answer: 'Mi respuesta', + question: { /* objeto pregunta */ }, + is_steal: false +}); +``` + +#### steal_decision + +Decidir si intentar robar. + +```javascript +socket.emit('steal_decision', { + attempt: true, // o false para pasar + question_id: 1, + answer: 'Mi respuesta' // solo si attempt=true +}); +``` + +#### chat_message + +Enviar mensaje al chat general. + +```javascript +socket.emit('chat_message', { + message: 'Hola a todos!' +}); +``` + +#### team_message + +Enviar mensaje al chat de equipo. + +```javascript +socket.emit('team_message', { + room_code: 'ABC123', + team: 'A', + player_name: 'MiNombre', + message: 'Mensaje privado del equipo' +}); +``` + +#### send_reaction + +Enviar reacción emoji. + +```javascript +socket.emit('send_reaction', { + emoji: '🎉', + room_code: 'ABC123', + player_name: 'MiNombre' +}); +``` + +--- + +### Eventos Servidor → Cliente + +#### room_created + +Confirmación de sala creada. + +```javascript +socket.on('room_created', (data) => { + console.log(data.room.code); // 'ABC123' +}); +``` + +**Payload:** +```json +{ + "room": { + "code": "ABC123", + "status": "waiting", + "host": "Player1", + "teams": {"A": [...], "B": []}, + "scores": {"A": 0, "B": 0} + } +} +``` + +#### player_joined + +Un jugador se unió a la sala. + +```javascript +socket.on('player_joined', (data) => { + console.log('Nuevo jugador:', data.room.teams); +}); +``` + +#### player_left + +Un jugador abandonó la sala. + +```javascript +socket.on('player_left', (data) => { + console.log('Jugador salió'); +}); +``` + +#### game_started + +El juego ha comenzado. + +```javascript +socket.on('game_started', (data) => { + // data.room.board contiene el tablero con las preguntas + console.log('Categorías:', Object.keys(data.room.board)); +}); +``` + +**Payload:** +```json +{ + "room": { + "status": "playing", + "board": { + "1": [ + { + "id": 1, + "question_text": "...", + "difficulty": 1, + "points": 100, + "answered": false + } + ] + }, + "current_team": "A", + "current_player_index": {"A": 0, "B": 0} + } +} +``` + +#### question_selected + +Se seleccionó una pregunta. + +```javascript +socket.on('question_selected', (data) => { + console.log('Pregunta:', data.question_id); +}); +``` + +#### answer_result + +Resultado de una respuesta. + +```javascript +socket.on('answer_result', (data) => { + if (data.valid) { + console.log('¡Correcto! +', data.points_earned); + } else { + console.log('Incorrecto:', data.reason); + } +}); +``` + +**Payload:** +```json +{ + "valid": true, + "reason": "Respuesta correcta", + "points_earned": 100, + "was_steal": false, + "room": { /* estado actualizado */ } +} +``` + +#### steal_prompt + +Oportunidad de robo disponible. + +```javascript +socket.on('steal_prompt', (data) => { + console.log('¡Puedes robar!'); +}); +``` + +#### game_finished + +El juego ha terminado. + +```javascript +socket.on('game_finished', (data) => { + console.log('Ganador:', data.winner); + console.log('Puntuación final:', data.room.scores); +}); +``` + +#### new_reaction + +Nueva reacción de un jugador. + +```javascript +socket.on('new_reaction', (data) => { + console.log(data.player_name, 'reaccionó con', data.emoji); +}); +``` + +#### team_message + +Mensaje de chat de equipo. + +```javascript +socket.on('team_message', (data) => { + console.log(`[Equipo] ${data.player_name}: ${data.message}`); +}); +``` + +#### error + +Error del servidor. + +```javascript +socket.on('error', (data) => { + console.error('Error:', data.message); +}); +``` + +--- + +## Códigos de Error + +| Código | Mensaje | Descripción | +|--------|---------|-------------| +| `room_not_found` | Room not found | La sala no existe | +| `room_full` | Room is full | La sala está llena | +| `not_host` | Only the host can start | Solo el host puede iniciar | +| `need_players` | Both teams need players | Ambos equipos necesitan jugadores | +| `not_your_turn` | Not your turn | No es tu turno | +| `question_answered` | Question already answered | Pregunta ya contestada | + +--- + +## Rate Limiting + +- API REST: 100 requests/minuto por IP +- WebSocket: Sin límite específico (controlado por lógica de juego) + +## Timeouts + +- Preguntas: 15-35 segundos según dificultad +- Robo: 50% del tiempo original +- Sala inactiva: 3 horas (configurable) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..952493f --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,374 @@ +# Arquitectura de Trivy + +## Visión General + +Trivy utiliza una arquitectura de microservicios con comunicación en tiempo real mediante WebSockets. + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ CLIENTES │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Browser │ │ Browser │ │ Browser │ │ Browser │ │ +│ │ React App│ │ React App│ │ React App│ │ React App│ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ │ +│ └─────────────┴──────┬──────┴─────────────┘ │ +│ │ │ +│ WebSocket/HTTP │ +└────────────────────────────┼─────────────────────────────────────────┘ + │ +┌────────────────────────────┼─────────────────────────────────────────┐ +│ BACKEND │ +│ │ │ +│ ┌─────────────────────────▼─────────────────────────────┐ │ +│ │ FastAPI + Socket.IO │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ REST API │ │ WebSocket │ │ ASGI │ │ │ +│ │ │ Endpoints │ │ Events │ │ Middleware │ │ │ +│ │ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ │ +│ └─────────┼────────────────┼────────────────────────────┘ │ +│ │ │ │ +│ ┌─────────▼────────────────▼────────────────────────────┐ │ +│ │ SERVICES │ │ +│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ +│ │ │ Room │ │ Game │ │ AI │ │ │ +│ │ │ Manager │ │ Manager │ │ Validator │ │ │ +│ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ +│ └────────┼───────────────┼───────────────┼──────────────┘ │ +│ │ │ │ │ +│ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ │ +│ │ Redis │ │ PostgreSQL│ │ Claude │ │ +│ │ (State) │ │ (Data) │ │ API │ │ +│ └───────────┘ └───────────┘ └───────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +## Componentes + +### Frontend (React + TypeScript) + +``` +frontend/src/ +├── components/ # Componentes reutilizables +│ ├── chat/ # EmojiReactions, ReactionOverlay, TeamChat +│ ├── game/ # Componentes del tablero +│ ├── lobby/ # Sala de espera +│ └── ui/ # SoundControl, botones, inputs +├── hooks/ # Custom hooks +│ ├── useSocket.ts # Gestión de WebSocket +│ └── useSound.ts # Gestión de audio +├── pages/ # Rutas principales +│ ├── Home.tsx # Crear/unir sala +│ ├── Lobby.tsx # Sala de espera +│ ├── Game.tsx # Tablero de juego +│ ├── Results.tsx # Resultados finales +│ └── admin/ # Panel de administración +├── services/ +│ └── socket.ts # Singleton de Socket.IO +├── stores/ # Estado global (Zustand) +│ ├── gameStore.ts # Estado del juego +│ ├── themeStore.ts # Tema visual +│ └── soundStore.ts # Configuración de audio +├── themes/ # Definiciones de temas +└── types/ # Tipos TypeScript +``` + +### Backend (FastAPI + Python) + +``` +backend/app/ +├── api/ # Endpoints REST +│ ├── admin.py # CRUD de preguntas/categorías +│ ├── auth.py # Autenticación admin +│ └── game.py # Datos públicos del juego +├── models/ # Modelos SQLAlchemy +│ ├── question.py # Preguntas +│ ├── category.py # Categorías +│ ├── admin.py # Administradores +│ └── game_session.py # Sesiones de juego +├── schemas/ # Schemas Pydantic +├── services/ # Lógica de negocio +│ ├── room_manager.py # Gestión de salas (Redis) +│ ├── game_manager.py # Lógica del juego +│ ├── question_service.py # Carga de preguntas +│ └── ai_validator.py # Validación con Claude +├── sockets/ # Eventos WebSocket +│ └── game_events.py # Todos los eventos del juego +├── config.py # Configuración +└── main.py # Punto de entrada ASGI +``` + +## Flujo de Datos + +### 1. Creación de Sala + +``` +Cliente Backend Redis + │ │ │ + │──── create_room ────────>│ │ + │ │ │ + │ │──── SETEX room:CODE ────>│ + │ │ │ + │ │<─── OK ─────────────────│ + │ │ │ + │<─── room_created ────────│ │ + │ │ │ +``` + +### 2. Unirse a Sala + +``` +Cliente Backend Redis + │ │ │ + │──── join_room ──────────>│ │ + │ │ │ + │ │──── GET room:CODE ──────>│ + │ │<─── room_data ──────────│ + │ │ │ + │ │──── SETEX (updated) ────>│ + │ │ │ + │<─── player_joined ───────│ │ + │ │ │ + │ │──── emit to room ───────>│ (otros clientes) +``` + +### 3. Inicio del Juego + +``` +Cliente Backend PostgreSQL + │ │ │ + │──── start_game ─────────>│ │ + │ │ │ + │ │──── SELECT questions ───>│ + │ │ (5 random cats) │ + │ │<─── questions ──────────│ + │ │ │ + │ │──── SELECT 1 per diff ──>│ + │ │<─── board data ─────────│ + │ │ │ + │<─── game_started ────────│ │ + │ (with board) │ │ +``` + +### 4. Responder Pregunta + +``` +Cliente Backend Claude API + │ │ │ + │──── submit_answer ──────>│ │ + │ │ │ + │ │──── validate_answer ────>│ + │ │ (question, answer) │ + │ │ │ + │ │<─── {valid, reason} ────│ + │ │ │ + │<─── answer_result ───────│ │ + │ │ │ +``` + +## Estado del Juego + +### Redis (Estado en Tiempo Real) + +```json +{ + "room:ABC123": { + "code": "ABC123", + "status": "playing", + "host": "Player1", + "teams": { + "A": [{"name": "Player1", "team": "A", "socket_id": "..."}], + "B": [{"name": "Player2", "team": "B", "socket_id": "..."}] + }, + "current_team": "A", + "current_player_index": {"A": 0, "B": 0}, + "current_question": null, + "can_steal": false, + "scores": {"A": 500, "B": 300}, + "board": { + "1": [/* preguntas categoría 1 */], + "3": [/* preguntas categoría 3 */] + } + }, + "player:socket_id": { + "room": "ABC123", + "name": "Player1", + "team": "A" + } +} +``` + +### PostgreSQL (Datos Persistentes) + +```sql +-- Categorías +categories (id, name, icon, color) + +-- Preguntas (pool de 200) +questions ( + id, category_id, question_text, correct_answer, + alt_answers[], difficulty, points, time_seconds, + date_active, status, fun_fact +) + +-- Sesiones de juego (historial) +game_sessions ( + id, room_code, status, + team_a_score, team_b_score, + questions_used[], created_at, finished_at +) + +-- Eventos de juego (analytics) +game_events ( + id, session_id, event_type, player_name, + question_id, data, created_at +) + +-- Administradores +admins (id, username, password_hash) +``` + +## Validación de Respuestas con IA + +El sistema usa Claude para validación flexible de respuestas: + +```python +# ai_validator.py +async def validate_answer( + question: str, + correct_answer: str, + alt_answers: list, + player_answer: str +) -> dict: + prompt = f""" + Pregunta: {question} + Respuesta correcta: {correct_answer} + Respuestas alternativas: {alt_answers} + Respuesta del jugador: {player_answer} + + ¿La respuesta del jugador es correcta? + Considera sinónimos, abreviaciones, errores menores de ortografía. + + Responde en JSON: {{"valid": bool, "reason": "explicación"}} + """ + + response = await anthropic.messages.create( + model="claude-3-haiku-20240307", + messages=[{"role": "user", "content": prompt}] + ) + + return json.loads(response.content[0].text) +``` + +## Selección de Preguntas + +Cada partida selecciona aleatoriamente: + +1. **5 categorías** de las 8 disponibles +2. **1 pregunta por dificultad** de las 5 opciones disponibles + +```python +# question_service.py +async def get_board_for_game(db, target_date): + # 1. Obtener todas las preguntas del día + full_board = await get_daily_questions(db, target_date) + + # 2. Seleccionar 5 categorías aleatorias + selected_cats = random.sample(list(full_board.keys()), 5) + + # 3. Para cada categoría, seleccionar 1 pregunta por dificultad + game_board = {} + for cat_id in selected_cats: + questions_by_diff = group_by_difficulty(full_board[cat_id]) + selected = [] + for diff in range(1, 6): + if diff in questions_by_diff: + selected.append(random.choice(questions_by_diff[diff])) + game_board[cat_id] = selected + + return game_board +``` + +## Temas y Sonidos + +### Estructura de Temas + +```typescript +// themes/drrr.ts +export const drrrTheme: ThemeConfig = { + name: 'drrr', + displayName: 'DRRR Style', + colors: { + primary: '#FFD93D', + secondary: '#6BCB77', + accent: '#FF6B6B', + bg: '#1a1a2e', + text: '#EAEAEA', + textMuted: '#888888' + }, + fonts: { + heading: '"Press Start 2P", monospace', + body: 'Inter, sans-serif' + } +} +``` + +### Estructura de Sonidos + +``` +public/sounds/ +├── drrr/ +│ ├── correct.mp3 +│ ├── incorrect.mp3 +│ ├── select.mp3 +│ └── ... +├── retro/ +├── minimal/ +├── rgb/ +└── anime/ +``` + +Cada tema tiene sus propios archivos de audio, con fallback a sonidos generados por Web Audio API si no existen. + +## Escalabilidad + +### Horizontal Scaling + +``` + ┌─────────────┐ + │ NGINX │ + │ (LB) │ + └──────┬──────┘ + │ + ┌─────────────────┼─────────────────┐ + │ │ │ + ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ + │ Backend │ │ Backend │ │ Backend │ + │ Node 1 │ │ Node 2 │ │ Node 3 │ + └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ + │ │ │ + └────────────────┼────────────────┘ + │ + ┌───────────┴───────────┐ + │ │ + ┌─────▼─────┐ ┌─────▼─────┐ + │ Redis │ │ PostgreSQL│ + │ Cluster │ │ Primary │ + └───────────┘ └───────────┘ +``` + +### Consideraciones + +1. **Socket.IO con Redis Adapter** - Para sincronizar eventos entre múltiples instancias +2. **Sticky Sessions** - Para mantener conexiones WebSocket +3. **PostgreSQL Read Replicas** - Para queries de lectura +4. **Redis Cluster** - Para alta disponibilidad del estado + +## Seguridad + +1. **CORS** - Configurado para dominios permitidos +2. **Rate Limiting** - En endpoints REST +3. **Input Validation** - Pydantic schemas +4. **SQL Injection** - Prevenido por SQLAlchemy ORM +5. **XSS** - React escapa contenido por defecto +6. **WebSocket Auth** - Validación de sala/jugador en cada evento diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..1369549 --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,469 @@ +# Guía de Instalación - Trivy + +## Requisitos Previos + +### Software Requerido + +| Software | Versión Mínima | Notas | +|----------|----------------|-------| +| Python | 3.11+ | Con pip | +| Node.js | 18+ | Con npm | +| PostgreSQL | 15+ | O Docker | +| Redis | 7+ | O Docker | +| Git | 2.0+ | Para clonar | + +### API Keys + +- **Anthropic Claude API** - Obtener en https://console.anthropic.com + +## Opción 1: Instalación con Docker (Recomendado) + +### 1. Clonar repositorio + +```bash +git clone https://gitea.local/frank/Trivy.git +cd Trivy +``` + +### 2. Configurar variables de entorno + +```bash +# Backend +cp backend/.env.example backend/.env + +# Editar backend/.env: +DATABASE_URL=postgresql+asyncpg://trivy:trivy@db:5432/trivy +REDIS_URL=redis://redis:6379 +ANTHROPIC_API_KEY=sk-ant-api03-YOUR_KEY_HERE + +# Frontend +cp frontend/.env.example frontend/.env + +# Editar frontend/.env: +VITE_API_URL=http://localhost:8000 +VITE_WS_URL=http://localhost:8000 +``` + +### 3. Iniciar con Docker Compose + +```bash +docker-compose up -d +``` + +Esto iniciará: +- PostgreSQL en puerto 5432 +- Redis en puerto 6379 +- Backend en puerto 8000 +- Frontend en puerto 3000 + +### 4. Ejecutar migraciones + +```bash +docker-compose exec backend alembic upgrade head +``` + +### 5. Crear datos iniciales + +```bash +docker-compose exec backend python scripts/seed_data.py +``` + +### 6. Acceder + +- Frontend: http://localhost:3000 +- Admin: http://localhost:3000/admin (admin/admin123) +- API: http://localhost:8000/docs + +--- + +## Opción 2: Instalación Manual + +### 1. Clonar repositorio + +```bash +git clone https://gitea.local/frank/Trivy.git +cd Trivy +``` + +### 2. Configurar PostgreSQL + +```bash +# Crear usuario y base de datos +sudo -u postgres psql + +CREATE USER trivy WITH PASSWORD 'trivy'; +CREATE DATABASE trivy OWNER trivy; +GRANT ALL PRIVILEGES ON DATABASE trivy TO trivy; +\q +``` + +### 3. Configurar Redis + +```bash +# Ubuntu/Debian +sudo apt install redis-server +sudo systemctl start redis +sudo systemctl enable redis + +# Verificar +redis-cli ping +# Respuesta: PONG +``` + +### 4. Configurar Backend + +```bash +cd backend + +# Crear entorno virtual +python3 -m venv venv +source venv/bin/activate + +# Instalar dependencias +pip install -r requirements.txt + +# Configurar variables de entorno +cp .env.example .env +``` + +Editar `backend/.env`: + +```env +# Base de datos +DATABASE_URL=postgresql+asyncpg://trivy:trivy@localhost:5432/trivy + +# Redis +REDIS_URL=redis://localhost:6379 + +# Anthropic API +ANTHROPIC_API_KEY=sk-ant-api03-YOUR_KEY_HERE + +# Configuración del juego +STEAL_PENALTY_MULTIPLIER=0.5 +STEAL_TIME_MULTIPLIER=0.5 +ROOM_TTL_HOURS=3 + +# CORS (separar con comas) +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 +``` + +### 5. Ejecutar migraciones + +```bash +cd backend +source venv/bin/activate +alembic upgrade head +``` + +### 6. Crear usuario admin + +```bash +python -c " +import asyncio +from app.models.base import get_async_session +from app.models.admin import Admin + +async def create_admin(): + AsyncSession = get_async_session() + async with AsyncSession() as db: + admin = Admin(username='admin') + admin.set_password('admin123') + db.add(admin) + await db.commit() + print('Admin creado: admin/admin123') + +asyncio.run(create_admin()) +" +``` + +### 7. Crear categorías + +```bash +python -c " +import asyncio +from app.models.base import get_async_session +from app.models.category import Category + +categories = [ + ('Nintendo', '🍄', '#E60012'), + ('Xbox', '🎮', '#107C10'), + ('PlayStation', '🎯', '#003791'), + ('Anime', '⛩️', '#FF6B9D'), + ('Música', '🎵', '#1DB954'), + ('Películas', '🎬', '#F5C518'), + ('Libros', '📚', '#8B4513'), + ('Historia', '🏛️', '#6B5B95'), +] + +async def create_categories(): + AsyncSession = get_async_session() + async with AsyncSession() as db: + for name, icon, color in categories: + cat = Category(name=name, icon=icon, color=color) + db.add(cat) + await db.commit() + print(f'{len(categories)} categorías creadas') + +asyncio.run(create_categories()) +" +``` + +### 8. Crear preguntas (ejecutar script completo) + +```bash +python scripts/seed_questions.py +``` + +### 9. Configurar Frontend + +```bash +cd frontend + +# Instalar dependencias +npm install + +# Configurar variables de entorno +cp .env.example .env +``` + +Editar `frontend/.env`: + +```env +VITE_API_URL=http://localhost:8000 +VITE_WS_URL=http://localhost:8000 +``` + +### 10. Iniciar servicios + +**Terminal 1 - Backend:** +```bash +cd backend +source venv/bin/activate +uvicorn app.main:socket_app --host 0.0.0.0 --port 8000 --reload +``` + +**Terminal 2 - Frontend:** +```bash +cd frontend +npm run dev -- --host +``` + +### 11. Acceder + +- Frontend: http://localhost:3000 +- Admin: http://localhost:3000/admin +- API Docs: http://localhost:8000/docs + +--- + +## Configuración para Red Local + +Para permitir acceso desde otros dispositivos en la red: + +### 1. Obtener IP local + +```bash +ip addr show | grep "inet " | grep -v 127.0.0.1 +# Ejemplo: 192.168.1.100 +``` + +### 2. Actualizar configuración + +**backend/.env:** +```env +CORS_ORIGINS=http://localhost:3000,http://192.168.1.100:3000 +``` + +**frontend/.env:** +```env +VITE_API_URL=http://192.168.1.100:8000 +VITE_WS_URL=http://192.168.1.100:8000 +``` + +### 3. Iniciar con host 0.0.0.0 + +```bash +# Backend +uvicorn app.main:socket_app --host 0.0.0.0 --port 8000 + +# Frontend +npm run dev -- --host +``` + +### 4. Acceder desde otros dispositivos + +- http://192.168.1.100:3000 + +--- + +## Producción + +### Backend con Gunicorn + +```bash +pip install gunicorn + +gunicorn app.main:socket_app \ + -w 4 \ + -k uvicorn.workers.UvicornWorker \ + -b 0.0.0.0:8000 \ + --access-logfile - \ + --error-logfile - +``` + +### Frontend Build + +```bash +cd frontend +npm run build + +# Servir con cualquier servidor estático +# Ejemplo con serve: +npm install -g serve +serve -s dist -l 3000 +``` + +### Nginx como Reverse Proxy + +```nginx +# /etc/nginx/sites-available/trivy +server { + listen 80; + server_name trivy.example.com; + + # Frontend + location / { + root /var/www/trivy/frontend/dist; + try_files $uri $uri/ /index.html; + } + + # API + location /api { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # WebSocket + location /socket.io { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } +} +``` + +### Systemd Service + +```ini +# /etc/systemd/system/trivy-backend.service +[Unit] +Description=Trivy Backend +After=network.target postgresql.service redis.service + +[Service] +Type=simple +User=www-data +WorkingDirectory=/var/www/trivy/backend +Environment=PATH=/var/www/trivy/backend/venv/bin +ExecStart=/var/www/trivy/backend/venv/bin/gunicorn app.main:socket_app -w 4 -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl enable trivy-backend +sudo systemctl start trivy-backend +``` + +--- + +## Solución de Problemas + +### Error: "Could not connect to Redis" + +```bash +# Verificar Redis +redis-cli ping + +# Si no responde, iniciar Redis +sudo systemctl start redis +``` + +### Error: "Database connection failed" + +```bash +# Verificar PostgreSQL +sudo systemctl status postgresql + +# Verificar credenciales +psql -U trivy -d trivy -h localhost +``` + +### Error: "CORS blocked" + +Verificar que la URL del frontend esté en `CORS_ORIGINS` del backend. + +### WebSocket no conecta + +1. Verificar que `VITE_WS_URL` use `http://` (no `ws://`) +2. Verificar que el backend use `socket_app` (no `app`) +3. Verificar firewall/puertos + +### Preguntas no cargan + +```bash +# Verificar preguntas en BD +cd backend +source venv/bin/activate +python -c " +import asyncio +from sqlalchemy import select +from app.models.base import get_async_session +from app.models.question import Question +from datetime import date + +async def check(): + AsyncSession = get_async_session() + async with AsyncSession() as db: + result = await db.execute( + select(Question).where(Question.date_active == date.today()) + ) + qs = result.scalars().all() + print(f'Preguntas para hoy: {len(qs)}') + +asyncio.run(check()) +" +``` + +### Sonidos no funcionan + +Los sonidos requieren archivos MP3 en `/public/sounds/{tema}/`. Si no existen, el sistema usa fallbacks generados por Web Audio API (tonos simples). + +--- + +## Actualizaciones + +```bash +cd Trivy +git pull origin main + +# Backend +cd backend +source venv/bin/activate +pip install -r requirements.txt +alembic upgrade head + +# Frontend +cd frontend +npm install +npm run build + +# Reiniciar servicios +sudo systemctl restart trivy-backend +```