codex/estilos-web-publica
codex/redisenio-dominio-musical
master
${ noResults }
23 Commits (master)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
fef07e6b8b |
La ficha de un tema: portada mayor, etiquetas y botones abajo
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> |
4 weeks ago |
|
|
af40c25e1b |
Cupido sin flechas ya suena y tiene portadas
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> |
4 weeks ago |
|
|
9cefeb879a |
«Cupido sin flechas»: diez canciones de verdad, con sus prompts
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> |
4 weeks ago |
|
|
31a28b18ab |
Lee la carpeta de un album y dice que hay dentro
`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> |
4 weeks ago |
|
|
1d287e3788 |
Los prompts, dentro de la ficha del género y con la forma que dice su modelo
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>
|
4 weeks ago |
|
|
4e53ba64f5 |
El panel ya edita, y enseña todo el contenido y no siete filas
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> |
4 weeks ago |
|
|
34668a80e3 |
El panel se va a su propio dominio y su propio proceso
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> |
4 weeks ago |
|
|
3981f33867 |
La puerta del panel, y una portada que dice qué falta
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> |
4 weeks ago |
|
|
174cf828e7 |
El punto del mapa ya no lo tapa el pais de al lado
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> |
4 weeks ago |
|
|
f91187ba08 |
El mapa es de Iberoamérica: los veintidós, no veinte
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> |
4 weeks ago |
|
|
d74700f9b0 |
Las fichas de género salen del taller y tienen su propia sección
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> |
4 weeks ago |
|
|
71c39db5db |
El glosario del taller, con su globo en el texto
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> |
4 weeks ago |
|
|
0b823d44b4 |
El catálogo se sirve de la base: música, canciones, discos y etiquetas
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> |
4 weeks ago |
|
|
715b28c3c0 |
El catálogo de la base ya es idéntico al de los archivos
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> |
4 weeks ago |
|
|
b22d7fbff2 |
Las compras dejan de apuntar a un texto y apuntan a la canción
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> |
4 weeks ago |
|
|
6797c03809 |
Ejemplos de versos en la ficha, y dónde escuchar cada obra
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> |
4 weeks ago |
|
|
8272f3c195 |
Un migrador propio: drizzle-kit dejaba de aplicar sin decirlo
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> |
4 weeks ago |
|
|
be28a29693 |
El capítulo del taller cubre el ancho, con su índice al lado
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> |
4 weeks ago |
|
|
e0a979bf5d |
La ficha de género pierde la barra lateral y gana su franja de datos
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> |
4 weeks ago |
|
|
54df820056 |
Vuelca el catálogo a PostgreSQL, y lo comprueba
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> |
4 weeks ago |
|
|
1a8c904ae8 |
Fase 2: el esquema entero en PostgreSQL
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> |
4 weeks ago |
|
|
65cb4e0edc |
Fondo real en la portada, cabecera translúcida y menos líneas
La portada estrena imagen de fondo propia, detrás del tocadiscos y del texto. Llega en PNG de 2,1 MB y se sirve en tres anchos WebP: 10 kB en un móvil, 57 kB en un panel grande. El velo que va encima se afloja hasta lo justo para que el texto se lea, porque taparlo entero sería no haberlo puesto. El tema claro todavía no tiene su versión, así que ahí sigue el clip de reserva. La cabecera deja ver lo que pasa por debajo. La transparencia va con desenfoque y no sola: sobre una imagen a sangre, un fondo solo translúcido dejaría el contraste del menú a merced de lo que quedara detrás. Y fuera las líneas entre secciones: separa el aire. Se quedan las que dibujan una pieza —el pie, el reproductor, las filas de un listado, la tabla—. Aparte, scripts/construir.mjs comprobaba `build/server/index.js`, un archivo que adapter-node no genera: el servidor sale troceado en chunks. Daba por fallido un build correcto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
1 month ago |
|
|
c25d432d30 |
Web de Senza Paura: catálogo, taller, blog y tienda
Sitio del letrista y compositor, con SvelteKit 2 y Svelte 5. El catálogo es contenido, no base de datos: estilos, álbumes, canciones y capítulos son Markdown en `src/content/`, validados al compilar y compilados dentro del bundle, así que recorrer la música no hace ni una consulta. La base de datos guarda solo lo que generan las personas: cuentas, carritos, pedidos, compras, valoraciones y favoritos. Lo que hay: - Música agrupada por estilos, con página propia para cada estilo, álbum y tema, buscador y tabla ordenable. - Reproductor persistente que sobrevive a la navegación. Suena un fragmento; el archivo completo solo se obtiene comprando. - Taller de letras: los principios del oficio y una ficha de creación por estilo hispanoamericano. - Blog con feed RSS, y etiquetas que cruzan blog y catálogo. - Tienda con cuentas por código de correo, carrito, Stripe y descargas protegidas. - Panel de contenido en `/admin` que escribe los mismos Markdown. - Tema oscuro de marca y su inversión clara, aplicado antes de pintar. Lo que falta y no es código: las claves de Stripe y de correo, el tratamiento del IVA, las páginas de condiciones y privacidad, y el material real. Las canciones de ejemplo son inventadas y están para sustituirse; el audio son marcadores salvo un tema. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
1 month ago |