# Modelo de datos
Diseño de la migración del contenido a PostgreSQL. **Fase 1 cerrada** : el
modelo está completo y las decisiones tomadas. No hay código escrito todavía.
Versión presentable, con diagramas entidad-relación:
< https: / / claude . ai / code / artifact / d87ceb4d-6038-402f-852e-13ff5d86f02f >
Este archivo es la especificación; el artefacto es la misma información puesta
para leerse. Cuando se implemente, la verdad pasa a ser
`src/lib/server/db/schema.ts` y esto queda como el porqué.
## Por qué
Lo que hay hoy en `src/content/**` son registros disfrazados de documento: una
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
canción tiene **veintiún campos estructurados en YAML** —`registro`, `duracion` ,
`interpretes` , `versiones` , `videos` , `etiquetas` …— y el archivo entero existe
para sostenerlos.
> **Corrección.** Una versión anterior de este documento decía que la letra
> también iba en el frontmatter y que el cuerpo del Markdown eran cuatro líneas
> de comentario. **Es al revés**: el cuerpo del Markdown es la letra, y el campo
> `letra:` —que aparece en un solo archivo— son los créditos de quién la firma.
> Se vio al escribir el volcado. No cambia la conclusión, pero sí el reparto: la
> letra es prosa legítima y se queda como texto; lo que no debía estar en YAML
> son los otros veintiún campos.
Dos hallazgos concretos, no cuestión de gusto:
**1. Alguien puede perder una canción que pagó.** Cinco tablas apuntan a una
canción por una cadena de texto —`carrito_item`, `pedido_item` , `compra` ,
`favorito` , `valoracion` — y ninguna tiene clave foránea, porque el destino no
está en la base sino en un archivo. Renombrar un slug deja huérfana una compra
sin que salte ningún error en ningún sitio.
**2. `estilo` hace dos trabajos distintos.** En el catálogo son las secciones del
artista (baladas románticas, clásica, rock, pop melódico); en el taller, una
«ficha de estilo» es un género hispanoamericano (bolero, tango, cumbia). Son dos
entidades que comparten palabra y hoy comparten nombre en el código. La
clasificación por país es del género, no del estilo, y mientras sean lo mismo no
se puede ni escribir.
A eso se suma que nada se valida —`estilo: bolreo` no falla, la canción
desaparece de su sección—, nada se consulta sin cargar el catálogo entero en
memoria, y cambiar una coma exige reconstruir y desplegar.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
Alcance: **60 tablas** (13 ya existen), **48 archivos** que migrar, **22 rutas**
que hoy leen `$lib/content` .
## Decisiones transversales
| Decisión | Elegido | Motivo |
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Identificadores | `text` (nanoid 21), como las tablas actuales | El slug es texto que se lee y algún día se corrige; una clave primaria no debería cambiar nunca |
| Slugs | únicos + tabla `redireccion` | Renombrar deja de romper enlaces publicados; la dirección vieja responde 301 |
| Prosa | columna `text` con Markdown, compuesto en el servidor | Comprobado: ningún archivo de contenido usa sintaxis de Svelte, así que no se pierde nada al dejar de compilarlo con mdsvex |
| Listas | tabla si se consulta, `text[]` si solo se enseña | Las etiquetas cruzan el catálogo; los temas y rasgos de un género solo se pintan |
| Publicación | `estado` (borrador · publicado · retirado) | Escribir una ficha a medias no puede significar publicarla |
| Fechas | `timestamptz` siempre; `creado_en` y `actualizado_en` | Y en los legales, además, historial: hay que poder demostrar qué decía el día de una compra |
| Enumerados | texto con `CHECK` , no `ENUM` de Postgres | Es lo que genera Drizzle y permite añadir un valor sin una migración que bloquee la tabla |
| Borrados | reales en contenido; `retirado` en lo vendido | Una canción comprada no se borra nunca: la compra tiene que seguir apuntando a algo |
## Las entidades
### 1 · Catálogo
- **`estilo`** _(cambia)_ — id, slug, nombre, resumen, orden, `principal` con
índice parcial único (hoy es un acuerdo de palabra), imagen_id, estado.
- **`album`** _(cambia)_ — + numero_catalogo, `formatos text[]` ,
pais_edicion_id. Los campos de `edicion` dejan de estar anidados.
- **`cancion`** _(cambia)_ — album_id nulo en los singles; **el álbum «Singles»
deja de existir como registro**, era un invento para pintar y se resuelve al
consultar. letra, nota, inspiracion y traducción salen del YAML.
- **`cancion.version_de_id`** _(decidido)_ — **una versión es una fila de
`cancion` , no una tabla aparte.** Nulo en las principales. Se llegó aquí
descartando partir en `obra` y `grabacion` : en ese reparto el estilo y las
etiquetas colgarían de la obra, y una versión no podría tener los suyos —una
balada con arreglo de bolero no cabría—. Con una fila por versión, cualquier
campo puede cambiar.
De regalo se arregla algo que estaba mal: **el disco pasa a ser de la fila** .
Hoy `album_id` y `numero` cuelgan del tema, así que la «Maqueta 2018» es el
corte 1 del álbum igual que la versión de estudio, y es falso.
Cuatro cabos que hay que atar, o se vuelve en contra:
1. **Vista `cancion_principal`.** Cuatro funciones devuelven listas de
canciones y siete rutas las pintan, más los recuentos por estilo y por
etiqueta. Si a una se le olvida `version_de_id IS NULL` , el catálogo dice
veinticinco temas donde hay dieciocho y nadie lo nota. Lo normal es
consultar la vista; pedir versiones es lo que hay que escribir a propósito.
2. **Un solo nivel, comprobado en la base.** Una autorreferencia admite ciclos
—A apunta a B y B a A— y una consulta recursiva sobre eso se cuelga. Se
ataja con una clave foránea compuesta contra una columna generada
`es_version` , sin triggers.
3. ** `ala_venta` solo en las principales**, y comprar la principal entrega
todas sus versiones. Es la regla que ya estaba escrita, ahora literal.
4. **Los campos se copian al crear, no se heredan al leer.** Un nulo heredado
nunca se sabe si es «no lo sé» o «lo mismo de arriba». Y aquí copiar es
además lo correcto: la maqueta de 2018 no tiene la misma letra, le falta el
puente.
- **`persona`** _(nueva)_ — nombre, nombre_legal, ipi. Hoy autores e intérpretes
son cadenas sueltas repetidas en cada archivo.
- **`cancion_persona`** _(nueva)_ — rol (letra · musica · interprete · arreglos ·
produccion), orden.
- ~~**`version`**~~ — **desaparece como tabla** : cada versión es una fila de
`cancion` con `version_de_id` . El `isrc` va en esa fila, no en la principal:
identifica una grabación, y cada arreglo es una grabación distinta. El `iswc`
es de la composición y se repite en la familia.
- **`registro_obra`** _(cambia)_ — iswc, entidad, numero, editorial. 1:1 con la
canción.
- **`autoria`** _(cambia)_ — porcentaje `numeric(5,2)` , no entero: los repartos
reales llevan decimales. Suma 100 comprobado en la base.
- **`etiqueta`, `cancion_etiqueta` ** _(cambia)_ — hoy se deducen de texto libre,
así que «desamor» y «Desamor» son dos etiquetas y nadie lo ve.
- **`traduccion`** _(nueva)_ — cancion_id + idioma. Hoy solo cabe una.
- **`video`, `cancion_enlace` , `album_enlace` , `credito_album` ** _(cambia)_ —
**dos tablas de enlaces y no una polimórfica** : una clave foránea de verdad
vale más que ahorrarse una tabla.
### 2 · Taller, géneros y glosario
**Decidido: el catálogo de géneros aspira a ser completo** y se llena poco a
poco. Eso cambia el peso de esta área: la ficha de género deja de ser una
tarjeta y pasa a ser la pieza más rica del sitio, con su historia, sus
instrumentos, sus letristas y sus obras de referencia.
- **`pais`** _(nueva)_ — código ISO 3166-1, nombre, gentilicio, region.
- **`genero`** _(nueva)_ — bolero, tango, cumbia. En columnas solo lo suyo:
origen_lugar, origen_ano (ordenable, hoy no lo es), tempo, rima, y las notas
en prosa `metrica_nota` y `compas_nota` para el matiz que no cabe en una
tabla —«a menudo alternados» es lo que hace útil la ficha—.
**Lo comparable sale a tablas auxiliares.** Con ocho fichas se lee; con cien,
que es a donde va esto, hay que poder preguntar.
- **`genero_origen`** _(nueva)_ — genero_id, origen_id, **tipo** (deriva ·
fusiona · influye), `principal` , nota, orden.
**Sustituye a `padre_id`, que no aguantaba.** Un género no deriva de uno:
el bolero-son es bolero _y_ son; la salsa viene del son, el mambo y la
guaracha; la bachata, del bolero, el son y el merengue. Con un solo padre, la
primera fusión obliga a elegir cuál de los dos mentir —y `bolero` y
`son-cubano` ya existen como fichas separadas—.
El tercer tipo, `influye` , es más débil que derivar y hoy ya está escrito,
aunque en el campo equivocado. Esto es literal de `son-cubano.md` :
> metrica: 'Octosílabo, **herencia de la décima y la copla españolas**'
Un vínculo entre géneros, metido dentro del campo de la métrica porque no
había dónde ponerlo.
El grafo guarda la verdad y la arista `principal` —única por género, con
índice parcial— da el árbol de navegación. Los subgéneros son las filas que
apuntan a uno con tipo `deriva` .
Guardas: `genero_id <> origen_id` , y **un disparador contra los ciclos** . Un
grafo admite que A derive de B y B de A, y una consulta recursiva sobre eso no
termina. En una tabla de este tamaño, comprobarlo al insertar con un
`WITH RECURSIVE` no cuesta nada.
- **`metro`** + ** `genero_metro` ** _(nuevas)_ — codigo, nombre, `silabas` , y
`termino_id` contra el glosario, porque «octosílabo» es también una entrada
suya. La tabla puente lleva `predominante` .
Semilla: de pentasílabo a alejandrino, más una fila ** `libre` ** con `silabas`
nulo, para que «géneros sin métrica fija» —la bachata: «Libre, cercana al
habla»— también sea una consulta y no una ausencia.
El motivo, con los datos delante: el octosílabo aparece en el bolero, el son y
el tango; el endecasílabo en el bolero y el tango; el alejandrino en el tango.
Los tres escritos en prosa. ** «Enséñame los géneros en octosílabo» es justo lo
que un taller de métrica necesita poder preguntar**, y hoy no se puede.
- **`compas`** + ** `genero_compas` ** _(nuevas)_ — codigo ('2/4', '4/4', '3/4',
'6/8', '2/2'), nombre, numerador y denominador —guardados aparte para poder
pedir los binarios o los ternarios—, y `principal` en la puente. El bolero
lleva dos filas: 2/4 en el clásico, 4/4 en el moderno.
- **`genero_tema`** _(nueva)_ — genero_id, **etiqueta_id** , orden.
Los temas salen del array y **comparten vocabulario con las etiquetas de las
canciones**, en vez de tener el suyo. Son etiquetas disfrazadas de frase —«El
amor no correspondido», «La despedida»— y con cien géneros «qué géneros hablan
del desarraigo» es la misma pregunta que no se podía hacer con la métrica.
Compartir vocabulario no es ahorro, es lo que hace posible cruzar: «el
desamor: nueve temas míos y cuatro géneros que viven de él». Con dos listas
separadas, «desamor» y «desamor» serían dos cosas distintas y nadie lo vería.
Los `rasgos` **se quedan como array** : aquellos sí son prosa de verdad
—«Tratamiento de usted o de tú, nunca coloquial»— y meterlos en un vocabulario
sería inventar etiquetas que nadie va a reutilizar.
- **`genero_alias`** _(nueva)_ — genero_id, nombre, pais_codigo, nota.
_Son cubano_ , _son jarocho_ , _son huasteco_ , _son montuno_ . La cumbia
colombiana y la argentina. Con cien fichas, buscar «son» devuelve cuatro cosas
que no se parecen; cada una tiene que encontrarse por como la llaman donde se
toca.
- **`genero_seccion`** _(nueva)_ — clave, titulo, cuerpo, orden. **Toda la prosa
de la ficha**: introducción, historia, estructura de las canciones, motivación
y temática. Y lo que surja mañana. Con una columna por bloque, en un mes hay
quince columnas y la mitad vacías; siendo secciones ordenadas, un apartado
nuevo no cuesta una migración. Es la respuesta a «y otros campos que puedan
surgir».
- **`genero_pais`** _(nueva)_ — relacion: cuna · adoptado. La cumbia nace
colombiana y se hace mexicana, peruana y argentina; el tango es de las dos
orillas. Un solo país sería mentira.
- **`instrumento`** _(nueva)_ — slug, nombre, **familia** (cuerda · viento ·
percusión · teclado · electrófono · voz) y subfamilia libre, pais_codigo,
descripcion, **afinacion** , `imagen_id` y `muestra_id` contra `medio` —el
sonido de muestra es un archivo como cualquier otro—, estado.
- **`genero_instrumento`** _(nueva)_ — con **importancia** (esencial · habitual ·
ocasional) y **papel** («marca la clave», «lleva el bajo»). Sin eso, el tres
cubano y una trompeta ocasional pesarían igual en la ficha, y no es verdad.
- **`genero_persona`** _(nueva)_ — papel: letrista · compositor · interprete ·
fundador. **No hay tabla `letrista`** : un letrista es una persona, y con dos
tablas la misma acabaría existiendo dos veces con distinta grafía el día que
colabore con alguien que además es figura de un género. `persona` gana los
campos biográficos: nacimiento, defuncion, pais_codigo, retrato_id, semblanza.
- **`obra_referencia`** + ** `obra_referencia_persona` ** _(nueva)_ — titulo, ano,
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
genero_id, nota, `cita` . ** «Letras de referencia de un letrista» y
«canciones del género» son la misma entidad**, mirada desde un lado o desde el
otro.
⚠️ **No se alojan letras completas de terceros.** «Bésame mucho» tiene dueño y
lo tendrá durante décadas. Se cita título, autor, año y un fragmento breve al
amparo del derecho de cita —el mismo que invoca nuestro propio aviso legal— y
se enlaza a dónde escucharla. Por eso el campo se llama `cita` y es corto, no
`letra` .
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
- **`obra_referencia_enlace`** _(nueva)_ — obra_id, plataforma, url.
**Sustituye a la columna `enlace`** , que era texto suelto y solo daba sitio
para uno: una obra está en Spotify **y** en YouTube, y quien lee usa la que
usa. Comparte el vocabulario `PLATAFORMAS` con `cancion_enlace` y
`album_enlace` : son la misma clase de dato, y dos formas de escribirlo son dos
formas de equivocarse.
#### Ejemplos de versos
- **`verso`** + ** `verso_linea` ** _(nuevas)_ — la ficha podía decir 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 —género, título del ejemplo, qué `ilustra` (metrica ·
rima · acento · imagen · registro · estructura), `analisis` , medida y rima del
conjunto— y no el verso suelto, porque la rima y la alternancia de medidas
solo se ven en dos o cuatro versos seguidos.
`verso_linea` lleva cada verso con su escansión: `silabas` es un array de
sílabas **métricas** , donde la sinalefa mete dos palabras en una casilla
—«za⁀a»— porque al cantarlas se dice una; `tonicas` son las posiciones
acentuadas, y `medida` va aparte y no se calcula del array porque un verso
agudo suma una sílaba y uno esdrújulo resta una.
`fuente` distingue ** `ejemplo` ** (escrito para el taller, sin dueño de
fuera), ** `propio` ** (de una canción del catálogo) y ** `cita` ** (fragmento
breve de obra ajena). No es cosmético: decide qué se puede publicar entero, y
dos `CHECK` lo sostienen —un verso `cita` sin `obra_id` o uno `propio` sin
`cancion_id` no entran—.
La escansión se comprueba antes de cargarla, en `scripts/db/escansion.mjs` ,
contra la ley del acento final. Un ejemplo mal contado enseña a contar mal, y
eso no lo detecta ningún tipo ni ningún lint.
- **`cancion_genero`** _(nueva)_ — un tema propio escrito en ese género, aunque
en el catálogo viva bajo «baladas románticas». Es lo que permite que una ficha
enseñe obra propia además de la ajena.
- **`capitulo`** _(cambia)_ — tipo (principio · ficha), genero_id solo en las
fichas, cuerpo, estado.
#### Prompts para IA generativa
Biblioteca de prompts por género para Suno y equivalentes.
- **`modelo_ia`** _(nueva)_ — codigo, familia (suno · udio · riffusion ·
stable-audio · mureka), **version** , nombre, vigente, nota_sintaxis y
** `esquema jsonb` **: un JSON Schema que describe la forma que toman los
prompts de ese modelo. Con él se validan al guardar y el panel pinta el
formulario solo, sin una pantalla por cada IA.
**El prompt depende del modelo, no de la IA.** Un prompt de Suno v3 no es uno
de v4: cambian la sintaxis, las etiquetas y lo que el modelo ignora. Si la
clave foránea apunta a «Suno» a secas, el día que salga la versión siguiente
queda una biblioteca de prompts sin saber para qué servían.
- **`animo`** _(nueva)_ — codigo, nombre, **familia GEMS** , valencia, energia,
descripcion.
La codificación es la **GEMS** (_Geneva Emotional Music Scale_), hecha para
emoción musical y no para emoción en general: asombro, trascendencia, ternura,
**nostalgia** , serenidad, fuerza, alegría vital, tensión y tristeza. Más los
dos ejes del modelo circumplejo de Russell —valencia y energía—, que sirven
para ordenar e interpolar y que además son redactables: «alta energía,
valencia baja» es una instrucción; «tensión» a secas, no.
Se llama `animo` y no `version` **a propósito** : `version` ya significa otra
cosa en este modelo —`cancion.version_de_id`— y con las dos compartiendo
nombre, en un mes ninguna consulta se lee sola.
- **`prompt`** _(nueva)_ — modelo_ia_id, genero_id, animo_id, idioma, notas,
`valoracion` 1– 5, `ejemplo_id` contra `medio` , `preferido` , visibilidad
(privado · taller · publico), estado, y ** `contenido jsonb` ** con el prompt
propiamente dicho.
**El contenido va abierto, y aquí sí toca.** La forma del prompt la define un
tercero y cambia sin avisarnos: Suno separa estilo de letra y admite
exclusiones, otro modelo pedirá `weirdness` o `style_influence` . Modelar eso
en columnas es perseguir el esquema de otro. Es el caso contrario al del
contenido del sitio, donde el esquema es nuestro y por eso ahí sí se modela.
Dos condiciones para que no se pudra, que es lo que le pasa siempre a un campo
abierto:
1. **Se valida contra el `esquema` de su modelo.** Sin eso, en seis meses hay
prompts con `exclude` , `excludes` y `excluded` y ninguna forma de saber
cuál era el bueno. Abierto no es lo mismo que sin validar.
2. **Lo que se consulta se queda fuera del JSON.** Género, ánimo, modelo,
valoración e idioma son columnas. Postgres indexa `jsonb` con GIN, así que
no sería fatal, pero «los prompts que funcionaron para el bolero» tiene que
seguir siendo una consulta trivial.
`jsonb` y no `json` : el binario se indexa y normaliza las claves; el otro
guarda el texto tal cual y no sirve para buscar.
**La versión del modelo no se repite dentro.** Vive en `modelo_ia.version` ; si
además se escribe en el JSON, tarde o temprano dirán cosas distintas. Si una
API la exige en el payload, se compone al enviar.
Se guarda **si funcionó** —valoración y archivo con el resultado—, porque sin
eso se acumulan doscientos prompts sin saber cuáles servían. Varios por
combinación, con uno preferido: esto se itera, no se acierta a la primera. Y
el idioma importa: en inglés suelen funcionar mejor, incluso para música en
español.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
- **`termino`** _(nueva)_ — la palabra: slug, termino único.
- **`acepcion`** _(nueva)_ — termino_id, **numero** , categoria, `definicion` de
una o dos frases (lo que cabe en un globo sin taparle el texto a nadie),
ampliacion, ejemplo.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
**Un término tiene varias acepciones numeradas** , y la categoría baja aquí:
«clave» es instrumento de percusión en la primera, patrón rítmico en la
segunda y armadura de tonalidad en la tercera. Con una sola definición por
palabra, dos de las tres serían falsas.
- **`termino_variante`** _(nueva)_ — «sinalefas», «heptasílabos». Al nivel del
término, no de la acepción: el plural no cambia de sentido. Sin esto el marcado
solo acierta en singular, que es casi nunca.
- **`capitulo_acepcion`** _(nueva)_ — se calcula **al guardar el capítulo, no al
pintarlo**, y **apunta a una acepción, no a un término** .
Eso resuelve la duda que quedaba sobre el marcado automático: en un texto sobre
el bolero, «clave» es el patrón rítmico y no la armadura. Lo automático sabe
encontrar la palabra pero no elegir el sentido, así que **propone y una persona
confirma cuál**. Es lo único que no se equivoca, y siendo tabla se puede además
desmarcar una aparición suelta.
Regla editorial: **solo la primera aparición de cada término por página.** Marcar
las veintidós veces que sale «estribillo» convierte el texto en un campo de minas
subrayado.
### 3 · Editorial
- **`entrada`, `entrada_cancion` , `entrada_etiqueta` ** _(cambia)_ — hoy la lista
de canciones son slugs sin comprobar: si el tema no existe, la entrada enlaza a
un 404 y nadie se entera.
- **`documento_legal`** + ** `version_legal` ** _(nueva)_ — historial con
`vigente_desde` y `resumen_cambios` . No es burocracia: es prueba.
- **`pagina`**, ** `redireccion` ** _(cambia / nueva)_ .
### 4 · Cuentas y comercio
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
> **Hecho en la fase 3.** Lo que sigue describía el problema; la solución está
> aplicada: `cancion_slug` ya no existe en ninguna de estas tablas.
- **`usuario`, `identidad` , `sesion` , `codigo_acceso` , `limite` ** _(sin cambios)_ .
- **`carrito_item`, `pedido_item` , `compra` ** _(cambia)_ — `cancion_slug text`
pasa a `cancion_id fk` . En `pedido_item` se congelan título y precio: una
factura tiene que seguir diciendo lo que decía.
- **`datos_facturacion`** _(nueva)_ — tipo, nombre, nif, dirección,
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
pais_codigo, predeterminada.
**El país decide el IVA, y no es el nuestro.** Una venta digital a un
consumidor de otro país de la UE lleva el IVA de _su_ país; fuera de la UE,
normalmente ninguno. Por eso el tipo se guarda por línea de factura y no como
una constante. Detrás de eso hay una obligación que no es una tabla sino un
trámite —el régimen de ventanilla única— y Stripe Tax puede calcularlo si se
activa.
- **`factura`** _(nueva)_ — serie y numero **correlativos y sin huecos** , base,
iva_tipo, iva_cuota, total, y `datos_congelados jsonb` : si el cliente cambia su
dirección, la factura vieja no puede cambiar con él.
- **`descarga`** _(nueva)_ — para poder demostrar la entrega en una reclamación.
### 5 · Interacción
- **`favorito`, `valoracion` ** _(cambia)_ — clave foránea, y `CHECK` de 1 a 5 en
la base y no solo al validar.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
- **`reproduccion`** _(nueva)_ — **decidido: se conservan por persona.** Con
`segundos` , porque una reproducción de tres segundos no es una escucha y
contarla igual falsea la cifra. Agregación por día y borrado del detalle a los
noventa días **son parte del diseño** , no un apaño para después.
⚠️ **Obliga a cambiar la política de privacidad antes de publicar esto.** Hoy
dice que no hay seguimiento, y con esta decisión deja de ser cierto. Hace
falta: la fila en la tabla de datos —qué se guarda, para qué, base legal y
plazo—, y que la persona pueda **ver y descargar su historial** desde su
cuenta. La base razonable es ejecución del contrato mientras alimente «lo
tuyo»; si además se usa para recomendar, ya es otra cosa y hay que decirlo.
### 6 · Medios
- **`medio`** _(nueva)_ — clase, clave, ** `publico` ** (los fragmentos sí, el
audio completo nunca; hoy eso lo garantiza la carpeta), mime, bytes,
duracion_s, ancho, alto, sha256, alt. Para que la administración pueda subir
audio, un archivo tiene que ser una fila.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
**Decidido: los archivos viven en el servidor pero bajo dominio propio**,
`m.senzapaura.es/archives` . Gana más de lo que parece: un dominio sin cookies no
manda la de sesión en cada imagen y cada audio, y el día que haya un CDN delante
no hay que tocar ninguna URL —la base del dominio es configuración, y
`medio.clave` sigue siendo la ruta—.
⚠️ **Esto cambia una decisión anterior.** En el proyecto está escrito que las
descargas no usan URLs firmadas «porque el servidor ya tiene la sesión delante».
En otro dominio la sesión **ya no va delante** : esa es justo la gracia. Así que:
- **Lo público** —portadas, muestras— se sirve directo y cacheado.
- **El audio completo** pasa a necesitar **URL firmada con caducidad corta** : la
firma la pone la aplicación después de comprobar la compra, y Nginx la valida
con `secure_link` . Sin eso, el dominio de medios sería una puerta abierta al
catálogo entero.
Y hace falta su propio registro A, su Proxy Host y su certificado.
### 7 · Importación de fichas de género
**Decidido: las fichas se generan fuera, con un modelo más potente, y entran en
JSON.** Eso convierte el contrato de importación en parte del modelo.
- **Un JSON Schema de ficha de género**, que se le da a esa IA como destino y
que valida el importador. Sin él, cada tanda llega con las claves un poco
distintas y la importación se vuelve artesanía.
- **Procedencia, en `genero` y en las auxiliares**: `origen` (manual ·
importado), `modelo_origen` , `importado_en` , `revisado_por` , `revisado_en` .
Es musicología publicada con tu nombre: la diferencia entre «importado» y
«revisado» tiene que estar en la base, no en la memoria de nadie.
- **Idempotente**, por slug: reimportar una ficha corregida actualiza, no
duplica.
- Las relaciones se resuelven por código —país, instrumento, metro, compás,
persona— y **lo que no exista se crea marcado como importado y sin revisar** ,
en vez de fallar o de colarse en silencio. Así se ve qué se ha inventado el
modelo.
### 8 · Bocetos, no se construyen ahora
`lista` + `lista_cancion` (la playlist que sigue sin definirse), `encargo` ,
`inscripcion_taller` . Se dejan sitio porque aparecer después obliga a migrar lo
ya cargado.
## Qué cambia en la web
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
1. **Sveltia CMS deja de servir.** Es un editor sobre git: escribía los Markdown
de `src/content/` , que ya no lee nadie. **Retirado** al empezar la fase 6, y
con él `static/admin/` ; un panel que guarda cambios que no salen en pantalla
es peor que no tener panel. Mientras no estén las pantallas de editar, el
contenido se carga por script y por SQL.
2. **El Markdown se compone en cada petición** , no al construir. Se cachea.
3. **Cada página consulta la base.** A cambio, cambiar una letra deja de exigir
un despliegue.
Los tres ya son ciertos. Los `.md` de `src/content` siguen en el repo porque son
de donde salió el volcado y con lo que se reconstruye la base, pero **editarlos
no cambia nada en pantalla**. `$lib/content` solo lo usan los scripts.
## Fases
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
1. ~~**Este documento.** Sin código.~~ **Hecha.**
2. ~~**Esquema y migración, con el sitio leyendo todavía de los archivos.**~~
**Hecha.** El volcado comparó las letras carácter a carácter antes de
confirmar. Por el camino se descubrió que `drizzle-kit migrate` deja de
aplicar en silencio cuando una migración lleva `$$` , y se escribió
`scripts/db/migrar.mjs` en su lugar.
3. ~~**Las claves foráneas de las compras.**~~ **Hecha** (migraciones 0007 y
0008). Siete tablas —`carrito_item`, `pedido_item` , `compra` , `favorito` ,
`valoracion` , `reproduccion` , `descarga` — apuntan ahora a `cancion.id` . En
dos pasos, para no tener que parar el sitio: primero convive la columna
vieja, después se borra. Las de dinero —compra, pedido_item, descarga—
**restringen** el borrado; las demás caen con la canción. Las guardas nuevas
se comprueban en `npm run db:guardas` .
4. ~~**Las 22 rutas, de archivos a consultas.**~~ **Hecha.** Ningún archivo de
`src/routes` importa ya de `$lib/content` .
Lo que la bloqueaba no eran las rutas sino el cliente: 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— y `$lib/catalogo/cliente.svelte` lo ata
a las mismas consultas que usa el servidor. Ya no viaja en el paquete.
La red fue `npm run db:comparar` , que pone el catálogo de la base y el de
los archivos lado a lado campo a campo. En su primera pasada encontró que
**ningún medio estaba enlazado** —las canciones tenían `audio_id` y
`portada_id` a nulo, así que la web habría dejado de reproducir—, que las
etiquetas volvían alfabéticas en vez de en su orden, y que faltaban columnas
para la nota de cada versión, el país de edición y el texto de estilos y
discos. `npm run db:completar` ata esos cabos y es idempotente.
5. ~~**Lo nuevo:** géneros por país, glosario con globos, introducción larga
del taller.~~ **Hecha.** Países con su relación, instrumentación con el peso
de cada instrumento, letristas, obras de referencia, linaje, ejemplos de
versos con su escansión y el glosario.
El glosario marca los términos sobre el HTML ya compuesto —no sobre el
Markdown, donde habría que esquivar la sintaxis—, una vez por término y por
apartado, y nunca dentro de un enlace, un encabezado o un bloque de código.
`capitulo_acepcion` sigue vacía a propósito: el marcado propone y una
persona confirma, y hasta que exista la administración manda la primera
acepción —que es la razón de que estén numeradas por orden de importancia—.
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
6. **La administración.** _En curso._ Está la puerta y está la portada.
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
**Decidido: el panel es otra aplicación, en `admin.senzapaura.es`.** Mismo
repositorio y misma base, otro proceso y otro dominio; el servidor público se
compila sin sus rutas, así que en `senzapaura.es` no es que `/admin` esté
protegido, es que no existe. Se hizo primero como `/admin` dentro de la web y
se movió: el síntoma de que estaba mal fue que el layout de la web pública
acabó con un `if` sobre `/admin` para quitarse la cabecera.
Comparten `src/lib` —esquema, consultas y acceso—, que es lo que evita
mantener las migraciones por duplicado. Lo único que las separa son tres
líneas de `vite.config.ts` .
El precio de tener 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, y eso es
deliberado —así la sesión del panel no viaja en ninguna petición de la web—.
Usa el mismo código de `$lib/server/auth` , y solo manda código a quien ya
administra, contestando lo mismo exista o no la cuenta.
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
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 «quién
administra esto» solo sabe contestar quien lea la configuración del servidor.
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
La guarda está en el `+layout.server.ts` del grupo que envuelve las pantallas
y no en cada ruta: así una pantalla nueva queda protegida por estar dentro.
Sin sesión manda a entrar con la vuelta puesta; con sesión y sin el rol,
**403 y con el motivo** , que ahí solo llega quien administraba y ha dejado de
administrar.
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
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.
Faltan las pantallas de editar: glosario —confirmar las acepciones marcadas,
que es lo que tiene `capitulo_acepcion` esperando—, capítulos y géneros.
## Decisiones tomadas por defecto
Las dos últimas se decidieron sin respuesta explícita, eligiendo en cada caso la
opción reversible. **Cambiarlas es añadir, no rehacer.**
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
- **Valoración y favoritos van en la canción principal**, no en cada versión. Si
cada versión se valorase aparte, la media de la tarjeta dejaría de significar
nada —¿la del máster, la de la maqueta, la mezcla?— y en favoritos saldrían dos
entradas del mismo tema. La unidad con la que se interactúa es la canción.
- **Los prompts nacen privados** (`visibilidad: privado`). No porque publicarlos
esté mal, sino porque es una decisión de posicionamiento sin tomar y esta es la
reversible: publicarlos es cambiar una columna; despublicarlos cuando ya están
indexados, no.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
## Todo lo decidido
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
- **Versiones**: una versión es una fila de `cancion` con `version_de_id` .
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
- **Catálogo de géneros completo**, que se llena poco a poco.
- **Las fichas se generan fuera en JSON** y se importan con esquema y
procedencia.
- **Género**: `genero_origen` en lugar de `padre_id` ; metro, compás y temática
como vocabularios; alias por país; completitud deducida, no marcada.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
- **El glosario lleva acepciones numeradas**, y el marcado apunta a una acepción:
lo automático propone, una persona confirma el sentido.
- **Los prompts van en `jsonb` ** validado contra el esquema de su modelo.
Cinco decisiones mas: fichas por JSON, acepciones, facturas, escuchas y medios
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>
4 weeks ago
- **Facturas de verdad**, con correlativo por contador, rectificativas e IVA por
país del comprador.
- **Se conservan las escuchas por persona.**
- **Los archivos, en `m.senzapaura.es/archives` **, con URLs firmadas para el
audio completo.
## Lo que no es una tabla y hay que hacer igual
1. **Reescribir la política de privacidad** antes de que las escuchas se
publiquen: hoy afirma que no hay seguimiento y dejaría de ser cierto. Es un documento legal, no una nota.
2. **El régimen de ventanilla única** del IVA para ventas digitales a otros
países de la UE. Trámite, no código. Stripe Tax calcula los tipos si se
activa.
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
3. **DNS, proxy y certificado** para `m.senzapaura.es` y para
`admin.senzapaura.es` , más el servicio del panel en el puerto 3001. Está
escrito en `despliegue/con-proxy-manager.md` .