Y la 5 empieza donde estaba: falta el glosario, que es lo único de lo
nuevo que sigue sin existir. Las tres cosas que el documento decía que
cambiarían en la web ya son ciertas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blog, biografía, RSS y mapa del sitio, que eran lo que quedaba. Con eso,
las veintidós rutas de la fase 4 están hechas: ningún archivo de
`src/routes` importa de `$lib/content`.
Una entrada del blog enlazaba los temas de los que habla con una lista de
slugs sin comprobar: si el tema no existía, llevaba a un 404 y nadie se
enteraba hasta que alguien pinchaba. Ahora es `entrada_cancion`, con
clave foránea.
El mapa del sitio hace sus siete consultas a la vez y no encadenadas: son
independientes y esperarlas en fila era tiempo regalado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Busca en él el disco y el estilo de su tema, y desde que el catálogo está
en la base eso baja por `page.data`. La prueba la monta suelta, así que
sin dárselo la línea de contexto salía vacía y fallaba sin que hubiera
nada roto en el componente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo que bloqueaba la fase 4 era el cliente, no las rutas: el catálogo
viajaba en el paquete de JavaScript y las tarjetas, el carrito, la cuenta
y el reproductor lo consultaban por su cuenta. Baja por `page.data` desde
el layout —veintiún kilobytes, medidos, menos de lo que ocupaba en el
paquete— y `$lib/catalogo/cliente.svelte` lo ata a las mismas consultas
que usa el servidor. La otra opción era pasar por props lo que cada
componente busca solo, y eso obligaba a tocar la compra, el carrito y el
reproductor a la vez.
Siete rutas dejan de ser cargadores universales: música, estilo, álbum,
canción, portada y las dos de etiquetas. La ficha del tema se resuelve
entera en el servidor, incluida la letra, que se pide sola y no viaja con
el catálogo.
Faltaba dónde guardar dos textos: la introducción de cada estilo y las
notas de cada disco eran el cuerpo de su Markdown y el volcado se quedó
solo con el frontmatter. Migración 0010 y `npm run db:completar` los
rellena.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo que falta no son las rutas: es que hoy el catálogo viaja en el
paquete de JavaScript y las tarjetas, el carrito, la cuenta y el
reproductor lo consultan por su cuenta. O baja por `page.data`, o esos
componentes reciben por props lo que buscan solos. Toca la compra, así
que conviene no hacerlo a medias.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Qué es un single, cómo se arma el álbum de sueltos, en qué orden van las
etiquetas: veinte funciones que estaban atadas a los archivos y que
habría que haber reescrito para la base. Dos copias «parecidas» son dos
sitios donde un día discrepan, y el sitio ya tuvo ese problema con las
anclas de los encabezados.
Ahora viven en `$lib/catalogo/consultas`, reciben el catálogo como
argumento y no leen nada por su cuenta. `$lib/content` las ata a los
archivos y `$lib/server/catalogo` a la base; cambiar de fuente no puede
cambiar el resultado.
La letra completa se queda fuera del catálogo y se pide aparte: dieciocho
letras enteras en cada petición para pintar una lista sería mandar cien
veces lo que hace falta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Antes de cambiar veinte rutas de sitio, poder demostrar que sale lo
mismo. `npm run db:comparar` pone lado a lado las dieciocho canciones,
los cuatro discos y los cuatro estilos, campo a campo, y dice dónde
difieren con la ruta exacta —`dorina.registro.reparto[0].porcentaje`—.
Confiar en que las pruebas de interfaz noten una diferencia de un año o
de un porcentaje sería confiar de más: pintan lo que les den.
La primera pasada encontró ciento y pico diferencias, y no eran del
comparador:
- **Ningún medio estaba enlazado.** El volcado del catálogo y el de
archivos se hicieron por separado, así que las veintitrés canciones
tenían `audio_id`, `muestra_id` y `portada_id` a nulo. La web habría
dejado de reproducir en cuanto leyera de la base. No se vio al volcar
porque aquello comprobaba las letras carácter a carácter —lo
delicado— y no que las relaciones estuvieran puestas.
- **Las etiquetas volvían en orden alfabético.** No es cosmético: la
primera etiqueta de un tema dice de qué va y las de después matizan.
Ahora `cancion_etiqueta` tiene `orden`.
- **La nota de cada versión no tenía columna** donde caer.
- El país de edición se guardaba en ninguna parte, y los cuatro discos
se quedaron sin él.
`scripts/db/completar-catalogo.mjs` ata esos cabos y es idempotente. Los
alias de `medio` son de verdad y no la misma tabla dos veces: sin ellos
Postgres devuelve la misma fila para el audio y para la portada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Segunda área que deja `src/content`. El texto ya no es un archivo: es la
versión vigente de un documento, con la fecha en que entró en vigor. Eso
no es burocracia —quien compró en marzo se acogió a las condiciones de
marzo, y si hay una reclamación hay que poder demostrar qué decían—, y de
paso la fecha que se enseña deja de ser un campo que alguien puede
olvidarse de tocar.
Un documento sin versión vigente no sale en el índice, así que se puede
preparar un texto nuevo sin que aparezca en el pie a medias.
El pie los recibe del layout en vez de importarlos: un componente no
puede consultar. Y el mapa del sitio saca de la base tanto el taller como
los legales, en un solo `Promise.all`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fase 3 del modelo, que iba primero por ser lo único que podía costarle
dinero a alguien: siete tablas guardaban `cancion_slug` sin clave
foránea, porque cuando se escribieron el catálogo vivía en archivos y no
había a qué apuntar. Renombrar un slug dejaba huérfana una compra en
silencio, y quien la había pagado se enteraba al intentar descargar.
Ahora apuntan a `cancion.id`, que es opaco y no cambia. El borrado no se
propaga igual en todas: `compra`, `pedido_item` y `descarga` lo
RESTRINGEN —una canción pagada no se puede borrar, y el registro de
entrega tiene que sobrevivir a lo que prueba—; `carrito_item`,
`favorito`, `valoracion` y `reproduccion` caen con ella.
En dos migraciones y no en una: entre la 0007 y la 0008 conviven las dos
columnas, así que el despliegue no tiene que parar el sitio. La 0007 se
niega a seguir si alguna compra apunta a un slug que ya no existe:
borrarla en silencio sería repetir el fallo que esto viene a arreglar.
Por fuera nada cambia de forma. Los módulos siguen recibiendo slugs —es
lo que hay en las URL— y traducen en el borde, en `$lib/server/canciones`.
Al hacerlo, tres funciones pasan a devolver `false` cuando el slug no
existe, que antes se guardaba tal cual: se podía meter en el carrito un
tema inventado.
El script de guardas estaba roto desde que se volcó el catálogo —usaba
slugs de verdad y moría en la primera inserción por clave duplicada, sin
comprobar nada—. Ahora usa el prefijo `zz-` y el país ZZ, que ISO 3166
reserva para uso privado. Y la prueba de la valoración pasaba por el
motivo equivocado: rechazaba el seis porque la columna ya no existía, no
por el rango.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eran dos cajas con un hueco entre ellas: el campo de búsqueda por un
lado y el conmutador de vista por otro, cada uno con su canto. Buscar y
elegir cómo se ve son el mismo gesto —acotar lo que hay en pantalla— y
separados parecían dos controles sin relación. Ahora el borde es de la
barra y dentro no hay más línea que la que separa lo que se escribe de
lo que se pulsa; el foco lo acusa la pieza entera.
Y el activo llevaba un dorado plano al lado de un botón dorado de
verdad, así que se notaba que no era el mismo. El aspecto sale de
`.btn--primary` a una clase `.oro` que comparten los dos: degradado,
filo de luz arriba y el barrido al pasar el puntero. Copiarlo habría
sido garantizar que un día uno de los dos se quedara atrás.
Dos cosas que aparecieron al montarlo:
- Svelte añade su clase de ámbito a todo lo que escribe en el
componente, así que un `.vista` de la página le gana por especificidad
al `.oro` general y el botón activo se quedaba con la letra gris
encima del oro. Va con `:not(.oro)`.
- En columna, `flex-basis` mide el alto: las doce rem que dan ancho al
campo lo convertían, apilado en un móvil, en un cajón de casi
doscientos píxeles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comprobaba «pendientes de revisión» literal, y al reescribir el aviso
pasó a decirse en singular. Ahora comprueba lo que importa: que el aviso
está y que nombra los versos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Enumeraba instrumentación, letristas, obras y linaje, y desde este
mediodía los ejemplos de versos y su escansión salen del mismo sitio:
los escribió un modelo. Dejarlos fuera de la lista era decir a medias
justo lo que el aviso existe para decir entero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La ficha decía que el bolero va en octosílabo y endecasílabo alternados
sin enseñar nunca uno. Un taller que describe la métrica y no la muestra
está pidiendo que se la imaginen.
`verso` es la estrofa y no el verso suelto —la rima y la alternancia de
medidas no se ven en menos de dos versos— y `verso_linea` lleva cada uno
con su escansión: `silabas` es la métrica y no la del diccionario, así
que la sinalefa mete dos palabras en una casilla —«za⁀a»—, que es justo
lo que en un texto corrido no se ve. `medida` va aparte porque un verso
agudo suma una sílaba y uno esdrújulo resta una: «yo respondo con el
son» tiene siete casillas y es un octosílabo.
Los diez ejemplos son originales, escritos para enseñar, y entran
marcados con `fuente = 'ejemplo'`. Ninguno es cita: para eso está
`obra_referencia.cita`, y dos CHECK lo sostienen —un verso «cita» sin
obra, o uno «propio» sin canción, no entran—.
La escansión se comprueba antes de cargarla, contra la ley del acento
final, y el sembrador aborta si algo no cuadra. Un ejemplo mal contado
enseña a contar mal y eso no lo detecta ningún tipo ni ningún lint: por
eso la comprobación está en su propio módulo, con nueve pruebas.
`obra_referencia_enlace` sustituye a la columna `enlace`, que era texto
suelto y solo daba sitio para uno; una obra está en Spotify **y** en
YouTube. Comparte el vocabulario `PLATAFORMAS` con los enlaces de las
canciones. De momento son búsquedas y no grabaciones concretas: una URL
inventada manda a un 404 con pinta de dato bueno, y los enlaces exactos
llegarán en el JSON de la ficha.
De paso, dos cosas que se vieron al montarlo:
- El orden de la ficha cambia. Primero lo que se usa para escribir
—ejemplos, rasgos, temática— y después lo que sitúa el género. Es un
taller: quien abre la página viene a escribir una letra.
- `repeat(auto-fit, minmax(24rem, 1fr))` desbordaba ochenta píxeles en
un móvil de 320, porque la rejilla pone la columna aunque no quepa.
Ahora es `min(24rem, 100%)`, en los tres sitios donde estaba.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Es la primera área que deja `src/content/**`. Se eligió esta porque es
donde estaban los datos que no cabían en el frontmatter de un archivo, y
porque es la que menos rutas toca si algo sale mal.
La ficha de género gana lo que pedía el artefacto y no estaba:
- Países con su relación: la cuna se distingue del país que lo adoptó, y
va primero aunque alfabéticamente no le toque.
- Instrumentación con el peso de cada instrumento y su papel. Sin el
peso, el tres cubano y una trompeta ocasional pesarían igual.
- Letristas con país, años y papel, de la tabla `persona`, la misma que
firma las canciones del catálogo.
- Obras de referencia fechadas, acreditadas y con su cita, con el aviso
de por qué se puede reproducir un fragmento.
- Linaje en las dos direcciones, distinguiendo derivar de fusionar. No
enlaza a un género sin capítulo: el bolero-son existe en la base
porque el bolero y el son confluyen en él, pero nadie ha escrito su
ficha, y enlazarlo mandaba a un 404.
- El aviso de lo que no ha revisado nadie. Mira la ficha entera y no
solo la fila del género: en los ocho que ya existían la prosa la
escribió el titular y lo que vino de un modelo son los datos de
alrededor, así que mirando solo el género el aviso callaba justo donde
hacía falta.
Todo va debajo de la prosa y a todo lo ancho, no en una barra lateral.
Lo único al lado del texto es el índice del propio capítulo, que es lo
que cubre el ancho que la prosa no puede cubrir sola.
El sitemap deja de listar los capítulos desde los archivos: si no, un
género nuevo en la base no aparecería. Y `$lib/content` pierde los
lectores del taller —los archivos siguen ahí, son de donde salió el
volcado— para que nadie edite un .md que ya no pinta nada en pantalla.
Ocho pruebas nuevas, una por cada bloque que antes no existía.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Su troceador de SQL no entiende los cuerpos `$$` de plpgsql, y este
esquema tiene dos funciones —la que comprueba que el reparto de autoría
suma cien y la que impide ciclos en el linaje de géneros—. A partir de
esa migración deja de aplicar nada y termina con éxito.
Se descubrió porque `cancion.nota` seguía existiendo dos migraciones
después de borrarla. Es decir: durante un rato la base y el esquema
dijeron cosas distintas y nada avisó.
`scripts/db/migrar.mjs` trocea por el mismo separador que escribe
Drizzle, aplica en orden, anota en su misma tabla y con su mismo hash, y
cuando algo falla dice en qué sentencia y deshace.
De paso:
- 0004 rehace `cancion_principal` alrededor del `DROP COLUMN`. Un
`CREATE VIEW ... SELECT *` expande las columnas al crearse, así que la
vista impedía borrar la columna.
- 0005 le da procedencia a `persona`. Las figuras de un género las
escribe un modelo igual que las fichas, con fechas que hay que
verificar; sin esto no se distingue a quien firma una canción del
catálogo de una semblanza importada sin revisar.
- `marked`, para componer en el servidor el Markdown que ahora vive en
una columna.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La queja era justa y va por la tercera vez, así que esta vez queda medida
y con prueba: la franja de datos, el índice del capítulo y los bloques de
abajo terminan los tres en el mismo borde.
Lo que no se puede hacer es ensanchar el texto. Para llenar novecientos
píxeles con prosa legible haría falta letra de veinticuatro puntos; a
menos de eso salen ciento veinte caracteres por línea y el ojo pierde el
renglón. Ya se midió al hacer las páginas legales: estirado daba 129.
Así que lo que cubre ese ancho es lo que va al lado, y no es la lista de
compás y tempo que se quitó —esa subió a la franja—: es el índice de
apartados del propio capítulo, el mismo recurso que llevan los textos
legales. Y las columnas miden lo suyo con el sobrante en el corredor,
porque con anchos fijos quedaban ciento veinte píxeles muertos al final de
la fila, que es exactamente el hueco que se ve como un fallo.
Además, la semilla de géneros: países, veintiún instrumentos, veintiuna
personas, obras de referencia y el linaje del bolero-son, que deriva de
dos. Entra marcada como importada y sin revisar, porque la escribió un
modelo y se publica con el nombre del titular.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los datos del género —compás, metro, tempo, rima— pasan de una lista de
definiciones apretada en una columna estrecha a la derecha a una franja a
todo lo ancho encima del texto. El compás y la métrica de un género se
leen juntos o no se leen, y en la lateral quedaban en letra pequeña con el
texto escorado.
La estructura, la temática y los rasgos bajan debajo de la prosa como
bloques a lo ancho: la temática en rejilla —son etiquetas disfrazadas de
frase— y los rasgos en lista, que son frases de verdad. Es el sitio donde
entrarán los instrumentos, los letristas, las obras de referencia y el
linaje cuando tengan datos.
Y el volcado de medios: 55 archivos a la tabla `medio` con su huella, su
peso y su duración MEDIDA, no escrita. De ahí salen dos cosas que no se
veían: el mismo audio está copiado bajo doce nombres distintos, y las
duraciones del catálogo dicen 252 segundos donde el archivo dura 6.
La clave de un medio lleva su almacén delante, porque `singles/dorina.mp3`
existe en audio y en muestras y sin eso serían la misma fila. Es además la
ruta con la que se servirán desde m.senzapaura.es/archives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los 48 archivos entran en la base: 18 canciones con sus 5 versiones como
filas hermanas, 4 discos, 4 estilos, 8 géneros, 14 capítulos, 9 personas,
53 etiquetas, 2 entradas, 5 legales y una página. La web sigue leyendo de
los archivos; esto solo llena el sitio donde caben.
Reutiliza los analizadores del sitio en vez de leer el frontmatter otra
vez. Es la decisión que sostiene todo lo demás: si un valor por defecto o
una normalización cambian, cambian para los dos. Un segundo analizador
«parecido» es lo que garantiza que un día la base y la web cuenten cosas
distintas sin que nadie lo note.
Todo en una transacción, y al final comprueba lo escrito contra lo leído
—la letra de cada tema carácter a carácter— porque «no dio error» no es lo
mismo que «está bien». Si no cuadra, deshace y dice qué falló. Y es
idempotente: se puede lanzar las veces que haga falta sin duplicar nada,
sin tocar cuentas, pedidos ni compras.
Para poder reutilizar ese código desde Node hizo falta un gancho de
resolución —`$lib`, imports sin extensión y un sustituto de `$app/paths`—
y quitar una propiedad de parámetro de `ErrorDeContenido`: es sintaxis
exclusiva de TypeScript y ningún cargador que solo borre tipos puede con
ella.
Y una corrección: el cuerpo del Markdown de una canción NO era un
comentario, es la letra. Lo dije al revés en el documento del modelo y lo
repetí varias veces. El campo `letra:` del frontmatter —que aparece en un
solo archivo— son los créditos de quién la firma. Corregido en el
documento, en el esquema, y quitada la columna `nota` que sobraba.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 tablas nuevas, una vista y las guardas que Drizzle no sabe expresar. El
sitio sigue leyendo de los archivos: esto solo crea el sitio donde caben.
El esquema pasa de archivo a carpeta —con el catálogo dentro pasaría de
las dos mil líneas—, y el corte se hizo por rangos de línea para no
reescribir los comentarios que explican decisiones. Se comprobó que era un
no-op: «No schema changes, nothing to migrate».
Lo que Drizzle no genera va escrito a mano al final de la migración, y es
justo lo que impide que el modelo mienta: la clave foránea de las
versiones contra su propia tabla, dos columnas generadas y una clave
compuesta que impiden a la vez la versión de una versión y los ciclos, la
vista `cancion_principal`, el disparador que exige que el reparto de
autoría sume cien, el que impide ciclos en el linaje de géneros, y los
índices parciales del estilo principal y de la arista principal.
Y las guardas se comprueban intentando lo que deben rechazar, en
`scripts/db/comprobar-guardas.mjs`. Que una restricción exista en
`pg_constraint` no significa que impida nada.
Tres cosas que salieron de hacerlo y no de suponerlo:
- `creado_en` era obligatorio SIN valor por defecto en la base: lo ponía
JavaScript al insertar con Drizzle, así que cualquier INSERT escrito a
mano fallaba y la invariante la sostenía la aplicación, no la tabla.
Ahora es `defaultNow()`.
- `drizzle-kit migrate` se atasca con los cuerpos `$$` de plpgsql. La
migración se aplicó con `scripts/db/probar-migracion.mjs`, que además
dice en qué sentencia falla, y se anotó en su registro.
- Dos pruebas pasaban por el motivo equivocado: una por clave duplicada en
vez de por la suma, y otra porque un disparador aplazado no salta dentro
de un savepoint que nunca se cierra. Las dos corregidas; una guarda dada
por buena sin ejecutarse es peor que no tenerla.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Las dos ultimas decisiones se toman por defecto eligiendo la opcion
reversible en cada caso: valoracion y favoritos en la cancion principal
—si cada version se valorase aparte, la media de la tarjeta dejaria de
significar nada— y prompts privados, porque publicarlos manana es cambiar
una columna y despublicarlos cuando ya estan indexados no.
Queda apuntado lo que no es una tabla y hay que hacer igual: reescribir
la politica de privacidad antes de guardar escuchas, la ventanilla unica
del IVA, y el DNS y el certificado de m.senzapaura.es.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Glosario: `termino` se parte en termino y acepciones numeradas, y el
marcado apunta a una acepcion. Eso cierra la duda del automatico: sabe
encontrar la palabra pero no elegir el sentido —«clave» es patron ritmico
en un texto de bolero y armadura en uno de armonia—, asi que propone y
una persona confirma.
Facturas: el correlativo sale de un contador con bloqueo y no de
max(numero)+1, que con dos compras simultaneas daria el mismo numero. Y
hacen falta rectificativas: una venta anulada no borra su factura. El IVA
es el del pais del comprador, no el nuestro.
Escuchas: se conservan por persona, y eso obliga a reescribir la politica
de privacidad, que hoy afirma que no hay seguimiento.
Medios en m.senzapaura.es: cambia una decision anterior. Estaba escrito
que las descargas no usan URLs firmadas porque la sesion va delante; en
otro dominio ya no va, asi que para el audio completo la firma deja de
ser opcional.
Fichas por JSON: el contrato de importacion pasa a ser parte del modelo,
con esquema, procedencia y creacion marcada de lo que la IA se invente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los temas salen del array a `genero_tema` y comparten vocabulario con las
etiquetas de las canciones. No es por ahorrar una tabla: es lo que
permite cruzar «el desamor: nueve temas mios y cuatro generos que viven
de el». Con dos listas separadas, «desamor» y «desamor» serian dos cosas
distintas y nadie lo veria.
Los rasgos se quedan como array porque si son prosa de verdad.
Y la completitud de una ficha se deduce de sus secciones y relaciones, sin
columna: una marca de progreso que se pone a mano miente a la semana.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`padre_id` no aguantaba. Un genero no deriva de uno: el bolero-son es
bolero y son, y la bachata viene de tres. `genero_origen` guarda el grafo
con el tipo de vinculo —deriva, fusiona, influye— y una arista marcada
como principal da el arbol de navegacion, asi que no hay que elegir entre
la verdad y la usabilidad.
Ese tercer tipo ya estaba escrito en el contenido, dentro del campo
equivocado: son-cubano.md pone la herencia de la decima y la copla dentro
de `metrica` porque no habia donde ponerla.
Y lo comparable sale de la prosa a vocabularios: el octosilabo aparece en
tres de las cinco fichas y no se podia preguntar por el. `metro` enlaza
ademas con el glosario, que es donde ese termino ya vive.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La forma del prompt la define un tercero y cambia sin avisarnos, asi que
modelarla en columnas es perseguir el esquema de otro. Es el caso
contrario al del contenido del sitio, donde el esquema es nuestro.
Abierto no es sin validar: cada `modelo_ia` trae su JSON Schema, se
valida al guardar y el panel puede pintar el formulario a partir de el.
Y lo que se consulta —genero, animo, modelo, valoracion, idioma— se
queda en columnas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tres tablas: modelo_ia, animo y prompt.
El prompt cuelga del MODELO y no de la IA: uno de Suno v3 no es uno de
v4, y con la clave apuntando a "Suno" a secas queda una biblioteca sin
saber para que servia cada entrada.
El animo se codifica con la GEMS, hecha para emocion musical y no para
emocion en general —nostalgia es una de sus nueve categorias—, mas los
dos ejes de Russell, que ademas son redactables. Se llama `animo` y no
`version` a proposito: `version` ya significa otra cosa en este modelo.
Y un prompt no es un solo texto: Suno separa estilo de letra y admite
exclusiones, y guardar si funciono es lo que distingue una biblioteca de
un monton de texto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Versiones: una version es una fila de `cancion` con `version_de_id`, no
una tabla aparte. Se descarto partir en obra y grabacion porque en ese
reparto el estilo y las etiquetas colgarian de la obra y una version no
podria tener los suyos. Van los cuatro cabos que hay que atar, empezando
por la vista `cancion_principal`: hay once sitios que listan o cuentan
canciones y a los once se les puede olvidar el filtro.
Generos: el catalogo aspira a ser completo. La ficha se amplia con
instrumentos, letristas, obras de referencia y secciones de prosa. Sin
tabla `letrista` —un letrista es una persona— y sin letras completas de
terceros, que tienen dueno: se cita y se enlaza.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fase 1 de la migracion del contenido a PostgreSQL: las 41 entidades con
sus campos, sus relaciones y el motivo de cada decision. Sin codigo.
De analizarlo salieron dos cosas que no se buscaban:
- Cinco tablas apuntan a una cancion por una cadena de texto y ninguna
tiene clave foranea, porque el destino esta en un archivo. Renombrar un
slug deja huerfana una compra en silencio. Por eso ese arreglo es la
fase 3 y no la ultima.
- `estilo` hace dos trabajos: las secciones del catalogo y los generos
del taller. El pais que hay que clasificar es del genero, y mientras
sean lo mismo esa clasificacion no se puede ni escribir.
Y el ISRC estaba en la cancion cuando identifica una grabacion, asi que
pasa a la version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El color forma parte de lo que cuenta el sitio, y seguir a
`prefers-color-scheme` hacía que media parte de las visitas viera una
versión que no es la de la marca —un portátil configurado en claro no
tiene nada que decir sobre el aspecto de un catálogo de canciones—. Al
revés que en una herramienta de trabajo, aquí nadie va a pasar ocho horas
seguidas leyendo.
El claro sigue entero y a un clic, y la elección se recuerda. Lo que se
deja de hacer es decidirlo por nadie.
La prueba de contraste emulaba el esquema del sistema, así que con este
cambio habría pasado a comprobar dos veces lo mismo sin que nadie lo
notara: ahora pone `data-tema` a mano. Y la medida de claridad convierte
el `oklch()` pintándolo en un lienzo, porque desmontar la cadena a mano
daba NaN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La regla de que la red local entra siempre no sirve para el porton, y es
lo que mas despista: una peticion al dominio desde la propia red sale al
router y vuelve reescrita, asi que al proxy le llega con la IP publica.
Sin esa direccion en la lista, uno ve su propia pagina de espera en su
propio ordenador y no hay forma de entender por que.
La IP es fija y contratada, asi que la lista no hay que revisarla.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Con `PROXIMAMENTE=1`, quien no esté en la lista acaba en una página de
espera con el mismo fondo animado que la portada. La red local entra
siempre sin configurar nada: encender el portón y quedarse uno mismo
fuera es la forma más rápida de no poder comprobar si funciona.
Tres decisiones que salieron de probarlo:
- **Redirige, no pinta la espera en la dirección pedida.** Se intentó lo
segundo, que conserva la dirección en la barra, y no vale: el HTML sale
compuesto para /proximamente y el navegador lo hidrata creyendo estar
en otra ruta, así que al terminar de cargar volvía a pintar la cabecera
y el pie que la espera no debe tener.
- **503, no 200.** La web existe pero todavía no atiende. Es lo que
impide que un buscador se lleve el cartel como si fuera el sitio. Y el
robots.txt pasa a cerrar el paso entero mientras dure: si no,
anunciaría un mapa lleno de direcciones que hoy contestan «muy pronto».
- **Rutas de recursos absolutas.** Con las relativas que SvelteKit usa por
defecto, la página servida en otra dirección pedía sus hojas de estilo
en un sitio donde no hay ninguna.
El reconocimiento de direcciones va aparte y con pruebas: una máscara mal
interpretada no da ningún error, solo deja pasar a quien no debía. Ahí
está el caso de `/0`, que en JavaScript se cuela porque el desplazamiento
de 32 bits es un no-op.
Y el portón está abierto mientras nadie diga lo contrario. Una web que se
esconde sola porque falta una variable es peor fallo que una que se ve
antes de tiempo: nadie mira una página que cree no publicada, así que
puede pasar semanas caída sin que se note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
En este repositorio npm es el gestor de paquetes de Node, el de npm run
dev. Abreviar Nginx Proxy Manager igual en un documento de despliegue
confunde justo a quien lo lee para montar el servidor.
De paso, el documento explica que es antes de decir donde tocarlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El proxy no lo hace el Nginx de la maquina de la web, sino un Nginx
Proxy Manager en otro sitio de la red. Con eso delante, el Nginx local
sobra: adapter-node ya sirve los estaticos con sus cabeceras de cache, y
un segundo salto solo complica el recuento de IPs.
Y ese recuento importa mas de lo que parece. Sin ADDRESS_HEADER y
XFF_DEPTH, getClientAddress() devuelve la IP del proxy: el limitador del
formulario de acceso cuenta por direccion, asi que con todas las
peticiones pareciendo venir del mismo sitio salta con una sola persona
insistiendo y nadie mas puede pedir su codigo.
El documento incluye que registros de DNS cambiar y, sobre todo, cuales
no tocar: los MX, el SPF y el DKIM son el correo del dominio, y borrar
cualquiera de ellos da un sintoma que aparece dias despues.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El servicio de systemd nunca llegó a instalarse y el sitio de fábrica de
Nginx seguía puesto: es él quien contesta a quien entra por la IP, así
que se veía su bienvenida aunque nuestro sitio estuviera bien
configurado.
`instalar.sh` hace las tres cosas que el despliegue normal no puede hacer
por sí solo —poner el unit, poner el sitio, quitar el de fábrica—, es
idempotente, y comprueba al final que Node y Nginx contestan de verdad en
vez de dar por hecho que sí.
El script va dentro de `despliegue/`, que ahora se sube con el resto: la
vez anterior remití a copiar de una ruta del servidor donde ese archivo
no estaba.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cambia las referencias al dominio de trabajo: la URL canónica, los correos
de los textos legales, las direcciones de retorno de Google y Facebook, y
lo que espera la suite.
El `robots.txt` pasa de archivo estático a ruta generada. Llevaba el
dominio escrito a mano y se quedó anunciando el anterior: un mapa del
sitio mal anunciado no da ningún error, simplemente no lo encuentra
nadie. Ahora sale de `site.url`, igual que el mapa y las etiquetas de
cada página. De paso deja fuera del rastreo lo que depende de quién mira
—cuenta, carrito, favoritos, entrar, descargas—, que no es contenido.
Y el Nginx queda listo para el dominio, con los tres pasos que hay que
dar fuera del servidor: los registros A a 89.46.247.16, la redirección de
los puertos 80 y 443 a 192.168.18.171, y Certbot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Detrás de la cuenta hay compras y facturación, y un código por correo apoya
toda esa puerta en el buzón: quien entre ahí, entra aquí. Delegar la
identidad en un proveedor que ya tiene doble factor la sostiene mejor.
Es el flujo de código de autorización con PKCE, escrito a mano porque son
cien líneas y las partes que importan son tres, mejor a la vista que
detrás de una dependencia: comparar el `state`, mandar el `code_verifier`
y exigir que el correo venga verificado antes de enlazar nada.
Lo que NO hace, a propósito: verificar la firma del `id_token`. La
identidad no se saca de lo que trae el navegador, sino de una llamada
nuestra al proveedor por TLS, así que no hay firma ajena que comprobar.
Y no guarda ningún token: se usa una vez para preguntar quién es y se
tira.
El enlazado por correo verificado es lo delicado, y por eso está separado
en `usuarioParaPerfil` con su explicación: sin exigir la verificación,
cualquiera podría poner la dirección de otro en un perfil suyo y quedarse
con su cuenta y sus compras.
El código por correo se queda debajo, como alternativa. Sin credenciales
puestas, los botones no se pintan y la ruta contesta 503: un botón que
solo lleva a un error es peor que no tenerlo.
Y los textos legales al día, que ahora sí hay terceros: qué se manda a
Google y a Facebook, cuándo, qué se guarda y cómo revocarlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El acceso deja de ser un salto de página: «Entrar» abre un diálogo con la
imagen de marca a la izquierda y el formulario a la derecha, y al entrar
se vuelve a donde se estaba en vez de aterrizar en /cuenta. `/entrar`
sigue existiendo con el mismo formulario y la misma estampa, para quien
llegue por un enlace directo o sin JavaScript.
Tres cosas que salieron de probarlo, no de suponerlo:
- `showModal()` enfocaba el campo trampa contra envíos automáticos: está
fuera de pantalla y fuera del recorrido de tabulación, pero se puede
enfocar. Ahora el campo se enfoca a mano.
- Una regla de `display` en el diálogo le gana a la del navegador, que es
hoja de usuario. El diálogo cerrado se pintaba igual: una pantalla de
vacío al final de todas las páginas del sitio.
- Los enlaces legales navegaban por debajo del modal. Van en pestaña
aparte: se consultan y se vuelve con lo escrito intacto.
Y aparte, el carrusel de la portada. Cada lámina lleva su fondo con
`z-index: -1`, pero con `opacity: 1` la lámina no crea contexto de
apilamiento, así que ese fondo se escapaba y se pintaba detrás del
carrusel entero. Al pasar de lámina asomaba la foto de la de al lado y el
fondo de la que entraba saltaba hacia atrás al terminar el fundido.
Parecía que el shader se desincronizaba; lo que se movía era el orden de
pintado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El ancho útil vuelve a 74rem (1184px). Se había subido a 88rem para
aprovechar los monitores anchos y salió al revés: la prosa tiene una
medida de lectura y no crece con el contenedor, así que lo único que
creció fue el hueco a su derecha.
Un vacío al lado de un texto no se tapa estirando el texto —probado: 129
caracteres por línea, ilegible— sino poniendo algo al lado:
- Los textos legales van a tres columnas: los otros documentos, el que se
lee y sus apartados. El índice de apartados sale del Markdown en bruto
y las anclas las pone un plugin al compilarlo, así que hay pruebas de
que las dos formas de calcularlas siguen coincidiendo.
- El índice legal pasa de lista de renglones a rejilla de tarjetas, como
el taller y los estilos; era el único listado que no se veía como los
demás.
- La bio lleva al lado lo que hay publicado —temas, discos y estilos, con
sus enlaces—, sacado del catálogo y no escrito a mano, para que no se
contradiga sola el día que se suba una canción.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cinco páginas legales en /legal, enlazadas desde el pie de todas las páginas:
información legal, condiciones de uso, privacidad, cookies y condiciones de
venta. Son borradores con los datos del titular marcados entre corchetes y un
aviso de que hace falta revisión de un abogado antes de vender: cubren lo que
pide la ley española, no son un documento validado.
La política de cookies y la de privacidad están escritas mirando el código, no
copiadas de una plantilla: dos cookies, las dos necesarias, y dos terceros
—Stripe y Resend— que reciben lo imprescindible.
Y eran tres, porque las tipografías venían de Google: cada visita mandaba la IP
del visitante a un tercero sin pedirlo y sin que aportara nada. Ahora se sirven
desde el propio dominio. Cargar la web ya no contacta con ningún dominio ajeno,
y hay una prueba que lo comprueba.
Aparte: el símbolo de marca registrada en el aviso de derechos, los botones de
reproducir de las tarjetas pasan de 48 a 36 píxeles —eran casi una quinta parte
de la portada, y cambiaban de proporción según la tarjeta— y el de ver el
carrito solo aparece cuando hay algo dentro.
De paso se arregla un fallo de tipos que estaba latente: `resolve()` tiene una
sobrecarga por ruta y no acepta una unión, así que la navegación dejaba de
compilar en cuanto se añadían rutas. Ahora se resuelve en `site.ts`, una a una.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El texto del botón dorado se aclaraba al pasar el puntero y quedaba en 1,67 de
contraste sobre el oro. La causa no era el color del botón: `.btn:hover` pone
`color: inherit` —para que la regla general de los enlaces no tiña de dorado los
secundarios— y le gana por especificidad. Ahora el dorado repite su color ahí.
De paso, el dorado se parte en dos tokens porque hacía dos trabajos
incompatibles: de relleno tiene que ser claro para que encima se lea el marino
—7,6 de contraste en el tema claro— y escrito tiene que ser hondo para leerse
sobre el fondo —5,3—. Con un solo valor, el tema claro se quedaba en 4,4, por
debajo del mínimo. Y encima del oro va marino, no blanco, en los dos temas.
Los botones tenían tres tamaños distintos: el dorado a 16px con relleno 8/16 y
el de al lado a 14px con 12/24. Dos botones juntos con distinta letra se ven
como un descuido. Ahora hay una sola escala, y los 15px son los del menú.
El botón de ver el carrito solo aparece cuando hay algo dentro, con la cuenta:
uno que lleva a una página vacía gasta el sitio de una llamada a la acción para
decir que no hay nada.
Tres pruebas nuevas lo fijan, incluida la del contraste con el puntero encima,
que es donde estaba el fallo y donde una captura en reposo no lo enseña.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los botones dejan de ser cápsulas y las tarjetas pierden la mitad del canto. De
paso se ordena el sistema: había tres radios sueltos y una píldora de 999px
repetida en quince archivos, y quedan dos —uno para lo que se pulsa, otro para
las superficies—. Los botones de icono suelto siguen redondos, que ahí sí tiene
sentido.
Las fichas del taller no usaban la tarjeta compartida: tenían su propio borde,
su propio canto y su propio hover, así que se veían de otra familia que las de
música. Ahora usan `.tarjeta`, con el mismo reflejo bajo el puntero y el mismo
subrayado.
Y la portada arranca en el borde de la ventana, con la cabecera flotando encima.
Para eso la cabecera pasa a medir su propio alto y publicarlo: estaba escrito a
mano en 4,75rem cuando mide 68px, y todo lo que dependía de ese número —el salto
a un ancla, las barras laterales pegajosas— quedaba desplazado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo escribí suponiendo, lo probé a mano contra la máquina y las suposiciones
fallaron en dos sitios:
- `rsync` no está en la máquina de desarrollo. Git Bash no lo trae. Se sube con
`tar` por SSH, que sí está en los dos lados.
- `npm ci` no funciona en el servidor. Tiene salida a internet, pero su DNS
resuelve unos dominios y otros no: `google.com` sí, `registry.npmjs.org` no.
Las dependencias se resuelven aquí y se suben instaladas. Los tres paquetes de
producción son JavaScript puro, así que viajan tal cual; el script comprueba
que sigan siéndolo y se para si aparece un binario de plataforma.
Comprobado en el servidor y no solo en el papel: la web arranca, las siete rutas
que probé devuelven 200, la muestra de audio sale entera y la ficha lee las
valoraciones de Postgres por el bucle local.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El servidor del usuario lleva Nginx, así que el montaje es proxy inverso por
delante y Node por detrás. La web no son archivos estáticos —cada página se
genera sabiendo quién la pide—, de modo que hace falta un proceso vivo y un
servicio que lo mantenga.
Nginx sirve además los estáticos por su cuenta: lo de `_app/immutable` lleva el
hash del contenido en el nombre y se cachea para siempre, así que abrir la web
deja de despertar a Node para devolver un CSS. Lo que no puede tocar son las
descargas: comprueban la compra antes de entregar el archivo, y el audio
completo vive fuera de `build/` para que no haya forma de pedirlo por URL.
El script construye aquí y sube el resultado, en vez de compilar en el servidor:
allí no hacen falta ni Vite ni el compilador, solo Node y tres paquetes. Las
migraciones también se lanzan desde aquí, porque `drizzle-kit` es herramienta de
desarrollo y en producción se instala con `--omit=dev`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El usuario ha puesto un servidor con PostgreSQL 16, así que la base pasa allí.
El catálogo no se toca: sigue siendo Markdown del repositorio; lo que se muda es
lo que generan las personas, que es lo único que estaba en la base.
Dos diferencias entre motores que habrían pasado desapercibidas y se cazaron
comprobando con datos de verdad, no compilando:
- `integer` en Postgres son cuatro bytes y en SQLite ocho. El limitador de
envíos guarda su vencimiento en milisegundos desde 1970 —trece dígitos, del
orden del billón— y en `integer` desbordaría, dejando de limitar. Va en
`bigint`.
- `avg()` y `count()` los entrega el driver como cadena para no perder
precisión. Sin convertirlas, `cuantas === 0` nunca se cumpliría y «1 voto»
saldría siempre en plural. Se convierten en la consulta.
Verificado contra el servidor: once tablas creadas, las 113 pruebas de extremo a
extremo y las 224 unitarias en verde, el servidor de producción arrancando, el
limitador escribiendo sus trece dígitos y el borrado en cascada llevándose
valoraciones y favoritos al borrar una cuenta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La página pasaba de las setecientas líneas y había que recorrerla entera para
tocar cualquier cosa. La columna de la derecha —valoración, ficha, créditos y
registro— son cuatro bloques de información que no tienen nada que ver con lo
que hace la página, que es presentar la letra y dejarla sonar.
Quedan 437 líneas en la página y 276 en el componente. De paso se va un bloque
de CSS muerto: un `@media` vacío con el comentario de las etiquetas, que se
quedó atrás cuando las pastillas pasaron a la clase global `.chip`.
Comprobado en el navegador, no solo compilando: los cuatro bloques salen en el
mismo orden, el de registro sigue apareciendo solo cuando hay algo que enseñar,
y no hay ningún error en consola.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El botón dorado se quedaba en un rectángulo oscuro con el borde de oro al pasar
el puntero. La causa: `.btn:hover` usaba el atajo `background`, que reinicia
también `background-image` y se llevaba el degradado por delante. Las reglas
base pasan a `background-color`, que es la longhand que no pisa lo que no le
toca.
De comprobarlo salió que no había ningún `a:hover` global: cualquier enlace sin
regla propia —el del feed, los del pie— no daba ninguna señal de ser pulsable.
Ahora todos responden con el dorado, que es el color de lo accionable aquí.
Tampoco la tenía el conmutador de vista de la página de música.
El botón secundario acusa el puntero con el fondo y el canto, no tiñendo el
texto de dorado: sería una señal doble y competiría con el botón dorado que
suele tener al lado.
Seis pruebas nuevas cubren la respuesta al puntero de cada tipo de control. Es
lo que faltaba: un estado de hover roto no se ve en una captura en reposo, y así
fue como se coló.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El pack pasa de cinco a diez canciones por el mismo 1,50 €: diez canciones por
lo que cuesta un café. Es la propuesta, así que el número y el importe se leen
juntos en todas partes.
Las pruebas de la tienda estaban escritas con el cinco metido a mano y hubo que
reescribirlas enteras. Ahora dicen la regla en términos del propio tamaño del
pack, y el helper de las pruebas de extremo a extremo añade lo que haya a la
venta hasta llenarlo en vez de nombrar los temas uno a uno: así cambiar el
tamaño otra vez no obliga a tocar nada.
Las dos láminas de llamada estrenan foto propia con su velo, más cubriente que
el de la textura: sobre una imagen clara el texto blanco no se lee.
Y el dorado deja de ser plano en los botones. Un metal refleja, y lo que lo hace
parecer metal es la banda de luz que lo cruza; el fondo se pinta más alto que el
botón para que al acercar el puntero la luz recorra la pieza en vez de
limitarse a aclararla. El acento sube un punto de croma y baja uno de claridad,
que es lo que lo separa de un amarillo de rotulador.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Un shader de ondas en los azules de marca, sobre la imagen de la portada. Es
WebGL a pelo: un fondo así es un triángulo y un programa de fragmentos, y
traerse una biblioteca 3D para eso sería medio mega de dependencia en la primera
pantalla del sitio.
El dibujo es domain warping —ruido que desplaza el dominio de otro ruido—, con
la escala horizontal mayor que la vertical para que las formas se tumben en
bandas en vez de quedarse redondas.
La paleta va en RGB lineal, que es en lo que mezcla el shader. El primer intento
usó los cuatro fondos de `app.css` tal cual y salió una plancha lisa: caben en
un pelo de claridad, y sin recorrido de color el ruido no se ve por bien que
esté calculado.
Con `prefers-reduced-motion` no se monta; se para al salir de pantalla y al
cambiar de pestaña; y si no hay WebGL o el programa no compila, debajo queda la
imagen de siempre. Solo lo lleva la cabecera: en las tres láminas serían tres
shaders pintando lo mismo a la vez.
De paso resuelve el tema claro, que no tenía imagen propia: lo pinta en azules
pálidos, así que el clip de relleno se va.
Las pruebas arrancan Chromium con SwiftShader para poder comprobar el efecto y
no solo su ausencia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tres piezas estaban copiadas en varios sitios y ahora se escriben una vez: el
fondo a sangre de la portada, las migas de pan y la pastilla de una etiqueta.
El fondo ya había divergido entre la cabecera y las láminas de llamada: el
degradado era distinto por descuido y no por decisión, que es lo que pasa con el
marcado duplicado.
Las migas pasan a ser una lista ordenada, que es su marcado correcto: un lector
de pantalla anuncia cuántos escalones hay y en cuál se va, y el último deja de
ser un enlace a donde ya estás.
Tres cosas salieron de hacerlo, ninguna cosmética:
- Con el texto como clave del bucle, un disco y un tema que se llaman igual
—«Como si nada» es las dos cosas— daban claves duplicadas y tumbaban la ficha
al hidratar. El SSR salía bien, así que solo se veía en el navegador.
- Al compartir el fondo, las tres láminas montaban el clip de vídeo. En tema
claro eso eran tres copias del mismo vídeo en la portada. Ahora lo pide solo
la cabecera.
- La lista de temas de un disco no tenía nombre, así que no se distinguía de
ninguna otra lista de la página. Ahora se llama «Temas».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El campo `volverA` de los formularios se comprobaba en tres sitios con tres
copias del mismo filtro, y las tres dejaban pasar `/\otro-dominio.com`: el
navegador convierte la barra invertida en barra normal al resolver, así que eso
es `//otro-dominio.com` con otra ropa. En la pantalla de entrar eso es phishing
servido: enlace a nuestro propio acceso, y quien escribe su correo acaba fuera.
Ahora hay un solo `destinoSeguro`, que no compara prefijos sino que resuelve la
URL y solo acepta lo que sigue en el origen. Con pruebas de cada disfraz.
Fuera siete funciones exportadas que no llamaba nadie. Las escribí "por si
acaso" y una API que no se usa no es una API: es código que leer y mantener sin
que nada dependa de ella.
Del repaso responsive salen dos fallos reales. Los puntos del carrusel medían
8×8 píxeles, la tercera parte del mínimo para acertarles con el dedo; ahora el
punto se ve igual y el botón mide 24×24. Y el hueco reservado bajo el contenido
para la barra de reproducción estaba escrito a mano en 88 píxeles cuando la
barra mide 97 en escritorio y 145 apilada en un móvil: se comía el pie. Ahora la
barra se mide sola y publica su alto.
Todo ello fijado en `responsive.e2e.ts`, que recorre diez páginas a cuatro
anchos.
Sobre rendimiento, nada que hacer: parecía que las páginas tardaban 215 ms, pero
eran de la resolución de `localhost` por IPv6 en la medición. Por IPv4 se
renderizan en 5-9 ms, así que memorizar el catálogo habría sido optimizar un
problema inexistente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>