Logo QuetzyQuetzy TFG

13. Anexos

STATUS: borrador (v1) Estimacion: 4-6 paginas

13.1 Codigo relevante

El codigo fuente completo del proyecto esta disponible en el repositorio publico: https://github.com/Fraancoboss/Quetzy-ERP 

A continuacion se incluyen tres fragmentos seleccionados por su valor pedagogico: representan patrones arquitectonicos centrales del sistema que justifican decisiones documentadas a lo largo de la memoria.

Maquina 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 validas, con guards funcionales opcionales que validan precondiciones sobre el snapshot del ticket. La implementacion 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 solucion adoptada ante la limitacion 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 justificacion 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: "/" }; }

Patron de compensacion manual en sendMessage

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

Ilustra como 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 compensacion: DELETE del mensaje huerfano 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 Documentacion adicional

Microsite TFG (tfg-site/)

Aplicacion web estatica construida con Nextra 4 (framework de documentacion sobre Next.js) que presenta la memoria del TFG en formato navegable. Se encuentra en la carpeta tfg-site/ del repositorio. Incluye:

  • Navegacion lateral por capitulos.
  • Renderizado de Markdown con soporte para diagramas Mermaid.
  • Diseño visual con efecto glass-morphism inspirado en la estetica del ERP.
  • Despliegue independiente del ERP principal.

[CAPTURA-01: pagina principal del microsite TFG con navegacion lateral y contenido del Cap 1]

DESIGN.md — Sistema visual

Archivo DESIGN.md en la raiz del repositorio (367 lineas). Documenta el sistema de diseño del ERP inspirado en Linear:

  • Tokens de color (paleta oscura con acentos azules).
  • Tipografia y escala de espaciado.
  • Componentes base (botones, inputs, modales, toasts).
  • Convenciones CSS (CSS Modules + Custom Properties, sin Tailwind).
  • Modo oscuro como unica opcion (color-scheme: dark).

Este archivo actua como fuente de verdad para las decisiones visuales y es referenciado por Claude Code durante la implementacion de componentes para mantener coherencia estetica.

Carpeta tfg/

Contiene todos los capitulos de la memoria en formato Markdown, versionados dentro del propio repositorio del proyecto. Esta decision permite:

  • Historial de cambios de la memoria con el mismo flujo Git del codigo.
  • PRs especificas para capitulos (docs/tfg-cap-X).
  • Revision por pares de la redaccion tecnica.
  • Generacion del .docx final desde los .md como paso de build.

Diagramas

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

13.3 Planificacion temporal

Diagrama de Gantt (representacion textual)

Fase 0: Investigacion previa |========================| [VERIFICAR: mes inicio investigacion previa] ────────── 15 abril 2026 Fase 1: Bootstrap CI/CD + primer deploy produccion |X| 16 abril 2026 Fase 2: Desarrollo intensivo (iteracion via PRs sobre produccion) |================| 17 abril ──── 5 mayo 2026 Fase 3: Estabilizacion + onboarding Manu + incidente BSI |==| 6-8 mayo 2026 Fase 4: Memoria TFG (en paralelo y luego dedicado) |=========| 5 mayo ── 17 mayo 2026 Fase 5: Defensa |X| Ultima semana mayo 2026

Desglose por fases

FaseDescripcionPeriodoHitos clave
0Investigacion previa: flujos de trabajo con IA generativa, evaluacion de tooling (Claude Code, SDKs BaaS), definicion de convenciones Git, analisis de stacks, diseño del modelo de datos[VERIFICAR: mes inicio] - 15 abril 2026Documento de convenciones Git, schema data_model.sql v1, seleccion stack definitivo
1Bootstrap: primer commit, CI/CD operativo, deploy a produccion en VPS Hetzner antes de cualquier feature16 abril 2026Primer commit (Actualizar contrato de datos v1), pipeline GitHub Actions activo, dominio erp.quetzy.eu en linea
2Desarrollo intensivo: features core implementadas iterativamente con merge a main y deploy automatico. Chat completo, tickets con maquina de estados, llamadas WebRTC, contexto, clientes, proyectos, horas, notificaciones, presencia17 abril - 5 mayo 2026M1-M6.5 completados, 10 features funcionales, 827 tests, 82 % coverage
3Estabilizacion: correccion de bugs criticos, onboarding de Manu (PR #70), incidente seguridad BSI y correccion inmediata, apertura de 15 issues de feedback6-8 mayo 2026PR #70 de Manu, reglas UFW corregidas, 15 issues triageadas informalmente
4Memoria TFG: redaccion de los 13 capitulos, scaffold del microsite, capturas de produccion5-17 mayo 2026Caps 1-9 completados (7-8 mayo), Caps 10-13 completados (10 mayo), microsite operativo
5Defensa del TFGUltima semana mayo 2026Entrega 17 mayo, defensa oral

Nota metodologica sobre la concentracion temporal. El proyecto se ejecuta en un modelo de dos fases claramente diferenciadas: cuatro meses de investigacion y diseño seguidos de tres semanas de implementacion intensiva. Este no es un accidente de planificacion sino una consecuencia directa de la metodologia: la fase de investigacion previa define convenciones, contratos y modelo de datos con suficiente profundidad como para que la fase de implementacion se ejecute sin bloqueos de diseño. Toda la iteracion se realiza contra produccion real (no existe staging), lo que fuerza calidad desde el primer deploy.

13.4 Manual de usuario

Acceso e inicio de sesion

El acceso al sistema se realiza a traves del navegador en la URL erp.quetzy.eu. El sistema requiere autenticacion con email y contraseña; no existe registro publico. 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 sesion” 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.

[CAPTURA-02: pantalla de login con formulario email/password]

Vista general del shell ERP

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

  • Navegacion global: Chat, Tickets, Clientes, Proyectos, Horas, Contexto.
  • Indicador de notificaciones con contador de no leidas.
  • Boton de usuario con opcion de cerrar sesion.

[CAPTURA-03: shell del ERP con AppHeader y navegacion principal]

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 boton 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 (imagenes del portapapeles).
  • Maximo 10 MB por archivo, 5 archivos por mensaje.

Enviar mensaje de audio:

  1. Pulsar el icono de microfono.
  2. Grabar el mensaje.
  3. Pulsar detener. El audio se envia automaticamente.

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 reaccion.

[CAPTURA-04: chat con mensajes, adjuntos, reacciones y reply-to]

Llamadas: iniciar, unirse y compartir pantalla

Iniciar llamada:

  1. Dentro de un canal de chat, pulsar el icono de telefono en la cabecera.
  2. El sistema conecta via WebRTC. El microfono se activa automaticamente.
  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 boton de compartir pantalla.
  2. Seleccionar la ventana o pantalla a compartir en el dialogo del navegador.
  3. Los demas participantes ven la pantalla compartida en un panel dedicado.

Finalizar:

  • Pulsar el boton rojo de colgar. Si es el ultimo participante, la llamada se cierra automaticamente.

[CAPTURA-05: llamada activa con controles de audio y screen share]

Tickets: crear y gestionar con state rail

Crear ticket:

  1. Ir a Tickets en la navegacion.
  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 razon (requireReason), introducir justificacion.
  6. Confirmar. El sistema registra la transicion con timestamp y autor.

[CAPTURA-06: detalle de ticket con state rail y formulario de transicion]

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 automaticamente un canal de chat vinculado.

[CAPTURA-07: lista de clientes y detalle de proyecto]

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 boton de pausa en el timer activo. Pulsar de nuevo para reanudar.

Detener y guardar:

  1. Pulsar el boton de detener.
  2. Opcionalmente añadir notas sobre el trabajo realizado.
  3. La entrada de tiempo se guarda con duracion calculada.

[CAPTURA-08: dashboard de horas con timer activo e historial]

Contexto tecnico (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 razon de rechazo.

Ingestar raw_context:

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

[CAPTURA-09: context bucket con items en distintos estados y acciones de revision]

Notificaciones

  1. El contador en la cabecera muestra notificaciones no leidas.
  2. Pulsar el icono para abrir la lista.
  3. Marcar como leida individualmente o en bloque con “Marcar todas como leidas”.

[CAPTURA-10: lista de notificaciones con contador y acciones]

Cerrar sesion (logout)

  1. Pulsar el boton de usuario en la cabecera.
  2. Seleccionar “Cerrar sesion”.
  3. El sistema limpia la presencia, destruye la cookie de sesion y redirige a /login.

13.5 Manual tecnico

Despliegue desde cero en VPS nuevo

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

1. Provision del VPS:

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

2. Endurecimiento basico:

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. Instalacion de Docker:

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

4. Clonacion y configuracion:

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 automaticamente en el primer arranque curl --fail https://erp.quetzy.eu/api/health

Variables de entorno

VariableDescripcionDonde se usa
INSFORGE_URLURL interna del backend InsforgeServer-side (SDK singleton)
INSFORGE_ANON_KEYClave anonima 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 (generacion tokens)
LIVEKIT_API_SECRETSecreto API de LiveKit serverServer-side (firma JWT llamadas)
LIVEKIT_URLURL WebSocket del servidor LiveKitClient browser (conexion room)
NEXT_PUBLIC_INSFORGE_URLURL publica de Insforge (via Caddy)Client browser
NEXT_PUBLIC_INSFORGE_ANON_KEYAnon key publicaClient browser
NEXT_PUBLIC_LIVEKIT_URLURL publica de LiveKit (via 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 config 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 desplegara automaticamente 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 politicas

  • Autenticacion exclusivamente por clave publica (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.

Renovacion PAT GitHub

El token de acceso personal (PAT) fine-grained de GitHub tiene expiracion 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 comun

ProblemaDiagnosticoSolucion
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 cambio 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 esta corriendo fuera de Compose y el puerto 7130 esta 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 disco en Hetzner

Cron jobs configurados

JobFrecuenciaEndpoint/ComandoProposito
Notificaciones de VPSManual / cron VPScurl -H "x-cron-secret: $CRON_SECRET" https://erp.quetzy.eu/api/cron/notificationsGenerar alertas de renovacion, mantenimiento

En v1 no hay cron automatizado configurado en el sistema operativo. La ejecucion es manual o bajo demanda. La automatizacion 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 + paginas protegidas │ │ └── api/ # Route Handlers REST (44 endpoints) │ ├── features/ # Modulos por dominio funcional │ │ ├── calls/ # WebRTC / LiveKit │ │ ├── chat/ # Mensajeria en tiempo real │ │ ├── clients/ # Gestion de clientes │ │ ├── context/ # context_items + raw_context │ │ ├── erp/ # Shell, header, componentes compartidos │ │ ├── hours/ # Control horario │ │ ├── notifications/ # Notificaciones internas │ │ ├── presence/ # Presencia online │ │ ├── projects/ # Gestion de proyectos │ │ └── tickets/ # Maquina de estados + pipeline │ ├── lib/ # Capas transversales │ │ ├── ai/ # AIProvider + Skills │ │ ├── auth/ # Session, JWT, Insforge client │ │ ├── contract/ # 18 schemas Zod │ │ └── repositories/ # Interfaces + implementacion Insforge │ └── mocks/ # Implementacion mock + seeds ├── e2e/ # Tests Playwright (5 specs) ├── tfg/ # Memoria TFG (este documento) ├── tfg-site/ # Microsite documentacion (Nextra 4) ├── docker-compose.prod.yml # Orquestacion produccion ├── Caddyfile # Configuracion reverse proxy ├── DESIGN.md # Sistema visual (367 lineas) ├── data_model.sql # Schema SQL (20 tablas) └── .github/workflows/ # CI/CD GitHub Actions