codex/estilos-web-publica
codex/redisenio-dominio-musical
master
${ noResults }
10 Commits (d74700f9b083f19a87be97dcbe04ac4048fb4a4a)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
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> |
1 month 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> |
1 month 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> |
1 month 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> |
1 month 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> |
1 month 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> |
1 month 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> |
1 month ago |
|
|
d3cc4e3488 |
Entrar con Google o con Facebook
Detrás de la cuenta hay compras y facturación, y un código por correo apoya toda esa puerta en el buzón: quien entre ahí, entra aquí. Delegar la identidad en un proveedor que ya tiene doble factor la sostiene mejor. Es el flujo de código de autorización con PKCE, escrito a mano porque son cien líneas y las partes que importan son tres, mejor a la vista que detrás de una dependencia: comparar el `state`, mandar el `code_verifier` y exigir que el correo venga verificado antes de enlazar nada. Lo que NO hace, a propósito: verificar la firma del `id_token`. La identidad no se saca de lo que trae el navegador, sino de una llamada nuestra al proveedor por TLS, así que no hay firma ajena que comprobar. Y no guarda ningún token: se usa una vez para preguntar quién es y se tira. El enlazado por correo verificado es lo delicado, y por eso está separado en `usuarioParaPerfil` con su explicación: sin exigir la verificación, cualquiera podría poner la dirección de otro en un perfil suyo y quedarse con su cuenta y sus compras. El código por correo se queda debajo, como alternativa. Sin credenciales puestas, los botones no se pintan y la ruta contesta 503: un botón que solo lleva a un error es peor que no tenerlo. Y los textos legales al día, que ahora sí hay terceros: qué se manda a Google y a Facebook, cuándo, qué se guarda y cómo revocarlo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
1 month ago |
|
|
3aebf91085 |
Mueve la base de datos de SQLite a PostgreSQL
El usuario ha puesto un servidor con PostgreSQL 16, así que la base pasa allí. El catálogo no se toca: sigue siendo Markdown del repositorio; lo que se muda es lo que generan las personas, que es lo único que estaba en la base. Dos diferencias entre motores que habrían pasado desapercibidas y se cazaron comprobando con datos de verdad, no compilando: - `integer` en Postgres son cuatro bytes y en SQLite ocho. El limitador de envíos guarda su vencimiento en milisegundos desde 1970 —trece dígitos, del orden del billón— y en `integer` desbordaría, dejando de limitar. Va en `bigint`. - `avg()` y `count()` los entrega el driver como cadena para no perder precisión. Sin convertirlas, `cuantas === 0` nunca se cumpliría y «1 voto» saldría siempre en plural. Se convierten en la consulta. Verificado contra el servidor: once tablas creadas, las 113 pruebas de extremo a extremo y las 224 unitarias en verde, el servidor de producción arrancando, el limitador escribiendo sus trece dígitos y el borrado en cascada llevándose valoraciones y favoritos al borrar una cuenta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
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 |