Había dos listados: una tabla ordenable para el catálogo y una lista
aparte para los discos. Cada arreglo había que hacerlo dos veces y el
resultado era que la portada estaba en una y el año en la otra. Ahora es
un solo componente, y lo que cambia son las columnas de contexto, que se
encienden por props: dentro de un disco, la columna «Álbum» diría diez
veces lo mismo.
Solo recibe canciones. El estilo, el disco y la portada los resuelve
contra el catálogo que ya baja en `page.data`, así que quien la usa no
tiene que armar filas a mano, que era lo que obligaba a repetir código.
Cada fila lleva su portada —con la ampliada al pasar el ratón—, y debajo
del título van los géneros y las etiquetas en vez del primer verso: la
letra está entera en la ficha, y aquí lo que sirve para elegir es de qué
está hecho el tema. Las etiquetas se cortan por número, con un «+2» al
final: recortarlas con un degradado dejaba «Autoengaño» en «Autoeng», que
no parece una elección sino algo roto.
La cola ya se puede llenar. Hasta ahora solo se podía sustituir —cada
play la vaciaba y la llenaba con su contexto—, así que no había forma de
armar una escucha cogiendo temas de discos distintos. `encolar()` es la
otra mitad, y con la cola vacía arranca, porque un botón que no hace nada
es peor que no tenerlo.
Los géneros de una canción entran en el modelo: `cancion_genero` existía
en la base y no llegaba a la web. Son cosa distinta del estilo —el estilo
es cómo se ordena la obra aquí y es uno solo; los géneros son de qué está
hecha la canción y pueden ser varios— y llevan al glosario.
De paso: las letras largas van a dos columnas de 37ch, que es el mínimo
que deja caber dos y a la vez el verso más largo del catálogo —importa
afinarlo, porque en una letra dónde acaba cada verso es parte del texto—;
la página baja de 2808 a 2023 píxeles. Contacto sale de la barra de menú
y se queda solo en el pie. Y el titular pasa a «Letrista y creador
musical».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Las láminas están dibujadas como carteles y venían armonizadas: el
dibujo ocupa el 62,6 % de la izquierda y el resto es una banda lisa, que
es el sitio del texto. Las recorté creyendo que sobraba y no: ahora va
la lámina entera de fondo, pegada a la izquierda con el alto exacto de
la pantalla, y lo que sobre a la derecha se pinta del color de la propia
banda, así que en un monitor ancho la banda se alarga y no se ve dónde
acaba la imagen. La columna del texto empieza a 0,88 × el alto, que es
donde empieza la banda sea cual sea el ancho.
Sin cabecera, sin pie y sin barra de reproducción. Un cartel a pantalla
completa con el menú del sitio encima deja de ser un cartel, y de un
error se sale por la puerta que pone la propia página.
En vertical no vale ese reparto: al alto de un móvil el cartel se
quedaría en una franja del centro con el número cortado por los lados.
Ahí se apila, y con la otra versión del archivo —solo el dibujo, sin la
banda—, con el panel del color del borde del dibujo, que es exactamente
el color con el que la imagen termina.
Un 403 no es ninguno de los dos casos: la página existe y el sitio
funciona, es que no te toca. Esos salen sin ilustración. Y se descartan
los mensajes que pone SvelteKit cuando no hay uno nuestro: «Internal
Error» en un sitio en español es peor que no poner nada.
Con `src/error.html`, desnudo a propósito, para cuando la aplicación no
puede ni pintar su propia página de error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cada una lleva la suya: la del 404 y la del 500, recortadas del original
—traían una banda plana pegada a la derecha, de generarlas— y pasadas a
webp, 95 y 70 KB. El encabezado no repite lo que la lámina ya lleva
dibujado: dice lo mismo con otras palabras, para que no se lea dos veces
seguidas y para que la página siga teniendo titular si la imagen no
carga.
La música suena con un botón, no sola. Los navegadores cortan el audio
que empieza sin que el usuario haya tocado nada, y quien llega a un 404
desde un enlace roto no ha tocado nada: arrancarla al cargar no fallaría,
es que no sonaría, y en silencio. Con `preload="none"`, además, el mp3 no
se descarga hasta que se pide; sin eso, 686 KB a todo el que aterrice.
Y suena una cosa cada vez. Al 404 no se puede llegar sin recarga entera
—SvelteKit no enruta lo que no existe—, así que ahí nunca hay barra; al
500 sí, y entonces darle aquí pausa el tema, y darle al play de la barra
para esto.
Un 403 no es ninguna de las dos cosas: la página existe y el sitio
funciona, es que no te toca. Esos salen sin ilustración, con su mensaje,
en vez de con una lámina que miente. Y se descartan los mensajes que pone
SvelteKit cuando no hay uno nuestro: «Internal Error» en un sitio en
español es peor que no poner nada.
Con `src/error.html`, que es lo que sale cuando la aplicación no puede ni
pintar su propia página de error. Va desnudo a propósito: sin hoja de
estilos, sin fuentes, sin imagen y sin JavaScript, porque todo lo que
pidiera fuera podría estar caído también.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La portada pasa de 17rem a 22rem y la columna del título crece hasta su
altura, así que los botones se descuelgan y su base queda a la misma
línea que la de la portada: 494 y 494 medidos en el navegador. El aviso
de venta y las plataformas suben por encima de ellos, porque lo que se
viene a hacer va abajo del todo. La separación la pone un `gap` y no un
margen: ninguna de las dos cosas está siempre, y un margen sobre un
hueco vacío despegaba los botones justo en las fichas que no las tienen.
Las etiquetas no salían porque no había ninguna: las diez de «Cupido sin
flechas» entraron sin ellas. No están en el material —el prompt de Suno
dice cómo suena el tema, no de qué va—, así que van escritas a mano en
`sembrar-etiquetas-cupido.mjs`, donde se leen y se discuten, reusando el
vocabulario que ya existía siempre que encajaba.
Y se pintan en fantasma y en color. Sin relleno: son un dato, no una
acción, y un botón lleno al lado del de escuchar competiría con él. El
tono sale del nombre —nueve elegidos, saltando el marino del fondo y el
oro del acento—, así que «Desamor» es del mismo color en la ficha, en el
blog y en la portada sin tabla que mantener. Medidos los dieciocho
casos: el peor da 6,14 sobre 4,5 que pide el texto pequeño.
El botón de añadir al pack faltaba por lo mismo: el importador ponía
`estado = publicado` pero dejaba `ala_venta` en el false de la columna, y
un tema publicado y no a la venta sale sin botón. Ahora lo pone al
crear la canción y no lo toca al reimportar, que esa decisión es del
panel. Las diez ya están a la venta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El importador metió el texto; esto mete los archivos. Diez másteres a
media/audio (fuera de git), su muestra de cuarenta segundos a
static/muestras y la portada cuadrada de mil píxeles a static/images,
con sus filas en `medio` y atadas a cancion.audio_id, muestra_id y
portada_id. El disco se queda con la portada del primer tema porque la
carpeta no trae una suya.
Las portadas estaban en subcarpetas `cover` y `covers`, no en la raíz de
cada canción: el lector solo miraba la raíz y `Video`, así que ocho de
las diez iban a acabar con la miniatura de YouTube recortada. Ahora
busca la subcarpeta sin mirar mayúsculas, que además es un fallo de
verdad en Linux, donde `Video` no es `video`.
Comprobado a ojo: el botón de escuchar ya no está apagado, la muestra
suena (0:40, con su etiqueta) y las diez portadas cargan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El disco entró publicado y no había forma de corregir ni un título. Ahora sí:
título, letra, de dónde sale, idioma, número de pista, año, estado, y las tres
casillas —instrumental, destacada, a la venta—.
Agrupado por disco y no en una lista alfabética de veintiocho títulos, que es
como está ordenado en la cabeza de quien lo escribió. Cada fila dice lo que no
se ve desde fuera: si tiene letra, si tiene audio y si tiene portada. Con el
disco recién importado eso enseña de un vistazo lo que falta, y de paso que
«Dorina» lleva desde siempre sin letra.
**Al guardar se tira la caché del catálogo.** Vive un minuto en memoria, así que
sin esto un cambio tardaba en verse y parecía que no se había guardado. El
propio `catalogo.ts` ya lo dejaba escrito: «cuando haya administración, esto
tendrá que invalidarse al guardar, no por reloj». Comprobado editando una letra
en el panel y viéndola en la web sin esperar.
No se edita aquí lo que no se escribe: el audio y las portadas son archivos, la
autoría y el registro son papeles, y las versiones son otras filas. Todo junto
sería un formulario de cuarenta campos donde lo de a diario queda enterrado.
Publicar un tema sin letra se rechaza, salvo que esté marcado instrumental: sin
eso queda en la web una página con el título y nada debajo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El primer disco real del catálogo. Hasta ahora todo era inventado salvo
«Dorina».
`npm run album:importar -- "<carpeta>"` lee la carpeta y dice lo que haría;
solo escribe con `--escribir`, y hay que pedirlo. Es idempotente por slug, así
que corregir un `.txt` y volver a lanzarlo actualiza en vez de duplicar.
El desmontaje de los `.txt` está en `scripts/album/carpeta.mjs`, compartido con
el lector: si Suno vuelve a cambiar lo que pone al copiar, se arregla en un
sitio. De cada archivo salen tres cosas —el prompt de estilo, la letra con
etiquetas y la letra limpia de detrás de la raya— y la que se publica es la
limpia; la de las etiquetas es lo que se le pega a Suno, y va en el prompt.
Los títulos salen del nombre del máster porque las carpetas están sin acentuar
—«Quèdate», «Pague»—, y `--titulo` corrige lo que ni siquiera eso acierta: el
archivo francés pone «libére» donde va «libère». El idioma se deduce contando
palabras frecuentes: nueve en español y una en francés. La fecha sale del
máster más reciente, que es lo más cerca que hay de «cuándo quedó terminado».
Y el prompt de cada canción, que es lo que obligó a la migración 0012: `prompt`
ataba a un género y estos son de canciones concretas. No sustituye a
`genero_id`, se suma, y es coherente con lo que el modelo ya preveía —
`ejemplo_id` apunta al audio que salió del prompt—.
## El hueco de la portada
Diez fichas publicadas sin imagen destaparon algo que llevaba ahí desde
siempre: un `<img>` sin `src` es el icono de imagen rota del navegador, y eso
salía en siete sitios —la ficha del disco, la del tema, las dos tarjetas, el
carrito, la cuenta y la barra del reproductor—. Nunca se había visto porque
todo el catálogo tenía imagen.
`SinPortada` ocupa el hueco con la inicial del título. No dice «falta la
portada»: eso es un problema nuestro, no de quien mira. Y va a hacer falta más,
porque el texto de un disco entra por un lado y sus archivos por otro.
El botón de reproducir ya se apagaba solo sin audio; eso estaba bien hecho.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Estaban en 0.9375rem —quince—, que no es ningun peldano de la escala: era un
numero puesto a mano. `--fs-base` si lo es, y es el cuerpo del texto del sitio.
Carrito y Entrar suben con ellos: van pegados a la navegacion en la misma fila,
y dos tamanos distintos ahi se leen como un descuido.
Comprobado que no desborda a 320, 375, 768 ni 1440.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Las mismas cinco paradas y el mismo ángulo que el botón dorado, recortadas
sobre las letras, y la banda de luz recorre el nombre al pasar el puntero igual
que recorre el botón. De 24 a 34 píxeles, que es el siguiente peldaño de la
escala y no un número a ojo.
El recorte va dentro de un `@supports` y el color plano se queda fuera: sin el
recorte, `color: transparent` dejaría la marca invisible, y eso es peor que
dejarla dorada y lisa. Es el mismo cuidado que ya tenía el botón, que guarda un
color de reserva por si el degradado no llega a pintar.
Y hace falta un segundo degradado, `--oro-escrito`. Sobre marino es
literalmente el mismo que el del botón —la peor de sus paradas da 5,4 de
contraste—, pero sobre papel no vale: medido contra el fondo claro, la parada
de brillo da **1,26**, la de superficie 2,05 y la de sombra 3,79, las tres por
debajo del mínimo de 4,5. Con el degradado del botón, el centro de las letras
desaparecía. En claro la banda va al revés: el punto más claro es el propio
`--c-accent` —5,29— y los extremos se oscurecen. Sigue pareciendo metal porque
lo que hace el efecto es la banda que cruza, no que llegue a brillar.
Es la regla que el proyecto ya tenía escrita y que yo no había aplicado: el
dorado hace dos trabajos, y de relleno tiene que ser claro mientras que escrito
tiene que ser hondo.
La prueba comprueba **cada parada** del degradado contra el fondo, en los dos
temas. A ojo, en la captura, el fallo se veía como «algo más pálido»; en
números, como ilegible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`node scripts/album/leer.mjs "<carpeta>"`. No escribe nada: solo informa.
Hace falta porque el material no esta en un formato, esta en cinco. Los `.txt`
de letra vienen de pegar lo que devuelve Suno, y Suno ha ido cambiando lo que
pone alrededor —«Add a Caption», «Weirdness 58%», «(Cover) artwork»—, asi que
la cabecera es distinta en casi cada cancion. Un importador que diera eso por
bueno meteria en la base la palabra «artwork» como si fuera una letra.
De cada `.txt` saca tres cosas: el prompt de estilo que se le dio a Suno, la
letra con etiquetas —que es lo que se le pega a Suno, no lo que se publica— y
la letra limpia de detras de la raya de guiones, que es la de la web. Cuando no
hay raya, la limpia se saca quitando las lineas de etiqueta.
El titulo sale del nombre del master y no de la carpeta: las carpetas estan sin
acentuar o mal acentuadas —«Quedate», «Pague», «para que recordar»— y el master
lleva el titulo como se escribe.
Sobre las diez canciones de «cupido-sin-flechas» encuentra letra, prompt, mp3 y
wav en todas; falta portada en una y video en tres.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Estaban modelados desde la fase 1 y no existían en el panel: `modelo_ia`,
`animo` y `prompt`, cero filas las tres y ni siquiera una línea en la portada.
Y la primera versión no respetaba el modelo. `modelo_ia.esquema` dice esto:
> JSON Schema con la forma que toman los prompts de este modelo. Se validan
> contra él al guardar, y el panel puede pintar el formulario a partir de él sin
> una pantalla por cada IA.
Había puesto un `textarea` único guardando `{ texto }` y forzado
`estado: 'publicado'` al crear, cuando el modelo dice borrador por defecto. Las
dos cosas van ahora como estaban escritas: el formulario del prompt sale del
esquema de su modelo —un campo por propiedad, con su etiqueta, su ayuda, su
lista cerrada y su rango— y lo que se guarda se comprueba contra él.
`$lib/esquema-prompt` es el lector, y dice en su cabecera qué subconjunto
entiende: objeto en la raíz con `properties` y `required`; por campo string,
number, integer o boolean, más `enum`, `maxLength`, `minimum`, `maximum`,
`title` y `description`. Lo que no esté se ignora; un esquema roto se rechaza al
guardarlo, no al usarlo. Un modelo sin esquema sigue funcionando con un bloque
de texto.
Y `x-order`, que no estaba previsto y hace falta: la base guarda el esquema como
`jsonb` y ahí Postgres reordena las claves por longitud, así que el campo
obligatorio salía el último del formulario. Se vio en la captura.
Los prompts van **dentro de la ficha del género**, no en una pestaña aparte: un
prompt es de ese género y escribir la ficha y sacar la música son la misma
sesión. Fuera de su formulario, porque cada prompt se guarda por su cuenta.
Los modelos se dan de alta a mano, con su versión: Suno v4 y v3.5 son dos
modelos, y una lista sembrada en un script envejecería en un mes. Los nueve
ánimos de la GEMS sí se siembran, con `npm run db:animos`.
Comprobado contra el panel de verdad: esquema aceptado y esquema roto
rechazado, formulario pintado desde el esquema —lista cerrada, número con
rango, letra en bloque—, campo obligatorio reclamado, un solo preferido por
combinación y el preferido el primero de la lista.
Falta `prompt.ejemplo_id`, el audio del resultado: pide los medios, que todavía
no se editan aquí.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo que se escribe de un género: resumen, lugar y año de origen, tempo, rima, las
notas de métrica y compás, los rasgos de escritura, el estado, en qué países se
canta y toda la prosa repartida en secciones.
Todo en una pantalla y un solo guardado, porque es una sola cosa. Repartido en
tres formularios habría que guardar tres veces para arreglar un párrafo, y en
una transacción, porque si al reemplazar los países fallara algo no puede quedar
el texto guardado y el mapa a medias.
Los países son lo que pinta el género en el mapa, así que la pantalla lo dice y
la validación lo defiende: una sola cuna —un género nace en un sitio; si nació
en dos orillas, la cuna es la región y el otro país va como adoptado— y ningún
género publicado sin país, que se quedaría fuera del mapa y sin manera de llegar
a él.
Las secciones se editan, se reordenan y se borran marcando una casilla, que se
puede desmarcar antes de guardar. Y se puede añadir una nueva con su clave.
De las fichas importadas por un modelo sale una casilla para darlas por
revisadas. Es la diferencia entre lo que propuso una máquina y lo que ha leído
una persona, y el modelo de datos la guarda a propósito: esto se publica con el
nombre del titular.
Comprobado de punta a punta: editar en el panel, guardar, y el cambio sale en la
web pública. Dos fallos que se vieron mirando las capturas y no las pruebas: un
`td` en flex se descolgaba de su fila y descuadraba las tres columnas de la
lista, y el «cuna» de cada país acababa a diez centímetros de su país, alineado
con el del de al lado, de modo que no se sabía de quién era cuál.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Un panel donde no se puede editar nada no es un panel.
**Se editan los capítulos** —los principios del taller y las fichas de género,
que son la misma tabla con un `tipo` distinto—: título, resumen, texto en
Markdown, estado y orden. Se guarda con el botón o con Ctrl+S, y sale publicado
al momento: el caché del Markdown tiene por clave el propio texto, así que un
cambio se invalida solo.
El botón de guardar va arriba y no al pie. Al pie tenía que ser flotante para
seguir a la vista mientras se escribe, y flotando se pintaba encima de la línea
que estabas escribiendo; se vio en la captura.
**La portada enseñaba siete cosas de las cuarenta largas que hay.** Ahora sale
todo el contenido agrupado —taller y géneros, glosario, catálogo, editorial—,
con su desglose y lo que falta en cada uno. Lo que no aparece en un panel no
existe: nadie se iba a acordar de que los metros y los compases siguen vacíos.
Fuera queda la explotación —pedidos, facturas, carritos—, que no se escribe.
Y los letristas ya se ven. Están en `persona` porque el papel no es de la
persona sino de su relación con cada género —`genero_persona.papel`, con clave
primaria (género, persona, papel)—, y la misma persona es letrista de uno y
compositora de otro. La página pública de letristas es una vista sobre eso. Lo
que estaba mal es que el panel no lo dijera: ahora la fila de Personas desglosa
15 letristas · 2 compositores · 1 intérprete · 4 fundadores.
Un menú, que tampoco había. Solo con lo que ya tiene pantalla: un menú con la
mitad de las entradas muertas es peor que uno corto.
`npm run db:admin -- correo --crear` para el arranque en frío. Sin eso no había
forma de nombrar al primer administrador: el script exigía que la cuenta ya
existiera, y en una base recién puesta no existe ninguna.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Estaba mal hecho y el síntoma era el `if` sobre `/admin` que acabó metido en el
layout de la web pública: la web sabiendo que existe el panel, al revés de como
debe ser.
Ahora son dos aplicaciones del mismo repositorio. Lo que las separa son tres
líneas de `vite.config.ts`: con `APP=panel`, las rutas salen de
`src/panel/rutas`, los hooks de `src/panel/hooks.server.ts` y el resultado va a
`build-panel/`. El servidor público se compila sin las rutas del panel, así que
en `senzapaura.es` no es que `/admin` esté protegido: no existe, y hay una
prueba que lo comprueba pidiéndolo.
Comparten `src/lib` —esquema, consultas, acceso—, que es lo que evita mantener
las migraciones por duplicado. Para que eso funcione, dos cosas que resolvían
rutas del sitio salen de en medio: la navegación, a `$lib/navegacion`, y los
tipos ambientales, que ahora son dos porque el `PageData` del sitio lleva
carrito y favoritos y el del panel no.
El precio de otro dominio es que el panel necesita su propia página de entrar:
la cookie se emite para el host y no para el dominio padre, que es justo lo que
hace que la sesión del panel no viaje en las peticiones de la web. Usa el mismo
código de `$lib/server/auth`, con una diferencia: solo manda código a una cuenta
que ya administre, y contesta lo mismo exista o no. Si dijera «esa cuenta no
administra», el formulario sería una forma cómoda de averiguar cuál sí.
Y con dominio propio, el 404 de ayer pasa a ser un 403 con el motivo escrito:
esconder el panel de quien ya está en `admin.senzapaura.es` no engaña a nadie, y
a esa rama solo llega alguien que administraba y ha dejado de administrar.
Dos servidores en las pruebas, uno por proyecto, con su `baseURL`: una prueba
del panel no puede pedir por accidente una página del sitio.
Aparte, la prueba de contacto otra vez. El arreglo de ayer no valía —comprobar
que el texto está escrito no impide que lo borren un instante después—, así que
el layout marca con `data-hidratado` el momento en que Svelte toma el control y
la prueba espera a eso. Quince pasadas con cuatro trabajadores, sin un fallo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fase 6. Está lo que todo lo demás necesita: quién entra y por dónde.
El rol vive en `usuario.rol`, con un CHECK que impide inventarse uno, y se da
con `npm run db:admin`. En una variable de entorno con correos habría sido más
rápido, pero entonces revocar a alguien pide un despliegue y a la pregunta
«quién administra esto» solo sabe contestar quien pueda leer la configuración
del servidor.
La guarda está en el `+layout.server.ts` de la carpeta y no en cada ruta: una
pantalla nueva queda protegida por el hecho de estar dentro. Sin sesión, a
entrar con la vuelta puesta; con sesión y sin el rol, 404 y no 403, porque un
403 confirma que el panel existe y dónde vive. Los tres casos, comprobados
contra la aplicación de verdad.
La portada contesta «qué falta» y no «cuánto hay». Cada sección trae lo que
tiene, lo que está a medias y los nombres de lo que está a medias: decir «1
género sin ficha» obliga a ir a buscar cuál, y esa búsqueda ya la ha hecho la
consulta. Dos viajes a la base para las siete filas.
Fuera Sveltia CMS y `static/admin/`. Editaba los Markdown de `src/content/`,
que desde la fase 4 no lee nadie: guardaba cambios que no salían en pantalla, y
además tapaba la ruta nueva.
Y de paso, la prueba de contacto que fallaba en la tanda completa y pasaba a
solas. No era aislamiento: `page.goto` vuelve al cargar, no al hidratar, y al
hidratar Svelte reescribe el valor de cada `input` con el del componente. Lo
tecleado antes de ese instante se perdía, y solo le pasaba al primer campo y
solo con la máquina cargada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cada punto se dibujaba dentro del grupo de su pais, y en un SVG no hay
`z-index`: manda el orden en que esta escrito. El relleno del pais siguiente se
pintaba encima y el punto salia mordido por un lado. Ahora hay dos pasadas,
primero todas las tierras y luego todos los puntos, y el que enciende cada uno
se anota en `bajoPuntero` y `conFoco`, porque desde la capa de arriba ningun
selector alcanza al pais de debajo.
De paso, tres cosas que se veian mal y no daban error:
- El radio salia de dividir por el ancho de la caja, pero con `max-height` el
dibujo se ajusta al alto y en una ventana baja America se pintaba a 0,57 en
vez de a 0,70. El punto de Mexico media doce pixeles y el de España quince.
Ahora la escala sale del lado que primero se queda corto.
- De España ya no se dibujan las islas: Canarias caia fuera del recuadro y las
Baleares eran cuatro pixeles sueltos al este.
- Un solo verde para todo pais con generos. El segundo tono servia para separar
la cuna de lo adoptado y lo unico que conseguia era que España, que solo
adopto el bolero, saliera de otro color que el resto. Esa diferencia se
cuenta en la lista de al lado.
La Peninsula, cuarenta pixeles a la izquierda; en movil no, que ahi su columna
mide noventa y se metia encima de America.
`e2e/mapa.e2e.ts` fija las cuatro: orden de pintado, tamaño del punto, color
unico y España sin islas. Ninguna daba error, el mapa salia igual, solo que
mal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Era proporcional al encuadre, y eso no basta: los dos mapas se dibujan a
anchos distintos —América ocupa una columna y la Península menos de la
mitad—, así que el mismo porcentaje daba un punto la mitad de grande en
España. Ahora se mide el ancho de cada lienzo en pantalla y se convierte:
catorce píxeles de diámetro en los dos, comprobado midiéndolos y no a
ojo.
Y «adoptado» pasa a ser el mismo tono que «cuna», solo que más flojo.
Antes era un verde claro que no se parecía a nada del otro mapa, así que
España parecía pintada de otro color en vez de con menos intensidad del
mismo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cuatro cosas que estaban mal y no vi por mirar las capturas de pasada:
- **No estaba centrado.** Para hacerle sitio a la Península había
ensanchado el lienzo de América por la derecha, y eso descentraba el
mapa en su caja. Ahora son dos columnas de verdad: cada mapa ocupa lo
suyo, el hueco entre ellos es el Atlántico y el conjunto se centra
solo.
- **Dos reglas `.mapa`**, y la vieja pisaba a la nueva: por eso la
Península salía a media altura en vez de al noreste, que es donde está.
- **Poco contraste en oscuro.** Los tres niveles —sin datos, adoptado,
cuna— se calibran ahora con distancia de luminosidad real, y el que no
tiene géneros deja de ser el color del fondo.
- **El punto es dorado**, con el mismo degradado de cinco paradas que los
botones: es el color de lo que se pulsa, y marca de un vistazo dónde
hay ficha que abrir.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La Península quedaba rozando el Caribe. El lienzo de América se ensancha
por la derecha con océano de sobra: entre las dos orillas hay un
Atlántico, y sin separación parecen el mismo continente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los dos lienzos tienen escalas muy distintas —seiscientas unidades de
ancho América, ciento diez la Península—, así que el mismo grosor salía
cinco veces más gordo en España y el contorno se comía el país.
`vector-effect: non-scaling-stroke` lo fija en píxeles de pantalla, que
es lo que se ve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
España y Portugal estaban fuera porque lo hice sobre un mapa de las
Américas y lo di por bueno con una nota. Iberoamérica es también la
Península, y España está en los datos: el bolero se adoptó ahí. El origen
pasa a ser el mapa del mundo de Wikimedia, en dominio público.
Cuatro cosas se rompieron por el camino y se ven todas en una captura:
- El mundial está en coordenadas **relativas**, donde los números son
desplazamientos y no posiciones. Medirlos como si lo fueran daba un
encuadre de cuarenta mil por cincuenta y ocho mil unidades.
- `d="…"` encajaba dentro de `id="Mexico_mainland"`, así que el trazado
que se extraía era el nombre del país. Salía un mapa vacío sin que nada
fallara.
- Al pegar trazados relativos, el `m` de cada uno se sumaba al final del
anterior. Ahora el primer salto de cada trazado es absoluto.
- El `clip-path` se aplica **después** del `transform` del elemento, así
que el recorte se movía con el mapa y el recuadro salía vacío.
La Península va en su propio lienzo, sobre el Atlántico norte: como
recuadro dentro del mapa acababa encima de Venezuela, y encima de la
lista de países no pintaba nada.
Y al lado, la lista de los veintidós con España primero y el resto
alfabético, con cuántos géneros nacieron en cada uno y cuántos adoptó.
Colores: el dorado mezclado con el fondo daba un marrón sucio. Entra
`--c-dato` en la paleta, un verdemar de la misma familia que el marino,
con dos intensidades para nacido y adoptado; el país elegido va en el
color del texto, que es el único contraste que se distingue de los dos
verdes en los dos temas. El radio del punto se mide contra el ancho de su
lienzo: fijo, en la Península salía del tamaño de Castilla.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sin esto la sección solo se alcanzaba desde el taller, que es justo lo
contrario de sacarla a su propio sitio.
La prueba del menú en móvil pasa a comprobar los nombres en vez de la
cuenta: así dice cuál falta en lugar de que el número no cuadra. Y las
cuatro páginas nuevas entran en la medida de desbordamiento a 320, 375,
768 y 1440.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Con lo que pediste: portada con mapa de Iberoamérica, buscador y un aside
que empieza por Inicio, Los principios, Glosario y Letristas antes de la
lista de géneros.
El mapa es interactivo de verdad: cada país se pinta según lo que tenga
—marcado si es cuna de algún género, tenue si solo lo adoptó, apagado si
no hay nada—, y al pulsarlo la lista de abajo se parte en «nació aquí» y
«también se canta ahí». Esa diferencia es lo que un mapa cuenta mejor que
una lista.
Los contornos son de `BlankMap-Americas.svg` de Wikimedia, en dominio
público, recortados a los veinte países de Iberoamérica en América. De
640 KB a 36 KB comprimidos, y solo en esta ruta: coordenadas a un decimal
y fuera los 134 islotes que a este tamaño miden menos de un píxel. Va por
script y no pegado a mano, para poder rehacerlo.
Cada país lleva además un punto de agarre calculado —el promedio de los
vértices de su costa, no el centro de su rectángulo, que en Cuba cae en
el mar—: sin él, Puerto Rico mide cuatro píxeles y una isla alargada no
hay quien la acierte.
«Letristas» es nueva: las mismas personas que salen repartidas en nueve
fichas, aquí juntas, con su papel en cada género y sus obras.
La página del capítulo pasa a `$lib/components/Capitulo.svelte` y la usan
las dos secciones: son setecientas líneas, y copiarlas habría sido dos
sitios donde arreglar cada cosa.
Las direcciones viejas redirigen con 301 —`/taller/bolero` a
`/generos/bolero`— resolviendo contra el catálogo de capítulos, no contra
una lista escrita a mano que alguien tenga que ampliar. Un enlace
compartido no puede empezar a dar 404 porque hayamos reordenado el menú.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo que se pidió: «cada vez que aparezca un término del glosario en el
texto, estará marcado para visualizarse con un popup con la explicación».
Veintinueve términos con acepciones numeradas —«clave» es el patrón
rítmico y el instrumento de percusión, y con una sola definición una de
las dos sería falsa—, más las formas con las que cada palabra aparece de
verdad: plurales, femeninos, con tilde y sin ella.
El marcado va sobre el HTML ya compuesto y no sobre el Markdown: antes
habría que esquivar la sintaxis, y un término dentro de un enlace o de un
bloque de código es justo donde un reemplazo de texto se equivoca en
silencio. Se marca una vez por término y por apartado, no en cada
aparición: nueve «compás» subrayados no hay quien los lea.
La pieza tiene más casos raros que cualquier otra del sitio y ninguno se
ve mirando la página —el fallo es que un globo no salga, y nadie echa en
falta lo que no sabe que existía—, así que va separada de la base y con
doce pruebas. Dos las escribí creyendo que ya pasaban:
- Marcaba «sinalefas» del final del párrafo en vez de la «sinalefa» de la
primera línea, porque probaba forma por forma en orden de longitud.
- Con las formas sin ordenar marcaba «rima» y no «rima asonante». Salía
bien solo porque la consulta las devolvía ordenadas; ahora el desempate
está en la función, que no puede depender de cómo se la llame.
El globo es un botón y no un `title`: un tooltip nativo no se abre con
teclado y en un móvil no existe. Se cierra con Escape devolviendo el
foco, y se coloca debajo del término o encima si no cabe, porque tapar lo
que se acaba de pulsar es lo peor que podría hacer.
El glosario entero tiene además su página, donde sí cabe la ampliación
que en un globo taparía el texto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>