# Problemas pendientes — para Diego al volver

## RECOVERY EN 60s

Si algo se rompe durante el evento, consulta `DISASTER-RECOVERY.md` para fixes paso a paso.

Backup pre-evento: ejecutar `bash scripts/backup-pre-event.sh` antes de salir hacia el local.

---

## ÚLTIMO SMOKE TEST 2026-05-14 21:50

**Resultado: GREEN — 37/37 OK** (reporte completo en `/tmp/karaoke-smoke-test-report.md`).

Ejecutado end-to-end sobre la stack viva en OCLAW (`192.168.1.182`):

- Player :8080, mic-server :8081, admin :8082 → todos responden correctamente
- Auth Basic en admin → 401 sin credenciales, 200 con `diego:D13g0$$2026`
- Endpoints públicos (now-playing, requests/add, mic/devices, manifest, sw.js, icons) → OK sin auth
- Round-trip control remoto POST→GET→ACK → OK
- Caddy edge LXC (`https://player.zensitpro.pro`) → OK incluyendo WS handshake
- Tailscale Funnel público (`https://zen-admin-oclaw.tailc9332.ts.net`) → OK desde IP pública 176.58.90.46 (desde la propia VM hay self-loop esperado; clientes externos no afectados)
- Frontend mic.html / player / admin / monitor.html → todas las secciones presentes
- Procesos vivos: admin/server, mic-server, caddy, karaoke_mic sink RUNNING

**Detalles no-bloqueantes detectados (todos by-design, no son bugs):**

1. `serve.json` aplica clean URLs → `.html` redirige 301 a la ruta sin extensión. El test esperaba 200 directo pero el contenido final llega bien
2. `GET /api/songs` devuelve `{songs:[...]}` (objeto envuelto), no array raw. Frontend ya consume así
3. Caddy edge sirve HTTP/2 por defecto; WS upgrade requiere HTTP/1.1 (estándar) — los browsers ya hacen esto automáticamente
4. Funnel desde la propia VM falla con TLS alert (Tailscale rechaza self-loops); acceso externo no está afectado
5. `tailscale serve` solo expone `/` → 8091; el routing `/karaoke`, `/admin`, `/mic-ws` lo hace Caddy local en :8091. Arquitectura correcta

**Riesgos NO cubiertos por el smoke test (siguen abiertos):**
- CRIT-1: audio HDMI nunca probado en el Lenovo físico del evento
- CRIT-2: estrategia de acceso público depende de la red del local
- LRC: 6 canciones sin LRC + 4 con drift > 15s

**Veredicto**: stack software lista para mañana en este equipo. El bloqueo real es la migración al Lenovo (ver `DEPLOY-LENOVO.md`).

---

> **Auditoría QA**: 2026-05-14 21:30 (~ 8h antes del evento del 2026-05-15)
> **Auditor**: Claude Opus 4.7 (sesión `lan-media`)
> **Estado general**: stack 100% verde en la VM dev (`192.168.1.182`). El verdadero riesgo está en el **deployment al Lenovo del evento**, que NO he podido probar (es otro equipo físico).

---

## 0. RESUMEN EJECUTIVO

| Pieza | Estado verificado | Riesgo evento |
|---|---|---|
| Player HTTP :8080 | ✅ HTTP 200, sirve estáticos correctamente | Bajo |
| playlist.json | ✅ 44 entradas, 38 con LRC, todos los paths existen físicamente | Bajo |
| mic-server :8081 | ✅ Escucha, acepta WS, handshake OK, broadcast a monitors OK | Bajo |
| admin :8082 | ✅ FastAPI con Basic Auth, todos los endpoints probados | Bajo |
| Caddy edge | ✅ `https://player.zensitpro.pro` OK, WSS OK, admin OK | Bajo |
| Control remoto player | ✅ POST → GET → ACK round-trip funciona (LAN + edge) | Bajo |
| Now-playing | ✅ Player → admin → mic.html flow OK (fix aplicado) | Bajo (fix nuevo) |
| Mic device tracking | ✅ JSON `/tmp/karaoke-mic-devices.json` se escribe atómico | Bajo |
| Audio HDMI en Lenovo evento | ⚠️ **NO PROBADO** — la VM dev solo tiene null-sink virtual | **ALTO** |
| Exposición pública (Tailscale/CF) | ⚠️ **NO PROBADO** — depende de la red del evento | Medio |
| LRC sin sync (6 canciones) | ⚠️ Listadas abajo — no se podrá leer letra | Bajo (no rompe player) |
| LRC con drift > 15s (4 canciones) | ⚠️ Validate-sync warning — letra se acaba antes que la música | Bajo |

---

## CRÍTICOS (resolver ANTES del evento)

### CRIT-1 — Verificar audio HDMI en el Lenovo físico del evento

**Por qué CRÍTICO**: en la VM dev (`zen-admin-oclaw`) NO hay sink físico — solo `karaoke_mic` virtual. Esto significa que el script `scripts/setup-mic-sink.sh` y la cadena de loopback **NUNCA se han ejecutado en un equipo con HDMI real**. Hay riesgo concreto de que:

1. El `module-loopback` se enganche al sink incorrecto.
2. El `pactl set-default-sink` haga falta manualmente.
3. El sink HDMI no esté disponible si el Lenovo no tiene proyector conectado en el momento del arranque.

**Fix**: seguir el paso 4 de `DEPLOY-LENOVO.md` AL PIE DE LA LETRA. Especial atención a §4.4 — verificar que el default sink NO es `karaoke_mic` antes de empezar.

### CRIT-2 — Decidir método de acceso público

El frontend del player + mic + monitor está pensado para LAN. Si todos los cantantes están en la misma WiFi del evento, NO HACE FALTA exposición pública.

**Opciones, en orden de simplicidad** (lo más simple es lo más fiable para mañana):

A) **LAN-only sin exposición pública** (RECOMENDADO si los cantantes pueden conectarse a la WiFi del local del evento).
   - 0 dependencias externas
   - El QR `player/mic-qr.png` apunta a una IP LAN
   - Hay que regenerarlo si la IP del Lenovo del evento es distinta de 192.168.1.182

B) **Tailscale Funnel** (recomendado si los cantantes tienen 4G/5G y no se pueden conectar al WiFi del local).
   - Requiere Tailscale instalado y autenticado en el Lenovo
   - Una URL pública `https://<hostname>.<tailnet>.ts.net` única
   - El cantante NO necesita estar en Tailscale (Funnel expone al mundo)
   - Limitación: un solo puerto público → necesita Caddy local que multiplexe player+admin+mic-ws

C) **player.zensitpro.pro vía Caddy edge LXC 168** (la ruta YA configurada).
   - Solo funciona si el Lenovo del evento está en la LAN 192.168.1.x del Diego (donde vive el edge)
   - El edge enruta `player.zensitpro.pro` → `192.168.1.182:8080`
   - Requiere que el Lenovo del evento tenga IP `192.168.1.182` (apagar la VM dev) o editar Caddyfile del edge para apuntar a la nueva IP

D) **Cloudflare Tunnel**.
   - Más burocracia (login, create tunnel, route dns, service install)
   - Útil si el evento está físicamente fuera de la LAN del Diego

**Mi recomendación**: A) LAN-only. Si la wifi del local del evento permite muchos clientes, esto es lo más seguro y simple.

### CRIT-3 — admin/.env NO existe → credenciales hardcoded

`admin/server.py` carga `.env` desde `admin/.env`, pero ese archivo no existe en el repo. Por tanto está usando los defaults hardcoded:

- ADMIN_USER = `diego`
- ADMIN_PASSWORD = `D13g0$$2026`

Esto **funciona**, pero si Diego quiere cambiar la password antes del evento debe crear `admin/.env`:

```bash
cat > admin/.env <<EOF
ADMIN_USER=diego
ADMIN_PASSWORD=otra_password_aqui
EOF
chmod 600 admin/.env
# Reiniciar admin server
pkill -f admin/server.py
nohup python3 admin/server.py > admin/logs/admin-server.log 2>&1 &
```

**Riesgo**: si el evento está expuesto al exterior (CRIT-2 b/c/d), una password tan obvia es **inaceptable**. Cambiarla antes de exponer.

### CRIT-4 — Diego debe verificar que el player de TV se carga vía URL correcta

Si el Lenovo es el equipo conectado a la TV: usar `http://localhost:8080/player/` (lo más fiable).

Si el equipo de TV es distinto del Lenovo (Mecool, otro PC): usar `http://<IP-LAN-del-Lenovo>:8080/player/` — la URL pública `https://player.zensitpro.pro/player/` SOLO si va a usar la ruta vía Caddy edge (CRIT-2 c).

---

## IMPORTANTES (resolver si hay tiempo)

### IMP-1 — 6 canciones sin LRC (no se verán letras)

Las siguientes canciones reproducen audio pero NO muestran letra:

| # | Canción | Notas |
|---|---|---|
| 18 | 𝗗𝗲𝗺𝗯𝗼𝘄 𝗗𝗲 𝗙𝗮𝘃𝗲𝗹𝗮 - Mauro Catalini... | Hay LRC raíz `Dembow De Favela.lrc` pero es scraping Genius (no sync) |
| 31 | Salvi x CHCKN x Anxther Sun - KUMBALA | Hay LRC raíz `KUMBALA.lrc` pero no es sync válido |
| 35 | ZONA PRIETA - CHICA PRÁCTICA | Sin LRC en ningún sitio |
| 36 | Como E' (Salsa Choke) ZIN NOW | Hay LRC raíz `Como E choke.lrc` pero no es sync |
| 37 | ¡UNA CUMBIA SABROSA...! DAME CANDELA | Hay `Dame Candela.lrc` pero no sync |
| 39 | Junior Lomi Ft Chukito - PARTY | Sin LRC en ningún sitio |

**Workaround rápido**: si Diego quiere letras manuales, abrir el playground:
- `http://<LAN>:8080/player/playground.html`
- Cargar el MP3, transcribir letra línea a línea con timestamps
- Guardar como `lyrics/<nombre exacto del mp3>.lrc`
- `bash scripts/build-playlist.sh` para incluirla

**Impacto en evento**: las canciones SE REPRODUCEN igual, solo no aparece letra. La gente puede cantar de memoria.

### IMP-2 — 4 canciones con drift de LRC > 15s (la letra se acaba antes que la música)

`scripts/validate-sync.sh` reporta:

| # | Canción | Diff (MP3 dur vs último timestamp LRC) |
|---|---|---|
| 33 | India Martinez, Andy Rivera - 5 SENTÍOS | 41s — el LRC se acaba 41s antes del fin del MP3 |
| 38 | Que Vuelvas | 33s |
| 41 | Thalía - Mujer Latina | 21s |
| 43 | NA - Maria Becerra, XROSS - 7 VIDAS GLADYS | 18s |

**Causa probable**: la versión LRC que se descargó corresponde a una edición más corta de la canción (radio edit vs original). El resto del LRC sí está bien sincronizado al principio.

**Workaround**: nada que hacer salvo regrabar el LRC. Aceptable — la mayoría del público ya no canta los outros.

### IMP-3 — Archivos LRC sueltos en raíz del proyecto

Hay 10 archivos `.lrc` en la raíz `/home/zen-admin/projects/active/lan-media/*.lrc` que SON BASURA (resultado del scraping Genius, no son LRC válidos — empiezan con "3 Contributors" en vez de `[00:00.00]`).

**No afectan el evento** porque `build-playlist.sh` no los lee — solo lee `lyrics/`. Pero ensucian git status.

**Si quieres limpiarlos**:
```bash
cd /home/zen-admin/projects/active/lan-media
mkdir -p lyrics/_scraping-rejected
mv *.lrc lyrics/_scraping-rejected/    # Los mueve fuera de la raíz
```

NO recomiendo hacerlo antes del evento — bajo beneficio, posible ruido en git.

### IMP-4 — Disco al 97% en la VM dev

`/dev/sda2` (raíz del Lenovo VM dev) está al 97% (2.5 GB libres). Si el agente del download chained baja canciones via yt-dlp, podría llenarse el disco y romper el sistema entero.

**NO aplica al Lenovo del evento** (es otro equipo). Pero si por alguna razón se usa esta VM dev como servidor del evento, **limpiar primero**:

```bash
docker system prune -af   # si Docker está en uso
sudo apt clean
journalctl --vacuum-time=7d
```

### IMP-5 — admin/index.html requiere `/static/` para JS+CSS, pero manifest+icons cargan de raíz

Bug menor: la PWA `manifest.json`, `sw.js`, `icons/*` se sirven desde la raíz `/`, mientras `admin.js` y `admin.css` solo desde `/static/`. **Funciona** porque el HTML referencia cada uno con su ruta correcta, pero es inconsistente. Si Diego ve un mensaje raro de Service Worker, ignorar — no afecta funcionalidad.

---

## NICE-TO-HAVE (post-evento)

### NH-1 — Centralizar la resolución de admin API base

El frontend tiene 3 sitios distintos que calculan la URL del admin:
- `player/app.js` → `adminApiBase()`
- `player/mic.html` → `nowPlayingUrl()`
- `player/monitor.html` → ya usa el patrón correcto

Convergerlas a una función compartida en un `player/api-base.js`. Hoy las tres están alineadas (post-fix de hoy), pero si alguien edita una y olvida las otras, vuelve el bug.

### NH-2 — admin/.env opcional con SECRET_KEY

Si el admin se expone a Internet, además de la password debería tener rate limiting (slowapi) o fail2ban en el log. Hoy es solo Basic Auth — un atacante puede brute-force a 1000 req/s.

### NH-3 — Refactor make-karaoke con demucs (vocal-removal real)

El método actual (`ffmpeg pan` con resta de canales) elimina vocales **centradas** pero deja partes con reverb fuerte. Para post-evento, evaluar `demucs` para separación stem-aware (más lento pero limpio).

### NH-4 — Auto-regenerar mic-qr.png con la IP actual

El script `scripts/start-event.sh` imprime la IP actual pero el QR `player/mic-qr.png` está congelado a una IP. Generar dinámicamente al arrancar (qrencode + IP detectada).

---

## DECISIONES PENDIENTES (para Diego al volver)

### DEC-1 — Método de exposición pública (ver CRIT-2)

Hoy elijo "LAN-only" si la WiFi del local lo permite. Si no:
- Tailscale Funnel (más moderno, una URL random pública)
- Caddy edge `player.zensitpro.pro` (ya configurado, pero el Lenovo del evento debe tener IP 192.168.1.182)
- CF Tunnel (más burocracia)

### DEC-2 — ¿El Lenovo del evento es el mismo HW que la VM dev?

Si **sí** (improbable, la VM dev parece ser un Proxmox VM con `ens18`): no hay que migrar nada, solo apagar otros procesos y validar HDMI.

Si **no** (probable): seguir `DEPLOY-LENOVO.md` para copiar el stack.

### DEC-3 — ¿IP estática o DHCP reservation?

El Caddy edge actual apunta a `192.168.1.182`. Si el Lenovo del evento va a usar la ruta `player.zensitpro.pro`:
- Opción A: reservar `192.168.1.182` en el Livebox para el MAC del Lenovo del evento, y apagar la VM dev durante el evento
- Opción B: editar el Caddyfile del edge (LXC 168 / 192.168.1.59) para apuntar a la nueva IP

---

## QUÉ HE PROBADO HOY

| Test | Resultado |
|---|---|
| GET `/player/` LAN | ✅ HTTP 200 |
| GET `/player/{mic,monitor,playground,mic-join}.html` LAN | ✅ HTTP 301 → 200 |
| GET `/player/playlist.json` (44 entradas, paths reales) | ✅ |
| GET `/api/status` admin sin auth | ✅ HTTP 401 |
| GET `/api/status` admin con auth | ✅ HTTP 200 |
| GET `/api/{songs,jobs,mic/devices,mic/muted,now-playing,player/command}` con auth | ✅ Todos 200 |
| GET `/api/{now-playing,mic/devices,player/command}` SIN auth (públicos) | ✅ 200 |
| WS handshake mic-server LAN `ws://192.168.1.182:8081` | ✅ Switching Protocols 101 |
| WS handshake mic vía edge `wss://player.zensitpro.pro/mic-ws/` | ✅ |
| Edge: `/player/`, `/admin/`, `/admin/api/*`, `/mic-ws/` | ✅ Todos rutados OK |
| Round-trip player command bus (POST + GET + ACK) | ✅ LAN y edge |
| POST now-playing/set + GET via mic.html flow | ✅ LAN y edge (tras fix) |
| `pactl` sinks + módulos cargados | ✅ Configuración correcta para VM dev |
| Tools instalados: yt-dlp, syncedlyrics, ffmpeg, ffprobe | ✅ |
| `validate-sync.sh` reporta WARNs documentados | ✅ |

## QUÉ NO HE PODIDO PROBAR (no había forma)

- ❌ Audio físico por HDMI (la VM dev no tiene hardware audio físico)
- ❌ Vocal removal con cantantes humanos reales en mic mientras suena música
- ❌ El Mecool KM1 / Android TV abriendo el player (no tengo el HW aquí)
- ❌ El Lenovo físico del evento (es otro equipo, no este VM dev)
- ❌ Carga simultánea con 10+ móviles conectados al mic (sin testers humanos)
- ❌ Auto-chain de descarga YouTube (no quiero gastar disco al 97%)
- ❌ Estabilidad bajo carga >1 hora (sesión QA dura mientras escribo esto)

---

## QUÉ HE TOCADO HOY (para que Diego sepa)

| Archivo | Cambio | Por qué |
|---|---|---|
| `player/app.js` | Refactor de `nowPlayingEndpoint()` + `adminApiBase()` para usar `/admin/api/...` en HTTPS edge | Bug: el path `/api/...` vía edge devolvía 404 (Caddy enruta `/admin/*` no `/api/*`) |
| `player/mic.html` | Misma fix: `nowPlayingUrl()` usa `/admin/api/now-playing` en HTTPS edge | Mismo bug |
| `DEPLOY-LENOVO.md` | NUEVO doc — guía paso a paso para desplegar en el Lenovo del evento | El briefing pedía algo claro y copy-pasteable |
| `PROBLEMAS-PENDIENTES.md` | NUEVO doc — este | Briefing del usuario |

**Housekeeping (1 acción)**:
Durante la auditoría detecté duplicados del MP3 `David Guetta - Sexy Chick`. Borré 2 copias byte-idénticas (entradas 45 y 46 con md5 idéntico al 44 que ya existía). Regeneré playlist a 44/38.

**ALERTA — otra sesión activa**: tras mi borrado, una OTRA sesión Claude (`tmux ls` muestra `claude-lan-media`) lanzó otro `/api/download` del mismo Sexy Chick que volvió a crear el `45 - David Guetta...Sexy Chick (Official Video).mp3` (job `aa2fae429a4a` a las 21:32:58). NO la borré una segunda vez porque puede ser algo deliberado del otro agente o un test en curso.

Al volver Diego, decidir:
- Si la nueva 45 es deseada, dejarla
- Si no, `rm "songs/45 - David Guetta - David Guetta Feat..."` + `rm` del karaoke equivalente + `bash scripts/build-playlist.sh`

El track 44 (`David Guetta - Sexy Chick (Akon).mp3` original con LRC) sigue intacto.

**Sesiones tmux activas detectadas** (no las toqué — regla del briefing):
- `claude-infra`, `claude-lan-media` (¡otra del mismo proyecto!), `claude-lenovo`, `claude-opencode`, `claude-vaultwarden-oracle`

**NO he tocado**:
- `admin/server.py` (ningún cambio)
- `scripts/*` (ningún cambio)
- `lyrics/` (ningún cambio)
- `player/index.html`, `player/style.css`, `player/lrc-parser.js`, `player/monitor.html`, `player/playground.html`, `player/mic-join.html`
- `serve.json`, `admin/.env` (no existe, no creo nada)
- Git: no he hecho commit ni push de nada

---

## CHECKLIST DEL DÍA DEL EVENTO

```
□ Lenovo del evento conectado a router/switch del local (LAN)
□ Audio HDMI confirmado (paplay test del DEPLOY §4.5)
□ pactl get-default-sink ≠ karaoke_mic (paso §4.4 del DEPLOY)
□ scripts/start-event.sh arranca limpio (vista §5)
□ curl test admin con auth devuelve 200 (§5.1)
□ Player abre en TV con http://localhost:8080/player/ (o LAN-IP)
□ Un primer móvil conecta al mic.html y se OYE su voz por TV
□ Tablet del operador abre admin :8082 con login OK
□ Plan B: USB con MP3+LRC al lado del Lenovo por si todo falla
□ Cable HDMI verificado, cargador del Lenovo conectado a corriente
□ Volumen de TV / amplificador subido al menos a 60%
□ (Si se usa edge) Caddyfile del edge LXC 168 apunta a la IP correcta del Lenovo del evento
□ (Si se usa edge) Diego NO ha apagado por error la VM dev si el edge sigue apuntando a 192.168.1.182
```

---

## CONTACTO PARA EMERGENCIA

Si todo falla durante el evento:
1. Plan B USB+VLC nativo Android TV (mismo nombre LRC y MP3 en el mismo folder)
2. YouTube Karaoke directo (buscar "Canción Karaoke" — los grandes hits están)
3. Spotify con letra (no sincronizada pero útil)

El sistema falla, la gente canta igual — no es el fin del mundo.
