feat: sitio comercial FSPM con micro CRM interno
Sitio público orientado a venta consultiva e implementación de las recomendaciones de la auditoría de marca y mercado: - Inicio con tres rutas (industria, riesgo, cumplimiento) y clasificador de riesgo en el héroe, contra el hallazgo de que el visitante tenía que inferir qué solución le correspondía. - Cinco páginas por segmento prioritario y catálogo comparable por clase de fuego, capacidad y agente. - Los claims sin evidencia por modelo quedan aislados y se presentan como compromiso de documentación, nunca como característica del producto. - SEO técnico completo: metadatos por página, sitemap derivado de los datos, JSON-LD, iconos y tarjeta social generadas. Micro CRM: - Captura por formulario embebido y por widget de WhatsApp que registra antes de abrir la conversación. - Atribución de primer toque que sobrevive a la navegación. - Deduplicación de contacto; un mismo contacto acumula oportunidades. - Motor de asignación por reglas con reparto por turno de respaldo. - Panel con dos roles de alcance distinto, tablero analítico, bitácora, pedidos y reglas. No enlazado desde el sitio público y con noindex. Empaquetado en Docker multietapa con la base de demostración horneada en la imagen y el volumen de datos separado, para que los leads capturados sobrevivan a los redespliegues. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
122e06a4bb
commit
01b817d098
+183
@@ -0,0 +1,183 @@
|
||||
# check=skip=SecretsUsedInArgOrEnv
|
||||
|
||||
# La directiva de arriba tiene que ser la primera línea del archivo. Silencia la
|
||||
# advertencia de BuildKit por el AUTH_SECRET de la etapa de construcción, que es
|
||||
# un relleno para que `next build` no falle: no llega a la imagen final ni se
|
||||
# usa en ejecución. Sin esto, cada construcción sale con una advertencia y se
|
||||
# acaba aprendiendo a ignorar también las advertencias de verdad.
|
||||
|
||||
# =============================================================================
|
||||
# FSPM — imagen de producción
|
||||
#
|
||||
# Tres etapas sobre node:22-alpine, aprovechando output: "standalone" de
|
||||
# next.config.ts. La idea de cada etapa:
|
||||
#
|
||||
# deps instala las dependencias en su propia capa, así un cambio de código
|
||||
# no vuelve a bajar node_modules.
|
||||
# build genera el cliente de Prisma, hornea una base de demostración y
|
||||
# compila Next.
|
||||
# runner imagen mínima que solo sabe arrancar el servidor, con usuario no
|
||||
# root y la base de datos en un volumen.
|
||||
#
|
||||
# OJO CON LAS VARIABLES NEXT_PUBLIC_*
|
||||
# Next las sustituye literalmente durante `next build`, no las lee al arrancar.
|
||||
# Por eso NEXT_PUBLIC_SITE_URL y NEXT_PUBLIC_WHATSAPP van como ARG de
|
||||
# construcción: cambiar el dominio exige reconstruir la imagen, no basta con
|
||||
# editar el entorno del contenedor. Está explicado en docs/DESPLIEGUE.md.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# --- etapa 1: dependencias -----------------------------------------------------
|
||||
FROM node:22-alpine AS deps
|
||||
|
||||
# libc6-compat y openssl: Prisma los necesita en Alpine para sus motores.
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# El esquema entra antes de npm ci porque el postinstall de @prisma/client
|
||||
# intenta generar el cliente y sin esquema deja una advertencia.
|
||||
COPY package.json package-lock.json ./
|
||||
COPY prisma/schema.prisma ./prisma/schema.prisma
|
||||
|
||||
# Aquí NO se define NODE_ENV=production: hacen falta las dependencias de
|
||||
# desarrollo (prisma, tsx, typescript, tailwind) para construir y para sembrar.
|
||||
RUN npm ci
|
||||
|
||||
# CLI de Prisma aislada, con sus dependencias propias resueltas en su propio
|
||||
# árbol. El punto de entrada del contenedor la necesita para aplicar el esquema
|
||||
# al arrancar, y copiar paquetes suelto por suelto no funciona: la CLI arrastra
|
||||
# dependencias transitivas (effect, entre otras) que no son evidentes.
|
||||
# La versión se lee de package.json, así que no puede desincronizarse.
|
||||
#
|
||||
# Después de instalarla se le quitan dos cosas que no va a usar nunca: los
|
||||
# mapas de código fuente y el motor de consultas. Esta CLI solo ejecuta
|
||||
# `db push`, que usa el motor de esquema; el motor de consultas que la
|
||||
# aplicación necesita es el que viene dentro de la salida standalone. Son unos
|
||||
# 40 MB menos en la imagen final.
|
||||
RUN npm install --no-save --no-audit --no-fund --prefix /prisma-cli \
|
||||
"prisma@$(node -p "require('/app/package.json').devDependencies.prisma")" \
|
||||
&& find /prisma-cli/node_modules -type f -name "*.map" -delete \
|
||||
&& rm -f /prisma-cli/node_modules/@prisma/engines/libquery_engine-*.so.node
|
||||
|
||||
|
||||
# --- etapa 2: construcción -----------------------------------------------------
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Valores por omisión iguales a .env.example: si nadie pasa --build-arg, la
|
||||
# imagen queda coherente en vez de quedar con la cadena vacía, que rompería
|
||||
# `new URL(SITIO_URL)` en el layout raíz.
|
||||
ARG NEXT_PUBLIC_SITE_URL="https://fspm-demo.urieljareth.org"
|
||||
ARG NEXT_PUBLIC_WHATSAPP="525540348395"
|
||||
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
||||
ENV NEXT_PUBLIC_WHATSAPP=${NEXT_PUBLIC_WHATSAPP}
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# `next build` y `prisma generate` exigen que DATABASE_URL exista, aunque no se
|
||||
# conecten a la base real. Este valor es solo de compilación: apunta a la base
|
||||
# de demostración que se hornea aquí mismo y nunca se usa en ejecución.
|
||||
ENV DATABASE_URL="file:/semilla/fspm.db"
|
||||
# Igual con el secreto de sesión: existe para que el build no falle. El
|
||||
# contenedor recibe el suyo por entorno.
|
||||
ENV AUTH_SECRET="secreto-de-compilacion-sin-valor-en-ejecucion"
|
||||
|
||||
RUN npx prisma generate
|
||||
|
||||
# --- base de demostración horneada en la imagen ---
|
||||
#
|
||||
# El seed es DESTRUCTIVO: `prisma/seed.ts` hace deleteMany() de todas las tablas
|
||||
# antes de reconstruir. Ejecutarlo dentro del contenedor de producción borraría
|
||||
# los leads capturados en cada reinicio. Por eso se ejecuta aquí, una sola vez,
|
||||
# contra una base que queda dentro de la imagen; el contenedor solo la copia al
|
||||
# volumen cuando el volumen está vacío. Ese es el motivo real por el que `tsx`,
|
||||
# que es dependencia de desarrollo, nunca se instala en la imagen final.
|
||||
RUN mkdir -p /semilla \
|
||||
&& npx prisma db push --skip-generate \
|
||||
&& if [ -f prisma/seed.ts ]; then \
|
||||
npx tsx prisma/seed.ts; \
|
||||
else \
|
||||
echo "AVISO: no existe prisma/seed.ts. La imagen queda con el esquema pero sin datos de demostración."; \
|
||||
fi
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# --- etapa 3: ejecución --------------------------------------------------------
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
# openssl y libc6-compat también aquí: el cliente de Prisma los carga en
|
||||
# ejecución, no solo al construir.
|
||||
RUN apk add --no-cache libc6-compat openssl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
ENV PORT=3000
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
# El aviso de "hay una versión nueva de Prisma" sale en cada arranque y solo
|
||||
# entrena a nadie a leer los registros. La versión se actualiza cuando el
|
||||
# proyecto lo decida, no cuando el contenedor lo sugiera.
|
||||
ENV PRISMA_HIDE_UPDATE_MESSAGE=true
|
||||
|
||||
# Ruta absoluta a propósito. Una relativa como "file:../data/fspm.db" se
|
||||
# resuelve contra la carpeta del esquema y es justo el tipo de detalle que
|
||||
# funciona en local y falla en el contenedor.
|
||||
ENV DATABASE_URL="file:/app/data/fspm.db"
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 --ingroup nodejs nextjs
|
||||
|
||||
# CLI de Prisma, en su propio árbol para que el punto de entrada pueda aplicar
|
||||
# el esquema al arrancar. Va en /app/prisma-cli, aparte del node_modules de la
|
||||
# aplicación: así no puede pisar el @prisma/client generado que trae la salida
|
||||
# standalone, que es el que la aplicación usa en ejecución.
|
||||
COPY --from=deps /prisma-cli/node_modules ./prisma-cli/node_modules
|
||||
|
||||
# El esquema tiene que estar presente en ejecución: `prisma db push` lo lee.
|
||||
COPY --from=build /app/prisma/schema.prisma ./prisma/schema.prisma
|
||||
|
||||
# Base de demostración horneada. Solo se copia al volumen si el volumen no
|
||||
# tiene una base todavía.
|
||||
COPY --from=build /semilla/fspm.db ./semilla/fspm.db
|
||||
|
||||
# Salida de Next. El standalone va al final para que su @prisma/client, que es
|
||||
# el que la aplicación usa, gane sobre cualquier otro.
|
||||
COPY --from=build /app/public ./public
|
||||
COPY --from=build /app/.next/standalone ./
|
||||
COPY --from=build /app/.next/static ./.next/static
|
||||
|
||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# El directorio de datos nace con dueño nextjs: si el volumen se crea vacío,
|
||||
# Docker hereda estos permisos y el proceso no root puede escribir la base.
|
||||
RUN mkdir -p /app/data \
|
||||
&& chown -R nextjs:nodejs /app/data /app/semilla /app/.next
|
||||
|
||||
USER nextjs
|
||||
|
||||
# Aquí vive el SQLite y TIENE que sobrevivir a los redespliegues. Si no se
|
||||
# monta, cada nueva versión de la imagen empieza con la base de demostración y
|
||||
# los leads capturados se pierden. Está explicado en docs/DESPLIEGUE.md.
|
||||
VOLUME ["/app/data"]
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# La sonda toca /api/salud, que a su vez consulta la base: así un volumen no
|
||||
# escribible se reporta como contenedor enfermo en vez de pasar por sano.
|
||||
# --start-period cubre el `prisma db push` del arranque más el boot de Next.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider "http://127.0.0.1:${PORT}/api/salud" || exit 1
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["node", "server.js"]
|
||||
Reference in New Issue
Block a user