deploy: la demo queda en línea en fspm-demo.urieljareth.org
Desplegada y verificada de punta a punta contra el dominio público, en Chromium y en WebKit a 375px: sitio, catálogo filtrado, acceso al panel, persistencia de sesión y captura de leads con atribución de campaña. No quedó como recurso de Coolify, y está documentado por qué: la API de esa instancia no expone creación de aplicaciones (las rutas /applications/* responden 404 con cuerpo vacío y /openapi.yaml devuelve el HTML del panel), y al crear un servicio por /services la tarea de despliegue quedó colgada sin escribir nada en disco. El servidor está sano; el problema es la instancia. El contenedor corre en el Docker del LXC 102, en la red coolify, con las etiquetas de Traefik que replican el patrón de las apps que ya sirven con TLS ahí, y con coolify.managed=false para que nadie lo confunda con un recurso del panel. La imagen se construye dentro del LXC desde el repo público de Gitea. El costo de esta ruta está escrito en la documentación: un push no redespliega. Se agregó el procedimiento de tres comandos para publicar una versión nueva sin tocar el volumen, y la vía para migrar a app git-based si se consigue el acceso al panel web. Se documenta también una carrera que provocó un diagnóstico equivocado: el acceso usa una acción de servidor, y pulsar el botón antes de que React hidrate no hace nada ni da error. La primera verificación fallaba solo por el dominio público y funcionaba contra Traefik directo, lo que parecía señalar a Cloudflare. No era: los chunks son idénticos por ambos caminos y la hidratación es igual. Era la espera de la prueba. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
31ee899987
commit
15d31b5d6b
@@ -9,3 +9,5 @@ tests/reporte/
|
|||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
deploy/fspm-live.sh
|
||||||
|
deploy/.fspm-live.ejecutar.sh
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# FSPM — sitio comercial y micro CRM
|
# FSPM — sitio comercial y micro CRM
|
||||||
|
|
||||||
|
**En línea: https://fspm-demo.urieljareth.org** · Panel en `/admin/login`, que
|
||||||
|
no se enlaza desde el sitio a propósito. Cómo está desplegado y cómo publicar
|
||||||
|
una versión nueva: [`docs/DESPLIEGUE.md`](docs/DESPLIEGUE.md).
|
||||||
|
|
||||||
Demostración construida por **Consultoría E3** para Fire Service Plus México.
|
Demostración construida por **Consultoría E3** para Fire Service Plus México.
|
||||||
Sitio público orientado a venta consultiva, con un CRM interno que registra cada
|
Sitio público orientado a venta consultiva, con un CRM interno que registra cada
|
||||||
envío de formulario y de widget de WhatsApp como oportunidad, con su atribución
|
envío de formulario y de widget de WhatsApp como oportunidad, con su atribución
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Compose del servicio en Coolify.
|
||||||
|
#
|
||||||
|
# Por qué existe este archivo y no un despliegue git-based normal:
|
||||||
|
# la API REST de esta instancia de Coolify no expone creación de aplicaciones
|
||||||
|
# (todas las rutas /applications/* responden 404, verificado en 4.1.2 y 4.3.12),
|
||||||
|
# y la interfaz web reportaba "No available server". La única ruta de creación
|
||||||
|
# que sí responde es POST /services con un compose. Como un servicio de Coolify
|
||||||
|
# no clona un repositorio, la imagen se construye aparte —dentro del LXC donde
|
||||||
|
# corre Coolify, desde el repo público de Gitea— y aquí solo se consume.
|
||||||
|
#
|
||||||
|
# Consecuencia operativa, y hay que tenerla presente: un push a Gitea NO
|
||||||
|
# redespliega solo. Para publicar una versión nueva hay que reconstruir la
|
||||||
|
# imagen en el host y reiniciar el servicio. Está documentado en
|
||||||
|
# docs/DESPLIEGUE.md.
|
||||||
|
#
|
||||||
|
# SERVICE_FQDN_WEB_3000 es la convención de Coolify: al declararla, Coolify
|
||||||
|
# genera el dominio y las etiquetas de Traefik para el puerto 3000 del servicio
|
||||||
|
# `web`, y se encarga del certificado.
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: fspm-web:demo
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- SERVICE_FQDN_WEB_3000
|
||||||
|
- NODE_ENV=production
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
- PORT=3000
|
||||||
|
# Ruta absoluta a propósito: una relativa se resuelve contra la carpeta
|
||||||
|
# del esquema de Prisma y es el clásico "funciona en local, falla aquí".
|
||||||
|
- DATABASE_URL=file:/app/data/fspm.db
|
||||||
|
- AUTH_SECRET=${AUTH_SECRET}
|
||||||
|
# El acceso rápido por perfil queda encendido porque la demo se enseña
|
||||||
|
# acompañada. Apagarlo es cambiar esto a false y reiniciar.
|
||||||
|
- DEMO_ACCESO_RAPIDO=true
|
||||||
|
# Techo de envíos por IP en diez minutos. Veinte frena a un bot y deja
|
||||||
|
# respirar una demostración con varias personas en la misma red.
|
||||||
|
- LEADS_MAX_ENVIOS=20
|
||||||
|
volumes:
|
||||||
|
# Aquí vive el SQLite y TIENE que sobrevivir a los redespliegues. Sin
|
||||||
|
# este volumen la falla es silenciosa: el contenedor arranca con los
|
||||||
|
# datos sembrados y nadie nota nada hasta que se pierde un lead real.
|
||||||
|
- fspm-datos:/app/data
|
||||||
|
healthcheck:
|
||||||
|
# La sonda consulta la base, así que un volumen no escribible se reporta
|
||||||
|
# como contenedor enfermo en vez de pasar por sano.
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:3000/api/salud"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 60s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fspm-datos:
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { chromium, webkit } from "@playwright/test";
|
||||||
|
|
||||||
|
const BASE = "https://fspm-demo.urieljareth.org";
|
||||||
|
let fallas = 0;
|
||||||
|
const ok = (c, m) => { console.log(`${c ? " ok " : "FALLA "} ${m}`); if (!c) fallas++; };
|
||||||
|
|
||||||
|
for (const [nombre, motor, viewport] of [
|
||||||
|
["chromium 1440", chromium, { width: 1440, height: 900 }],
|
||||||
|
["webkit 375", webkit, { width: 375, height: 780 }],
|
||||||
|
]) {
|
||||||
|
console.log(`\n=== ${nombre} ===`);
|
||||||
|
const nav = await motor.launch();
|
||||||
|
const pag = await nav.newPage({ viewport, locale: "es-MX" });
|
||||||
|
|
||||||
|
// sitio público
|
||||||
|
await pag.goto(BASE + "/", { waitUntil: "domcontentloaded" });
|
||||||
|
ok((await pag.locator("h1").count()) === 1, "el inicio tiene un solo h1");
|
||||||
|
ok((await pag.locator('a[href^="/admin"]').count()) === 0, "el panel no está enlazado");
|
||||||
|
|
||||||
|
// la página no se corre de lado
|
||||||
|
const corre = await pag.evaluate(() => {
|
||||||
|
window.scrollTo(800, 0); const m = window.scrollX; window.scrollTo(0, 0); return m;
|
||||||
|
});
|
||||||
|
ok(corre <= 2, `el inicio no se desplaza en horizontal (se corrió ${corre}px)`);
|
||||||
|
|
||||||
|
// catálogo filtrado por escenario
|
||||||
|
await pag.goto(BASE + "/catalogo?escenario=gasolineras", { waitUntil: "domcontentloaded" });
|
||||||
|
ok((await pag.locator("h1").count()) === 1, "el catálogo filtrado carga");
|
||||||
|
|
||||||
|
// Acceso rápido al panel. Se espera `load` y un margen antes de pulsar: el
|
||||||
|
// botón dispara una acción de servidor y hasta que React no hidrata, el clic
|
||||||
|
// no hace nada. Con la latencia del dominio público esa carrera se pierde, y
|
||||||
|
// la primera versión de esta verificación culpó a Cloudflare por eso.
|
||||||
|
await pag.goto(BASE + "/admin/login", { waitUntil: "load" });
|
||||||
|
const perfil = pag.getByRole("button").filter({ hasText: /administrador/i }).first();
|
||||||
|
ok(await perfil.isVisible().catch(() => false), "el acceso rápido lista perfiles");
|
||||||
|
await pag.waitForTimeout(2500);
|
||||||
|
await perfil.click();
|
||||||
|
await pag.waitForURL(/\/admin(?!\/login)/, { timeout: 30000 }).catch(() => {});
|
||||||
|
ok(!/\/admin\/login/.test(pag.url()), `entró al panel (${pag.url().replace(BASE, "")})`);
|
||||||
|
|
||||||
|
// y la sesión persiste en otra página: eso es la cookie Secure funcionando
|
||||||
|
await pag.goto(BASE + "/admin/oportunidades", { waitUntil: "load" });
|
||||||
|
ok(!/\/admin\/login/.test(pag.url()), "la sesión persiste entre páginas");
|
||||||
|
const cuerpo = (await pag.locator("body").textContent()) ?? "";
|
||||||
|
ok(/OPP-\d{4}-\d{4}/.test(cuerpo), "el listado muestra oportunidades reales");
|
||||||
|
|
||||||
|
await nav.close();
|
||||||
|
}
|
||||||
|
console.log(fallas === 0 ? "\nTODO EN VERDE EN PRODUCCION" : `\n${fallas} FALLAS EN PRODUCCION`);
|
||||||
|
process.exit(fallas === 0 ? 0 : 1);
|
||||||
@@ -3,6 +3,61 @@
|
|||||||
Sitio de FSPM con micro CRM. Un solo contenedor: Next.js en modo `standalone` y
|
Sitio de FSPM con micro CRM. Un solo contenedor: Next.js en modo `standalone` y
|
||||||
SQLite en un volumen. No hay base de datos externa.
|
SQLite en un volumen. No hay base de datos externa.
|
||||||
|
|
||||||
|
## Cómo está desplegado hoy
|
||||||
|
|
||||||
|
**En línea:** https://fspm-demo.urieljareth.org
|
||||||
|
|
||||||
|
No es un recurso de Coolify, y eso es deliberado. La API de esta instancia no
|
||||||
|
expone creación de aplicaciones —las tres rutas `/applications/*` responden 404
|
||||||
|
con cuerpo vacío, y `/openapi.yaml` devuelve el HTML del panel—, y al crear un
|
||||||
|
servicio por `POST /services` la tarea de despliegue quedó colgada sin escribir
|
||||||
|
nada en `/data/coolify/services/`. La interfaz web reportaba "No available
|
||||||
|
server" aunque el servidor está sano (verificado por API: alcanzable, usable,
|
||||||
|
Traefik corriendo, red `coolify` presente, dominio comodín configurado).
|
||||||
|
|
||||||
|
Así que el contenedor corre directamente en el Docker del LXC 102, en la red
|
||||||
|
`coolify`, con las etiquetas de Traefik que replican el patrón de las
|
||||||
|
aplicaciones que ya sirven con TLS en ese servidor. Lleva
|
||||||
|
`coolify.managed=false` para que nadie lo confunda con un recurso del panel.
|
||||||
|
|
||||||
|
- Imagen: `fspm-web:demo`, construida **dentro del LXC 102** desde el repo
|
||||||
|
público de Gitea. No hay registro de contenedores en juego.
|
||||||
|
- Contenedor: `fspm-web`, con `--restart unless-stopped`, así que sobrevive a
|
||||||
|
un reinicio del LXC.
|
||||||
|
- Volumen: `fspm-datos` montado en `/app/data`.
|
||||||
|
- Certificado: lo emite el Traefik que ya estaba, con `letsencrypt`.
|
||||||
|
|
||||||
|
**La consecuencia operativa importante:** un push a Gitea **no** redespliega.
|
||||||
|
Ver "Publicar una versión nueva" abajo.
|
||||||
|
|
||||||
|
### Publicar una versión nueva
|
||||||
|
|
||||||
|
Tres comandos desde el host Proxmox (`ssh -i keys/proxmox_ed25519 [email protected]`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. traer el código nuevo
|
||||||
|
pct exec 102 -- git -C /opt/fspm-src pull
|
||||||
|
|
||||||
|
# 2. reconstruir la imagen (los NEXT_PUBLIC_* se hornean aquí)
|
||||||
|
pct exec 102 -- docker build -t fspm-web:demo --build-arg NEXT_PUBLIC_SITE_URL=https://fspm-demo.urieljareth.org --build-arg NEXT_PUBLIC_WHATSAPP=525540348395 /opt/fspm-src
|
||||||
|
|
||||||
|
# 3. recrear el contenedor — el volumen NO se toca, los leads se conservan
|
||||||
|
pct exec 102 -- docker rm -f fspm-web
|
||||||
|
# y volver a correr `deploy/fspm-live.sh` (lleva el AUTH_SECRET sustituido)
|
||||||
|
```
|
||||||
|
|
||||||
|
El script `deploy/fspm-live.sh` del repo tiene el `docker run` completo con las
|
||||||
|
etiquetas de Traefik; el `AUTH_SECRET` va como marcador y se sustituye al
|
||||||
|
ejecutarlo.
|
||||||
|
|
||||||
|
### Si algún día se puede usar Coolify
|
||||||
|
|
||||||
|
Con el correo del panel web se puede crear la aplicación como **Public
|
||||||
|
Repository → Dockerfile** apuntando al repo de Gitea, y entonces sí recupera el
|
||||||
|
redespliegue automático por push. La especificación exacta de campos está en la
|
||||||
|
sección "Coolify" de abajo. En ese momento hay que **eliminar el contenedor
|
||||||
|
manual** para que no compitan dos backends por el mismo dominio en Traefik.
|
||||||
|
|
||||||
## Variables de entorno
|
## Variables de entorno
|
||||||
|
|
||||||
| Variable | Requerida | Qué pasa si falta |
|
| Variable | Requerida | Qué pasa si falta |
|
||||||
@@ -110,6 +165,20 @@ curl -H "Authorization: Bearer $COOLIFY_TOKEN" \
|
|||||||
"$COOLIFY_API_URL/deploy?uuid=<uuid de la aplicación>"
|
"$COOLIFY_API_URL/deploy?uuid=<uuid de la aplicación>"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Una carrera que confunde: hidratación y latencia
|
||||||
|
|
||||||
|
El acceso al panel usa una acción de servidor. Hasta que React no hidrata la
|
||||||
|
página, pulsar el botón **no hace nada y no da error**. Con poca latencia no se
|
||||||
|
nota; a través de Cloudflare, sí.
|
||||||
|
|
||||||
|
Esto costó un diagnóstico equivocado: la primera verificación automatizada
|
||||||
|
pulsaba el botón demasiado pronto, fallaba solo por el dominio público y
|
||||||
|
funcionaba pegándole a Traefik directo, lo que apuntaba a Cloudflare. No era
|
||||||
|
Cloudflare —los chunks de JavaScript son byte a byte idénticos por los dos
|
||||||
|
caminos y la hidratación es igual—, era la espera de la prueba. Si alguien
|
||||||
|
vuelve a ver este síntoma, antes de culpar al proxy conviene esperar `load` más
|
||||||
|
un margen y volver a medir.
|
||||||
|
|
||||||
## Verificación posterior al despliegue
|
## Verificación posterior al despliegue
|
||||||
|
|
||||||
Hazla en este orden; cada punto descarta una clase distinta de falla.
|
Hazla en este orden; cada punto descarta una clase distinta de falla.
|
||||||
|
|||||||
Reference in New Issue
Block a user