# Guía Operacional — Sistema de Karaoke en Red Local

> Documento para el operador del sistema. No requiere conocimientos técnicos para las secciones 1–4.

---

## 1. Arranque rápido (día del evento)

### Comando único para iniciar todo

Desde la carpeta del proyecto, ejecuta:

```bash
bash scripts/start-event.sh
```

Este comando realiza en orden:
1. Reconstruye la playlist con las canciones disponibles
2. Configura el sink de audio virtual para el micrófono
3. Arranca el servidor WebSocket de micrófono (puerto 8081)
4. Arranca el servidor HTTP (puerto 8080)
5. Imprime las URLs de acceso

Cuando el script termine de arrancar, verás en pantalla:

```
  ┌──────────────────────────────────────────────────────────┐
  │  PLAYER (TV / Proyector)                                 │
  │  https://player.zensitpro.pro/player/                    │
  │  http://192.168.1.182:8080/player/                       │
  │                                                          │
  │  MICRÓFONO (móvil del cantante)                          │
  │  http://192.168.1.182:8080/player/mic.html               │
  │                                                          │
  │  CALIBRACIÓN / PLAYGROUND                                │
  │  http://192.168.1.182:8080/player/playground.html        │
  └──────────────────────────────────────────────────────────┘
```

Deja el terminal abierto durante toda la presentación. Para detener, pulsa `Ctrl+C`.

### Pasos para el operador antes de que empiece el evento

1. Conectar el ordenador al router o switch de la red local
2. Abrir el terminal y ejecutar `bash scripts/start-event.sh`
3. Abrir el player en la TV/proyector con la URL que aparece en pantalla
4. Distribuir la URL de micrófono a los cantantes (o escanear el QR en `player/mic-qr.png`)
5. Verificar que el audio suena correctamente antes de empezar

---

## 2. URLs del sistema

### URL pública (Tailscale Funnel) — funciona desde cualquier red (4G/5G/Wi-Fi visitante)

URL base: **`https://zen-admin-oclaw.tailc9332.ts.net`**

| Endpoint | URL completa |
|---|---|
| Player TV | `https://zen-admin-oclaw.tailc9332.ts.net/player/` |
| Micrófono cantantes (atajo) | `https://zen-admin-oclaw.tailc9332.ts.net/karaoke` |
| Micrófono cantantes (largo) | `https://zen-admin-oclaw.tailc9332.ts.net/player/mic.html` |
| Calibración | `https://zen-admin-oclaw.tailc9332.ts.net/player/playground.html` |
| Panel admin (Basic Auth) | `https://zen-admin-oclaw.tailc9332.ts.net/admin/` |
| API now-playing | `https://zen-admin-oclaw.tailc9332.ts.net/api/now-playing` |
| WebSocket micrófono | `wss://zen-admin-oclaw.tailc9332.ts.net/mic-ws` (automático) |

Cert TLS Let's Encrypt válido. Los cantantes pueden conectar desde 4G/5G sin instalar nada. Para distribuir, escribe en la pizarra:

```
https://zen-admin-oclaw.tailc9332.ts.net/karaoke
```

### URLs locales (LAN, fallback si falla Internet)

| Dispositivo | URL local |
|---|---|
| TV / Proyector (player) | `http://192.168.1.182:8080/player/` |
| Micrófono (móvil cantante) | `http://192.168.1.182:8080/player/mic.html` |
| Calibración (playground) | `http://192.168.1.182:8080/player/playground.html` |
| Panel de administración | `http://192.168.1.182:8082/` |

**Nota**: ambas modalidades coexisten. Si Internet se cae, la LAN sigue funcionando. Si los móviles de los cantantes no se quieren conectar al Wi-Fi del evento, la URL Funnel funciona vía 4G.

---

## 3. Controles del player

### Teclado (recomendado con teclado USB conectado a la TV/PC)

| Tecla | Acción |
|---|---|
| `Espacio` | Play / Pausa |
| `→` | Siguiente canción |
| `←` | Canción anterior |
| `↑` | Atrasar letra 0,5 segundos (letra aparece más tarde) |
| `↓` | Adelantar letra 0,5 segundos (letra aparece antes) |
| `F` | Pantalla completa / salir de pantalla completa |
| `D` | Activar/desactivar modo de diagnóstico (debug overlay) |

### Botones en pantalla

Los botones aparecen al mover el ratón o tocar la pantalla. Desaparecen solos tras 3 segundos.

| Botón | Acción |
|---|---|
| `⏮` | Canción anterior |
| `⏯` | Play / Pausa |
| `⏭` | Siguiente canción |
| `−` | Adelantar letra 0,5 segundos |
| `+` | Atrasar letra 0,5 segundos |
| `⛶` | Pantalla completa |

El indicador **`offset X.Xs`** muestra el ajuste de sincronía activo. En `0.0s` la letra está en su posición original.

### Gestos táctiles (TV táctil o tablet)

| Gesto | Acción |
|---|---|
| Deslizar a la izquierda | Siguiente canción |
| Deslizar a la derecha | Canción anterior |
| Deslizar hacia arriba | Atrasar letra +0,5 segundos |
| Deslizar hacia abajo | Adelantar letra −0,5 segundos |
| Toque simple | Muestra los botones de control |

---

## 4. Sistema de micrófono inalámbrico

### Requisitos

- El móvil del cantante debe estar conectado a la **misma red Wi-Fi** que el ordenador servidor
- El móvil debe tener un navegador moderno (Chrome o Firefox recomendados)
- El navegador pedirá permiso para acceder al micrófono — se debe aceptar

### Cómo conectar el micrófono desde el móvil (instrucciones para el cantante)

1. Conectar el móvil a la red Wi-Fi del evento
2. Abrir el navegador y entrar a:
   ```
   http://192.168.1.182:8080/player/mic.html
   ```
3. Pulsar el botón verde **"Cantar"**
4. Si el navegador pide permiso para el micrófono, pulsar **Permitir**
5. El botón cambiará a rojo **"Parar"** y el indicador mostrará `🔴 EN VIVO`
6. Para dejar de transmitir, pulsar **"Parar"**

El medidor de barras (VU meter) en la pantalla del móvil muestra el nivel de señal en tiempo real.

### Qué hace el sistema de audio

El servidor recibe la voz del móvil y la mezcla directamente con el audio del karaoke mediante un sink virtual de PulseAudio/PipeWire llamado `karaoke_mic`. No requiere configuración adicional de mezclador.

### Monitor en vivo (escuchar desde el laptop)

Para verificar **antes de que el cantante salga al escenario** que su voz llega bien al servidor — sin necesidad de tener altavoces conectados al portátil ni hardware adicional — abre la página de monitor:

- **LAN**: `http://192.168.1.182:8080/player/monitor.html`
- **Vía Caddy edge**: `https://player.zensitpro.pro/player/monitor.html`
- **Atajo**: desde el panel admin, sección "Dispositivos de micrófono conectados", botón **🎧 Abrir monitor**.

Cómo funciona:
1. Abre la página en el navegador del portátil (Chrome o Firefox).
2. Pulsa el botón **▶ ESCUCHAR**. El navegador necesita un gesto explícito para iniciar la reproducción de audio.
3. El botón pasa a verde **ESCUCHANDO** y el indicador a `🟢 En vivo`.
4. Mientras haya un cantante con el mic activo, oirás su voz por los altavoces del portátil con ~200–500 ms de retardo (es solo para verificación, no para performance).
5. El panel lateral muestra estadísticas en vivo: bytes recibidos, buffer pendiente, sample rate del navegador y latencia estimada. Si "Buffer pendiente" sube por encima de 100, hay congestión de red.

Detalles técnicos:
- El monitor abre un WebSocket al mic-server con la query `?monitor=1`. El mic-server reenvía a todos los monitors los mismos bytes PCM que recibe de los móviles.
- Puedes abrir varios monitors al mismo tiempo (por ejemplo, uno en el portátil del DJ y otro en una tableta).
- El monitor **no aparece** en la lista de dispositivos del panel admin (solo se listan los móviles que envían audio).
- La página es estática y no requiere credenciales. La lista lateral de "Cantantes conectados" sí pide login del admin si la abres vía Caddy edge; en LAN funciona si Diego ya está logueado en el panel admin.

### Solución de problemas del micrófono

| Síntoma | Causa probable | Solución |
|---|---|---|
| El botón "Cantar" no responde | El navegador bloqueó el micrófono | Ir a ajustes del navegador → Permisos → Micrófono → Permitir |
| "Error de conexión al servidor" | El servidor WebSocket no está corriendo | Verificar que `start-event.sh` está activo; comprobar puerto 8081 |
| Voz no se escucha en el audio | El sink `karaoke_mic` no existe | Ejecutar `bash scripts/setup-mic-sink.sh` manualmente |
| Mucho eco o realimentación | El volumen del altavoz está muy alto | Bajar el volumen del sistema o alejar el móvil del altavoz |

---

## 5. Calibración de letra (offset)

### Qué es el offset

El offset es el ajuste de tiempo entre el audio y la letra sincronizada (archivo LRC). Si la letra aparece antes de que se cante, hay que atrasar. Si aparece después, hay que adelantar.

El offset se guarda automáticamente por canción en el navegador. La próxima vez que se cargue esa canción, el valor se recupera.

### Cuándo ajustar

- La letra aparece **antes** de que suene: presionar `↓` o botón `−` (adelanta la letra)
- La letra aparece **después** de que suene: presionar `↑` o botón `+` (atrasa la letra)

### Referencia rápida

| Acción | Teclado (player) | Botón pantalla | Playground |
|---|---|---|---|
| Atrasar letra +0,5s | `↑` | `+` | Botón `+0.1s`, `+1s`, `+5s` |
| Adelantar letra −0,5s | `↓` | `−` | Botón `−0.1s`, `−1s`, `−5s` |
| Restablecer a cero | — | — | Botón `Reset` |

### Usar el playground para calibración fina

El playground (`/player/playground.html`) permite ajustar el offset con mayor precisión antes de la presentación.

Pasos:

1. Abrir `http://192.168.1.182:8080/player/playground.html` en el ordenador del operador
2. Seleccionar la canción en la lista de la izquierda (✅ = tiene letra, ❌ = sin letra)
3. Pulsar play y escuchar
4. Usar los botones de offset: `−5s`, `−1s`, `−0.1s`, `+0.1s`, `+1s`, `+5s`
5. El monitor de sincronía muestra:
   - `🟢 Sincronizado` — todo correcto
   - `🟡 Próxima` — la siguiente línea cambia pronto
   - `🟠 Inminente` — cambio en menos de 0,5 segundos
   - `🔴 Offset demasiado positivo` — la letra se está adelantando demasiado, usar `−`
6. El valor se guarda automáticamente y se aplica cuando la canción se cargue en el player principal

#### Atajos de teclado en el playground

| Tecla | Acción |
|---|---|
| `Espacio` | Play / Pausa |
| `←` / `→` | Retroceder / avanzar 5 segundos |
| `↑` | Offset +0,1 segundos |
| `↓` | Offset −0,1 segundos |
| `Shift` + `↑` | Offset +1 segundo |
| `Shift` + `↓` | Offset −1 segundo |
| `R` | Restablecer offset a cero |

---

## 6. Añadir canciones nuevas

### Estructura de directorios

```
lan-media/
├── songs/              ← MP3 originales (con voz)
├── songs-karaoke/      ← MP3 procesados sin voz (generados)
├── lyrics/             ← Archivos de letra sincronizada (.lrc)
└── player/
    └── playlist.json   ← Generado automáticamente
```

### Paso 1 — Colocar el MP3 original

Copiar el archivo MP3 a la carpeta `songs/`. Se recomienda nombrar los archivos con prefijo numérico para controlar el orden:

```
01 - Artista - Titulo.mp3
02 - Artista - Titulo.mp3
```

### Paso 2 — Generar la versión karaoke (sin voz)

```bash
# Procesar todas las canciones nuevas (omite las ya procesadas)
bash scripts/make-karaoke.sh

# Forzar regeneración de una canción específica
bash scripts/make-karaoke.sh "songs/01 - Artista - Titulo.mp3"

# Regenerar todas aunque ya existan
bash scripts/make-karaoke.sh --force
```

El script usa FFmpeg para aplicar cancelación de fase estéreo (elimina vocales centradas). El resultado se guarda en `songs-karaoke/` con el mismo nombre de archivo.

### Paso 3 — Añadir el archivo de letra (LRC)

El archivo LRC debe tener el **mismo nombre** que el MP3 (sin extensión) y guardarse en `lyrics/`:

```
lyrics/01 - Artista - Titulo.lrc
```

Para buscar archivos LRC sincronizados:

```bash
# Requiere: pip3 install syncedlyrics
syncedlyrics "Artista Titulo" -o "lyrics/01 - Artista - Titulo.lrc"
```

Si el LRC descargado no sincroniza bien, se puede ajustar con el playground (sección 5).

### Paso 4 — Reconstruir la playlist

```bash
bash scripts/build-playlist.sh
```

El script escanea `songs-karaoke/` y `lyrics/`, genera `player/playlist.json` y muestra un resumen:

```
Playlist generada: player/playlist.json
  12 canciones | 9 con letras sincronizadas
```

Si el servidor HTTP ya está corriendo, recargar el player en la TV con `F5` o `Ctrl+Shift+R` es suficiente.

---

## 7. Panel de Administración

El panel es una interfaz web para gestionar canciones, letras y el estado del sistema sin necesidad de tocar la terminal durante el evento.

### URL de acceso

```
http://192.168.1.182:8082/
```

El panel se arranca automáticamente con `bash scripts/start-event.sh`.

Para arrancarlo solo (sin reiniciar todo lo demás):

```bash
python3 admin/server.py
```

### Credenciales de acceso

El panel está protegido con HTTP Basic Auth. Al abrir la URL, el navegador pide usuario y contraseña.

- **Usuario / contraseña**: ver `admin/.env` (no versionado).
- Credenciales también disponibles en tuipass: `infra/karaoke-admin`.
- Para cambiarlas: editar `admin/.env` y reiniciar `python3 admin/server.py`.

> **Aviso de seguridad**: aún con auth, el panel está pensado para LAN privada. Basic Auth viaja en base64 sin TLS — NO exponer a Internet sin añadir HTTPS (reverse proxy con TLS) por delante.

### Qué muestra el panel

| Sección | Descripción |
|---|---|
| **Estado del sistema** | Verifica que los puertos 8080 (HTTP), 8081 (mic), 8082 (admin) responden, que el sink `karaoke_mic` existe, y muestra contadores: canciones totales, con karaoke, con LRC, con duración inconsistente, y resumen de `playlist.json`. |
| **Descargar canción nueva** | Permite descargar un MP3 desde una URL (YouTube u otra fuente soportada por yt-dlp). Pide URL + nombre destino. **El botón "Descargar" ahora ENCADENA todo el pipeline automáticamente**: (1) descarga MP3 a `songs/`, (2) genera versión karaoke en `songs-karaoke/`, (3) busca LRC sincronizado en `lyrics/`, (4) regenera `player/playlist.json`. No requiere acciones manuales adicionales. El job aparece como `Pipeline: <nombre>` en background. |
| **Canciones** | Tabla con todas las canciones de `songs/`. Indica si tiene karaoke (`songs-karaoke/`) y LRC (`lyrics/`), duraciones, y acciones por fila. |
| **Tareas en background** | Lista las últimas 30 operaciones (descarga, karaoke, búsqueda de LRC, rebuild). Muestra estado en vivo, duración y permite ver el log completo. |

### Botones por canción

| Botón | Acción |
|---|---|
| `KARAOKE` | Ejecuta `make-karaoke.sh` sobre el MP3 indicado. Genera `songs-karaoke/<nombre>.mp3`. |
| `LRC` | Pide query (artista + título) y lanza `syncedlyrics` contra lrclib.net. Guarda el resultado en `lyrics/<nombre>.lrc`. |
| `EDIT` | Abre el editor inline del LRC. Permite ver, modificar o pegar contenido del portapapeles. Guarda directamente sobre `lyrics/<nombre>.lrc`. |
| `×` | Borra la canción: elimina de `songs/`, `songs-karaoke/` y `lyrics/`. Pide confirmación antes. |

### Botón principal: `Rebuild playlist`

Reconstruye `player/playlist.json` ejecutando `scripts/build-playlist.sh`. Útil cuando se hacen renombrados manuales o se edita un LRC. **No es necesario** después de pulsar "Descargar" — el pipeline ya rebuildea automáticamente. Tras un rebuild, recargar el player con `Ctrl+Shift+R` para que coja los cambios.

### Auto-chain en descarga (qué hace cada paso del pipeline)

El botón "Descargar" lanza un único job background con 4 pasos secuenciales. El log del job (botón `LOG`) los separa con cabeceras claras:

```
=== STEP 1/4: Download (yt-dlp) ===            # baja MP3 a songs/
=== STEP 2/4: Karaoke (make-karaoke.sh) ===    # resta vocales → songs-karaoke/
=== STEP 3/4: LRC search (syncedlyrics) ===    # busca letra → lyrics/
=== STEP 4/4: Rebuild playlist ===             # regenera playlist.json
=== PIPELINE SUMMARY ===
  download : OK
  karaoke  : OK
  lrc      : OK | FAIL rc=N   ← suele fallar si no hay letra en lrclib
  playlist : OK
```

Comportamiento ante fallos:
- Si **STEP 1 (download) falla**: el pipeline para. Status final = `error`. STEPs 2 y 3 se skipean. STEP 4 sí se ejecuta (defensivo).
- Si **STEP 2 (karaoke) o STEP 3 (LRC) fallan**: los siguientes siguen ejecutándose. Status final = `partial`. El operador puede reintentar manualmente con los botones por canción.
- Si **todo OK**: status final = `success`.

LRC es el paso más frágil — muchas canciones no tienen letra sincronizada en lrclib.net. Un fallo en LRC NO impide que la canción aparezca en el player (queda como `lyrics: null` y el player la reproduce sin texto).

### Indicador de duración

La columna **Dur. karaoke** se pinta en rojo cuando la duración de `songs/<file>` y `songs-karaoke/<file>` difieren más de 1 segundo. Indica que el procesado de karaoke quedó truncado o corrupto. Pulsar `KARAOKE` para regenerarlo.

### Polling y tareas

Las tareas largas (descarga, karaoke, búsqueda LRC) se ejecutan en background. El panel hace polling cada ~1.5s y muestra el spinner mientras corren. Pulsar `LOG` abre el output completo del comando para diagnosticar fallos.

### Troubleshooting del panel

| Síntoma | Solución |
|---|---|
| El panel no carga (timeout) | Verificar que el puerto 8082 está activo: `lsof -i :8082`. Si no, ejecutar `python3 admin/server.py`. |
| "Status: 500" en todos los endpoints | Mirar el log del proceso: `cat admin/logs/admin-server.log`. |
| Una tarea queda en `RUNNING` para siempre | Abrir el log de esa tarea (botón `LOG`). El proceso underlying probablemente sigue vivo: `ps aux \| grep yt-dlp` o `ps aux \| grep ffmpeg`. |
| `LRC` no encuentra letra | Reintentar con query distinto (más simple, solo el título, o "Artista canción" sin caracteres especiales). El plan C es pegar manualmente la LRC en `EDIT`. |
| `KARAOKE` falla con `DURATION_MISMATCH` | Es bug conocido del script: el MP3 origen tiene metadatos raros. Re-codificarlo manualmente con `ffmpeg -i original.mp3 -c:a libmp3lame -q:a 2 songs/X.mp3`. |
| Quiero matar el admin server | `lsof -i :8082 -t \| xargs kill` |

### Pegar LRC manual desde el portapapeles

1. Copiar el LRC de cualquier sitio (web, editor, documento) al portapapeles del sistema
2. En el panel, pulsar `EDIT` en la fila de la canción
3. Pulsar el botón `Pegar de portapapeles`
4. Verificar que el contenido es correcto y pulsar `Guardar`

El navegador puede pedir permiso para acceder al portapapeles la primera vez.

---

## 8. Solicitud de canciones por cantantes

Para evitar caos durante el evento, los cantantes NO añaden canciones directamente: piden y el operador aprueba.

### Flujo

1. **Cantante** (móvil, `mic.html`): al final de la pantalla del micrófono ve una tarjeta "🎵 ¿No está tu canción?".
   - Pega una URL de YouTube o escribe el nombre.
   - Pulsa **Pedir canción**.
   - Recibe feedback `✓ Solicitud enviada, esperando aprobación`.
   - Ve la lista de sus peticiones con estado: PENDIENTE / APROBADA / RECHAZADA (auto-refresca cada 5s).

2. **Operador** (admin, tablet): en el panel aparece la sección "📥 Solicitudes pendientes" entre el control remoto y el estado del sistema.
   - Tabla con: cantante, URL/título, hace cuánto tiempo, botones acción.
   - **✅ Aprobar**: si es URL de YouTube → dispara el auto-chain (download → karaoke → LRC → rebuild) automáticamente. Si es solo un título → marca como aprobada y el operador descarga manualmente desde "Descargar canción nueva".
   - **❌ Rechazar**: marca como rechazada, el cantante ve el cambio en su móvil.
   - Auto-refresca cada 5s.

### Endpoints API

| Método | Ruta | Auth | Quién la usa |
|---|---|---|---|
| `POST` | `/api/requests/add` | **NO** | Cantante desde `mic.html` |
| `GET`  | `/api/requests[?status=pending\|approved\|rejected][&client_id=...]` | **NO** | Cantante (filtrado por su `client_id`) o admin |
| `POST` | `/api/requests/{id}/approve` | **Sí** | Operador |
| `POST` | `/api/requests/{id}/reject` | **Sí** | Operador |
| `DELETE` | `/api/requests/{id}` | **Sí** | Operador (borrar del historial) |

Body esperado en `POST /add`:
```json
{
  "requested_by": "Diego",
  "url_or_title": "https://youtube.com/watch?v=...",
  "client_id": "c-abc123"
}
```

### Persistencia y limites

- Estado guardado en `/tmp/karaoke-pending-requests.json`. Persiste a reinicio del servidor (recargado al boot).
- Conservamos solo las últimas 200 peticiones.
- **Rate limit**: máximo 5 peticiones `pending` por `client_id` (el del localStorage del móvil del cantante). Una vez aprobadas o rechazadas, la cuota se libera.

### Validaciones de seguridad

- `url_or_title`: mínimo 3, máximo 200 caracteres.
- Rechaza schemes peligrosos: `javascript:`, `data:`, `vbscript:`, `file:`, `about:`, `blob:`.
- Rechaza URLs no-http(s) (p. ej. `ftp://`).
- Rechaza HTML/script tags.
- `requested_by`: mínimo 2, máximo 30 caracteres.
- `client_id`: alfanumérico + `._-`, máximo 64 caracteres.

### Troubleshooting del flujo de solicitudes

| Síntoma | Solución |
|---|---|
| El cantante no ve el formulario de petición | Asegurarse de que `mic.html` se ha recargado (Ctrl+Shift+R en móvil) tras el último deploy. La sección está al final de la pantalla, debajo del footer. |
| Tras aprobar, no aparece job en background | Verificar que la URL es de YouTube (`youtube.com` o `youtu.be`). Si es solo un título, se marca aprobada pero el operador debe descargar manualmente. |
| El cantante ve "ya tienes 5 peticiones pendientes" | Procesar las pendientes (aprobar o rechazar). Tras decidirlas, el cantante puede volver a pedir. |
| Las peticiones reaparecen tras reiniciar | Es intencional — se persisten en `/tmp/karaoke-pending-requests.json`. Para limpiar todo: detener server, `rm /tmp/karaoke-pending-requests.json`, reiniciar. |
| El cantante recibe "scheme no permitido" | Está intentando enviar algo distinto de `http://` o `https://`. Pedirle que copie la URL completa de YouTube desde el navegador. |

### Instalar el admin como app en la tablet Android (PWA)

El panel está empaquetado como **PWA (Progressive Web App)**. Se puede instalar en la tablet como si fuera una app nativa: aparece con icono propio en el launcher, abre en pantalla completa (sin barra de Chrome) y arranca incluso si el WiFi está caído (la UI se cachea localmente; los datos en vivo necesitan red).

**Pasos en la tablet (recomendado: Chrome o Edge):**

1. Conectar la tablet a la WiFi `MD-Karaoke-LAN` (la misma que el resto del sistema).
2. Abrir Chrome y entrar a `https://player.zensitpro.pro/admin/` (o, si no hay internet hacia el edge, `http://192.168.1.182:8082/`).
3. Introducir las credenciales `diego` / contraseña del .env.
4. Cuando cargue el panel, verás arriba a la derecha un botón rosa: **📥 Instalar como app**. Pulsarlo y aceptar el diálogo de Chrome ("Instalar").
   - **Alternativa**: menú de Chrome (⋮) → "Añadir a pantalla de inicio" o "Instalar app".
5. En unos segundos aparecerá el icono **MDK Admin** en el launcher de Android.
6. Pulsarlo → la app abre en pantalla completa, con su propio splash screen, sin barra de URL.

Después de instalada:

- El icono persiste tras cerrar Chrome o reiniciar la tablet.
- La app se actualiza sola cuando el servidor publica una versión nueva (el Service Worker revalida al primer fetch).
- Si la tablet pierde red WiFi durante el evento, la app sigue **abriéndose** (UI cacheada). Los datos en vivo ("AHORA SUENA", lista de canciones) requieren red — mostrarán "—" hasta que vuelva la conexión.

**Desinstalar**: mantener pulsado el icono → "Desinstalar" (o desde Ajustes Android → Apps → MDK Admin → Desinstalar).

**iOS / Safari**: el botón "📥 Instalar como app" no aparece (Apple no soporta `beforeinstallprompt`). Para instalar: botón compartir de Safari → "Añadir a pantalla de inicio".

---

## 8. Troubleshooting

### Los estilos no cargan o la página se ve mal

El navegador está sirviendo una versión en caché.

**Solución**: `Ctrl+Shift+R` (recarga forzada sin caché)

---

### No suena el audio

**Verificación rápida**:

```bash
# ¿El servidor HTTP está corriendo?
lsof -i :8080
```

Si no devuelve nada, el servidor no está activo. Ejecutar:

```bash
npx serve . -l 8080 -c serve.json --no-clipboard
```

O reiniciar todo:

```bash
bash scripts/start-event.sh
```

---

### El micrófono no conecta

**Paso 1** — Verificar que el servidor WebSocket está activo:

```bash
lsof -i :8081
```

Si no devuelve nada, arrancarlo manualmente:

```bash
python3 scripts/mic-server.py &
```

**Paso 2** — Verificar que el sink de audio existe:

```bash
pactl list sinks short | grep karaoke_mic
```

Si no aparece `karaoke_mic`, crearlo:

```bash
bash scripts/setup-mic-sink.sh
```

**Paso 3** — Verificar que el móvil y el servidor están en la misma red Wi-Fi.

---

### La letra está desincronizada

- Ajuste rápido durante la presentación: teclas `↑` / `↓` en el player (±0,5s por pulsación)
- Ajuste fino antes del evento: usar el playground (`/player/playground.html`) con botones de ±0,1s
- Si el desface es constante en todas las canciones, el archivo LRC tiene un offset global incorrecto

---

### La playlist no incluye una canción recién añadida

El archivo `playlist.json` no se actualiza solo. Ejecutar:

```bash
bash scripts/build-playlist.sh
```

Y recargar el player con `Ctrl+Shift+R`.

---

### El script `start-event.sh` falla al arrancar

Causa más frecuente: dependencias no instaladas.

```bash
# Verificar Node.js y npx
node --version
npx --version

# Verificar Python y websockets
python3 --version
python3 -c "import websockets; print('OK')"

# Verificar FFmpeg (para make-karaoke.sh)
ffmpeg -version | head -1

# Verificar PulseAudio/PipeWire
pactl info | grep "Server Name"
```

Si falta alguna dependencia, instalarla antes del evento y verificar de nuevo.

---

## Acceso público vía Tailscale Funnel

### Cómo funciona

El servidor OCLAW (192.168.1.182) expone el karaoke a Internet vía **Tailscale Funnel**, sin necesidad de abrir puertos en el router ni configurar DNS propio. La URL pública usa el certificado Let's Encrypt automático de Tailscale.

Arquitectura:
```
Internet → Tailscale Funnel relay (176.58.x.x:443)
        → tailnet → OCLAW :8091 (Caddy local)
        → path routing:
              /api/*   → admin :8082
              /admin/* → admin :8082
              /mic-ws  → mic-server :8081 (WebSocket)
              resto    → player :8080
```

### Comandos de operación

```bash
# Ver estado del Funnel
sudo tailscale funnel status

# Apagar Funnel (deja stack interno intacto)
sudo tailscale funnel --https=443 off

# Volver a encenderlo
sudo tailscale funnel --bg http://localhost:8091

# Ver estado del proxy local (Caddy en :8091)
sudo systemctl status caddy
sudo systemctl reload caddy   # Tras editar /etc/caddy/Caddyfile
```

### Verificación rápida desde un móvil 4G (sin Wi-Fi del evento)

Abrir `https://zen-admin-oclaw.tailc9332.ts.net/karaoke` — debe cargar la pantalla de micrófono con el botón verde "Cantar".

### Migración a otro equipo (Lenovo, etc.)

Cuando cambies el servidor:
1. Instalar Tailscale en la nueva máquina, login con la misma cuenta.
2. Replicar Caddy `:8091` con el mismo path-routing.
3. En el equipo nuevo: `sudo tailscale funnel --bg http://localhost:8091`.
4. En OCLAW: `sudo tailscale funnel --https=443 off`.
5. La URL pública cambia al hostname del equipo nuevo (`<host>.<tailnet>.ts.net`).
