# Plan de refactor tras la auditoría del frontend > **Cerrado.** Todas las partes están hechas; el plan activo es `frontend-structure-plan.md`. Plan de trabajo derivado de la auditoría del `web-player` (2026-09-24). Cada parte es un PR independiente, revisable por separado, que deja `npm run check` y `npm run lint` en verde. Antes de empezar una parte, mira el **Registro** del final. Leyenda de tamaño: **S** (< 1 h) · **M** (medio día) · **L** (1 día o más). Las partes marcadas con 🔗 dependen de cambios en `Musify.Api`. ## Orden y dependencias ``` P0 Red de seguridad └─ P1 Seguridad ─ P2 Reproductor ─ P3 Bugs de páginas ─ P4 Código muerto └─ P5 Modelo Track único ├─ P6 Capa de servidor ─ P7 Formularios y validación │ └─ P8 Rendimiento 🔗 └─ P9 Responsabilidades ─ P10 Componentes y páginas └─ P11 Consistencia ─ P12 Guardarraíles y docs ``` P1 a P4 son arreglos acotados y se pueden mergear en cualquier orden. De P5 en adelante el orden importa: cada parte simplifica la siguiente. --- ## P0 · Red de seguridad (M) Objetivo: tener tests de las funciones puras antes de tocar lógica. - Añadir Vitest (`vitest`, script `npm run test`, incluido en `lint` o en CI). - Tests para lo que ya es puro: `fmtTime`, `fmtDurationLong`, `plural`, `albumMeta` (`utils/format.ts`), `findTopResult` (`data/search.ts`), `findConflict`, `genreHue` (`data/genres.ts`), `pickGreeting` (`server/greeting.ts`), `parseAlbumForm` (`server/albumForm.ts`), `appendUnique`, `shuffle`. - Extraer las operaciones de cola de `PlayerState` a funciones puras en `player/queue.ts` (`insertNext`, `append`, `move`, `nextIndex`, `previousIndex`) y testearlas. `PlayerState` pasa a llamarlas. Esto prepara P2. **Cierre:** `npm run test` en verde y cubre las funciones anteriores. --- ## P1 · Seguridad (S) 1. **Redirección abierta con `returnTo`.** - Nueva función `safeReturnTo(value: string | null | undefined): string` en `$lib/server/auth.ts`: acepta solo rutas que empiecen por `/` y no por `//` ni `/\`; si no, devuelve `/`. - Aplicarla en `routes/auth/+page.server.ts` (ambos usos), en `routes/auth/login/+server.ts` (antes de guardar la cookie) y en `routes/auth/callback/+server.ts` (al leer la cookie). 2. **`SESSION_SECRET` obligatorio.** En `$lib/server/config.ts`, `sessionSecret` pasa por `required()` y exige una longitud mínima de 32 caracteres. Actualizar el comentario de `.env.example`. 3. **Validar las entradas de los endpoints `/api`.** - `api/likes/+server.ts`: no reenviar el body tal cual; leer `trackId`, comprobar que es un string no vacío y devolver 400 si no. - `api/listens/[id]/progress/+server.ts`: envolver `request.json()` para devolver 400 con JSON inválido en vez de 500. - `server/image.ts`: `encodeURIComponent(id)` al construir la URL de upstream. - `api/tracks/+server.ts`: acotar `pageSize` con un máximo (`MAX_PAGE_SIZE` en `config.ts`). **Cierre:** `/auth?returnTo=https://example.com` acaba en `/`; arrancar sin `SESSION_SECRET` falla con un mensaje claro; tests de `safeReturnTo`. --- ## P2 · Reproductor y cola (M) 1. **Duplicados en la cola** (`player.svelte.ts` `playNext`/`appendToQueue`, `Queue.svelte`). - `playNext` mueve la pista si ya estaba en la cola, en vez de duplicarla. - `appendToQueue` omite las pistas que ya están pendientes. - Con esto las claves `(track.id)` vuelven a ser únicas y `#index()` es fiable. 2. **Fin de cola.** Con repetir desactivado, al terminar la última pista el reproductor se detiene en vez de volver al principio. Si más adelante se quiere "repetir todo", `repeat` pasará a `'off' | 'all' | 'one'` (queda fuera de este plan). 3. **La cola se abre sola** (`+layout.svelte:44`). Abrirla solo la primera vez que empieza a sonar algo en la sesión (`currentId` pasa de `null` a un valor) y no volver a abrirla si el usuario la cierra. 4. **Rollback de "Me gusta"** (`liked.svelte.ts:69`). En caso de error, revertir solo la entrada de ese id, no sustituir el mapa entero. 5. **localStorage seguro.** - Nuevo `$lib/utils/storage.ts` con `readStorage(key, fallback)` y `writeStorage(key, value)` envueltos en try/catch. - Volumen: validar y acotar a 0–100 (hoy puede quedar `NaN`). - Unificar claves: `musify.player.volume`, `musify.player.muted`, `musify.theme`. - Persistir el volumen siempre, no solo cuando existe el elemento `