Compare commits
47 Commits
master
...
codex/redi
| Author | SHA1 | Date |
|---|---|---|
|
|
65da577c05 | 4 weeks ago |
|
|
a9524fe411 | 4 weeks ago |
|
|
6089763938 | 4 weeks ago |
|
|
e04a0f0e49 | 4 weeks ago |
|
|
c21685f79d | 4 weeks ago |
|
|
41141632f5 | 4 weeks ago |
|
|
3518da8b00 | 4 weeks ago |
|
|
b6b35c8ed7 | 4 weeks ago |
|
|
f1527efd25 | 4 weeks ago |
|
|
3a75eb768d | 4 weeks ago |
|
|
b390dcf6c0 | 4 weeks ago |
|
|
e7dd9b4599 | 4 weeks ago |
|
|
5e97336e56 | 4 weeks ago |
|
|
129c416263 | 4 weeks ago |
|
|
dba51dfa77 | 4 weeks ago |
|
|
a1154b3a0b | 4 weeks ago |
|
|
ac1ee034db | 4 weeks ago |
|
|
7b3a17e6eb | 4 weeks ago |
|
|
76ebe92c99 | 4 weeks ago |
|
|
b53f41d48c | 4 weeks ago |
|
|
26f58d1c3a | 4 weeks ago |
|
|
6ed006959f | 4 weeks ago |
|
|
f72f2eb4d4 | 4 weeks ago |
|
|
c15b4582e7 | 4 weeks ago |
|
|
bdec1e8b3c | 4 weeks ago |
|
|
f2e1d0080c | 4 weeks ago |
|
|
fe42b4b6ce | 4 weeks ago |
|
|
4ce062fbb5 | 4 weeks ago |
|
|
94dcf2eea5 | 4 weeks ago |
|
|
ac0119a16d | 4 weeks ago |
|
|
ac231c7600 | 4 weeks ago |
|
|
0a50def3c8 | 4 weeks ago |
|
|
0a611a4f59 | 4 weeks ago |
|
|
28420bc64f | 4 weeks ago |
|
|
da59607e58 | 4 weeks ago |
|
|
3789442b3d | 4 weeks ago |
|
|
38fc78aa9a | 4 weeks ago |
|
|
29e8789312 | 4 weeks ago |
|
|
7f1b0e8c75 | 4 weeks ago |
|
|
1f86097d97 | 4 weeks ago |
|
|
b84b81fc1d | 4 weeks ago |
|
|
988a874b72 | 4 weeks ago |
|
|
ae9f9bb5c5 | 4 weeks ago |
|
|
46fe708bc2 | 4 weeks ago |
|
|
3708c84e90 | 4 weeks ago |
|
|
3bcf92b638 | 4 weeks ago |
|
|
5889fad361 | 4 weeks ago |
@ -0,0 +1,73 @@
|
||||
# Despliegue de web y administración
|
||||
|
||||
El código se publica conjuntamente; las claves y los medios quedan fuera de cada release.
|
||||
|
||||
## Estructura
|
||||
|
||||
```text
|
||||
/var/www/senzapaura/
|
||||
.env # sitio: DATABASE_URL, ORIGIN=https://senzapaura.es, claves
|
||||
.env.panel # misma DATABASE_URL; ORIGIN=https://admin.senzapaura.es
|
||||
current -> releases/<version>
|
||||
releases/<version>/ # build, build-panel, node_modules, migraciones y scripts
|
||||
media/audio/ # originales y descargas MP3 privadas
|
||||
media/escucha/ # escucha completa privada
|
||||
media/ondas/ # ondas compartidas
|
||||
media/imagenes/ # imágenes cargadas desde el panel
|
||||
```
|
||||
|
||||
Las dos unidades fijan rutas absolutas de medios, `HOST=127.0.0.1` y puertos
|
||||
3000/3001. El panel reserva 260 MiB por petición para un archivo de hasta 256 MiB
|
||||
más el formulario. Nginx permite 260m en el dominio administrativo.
|
||||
|
||||
## Instalación y actualización
|
||||
|
||||
1. Preparar Node.js compatible con el proyecto, PostgreSQL, ffmpeg y ffprobe.
|
||||
Crear el usuario de servicio y los dos archivos de entorno con permisos 600.
|
||||
Ambos deben usar la misma base. El script comprueba la coincidencia sin mostrar claves.
|
||||
2. En desarrollo, ejecutar `bash despliegue/desplegar.sh usuario@servidor`.
|
||||
Comprueba tipos, formato, tests y ambos builds con código de salida correcto.
|
||||
Sube las dependencias a una release nueva y copia medios sin sobrescribir.
|
||||
3. Las migraciones se ejecutan en el destino con su `.env`. Las de esta entrega
|
||||
son `0024_intenciones_pago.sql` y `0025_guardas_publicacion.sql`.
|
||||
4. La primera vez, ejecutar en el servidor
|
||||
`sudo bash /var/www/senzapaura/current/despliegue/instalar.sh`.
|
||||
Instala ambas unidades y el ejemplo Nginx. Configurar DNS y TLS de los dos dominios.
|
||||
5. Las actualizaciones posteriores cambian `current` y reinician ambos servicios.
|
||||
Si falla el arranque o la comprobación HTTP, se restaura el enlace anterior.
|
||||
Las releases antiguas se conservan. La base no se revierte automáticamente.
|
||||
|
||||
## Migrar una instalación anterior
|
||||
|
||||
Conservar la instalación y su copia de seguridad. Preparar los entornos y dejar que
|
||||
el primer despliegue cree `current`; después instalar las nuevas unidades. Los
|
||||
antiguos `build` y `node_modules` de la raíz no se borran. Si se usaba otro sistema
|
||||
de arranque, adaptar sus directorios de trabajo a `current` y sus rutas de medios
|
||||
a las compartidas antes de reiniciarlo.
|
||||
|
||||
La subida nunca reemplaza un audio existente con la misma clave. Para cambiar
|
||||
material ya publicado, preparar una operación editorial explícita. Las imágenes
|
||||
nuevas tienen nombres únicos y se pueden asignar desde el panel.
|
||||
|
||||
Las nuevas guardas no rellenan créditos ausentes. En el catálogo legado deben
|
||||
asignarse los intérpretes reales; a partir de la migración, las escrituras que
|
||||
rompen los requisitos de publicación se rechazan al confirmar la transacción.
|
||||
Las importaciones deben agrupar canción, créditos y audio en una transacción.
|
||||
|
||||
## Proxy y recuperación
|
||||
|
||||
`/escucha` y `/descargas` siempre pasan a Node para autorizar la petición y resolver
|
||||
Range. No añadir aliases que publiquen `media/audio` o `media/escucha`.
|
||||
El webhook `/api/stripe/webhook` atraviesa el modo de espera y comprueba la firma.
|
||||
|
||||
La activación usa `flock` para impedir despliegues concurrentes. El cambio de enlace
|
||||
es atómico, pero el reinicio puede producir una breve interrupción del servicio.
|
||||
Antes de cambios destructivos de esquema debe diseñarse una migración compatible
|
||||
con la versión anterior o un procedimiento específico de recuperación.
|
||||
|
||||
`npm run db:recuperar-audios` informa de manifiestos de procesado. Con
|
||||
`-- --limpiar` elimina solo restos de más de 24 horas, comprobando primero sus
|
||||
referencias en PostgreSQL. Un final registrado nunca se elimina por esta vía.
|
||||
|
||||
Las pruebas locales verifican código y migraciones; no equivalen a un ensayo real
|
||||
de instalación, TLS y restauración del servidor de producción.
|
||||
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Se ejecuta en el destino. Conserva releases anteriores y revierte código si falla.
|
||||
set -euo pipefail
|
||||
RUTA="${1:?}"; VERSION="${2:?}"
|
||||
[[ "$RUTA" =~ ^/[A-Za-z0-9_/-]+$ && "$RUTA" != / && "$RUTA" != *..* ]]
|
||||
[[ "$VERSION" =~ ^[A-Za-z0-9_-]+$ ]]
|
||||
exec 9>"$RUTA/.despliegue.lock"
|
||||
flock -n 9 || { echo 'Ya hay un despliegue en curso' >&2; exit 1; }
|
||||
NUEVA="$RUTA/releases/$VERSION"
|
||||
[[ -f "$NUEVA/build/index.js" && -f "$NUEVA/build-panel/index.js" && -f "$RUTA/.env" && -f "$RUTA/.env.panel" ]]
|
||||
command -v ffmpeg >/dev/null
|
||||
command -v ffprobe >/dev/null
|
||||
ANTERIOR="$(readlink "$RUTA/current" || true)"
|
||||
cd "$NUEVA"
|
||||
node despliegue/comprobar-entornos.mjs "$RUTA"
|
||||
node --env-file="$RUTA/.env" scripts/db/migrar.mjs
|
||||
if [[ ! -e "$RUTA/current" ]]; then
|
||||
ln -s "$NUEVA" "$RUTA/current"
|
||||
echo 'Primera versión preparada. Instala las unidades con sudo bash current/despliegue/instalar.sh.'
|
||||
exit 0
|
||||
fi
|
||||
[[ -L "$RUTA/current" ]] || { echo 'current debe ser un enlace simbólico' >&2; exit 1; }
|
||||
SERVICIOS=(senzapaura senzapaura-panel)
|
||||
if systemctl is-enabled --quiet senzapaura-estudio 2>/dev/null; then SERVICIOS+=(senzapaura-estudio); fi
|
||||
ACTIVADA=0
|
||||
revertir() {
|
||||
if [[ "$ACTIVADA" == 1 && -n "$ANTERIOR" ]]; then
|
||||
ln -s "$ANTERIOR" "$RUTA/.rollback-$VERSION"
|
||||
mv -Tf "$RUTA/.rollback-$VERSION" "$RUTA/current"
|
||||
sudo systemctl restart "${SERVICIOS[@]}"
|
||||
echo 'Se ha restaurado el código anterior. Las migraciones no se deshacen automáticamente.' >&2
|
||||
fi
|
||||
}
|
||||
trap revertir ERR
|
||||
ln -s "$NUEVA" "$RUTA/.current-$VERSION"
|
||||
mv -Tf "$RUTA/.current-$VERSION" "$RUTA/current"
|
||||
ACTIVADA=1
|
||||
sudo systemctl restart "${SERVICIOS[@]}"
|
||||
COMPROBADO=0
|
||||
for INTENTO in {1..15}; do
|
||||
if curl -fsS -o /dev/null http://127.0.0.1:3000/ && curl -fsS -o /dev/null http://127.0.0.1:3001/entrar; then COMPROBADO=1; break; fi
|
||||
sleep 1
|
||||
done
|
||||
[[ "$COMPROBADO" == 1 ]]
|
||||
systemctl is-active --quiet "${SERVICIOS[@]}"
|
||||
if [[ ${#SERVICIOS[@]} == 3 ]]; then curl -fsS -o /dev/null http://127.0.0.1:3002/entrar; fi
|
||||
ACTIVADA=0
|
||||
trap - ERR
|
||||
echo "Publicadas web y administración: $VERSION"
|
||||
@ -0,0 +1,12 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { parseEnv } from 'node:util';
|
||||
import { join } from 'node:path';
|
||||
const raiz = process.argv[2];
|
||||
if (!raiz) throw new Error('Indica la raíz de la instalación.');
|
||||
const sitio = parseEnv(await readFile(join(raiz, '.env'), 'utf8'));
|
||||
const panel = parseEnv(await readFile(join(raiz, '.env.panel'), 'utf8'));
|
||||
if (!sitio.DATABASE_URL || sitio.DATABASE_URL !== panel.DATABASE_URL)
|
||||
throw new Error('La web y el panel deben apuntar a la misma base PostgreSQL.');
|
||||
if (!sitio.ORIGIN || !panel.ORIGIN || sitio.ORIGIN === panel.ORIGIN)
|
||||
throw new Error('Configura los dos ORIGIN, uno por dominio.');
|
||||
console.log('Entornos de sitio y panel coherentes.');
|
||||
@ -1,87 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Despliegue por SSH, sin repositorio remoto.
|
||||
#
|
||||
# ./despliegue/desplegar.sh senzapaura@192.168.18.171
|
||||
#
|
||||
# Construye aquí y sube el resultado. Se hace así y no compilando en el servidor
|
||||
# porque el servidor no necesita las herramientas de desarrollo —ni Vite, ni
|
||||
# Playwright, ni el compilador— para servir la web: solo Node y tres paquetes.
|
||||
#
|
||||
# Dos decisiones que salen de haberlo hecho una vez a mano, no de suponer:
|
||||
#
|
||||
# - **Se sube con `tar` por SSH, no con `rsync`.** La máquina de desarrollo es
|
||||
# Windows y Git Bash no trae rsync. Tar sí, y está en los dos lados.
|
||||
# - **Las dependencias se resuelven aquí y se suben ya instaladas.** El servidor
|
||||
# tiene salida a internet pero su DNS no resuelve `registry.npmjs.org`, así
|
||||
# que `npm ci` allí falla. Da igual: los tres paquetes de producción son
|
||||
# JavaScript puro, sin binarios de plataforma, y viajan tal cual.
|
||||
#
|
||||
# Lo que NO se sube: `.env`. Las claves se ponen a mano en el servidor una vez
|
||||
# y se quedan ahí. Subirlas en cada despliegue es cómo acaban en un historial.
|
||||
|
||||
# Construye y prepara ambas aplicaciones en una versión inmutable.
|
||||
# Uso: bash despliegue/desplegar.sh usuario@host [/var/www/senzapaura]
|
||||
set -euo pipefail
|
||||
|
||||
DESTINO="${1:-}"
|
||||
RUTA="${2:-/var/www/senzapaura}"
|
||||
CLAVE="${SSH_CLAVE:-$HOME/.ssh/senzapaura_deploy}"
|
||||
|
||||
if [[ -z "$DESTINO" ]]; then
|
||||
echo "uso: $0 usuario@servidor [ruta]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ "$DESTINO" =~ ^[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+$ ]] || { echo 'Indica usuario@servidor' >&2; exit 1; }
|
||||
[[ "$RUTA" =~ ^/[A-Za-z0-9_/-]+$ && "$RUTA" != / && "$RUTA" != *..* ]] || { echo 'Ruta de despliegue no válida' >&2; exit 1; }
|
||||
SSH=(ssh -i "$CLAVE" -o BatchMode=yes)
|
||||
VERSION="$(date -u +%Y%m%dT%H%M%SZ)-$(git rev-parse --short HEAD)"
|
||||
TEMPORAL="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEMPORAL"' EXIT
|
||||
|
||||
echo "==> Construyendo"
|
||||
node scripts/construir.mjs
|
||||
|
||||
if [[ ! -f build/index.js ]]; then
|
||||
echo "el build no ha dejado build/index.js; se aborta" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Resolviendo dependencias de producción"
|
||||
trap 'rm -rf -- "$TEMPORAL"' EXIT
|
||||
npm run check
|
||||
npm run lint
|
||||
npm run test:unit -- --run
|
||||
# En producción no se acepta un empaquetador que termine con error.
|
||||
npm run build
|
||||
npm run build:panel
|
||||
npm run build:estudio
|
||||
[[ -f build/index.js && -f build-panel/index.js && -f build-estudio/index.js ]]
|
||||
cp package.json package-lock.json "$TEMPORAL/"
|
||||
(cd "$TEMPORAL" && npm ci --omit=dev --silent)
|
||||
|
||||
# Un binario compilado para Windows no arrancaría en el servidor. Hoy no hay
|
||||
# ninguno, pero si algún día se añade una dependencia que lo traiga, es mejor
|
||||
# enterarse aquí que con el servicio caído.
|
||||
if find "$TEMPORAL/node_modules" -name '*.node' | grep -q .; then
|
||||
echo "hay binarios de plataforma en node_modules: instálalos en el servidor" >&2
|
||||
find "$TEMPORAL/node_modules" -name '*.node' | head -5 >&2
|
||||
exit 1
|
||||
echo 'Hay dependencias nativas: requieren una construcción Linux.' >&2; exit 1
|
||||
fi
|
||||
"${SSH[@]}" "$DESTINO" "mkdir -p '$RUTA/releases' '$RUTA/media/audio' '$RUTA/media/escucha' '$RUTA/media/ondas' '$RUTA/media/imagenes'; mkdir '$RUTA/releases/$VERSION'"
|
||||
tar -czf - build build-panel build-estudio package.json package-lock.json drizzle scripts/db/migrar.mjs scripts/estudio despliegue |
|
||||
"${SSH[@]}" "$DESTINO" "tar -xzf - -C '$RUTA/releases/$VERSION'"
|
||||
tar -czf - -C "$TEMPORAL" node_modules | "${SSH[@]}" "$DESTINO" "tar -xzf - -C '$RUTA/releases/$VERSION'"
|
||||
# Los medios se comparten y nunca se sobrescriben al publicar código.
|
||||
# Una sustitución editorial se carga desde el panel con una clave nueva.
|
||||
for CARPETA in audio escucha imagenes; do
|
||||
if [[ -d "media/$CARPETA" ]]; then
|
||||
tar -czf - -C "media/$CARPETA" . | "${SSH[@]}" "$DESTINO" "tar --skip-old-files -xzf - -C '$RUTA/media/$CARPETA'"
|
||||
fi
|
||||
done
|
||||
if [[ -d static/ondas ]]; then
|
||||
tar -czf - -C static/ondas . | "${SSH[@]}" "$DESTINO" "tar --skip-old-files -xzf - -C '$RUTA/media/ondas'"
|
||||
fi
|
||||
|
||||
echo "==> Subiendo la aplicación"
|
||||
# `build` se borra antes: los nombres llevan hash y si no se acumularían sin fin.
|
||||
tar -czf - build package.json package-lock.json drizzle.config.ts drizzle |
|
||||
"${SSH[@]}" "$DESTINO" "rm -rf '$RUTA/build' '$RUTA/drizzle' && tar -xzf - -C '$RUTA'"
|
||||
|
||||
echo "==> Subiendo dependencias"
|
||||
tar -czf - -C "$TEMPORAL" node_modules |
|
||||
"${SSH[@]}" "$DESTINO" "rm -rf '$RUTA/node_modules' && tar -xzf - -C '$RUTA'"
|
||||
|
||||
echo "==> Subiendo el audio privado"
|
||||
# Sin borrar lo que hubiera: aquí puede haber material que no esté en esta
|
||||
# máquina, y perderlo sería irreversible.
|
||||
tar -czf - media/audio | "${SSH[@]}" "$DESTINO" "tar -xzf - -C '$RUTA'"
|
||||
|
||||
echo "==> Migrando la base de datos"
|
||||
# Desde aquí y no en el servidor: `drizzle-kit` es una herramienta de desarrollo
|
||||
# y allí no se instala. Como la base es la misma, da igual desde dónde se lance.
|
||||
npm run db:migrate
|
||||
|
||||
echo "==> Reiniciando"
|
||||
# Requiere que el usuario pueda reiniciar este servicio sin contraseña. En
|
||||
# /etc/sudoers.d/senzapaura:
|
||||
# senzapaura ALL=(root) NOPASSWD: /bin/systemctl restart senzapaura
|
||||
"${SSH[@]}" "$DESTINO" "sudo systemctl restart senzapaura && sleep 3 && systemctl is-active senzapaura"
|
||||
|
||||
echo "==> Comprobando que responde"
|
||||
"${SSH[@]}" "$DESTINO" "curl -sf -o /dev/null -w 'portada -> %{http_code}\n' http://127.0.0.1:3000/"
|
||||
|
||||
echo "Listo."
|
||||
# La base objetivo se toma exclusivamente del entorno del servidor.
|
||||
"${SSH[@]}" "$DESTINO" "bash '$RUTA/releases/$VERSION/despliegue/activar.sh' '$RUTA' '$VERSION'"
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
# Puesta en marcha de Estudio
|
||||
|
||||
Estudio es la aplicación nueva de administración, con artefacto `build-estudio` y servicio `senzapaura-estudio` en `127.0.0.1:3002`. Su instalación es independiente del panel anterior. Estos pasos son un procedimiento para el servidor; no se han ejecutado en producción.
|
||||
|
||||
## Preparación
|
||||
|
||||
1. Conservar una copia PostgreSQL completa y una copia de los medios antes de importar. La exportación JSON editorial no sustituye esas copias.
|
||||
2. Preparar la release mediante `desplegar.sh`, que construye sitio, panel y Estudio. El instalador general instala sitio y panel; Estudio se instala expresamente con los pasos siguientes.
|
||||
3. Crear `/var/www/senzapaura/.env.estudio`, propiedad del usuario de servicio y con permisos 600. Debe contener la misma `DATABASE_URL` de producción que usa el sitio, `ORIGIN` con el dominio HTTPS elegido para Estudio y la configuración de correo necesaria para el acceso normal. No copiar `.env.estudio.local` ni las credenciales demo. El código fijo de desarrollo no está habilitado en el build de producción.
|
||||
4. Crear `/var/www/senzapaura/media/estudio/imagenes` con permisos para el usuario `senzapaura`. El servicio fija rutas absolutas para imágenes, audio y ondas. Se necesitan `ffmpeg` y `ffprobe`.
|
||||
|
||||
Desde la release, ejecutar como el usuario de servicio:
|
||||
|
||||
```sh
|
||||
node --env-file=/var/www/senzapaura/.env.estudio scripts/estudio/preparar.mjs
|
||||
node --env-file=/var/www/senzapaura/.env.estudio scripts/estudio/administrar.mjs ver
|
||||
```
|
||||
|
||||
La preparación crea las tablas editoriales, importa una instantánea, aplica la unificación de Personas y sitúa la inspiración en la ficha de Canción sin modificar el catálogo público. Si ya se importó, conserva los borradores y ejecuta esas migraciones de forma idempotente. Para un almacén importado por una versión anterior también pueden ejecutarse explícitamente `scripts/estudio/actualizar-personas.mjs` y `scripts/estudio/actualizar-inspiracion.mjs` con el mismo archivo de entorno.
|
||||
|
||||
## Servicio y revisión
|
||||
|
||||
```sh
|
||||
sudo install -m 644 despliegue/senzapaura-estudio.service /etc/systemd/system/senzapaura-estudio.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now senzapaura-estudio
|
||||
curl -fsS -o /dev/null http://127.0.0.1:3002/entrar
|
||||
```
|
||||
|
||||
Configurar el proxy para el dominio elegido hacia `127.0.0.1:3002`, conservando cabecera Host y protocolo HTTPS. El límite de carga es 260 MiB. Los archivos privados deben pasar por los endpoints de la aplicación; no publicar el directorio de medios mediante un alias genérico.
|
||||
|
||||
Revisar los recorridos con una cuenta administradora real y códigos enviados por correo. Preparar no equivale a activar: mientras esté en preparación, publicar una ficha editorial no cambia el sitio público.
|
||||
|
||||
## Cambio de escritor
|
||||
|
||||
Detener la edición durante el cambio. Si el catálogo legado cambió desde la importación, conciliarlo antes de continuar; la activación rechaza una fuente distinta. Revisar también los intérpretes reales que falten en el catálogo, sin inventarlos.
|
||||
|
||||
Guardar la exportación fuera del directorio de release, con nombre nuevo y permisos restringidos:
|
||||
|
||||
```sh
|
||||
node --env-file=/var/www/senzapaura/.env.estudio scripts/estudio/administrar.mjs exportar /ruta/privada/estudio-previo.json
|
||||
node --env-file=/var/www/senzapaura/.env.estudio scripts/estudio/administrar.mjs activar /ruta/privada/estudio-previo.json
|
||||
```
|
||||
|
||||
La activación comprueba el esquema del respaldo, la revisión editorial y la huella del catálogo. Instala guardas para que Estudio sea el escritor editorial del catálogo. No publica todos los borradores automáticamente. Cambiar entonces el acceso administrativo al dominio de Estudio y retirar el acceso editorial al panel antiguo.
|
||||
|
||||
Las actualizaciones de código reinician Estudio y comprueban su endpoint de acceso cuando el servicio está habilitado. No activan el almacén editorial automáticamente.
|
||||
|
||||
## Suspensión y recuperación
|
||||
|
||||
`administrar.mjs desactivar` suspende las publicaciones desde Estudio y elimina el bloqueo efectivo del escritor legado al cambiar el modo. No revierte contenido publicado ni restaura una copia. La vuelta a una versión de código anterior tampoco revierte la base: si se necesita recuperar datos, debe seguirse el procedimiento de restauración de PostgreSQL y medios, comprobando compras y referencias.
|
||||
@ -0,0 +1,37 @@
|
||||
[Unit]
|
||||
Description=Senza Paura — Estudio
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=senzapaura
|
||||
Group=senzapaura
|
||||
WorkingDirectory=/var/www/senzapaura/current
|
||||
EnvironmentFile=/var/www/senzapaura/.env.estudio
|
||||
Environment=HOST=127.0.0.1
|
||||
Environment=PORT=3002
|
||||
Environment=BODY_SIZE_LIMIT=260M
|
||||
Environment=MEDIA_DIR=/var/www/senzapaura/media/audio
|
||||
Environment=ESCUCHA_DIR=/var/www/senzapaura/media/escucha
|
||||
Environment=ONDA_DIR=/var/www/senzapaura/media/ondas
|
||||
Environment=ESTUDIO_IMAGEN_DIR=/var/www/senzapaura/media/estudio/imagenes
|
||||
Environment=IMAGEN_DIR=/var/www/senzapaura/media/imagenes
|
||||
ExecStart=/usr/bin/node build-estudio/index.js
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
ReadWritePaths=/var/www/senzapaura/media
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -0,0 +1,36 @@
|
||||
[Unit]
|
||||
Description=Senza Paura — administración
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=senzapaura
|
||||
Group=senzapaura
|
||||
WorkingDirectory=/var/www/senzapaura/current
|
||||
EnvironmentFile=/var/www/senzapaura/.env.panel
|
||||
Environment=HOST=127.0.0.1
|
||||
Environment=PORT=3001
|
||||
Environment=BODY_SIZE_LIMIT=260M
|
||||
Environment=MEDIA_DIR=/var/www/senzapaura/media/audio
|
||||
Environment=ESCUCHA_DIR=/var/www/senzapaura/media/escucha
|
||||
Environment=ONDA_DIR=/var/www/senzapaura/media/ondas
|
||||
Environment=IMAGEN_DIR=/var/www/senzapaura/media/imagenes
|
||||
ExecStart=/usr/bin/node build-panel/index.js
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
ReadWritePaths=/var/www/senzapaura/media
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -0,0 +1,329 @@
|
||||
# Análisis de la aplicación Senza Paura
|
||||
|
||||
Revisión técnica y de experiencia de uso del 11 de septiembre de 2026
|
||||
|
||||
La prioridad es corregir la autorización del panel antes de ampliar funciones o dar por cerrado el dominio musical. La aplicación compila y supera sus pruebas actuales, pero una petición anónima alcanza acciones administrativas sin comprobar el rol. Además, hay inconsistencias en los créditos, las descargas y el despliegue que esas pruebas no cubren.
|
||||
|
||||
Esta revisión identifica **16 hallazgos**, cuatro líneas de refactorización y seis propuestas de ampliación. Cada hallazgo distingue el defecto observado de los escenarios que aún requieren una prueba de integración. Las recomendaciones se refieren al código revisado; la configuración efectiva del servidor de producción no se ha inspeccionado.
|
||||
|
||||
| Prioridad | Significado | Hallazgos |
|
||||
| --------- | ------------------------------------------------------------- | ------------------------------- |
|
||||
| P0 | Corregir inmediatamente | H01 autorización administrativa |
|
||||
| P1 | Resolver antes de ampliar ventas o publicar cambios afectados | H02 a H07 y H10 |
|
||||
| P2 | Corregir en la siguiente entrega técnica | H08, H09, H11, H12, H13 y H15 |
|
||||
| P3 | Resolver con mantenimiento editorial | H14 y H16 |
|
||||
|
||||
## Decisiones recomendadas
|
||||
|
||||
- Proteger todas las peticiones del grupo administrativo desde el hook del panel y verificar también los puntos de escritura.
|
||||
- Completar los intérpretes y endurecer el auditor para que falle ante publicaciones incompletas.
|
||||
- Separar la biblioteca comprada del catálogo público y garantizar el formato de descarga prometido.
|
||||
- Actualizar el despliegue para las dos aplicaciones y los tres almacenes compartidos.
|
||||
- Abordar después las consultas por ruta, los editores grandes y las nuevas funciones.
|
||||
|
||||
La base tiene elementos sólidos que conviene conservar: transacciones en pedidos y OTP, consumo único del voto, cookies de sesión limitadas al host, streaming autorizado con rangos y generación de audio sin sobrescribir archivos existentes.
|
||||
|
||||
**Referencia de la revisión:** rama `codex/redisenio-dominio-musical`, commit `c15b4582e765e940fb8be83503a384b27c0f82bc`. El árbol de trabajo estaba limpio al comenzar. No se han corregido archivos de la aplicación ni aplicado migraciones durante este análisis.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## Alcance y resultados de las comprobaciones
|
||||
|
||||
Se revisaron las rutas públicas y administrativas, autenticación, compra, entrega de audio, consultas, esquema, importación, despliegue y pruebas. Se ejecutaron los builds y se recorrieron vistas locales de la aplicación con Chromium. Las consultas adicionales a PostgreSQL fueron de solo lectura y devolvieron agregados.
|
||||
|
||||
| Comprobación | Resultado observado |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run check` | Sitio y panel con 0 errores y 0 avisos de svelte-check. El proceso del panel imprime una advertencia adicional sobre tsconfig. |
|
||||
| `npm run test:unit -- --run` | 34 archivos y 387 pruebas pasan. Se imprime un TypeError de inicialización; se reproduce en una segunda ejecución sin check simultáneo. |
|
||||
| `npm run lint` | Falla por formato en 11 archivos. ESLint no llega a ejecutarse dentro de este comando. |
|
||||
| `npx eslint .` | Pasa por separado, código de salida 0. |
|
||||
| Builds de sitio y panel | Pasan con `node scripts/construir.mjs` y su variante `--panel`; ambos procesos salen con 0. |
|
||||
| E2E de navegación y responsive | 52 pruebas pasan con dos workers. |
|
||||
| Muestreo de páginas | Inicio, música, Dorina, carrito y acceso a 375 y 1440 px: diez respuestas 200, sin desbordamiento horizontal ni errores JavaScript observados. |
|
||||
| Petición anónima al panel | GET redirige al acceso, pero POST inválido ejecuta la validación de la acción H01. |
|
||||
| Auditor musical y consultas agregadas | 11 principales publicadas, 0 versiones, 6 artistas, 11 másteres MP3 y 11 streams MP3. Cero créditos de interpretación y cero representaciones explícitas de descarga. |
|
||||
|
||||
**Límites:** no se ejecutó la suite E2E completa, un cobro real, un envío de correo ni una edición administrativa válida. Tampoco se probó recuperación de copias, carga concurrente o el proxy real. Los escenarios de cobro duplicado, pérdida de créditos y compra retirada se sustentan en sus rutas de código; no se provocaron en los datos existentes.
|
||||
|
||||
## Cambios respecto al seguimiento anterior
|
||||
|
||||
El plan `docs/plan-implementacion-dominio-musical.md` sigue siendo útil, pero algunos estados de “hecho” necesitan matices. La generación de stream y onda funciona y tiene prueba real con FFmpeg; la descarga MP3 desde cualquier máster sigue incompleta. Las guardas del catálogo existen, pero no protegen por sí solas las acciones del panel. Los intérpretes son entidades administrables, aunque todavía no están asociados a las canciones publicadas. PR 9 y la división de UI de PR 10 continúan pendientes.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H01 Acciones administrativas sin autorización efectiva
|
||||
|
||||
**P0 · Confirmado mediante petición local inocua.**
|
||||
|
||||
El único uso productivo de `exigirAdmin` está en el `load` del layout protegido. El hook del panel valida la sesión si existe y luego llama a `resolve`, incluso cuando `locals.usuario` es nulo. Las acciones de artistas, canciones, géneros y modelos no comprueban el rol antes de ejecutar su lógica.
|
||||
|
||||
Una acción JSON de SvelteKit se resuelve antes del `load` del layout. El runtime instalado lo muestra en `node_modules/@sveltejs/kit/src/runtime/server/page/index.js:57`. El comentario que afirma que el layout siempre protege a sus páginas induce a confiar en una barrera que no cubre las escrituras.
|
||||
|
||||
**Prueba realizada:** contra el panel local en el puerto 4174, sin cookies:
|
||||
|
||||
- `GET /artistas` devuelve 303 hacia `/entrar`.
|
||||
- `POST /artistas?/crear`, con origen local, `Accept: application/json` y los campos vacíos `nombre=&slug=`, devuelve HTTP 200 con resultado de acción `failure`, estado lógico 400 y el mensaje “Escribe un nombre y un slug válidos.”
|
||||
|
||||
Esto demuestra que la acción se ejecutó antes de exigir autenticación. La petición vacía termina antes de llamar a `crearArtista`, por lo que no se crearon ni alteraron datos. Con campos válidos, el código continúa directamente hacia la escritura; ese paso no se ejecutó durante la revisión.
|
||||
|
||||
**Impacto:** una persona capaz de alcanzar el servidor del panel puede invocar operaciones de contenido sin ser administradora. La separación en otro dominio y la protección CSRF no sustituyen la autorización de una petición directa.
|
||||
|
||||
**Corrección:** aplicar `exigirAdmin` en el hook antes de resolver rutas del grupo `(dentro)`. Mantener las rutas de entrada y salida necesarias fuera de esa guarda. Añadir una comprobación compartida en las acciones o servicios administrativos para que las nuevas rutas no omitan el permiso.
|
||||
|
||||
**Aceptación:** probar GET, datos de navegación, POST normal y POST JSON con usuario anónimo, usuario ordinario, administrador y rol revocado. Verificar tanto la respuesta como la ausencia de escrituras para los tres casos no autorizados.
|
||||
|
||||
**Código:** `src/panel/hooks.server.ts:24-37`; `src/panel/rutas/(dentro)/+layout.server.ts:5-15`; `src/panel/rutas/(dentro)/artistas/+page.server.ts:9-18`; `src/lib/server/administracion.ts:1-8`.
|
||||
|
||||
**Documentación de contraste:** [SvelteKit sobre carga y autenticación](https://svelte.dev/docs/kit/load#Implications-for-authentication). La documentación recomienda comprobar permisos en hooks o en cada carga protegida; no confiar en la ejecución universal de un layout.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H02 El ejemplo de Nginx intercepta la escucha autorizada
|
||||
|
||||
**P1 · Confirmado en configuración versionada; pendiente contrastar el proxy instalado.**
|
||||
|
||||
`location /escucha/` busca archivos en `build/client` y devuelve 404 si no existen. La aplicación actual usa esa misma ruta para resolver la canción, comprobar su acceso y entregar el archivo privado desde `ESCUCHA_DIR`.
|
||||
|
||||
Con este archivo de Nginx, `/escucha/prometiste` no llega a Node y no encuentra el archivo que espera la ruta dinámica. Si permanecen copias antiguas bajo ese prefijo, se siguen ofreciendo como estáticos sin la nueva política de acceso.
|
||||
|
||||
**Corrección:** dirigir `/escucha/` al servidor autorizado, o usar una entrega interna del proxy después de autorización. Inventariar los estáticos heredados antes de retirar las copias sobrantes.
|
||||
|
||||
**Aceptación:** a través del proxy, comprobar 200 y 206 para escucha permitida, 401/403 para acceso restringido y ausencia de copias públicas residuales. No basta con probar `vite preview`.
|
||||
|
||||
**Código:** `despliegue/senzapaura.nginx.conf:82-92`; `src/routes/escucha/[cancion]/[[version]]/+server.ts`; `src/lib/server/medios.ts:42-49`.
|
||||
|
||||
## H03 El script de despliegue no entrega la arquitectura actual
|
||||
|
||||
**P1 · Confirmado en el script; riesgo al utilizarlo como procedimiento completo.**
|
||||
|
||||
El script construye y sube `build`, pero no `build-panel`. Transfiere `media/audio`, pero no `media/escucha`. En una instalación limpia puede dejar los másteres presentes y la escucha sin sus archivos. Tampoco reinicia el panel.
|
||||
|
||||
Además, borra el build y las dependencias remotas antes de completar la transferencia. Una interrupción deja una instalación parcial. La migración se ejecuta con el `.env` local, sin demostrar que su base sea la del destino SSH.
|
||||
|
||||
**Corrección:** desplegar ambas aplicaciones mediante una versión preparada en un directorio nuevo; mantener los almacenes en rutas persistentes compartidas y cambiar la versión activa al terminar las comprobaciones. Validar explícitamente la identidad del destino de la migración. Definir cómo se provisionan streams y ondas sin sobrescribir material subido desde el panel.
|
||||
|
||||
**Aceptación:** despliegue sobre entorno vacío con una canción, escucha y descarga funcionales; corte de transferencia con la versión anterior aún disponible; comprobación de ambos procesos y ensayo de reversión.
|
||||
|
||||
**Código:** `despliegue/desplegar.sh:35-76`; `despliegue/con-proxy-manager.md:148-190`; `vite.config.ts:119-130`.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H04 El auditor declara íntegro un catálogo sin intérpretes
|
||||
|
||||
**P1 · Confirmado en PostgreSQL y en la condición de salida del auditor.**
|
||||
|
||||
La lectura actual devuelve 11 canciones publicadas y 11 publicadas sin intérprete. Hay seis artistas, uno publicado, pero ninguna fila en `cancion_interprete`. El auditor devuelve salida 0 y anuncia “créditos y streams completos”.
|
||||
|
||||
El auditor detecta créditos antiguos sin migrar, pero no canciones sin ningún crédito antiguo ni nuevo. Por tanto, ambos contadores a cero pasan. Esto contradice la validación del panel, que exige un intérprete para publicar, y deja vacías las conexiones entre canciones y discografías.
|
||||
|
||||
**Corrección:** añadir al auditor publicaciones sin intérprete, versiones sin créditos y representaciones de entrega que falten. Completar las asociaciones con identidades confirmadas; no asignar automáticamente un nombre supuesto. Revisar también las publicaciones previas a la nueva validación.
|
||||
|
||||
**Aceptación:** una publicación sin intérprete debe producir un hallazgo y salida distinta de cero. El catálogo corregido debe tener cero publicaciones incompletas. Las fichas de artistas deben enlazar las canciones asociadas.
|
||||
|
||||
**Código:** `scripts/db/auditar-dominio-musical.mjs:20-50`; `src/lib/server/panel-canciones.ts:384-389`. La cifra procede de consultas agregadas de esta revisión, no del documento anterior.
|
||||
|
||||
## H05 Guardar una canción elimina detalles del crédito principal
|
||||
|
||||
**P1 · Confirmado por lectura; requiere créditos detallados para manifestarse.**
|
||||
|
||||
`guardarCancion` borra todos los créditos de rol `principal` y los inserta de nuevo usando únicamente artista, rol y orden. El esquema permite `instrumento` y `acreditadoComo`; esos valores desaparecen aunque el usuario solo haya cambiado el título o la letra. Los otros roles no se borran en ese bloque.
|
||||
|
||||
Actualmente no hay créditos en la base revisada, por lo que no se ha observado pérdida histórica. El defecto aparecerá al completar los datos detallados que prevé el plan.
|
||||
|
||||
**Corrección:** reconciliar altas y bajas conservando los atributos de las relaciones existentes. Editar los campos detallados de forma explícita y preservar la acreditación histórica al cambiar el nombre del artista.
|
||||
|
||||
**Aceptación:** crear en una base aislada un crédito con instrumento, acreditación y orden; cambiar solo el título de la canción y verificar que esos tres valores se conservan.
|
||||
|
||||
**Código:** `src/lib/server/panel-canciones.ts:699-711`; `src/lib/server/db/schema/catalogo.ts:222-248`.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H06 Las compras retiradas desaparecen de la biblioteca
|
||||
|
||||
**P1 · Confirmado por el recorrido de datos; sin retirar canciones reales.**
|
||||
|
||||
La cuenta transforma `data.compras` en canciones mediante el catálogo público y descarta los resultados ausentes. Si se retira una canción, su álbum o su estilo, esa canción deja de aparecer en “Mis canciones”. Las versiones retiradas también quedan fuera de los enlaces generados desde ese catálogo.
|
||||
|
||||
La autorización de descarga sí permite resolver material comprado que ya no está publicado. El derecho puede seguir existiendo, pero la interfaz deja de ofrecer el enlace. Esto contradice el mensaje “Las descargas no caducan” y puede mostrar una biblioteca vacía a alguien con compras.
|
||||
|
||||
**Corrección:** devolver desde la cuenta un modelo específico de biblioteca comprada con títulos históricos, estado de disponibilidad y enlaces autorizados. Separar la visibilidad pública de la disponibilidad para compradores.
|
||||
|
||||
**Aceptación:** después de retirar la canción, su disco o su estilo en una base aislada, el comprador conserva sus enlaces permitidos y un visitante público no recupera la ficha retirada.
|
||||
|
||||
**Código:** `src/routes/(sitio)/cuenta/+page.svelte:19-22,75-114`; `src/lib/server/catalogo.ts:193-195`; `src/lib/server/audios.ts:84-125`.
|
||||
|
||||
## H07 La subida de un máster no garantiza la descarga MP3 prometida
|
||||
|
||||
**P1 · Defecto confirmado en el flujo; condicionado a subir un formato distinto de MP3.**
|
||||
|
||||
Stripe describe la compra como descarga MP3. El alta acepta WAV, FLAC, M4A, AAC y OGG, pero genera únicamente máster y stream. La descarga prioriza una representación `descarga` y, si no existe, entrega el máster original con su extensión.
|
||||
|
||||
Un WAV subido desde el panel termina ofreciéndose como WAV al comprador, aunque el producto indique MP3. En la base actual los once másteres ya son MP3 y no hay representaciones explícitas de descarga: los datos actuales ocultan la discrepancia del flujo nuevo.
|
||||
|
||||
**Corrección:** generar y registrar la representación MP3 comercial desde el máster, con un perfil de calidad explícito. Conservar el original como archivo y evitar que la ausencia de un derivado cambie silenciosamente el formato vendido. Si se desean varios formatos, mostrarlos como opciones de producto.
|
||||
|
||||
**Aceptación:** probar WAV, FLAC y MP3 de entrada; verificar MIME, extensión y metadatos técnicos del archivo descargado. No presentar una conversión desde una fuente comprimida como recuperación de calidad perdida.
|
||||
|
||||
**Código:** `src/lib/server/ingreso-audio.ts:21-28,243-245`; `src/lib/server/panel-canciones.ts:429-478`; `src/lib/server/audios.ts:119-125`; `src/lib/server/stripe.ts:72`.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H08 Las versiones no reciben las mismas guardas de publicación
|
||||
|
||||
**P2 · Confirmado en código; actualmente hay cero versiones en la base.**
|
||||
|
||||
La acción valida el stream de la canción principal mediante `actual.tieneAudio`. En el bucle de versiones se comprueban valores básicos y se escribe el estado solicitado, sin verificar su propio stream ni sus créditos. Una versión recién creada como borrador puede pasar a publicada con el audio principal presente, aunque ella carezca de audio.
|
||||
|
||||
**Corrección:** validar cada grabación que cambia a publicada dentro del servicio de dominio y la transacción; devolver errores asociados a la versión concreta. La carga de audio actual también está limitada a principales, por lo que debe completarse el recorrido de versiones.
|
||||
|
||||
**Aceptación:** impedir publicar una versión sin stream o intérprete sin bloquear los cambios válidos de su principal. **Código:** `src/panel/rutas/(dentro)/canciones/[slug]/+page.server.ts:68-74`; `src/lib/server/panel-canciones.ts:391-400,597-610,713-729`.
|
||||
|
||||
## H09 La invalidación del panel no alcanza la caché del sitio
|
||||
|
||||
**P2 · Confirmado por separación de procesos.**
|
||||
|
||||
El catálogo se conserva en una variable de módulo durante 60 segundos. El panel llama a `olvidarCatalogo`, pero vacía su propia memoria; el proceso público conserva su copia. Un cambio guardado puede tardar hasta la caducidad de esa copia en aparecer, y las pestañas ya abiertas requieren además refrescar sus datos.
|
||||
|
||||
**Corrección:** retirar primero la caché global al introducir consultas pequeñas por ruta, o usar una revisión persistente de contenido que ambos procesos observen. Evitar añadir un servicio de caché antes de medir. Revalidar disponibilidad al cobrar.
|
||||
|
||||
**Aceptación:** modificar un contenido desde el panel y comprobar su visibilidad en otro proceso según una latencia definida. **Código:** `src/lib/server/catalogo.ts:72-85`; `src/lib/server/panel-canciones.ts:773`; `vite.config.ts`.
|
||||
|
||||
## H10 Varias solicitudes de pago abren sesiones independientes
|
||||
|
||||
**P1 · Riesgo confirmado en el diseño; no se realizaron cobros.**
|
||||
|
||||
Cada POST de pago crea una nueva sesión Stripe y un nuevo pedido. No hay clave de idempotencia de la intención de compra ni recuperación de una sesión pendiente equivalente. El botón no queda bloqueado por envío en curso. Dos pestañas pueden abrir dos sesiones válidas para las mismas canciones y pagar ambas.
|
||||
|
||||
La idempotencia de `finalizarPedido` resuelve repeticiones de una misma sesión, no sesiones distintas. Ignorar compras duplicadas en SQL tampoco devuelve un segundo cobro.
|
||||
|
||||
**Corrección y aceptación:** representar una intención de compra por cuenta y selección; reutilizar su sesión y usar idempotencia en Stripe. En pruebas con proveedor simulado, dos peticiones simultáneas deben devolver la misma intención y una única sesión. **Código:** `src/routes/(sitio)/carrito/+page.server.ts:93-117`; `src/routes/(sitio)/carrito/+page.svelte:147-151`; `src/lib/server/stripe.ts:60-85`; `src/lib/server/pedidos.ts:134-172`.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H11 El modo de espera también bloquea los webhooks
|
||||
|
||||
**P2 · Confirmado en el enrutamiento; condicionado a activar PROXIMAMENTE.**
|
||||
|
||||
El portón permite únicamente la página de espera y `/_app/` para direcciones no autorizadas. Un POST externo a `/api/stripe/webhook` termina redirigido a la espera antes de verificar su firma. Si se activa el cierre con sesiones de pago todavía abiertas, sus confirmaciones dejan de procesarse mientras continúe activo.
|
||||
|
||||
**Corrección:** permitir la ruta exacta del webhook a través del portón, manteniendo obligatoria la firma Stripe. Definir también el comportamiento de los retornos de pago durante el cierre.
|
||||
|
||||
**Aceptación:** con el portón activo, un evento firmado llega al controlador y uno inválido se rechaza; las páginas públicas siguen cerradas. **Código:** `src/hooks.server.ts:58-60`; `src/lib/server/proximamente.ts:59-60`; `src/routes/api/stripe/webhook/+server.ts`.
|
||||
|
||||
## H12 El límite de subida no reserva espacio al formulario
|
||||
|
||||
**P2 · Confirmado en límites configurados; sin subir un archivo grande.**
|
||||
|
||||
El archivo admite 256 MiB, pero el cuerpo HTTP completo también tiene un límite de 256M y el proxy documenta 256m. Un archivo que alcanza el máximo válido requiere bytes adicionales para cabeceras multipart y los demás campos; se rechaza antes de que la validación de la aplicación pueda responder de forma útil.
|
||||
|
||||
**Corrección:** dejar margen acotado para el formulario o reducir el límite visible del archivo. Alinear el límite del proxy con el del adaptador. La transcodificación síncrona merece además un trabajo en segundo plano si aumenta el uso: cada proceso tiene hasta cinco minutos y el proxy dispone de 300 segundos para la petición completa.
|
||||
|
||||
**Aceptación:** verificar justo por debajo, en el límite y por encima; probar el conjunto proxy y adapter-node. **Código:** `src/lib/server/ingreso-audio.ts:19,76,102,242`; `.env.example:18`; `despliegue/con-proxy-manager.md:174-185`.
|
||||
|
||||
## H15 La señal de calidad no está completamente limpia
|
||||
|
||||
**P2 · Reproducido con los comandos del proyecto.**
|
||||
|
||||
Vitest anuncia 387 pruebas correctas y salida 0, pero imprime `Cannot read properties of undefined (reading 'wrapDynamicImport')` durante la inicialización de SvelteKit. Se repite ejecutando la suite sin una sincronización de tipos simultánea. La causa exacta de la interacción entre versiones y entorno necesita diagnóstico; no se atribuye aquí a un paquete concreto.
|
||||
|
||||
El lint falla en 11 archivos por Prettier. Los E2E del panel prueban acceso visual, no POST anónimos. Por eso una suite verde no ha detectado H01.
|
||||
|
||||
**Corrección y aceptación:** aislar la configuración de pruebas si procede, eliminar el error de arranque, restaurar el formato e incorporar integración HTTP y PostgreSQL para las invariantes sensibles. Exigir pruebas sin excepciones inesperadas en stderr. **Código:** `vite.config.ts` sección `test`; `package.json` scripts; `e2e/panel.e2e.ts:5-10`; runtime señalado en H01.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H13 El modal deja una columna vacía en pantallas bajas
|
||||
|
||||
**P2 · Confirmado visualmente en Chromium a 1024 por 500 px.**
|
||||
|
||||
La regla de altura oculta la ilustración, pero conserva las dos columnas de `.marco`. El formulario ocupa la primera columna, de 320 px, y deja otra de 382 px vacía dentro de un diálogo de 704 px. El cierre queda junto al formulario en vez de en el extremo del marco. El fallo también es relevante al usar zoom o una ventana baja.
|
||||
|
||||
**Corrección:** cuando se oculta la ilustración, convertir el marco en una columna y limitar el ancho al del formulario. Mantener el foco y el botón de cierre accesibles.
|
||||
|
||||
**Aceptación:** comprobar 1024 por 500 y 844 por 390 px, zoom al 200 %, teclado y retorno del foco al activador. **Código:** `src/lib/components/ModalAcceso.svelte:167,273-297`.
|
||||
|
||||

|
||||
|
||||
## H14 La cuenta anuncia packs de cinco en una tienda de diez
|
||||
|
||||
**P3 · Confirmado en el texto de la interfaz.**
|
||||
|
||||
El estado vacío de “Mis canciones” afirma que se vende en packs de cinco. La regla real exige diez y el resto de la compra usa ese tamaño. Es una contradicción comercial visible para quien acaba de crear su cuenta.
|
||||
|
||||
**Corrección:** componer el mensaje con `CANCIONES_POR_PACK`, y centralizar también precio, formato e inclusión de versiones en los mensajes de producto.
|
||||
|
||||
**Aceptación:** con cuenta sin compras, verificar que la pantalla coincide con el carrito y Checkout. **Código:** `src/routes/(sitio)/cuenta/+page.svelte:77`; `src/lib/tienda.ts:15`.
|
||||
|
||||
La identidad visual general se mantiene coherente en las vistas revisadas. Las 52 pruebas seleccionadas cubren navegación, anchos, controles, respuesta al puntero, contraste del dorado y comportamiento del fondo. Estos resultados no equivalen a una certificación integral de accesibilidad.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## H16 La documentación mezcla procedimientos actuales y retirados
|
||||
|
||||
**P3 · Confirmado en documentación versionada.**
|
||||
|
||||
README indica que migrar crea `local.db` y anuncia el panel en `/admin`, aunque la aplicación usa PostgreSQL y un build separado. `.env.example` describe SQLite y Turso sobre una URL PostgreSQL. También se dice que faltan páginas legales que ya tienen rutas y contenido; eso no permite concluir que su contenido esté validado, solo que el inventario está desactualizado.
|
||||
|
||||
**Corrección:** distinguir puesta en marcha actual, importación histórica y despliegue de dos procesos. Retirar los comentarios de catálogo que afirman que todavía no hay administración. Eliminar la explicación errónea sobre autorización en layouts señalada en H01.
|
||||
|
||||
**Aceptación:** otra persona debe poder arrancar sitio y panel siguiendo únicamente el procedimiento documentado. **Código:** `README.md:66,73,86,185-188`; `.env.example:2`; `src/lib/server/catalogo.ts:23,76`.
|
||||
|
||||
## Refactorizaciones recomendadas
|
||||
|
||||
**R01 Consultas específicas por pantalla.** El layout público envía el catálogo completo a cualquier página. La carga del catálogo consulta la fila completa de cada canción, incluida la letra que luego se reduce a extracto, y varias relaciones completas. El pago resuelve compras tema a tema. Crear modelos de listado, ficha, reproductor, carrito y biblioteca comprada; consultar en lote los identificadores necesarios. Medir bytes del HTML y datos, número de consultas y latencias antes y después. Fuentes: `src/routes/(sitio)/+layout.server.ts:27-34`; `src/lib/server/catalogo.ts:180-198`; `src/routes/(sitio)/carrito/+page.server.ts:68-78`.
|
||||
|
||||
**R02 Separar responsabilidades en los módulos grandes.** Hay 1211 líneas en `Capitulo.svelte`, 1137 en el editor de géneros, 775 en `panel-canciones.ts` y 662 en `TablaCanciones.svelte`. El tamaño orienta dónde revisar, pero no constituye por sí solo un error. Extraer secciones de edición, validadores, presentación de créditos y servicios de audio según sus responsabilidades; conservar contratos estables y evitar fragmentar por número arbitrario de líneas.
|
||||
|
||||
**R03 Consolidar reglas de dominio.** La publicación, la acreditación, la selección de audio y las reglas comerciales están repartidas entre acciones, consultas y componentes. Crear operaciones explícitas como publicar grabación, registrar crédito y preparar entrega. Aplicar la misma validación desde panel e importadores. La base debe seguir garantizando las invariantes que pueda expresar, sin delegarlas únicamente al formulario.
|
||||
|
||||
**R04 Dar un ciclo de vida recuperable al procesado.** La publicación de archivos y la transacción PostgreSQL compensan fallos controlados, pero no forman una transacción única frente a la caída del proceso. Registrar trabajos y sus estados, limitar concurrencia y añadir reconciliación de temporales y archivos huérfanos. Mantener la estrategia de no sobrescribir. Introducir esta infraestructura cuando se implemente regeneración o subida frecuente; priorizar antes H01 y las entregas incorrectas.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## Mejoras de diseño y funciones propuestas
|
||||
|
||||
Estas propuestas son ampliaciones, no defectos confirmados. Deben implementarse después de proteger el panel y cerrar los problemas que afectan a compras y datos.
|
||||
|
||||
**A01 Alta musical completa.** Añadir portada opcional con previsualización, recorte y validación; asociar intérpretes sin abandonar el alta y mostrar el estado de máster, stream, onda y descarga. Completar audio propio por versión y créditos con rol, instrumento, acreditación y orden. Valor alto para publicación diaria; esfuerzo medio o alto, dependiente de H05, H07 y H08.
|
||||
|
||||
**A02 Biblioteca y pedidos gestionables.** Mostrar disponibilidad, formato y tamaño de cada descarga; ofrecer pedidos pendientes y una vía clara para incidencias. Añadir conciliación de pagos y estados de reembolso en el panel. Una descarga conjunta puede evaluarse si hay demanda, cuidando que los archivos se preparen sin cargar todos los másteres en memoria. Dependencias: H06, H07 y H10.
|
||||
|
||||
**A03 Listas con edición completa.** La implementación actual permite crear una lista con una canción y alternar pertenencia. Añadir renombrado, borrado con confirmación y orden mediante teclado y controles accesibles; preservar la propiedad de la lista en servidor. Criterio de éxito: ordenar una lista desde móvil y verla igual al volver a entrar.
|
||||
|
||||
**A04 Recorrido comprensible de la suscripción.** La cuenta muestra si está activa, pero no ofrece contratación o gestión. Si el catálogo exclusivo va a comercializarse, completar alta, estado, renovación, cancelación y acceso al portal de gestión. Si es una concesión manual, explicar cómo solicitarla. El esquema de suscripción existente no implica que todo ese producto esté implementado.
|
||||
|
||||
**A05 Descubrimiento y primera escucha.** En la portada móvil revisada, el disco decorativo ocupa la parte superior y la llamada principal comienza cerca de los 800 px de altura. Probar una composición más compacta que acerque “Poner el disco” al primer tramo visible. Conservar la identidad de la marca y medir inicio de reproducción; no asumir que menos imagen mejora necesariamente la experiencia. En el catálogo, facilitar la llegada a intérpretes y explicar los estilos sin obras disponibles.
|
||||
|
||||
**A06 Estado operativo del catálogo.** Crear una vista que reúna publicaciones sin intérpretes, medios ausentes, derivados pendientes y fallos de procesado. Mostrar acciones concretas para resolverlos. Diferenciar integridad de relaciones en PostgreSQL de existencia y calidad de los archivos: el auditor actual no demuestra por sí solo que los 22 audios estén físicamente presentes ni que se puedan decodificar.
|
||||
|
||||
## Criterios de diseño transversales
|
||||
|
||||
Conservar tipografía, contraste y la distinción entre contenido y controles. Hacer consistentes los estados vacío, cargando, error y guardado; indicar el progreso de operaciones largas. Validar móviles en vertical y horizontal, zoom, teclado y tecnologías de asistencia en los flujos principales. Evitar cambios visuales generales hasta resolver los defectos concretos y contar con una hipótesis medible.
|
||||
|
||||
<!-- salto -->
|
||||
|
||||
## Secuencia de trabajo propuesta
|
||||
|
||||
**Entrega inmediata.** Corregir H01 y añadir las pruebas HTTP de autorización antes de reabrir el panel a usuarios no controlados. Revisar la configuración efectiva del proxy para determinar si H02 está presente. La auditoría no demuestra que el defecto se haya explotado.
|
||||
|
||||
**Entrega de integridad y compra.** Completar intérpretes y auditor; preservar créditos al guardar; separar la biblioteca comprada; generar la representación comercial; aplicar guardas a versiones e idempotencia de la intención de pago. Preparar casos de integración en una base aislada antes de modificar datos existentes.
|
||||
|
||||
**Entrega operativa.** Actualizar despliegue y rutas compartidas, resolver el paso de webhooks durante el cierre y alinear límites de subida. Ensayar instalación limpia, reinicio, interrupción del despliegue y restauración de una copia.
|
||||
|
||||
**Entrega de mantenimiento y producto.** Limpiar la señal de tests y lint, corregir el modal y textos, actualizar README e introducir modelos por ruta. Después, completar el alta de portadas y versiones, listas y gestión comercial según las prioridades del negocio.
|
||||
|
||||
## Comprobaciones de cierre
|
||||
|
||||
| Área | Evidencia necesaria para cerrar |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Autorización | Ninguna petición no autorizada ejecuta consultas sensibles o escrituras del panel. Probar también rol revocado. |
|
||||
| Datos | Cero publicaciones sin créditos o stream; guardar campos básicos conserva acreditaciones detalladas. |
|
||||
| Compra | Una intención genera un único Checkout; confirmar dos veces no duplica derechos; retirar una ficha no oculta descargas permitidas. |
|
||||
| Medios | Fuentes WAV y FLAC producen la descarga MP3 descrita. MIME, rango, tamaño y autorización comprobados. |
|
||||
| Operación | Sitio y panel arrancan desde una instalación limpia y comparten almacenes correctos; no se pierde servicio ante un fallo de transferencia. |
|
||||
| Interfaz | Modal sin columna vacía, mensajes comerciales consistentes, teclado y zoom comprobados. |
|
||||
| Calidad | Check, formato, ESLint, unitarias e integración pasan sin excepciones de inicialización inesperadas. |
|
||||
|
||||
## Inventario del fallo de formato
|
||||
|
||||
Prettier señaló once archivos: `Capitulo.svelte`, `FormularioAcceso.svelte`, `Heroe.svelte`, `player/BarraReproductor.svelte` y `TarjetaEstilo.svelte` dentro de `src/lib/components`; y las páginas de inicio, blog, entrada de blog, carrito, letristas y legal dentro de `src/routes/(sitio)`. ESLint pasó al ejecutarse de forma independiente.
|
||||
@ -0,0 +1,102 @@
|
||||
# Arquitectura de comercio extensible
|
||||
|
||||
Fecha: 12 de septiembre de 2026
|
||||
|
||||
## Decisión principal
|
||||
|
||||
El carrito ya no representa directamente lo que se cobra. Mantiene las cosas
|
||||
que ha elegido la persona y, al comenzar el pago, un resolutor las convierte en
|
||||
líneas de cobro inmutables.
|
||||
|
||||
Esta separación resuelve dos necesidades distintas:
|
||||
|
||||
- una canción es un derecho de descarga individual;
|
||||
- el precio de las canciones se calcula por packs de hasta diez;
|
||||
- libros, partituras, MIDI, stems y servicios tienen cantidad y precio propios;
|
||||
- una suscripción tiene recurrencia, plan y ciclo de vida, no es un artículo
|
||||
puntual camuflado en el carrito.
|
||||
|
||||
## Regla de packs
|
||||
|
||||
El número de packs se calcula con `ceil(canciones / 10)`. Por tanto:
|
||||
|
||||
| Canciones elegidas | Packs cobrados | Huecos incluidos |
|
||||
| -----------------: | -------------: | ---------------: |
|
||||
| 0 | 0 | 0 |
|
||||
| 1 | 1 | 9 |
|
||||
| 10 | 1 | 0 |
|
||||
| 11 | 2 | 9 |
|
||||
| 20 | 2 | 0 |
|
||||
| 21 | 3 | 9 |
|
||||
|
||||
No existe ya el bloqueo por pack incompleto. El carrito informa de cuántas
|
||||
canciones más caben sin aumentar el total, pero permite pagar cualquier
|
||||
cantidad positiva.
|
||||
|
||||
## Capas del modelo
|
||||
|
||||
### Selección
|
||||
|
||||
- `carrito_item` guarda las canciones concretas.
|
||||
- `carrito_producto` guarda los demás productos y sus cantidades.
|
||||
- `producto` contiene las ofertas comerciales disponibles: libro, partitura,
|
||||
MIDI, stems o servicio.
|
||||
|
||||
Los precios siempre se vuelven a leer en el servidor. Ninguna acción acepta el
|
||||
precio enviado por el navegador.
|
||||
|
||||
### Resolución y cobro
|
||||
|
||||
`resolverCompra` transforma la selección en `LineaDeCobro[]`. Diecisiete
|
||||
canciones producen una línea de dos packs, no diecisiete líneas. Cada producto
|
||||
adicional produce su propia línea.
|
||||
|
||||
Antes de contactar con Stripe se comprueban identidad, cantidades, importes,
|
||||
moneda, modalidad y recurrencia. Una misma sesión no puede mezclar pago único y
|
||||
suscripción: se resuelven en operaciones separadas porque tienen ciclos de
|
||||
cancelación, renovación y fiscalidad distintos.
|
||||
|
||||
### Pedido y derechos
|
||||
|
||||
- `pedido_linea` congela nombre, tipo, cantidad, precio e importe de cada línea.
|
||||
Una factura histórica no cambia al editar el catálogo.
|
||||
- `pedido_item` conserva qué canciones incluía el pedido.
|
||||
- `compra` es el derecho rápido de descarga de cada canción.
|
||||
|
||||
Las líneas facturadas y los derechos entregados no se deben fusionar. Un pack
|
||||
puede entregar entre una y diez canciones; un libro o unas partituras podrán
|
||||
entregar varios archivos mediante relaciones específicas cuando se publiquen.
|
||||
|
||||
## Suscripciones
|
||||
|
||||
`plan_suscripcion` define código, nombre, nivel, precio y periodicidad. Sus
|
||||
capacidades operativas —publicidad, calidad de escucha, cupo y calidad de
|
||||
descargas, acceso a versiones, Juke-box, contenido del taller y descarga de sus
|
||||
materiales— son columnas estructuradas y dependen del ámbito del plan;
|
||||
`prestaciones`
|
||||
queda para ventajas editoriales adicionales y no para tomar decisiones de
|
||||
acceso. También conserva la referencia de precio externa. `suscripcion` registra el periodo
|
||||
contratado por cada cuenta y apunta opcionalmente al plan. La relación es
|
||||
opcional para conservar las concesiones manuales históricas.
|
||||
|
||||
Los planes musicales públicos iniciales son Seguidor, Amigo y Buen amigo. El Juke-box
|
||||
es una capacidad explícita de Amigo y Buen amigo; Seguidor no la concede. El Taller
|
||||
de letras dispone de una suscripción anual independiente, con acceso completo y
|
||||
descarga de materiales. La contratación y la
|
||||
aplicación efectiva de publicidad, calidades y cupos se conectarán sobre estas
|
||||
capacidades, sin comparar nombres ni textos comerciales.
|
||||
|
||||
## Extensión prevista
|
||||
|
||||
Para publicar una nueva clase de producto no será necesario crear otro sistema
|
||||
de pedidos. Se añadirá su ficha editorial, sus archivos entregables y una acción
|
||||
que use `anadirProductoAlCarrito`. El mismo resolutor, la intención de pago y
|
||||
`pedido_linea` ya aceptan esos artículos.
|
||||
|
||||
Antes de activar cada tipo quedan por concretar:
|
||||
|
||||
1. archivos o prestación que entrega;
|
||||
2. política de acceso y caducidad;
|
||||
3. impuestos y texto legal;
|
||||
4. reembolsos y revocación del derecho;
|
||||
5. interfaz de administración y publicación.
|
||||
@ -0,0 +1,67 @@
|
||||
# Arquitectura editorial y plantillas
|
||||
|
||||
## Decisión
|
||||
|
||||
La aplicación no guardará páginas completas ni nombres de componentes en la base de datos. Cada
|
||||
entidad conserva sus datos de dominio y añade dos piezas editoriales comunes:
|
||||
|
||||
- `secciones`: bloques de contenido ordenados y tipados;
|
||||
- `presentacion`: una plantilla registrada en código y el modo del hero.
|
||||
|
||||
Así se obtiene flexibilidad editorial sin convertir el panel en un constructor libre de páginas ni
|
||||
permitir HTML, CSS o componentes arbitrarios desde la base de datos.
|
||||
|
||||
## Contrato de presentación
|
||||
|
||||
`presentacion` tiene esta forma:
|
||||
|
||||
```ts
|
||||
{
|
||||
plantilla: string;
|
||||
heroe: 'automatico' | 'visible' | 'oculto';
|
||||
}
|
||||
```
|
||||
|
||||
El registro inicial admite:
|
||||
|
||||
| Entidad | Plantilla |
|
||||
| ------- | -------------------- |
|
||||
| Artista | `perfil-artista` |
|
||||
| Álbum | `detalle-album` |
|
||||
| Canción | `detalle-cancion` |
|
||||
| Blog | `articulo-editorial` |
|
||||
|
||||
`automatico` muestra el hero cuando la plantilla dispone de imagen o contenido suficiente. Los
|
||||
modos `visible` y `oculto` son decisiones editoriales explícitas. Cualquier valor desconocido se
|
||||
normaliza al valor seguro de su tipo.
|
||||
|
||||
## Contenido estructurado
|
||||
|
||||
Artistas, álbumes y artículos comparten `secciones`. Las canciones reutilizan
|
||||
`inspiracionSecciones`, porque ya representa el contenido editorial propio de su ficha y duplicarlo
|
||||
crearía dos fuentes de verdad.
|
||||
|
||||
Los textos antiguos se convierten en un bloque de texto al abrirlos en Estudio y durante la
|
||||
migración de álbumes. Las columnas de texto se mantienen como respaldo para consumidores antiguos,
|
||||
pero Estudio publica su valor a partir de los bloques.
|
||||
|
||||
## Flujo
|
||||
|
||||
1. Estudio muestra los campos de plantilla y hero junto al editor de bloques.
|
||||
2. La vista previa privada indica la configuración que se publicará.
|
||||
3. El adaptador de publicación valida la plantilla contra el registro y proyecta los bloques a las
|
||||
tablas públicas.
|
||||
4. Cada ruta pública interpreta el modo del hero con su propia regla automática.
|
||||
|
||||
## Cómo añadir una variante
|
||||
|
||||
Una nueva plantilla requiere una decisión deliberada en código:
|
||||
|
||||
1. añadir su identificador al registro de `src/lib/presentacion.ts`;
|
||||
2. implementar la composición visual en la ruta o componente del tipo correspondiente;
|
||||
3. comprobar escritorio y móvil;
|
||||
4. añadir pruebas de normalización y renderizado;
|
||||
5. solo entonces queda disponible en Estudio.
|
||||
|
||||
Esto evita valores huérfanos en la base de datos y mantiene sincronizados editor, vista previa y
|
||||
sitio público.
|
||||
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 633 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 463 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 427 KiB |
|
After Width: | Height: | Size: 504 KiB |
|
After Width: | Height: | Size: 1003 KiB |
@ -0,0 +1,427 @@
|
||||
{
|
||||
"fecha": "2026-09-11T19:24:01.806Z",
|
||||
"base": "http://127.0.0.1:4180",
|
||||
"paginas": [
|
||||
{
|
||||
"ruta": "/",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/musica",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/interpretes",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/bio",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/taller",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/generos",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/letristas",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/glosario",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/blog",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/contacto",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/carrito",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/entrar",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/legal",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/estilos",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/oficio",
|
||||
"estado": 200,
|
||||
"desborde": false,
|
||||
"imagenesRotas": []
|
||||
}
|
||||
],
|
||||
"movil": [
|
||||
{
|
||||
"ruta": "/",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/musica",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/interpretes",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/bio",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/taller",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/generos",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/letristas",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/glosario",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/blog",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/contacto",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/carrito",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/entrar",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/legal",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/estilos",
|
||||
"desborde": false
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/oficio",
|
||||
"desborde": false
|
||||
}
|
||||
],
|
||||
"errores": [],
|
||||
"enlaces": [
|
||||
{
|
||||
"ruta": "/",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/musica",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/taller",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/blog",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/bio",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/carrito",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/entrar",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/albumes/cupido-sin-flechas",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/taller/lo-que-hace-que-una-letra-funcione",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/deseo",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/mentiras",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/rabia",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/ruptura",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/una-noche",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/autoengano",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/musica/baladas-romanticas",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/albumes/singles-baladas-romanticas",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/musica/clasica",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/musica/rock",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/musica/pop-melodico",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/contacto",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/legal/aviso-legal",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/legal/condiciones-de-uso",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/legal/privacidad",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/legal/cookies",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/legal/condiciones-de-venta",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/interpretes",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/interpretes/senza-paura",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/taller/la-imagen-concreta",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/taller/metrica-y-acento",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/taller/la-rima-como-consecuencia",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/taller/estructura-y-giro",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/taller/el-estribillo",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/glosario",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/letristas",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/bolero",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/son-cubano",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/ranchera",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/tango",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/vals-criollo",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/bachata",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/vallenato",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/generos/cumbia",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/desamor",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/oficio",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/balance",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/celos",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/compasion",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/despecho",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/despedidas",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/entrega",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/estilos",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/melancolia",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/paso-del-tiempo",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/promesas",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/recuerdos",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/silencio",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/soledad",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/etiquetas/suplica",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/blog/rss.xml",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/blog/escribir-en-un-estilo-ajeno",
|
||||
"estado": 200
|
||||
},
|
||||
{
|
||||
"ruta": "/blog/lo-que-no-cabe-en-una-cancion",
|
||||
"estado": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 426 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 830 KiB |
|
After Width: | Height: | Size: 838 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 120 KiB |