Files
flux-srl/prisma/schema.prisma
T
davidherran 3a94e7c003 feat(security+ai): security hardening + FluxAI conversation analytics
Security (critical):
- SESSION_SECRET fail-fast: refuse to boot without a 32+ char secret
  (src/lib/session.ts, src/app/actions/clientAuth.ts)
- Rate limit with pluggable backend: in-memory by default, auto-promotes
  to Upstash Redis when REDIS_URL is set (src/lib/rateLimit.ts)
- CSRF (double-submit HMAC) + Zod validation on /api/consultation;
  new /api/csrf endpoint mints tokens (src/lib/csrf.ts)
- escapeHtml + safeMailto helpers; consultation email template now
  fully escapes user-controlled fields (src/lib/escapeHtml.ts)
- Magic-byte validation for /api/public-upload — rejects HTML/JS
  payloads renamed to .png/.mp4 (src/lib/fileType.ts)
- Nginx: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
  Permissions-Policy + 5r/m upload zone for /api/public-upload and
  /api/assets (nginx/conf.d/flux.conf)

Quality:
- Delete GlobalOperations_old.tsx dead code (310 LOC)
- NavBar: replace 2s session polling with CustomEvent("flux:session-
  changed") + visibilitychange listener (no more interval leaks)
- Type-safe CMS shapes via src/types/cms.ts (replaces any[] in
  ApplicationsDashboard + GlobalOperations)
- /api/health now pings Postgres; docker-compose healthcheck added
- Structured JSON logger (src/lib/logger.ts) — drop-in replacement
  for console.error across API routes
- Prisma indices on isActive/category/nodeType filters

FluxAI persistence + analytics:
- New models AiConversation + AiEvent with funnel stage detection
  (DISCOVERY -> QUALIFY -> RECOMMEND -> HANDOFF) and OperationsSignal
  back-ref so converted chats link to their consultation ticket
- /api/chat persists every user msg, ai msg, tool call, tool result;
  IP is sha256-hashed with SESSION_SECRET salt; promptCacheKey wired
  for when @ai-sdk/openai lands the feature
- New HQ dashboard at /hq-command/dashboard/conversations: 4 KPIs
  (total, conversion rate, avg messages, avg tools), funnel + industry
  breakdowns, last-50 table, per-id transcript with tool timeline
- SilentObserver sends sessionId/locale/pageUrl in transport body so
  the route can stitch messages into the same conversation
- src/lib/aiSessionId.ts: localStorage UUID with sessionStorage +
  in-memory fallbacks for privacy mode
- Golden tests via node --test (13 cases, no new deps);
  npm run test:ai

Migration:
- prisma/migrations/20260526180000_add_indexes_and_ai_telemetry —
  additive only, IF NOT EXISTS guards, safe for migrate deploy

env template hardened: SESSION_SECRET documented as required + how
to generate; REDIS_URL/REDIS_TOKEN documented as opt-in for multi-
instance deploys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:10:19 -05:00

383 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
//url = env("DATABASE_URL")
}
// ------------------------------------------------------
// 1. BÓVEDA DE SEGURIDAD (Usuarios del CMS)
// ------------------------------------------------------
model AdminUser {
id String @id @default(cuid())
username String @unique
email String? // 🔥 NUEVO CAMPO: Correo del administrador
passwordHash String
twoFactorSecret String?
is2FAEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 2. EL GLOBO HOLOGRÁFICO (Nodos y Casos de Estudio Profundos)
// ------------------------------------------------------
model GlobalNode {
id String @id @default(cuid())
title String // Ej: "Toray Advanced Textiles"
location String
// Ej: "Tokyo, Japan"
lat Float // Ej: 35.6895
lon Float // Ej: 139.6917
// Taxonomía
nodeType String @default("installation") // "installation", "event", "hq"
application String // Ej: "textile-drying", "hq", "event"
stats String // Ej: "2,400 kg/h throughput"
isActive Boolean @default(true) // Permite ocultar un nodo sin borrarlo
// 📖 GEO-CHRONICLE (THE STORY)
projectOverview String? // El Artículo completo / Resumen del evento (Markdown)
energySavings String? // Métrica (Ej: "-45% vs Conventional" o "Stand 4B")
eventDate DateTime?// Fecha para controlar si el evento es pasado o futuro
// 🔥 NUEVOS CAMPOS FASE 1: MULTIMEDIA Y DATASHEET ESPECÍFICO 🔥
mediaFileName String? // Imagen de Portada principal
galleryJson String? @default("[]") // Array de imágenes extra
videosJson String? @default("[]") // Links a videos reales
specificDatasheetJson String? @default("[]") // Ficha técnica de ESTA máquina
model3DPath String? // Ruta al archivo GLB/USDZ
rendersJson String? @default("[]") // Renders 3D fotorrealistas
model3DDimsJson String? // Dimensiones físicas AR: { w, h, d, unit, weight }
// 🌍 MOTOR DE TRADUCCIONES
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([nodeType])
@@index([nodeType, isActive])
}
// ------------------------------------------------------
// 3. LA BASE DE CONOCIMIENTO (Páginas de Aplicaciones)
// ------------------------------------------------------
model Application {
id String @id @default(cuid())
slug String @unique // Ej: "textile-drying" (Debe coincidir con la URL)
title String
subtitle String
category String
// 🔥 NUEVO: La descripción corta para las tarjetas de la página principal
shortDescription String @default("Learn more about this FLUX RF technology application.")
heroDescription String // Recibirá MARKDOWN para la teoría científica general
// JSONs para estructuras complejas
sectionsJson String
advantagesJson String
datasheetJson String
// Métricas Rápidas para el Dashboard
dashboardMetricsJson String? @default("[]")
isActive Boolean @default(true) // 🔥 NUEVO: Para poder ocultarlas
// 🌍 MOTOR DE TRADUCCIONES
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([category])
}
// ------------------------------------------------------
// 4. NUESTRA HISTORIA (Línea de tiempo de la empresa)
// ------------------------------------------------------
model TimelineEvent {
id String @id @default(cuid())
year String // Ej: "1978" o "1990s"
title String
description String
order Int @default(0) // Para ordenar cronológicamente
isActive Boolean @default(true)
// 🌍 MOTOR DE TRADUCCIONES
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 5. INSIDE FLUX (Motor de Noticias y Detrás de Cámaras)
// ------------------------------------------------------
model NewsArticle {
id String @id @default(cuid())
slug String @unique
title String
excerpt String // Resumen corto para la tarjeta
content String // El artículo completo (Markdown)
coverImage String? // Ej: "team-meeting.jpg"
category String @default("News")
publishedAt DateTime @default(now())
isActive Boolean @default(true)
// Editor avanzado
order Int @default(0) // Para ordenar las noticias
galleryJson String? @default("[]") // Galería de imágenes extra
linkedinUrl String? // Enlace oficial para LinkedIn
// 🌍 MOTOR DE TRADUCCIONES
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([isActive, publishedAt(sort: Desc)])
}
// ------------------------------------------------------
// 6. OUR HERITAGE (La Historia Profunda de Patrizio)
// ------------------------------------------------------
model HeritageSection {
id String @id @default(cuid())
type String @default("text") // "text", "image", "video"
title String?
content String? // Párrafos de la historia
mediaUrl String? // Ej: "patrizio-1980.jpg" o enlace de YouTube
order Int @default(0) // Para ordenar cómo se lee la página
// 🌍 MOTOR DE TRADUCCIONES
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 7. COMPONENT MATRIX (Catálogo de Repuestos)
// ------------------------------------------------------
model SparePart {
id String @id @default(cuid())
sku String @unique // Identificador único / Referencia (Ej: "FLX-GEN-001")
title String // Nombre de la pieza en Inglés
description String // Descripción técnica / Función (Markdown)
// Multimedia & Ficha Técnica
mediaJson String? @default("[]") // Imágenes, videos, renders 3D
specsJson String? @default("[]") // Array de métricas [{label: "Voltage", value: "24V"}]
// Estrategia de Ventas
price Float? // Precio (Opcional)
showPrice Boolean @default(false) // Interruptor: true = mostrar precio, false = "Request Quote"
isActive Boolean @default(true) // Para ocultar repuestos descontinuados
// 🌍 MOTOR DE TRADUCCIONES (Integración con aiTranslator.ts)
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
}
// ------------------------------------------------------
// 8. OPERATIONS INBOX (Signal Hub - Mesa de Ayuda y Órdenes)
// ------------------------------------------------------
model OperationsSignal {
id String @id @default(cuid())
ticketId String @unique
ticketNumber Int @default(autoincrement()) // Sequential for analytics
type String // "ORDER", "DIAGNOSTIC", "CONSULTATION"
status String @default("PENDING") // "PENDING", "REVIEWING", "RESOLVED"
// Client data
clientName String
clientEmail String
clientCompany String
clientPhone String?
message String?
// Payloads
cartPayload String? @default("[]")
attachedFiles String? @default("[]")
aiAnalysis String?
// Email delivery tracking
emailSentTo String? // Comma-separated list of emails that received notification
emailSentAt DateTime? // When the email was dispatched
emailError String? // Error message if email failed
// 🔥 NUEVO: Relación opcional con el Cliente Registrado (Para el futuro CRM)
clientId String?
client ClientUser? @relation(fields: [clientId], references: [id])
// FluxAI telemetry back-ref: which AI conversations converted into this ticket.
conversations AiConversation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([type])
@@index([status])
@@index([createdAt(sort: Desc)])
}
// ------------------------------------------------------
// 9. RUTAS DE NOTIFICACIÓN (Gestión de Emails)
// ------------------------------------------------------
model NotificationRoute {
id String @id @default(cuid())
routeType String @unique // Ej: "ORDER", "DIAGNOSTIC", "CONSULTATION"
emails String // Correos separados por coma (Ej: "sales@fluxsrl.com, tech@fluxsrl.com")
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 10. PAGE CONTENT (Metadata y Textos de Páginas)
// ------------------------------------------------------
model PageContent {
id String @id @default(cuid())
slug String @unique // Identificador de la página (Ej: "parts-catalog")
title String
subtitle String?
description String?
// 🌍 MOTOR DE TRADUCCIONES
translationsJson String? @default("{}")
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 11. HERO REEL (Carrusel principal del Home)
// ------------------------------------------------------
// Manages the rotating images/videos shown in the home hero section.
// Replaces the previous filesystem-scan approach (fs.readdirSync of
// /public/footage/main) with full CMS control: ordering, on/off toggle,
// focal-point per slide for proper responsive cropping on mobile/tablet,
// and per-slide alt text for SEO.
model HeroSlide {
id String @id @default(cuid())
mediaUrl String // Public path, e.g. "/footage/main/01_tifas.png"
mediaType String @default("image") // "image" | "video"
altText String? // For accessibility + SEO; falls back to title if null
order Int @default(0)
isActive Boolean @default(true)
// Focal point for object-position on mobile/tablet crops (01 range).
// Lets the editor pick "what should stay visible when the image is cropped".
focalPointX Float @default(0.5)
focalPointY Float @default(0.5)
// Optional per-slide caption that overrides the global Hero text.
// Stored as JSON keyed by locale: {"en":{"title":"...","subtitle":"..."}}
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 12. SITE SETTINGS (Favicon, Footer, Branding global)
// ------------------------------------------------------
// Single-row pattern (key-value) for global site config that doesn't
// fit any other model: favicon, logos, footer, OG image, social links.
model SiteSetting {
id String @id @default(cuid())
key String @unique // e.g. "favicon", "footer", "logo", "og_image", "hero_text"
valueJson String @default("{}") // Flexible JSON payload per key
// 🌍 Translation engine (used for things like footer link labels)
translationsJson String? @default("{}")
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 13b. FLUXAI TELEMETRY (Conversaciones + eventos del chat IA)
// ------------------------------------------------------
// Persiste cada conversación con FluxAI para análisis de funnel B2B.
// Una conversación se identifica por sessionId (UUID generado en cliente,
// persistido en localStorage). Los eventos individuales (mensajes,
// tool calls, errores) viven en AiEvent.
model AiConversation {
id String @id @default(cuid())
sessionId String @unique
visitorIp String? // sha256(ip + SESSION_SECRET) — pseudonymous
userAgent String?
locale String? // "it","en","es","fr","de"
pageUrl String? // entry page
industryLabel String? // SPIN-detected: "textile","food", etc.
funnelStage String @default("DISCOVERY") // DISCOVERY|QUALIFY|RECOMMEND|HANDOFF
outcome String @default("OPEN") // OPEN|CONSULTATION|ABANDONED
messageCount Int @default(0)
toolCallCount Int @default(0)
estimatedSavingsPercent Float?
productionVolume String?
signalId String?
signal OperationsSignal? @relation(fields: [signalId], references: [id])
startedAt DateTime @default(now())
lastMessageAt DateTime @default(now())
closedAt DateTime?
events AiEvent[]
@@index([funnelStage])
@@index([outcome])
@@index([startedAt(sort: Desc)])
@@index([industryLabel])
}
model AiEvent {
id String @id @default(cuid())
conversationId String
conversation AiConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
type String // "user_msg" | "ai_msg" | "tool_call" | "tool_result" | "error"
payloadJson String // truncated to 8KB at write time
toolName String?
latencyMs Int?
tokensIn Int?
tokensOut Int?
cachedTokens Int? // populated when OpenAI returns cached_tokens
createdAt DateTime @default(now())
@@index([conversationId, createdAt])
@@index([type])
@@index([toolName])
}
// ------------------------------------------------------
// 13. CLIENT PORTAL (Usuarios B2B Aprobados)
// ------------------------------------------------------
model ClientUser {
id String @id @default(cuid())
email String @unique
passwordHash String
fullName String
companyName String
phone String?
// Control de Acceso
isApproved Boolean @default(false) // Requiere aprobación del Admin
// Historial de Compras/Tickets
signals OperationsSignal[]
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}