From 543d6bc6af33eb8608e6566ca703455adc743887 Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Thu, 6 Nov 2025 23:15:23 -0700 Subject: [PATCH] feat: update studio-panel and broadcast-panel configurations - Changed development port for studio-panel from 3001 to 3020 in package.json and vite.config.ts. - Refactored Studio component in studio-panel to improve token handling and error diagnostics. - Added utility functions for token validation and JWT decoding in Studio component. - Enhanced error handling and user feedback in Studio component when token is invalid. - Implemented sessionStorage management for token and server URL in Studio component. - Created Docker setup for broadcast-panel including Dockerfile, Dockerfile.dev, and docker-compose.yml. - Added Nginx configuration for serving the broadcast-panel as a Single Page Application. - Introduced Banner component in broadcast-panel for displaying messages and actions. - Added start-docker.sh script for easy Docker management of broadcast-panel. - Implemented Playwright E2E tests for token handling and UI interactions between broadcast-panel and studio-panel. - Included SSL certificates for local development in studio-panel. --- package-lock.json | 72 +++++- package.json | 1 + packages/broadcast-panel/.dockerignore | 11 + packages/broadcast-panel/.env.example | 7 + packages/broadcast-panel/DOCKER_README.md | 201 +++++++++++++++++ packages/broadcast-panel/Dockerfile | 30 +++ packages/broadcast-panel/Dockerfile.dev | 25 +++ packages/broadcast-panel/docker-compose.yml | 48 ++++ packages/broadcast-panel/nginx.conf | 34 +++ packages/broadcast-panel/package.json | 9 +- .../broadcast-panel/src/components/Banner.tsx | 42 ++++ .../components/TransmissionsTable.module.css | 20 ++ .../src/components/TransmissionsTable.tsx | 48 +++- packages/broadcast-panel/start-docker.sh | 35 +++ packages/broadcast-panel/vite-dev.log | 15 ++ packages/broadcast-panel/vite.config.ts | 41 +++- packages/studio-panel/package.json | 4 +- .../studio-panel/src/components/Studio.tsx | 209 ++++++++++++++---- packages/studio-panel/ssl/localhost.crt | 20 ++ packages/studio-panel/ssl/localhost.csr | 15 ++ packages/studio-panel/ssl/localhost.key | 28 +++ packages/studio-panel/vite-dev.log | 25 +++ packages/studio-panel/vite.config.ts | 5 +- scripts/e2e/playwright-test.js | 75 +++++++ 24 files changed, 955 insertions(+), 65 deletions(-) create mode 100644 packages/broadcast-panel/.dockerignore create mode 100644 packages/broadcast-panel/.env.example create mode 100644 packages/broadcast-panel/DOCKER_README.md create mode 100644 packages/broadcast-panel/Dockerfile create mode 100644 packages/broadcast-panel/Dockerfile.dev create mode 100644 packages/broadcast-panel/docker-compose.yml create mode 100644 packages/broadcast-panel/nginx.conf create mode 100644 packages/broadcast-panel/src/components/Banner.tsx create mode 100755 packages/broadcast-panel/start-docker.sh create mode 100644 packages/broadcast-panel/vite-dev.log create mode 100644 packages/studio-panel/ssl/localhost.crt create mode 100644 packages/studio-panel/ssl/localhost.csr create mode 100644 packages/studio-panel/ssl/localhost.key create mode 100644 packages/studio-panel/vite-dev.log create mode 100644 scripts/e2e/playwright-test.js diff --git a/package-lock.json b/package-lock.json index c7f5797..82a399a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ }, "devDependencies": { "concurrently": "^8.2.2", + "playwright": "^1.38.0", "typescript": "^5.2.2" }, "engines": { @@ -1285,6 +1286,50 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@prisma/client": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", @@ -7538,33 +7583,33 @@ } }, "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", + "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", "dev": true, "dependencies": { - "playwright-core": "1.56.1" + "playwright-core": "1.38.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=16" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", + "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=16" } }, "node_modules/playwright/node_modules/fsevents": { @@ -11852,10 +11897,17 @@ "@livekit/components-styles": "^1.1.6", "livekit-client": "^2.15.14", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-icons": "^5.5.0" }, "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.24", + "tailwindcss": "^3.4.16", + "typescript": "^5.0.2", "vite": "^7.2.0" } }, diff --git a/package.json b/package.json index 10f39d5..138767b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "devDependencies": { "concurrently": "^8.2.2", + "playwright": "^1.38.0", "typescript": "^5.2.2" }, "engines": { diff --git a/packages/broadcast-panel/.dockerignore b/packages/broadcast-panel/.dockerignore new file mode 100644 index 0000000..c3a0675 --- /dev/null +++ b/packages/broadcast-panel/.dockerignore @@ -0,0 +1,11 @@ +node_modules +dist +.git +.gitignore +*.md +.env.local +.env.*.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store diff --git a/packages/broadcast-panel/.env.example b/packages/broadcast-panel/.env.example new file mode 100644 index 0000000..ccb0526 --- /dev/null +++ b/packages/broadcast-panel/.env.example @@ -0,0 +1,7 @@ +# LiveKit Configuration +VITE_LIVEKIT_WS_URL=wss://livekit-server.bfzqqk.easypanel.host +VITE_TOKEN_SERVER_URL=http://localhost:3010 + +# Application Configuration +VITE_APP_NAME=AvanzaCast Broadcast Panel +VITE_APP_VERSION=1.0.0 diff --git a/packages/broadcast-panel/DOCKER_README.md b/packages/broadcast-panel/DOCKER_README.md new file mode 100644 index 0000000..efe1242 --- /dev/null +++ b/packages/broadcast-panel/DOCKER_README.md @@ -0,0 +1,201 @@ +# 🐳 Docker Setup - Broadcast Panel + +## Archivos Docker Creados + +- **Dockerfile**: Imagen de producción con Nginx +- **Dockerfile.dev**: Imagen de desarrollo con Vite HMR +- **docker-compose.yml**: Orquestación de contenedores +- **nginx.conf**: Configuración de Nginx para SPA +- **.dockerignore**: Archivos excluidos del build +- **start-docker.sh**: Script de inicio rápido + +## 🚀 Inicio Rápido + +### Modo Desarrollo (Recomendado para trabajar) +```bash +cd packages/broadcast-panel +chmod +x start-docker.sh +./start-docker.sh dev +``` + +Accede a: **http://localhost:5173** + +### Modo Producción +```bash +./start-docker.sh prod +``` + +Accede a: **http://localhost:8080** + +## 📦 Comandos Manuales + +### Desarrollo +```bash +# Build +docker-compose build broadcast-panel-dev + +# Iniciar +docker-compose up broadcast-panel-dev + +# Iniciar en background +docker-compose up -d broadcast-panel-dev + +# Ver logs +docker-compose logs -f broadcast-panel-dev + +# Detener +docker-compose down +``` + +### Producción +```bash +# Build +docker-compose --profile production build broadcast-panel-prod + +# Iniciar +docker-compose --profile production up broadcast-panel-prod + +# Iniciar en background +docker-compose --profile production up -d broadcast-panel-prod +``` + +## 🔧 Configuración + +### Variables de Entorno + +Copia `.env.example` a `.env` y ajusta las variables: + +```env +VITE_LIVEKIT_WS_URL=wss://livekit-server.bfzqqk.easypanel.host +VITE_TOKEN_SERVER_URL=http://localhost:3010 +``` + +### Puertos + +- **5173**: Vite Dev Server (desarrollo) +- **8080**: Nginx (producción) + +## 🌐 Conexión con LiveKit + +El broadcast-panel se conectará a: + +1. **Servidor LiveKit**: `wss://livekit-server.bfzqqk.easypanel.host` +2. **Token Server**: `http://localhost:3010` (Docker en tu máquina) + +### Token Server + +Asegúrate de que el token server esté corriendo: + +```bash +cd packages/backend-api +docker-compose up -d +``` + +O si ya está corriendo con Docker, verifica: + +```bash +docker ps | grep token-server +curl http://localhost:3010/api/token?room=test&username=demo +``` + +## 🔄 Hot Reload (Modo Desarrollo) + +Los cambios en estos archivos se reflejan automáticamente: +- `src/**/*` +- `public/**/*` +- `index.html` +- `vite.config.ts` +- `tsconfig.json` + +## 🐛 Troubleshooting + +### Error: No se puede conectar al token server + +```bash +# Verifica que el token server esté corriendo +docker ps + +# Verifica la URL en .env +cat .env | grep TOKEN_SERVER +``` + +### Error: No se puede conectar a LiveKit + +```bash +# Verifica la URL de LiveKit en .env +cat .env | grep LIVEKIT + +# Prueba la conexión +curl -I https://livekit-server.bfzqqk.easypanel.host +``` + +### Rebuild completo + +```bash +# Detener y eliminar contenedores +docker-compose down + +# Eliminar imágenes +docker-compose rm -f + +# Rebuild desde cero +docker-compose build --no-cache broadcast-panel-dev +docker-compose up broadcast-panel-dev +``` + +## 📊 Logs + +```bash +# Ver logs en tiempo real +docker-compose logs -f broadcast-panel-dev + +# Ver últimas 100 líneas +docker-compose logs --tail=100 broadcast-panel-dev +``` + +## 🧹 Limpieza + +```bash +# Detener contenedores +docker-compose down + +# Eliminar volúmenes (cuidado, borra datos) +docker-compose down -v + +# Eliminar imágenes +docker rmi avanzacast-broadcast-panel-dev +docker rmi avanzacast-broadcast-panel-prod +``` + +## 🔗 Integración con Studio-Panel + +Para que **studio-panel** se conecte con **broadcast-panel**: + +1. Inicia broadcast-panel en Docker: + ```bash + ./start-docker.sh dev + ``` + +2. En studio-panel, asegúrate de que esté en modo real (no demo): + - El token server debe estar accesible en `localhost:3010` + - Studio-panel intentará obtener un token real + - Se conectará a LiveKit + +3. Abre ambos: + - Broadcast Panel: http://localhost:5173 + - Studio Panel: http://localhost:3001 + +4. Desde broadcast-panel, crea una sala y genera un link de invitación +5. Abre ese link en studio-panel para unirte a la sala + +## 🎯 Siguiente Paso + +Una vez que broadcast-panel esté corriendo en Docker, podrás: + +1. ✅ Crear salas en broadcast-panel +2. ✅ Generar tokens de invitación +3. ✅ Conectar studio-panel a esas salas +4. ✅ Ver video/audio en tiempo real entre ambos +5. ✅ Probar todas las funcionalidades de LiveKit + +¡Disfruta tu streaming en vivo! 🎥 diff --git a/packages/broadcast-panel/Dockerfile b/packages/broadcast-panel/Dockerfile new file mode 100644 index 0000000..553c8fa --- /dev/null +++ b/packages/broadcast-panel/Dockerfile @@ -0,0 +1,30 @@ +# Dockerfile para Broadcast Panel +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copiar package files +COPY package*.json ./ + +# Instalar dependencias +RUN npm install + +# Copiar código fuente +COPY . . + +# Build +RUN npm run build + +# Etapa de producción con nginx +FROM nginx:alpine + +# Copiar build a nginx +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copiar configuración de nginx +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Exponer puerto +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/packages/broadcast-panel/Dockerfile.dev b/packages/broadcast-panel/Dockerfile.dev new file mode 100644 index 0000000..aa45dd8 --- /dev/null +++ b/packages/broadcast-panel/Dockerfile.dev @@ -0,0 +1,25 @@ +# Dockerfile de desarrollo para Broadcast Panel +FROM node:20-alpine + +WORKDIR /app + +# Instalar dependencias del sistema +RUN apk add --no-cache git + +# Copiar tsconfig.json base desde la raíz del monorepo +COPY tsconfig.json /tsconfig.json + +# Copiar shared folder (necesario para @shared alias) +COPY shared /shared + +# Copiar package files +COPY packages/broadcast-panel/package*.json ./ + +# Instalar dependencias +RUN npm install + +# Exponer puerto de Vite +EXPOSE 5173 + +# Comando para desarrollo +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/packages/broadcast-panel/docker-compose.yml b/packages/broadcast-panel/docker-compose.yml new file mode 100644 index 0000000..bfbc8cf --- /dev/null +++ b/packages/broadcast-panel/docker-compose.yml @@ -0,0 +1,48 @@ +services: + # Broadcast Panel en modo desarrollo + broadcast-panel-dev: + build: + context: ../.. + dockerfile: packages/broadcast-panel/Dockerfile.dev + container_name: avanzacast-broadcast-panel-dev + ports: + - "5173:5173" + volumes: + # Montar código fuente para hot reload + - ../../packages/broadcast-panel/src:/app/src:delegated + - ../../packages/broadcast-panel/public:/app/public:delegated + - ../../packages/broadcast-panel/index.html:/app/index.html:delegated + - ../../packages/broadcast-panel/vite.config.ts:/app/vite.config.ts:delegated + - ../../packages/broadcast-panel/tsconfig.json:/app/tsconfig.json:delegated + # Montar shared folder + - ../../shared:/shared:delegated + # (NO montar node_modules aquí; usar los node_modules instalados en la imagen) + environment: + - NODE_ENV=development + - DOCKER_ENV=true + - VITE_LIVEKIT_WS_URL=wss://livekit-server.bfzqqk.easypanel.host + - VITE_TOKEN_SERVER_URL=http://localhost:3010 + networks: + - avanzacast-network + restart: unless-stopped + + # Broadcast Panel en modo producción + broadcast-panel-prod: + build: + context: . + dockerfile: Dockerfile + container_name: avanzacast-broadcast-panel-prod + ports: + - "8080:80" + environment: + - VITE_LIVEKIT_WS_URL=wss://livekit-server.bfzqqk.easypanel.host + - VITE_TOKEN_SERVER_URL=http://localhost:3010 + networks: + - avanzacast-network + restart: unless-stopped + profiles: + - production + +networks: + avanzacast-network: + driver: bridge diff --git a/packages/broadcast-panel/nginx.conf b/packages/broadcast-panel/nginx.conf new file mode 100644 index 0000000..470ca05 --- /dev/null +++ b/packages/broadcast-panel/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Configuración para SPA (Single Page Application) + location / { + try_files $uri $uri/ /index.html; + } + + # Headers de seguridad + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Cache para assets estáticos + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # No cache para HTML + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + } + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; +} diff --git a/packages/broadcast-panel/package.json b/packages/broadcast-panel/package.json index 66b78f3..f02653d 100644 --- a/packages/broadcast-panel/package.json +++ b/packages/broadcast-panel/package.json @@ -13,10 +13,17 @@ "@livekit/components-styles": "^1.1.6", "livekit-client": "^2.15.14", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-icons": "^5.5.0" }, "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.0.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.24", + "tailwindcss": "^3.4.16", + "typescript": "^5.0.2", "vite": "^7.2.0" } } diff --git a/packages/broadcast-panel/src/components/Banner.tsx b/packages/broadcast-panel/src/components/Banner.tsx new file mode 100644 index 0000000..fef0a09 --- /dev/null +++ b/packages/broadcast-panel/src/components/Banner.tsx @@ -0,0 +1,42 @@ +import React from 'react' + +interface BannerProps { + children: React.ReactNode + type?: 'info' | 'error' | 'warning' + onClose?: () => void + actionLabel?: string + onAction?: () => void + actionDisabled?: boolean +} + +const Banner: React.FC = ({ children, type = 'info', onClose, actionLabel, onAction, actionDisabled = false }) => { + const bg = type === 'error' ? 'bg-red-700' : type === 'warning' ? 'bg-amber-500' : 'bg-sky-600' + const icon = type === 'error' ? ( + + ) : null + + return ( +
+
+ {icon} +
{children}
+
+
+ {actionLabel && ( + + )} + {onClose && ( + + )} +
+
+ ) +} + +export default Banner diff --git a/packages/broadcast-panel/src/components/TransmissionsTable.module.css b/packages/broadcast-panel/src/components/TransmissionsTable.module.css index 1ed3f09..3a0821c 100644 --- a/packages/broadcast-panel/src/components/TransmissionsTable.module.css +++ b/packages/broadcast-panel/src/components/TransmissionsTable.module.css @@ -201,3 +201,23 @@ font-size: 13px; } } + +/* Spinner used in the Entrar button */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.toast { + position: fixed; + right: 20px; + bottom: 20px; + background: rgba(0,0,0,0.8); + color: #fff; + padding: 10px 14px; + border-radius: 8px; + z-index: 9999; + box-shadow: 0 6px 20px rgba(2,6,23,0.2); +} +.toast.error { background: rgba(160, 40, 40, 0.95); } +.toast.info { background: rgba(30, 120, 255, 0.95); } diff --git a/packages/broadcast-panel/src/components/TransmissionsTable.tsx b/packages/broadcast-panel/src/components/TransmissionsTable.tsx index 7b43fa9..1456229 100644 --- a/packages/broadcast-panel/src/components/TransmissionsTable.tsx +++ b/packages/broadcast-panel/src/components/TransmissionsTable.tsx @@ -29,6 +29,7 @@ const TransmissionsTable: React.FC = ({ transmissions, onDelete, onUpdate const [inviteLink, setInviteLink] = useState(undefined) const [editOpen, setEditOpen] = useState(false) const [editTransmission, setEditTransmission] = useState(undefined) + const [loadingId, setLoadingId] = useState(null) const handleEdit = (t: Transmission) => { setEditTransmission(t) @@ -50,6 +51,33 @@ const TransmissionsTable: React.FC = ({ transmissions, onDelete, onUpdate } }) + const openStudioForTransmission = async (t: Transmission) => { + if (loadingId) return + setLoadingId(t.id) + try { + const userRaw = localStorage.getItem('avanzacast_user') || 'Demo User' + const user = encodeURIComponent(userRaw) + const room = encodeURIComponent(t.id || 'avanzacast-studio') + const tokenRes = await fetch(`http://localhost:3010/api/token?room=${room}&username=${user}`) + if (!tokenRes.ok) throw new Error('No se pudo obtener token') + const tokenData = await tokenRes.json() + + // Guardar token, serverUrl, room, user en sessionStorage + sessionStorage.setItem('avanzacast_studio_token', tokenData.token) + sessionStorage.setItem('avanzacast_studio_serverUrl', tokenData.serverUrl) + sessionStorage.setItem('avanzacast_studio_room', decodeURIComponent(room)) + sessionStorage.setItem('avanzacast_studio_user', decodeURIComponent(user)) + + // Redirigir a studio-panel en la misma pestaña + const shortId = Math.random().toString(36).slice(2, 10) + window.location.href = `http://localhost:3020/${shortId}` + } catch (err) { + console.error('Error entrando al estudio', err) + alert('No fue posible entrar al estudio. Revisa el servidor de tokens.') + setLoadingId(null) + } + } + if (isLoading) { return (
@@ -131,8 +159,22 @@ const TransmissionsTable: React.FC = ({ transmissions, onDelete, onUpdate
- = ({ transmissions, onDelete, onUpdate { label: 'Eliminar transmisión', icon: , onClick: () => onDelete(t.id), containerProps: { className: styles.deleteItem }, labelProps: { className: styles.dangerLabel } } ]} /> - setInviteOpen(false)} link={inviteLink} /> + setInviteOpen(false)} link={inviteLink || ''} />
diff --git a/packages/broadcast-panel/start-docker.sh b/packages/broadcast-panel/start-docker.sh new file mode 100755 index 0000000..3717262 --- /dev/null +++ b/packages/broadcast-panel/start-docker.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Script para iniciar Broadcast Panel con Docker + +set -e + +echo "🚀 Iniciando AvanzaCast Broadcast Panel..." + +# Verificar si existe .env +if [ ! -f .env ]; then + echo "📝 Creando archivo .env desde .env.example..." + cp .env.example .env +fi + +# Modo de ejecución (dev o prod) +MODE=${1:-dev} + +if [ "$MODE" = "dev" ]; then + echo "🔧 Modo: DESARROLLO" + echo "📦 Construyendo imagen de desarrollo..." + docker-compose build broadcast-panel-dev + + echo "🎬 Iniciando contenedor en modo desarrollo..." + docker-compose up broadcast-panel-dev +elif [ "$MODE" = "prod" ]; then + echo "🏭 Modo: PRODUCCIÓN" + echo "📦 Construyendo imagen de producción..." + docker-compose --profile production build broadcast-panel-prod + + echo "🎬 Iniciando contenedor en modo producción..." + docker-compose --profile production up broadcast-panel-prod +else + echo "❌ Modo no válido. Usa: ./start-docker.sh [dev|prod]" + exit 1 +fi diff --git a/packages/broadcast-panel/vite-dev.log b/packages/broadcast-panel/vite-dev.log new file mode 100644 index 0000000..b2bf7e9 --- /dev/null +++ b/packages/broadcast-panel/vite-dev.log @@ -0,0 +1,15 @@ + +> broadcast-panel@0.1.0 dev +> vite + +Port 5173 is in use, trying another one... +Port 5174 is in use, trying another one... + + VITE v7.2.0 ready in 1978 ms + + ➜ Local: http://localhost:5175/ + ➜ Network: http://192.168.1.19:5175/ + ➜ Network: http://192.168.1.17:5175/ + ➜ Network: http://172.18.0.1:5175/ + ➜ Network: http://172.19.0.1:5175/ + ➜ press h + enter to show help diff --git a/packages/broadcast-panel/vite.config.ts b/packages/broadcast-panel/vite.config.ts index 6e33c0a..9b27be6 100644 --- a/packages/broadcast-panel/vite.config.ts +++ b/packages/broadcast-panel/vite.config.ts @@ -5,13 +5,50 @@ import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + optimizeDeps: { + include: [ + 'react-icons', + 'react-icons/si', + 'react-icons/md', + 'react-icons/fa', + 'react-icons/fa6', + 'react-icons/bs' + ] + }, resolve: { alias: { '@': path.resolve(__dirname, './src'), - '@shared': path.resolve(__dirname, '../../shared') + '@shared': process.env.DOCKER_ENV ? '/shared' : path.resolve(__dirname, '../../shared'), + '@avanzacast/shared-hooks': process.env.DOCKER_ENV ? '/shared/hooks' : path.resolve(__dirname, '../../shared/hooks'), + '@avanzacast/shared-utils': process.env.DOCKER_ENV ? '/shared/utils' : path.resolve(__dirname, '../../shared/utils'), + '@avanzacast/shared-types': process.env.DOCKER_ENV ? '/shared/types' : path.resolve(__dirname, '../../shared/types'), + '@avanzacast/shared-config': process.env.DOCKER_ENV ? '/shared/config' : path.resolve(__dirname, '../../shared/config') + , + // Ensure react-icons subpackages imported from /shared resolve to the + // node_modules installed in the image. + 'react-icons': process.env.DOCKER_ENV ? '/app/node_modules/react-icons' : 'react-icons', + 'react-icons/si': process.env.DOCKER_ENV ? '/app/node_modules/react-icons/si' : 'react-icons/si', + 'react-icons/md': process.env.DOCKER_ENV ? '/app/node_modules/react-icons/md' : 'react-icons/md', + 'react-icons/fa': process.env.DOCKER_ENV ? '/app/node_modules/react-icons/fa' : 'react-icons/fa', + 'react-icons/fa6': process.env.DOCKER_ENV ? '/app/node_modules/react-icons/fa6' : 'react-icons/fa6', + 'react-icons/bs': process.env.DOCKER_ENV ? '/app/node_modules/react-icons/bs' : 'react-icons/bs' } }, server: { - port: 5173 + port: 5173, + host: true, + fs: { + // Allow serving files from the shared folder when mounted in Docker + allow: [ + path.resolve(__dirname), + process.env.DOCKER_ENV ? '/shared' : path.resolve(__dirname, '../../shared') + ] + , + // Disable strict fs checking so imports from outside project root work + strict: false + }, + watch: { + usePolling: true + } } }) diff --git a/packages/studio-panel/package.json b/packages/studio-panel/package.json index a9ed24a..facb854 100644 --- a/packages/studio-panel/package.json +++ b/packages/studio-panel/package.json @@ -5,8 +5,8 @@ "description": "AvanzaCast - Broadcast Studio", "type": "module", "scripts": { - "dev": "vite --port 3001", - "dev:vite": "vite --port 3001", + "dev": "vite --port 3020", + "dev:vite": "vite --port 3020", "dev:server": "node server.js", "dev:full": "concurrently \"npm run dev:vite\" \"npm run dev:server\"", "build": "vite build", diff --git a/packages/studio-panel/src/components/Studio.tsx b/packages/studio-panel/src/components/Studio.tsx index f3be47a..c4d94d0 100644 --- a/packages/studio-panel/src/components/Studio.tsx +++ b/packages/studio-panel/src/components/Studio.tsx @@ -6,7 +6,7 @@ import StudioLeftSidebar from './StudioLeftSidebar' import StudioVideoArea from './StudioVideoArea' import StudioRightPanel from './StudioRightPanel' import StudioControls from './StudioControls' -import { DEMO_MODE, DEMO_TOKEN } from '../config/demo' +import { DEMO_TOKEN } from '../config/demo' interface StudioProps { userName: string @@ -16,39 +16,126 @@ interface StudioProps { const Studio: React.FC = ({ userName, roomName }) => { const [token, setToken] = useState(DEMO_TOKEN) const [isConnecting, setIsConnecting] = useState(true) - const [error, setError] = useState('') const [isDemoMode, setIsDemoMode] = useState(true) // Iniciar en modo demo por defecto + // Utility: heurística ligera para validar token en cliente + const isTokenLikelyValid = (t?: string) => { + if (!t) return false + // los tokens de LiveKit suelen ser JWT (contienen '.') o bastante largos + if (t.includes('.')) return true + if (t.length > 80) return true + return false + } + + const decodeJwt = (t: string) => { + try { + const parts = t.split('.') + if (parts.length < 2) return null + const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/') + const padded = payload.padEnd(Math.ceil(payload.length / 4) * 4, '=') + const json = JSON.parse(atob(padded)) + return json + } catch (e) { + return null + } + } + + // Log decoded token claims for diagnostic when token changes useEffect(() => { - const fetchToken = async () => { - try { - const response = await fetch(`http://localhost:3010/api/token?room=${roomName}&username=${userName}`, { - signal: AbortSignal.timeout(2000) // Timeout de 2 segundos - }) - - if (!response.ok) { - throw new Error('Error al obtener el token') - } - - const data = await response.json() - setToken(data.token) + if (!token) return + const claims = decodeJwt(token) + if (claims) { + console.log('[Studio] token claims:', claims) + } else { + console.log('[Studio] token does not look like JWT or could not be decoded') + } + }, [token]) + + useEffect(() => { + // Leer token y datos desde sessionStorage (guardado por broadcast-panel) + try { + const storedToken = sessionStorage.getItem('avanzacast_studio_token') + const storedServerUrl = sessionStorage.getItem('avanzacast_studio_serverUrl') + const storedRoom = sessionStorage.getItem('avanzacast_studio_room') + const storedUser = sessionStorage.getItem('avanzacast_studio_user') + + console.log('[Studio] sessionStorage:', { storedToken: storedToken ? `${storedToken.length} chars` : 'none', storedServerUrl, storedRoom, storedUser }) + + if (storedToken && storedServerUrl) { + setToken(storedToken) setIsDemoMode(false) - } catch (err) { - console.error('Error getting token:', err) - - // Mantener modo demo si no se puede conectar al servidor - console.log('⚠️ No se pudo conectar al servidor de tokens. Usando modo DEMO...') + } else { + console.log('⚠️ No se encontró token en sessionStorage. Usando modo DEMO...') setToken(DEMO_TOKEN) setIsDemoMode(true) - setError('') - } finally { - setIsConnecting(false) } + } catch (err) { + console.error('Error leyendo sessionStorage:', err) + setToken(DEMO_TOKEN) + setIsDemoMode(true) + } finally { + setIsConnecting(false) } - - fetchToken() }, [roomName, userName]) + // Dev-only: wrap window.WebSocket once to capture close codes/reasons/messages + useEffect(() => { + try { + const w = window as any + if (w && !w.__AVZ_WS_WRAPPED) { + const OriginalWS = w.WebSocket + function WrappedWebSocket(url: string, protocols?: string | string[]) { + // @ts-ignore + const sock = protocols ? new OriginalWS(url, protocols) : new OriginalWS(url) + try { + sock.addEventListener('open', (ev: Event) => console.log('[WS Monitor] open', { url, type: (ev && (ev as any).type) || 'open' })) + sock.addEventListener('close', (ev: any) => console.log('[WS Monitor] close', { url, code: ev.code, reason: ev.reason, wasClean: ev.wasClean })) + sock.addEventListener('error', (ev: any) => console.error('[WS Monitor] error', { url, ev })) + sock.addEventListener('message', (ev: any) => { + // don't log binary payloads + try { + const data = typeof ev.data === 'string' ? ev.data : '[binary]' + // limit length + console.log('[WS Monitor] message', { url, data: data && data.slice ? data.slice(0, 200) : data }) + } catch (e) { + // ignore + } + }) + } catch (e) { + // ignore + } + return sock + } + WrappedWebSocket.prototype = OriginalWS.prototype + w.WebSocket = WrappedWebSocket + w.__AVZ_WS_WRAPPED = true + console.log('[Studio] WebSocket wrapped for debug: will log open/close/error/messages') + } + } catch (e) { + // ignore failures in exotic environments + } + }, []) + + // Global error capture to help debugging connection failures + useEffect(() => { + const onErr = (ev: ErrorEvent) => console.error('[Studio][global error]', ev.message, ev.error) + const onReject = (ev: PromiseRejectionEvent) => console.error('[Studio][unhandledrejection]', ev.reason) + window.addEventListener('error', onErr) + window.addEventListener('unhandledrejection', onReject) + return () => { + window.removeEventListener('error', onErr) + window.removeEventListener('unhandledrejection', onReject) + } + }, []) + + // Development-only WebSocket monitor: wrap window.WebSocket to log events for debugging + // NOTE: development WebSocket monitor removed to keep hooks stable during HMR + + useEffect(() => { + console.log('[Studio] rendering LiveKitRoom, token_len=', token ? token.length : 0) + return () => console.log('[Studio] unmount LiveKitRoom') + }, [token]) + if (isConnecting) { return (
@@ -60,38 +147,66 @@ const Studio: React.FC = ({ userName, roomName }) => { ) } - if (error) { + // Heurística para no montar LiveKitRoom con tokens de demo o cortos + const serverUrl = import.meta.env.VITE_LIVEKIT_URL || 'wss://livekit-server.bfzqqk.easypanel.host' + const hasValidToken = isTokenLikelyValid(token) + + if (!hasValidToken) { + // Mostrar diagnóstico más útil cuando el token es claramente inválido/corto + const preview = token ? `${token.slice(0, 6)}...${token.slice(-6)}` : 'none' + console.log('[Studio] token not valid, will not mount LiveKitRoom. preview=', preview, ' len=', token ? token.length : 0) + + const broadcastUrl = import.meta.env.VITE_BROADCAST_URL || 'http://localhost:5175' + const goBackToBroadcast = () => { + // Limpiar sessionStorage + try { + sessionStorage.removeItem('avanzacast_studio_token') + sessionStorage.removeItem('avanzacast_studio_serverUrl') + sessionStorage.removeItem('avanzacast_studio_room') + sessionStorage.removeItem('avanzacast_studio_user') + } catch (e) {} + + // Intentar volver atrás en el historial si hay entrada previa + if (window.history.length > 1) { + window.history.back() + } else { + // Redirigir al broadcast + window.location.href = broadcastUrl + } + } + + // Modal bloqueante: overlay completo que no permite interacción con UI return ( -
-
+
+ {/* Modal */} +
- + -

Error de conexión

+

No se pudo conectar al estudio

+
+

No se obtuvo un token válido para autenticarse con el servidor de LiveKit.

+

Token obtenido: {token ? `${token.length} chars` : 'ninguno'}

+
+ +
-

{error}

-
) } - if (!token) { - return ( -
-

No se pudo obtener el token de acceso

-
- ) - } - - const serverUrl = import.meta.env.VITE_LIVEKIT_URL || 'wss://livekit-server.bfzqqk.easypanel.host' - // Renderizar interfaz en modo demo (sin LiveKit) if (isDemoMode) { return ( @@ -123,6 +238,8 @@ const Studio: React.FC = ({ userName, roomName }) => { } // Renderizar con LiveKit (modo normal) + + return ( @avanzacast/broadcast-studio@1.0.0 dev +> vite --port 3020 + + + VITE v4.3.9 ready in 1346 ms + + ➜ Local: http://localhost:3020/ + ➜ Network: http://192.168.1.19:3020/ + ➜ Network: http://192.168.1.17:3020/ + ➜ Network: http://172.18.0.1:3020/ + ➜ Network: http://172.19.0.1:3020/ +10:40:42 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +10:42:54 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +10:43:26 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +10:44:47 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +10:45:20 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +10:50:16 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +11:04:49 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +11:05:21 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +11:05:47 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +11:06:22 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +11:07:13 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css +11:11:12 p.m. [vite] hmr update /src/components/Studio.tsx, /src/index.css diff --git a/packages/studio-panel/vite.config.ts b/packages/studio-panel/vite.config.ts index a5a0ed7..bb15e4b 100644 --- a/packages/studio-panel/vite.config.ts +++ b/packages/studio-panel/vite.config.ts @@ -8,7 +8,10 @@ export default defineConfig(({ mode }) => { return { plugins: [react()], server: { - port: 3001, + port: 3020, + host: true, + // Usar HTTP en dev para pruebas E2E locales (localhost permite getUserMedia sin HTTPS en muchos navegadores) + watch: { usePolling: true } }, define: { // Exponer variables de entorno al cliente diff --git a/scripts/e2e/playwright-test.js b/scripts/e2e/playwright-test.js new file mode 100644 index 0000000..8634481 --- /dev/null +++ b/scripts/e2e/playwright-test.js @@ -0,0 +1,75 @@ +const { chromium } = require('playwright') + +;(async () => { + console.log('E2E: solicitando token al token-server...') + const tokenRes = await fetch('http://localhost:3010/api/token?room=test&username=Playwright') + if (!tokenRes.ok) throw new Error('No se pudo obtener token desde http://localhost:3010') + const tokenData = await tokenRes.json() + console.log('E2E: token recibido (longitud):', tokenData.token ? tokenData.token.length : 'n/a') + + const shortId = Math.random().toString(36).slice(2, 10) + const studioUrl = `http://localhost:3020/${shortId}` + + console.log('E2E: lanzando navegador headless...') + const browser = await chromium.launch({ headless: true }) + const context = await browser.newContext() + + // Abrir una 'broadcast page' (simula origen que realiza window.open y postMessage) + const broadcastPage = await context.newPage() + try { + await broadcastPage.goto('http://localhost:5175', { waitUntil: 'domcontentloaded', timeout: 8000 }) + console.log('E2E: broadcast-page abierta') + } catch (err) { + console.warn('E2E: no se pudo cargar broadcast UI, continuamos (no es obligatorio):', err.message) + } + + // Preparar espera de la nueva página (studio) + const newPagePromise = context.waitForEvent('page') + + // Desde la broadcastPage crear la ventana y enviar postMessage (imitando el comportamiento de la app) + await broadcastPage.evaluate(({ studioUrl, tokenData }) => { + const win = window.open(studioUrl, '_blank') + // Intentar enviar token repetidamente hasta que la ventana esté lista + let attempts = 0 + const tryPost = () => { + attempts++ + try { + const origin = new URL(studioUrl).origin + win.postMessage({ type: 'AVANZACAST_TOKEN', token: tokenData.token, serverUrl: tokenData.serverUrl, room: 'test', user: 'Playwright' }, origin) + // eslint-disable-next-line no-console + console.log('E2E: postMessage enviado desde broadcast page') + } catch (e) { + if (attempts < 15) setTimeout(tryPost, 200) + else console.error('E2E: no se pudo enviar postMessage a la ventana del studio') + } + } + tryPost() + }, { studioUrl, tokenData }) + + const studioPage = await newPagePromise + await studioPage.waitForLoadState('domcontentloaded') + console.log('E2E: studio page abierta, esperando que el banner de DEMO desaparezca si llega...') + + // El componente muestra un banner de modo demo; si recibimos token, ese banner debería desaparecer. + try { + // Si existe el banner, esperamos que desaparezca + const demoLocator = studioPage.locator('text=⚠️ MODO DEMO') + // Esperar a que sea visible (si no aparece, skip) + await demoLocator.waitFor({ state: 'visible', timeout: 3000 }).catch(() => {}) + // Ahora esperar a que se oculte tras recibir el token + await demoLocator.waitFor({ state: 'hidden', timeout: 8000 }) + console.log('E2E: banner de DEMO desapareció -> token aplicado') + } catch (err) { + console.warn('E2E: no se pudo confirmar desaparición del banner DEMO (puede que la página ya estuviera en modo no-demo):', err.message) + } + + // Tomar un snapshot de la página (longitud del HTML) + const html = await studioPage.content() + console.log('E2E: longitud del HTML del studio:', html.length) + + await browser.close() + console.log('E2E: prueba finalizada correctamente') +})().catch(err => { + console.error('E2E: error en la prueba', err) + process.exit(1) +})