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: [],
};Patron “Plan B” de cookie httpOnly
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
.docxfinal desde los.mdcomo 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 2026Desglose por fases
| Fase | Descripcion | Periodo | Hitos clave |
|---|---|---|---|
| 0 | Investigacion 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 2026 | Documento de convenciones Git, schema data_model.sql v1, seleccion stack definitivo |
| 1 | Bootstrap: primer commit, CI/CD operativo, deploy a produccion en VPS Hetzner antes de cualquier feature | 16 abril 2026 | Primer commit (Actualizar contrato de datos v1), pipeline GitHub Actions activo, dominio erp.quetzy.eu en linea |
| 2 | Desarrollo 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, presencia | 17 abril - 5 mayo 2026 | M1-M6.5 completados, 10 features funcionales, 827 tests, 82 % coverage |
| 3 | Estabilizacion: correccion de bugs criticos, onboarding de Manu (PR #70), incidente seguridad BSI y correccion inmediata, apertura de 15 issues de feedback | 6-8 mayo 2026 | PR #70 de Manu, reglas UFW corregidas, 15 issues triageadas informalmente |
| 4 | Memoria TFG: redaccion de los 13 capitulos, scaffold del microsite, capturas de produccion | 5-17 mayo 2026 | Caps 1-9 completados (7-8 mayo), Caps 10-13 completados (10 mayo), microsite operativo |
| 5 | Defensa del TFG | Ultima semana mayo 2026 | Entrega 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.
- Abrir
erp.quetzy.euen un navegador compatible (Chromium >= 100, Firefox >= 100, Safari >= 16). - Introducir email y contraseña en el formulario de login.
- Pulsar “Iniciar sesion” o Enter.
- Si las credenciales son correctas, el sistema redirige a la vista principal del ERP.
- 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:
- Seleccionar un canal en la lista lateral.
- Escribir el mensaje en el input inferior.
- Pulsar Enter o el boton de enviar.
Responder a un mensaje (reply-to):
- Pasar el cursor sobre un mensaje.
- Pulsar el icono de respuesta.
- El input muestra una referencia al mensaje original.
- 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:
- Pulsar el icono de microfono.
- Grabar el mensaje.
- Pulsar detener. El audio se envia automaticamente.
Reaccionar a un mensaje:
- Pasar el cursor sobre un mensaje.
- Pulsar el icono de reaccion.
- 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:
- Dentro de un canal de chat, pulsar el icono de telefono en la cabecera.
- El sistema conecta via WebRTC. El microfono se activa automaticamente.
- Los demas miembros del canal ven un banner de “llamada activa”.
Unirse a llamada existente:
- Pulsar el banner de “llamada activa” que aparece en el canal.
- El sistema conecta al room existente.
Compartir pantalla:
- Durante una llamada, pulsar el boton de compartir pantalla.
- Seleccionar la ventana o pantalla a compartir en el dialogo del navegador.
- 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:
- Ir a Tickets en la navegacion.
- Pulsar “Nuevo ticket”.
- Rellenar: titulo, tipo, proyecto asociado.
- El ticket se crea en estado
received.
Transicionar ticket:
- Abrir el detalle de un ticket.
- En el rail lateral se muestra el estado actual y los estados siguientes disponibles.
- Pulsar la transicion deseada.
- Si tiene guard (ej.
requireComplexity), rellenar el campo obligatorio. - Si requiere razon (
requireReason), introducir justificacion. - 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:
- Ir a Clientes. Se muestra la lista con nombre, tier y estado.
- Pulsar “Nuevo cliente” para crear.
- Pulsar un cliente para ver detalle con proyectos asociados.
Gestionar proyectos:
- Ir a Proyectos. Se muestra la lista con cliente asociado y metadatos.
- Pulsar “Nuevo proyecto” para crear (requiere seleccionar cliente).
- 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:
- Ir a Horas.
- Pulsar “Iniciar timer”.
- Seleccionar scope: cliente, proyecto y opcionalmente ticket.
- 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:
- Pulsar el boton de detener.
- Opcionalmente añadir notas sobre el trabajo realizado.
- 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:
- Ir a Contexto. Se muestra el bucket con items filtrables por estado (propuesto, aprobado, obsoleto).
Aprobar / rechazar item propuesto:
- En un item con estado
proposed, pulsar Aprobar o Rechazar. - Si se rechaza, introducir razon de rechazo.
Ingestar raw_context:
- Pulsar “Añadir contexto crudo”.
- Pegar texto (notas de reunion, emails, documentacion).
- Pulsar “Extraer” para invocar la skill de IA
context_extractor. - Revisar los items extraidos y aprobar/rechazar individualmente.
[CAPTURA-09: context bucket con items en distintos estados y acciones de revision]
Notificaciones
- El contador en la cabecera muestra notificaciones no leidas.
- Pulsar el icono para abrir la lista.
- Marcar como leida individualmente o en bloque con “Marcar todas como leidas”.
[CAPTURA-10: lista de notificaciones con contador y acciones]
Cerrar sesion (logout)
- Pulsar el boton de usuario en la cabecera.
- Seleccionar “Cerrar sesion”.
- 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 enable3. Instalacion de Docker:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker deploy
# Reconectar SSH para aplicar grupo4. 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/erp5. 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/healthVariables de entorno
| Variable | Descripcion | Donde se usa |
|---|---|---|
INSFORGE_URL | URL interna del backend Insforge | Server-side (SDK singleton) |
INSFORGE_ANON_KEY | Clave anonima para el SDK | Server-side + client browser |
INSFORGE_JWT_SECRET | Secreto para verificar JWT con jose | Server-side (session.ts) |
LIVEKIT_API_KEY | Clave API de LiveKit server | Server-side (generacion tokens) |
LIVEKIT_API_SECRET | Secreto API de LiveKit server | Server-side (firma JWT llamadas) |
LIVEKIT_URL | URL WebSocket del servidor LiveKit | Client browser (conexion room) |
NEXT_PUBLIC_INSFORGE_URL | URL publica de Insforge (via Caddy) | Client browser |
NEXT_PUBLIC_INSFORGE_ANON_KEY | Anon key publica | Client browser |
NEXT_PUBLIC_LIVEKIT_URL | URL publica de LiveKit (via Caddy) | Client browser |
ERP_DATA_SOURCE | Selector: mock o insforge | Factory de repositorios |
CRON_SECRET | Secreto del endpoint cron | API route /api/cron/notifications |
ENABLE_TEST_HARNESS | Habilita endpoints de test e2e | Middleware |
LIVEKIT_CONFIG | YAML en base64 de config LiveKit | Contenedor 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 psBackup 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.sqlRollback 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/healthAlternativa 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 erpAcceso SSH y politicas
- Autenticacion exclusivamente por clave publica (contraseña deshabilitada).
- Usuario de deploy:
deploycon permisossudo. - 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:
- GitHub Settings > Developer settings > Fine-grained personal access tokens.
- Regenerar con permisos
repo:read(minimo necesario para deploy). - Actualizar el secreto
GH_PATen el VPS si aplica.
Troubleshooting comun
| Problema | Diagnostico | Solucion |
|---|---|---|
| TLS no renueva | docker compose logs caddy buscar errores ACME | Verificar puertos 80/443 abiertos en UFW; Caddy necesita responder al challenge HTTP-01 |
| Health check falla tras deploy | docker compose logs erp --tail 50 | Verificar que .env.production tiene todas las vars; rebuild imagen si cambio package.json |
| LiveKit puerto bloqueado | sudo ufw status + nc -zvu <IP> 50000 desde exterior | Confirmar regla 50000:50100/udp ALLOW en UFW |
| Insforge no alcanzable desde ERP | docker compose exec erp curl http://host.docker.internal:7130/health | Verificar que Insforge esta corriendo fuera de Compose y el puerto 7130 esta bind a host |
| WebRTC sin audio en red corporativa | Logs LiveKit: no ICE candidates | Red con NAT estricto bloquea UDP. Fallback TCP 7881 o desplegar servidor TURN (deuda) |
| Base de datos llena | docker 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
| Job | Frecuencia | Endpoint/Comando | Proposito |
|---|---|---|---|
| Notificaciones de VPS | Manual / cron VPS | curl -H "x-cron-secret: $CRON_SECRET" https://erp.quetzy.eu/api/cron/notifications | Generar 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