Logo QuetzyQuetzy TFG

13. Anexos

13.1 Código relevante

El código fuente completo del proyecto está disponible en el repositorio público: https://github.com/Fraancoboss/Quetzy-ERP 

A continuación se incluyen tres fragmentos seleccionados por su valor pedagógico: representan patrones arquitectónicos centrales del sistema que justifican decisiones documentadas a lo largo de la memoria.

Máquina de estados de tickets — transitionPolicies

Archivo: src/features/tickets/model/ticket-state-machine.ts:144-202

Este fragmento define la totalidad de transiciones permitidas del pipeline de 14 estados. Cada entrada del Record mapea un estado origen a sus transiciones válidas, con guards funcionales opcionales que validan precondiciones sobre el snapshot del ticket. La implementación es pura y funcional: no depende de efectos secundarios ni de base de datos.

const transitionPolicies: Record<TicketState, TransitionPolicy[]> = { received: [ { to: "classified", guard: requireComplexity }, { to: "cancelled", requireReason: true }, ], classified: [ { to: "discovering" }, { to: "cancelled", requireReason: true }, ], discovering: [ { to: "ambiguous" }, { to: "cancelled", requireReason: true }, ], ambiguous: [ { to: "discovering", requireReason: true }, { to: "pending_client_review", guard: requireClarification }, { to: "cancelled", requireReason: true }, ], pending_client_review: [ { to: "designed" }, { to: "ambiguous", requireReason: true }, { to: "paused_awaiting_client" }, { to: "cancelled", requireReason: true }, ], designed: [ { to: "in_progress" }, { to: "discovering", requireReason: true }, { to: "ambiguous", requireReason: true }, { to: "cancelled", requireReason: true }, ], in_progress: [ { to: "pr_open", guard: requirePrUrl }, { to: "cancelled", requireReason: true }, ], pr_open: [ { to: "validated" }, { to: "in_progress", requireReason: true }, { to: "cancelled", requireReason: true }, ], validated: [ { to: "deployed", guard: requireDeployData }, { to: "cancelled", requireReason: true }, ], deployed: [ { to: "learned" }, { to: "cancelled", requireReason: true }, ], learned: [ { to: "closed", guard: requireCloseEvidence }, { to: "cancelled", requireReason: true }, ], closed: [], paused_awaiting_client: [ { to: "pending_client_review", requireReason: true }, { to: "ambiguous", requireReason: true }, { to: "cancelled", requireReason: true }, ], cancelled: [], };

Archivo: src/app/(auth)/actions.ts:16-48

Demuestra la solución adoptada ante la limitación del SDK de Insforge en server mode: los tokens se devuelven en el body JSON y la cookie se persiste manualmente. El comentario Decision/Reason/Alternative documenta la justificación para futuros mantenedores.

export async function signinAction( _prev: SigninResult, formData: FormData, ): Promise<SigninResult> { const raw = { email: formData.get("email"), password: formData.get("password"), }; const parsed = signinSchema.safeParse(raw); if (!parsed.success) { return { error: parsed.error.issues[0].message }; } const insforge = getInsforgeClient(); const { data, error } = await insforge.auth.signInWithPassword({ email: parsed.data.email, password: parsed.data.password, }); if (error || !data) { return { error: "Credenciales incorrectas." }; } // Decision: Plan B — manually persist session cookie. // Reason: isServerMode uses mobile flow (client_type=mobile) which returns // accessToken + refreshToken in the JSON body. The SDK does NOT set httpOnly // cookies in server mode. We write the cookie ourselves. await setSessionCookie({ accessToken: data.accessToken, refreshToken: data.refreshToken, }); return { redirectTo: "/" }; }

Patrón de compensación manual en sendMessage

Archivo: src/lib/repositories/insforge/insforge-chat-repository.ts:202-255

Ilustra cómo se resuelve la ausencia de transacciones multi-tabla en el SDK de Insforge. Si el INSERT de attachments falla tras un INSERT exitoso del mensaje, se ejecuta compensación: DELETE del mensaje huérfano y cleanup de archivos en storage.

async sendMessage(input: SendMessageInput): Promise<ChatMessage> { // 1. Insert message const { data: msgData, error: msgError } = await db() .from("chat_message") .insert({ channel_id: input.channelId, sender_email: input.senderEmail, content: input.content, reply_to_id: input.replyToId ?? null, type: input.type ?? "text", }) .select("*") .single(); throwIfError(msgError, "sendMessage:insertMessage"); const msgRow = msgData as ChatMessageRow; // 2. Insert file attachments if present (with compensation on failure) let attRows: ChatAttachmentRow[] = []; if (input.attachments && input.attachments.length > 0) { const attInserts = input.attachments.map((a) => ({ message_id: msgRow.id, file_name: a.fileName, file_type: a.fileType, file_size: a.fileSize, storage_key: a.storageKey, url: a.url, thumbnail_url: a.thumbnailUrl ?? null, })); const { data: attData, error: attError } = await db() .from("chat_attachment") .insert(attInserts) .select("*"); if (attError) { // Compensation: delete the orphaned message (CASCADE cleans up) await db().from("chat_message").delete().eq("id", msgRow.id); // Defensive cleanup: remove uploaded files from storage const bucket = getInsforgeClient().storage.from("chat-attachments"); for (const a of input.attachments) { if (a.storageKey) { try { await bucket.remove(a.storageKey); } catch { // Non-critical: orphaned files are tolerable } } } throw new RepositoryError( 500, `sendMessage:insertAttachments: ${attError.message}`, ); } attRows = (attData ?? []) as ChatAttachmentRow[]; } // ... (continua con project_attachments y update de channel.updated_at) }

13.2 Documentación adicional

Microsite TFG (tfg-site/)

Aplicación web estática construida con Nextra 4 (framework de documentación sobre Next.js) que presenta la memoria del TFG en formato navegable. Se encuentra en la carpeta tfg-site/ del repositorio. Incluye:

  • Navegación lateral por capítulos.
  • Renderizado de Markdown con soporte para diagramas Mermaid.
  • Diseño visual con efecto glass-morphism inspirado en la estética del ERP.
  • Despliegue independiente del ERP principal.

DESIGN.md — Sistema visual

Archivo DESIGN.md en la raíz del repositorio (367 líneas). Documenta el sistema de diseño del ERP inspirado en Linear:

  • Tokens de color (paleta oscura con acentos azules).
  • Tipografía y escala de espaciado.
  • Componentes base (botones, inputs, modales, toasts).
  • Convenciones CSS (CSS Modules + Custom Properties, sin Tailwind).
  • Modo oscuro como única opción (color-scheme: dark).

Este archivo actúa como fuente de verdad para las decisiones visuales y es referenciado por Claude Code durante la implementación de componentes para mantener coherencia estética.

Carpeta tfg/

Contiene todos los capítulos de la memoria en formato Markdown, versionados dentro del propio repositorio del proyecto. Esta decisión permite:

  • Historial de cambios de la memoria con el mismo flujo Git del código.
  • PRs específicas para capítulos (docs/tfg-cap-X).
  • Revisión por pares de la redacción técnica.
  • Generación del .docx final desde los .md como paso de build.

Diagramas

Todos los diagramas de la memoria están escritos en formato Mermaid embebido en los propios archivos Markdown (Cap 5 contiene 8 diagramas: casos de uso, secuencia, clases, ER, máquina de estados, arquitectura de despliegue, capas y red). Se renderizan directamente en GitHub y en el microsite. No se usan herramientas externas de diagramación.

13.3 Planificación temporal

Diagrama de Gantt (representación textual)

Fase 0: Investigación previa |========================| 23 enero 2026 ────────── 15 abril 2026 Fase 1: Bootstrap CI/CD + primer deploy producción |X| 16 abril 2026 Fase 2: Desarrollo intensivo (iteración vía PRs sobre producción) |================| 17 abril ──── 5 mayo 2026 Fase 3: Estabilización + onboarding Manuel + incidente BSI |==| 6-8 mayo 2026 Fase 4: Memoria TFG (en paralelo y luego dedicado) |=========| 5 mayo ── 17 mayo 2026 Fase 5: Defensa |X| Última semana mayo 2026

Desglose por fases

FaseDescripciónPeriodoHitos clave
0Investigación previa: flujos de trabajo con IA generativa, evaluación de tooling (Claude Code, SDKs BaaS), definición de convenciones Git, análisis de stacks, diseño del modelo de datos23 enero - 15 abril 2026Documento de convenciones Git, schema data_model.sql v1, selección stack definitivo
1Bootstrap: primer commit, CI/CD operativo, deploy a producción en VPS Hetzner antes de cualquier feature16 abril 2026Primer commit (Actualizar contrato de datos v1), pipeline GitHub Actions activo, dominio erp.quetzy.eu en línea
2Desarrollo intensivo: features core implementadas iterativamente con merge a main y deploy automático. Chat completo, tickets con máquina de estados, llamadas WebRTC, contexto, clientes, proyectos, horas, notificaciones, presencia17 abril - 5 mayo 2026M1-M6.5 completados, 10 features funcionales, 827 tests, 82 % coverage
3Estabilización: corrección de bugs críticos, onboarding de Manuel Grande Écija (PR #70), incidente seguridad BSI y corrección inmediata, apertura de 15 issues de feedback6-8 mayo 2026PR #70 de Manuel, reglas UFW corregidas, 15 issues triageadas informalmente
4Memoria TFG: redacción de los 13 capítulos, scaffold del microsite, capturas de producción5-17 mayo 2026Caps 1-9 completados (7-8 mayo), Caps 10-13 completados (10 mayo), microsite operativo
5Defensa del TFGÚltima semana mayo 2026Entrega 17 mayo, defensa oral

Nota metodológica sobre la concentración temporal. El proyecto se ejecuta en un modelo de dos fases claramente diferenciadas: cuatro meses de investigación y diseño seguidos de tres semanas de implementación intensiva. Este no es un accidente de planificación sino una consecuencia directa de la metodología: la fase de investigación previa define convenciones, contratos y modelo de datos con suficiente profundidad como para que la fase de implementación se ejecute sin bloqueos de diseño. Toda la iteración se realiza contra producción real (no existe staging), lo que fuerza calidad desde el primer deploy.

13.4 Manual de usuario

Acceso e inicio de sesión

El acceso al sistema se realiza a través del navegador en la URL erp.quetzy.eu. El sistema requiere autenticación con email y contraseña; no existe registro público. Las credenciales las proporciona el administrador del equipo.

  1. Abrir erp.quetzy.eu en un navegador compatible (Chromium >= 100, Firefox >= 100, Safari >= 16).
  2. Introducir email y contraseña en el formulario de login.
  3. Pulsar “Iniciar sesión” o Enter.
  4. Si las credenciales son correctas, el sistema redirige a la vista principal del ERP.
  5. Si son incorrectas, se muestra el mensaje «Credenciales incorrectas.» sin revelar la causa.

Vista general del shell ERP

Tras autenticarse, el usuario accede al shell principal del ERP. La cabecera (AppHeader) proporciona:

  • Navegación global: Chat, Tickets, Clientes, Proyectos, Horas, Contexto.
  • Indicador de notificaciones con contador de no leídas.
  • Botón de usuario con opción de cerrar sesión.

Chat: enviar mensajes, audio, adjuntos, reacciones y reply-to

Enviar mensaje de texto:

  1. Seleccionar un canal en la lista lateral.
  2. Escribir el mensaje en el input inferior.
  3. Pulsar Enter o el botón de enviar.

Responder a un mensaje (reply-to):

  1. Pasar el cursor sobre un mensaje.
  2. Pulsar el icono de respuesta.
  3. El input muestra una referencia al mensaje original.
  4. Escribir la respuesta y enviar.

Adjuntar imagen o PDF:

  • Arrastrar archivo al area de chat, o
  • Pulsar el icono de adjuntar, o
  • Pegar directamente con Ctrl+V (imágenes del portapapeles).
  • Máximo 10 MB por archivo, 5 archivos por mensaje.

Enviar mensaje de audio:

  1. Pulsar el icono de micrófono.
  2. Grabar el mensaje.
  3. Pulsar detener. El audio se envía automáticamente.

Reaccionar a un mensaje:

  1. Pasar el cursor sobre un mensaje.
  2. Pulsar el icono de reaccion.
  3. Seleccionar emoji. Toggle: pulsar de nuevo retira la reacción.

Llamadas: iniciar, unirse y compartir pantalla

Iniciar llamada:

  1. Dentro de un canal de chat, pulsar el icono de teléfono en la cabecera.
  2. El sistema conecta vía WebRTC. El micrófono se activa automáticamente.
  3. Los demas miembros del canal ven un banner de “llamada activa”.

Unirse a llamada existente:

  1. Pulsar el banner de “llamada activa” que aparece en el canal.
  2. El sistema conecta al room existente.

Compartir pantalla:

  1. Durante una llamada, pulsar el botón de compartir pantalla.
  2. Seleccionar la ventana o pantalla a compartir en el diálogo del navegador.
  3. Los demas participantes ven la pantalla compartida en un panel dedicado.

Finalizar:

  • Pulsar el botón rojo de colgar. Si es el último participante, la llamada se cierra automáticamente.

Tickets: crear y gestionar con state rail

Crear ticket:

  1. Ir a Tickets en la navegación.
  2. Pulsar “Nuevo ticket”.
  3. Rellenar: titulo, tipo, proyecto asociado.
  4. El ticket se crea en estado received.

Transicionar ticket:

  1. Abrir el detalle de un ticket.
  2. En el rail lateral se muestra el estado actual y los estados siguientes disponibles.
  3. Pulsar la transicion deseada.
  4. Si tiene guard (ej. requireComplexity), rellenar el campo obligatorio.
  5. Si requiere razón (requireReason), introducir justificación.
  6. Confirmar. El sistema registra la transición con timestamp y autor.

Clientes y proyectos

Gestionar clientes:

  1. Ir a Clientes. Se muestra la lista con nombre, tier y estado.
  2. Pulsar “Nuevo cliente” para crear.
  3. Pulsar un cliente para ver detalle con proyectos asociados.

Gestionar proyectos:

  1. Ir a Proyectos. Se muestra la lista con cliente asociado y metadatos.
  2. Pulsar “Nuevo proyecto” para crear (requiere seleccionar cliente).
  3. Al crear un proyecto se genera automáticamente un canal de chat vinculado.

Control horario con timer

Iniciar timer:

  1. Ir a Horas.
  2. Pulsar “Iniciar timer”.
  3. Seleccionar scope: cliente, proyecto y opcionalmente ticket.
  4. El timer comienza a contar. Se muestra en la cabecera del ERP.

Pausar / reanudar:

  • Pulsar el botón de pausa en el timer activo. Pulsar de nuevo para reanudar.

Detener y guardar:

  1. Pulsar el botón de detener.
  2. Opcionalmente añadir notas sobre el trabajo realizado.
  3. La entrada de tiempo se guarda con duración calculada.

Contexto técnico (context_items y raw_context)

Consultar context_items:

  1. Ir a Contexto. Se muestra el bucket con items filtrables por estado (propuesto, aprobado, obsoleto).

Aprobar / rechazar item propuesto:

  1. En un item con estado proposed, pulsar Aprobar o Rechazar.
  2. Si se rechaza, introducir razón de rechazo.

Ingestar raw_context:

  1. Pulsar “Añadir contexto crudo”.
  2. Pegar texto (notas de reunión, emails, documentación).
  3. Pulsar “Extraer” para invocar la skill de IA context_extractor.
  4. Revisar los items extraidos y aprobar/rechazar individualmente.

Notificaciones

  1. El contador en la cabecera muestra notificaciones no leidas.
  2. Pulsar el icono para abrir la lista.
  3. Marcar como leída individualmente o en bloque con «Marcar todas como leídas».

Cerrar sesión (logout)

  1. Pulsar el botón de usuario en la cabecera.
  2. Seleccionar «Cerrar sesión».
  3. El sistema limpia la presencia, destruye la cookie de sesión y redirige a /login.

13.5 Manual técnico

Despliegue desde cero en VPS nuevo

Resumen expandido del proceso documentado en Cap 9.1. Comandos exactos para reproducir el entorno.

1. Provisión del VPS:

# Hetzner Cloud: crear CX33 con Ubuntu LTS, clave SSH pública del operador ssh deploy@<IP_VPS>

2. Endurecimiento básico:

sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow 443/udp sudo ufw allow 50000:50100/udp sudo ufw deny 5432/tcp sudo ufw deny 5430/tcp sudo ufw enable

3. Instalación de Docker:

curl -fsSL https://get.docker.com | sh sudo usermod -aG docker deploy # Reconectar SSH para aplicar grupo

4. Clonación y configuración:

sudo mkdir -p /opt/quetzy sudo chown deploy:deploy /opt/quetzy git clone https://github.com/Fraancoboss/Quetzy-ERP.git /opt/quetzy/erp cd /opt/quetzy/erp

5. Crear .env.production:

nano .env.production # Contenido (valores reales, no commitear): # INSFORGE_URL=http://host.docker.internal:7130 # INSFORGE_ANON_KEY=<anon_key_del_dashboard> # INSFORGE_JWT_SECRET=<jwt_secret> # LIVEKIT_API_KEY=<api_key> # LIVEKIT_API_SECRET=<api_secret> # LIVEKIT_URL=wss://livekit.quetzy.eu # NEXT_PUBLIC_INSFORGE_URL=https://internal.quetzy.eu # NEXT_PUBLIC_INSFORGE_ANON_KEY=<anon_key> # NEXT_PUBLIC_LIVEKIT_URL=wss://livekit.quetzy.eu # CRON_SECRET=<secreto_alta_entropia>

6. Primer despliegue:

docker compose -f docker-compose.prod.yml up -d # Caddy provisiona certificados TLS automáticamente en el primer arranque curl --fail https://erp.quetzy.eu/api/health

Variables de entorno

VariableDescripciónDonde se usa
INSFORGE_URLURL interna del backend InsforgeServer-side (SDK singleton)
INSFORGE_ANON_KEYClave anónima para el SDKServer-side + client browser
INSFORGE_JWT_SECRETSecreto para verificar JWT con joseServer-side (session.ts)
LIVEKIT_API_KEYClave API de LiveKit serverServer-side (generación tokens)
LIVEKIT_API_SECRETSecreto API de LiveKit serverServer-side (firma JWT llamadas)
LIVEKIT_URLURL WebSocket del servidor LiveKitClient browser (conexión room)
NEXT_PUBLIC_INSFORGE_URLURL publica de Insforge (via Caddy)Client browser
NEXT_PUBLIC_INSFORGE_ANON_KEYAnon key públicaClient browser
NEXT_PUBLIC_LIVEKIT_URLURL pública de LiveKit (vía Caddy)Client browser
ERP_DATA_SOURCESelector: mock o insforgeFactory de repositorios
CRON_SECRETSecreto del endpoint cronAPI route /api/cron/notifications
ENABLE_TEST_HARNESSHabilita endpoints de test e2eMiddleware
LIVEKIT_CONFIGYAML en base64 de configuración LiveKitContenedor livekit

Comandos operativos

# Ver logs en tiempo real de todos los servicios docker compose -f docker-compose.prod.yml logs -f # Ver logs solo del ERP docker compose -f docker-compose.prod.yml logs -f erp # Reiniciar LiveKit (ej. tras cambio de config) docker compose -f docker-compose.prod.yml restart livekit # Reiniciar ERP sin afectar otros servicios docker compose -f docker-compose.prod.yml restart erp # Rebuild y redeploy del ERP docker compose -f docker-compose.prod.yml build erp && docker compose -f docker-compose.prod.yml up -d erp # Health check manual curl --fail https://erp.quetzy.eu/api/health # Ver estado de todos los contenedores docker compose -f docker-compose.prod.yml ps

Backup de base de datos

# Backup completo de PostgreSQL (ejecutar en el VPS) docker exec insforge-postgres-1 pg_dump -U postgres -d postgres --clean --if-exists > /opt/quetzy/backups/backup_$(date +%Y%m%d_%H%M%S).sql # Restaurar desde backup docker exec -i insforge-postgres-1 psql -U postgres -d postgres < /opt/quetzy/backups/backup_YYYYMMDD_HHMMSS.sql

Rollback de deploy

# 1. Identificar commit anterior funcional git log --oneline -5 # 2. Revertir al commit anterior git revert HEAD --no-edit git push origin main # 3. GitHub Actions desplegará automáticamente el revert # 4. Verificar health check tras deploy curl --fail --retry 5 --retry-delay 3 https://erp.quetzy.eu/api/health

Alternativa manual (sin esperar CI/CD):

ssh deploy@178.104.179.244 cd /opt/quetzy/erp git pull origin main docker compose -f docker-compose.prod.yml build erp docker compose -f docker-compose.prod.yml up -d erp

Acceso SSH y políticas

  • Autenticación exclusivamente por clave pública (contraseña deshabilitada).
  • Usuario de deploy: deploy con permisos sudo.
  • Clave SSH almacenada como GitHub Secret (SSH_PRIVATE_KEY) para CI/CD.
  • Acceso humano: clave personal del operador añadida a ~/.ssh/authorized_keys.

Renovación PAT GitHub

El token de acceso personal (PAT) fine-grained de GitHub tiene expiración configurable. Para renovar:

  1. GitHub Settings > Developer settings > Fine-grained personal access tokens.
  2. Regenerar con permisos repo:read (minimo necesario para deploy).
  3. Actualizar el secreto GH_PAT en el VPS si aplica.

Troubleshooting común

ProblemaDiagnósticoSolución
TLS no renuevadocker compose logs caddy buscar errores ACMEVerificar puertos 80/443 abiertos en UFW; Caddy necesita responder al challenge HTTP-01
Health check falla tras deploydocker compose logs erp --tail 50Verificar que .env.production tiene todas las vars; rebuild imagen si cambió package.json
LiveKit puerto bloqueadosudo ufw status + nc -zvu <IP> 50000 desde exteriorConfirmar regla 50000:50100/udp ALLOW en UFW
Insforge no alcanzable desde ERPdocker compose exec erp curl http://host.docker.internal:7130/healthVerificar que Insforge está corriendo fuera de Compose y el puerto 7130 está bind a host
WebRTC sin audio en red corporativaLogs LiveKit: no ICE candidatesRed con NAT estricto bloquea UDP. Fallback TCP 7881 o desplegar servidor TURN (deuda)
Base de datos llenadocker exec insforge-postgres-1 psql -U postgres -c "SELECT pg_size_pretty(pg_database_size('postgres'))"Limpieza de tablas de log, upgrade de disco en Hetzner

Cron jobs configurados

JobFrecuenciaEndpoint/ComandoPropósito
Notificaciones de VPSManual / cron VPScurl -H "x-cron-secret: $CRON_SECRET" https://erp.quetzy.eu/api/cron/notificationsGenerar alertas de renovación, mantenimiento

En v1 no hay cron automatizado configurado en el sistema operativo. La ejecución es manual o bajo demanda. La automatización con crontab del VPS queda como mejora post-TFG.

Estructura del repositorio (alto nivel)

ia-erp/ ├── src/ │ ├── app/ # Next.js App Router │ │ ├── (auth)/ # Login, logout (Server Actions) │ │ ├── (erp)/ # Shell principal + páginas protegidas │ │ └── api/ # Route Handlers REST (44 endpoints) │ ├── features/ # Módulos por dominio funcional │ │ ├── calls/ # WebRTC / LiveKit │ │ ├── chat/ # Mensajería en tiempo real │ │ ├── clients/ # Gestión de clientes │ │ ├── context/ # context_items + raw_context │ │ ├── erp/ # Shell, header, componentes compartidos │ │ ├── hours/ # Control horario │ │ ├── notifications/ # Notificaciones internas │ │ ├── presence/ # Presencia online │ │ ├── projects/ # Gestión de proyectos │ │ └── tickets/ # Máquina de estados + pipeline │ ├── lib/ # Capas transversales │ │ ├── ai/ # AIProvider + Skills │ │ ├── auth/ # Session, JWT, Insforge client │ │ ├── contract/ # 18 schemas Zod │ │ └── repositories/ # Interfaces + implementación Insforge │ └── mocks/ # Implementacion mock + seeds ├── e2e/ # Tests Playwright (5 specs) ├── tfg/ # Memoria TFG (este documento) ├── tfg-site/ # Microsite documentación (Nextra 4) ├── docker-compose.prod.yml # Orquestación producción ├── Caddyfile # Configuracion reverse proxy ├── DESIGN.md # Sistema visual (367 líneas) ├── data_model.sql # Schema SQL (20 tablas) └── .github/workflows/ # CI/CD GitHub Actions