docs: añade documentación completa del proyecto

- README.md: descripción general, stack, instalación rápida
- docs/API.md: referencia completa de API REST y WebSocket
- docs/ARCHITECTURE.md: arquitectura del sistema con diagramas
- docs/INSTALLATION.md: guía detallada de instalación
- backend/.env.example: plantilla de configuración

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 23:50:34 +00:00
parent ab201e113a
commit 6248037b47
5 changed files with 1724 additions and 0 deletions

492
docs/API.md Normal file
View File

@@ -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)

374
docs/ARCHITECTURE.md Normal file
View File

@@ -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

469
docs/INSTALLATION.md Normal file
View File

@@ -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
```