Estaba mal hecho y el síntoma era el `if` sobre `/admin` que acabó metido en el
layout de la web pública: la web sabiendo que existe el panel, al revés de como
debe ser.
Ahora son dos aplicaciones del mismo repositorio. Lo que las separa son tres
líneas de `vite.config.ts`: con `APP=panel`, las rutas salen de
`src/panel/rutas`, los hooks de `src/panel/hooks.server.ts` y el resultado va a
`build-panel/`. El servidor público se compila sin las rutas del panel, así que
en `senzapaura.es` no es que `/admin` esté protegido: no existe, y hay una
prueba que lo comprueba pidiéndolo.
Comparten `src/lib` —esquema, consultas, acceso—, que es lo que evita mantener
las migraciones por duplicado. Para que eso funcione, dos cosas que resolvían
rutas del sitio salen de en medio: la navegación, a `$lib/navegacion`, y los
tipos ambientales, que ahora son dos porque el `PageData` del sitio lleva
carrito y favoritos y el del panel no.
El precio de otro dominio es que el panel necesita su propia página de entrar:
la cookie se emite para el host y no para el dominio padre, que es justo lo que
hace que la sesión del panel no viaje en las peticiones de la web. Usa el mismo
código de `$lib/server/auth`, con una diferencia: solo manda código a una cuenta
que ya administre, y contesta lo mismo exista o no. Si dijera «esa cuenta no
administra», el formulario sería una forma cómoda de averiguar cuál sí.
Y con dominio propio, el 404 de ayer pasa a ser un 403 con el motivo escrito:
esconder el panel de quien ya está en `admin.senzapaura.es` no engaña a nadie, y
a esa rama solo llega alguien que administraba y ha dejado de administrar.
Dos servidores en las pruebas, uno por proyecto, con su `baseURL`: una prueba
del panel no puede pedir por accidente una página del sitio.
Aparte, la prueba de contacto otra vez. El arreglo de ayer no valía —comprobar
que el texto está escrito no impide que lo borren un instante después—, así que
el layout marca con `data-hidratado` el momento en que Svelte toma el control y
la prueba espera a eso. Quince pasadas con cuatro trabajadores, sin un fallo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fase 6. Está lo que todo lo demás necesita: quién entra y por dónde.
El rol vive en `usuario.rol`, con un CHECK que impide inventarse uno, y se da
con `npm run db:admin`. En una variable de entorno con correos habría sido más
rápido, pero entonces revocar a alguien pide un despliegue y a la pregunta
«quién administra esto» solo sabe contestar quien pueda leer la configuración
del servidor.
La guarda está en el `+layout.server.ts` de la carpeta y no en cada ruta: una
pantalla nueva queda protegida por el hecho de estar dentro. Sin sesión, a
entrar con la vuelta puesta; con sesión y sin el rol, 404 y no 403, porque un
403 confirma que el panel existe y dónde vive. Los tres casos, comprobados
contra la aplicación de verdad.
La portada contesta «qué falta» y no «cuánto hay». Cada sección trae lo que
tiene, lo que está a medias y los nombres de lo que está a medias: decir «1
género sin ficha» obliga a ir a buscar cuál, y esa búsqueda ya la ha hecho la
consulta. Dos viajes a la base para las siete filas.
Fuera Sveltia CMS y `static/admin/`. Editaba los Markdown de `src/content/`,
que desde la fase 4 no lee nadie: guardaba cambios que no salían en pantalla, y
además tapaba la ruta nueva.
Y de paso, la prueba de contacto que fallaba en la tanda completa y pasaba a
solas. No era aislamiento: `page.goto` vuelve al cargar, no al hidratar, y al
hidratar Svelte reescribe el valor de cada `input` con el del componente. Lo
tecleado antes de ese instante se perdía, y solo le pasaba al primer campo y
solo con la máquina cargada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Y la 5 empieza donde estaba: falta el glosario, que es lo único de lo
nuevo que sigue sin existir. Las tres cosas que el documento decía que
cambiarían en la web ya son ciertas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lo que falta no son las rutas: es que hoy el catálogo viaja en el
paquete de JavaScript y las tarjetas, el carrito, la cuenta y el
reproductor lo consultan por su cuenta. O baja por `page.data`, o esos
componentes reciben por props lo que buscan solos. Toca la compra, así
que conviene no hacerlo a medias.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
Las dos ultimas decisiones se toman por defecto eligiendo la opcion
reversible en cada caso: valoracion y favoritos en la cancion principal
—si cada version se valorase aparte, la media de la tarjeta dejaria de
significar nada— y prompts privados, porque publicarlos manana es cambiar
una columna y despublicarlos cuando ya estan indexados no.
Queda apuntado lo que no es una tabla y hay que hacer igual: reescribir
la politica de privacidad antes de guardar escuchas, la ventanilla unica
del IVA, y el DNS y el certificado de m.senzapaura.es.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Glosario: `termino` se parte en termino y acepciones numeradas, y el
marcado apunta a una acepcion. Eso cierra la duda del automatico: sabe
encontrar la palabra pero no elegir el sentido —«clave» es patron ritmico
en un texto de bolero y armadura en uno de armonia—, asi que propone y
una persona confirma.
Facturas: el correlativo sale de un contador con bloqueo y no de
max(numero)+1, que con dos compras simultaneas daria el mismo numero. Y
hacen falta rectificativas: una venta anulada no borra su factura. El IVA
es el del pais del comprador, no el nuestro.
Escuchas: se conservan por persona, y eso obliga a reescribir la politica
de privacidad, que hoy afirma que no hay seguimiento.
Medios en m.senzapaura.es: cambia una decision anterior. Estaba escrito
que las descargas no usan URLs firmadas porque la sesion va delante; en
otro dominio ya no va, asi que para el audio completo la firma deja de
ser opcional.
Fichas por JSON: el contrato de importacion pasa a ser parte del modelo,
con esquema, procedencia y creacion marcada de lo que la IA se invente.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Los temas salen del array a `genero_tema` y comparten vocabulario con las
etiquetas de las canciones. No es por ahorrar una tabla: es lo que
permite cruzar «el desamor: nueve temas mios y cuatro generos que viven
de el». Con dos listas separadas, «desamor» y «desamor» serian dos cosas
distintas y nadie lo veria.
Los rasgos se quedan como array porque si son prosa de verdad.
Y la completitud de una ficha se deduce de sus secciones y relaciones, sin
columna: una marca de progreso que se pone a mano miente a la semana.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`padre_id` no aguantaba. Un genero no deriva de uno: el bolero-son es
bolero y son, y la bachata viene de tres. `genero_origen` guarda el grafo
con el tipo de vinculo —deriva, fusiona, influye— y una arista marcada
como principal da el arbol de navegacion, asi que no hay que elegir entre
la verdad y la usabilidad.
Ese tercer tipo ya estaba escrito en el contenido, dentro del campo
equivocado: son-cubano.md pone la herencia de la decima y la copla dentro
de `metrica` porque no habia donde ponerla.
Y lo comparable sale de la prosa a vocabularios: el octosilabo aparece en
tres de las cinco fichas y no se podia preguntar por el. `metro` enlaza
ademas con el glosario, que es donde ese termino ya vive.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
La forma del prompt la define un tercero y cambia sin avisarnos, asi que
modelarla en columnas es perseguir el esquema de otro. Es el caso
contrario al del contenido del sitio, donde el esquema es nuestro.
Abierto no es sin validar: cada `modelo_ia` trae su JSON Schema, se
valida al guardar y el panel puede pintar el formulario a partir de el.
Y lo que se consulta —genero, animo, modelo, valoracion, idioma— se
queda en columnas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tres tablas: modelo_ia, animo y prompt.
El prompt cuelga del MODELO y no de la IA: uno de Suno v3 no es uno de
v4, y con la clave apuntando a "Suno" a secas queda una biblioteca sin
saber para que servia cada entrada.
El animo se codifica con la GEMS, hecha para emocion musical y no para
emocion en general —nostalgia es una de sus nueve categorias—, mas los
dos ejes de Russell, que ademas son redactables. Se llama `animo` y no
`version` a proposito: `version` ya significa otra cosa en este modelo.
Y un prompt no es un solo texto: Suno separa estilo de letra y admite
exclusiones, y guardar si funciono es lo que distingue una biblioteca de
un monton de texto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Versiones: una version es una fila de `cancion` con `version_de_id`, no
una tabla aparte. Se descarto partir en obra y grabacion porque en ese
reparto el estilo y las etiquetas colgarian de la obra y una version no
podria tener los suyos. Van los cuatro cabos que hay que atar, empezando
por la vista `cancion_principal`: hay once sitios que listan o cuentan
canciones y a los once se les puede olvidar el filtro.
Generos: el catalogo aspira a ser completo. La ficha se amplia con
instrumentos, letristas, obras de referencia y secciones de prosa. Sin
tabla `letrista` —un letrista es una persona— y sin letras completas de
terceros, que tienen dueno: se cita y se enlaza.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fase 1 de la migracion del contenido a PostgreSQL: las 41 entidades con
sus campos, sus relaciones y el motivo de cada decision. Sin codigo.
De analizarlo salieron dos cosas que no se buscaban:
- Cinco tablas apuntan a una cancion por una cadena de texto y ninguna
tiene clave foranea, porque el destino esta en un archivo. Renombrar un
slug deja huerfana una compra en silencio. Por eso ese arreglo es la
fase 3 y no la ultima.
- `estilo` hace dos trabajos: las secciones del catalogo y los generos
del taller. El pais que hay que clasificar es del genero, y mientras
sean lo mismo esa clasificacion no se puede ni escribir.
Y el ISRC estaba en la cancion cuando identifica una grabacion, asi que
pasa a la version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>