Logo QuetzyQuetzy TFG

5. Diseño del Proyecto

STATUS: borrador (v1) Estimación: 8-10 páginas

5.1 Diseño funcional

Casos de uso

El siguiente diagrama identifica los actores reales del sistema y los casos de uso principales, agrupados por dominio funcional. Los actores se derivan de los perfiles definidos en 3.2 y del análisis del código fuente.

Figura 5.1. Diagrama de casos de uso del sistema. Los actores Consultor y PM comparten la mayor parte de las acciones en equipos pequeños; la separación es lógica, no de control de acceso (ver 3.6, limitación sobre RBAC).

CU-IDActor principalDescripciónRF cubierto
CU-01ConsultorEnviar mensaje de texto/audio con adjuntos (imágenes, PDF, Ctrl+V) y reply-toRF-03, RF-04, RF-05
CU-02ConsultorReaccionar a un mensaje con emoji (toggle)RF-06
CU-03Consultor / SistemaIniciar o unirse a llamada WebRTC con audio y screen share; webhook actualiza estadoRF-08
CU-04ConsultorCrear ticket asociado a proyecto, asignar tipo y prioridadRF-09, RF-10
CU-05PMTransicionar un ticket entre los 14 estados, satisfaciendo guards de la máquina de estadosRF-09
CU-06PMCRUD de clientes y proyectos con metadatos de infraestructuraRF-10
CU-07ConsultorIniciar, pausar, reanudar y detener timer asociado a cliente/proyecto/ticketRF-11
CU-08PMCrear, aprobar, rechazar, revalidar y marcar obsoleto un context_itemRF-12
CU-09PM / IAIngestar raw_context y ejecutar extracción asistida por IA vía Skill context_extractorRF-12
CU-10Consultor / SistemaConsultar notificaciones, marcar como leídas; el sistema genera alertas de VPSRF-13

Historias de usuario

Las historias se centran en flujos diferenciales del sistema, no en CRUD genérico:

  1. Como consultor, quiero pegar una imagen directamente con Ctrl+V en el chat para no tener que buscar el archivo en el explorador, adjuntarlo manualmente y perder el hilo de la conversación.

  2. Como PM, quiero que la máquina de estados de tickets rechace transiciones inválidas con un mensaje claro (ej: “No se puede abrir PR sin pr_url”) para no avanzar un ticket a un estado inconsistente.

  3. Como consultor, quiero iniciar una llamada de audio desde el propio canal de chat y que mis compañeros vean un banner de “llamada activa” en tiempo real para no tener que saltar a Google Meet.

  4. Como PM, quiero aprobar un context_item propuesto por la IA y que quede enlazado al proyecto correspondiente, para que futuros prompts de IA consuman solo contexto validado por humanos.

  5. Como consultor, quiero que al iniciar un timer de horas quede asociado automáticamente al cliente, proyecto y ticket en el que estoy trabajando, para no tener que rellenar campos manualmente.

  6. Como PM, quiero ingestar texto crudo (notas de reunión, emails) como raw_context y que la IA extraiga context_items estructurados que yo pueda revisar antes de aprobar, para reducir el tiempo de documentación sin perder la validación humana.

  7. Como consultor, quiero recibir notificaciones internas cuando se acerca la fecha de renovación del VPS de un cliente, para evitar cortes de servicio imprevistos.

Diagramas funcionales

El siguiente diagrama muestra el flujo end-to-end de envío de un mensaje con adjuntos y entrega en tiempo real, extraído de la implementación en insforge-chat-repository.ts y los triggers SQL de data_model.sql.

Figura 5.2. Flujo de envío de mensaje con patrón de compensación manual. El trigger SQL notify_chat_message() publica el evento sin incluir sub-entidades (attachments, reactions); el cliente receptor hace refetch del mensaje completo para obtener los datos enriquecidos.

5.2 Diseño técnico (DAM)

Arquitectura del sistema

El siguiente diagrama refleja la arquitectura de despliegue real, extraída de docker-compose.prod.yml y Caddyfile.

Figura 5.3. Arquitectura de despliegue del sistema. Caddy actúa como punto de entrada único con TLS automático. Los tres servicios del docker-compose.prod.yml (erp, livekit, caddy) se comunican por la red Docker quetzy-net. Insforge corre fuera de Compose pero es alcanzable vía host.docker.internal.


El siguiente diagrama muestra la arquitectura de capas interna del ERP, extraída de la estructura real de src/.

Figura 5.4. Arquitectura de capas del ERP. La separación entre interfaces de repositorio e implementaciones permite el patrón de doble implementación (mock + producción) que cubre el RNF-06 (testabilidad). Los contratos Zod actúan como barrera de validación compartida entre cliente y servidor (RNF-04). La estructura por features (src/features/) agrupa cada dominio funcional (chat, calls, tickets, clients, projects, context, hours, notifications, presence) con sus componentes, contextos y hooks.

Diagrama de clases

Capa Repository

El sistema define dos interfaces de repositorio separadas por dominio: ErpRepository para la gestión operativa y ChatRepository para la comunicación en tiempo real. Ambas siguen el patrón de doble implementación (ver 4.5 y OE5).

Figura 5.5. Diagrama de clases de la capa Repository. ChatRepository (10 métodos) se separó de ErpRepository (~40 métodos) porque el chat es workspace-scoped y tiene patrones de acceso distintos (realtime, sin client_id). Se muestran los métodos más representativos de cada interfaz; ErpRepository tiene aproximadamente 40 métodos en total cubriendo clientes, proyectos, tickets, contexto, horas y notificaciones.

Capa AI

Figura 5.6. Diagrama de clases de la capa AI. AIProvider abstrae el proveedor de LLM (actualmente Gemini; NoneProvider para entornos sin IA configurada). Skill<TInput, TOutput> es el patrón genérico para skills de IA: construye el prompt, parsea la salida JSON y valida con reglas de negocio. ContextExtractor es la única skill implementada en v1 (ver 4.3 sobre la decisión de no usar LangChain).

Diagrama de secuencia

Login

Extraído de src/app/(auth)/actions.ts y src/lib/auth/session.ts.

Figura 5.7. Secuencia de login. El SDK de Insforge en server mode devuelve tokens en el body JSON (no establece cookies automáticamente), por lo que la cookie se persiste manualmente. Esta decisión está documentada en el código como “Plan B” por las limitaciones del SDK en server mode (client_type=mobile).

Llamada WebRTC completa

Extraído de src/app/api/calls/start/route.ts, src/app/api/calls/webhook/route.ts y src/features/calls/lib/livekit-room.ts.

Figura 5.8. Secuencia completa de una llamada WebRTC. El endpoint POST /api/calls/start es idempotente: si ya existe una llamada activa en el canal, devuelve la existente en vez de crear una nueva. La suscripción a tracks remotos se registra antes de connect() para evitar el race condition de tracks no recibidos, y un catch-up loop fuerza la suscripción a tracks ya publicados por peers que estaban antes en la room (livekit-room.ts:68-74).

Modelo entidad-relación

Diagrama ER simplificado de las 22 tablas del sistema, agrupadas por dominio funcional. Extraído de data_model.sql (20 tablas) más time_entry y notification (creadas directamente en Insforge, ver 3.6 limitaciones).

Figura 5.9. Modelo entidad-relación simplificado (22 tablas). Las tablas time_entry y notification no aparecen en data_model.sql; fueron creadas directamente en la consola de Insforge y están pendientes de migración como deuda explícita. Las relaciones CASCADE solo se aplican en sub-entidades de chat (chat_attachment, chat_reaction, chat_project_attachment) y en chat_call; el resto usa RESTRICT por defecto.

Diseño de la base de datos

Esta sección documenta las decisiones de diseño que no son evidentes en el diagrama ER.

UUIDs como PK universales. Todas las tablas usan uuid PRIMARY KEY DEFAULT gen_random_uuid(). Esto permite la generación de IDs sin coordinación centralizada (relevante para futura inserción desde múltiples servicios) y elimina el riesgo de colisiones de IDs secuenciales entre entornos (dev, staging, producción).

timestamptz para auditoría temporal. Todas las columnas temporales usan timestamptz (timestamp with time zone) en UTC. Esto garantiza que la interpretación temporal sea consistente independientemente de la zona horaria del servidor o del cliente.

ON DELETE CASCADE selectivo. Solo se aplica en tablas hijas cuya existencia no tiene sentido sin el padre: chat_attachment, chat_reaction, chat_project_attachment (dependen de chat_message) y chat_call (depende de chat_channel). El resto del sistema usa ON DELETE RESTRICT por defecto para prevenir borrados en cascada accidentales en datos de negocio (tickets, context_items).

RLS restringido a erp_presence. Row Level Security solo se aplica a la tabla erp_presence, donde cada usuario solo puede hacer UPSERT de su propio registro. El resto de tablas no tienen RLS porque en v1 no hay sistema de roles granular (ver 3.6): todos los usuarios autenticados tienen acceso completo. Aplicar RLS a todas las tablas requeriría un modelo de permisos completo que queda fuera del alcance.

Triggers SQL para realtime. Los triggers trg_chat_message_realtime y trg_chat_reaction_realtime ejecutan realtime.publish() en cada INSERT/DELETE, propagando eventos por WebSocket sin polling. El payload del trigger de mensajes no incluye sub-entidades (attachments, reactions); el cliente receptor hace refetch del mensaje completo. Los eventos de llamada (CALL_STARTED, CALL_ENDED) se publican desde las API routes vía db().rpc('publish_realtime_event') en vez de triggers, porque requieren un payload controlado desde TypeScript.

Compensación manual en sendMessage. Al no disponer de transacciones multi-tabla en el SDK de Insforge, sendMessage implementa compensación manual: si el INSERT de attachments falla tras un INSERT exitoso del mensaje, se ejecuta un DELETE del mensaje huérfano y un storage.remove() de los archivos subidos. Este patrón está implementado en insforge-chat-repository.ts:237-255.

Deduplicación de context_items. Dos índices UNIQUE parciales (idx_context_dedup_project y idx_context_dedup_client_general) previenen la inserción de items con el mismo content_hash dentro del mismo scope (proyecto o cliente general), evitando duplicados exactos sin requerir lógica aplicativa.

Flujos de interacción

Máquina de estados de tickets

Extraída de src/features/tickets/model/ticket-state-machine.ts, constante transitionPolicies. Es el diagrama central del sistema: modela los 14 estados del ciclo de vida de un ticket de consultoría con sus transiciones permitidas y guards funcionales.

Figura 5.10. Máquina de estados de tickets (14 estados, extraída de ticket-state-machine.ts). Las transiciones con guard: ejecutan funciones puras que validan precondiciones sobre el snapshot del ticket:

  • requireComplexity: el ticket debe tener complejidad asignada (simple/medium/complex).
  • requireClarification: requiere clarified_input y client_summary rellenos.
  • requirePrUrl: requiere pr_url no nulo.
  • requireDeployData: requiere deploy_ref y deployed_at.
  • requireCloseEvidence: requiere al menos un context_item en estado valid o un close_override_note.

Las transiciones con requireReason exigen un texto justificativo obligatorio. Los estados closed y cancelled son terminales (sin transiciones de salida). Los 4 gates humanos obligatorios del pipeline son: received a classified (clasificación), pending_client_review a designed (aprobación de diseño), pr_open a validated (validación de PR) y learned a closed (cierre con evidencia).

Diseño de interfaz (mockups, wireframes)

En lugar de mockups inventados, las capturas de la interfaz de producción se incluirán como imágenes en la versión final de la memoria. Las siguientes vistas clave están pendientes de captura:

VistaRutaElementos destacados
Login/loginFormulario email/password, mensajes de error Zod
Chat con llamada activa/chatLista de canales, mensajes con adjuntos, banner de llamada, controles audio/screen share
Detalle de ticket con state rail/tickets/[ticketId]Rail visual de los 14 estados, formulario de transición con guard feedback
Context bucket/contextLista filtrable de context_items, badges de status, acciones de revisión
Gestión de clientes/clientsLista con tier badges, detalle con proyectos asociados
Gestión de proyectos/projectsLista con metadatos de infra (repo_url, vps_host), tickets asociados
Dashboard de horas/hoursTimer activo, historial de entradas, filtros por cliente/proyecto
Notificaciones/notificationsLista con contador de no leídas, acciones de marcar leído

Nota para la versión final: sustituir esta tabla por capturas de pantalla reales de erp.quetzy.eu en el directorio capturas/.

5.3 Diseño técnico (ASIR) — Aspectos de red e infraestructura

Nota: Quetzy es un proyecto DAM. Esta sección documenta brevemente las decisiones de red e infraestructura que comparten responsabilidad con el perfil ASIR.

Arquitectura de red

Figura 5.11. Arquitectura de red simplificada. Todo el tráfico HTTPS entra por Caddy. El tráfico WebRTC (audio, screen share) usa UDP directo contra LiveKit en los puertos 50000-50100, con TCP 7881 como fallback para redes que bloquean UDP.

Infraestructura del sistema

ServicioImagen/RuntimePuerto internoPuerto expuestoProtocoloPropósito
caddycaddy:2-alpine80, 44380, 443 (TCP+UDP)HTTPS / HTTP/3Reverse proxy + TLS automático
erpquetzy-erp:latest (Next.js standalone)3000— (solo vía Caddy)HTTPAplicación ERP
livekitlivekit/livekit-server:v1.11.078807881/tcp, 50000-50100/udpWebSocket + WebRTCSeñalización y media server
InsforgeStack Insforge (fuera de Compose)7130— (solo vía Caddy)HTTPAuth + DB + Realtime + Storage
PostgreSQLPostgreSQL 15.15 (embebido en Insforge)5432— (bloqueado por UFW)TCPBase de datos

Topologías

Topología cliente-servidor centralizada con un único punto de servicio (VPS). No hay clusterizado, balanceo de carga ni réplicas en v1. La simplicidad es intencionada para un equipo de 2-20 personas (ver RNF-02).

Deuda futura: alta disponibilidad queda explícitamente fuera del alcance v1 (ver 3.6).

Protocolos y servicios

  • HTTPS / HTTP/3 vía Caddy con TLS automático Let’s Encrypt (protocolo ACME). HTTP/3 habilitado por el puerto 443/udp expuesto en Docker.
  • WebSocket sobre TLS para realtime de Insforge (mensajes, reacciones, typing, eventos de llamada).
  • WebRTC con UDP directo para LiveKit (puertos 50000-50100). Protocolo DTLS-SRTP para cifrado de media.
  • TCP fallback en LiveKit puerto 7881 para redes que bloquean UDP (NAT restrictivo, firewalls corporativos).
  • SSH para deploy automático vía GitHub Actions (puerto 22, autenticación por clave pública).

Políticas de seguridad

  • UFW activo en VPS: solo puertos 22 (SSH), 80 (HTTP), 443 (HTTPS), 50000-50100/udp (WebRTC) abiertos al público.
  • PostgreSQL bloqueado explícitamente: reglas UFW para DENY en puertos 5432 y 5430. Motivado por un incidente de seguridad real del 6 de mayo de 2026: el BSI alemán (Bundesamt fur Sicherheit in der Informationstechnik) reportó el puerto PostgreSQL expuesto públicamente, que fue corregido de inmediato.
  • Cookie httpOnly + secure + sameSite=lax para la sesión de usuario. Impide acceso desde JavaScript del cliente y limita el envío en contextos cross-site.
  • Secrets fuera del repositorio: .env.production en el VPS + GitHub Secrets para CI/CD. Ningún secreto en el código versionado.
  • TLS gestionado automáticamente por Caddy para los tres dominios (erp.quetzy.eu, internal.quetzy.eu, livekit.quetzy.eu).
  • RLS aplicado en erp_presence: cada usuario solo puede modificar su propio registro de presencia.

Plan de direccionamiento

  • IP pública: IPv4 178.104.179.244 + IPv6 asignada por Hetzner.
  • Red Docker interna: quetzy-net en modo bridge. Subnet asignada por Docker Compose por defecto (no se ha customizado el rango CIDR).
  • DNS: registros A apuntando los tres subdominios (erp, internal, livekit) a la IP del VPS.

Estrategias de alta disponibilidad

Fuera del alcance v1. Documentado como deuda futura:

  • Snapshots diarios vía panel de Hetzner Cloud (configurados).
  • Backup de BD vía pg_dump diario (no automatizado en v1; ejecución manual periódica).
  • Migración futura posible a multi-VPS con load balancer cuando el equipo o la carga lo justifiquen. La arquitectura Docker Compose facilita esta transición.