14 KiB
Plan de estructura del frontend
Plan derivado de la revisión de estructura del web-player (2026-09-24), hecha después de cerrar frontend-audit-plan.md (P0–P12). Cada parte es un PR independiente que deja npm run check y npm run lint en verde y no cambia el comportamiento visible. 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).
Orden y dependencias
E1 Layout del grupo (app)
└─ E2 Carpeta shell
└─ E3 Frontera de ui/ ─ E6 Explorar en componentes
E4 Ubicación de estados (independiente)
E5 Capa de servidor (independiente)
E7 Acento fuera del reproductor (después de E1)
E9 Tests (antes o junto con E1 y E5)
E8 Documentación (al final)
E1 → E2 → E3 van en orden porque tocan los mismos imports. E4 y E5 se pueden hacer en cualquier momento. Conviene hacer E9 primero, o al menos sus puntos 1 y 2, para que E1 y E5 muevan el código de sesión con tests que detecten si algo se rompe.
E1 · Layout del grupo (app) (M)
Problema: el grupo (app) no tiene +layout. El shell y las cargas de la app están en la raíz, que distingue la pantalla de login a mano (isAuthPage en +layout.svelte, PUBLIC_PATHS en +layout.server.ts). Por eso /auth también consulta playbackConfig, y +error.svelte se pinta fuera del shell (un 404 dentro de la app pierde la barra lateral y el reproductor).
routes/+layout.sveltese queda con lo global: fuentes,theme.css,layout.css, favicon, el<style>inicial del tema,animateThemeHuey<DialogHost />. Renderizachildrensin envoltorio.routes/+layout.server.tsdesaparece (o queda vacío). Si alguna ruta fuera de(app)necesitauser, lo lee delocalsen su propioload.- Nuevo
routes/(app)/+layout.server.tscon todo lo que hoy hay en la raíz:getAllowAnonymousListening, el guard (loginRedirectsi no hay usuario ni escucha anónima),accountUrl, playlists del usuario, me gusta y última pista. SinPUBLIC_PATHS. - Nuevo
routes/(app)/+layout.sveltecon el shell: backdrop,Sidebar,MobileHeader,TopBar,<main>con el{#key},Queue,PlayerDock, el modal de crear playlist, la hidratación delikedyplayer, la apertura automática de la cola y el atajo de la barra espaciadora. DesaparecenisAuthPagey la rama!data.user && !data.allowAnonymousListening, porque el guard ya ha redirigido. - Nuevo
routes/(app)/+error.svelte(el contenido del actual) para que los errores dentro de la app se vean con el shell. El+error.sveltede la raíz se queda como fallback mínimo para errores fuera del grupo. - No quitar los
requireUser/optionalUserde las páginas niauthedAction. Losloadde página se ejecutan en paralelo con los del layout, y las acciones de formulario no ejecutan elloaddel layout, así que el guard del layout no basta.
Cierre: grep -rn "isAuthPage\|PUBLIC_PATHS" src vacío; /auth no llama a la API; un 404 en /albums/xxx se ve dentro del shell; prueba manual con sesión, sin sesión y con escucha anónima.
E2 · Carpeta shell (S)
Problema: hay dos carpetas layout con significados distintos (components/layout/ es el shell de la app y components/ui/layout/ son piezas para montar páginas). AccountMenu está en ui/overlay/, pero solo lo usan TopBar y MobileHeader.
- Renombrar
components/layout/acomponents/shell/(Sidebar,TopBar,TabsBar,MobileHeader,navLinks.ts). - Mover
components/ui/overlay/AccountMenu.svelteacomponents/shell/AccountMenu.svelte. - Actualizar imports en
(app)/+layout.svelteycomponents/player/PlayerDock.svelte.
Cierre: grep -rn "components/layout/\|overlay/AccountMenu" src vacío.
E3 · Frontera de components/ui/ (M)
Problema: ui/ se presenta como el design system, pero cuatro componentes dependen del estado global de la app. TrackList y PlayAllButton importan player. TrackContextMenu y AlbumContextMenu importan player, y AlbumContextMenu también importa $lib/data/albums. El resto de ui/media/ son componentes de presentación sin estado (reciben props), y así deben quedarse.
- Nueva carpeta
components/music/para los componentes que conectan la interfaz con el estado de la app:ui/media/TrackList.svelte→music/TrackList.svelteui/media/PlayAllButton.svelte→music/PlayAllButton.svelteui/overlay/TrackContextMenu.svelte→music/TrackContextMenu.svelteui/overlay/AlbumContextMenu.svelte→music/AlbumContextMenu.svelte
- Regla de ESLint: un bloque con
files: ['src/lib/components/ui/**']yno-restricted-importsque prohíba$lib/player/*,$lib/data/*,$lib/server/*,$lib/components/music/*,$lib/components/shell/*y$lib/components/player/*. Hay que repetir en ese bloque el patrón../*del bloque general, porque la configuración de un override sustituye a la del bloque general, no se suma. - Comprobar que la regla pasa sin excepciones.
ui/puede seguir importando$lib/state/*(historial, diálogos),$lib/utils/*,$lib/actions/*,$lib/theme/*y$lib/types.
Cierre: npm run lint en verde; si un componente de ui/ importa player, el lint falla.
E4 · Ubicación de estados (S)
player/liked.svelte.ts→state/liked.svelte.ts. Guarda los me gusta del usuario, no el estado del reproductor, y lo usanSidebar,TrackInfo, el layout y las páginas.data/recentlyPlayed.ts→player/recentlyPlayed.ts. Solo mezclaplayer.recentlyPlayedcon el historial, y hoy es el único archivo dedata/que depende del reproductor.
Cierre: grep -rn "player/liked\|data/recentlyPlayed" src vacío; data/ no importa de player/.
E5 · Capa de servidor (S–M)
Problema: server/api.ts mezcla el cliente de la API, los guards de sesión, la lectura de formularios y el envoltorio de acciones. Los parsers de server/forms/ importan api.ts solo para usar formString/formFile.
server/api.tsse queda con el cliente y el manejo de errores:createApiClient,ApiClient,apiFor,unwrapOrError,apiErrorDetail,failOnError,requireData.- Nuevo
server/guards.ts:loginRedirect,requireUser,optionalUser(14 imports enroutes/más(app)/+layout.server.ts). - Nuevo
server/forms/fields.ts:formString,formFile. Los parsers deforms/lo importan desde ahí. - Nueva carpeta
server/actions/:authedAction.tsconauthedActionyActionContext.followActions.ts→actions/follow.ts.playlistActions.ts→actions/playlist.ts.
Cierre: server/forms/ no importa server/api; api.ts no importa redirect ni RequestEvent salvo para apiFor.
E6 · Explorar en componentes (M)
Problema: (app)/explore/+page.svelte (393 líneas) resuelve tres vistas en una: la rejilla de géneros, la vista de un género y los resultados de búsqueda (mejor resultado y cuatro grupos).
- Sacar componentes junto a la página (SvelteKit ignora los archivos sin
+dentro deroutes/):explore/GenreGrid.svelte: la rejilla de géneros y el estado vacío.explore/TopResult.svelte: el snippettopResultBodyyplayTopResult.explore/SearchResults.svelte: los cuatro grupos,capped,showGroupy elInfiniteScrollde pistas.
+page.sveltequeda como selector de vista: cabecera, chips, mensaje de "sin resultados" y los dos menús contextuales.- El estado compartido (
createPagedList,trackMenu,albumMenu) se crea en la página y se pasa por props; los componentes no leenpage.data.
Cierre: explore/+page.svelte por debajo de ~150 líneas; ninguna vista nueva supera las ~150.
E7 · Acento fuera del reproductor (S)
Problema: player.svelte.ts importa theme/accent.svelte y en #announce llama a accent.follow(...), así que el estado del reproductor depende del tema visual.
- Quitar
accentytrackCoverdeplayer.svelte.ts;#announcesolo actualizamediaSession. - En
(app)/+layout.svelte, un$effectque siga aplayer.currentIdy llame aaccent.follow(trackCover(id, 'small'), () => player.currentId === id). - Comprobar que la hidratación de la última pista también actualiza el acento (el efecto depende de
currentId, así que debería).
Cierre: player/ no importa de theme/; el color cambia al cambiar de canción y al cargar la página con una última pista.
E8 · Documentación y restos (S)
-
CLAUDE.md:- Estructura de carpetas con
components/shell/,components/music/, la regla de frontera deui/,state/liked.svelte.ts,server/guards.ts,server/forms/fields.tsyserver/actions/. - Documentar lo que falta:
lib/actions/(clickOutside,onEscape,pressable),state/toggle.svelte.tsytheme/color.ts. - Corregir la referencia a
$lib/state/navigation.svelte.tsen "App shell" (ahora esrestoreScrollen$lib/state/scroll.ts) y describir el shell en(app)/+layout.svelte. - La skill
musify-webque cita la introducción no está enweb-player/.claude/skills: añadirla o quitar la referencia.
- Estructura de carpetas con
-
Conversiones fuera de los mappers. La regla de P5 dice que los
Number(...)sobre DTOs solo se hacen enserver/mappers.ts, pero quedan varios enroutes/:(app)/+page.server.ts: las estadísticas de escucha (nuevotoListeningStats, con test) ytotalItemCountde las playlists.(app)/explore/+page.server.ts:albumsTotalyusersTotal.(app)/user/[id]/+page.server.ts:followersCount.- La carga de playlists del layout (que tras E1 está en
(app)/+layout.server.ts):totalItemCount.
Los totales pueden salir de
toPage(...).totalItemCounto de un helpertoCounten los mappers. -
Planes: marcar
frontend-audit-plan.mdcomo cerrado (o archivarlo) y dejar este como el plan activo.
Cierre: cada ruta citada en CLAUDE.md existe; grep -rn "Number(" src/routes solo devuelve lecturas de parámetros o formularios.
E9 · Tests de los arreglos y del código con efectos (M)
Los tests se quedan junto al archivo que prueban (x.ts + x.test.ts); no se crea una carpeta tests/. Hoy cubren bien la lógica pura (formatos, mappers, parsers, cola, búsqueda), pero no el código con fetch, estado o tiempo, ni los arreglos de seguridad de P1 y P2, que pueden volver sin que nada falle.
- Sesión y guards.
server/auth.ts:encodeSession→decodeSessiondevuelve la misma sesión; un token manipulado, cifrado con otra clave o vacío devuelvenull. Para el secreto, usarvi.mock('$env/dynamic/private')con unSESSION_SECRETde prueba, no el.envlocal (en CI no existe).- Guards (
requireUser,optionalUser,loginRedirect; enserver/guards.tstras E5): redirigen a/auth?returnTo=…con la ruta y la query codificadas, y dejan pasar cuando toca. - Tras E1, el
loadde(app)/+layout.server.ts: redirige sin sesión ni escucha anónima, devuelve datos vacíos en modo anónimo y no llama a la API sin usuario.
- Endpoints
/api(arreglos de P1). Llamar alRequestHandlercon un evento falso y unfetchsimulado:api/likes: 401 sin token, 400 contrackIdausente, vacío o no string, 502 si la API falla.api/listens/[id]/progress: 400 con JSON inválido, conplayedSecondsnegativo, no finito o no numérico; 404 si la API devuelve 404.api/tracks:pageSizese limita aMAX_PAGE_SIZEypageNumberinválido pasa a 1.
- Rollback de "Me gusta" (
liked.svelte.ts, arreglo de P2). Confetchsimulado que falla: se revierte solo esa pista y se conservan los cambios hechos mientras tanto en otras. También los casos de dar y quitar el me gusta, y quehydratesolo se aplica una vez. ListenTracker. Convi.useFakeTimers()yfetchsimulado:- Suma solo los avances de hasta 2,5 s; un salto mayor (seek) o hacia atrás no cuenta.
interrupt()hace que el siguientetickno sume.- Envía a los 30 s y en
flush(); no reenvía si no hay progreso nuevo. - Si el envío falla, el siguiente
flush()vuelve a enviar el total. begin()envía lo pendiente de la escucha anterior antes de empezar la nueva.
createPagedList. Añade la página siguiente sin duplicados, actualizahasMore, ignora unloadMorecon otro en curso y, si falla, marcaerrory conserva los elementos.resetlimpia el error.apiErrorDetail. Mensajes deerrors(varios campos, arrays), despuésdetail, despuéstitle, yundefinedpara valores que no son objeto o sin texto.
Fuera de alcance, a propósito: tests de componentes (sobre todo presentación, ya vigilada por check-tokens), utilidades triviales (storage, menuPosition, navLinks), mediaSession, progressClock, palette (necesita canvas) y player.svelte.ts entero (depende de <audio>; su lógica está en queue.ts, que ya tiene tests). E2E con Playwright queda para cuando CI pueda levantar el backend y Zitadel.
Cierre: cada punto anterior tiene su *.test.ts junto al módulo; npm run test en verde sin .env local; volver a introducir cualquiera de los bugs de P1/P2 cubiertos hace fallar un test.
Registro
| Parte | Estado | PR / notas |
|---|---|---|
| E1 Layout del grupo (app) | ⬜ | |
| E2 Carpeta shell | ⬜ | |
| E3 Frontera de ui/ | ⬜ | |
| E4 Ubicación de estados | ⬜ | |
| E5 Capa de servidor | ⬜ | |
| E6 Explorar en componentes | ⬜ | |
| E7 Acento fuera del reproductor | ⬜ | |
| E8 Documentación y restos | ⬜ | |
| E9 Tests | ⬜ |
Leyenda: ⬜ pendiente · 🟡 en curso · ✅ hecho.