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.
This commit is contained in:
parent
0ca2b36b5c
commit
543d6bc6af
72
package-lock.json
generated
72
package-lock.json
generated
@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
"playwright": "^1.38.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
11
packages/broadcast-panel/.dockerignore
Normal file
11
packages/broadcast-panel/.dockerignore
Normal file
@ -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
|
||||
7
packages/broadcast-panel/.env.example
Normal file
7
packages/broadcast-panel/.env.example
Normal file
@ -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
|
||||
201
packages/broadcast-panel/DOCKER_README.md
Normal file
201
packages/broadcast-panel/DOCKER_README.md
Normal file
@ -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! 🎥
|
||||
30
packages/broadcast-panel/Dockerfile
Normal file
30
packages/broadcast-panel/Dockerfile
Normal file
@ -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;"]
|
||||
25
packages/broadcast-panel/Dockerfile.dev
Normal file
25
packages/broadcast-panel/Dockerfile.dev
Normal file
@ -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"]
|
||||
48
packages/broadcast-panel/docker-compose.yml
Normal file
48
packages/broadcast-panel/docker-compose.yml
Normal file
@ -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
|
||||
34
packages/broadcast-panel/nginx.conf
Normal file
34
packages/broadcast-panel/nginx.conf
Normal file
@ -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;
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
42
packages/broadcast-panel/src/components/Banner.tsx
Normal file
42
packages/broadcast-panel/src/components/Banner.tsx
Normal file
@ -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<BannerProps> = ({ 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' ? (
|
||||
<svg className="w-5 h-5 mr-3 text-white/90" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"></path><path strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M12 9v4m0 4h.01"></path></svg>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className={`${bg} text-white px-4 py-3 rounded-md mb-4 shadow-sm flex items-center justify-between`}>
|
||||
<div className="flex items-center">
|
||||
{icon}
|
||||
<div className="text-sm font-medium">{children}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{actionLabel && (
|
||||
<button
|
||||
onClick={onAction}
|
||||
disabled={actionDisabled}
|
||||
className={`px-3 py-1 rounded-md text-sm font-semibold ${actionDisabled ? 'bg-white/20 text-white/60 cursor-not-allowed' : 'bg-white/10 hover:bg-white/20 text-white'}`}
|
||||
>
|
||||
{actionLabel}
|
||||
</button>
|
||||
)}
|
||||
{onClose && (
|
||||
<button onClick={onClose} className="text-white/80 underline ml-2 text-sm">Cerrar</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Banner
|
||||
@ -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); }
|
||||
|
||||
@ -29,6 +29,7 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
|
||||
const [inviteLink, setInviteLink] = useState<string | undefined>(undefined)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [editTransmission, setEditTransmission] = useState<Transmission | undefined>(undefined)
|
||||
const [loadingId, setLoadingId] = useState<string | null>(null)
|
||||
|
||||
const handleEdit = (t: Transmission) => {
|
||||
setEditTransmission(t)
|
||||
@ -50,6 +51,33 @@ const TransmissionsTable: React.FC<Props> = ({ 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 (
|
||||
<div className={styles.transmissionsSection}>
|
||||
@ -131,8 +159,22 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
|
||||
</td>
|
||||
<td className={styles.tableCell} style={{ textAlign: 'right' }}>
|
||||
<div className={styles.actionsCell}>
|
||||
<button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}>
|
||||
Entrar al estudio
|
||||
<button
|
||||
aria-label={`Entrar al estudio ${t.title}`}
|
||||
className={styles.enterStudioButton}
|
||||
disabled={loadingId !== null}
|
||||
onClick={() => openStudioForTransmission(t)}
|
||||
>
|
||||
{loadingId === t.id ? (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 50 50" style={{ animation: 'spin 1s linear infinite' }}>
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke="#fff" strokeWidth="5" strokeLinecap="round" strokeDasharray="31.4 31.4" />
|
||||
</svg>
|
||||
Entrando...
|
||||
</span>
|
||||
) : (
|
||||
'Entrar al estudio'
|
||||
)}
|
||||
</button>
|
||||
|
||||
<Dropdown
|
||||
@ -146,7 +188,7 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
|
||||
{ label: 'Eliminar transmisión', icon: <MdDelete size={16} />, onClick: () => onDelete(t.id), containerProps: { className: styles.deleteItem }, labelProps: { className: styles.dangerLabel } }
|
||||
]}
|
||||
/>
|
||||
<InviteGuestsModal open={inviteOpen} onClose={() => setInviteOpen(false)} link={inviteLink} />
|
||||
<InviteGuestsModal open={inviteOpen} onClose={() => setInviteOpen(false)} link={inviteLink || ''} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
35
packages/broadcast-panel/start-docker.sh
Executable file
35
packages/broadcast-panel/start-docker.sh
Executable file
@ -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
|
||||
15
packages/broadcast-panel/vite-dev.log
Normal file
15
packages/broadcast-panel/vite-dev.log
Normal file
@ -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
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<StudioProps> = ({ userName, roomName }) => {
|
||||
const [token, setToken] = useState<string>(DEMO_TOKEN)
|
||||
const [isConnecting, setIsConnecting] = useState(true)
|
||||
const [error, setError] = useState<string>('')
|
||||
const [isDemoMode, setIsDemoMode] = useState(true) // Iniciar en modo demo por defecto
|
||||
|
||||
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')
|
||||
// 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 data = await response.json()
|
||||
setToken(data.token)
|
||||
setIsDemoMode(false)
|
||||
} catch (err) {
|
||||
console.error('Error getting token:', err)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Mantener modo demo si no se puede conectar al servidor
|
||||
console.log('⚠️ No se pudo conectar al servidor de tokens. Usando modo DEMO...')
|
||||
// Log decoded token claims for diagnostic when token changes
|
||||
useEffect(() => {
|
||||
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)
|
||||
} else {
|
||||
console.log('⚠️ No se encontró token en sessionStorage. Usando modo DEMO...')
|
||||
setToken(DEMO_TOKEN)
|
||||
setIsDemoMode(true)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error leyendo sessionStorage:', err)
|
||||
setToken(DEMO_TOKEN)
|
||||
setIsDemoMode(true)
|
||||
setError('')
|
||||
} 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 (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
@ -60,38 +147,66 @@ const Studio: React.FC<StudioProps> = ({ 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 (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="bg-red-900/20 border border-red-500 rounded-lg p-6 max-w-md">
|
||||
<div className="fixed inset-0 z-[9999] bg-gray-900 flex items-center justify-center">
|
||||
{/* Modal */}
|
||||
<div role="dialog" aria-modal="true" className="bg-gray-800 p-8 rounded-lg max-w-md w-full mx-4 shadow-2xl">
|
||||
<div className="flex items-center mb-4">
|
||||
<svg className="w-6 h-6 text-red-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-8 h-8 text-red-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h3 className="text-red-400 font-semibold">Error de conexión</h3>
|
||||
<h3 className="text-xl text-white font-semibold">No se pudo conectar al estudio</h3>
|
||||
</div>
|
||||
<p className="text-red-300 text-sm mb-4">{error}</p>
|
||||
<p className="text-gray-300 mb-3">No se obtuvo un token válido para autenticarse con el servidor de LiveKit.</p>
|
||||
<p className="text-gray-400 text-sm mb-6">Token obtenido: {token ? `${token.length} chars` : 'ninguno'}</p>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-full py-2 px-4 bg-red-600 hover:bg-red-700 text-white rounded-lg transition"
|
||||
className="py-2 px-4 bg-slate-600 hover:bg-slate-700 text-white rounded-lg font-medium"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
<button
|
||||
onClick={goBackToBroadcast}
|
||||
className="py-2 px-4 bg-pink-600 hover:bg-pink-700 text-white rounded-lg font-medium"
|
||||
>
|
||||
Volver al Broadcast
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||
<p className="text-white">No se pudo obtener el token de acceso</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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<StudioProps> = ({ userName, roomName }) => {
|
||||
}
|
||||
|
||||
// Renderizar con LiveKit (modo normal)
|
||||
|
||||
|
||||
return (
|
||||
<LiveKitRoom
|
||||
video={true}
|
||||
|
||||
20
packages/studio-panel/ssl/localhost.crt
Normal file
20
packages/studio-panel/ssl/localhost.crt
Normal file
@ -0,0 +1,20 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDODCCAiCgAwIBAgIUcNTZqPhOwoYkYilwsQR3YTZ6YK0wDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTEwNzA0MjMxMloXDTI2MTEw
|
||||
NzA0MjMxMlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAlUMUTx43JyWIOXYSC1HG5RJ/TpIouuym0aRThuIBogqR
|
||||
LCQgHF+L3mawpiGTpdAFWnPsttArf8UUkROJeSt37NFqiA8Kmogyx1Xb6At+uiyt
|
||||
KTOG/br+PeGnN/A644lmqP9AAmG2TnVfsgFqKm4FUs8LFTEVDV8zxXx1oMCSTE9G
|
||||
a6h7IQ4VpFZLU3no0eNppvbCXmVpwZC25WI4R0ee51Ii2DPSguA/og/urIhsxQ/6
|
||||
0yrIUqRQbhYcVTO8DLyNfMzRjrejpDM8ii2L+j6mslaIzeqiAEjrhX0O5XzowP/j
|
||||
/HPuZ4fltHdMMTOR/oSZkomv1bOQCz8DiIo04OJOtwIDAQABo4GBMH8wHQYDVR0O
|
||||
BBYEFD/+kW7z0masF/hvZTBEjQ2VaPNcMB8GA1UdIwQYMBaAFD/+kW7z0masF/hv
|
||||
ZTBEjQ2VaPNcMA8GA1UdEwEB/wQFMAMBAf8wLAYDVR0RBCUwI4IJbG9jYWxob3N0
|
||||
hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQB21ONB
|
||||
HsBIUaf7ZhvGndWo6EEavscvSD2OOItHpzz0ThyuUZEAwjTpor+oETWoPDEFI/il
|
||||
BBny/5bB7ajYrmZ/+OdxaL2nTzO+0oQtHjIwUDUmCh7YvrXYhCHxzmA9j+300Gel
|
||||
rIqyAIhV6gJvmq93CmmWPf/C4deDyiU2ZvqfdQxfc2iM3jvOYig5qaqT5vyBV9Jk
|
||||
3HuxgPsQwID79USJRxxYlvI09wFBhMwNBlQA02t+zpi/BcC8/tUp4+rrf3g4o9Qb
|
||||
AEJDC69dwLi0EAACljD8cFeK768PhDQ5uyAxB5szFJWQPJkynRZ3JB/us86TuQVc
|
||||
Cqde9MNl5SWEcU6O
|
||||
-----END CERTIFICATE-----
|
||||
15
packages/studio-panel/ssl/localhost.csr
Normal file
15
packages/studio-panel/ssl/localhost.csr
Normal file
@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAxo7rYtforBLUsyoYo+hnsL0dK0FU72gFJF25M1lJ
|
||||
Z/jFfJYdNGuoh6d5lQm1X9pvA4AKbdSEU+7y/AEb8xZgAibGrecnusJOIrIr1fPW
|
||||
Xe7NUp5e65OtCQhlYGHK3vnMUY50oLReLPiMoR/SsX9jPLFlIkpLy2BRNTL933SC
|
||||
eaTxPfxVIg4HME/Zs/cqbIynJWN6f0SbhQQVPMzLua6WXzEt4bnCW4RsgJpWY6tS
|
||||
8zrROMSo3zzTJ8qjV/EjGm2aGmhuIHkIMo50E5+MdjWkWIm9F6xErriyVmeWETyR
|
||||
n17DyqfRRCD0q76I75xKwLAFpnRO03lOygdd4QtY3XtKTQIDAQABoAAwDQYJKoZI
|
||||
hvcNAQELBQADggEBAF7h8ER0kBK60QOsDQGI70isBDTMci0RRCLSTa82KVNMZ6NY
|
||||
0Y0eOG1iP0KwnMup8A/9roe0RUlG2DYwaxEd1hskYhxtHvakrxI+oypQyW6USrz8
|
||||
120vce3GY9jLhcqz/Ws5YTZ1aG+gBi7ps1GyzfLIMG4BhT7tiTvQSLbP3kX08CgF
|
||||
L+Zz7ukoW1pUTMYa/sWVy1+a6Gelf/V6wIekNXzrT0/xkj2VmxlAVS0Do7E7y/Z1
|
||||
t9MjYVlxIvw8lDvUyrVwm9S5tPIni1mVQCgiFej9qFcJIzn2Hhgt0TnQi8piQFDe
|
||||
JiEc0xpxVyZeeMR7ds2KK2FVy+Afo5+4py7Ib3w=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
28
packages/studio-panel/ssl/localhost.key
Normal file
28
packages/studio-panel/ssl/localhost.key
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDGjuti1+isEtSz
|
||||
Khij6GewvR0rQVTvaAUkXbkzWUln+MV8lh00a6iHp3mVCbVf2m8DgApt1IRT7vL8
|
||||
ARvzFmACJsat5ye6wk4isivV89Zd7s1Snl7rk60JCGVgYcre+cxRjnSgtF4s+Iyh
|
||||
H9Kxf2M8sWUiSkvLYFE1Mv3fdIJ5pPE9/FUiDgcwT9mz9ypsjKclY3p/RJuFBBU8
|
||||
zMu5rpZfMS3hucJbhGyAmlZjq1LzOtE4xKjfPNMnyqNX8SMabZoaaG4geQgyjnQT
|
||||
n4x2NaRYib0XrESuuLJWZ5YRPJGfXsPKp9FEIPSrvojvnErAsAWmdE7TeU7KB13h
|
||||
C1jde0pNAgMBAAECgf8roLuXvFkjdf/GXjmLykT+UI9YMcK31+NJWk6XOccnUUIT
|
||||
XeiM3Brrs6DDXp67sQMzga7I8ykgSCCbvqKlhwURc/Ozwla4cnk4pm17VViEyzPS
|
||||
M3onyQr8MRwVUWeCFuEOCn8V0Ivg1bwQqy1gUt07OL9ACZMd3Mv7JHkj4DXLBE/J
|
||||
EmupZZJZqfcJWzQmOrA7T278MmvySUFUXY7wCMwL/y2/3FywpKUzuciwwY2j+J1g
|
||||
3HRDJwT1uLxQ/2m2Kv1YYKC6BZatyO0rVJAiMQQOvbDEX918eVGNmJ3nr3xjzIOe
|
||||
AUOAjWAlt9OVxHCw3Ztu183FoOrMnWfWTApHTfkCgYEA7SyAf60+NLiHbVfr4+41
|
||||
jqg7CPvi3KiWTuvb7EElhJNba7DoCApPJ3UpZI9sLHEcnhcKXrnRs9GWqu/L53Ov
|
||||
IXwJYQkvAdRSVEvlaaOxKtePLWuA872EPDSDJ1slDk9STvsjyA38H7Wck7kE6ve+
|
||||
rIo2FIYlJ0SBRJgH3wjJVVUCgYEA1lG6nKRKxxqM9kGNBsCi7W5fcU5zPIB19ZOF
|
||||
S1Ft1R/Uu2GQXe0PVa2XsVt1ytGmtYX2N2E8VE0kSideN0kUBsfvq+f1iEybN+Hd
|
||||
fN8DOcrnOk1wHmBNPkIjX+oxCl2LL8m9d4stWk+XRJ2ztCus3N8HrLHCy7CdD0OK
|
||||
StaKIRkCgYEAv8KHcrOj5AR+ms3Hj9Z9vwYOFUlzN22nedABVJenufVaqUuzjyym
|
||||
qwRznzbHA3fA56FBZS1ge78tzq9rcYt7QduDyc0fEJ+WoUlsB2muSTFYNiUBchD8
|
||||
5aCfmiZ7Y4KGzg0H9Sw5eXnhSx8A9umwZNiquRVs3L7qtYcmdhIolrECgYAagHC6
|
||||
/fXhOP9FVEXEF/4NriBPOow1Zw0vGNbawW77c7wyj5Xyh2XmClk/rTebpOEggTg5
|
||||
EOUM550dLlEQNREs5XxVnZFXEWIAPwXMcydK9jQxmXHLz8y9biBBtAvsZDTZ6/Bp
|
||||
3+PzzvO9oGKgXOY7SbkBOdoEpgpF4Oww5OafwQKBgCDrafoYJqCx7wiayAu3AhHg
|
||||
FN4HsDA1PlCziqXmUd2L16O2fFpZ5/MfSXKqAXREe1p2ADX3HHJvzMzQksvju7n+
|
||||
H2Vmi5ejuhvVKxhvGuxi+T4XsCv9B8c8K4msVhdCIycKRFKVMn2NFfJPQRMT0VI1
|
||||
FpYz/TshNdaFBxDn7CUt
|
||||
-----END PRIVATE KEY-----
|
||||
25
packages/studio-panel/vite-dev.log
Normal file
25
packages/studio-panel/vite-dev.log
Normal file
@ -0,0 +1,25 @@
|
||||
nohup: no se tendrá en cuenta la entrada
|
||||
|
||||
> @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
|
||||
@ -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
|
||||
|
||||
75
scripts/e2e/playwright-test.js
Normal file
75
scripts/e2e/playwright-test.js
Normal file
@ -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)
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user