{
  "$schema": "https://bpms.ai/schemas/architecture-map-1.json",
  "meta": {
    "product": "BPMS.ai",
    "repository": "bpms/bpms-platform",
    "line": "3.x — Java 8 maintenance line",
    "generatedFrom": "static analysis of src/main, WEB-INF/spring, Dockerfile and docker-compose.yml",
    "note": "Counts are class counts under src/main/java. This file is the source of truth; docs/architecture/index.html embeds a copy for offline viewing.",
    "stack": {
      "language": "Java 8",
      "framework": "Spring MVC (XML configuration, not Boot)",
      "orm": "Hibernate 4 with c3p0 pooling",
      "packaging": "WAR deployed as ROOT.war",
      "server": "Tomcat 9.0.97"
    }
  },

  "layers": [
    { "id": "clients", "label": "Clientes", "order": 1 },
    { "id": "edge", "label": "Ingress", "order": 2 },
    { "id": "web", "label": "Entrada web", "order": 3 },
    { "id": "app", "label": "Aplicación", "order": 4 },
    { "id": "data", "label": "Persistencia", "order": 5 },
    { "id": "services", "label": "Servicios de infraestructura", "order": 6 }
  ],

  "nodes": [
    {
      "id": "browser",
      "label": "Navegador",
      "layer": "clients",
      "kind": "client",
      "tech": "JSPX + jQuery + bpmn-js",
      "description": "Interfaz principal. Las vistas son JSPX renderizadas en servidor con Tiles, y el modelador de procesos corre en el cliente sobre bpmn-js.",
      "paths": ["src/main/webapp/WEB-INF/views", "src/main/webapp/resources/scripts"]
    },
    {
      "id": "mobile",
      "label": "App móvil",
      "layer": "clients",
      "kind": "client",
      "tech": "HTTP sobre servlet dedicado",
      "description": "Consume un servlet propio, fuera del dispatcher de Spring MVC.",
      "paths": ["src/main/java/co/com/processonline/mobile"]
    },
    {
      "id": "external",
      "label": "Sistemas externos",
      "layer": "clients",
      "kind": "client",
      "tech": "SOAP y REST",
      "description": "Integraciones de terceros que invocan los servicios publicados en /services/*.",
      "paths": ["src/main/java/co/com/processonline/services"]
    },

    {
      "id": "traefik",
      "label": "Traefik / Dokploy",
      "layer": "edge",
      "kind": "infra",
      "tech": "Traefik",
      "description": "Termina TLS y enruta el dominio al contenedor. Tomcat lo reconoce mediante RemoteIpValve, que traduce X-Forwarded-Proto y devuelve la IP real del cliente.",
      "paths": ["Dockerfile", "docker-compose.yml"]
    },

    {
      "id": "tomcat",
      "label": "Tomcat 9.0.97",
      "layer": "web",
      "kind": "runtime",
      "tech": "Tomcat 9 sobre JRE 8",
      "description": "Sirve una sola aplicación como ROOT.war, en el contexto raíz. El contexto /bpms se abandonó al pasar a un contenedor por servicio.",
      "paths": ["Dockerfile"]
    },
    {
      "id": "filters",
      "label": "Filtros de servlet",
      "layer": "web",
      "kind": "component",
      "tech": "Servlet Filter",
      "count": 4,
      "description": "CustomFilter, ResponseFilter, SessionRevokerFilter y HtmlResponseWrapper. Intervienen antes de que la petición llegue a Spring.",
      "paths": ["src/main/java/co/com/processonline/filters"]
    },
    {
      "id": "security",
      "label": "Spring Security",
      "layer": "web",
      "kind": "component",
      "tech": "Spring Security XML",
      "count": 12,
      "description": "form-login, CSRF, control de concurrencia de sesión, cabeceras de seguridad y un proveedor de autenticación propio. Soporta login nativo y OAuth2 según configuración.",
      "paths": [
        "src/main/webapp/WEB-INF/spring/security-context.xml",
        "src/main/java/co/com/processonline/security"
      ]
    },
    {
      "id": "appServlet",
      "label": "appServlet  /",
      "layer": "web",
      "kind": "entrypoint",
      "tech": "Spring MVC DispatcherServlet",
      "description": "Punto de entrada de toda la interfaz web.",
      "paths": ["src/main/webapp/WEB-INF/web.xml"]
    },
    {
      "id": "mobileServlet",
      "label": "MobileServlet",
      "layer": "web",
      "kind": "entrypoint",
      "tech": "HttpServlet",
      "count": 4,
      "description": "Canal de la app móvil. También arma el enlace de recuperación de contraseña.",
      "paths": ["src/main/java/co/com/processonline/mobile/servlet"]
    },
    {
      "id": "cxf",
      "label": "CXF  /services/*",
      "layer": "web",
      "kind": "entrypoint",
      "tech": "Apache CXF",
      "count": 4,
      "description": "Servicios SOAP publicados hacia sistemas externos.",
      "paths": ["src/main/webapp/WEB-INF/spring/application-ws.xml"]
    },
    {
      "id": "websocket",
      "label": "WebSocket",
      "layer": "web",
      "kind": "entrypoint",
      "tech": "JSR-356",
      "description": "Canal de notificaciones y control de sesión hacia el navegador.",
      "paths": ["src/main/java/co/com/processonline/sockets"]
    },

    {
      "id": "controllers",
      "label": "Controllers",
      "layer": "app",
      "kind": "component",
      "tech": "Spring MVC",
      "count": 127,
      "description": "Agrupados por módulo funcional: administrator, bam, designer, documentManagement, dynamicForm, execution, taskTray, health, ws.",
      "paths": ["src/main/java/co/com/processonline/controller"]
    },
    {
      "id": "componentServices",
      "label": "Component services",
      "layer": "app",
      "kind": "component",
      "count": 46,
      "description": "Capa de servicios entre controllers y persistencia. Concentra la lógica que no pertenece al motor.",
      "paths": ["src/main/java/co/com/processonline/component/services"]
    },
    {
      "id": "engine",
      "label": "Motor BPM",
      "layer": "app",
      "kind": "core",
      "count": 85,
      "description": "El corazón del producto: modelo de proceso, ejecución, planificación y enumeradores. Subpaquetes model, execution, schedule, services, enumerators y exceptions.",
      "paths": ["src/main/java/co/com/processonline/engine"]
    },
    {
      "id": "mail",
      "label": "Correo",
      "layer": "app",
      "kind": "component",
      "count": 7,
      "description": "MailProvider abstrae el envío. Elige SMTP o Amazon SES según BPMS_MAIL_PROVIDER, sin que el resto del código lo sepa.",
      "paths": ["src/main/java/co/com/processonline/mail"]
    },
    {
      "id": "batch",
      "label": "Batch y jobs",
      "layer": "app",
      "kind": "component",
      "description": "Spring Batch más tareas programadas para procesos desatendidos.",
      "paths": ["src/main/webapp/WEB-INF/spring/job.xml", "src/main/java/co/com/processonline/batch"]
    },
    {
      "id": "config",
      "label": "AppConfiguration",
      "layer": "app",
      "kind": "component",
      "count": 16,
      "description": "Toma config.properties, cuyos valores son referencias al entorno. Es lo que permite que un mismo artefacto sirva SaaS, PaaS y on-premise.",
      "paths": [
        "src/main/java/co/com/processonline/configuration",
        "src/main/resources/config.properties"
      ]
    },

    {
      "id": "managers",
      "label": "Managers / DAO",
      "layer": "data",
      "kind": "component",
      "count": 136,
      "description": "Acceso a datos por módulo, con implementaciones separadas para PostgreSQL y SQL Server.",
      "paths": [
        "src/main/java/co/com/processonline/managers",
        "src/main/java/co/com/processonline/dao"
      ]
    },
    {
      "id": "entities",
      "label": "Entidades",
      "layer": "data",
      "kind": "component",
      "count": 240,
      "description": "Modelo persistente mapeado con Hibernate.",
      "paths": ["src/main/java/co/com/processonline/entities"]
    },
    {
      "id": "hibernate",
      "label": "Hibernate + Initializer",
      "layer": "data",
      "kind": "component",
      "count": 7,
      "description": "sessionFactory sobre pool c3p0. El Initializer siembra el tenant y el administrador cuando la base está vacía, tomando los valores del entorno.",
      "paths": ["src/main/java/co/com/processonline/hibernate"]
    },
    {
      "id": "ehcache",
      "label": "Ehcache",
      "layer": "data",
      "kind": "component",
      "description": "Caché de segundo nivel. Persiste a disco, lo que exige que los objetos cacheados sean serializables.",
      "paths": ["src/main/webapp/WEB-INF/spring/ehcache.xml"]
    },

    {
      "id": "postgres",
      "label": "PostgreSQL 17.10",
      "layer": "services",
      "kind": "external",
      "description": "Base de datos principal. La aplicación no arranca sin ella: si el sessionFactory falla, cae toda la cadena de beans.",
      "paths": ["docker-compose.yml"]
    },
    {
      "id": "solr",
      "label": "Solr 8.11.4",
      "layer": "services",
      "kind": "external",
      "description": "Índice de búsqueda documental, en modo SolrCloud.",
      "paths": ["src/main/webapp/WEB-INF/spring/solr-context.xml"]
    },
    {
      "id": "rabbitmq",
      "label": "RabbitMQ 3.13.7",
      "layer": "services",
      "kind": "external",
      "description": "Mensajería para eventos de trazabilidad y login.",
      "paths": ["docker-compose.yml", "src/main/resources/config.properties"]
    },
    {
      "id": "filemanager",
      "label": "FileManager",
      "layer": "services",
      "kind": "external",
      "description": "Producto aparte, con su propio repositorio, imagen y OAuth2. El BPMS lo consume por HTTP; el acoplamiento es de configuración, sin código compartido.",
      "paths": ["docker-compose.yml"]
    },
    {
      "id": "gotenberg",
      "label": "Gotenberg 8.34",
      "layer": "services",
      "kind": "external",
      "description": "Conversión de ofimática a PDF. Lo usa el FileManager, no el BPMS directamente.",
      "paths": ["docker-compose.yml"]
    },
    {
      "id": "seaweedfs",
      "label": "SeaweedFS 4.40",
      "layer": "services",
      "kind": "external",
      "description": "Almacenamiento compatible con S3. Guarda cada documento de la plataforma; su volumen es el que nunca se puede perder.",
      "paths": ["docker-compose.yml"]
    },
    {
      "id": "mailProvider",
      "label": "SMTP / Amazon SES",
      "layer": "services",
      "kind": "external",
      "description": "Salida de correo. El proveedor se elige por entorno.",
      "paths": ["src/main/resources/config.properties"]
    }
  ],

  "edges": [
    { "from": "browser", "to": "traefik", "label": "HTTPS", "protocol": "https" },
    { "from": "mobile", "to": "traefik", "label": "HTTPS", "protocol": "https" },
    { "from": "external", "to": "traefik", "label": "HTTPS", "protocol": "https" },

    { "from": "traefik", "to": "tomcat", "label": "HTTP + X-Forwarded-*", "protocol": "http" },

    { "from": "tomcat", "to": "filters", "label": "cadena de filtros" },
    { "from": "filters", "to": "security", "label": "autenticación" },
    { "from": "security", "to": "appServlet", "label": "petición autorizada" },
    { "from": "tomcat", "to": "mobileServlet", "label": "/MobileServlet" },
    { "from": "tomcat", "to": "cxf", "label": "/services/*" },
    { "from": "tomcat", "to": "websocket", "label": "upgrade" },

    { "from": "appServlet", "to": "controllers", "label": "routing" },
    { "from": "controllers", "to": "componentServices", "label": "delega" },
    { "from": "controllers", "to": "engine", "label": "ejecuta proceso" },
    { "from": "mobileServlet", "to": "componentServices", "label": "delega" },
    { "from": "cxf", "to": "engine", "label": "invoca" },
    { "from": "componentServices", "to": "engine", "label": "usa" },
    { "from": "componentServices", "to": "managers", "label": "persiste" },
    { "from": "engine", "to": "managers", "label": "persiste" },
    { "from": "batch", "to": "engine", "label": "dispara" },
    { "from": "engine", "to": "mail", "label": "notifica" },
    { "from": "componentServices", "to": "mail", "label": "notifica" },
    { "from": "config", "to": "hibernate", "label": "configura" },
    { "from": "config", "to": "mail", "label": "elige proveedor" },

    { "from": "managers", "to": "entities", "label": "mapea" },
    { "from": "managers", "to": "hibernate", "label": "sessionFactory" },
    { "from": "hibernate", "to": "ehcache", "label": "caché L2" },
    { "from": "hibernate", "to": "postgres", "label": "JDBC + c3p0", "protocol": "jdbc" },

    { "from": "componentServices", "to": "solr", "label": "indexa y consulta", "protocol": "http" },
    { "from": "engine", "to": "rabbitmq", "label": "publica eventos", "protocol": "amqp" },
    { "from": "security", "to": "rabbitmq", "label": "evento de login", "protocol": "amqp" },
    { "from": "controllers", "to": "filemanager", "label": "documentos", "protocol": "http" },
    { "from": "componentServices", "to": "filemanager", "label": "documentos", "protocol": "http" },
    { "from": "filemanager", "to": "seaweedfs", "label": "S3", "protocol": "s3" },
    { "from": "filemanager", "to": "gotenberg", "label": "convierte a PDF", "protocol": "http" },
    { "from": "mail", "to": "mailProvider", "label": "envía", "protocol": "smtp/api" },
    { "from": "websocket", "to": "componentServices", "label": "notificaciones" }
  ],

  "flows": [
    {
      "id": "login",
      "label": "Autenticación",
      "description": "Un usuario entra a la plataforma. Es el camino que rompió en producción cuando Tomcat no sabía que Traefik terminaba el TLS.",
      "steps": [
        { "n": 1, "node": "browser", "action": "Pide / y recibe una redirección a /login" },
        { "n": 2, "node": "traefik", "edge": ["browser", "traefik"], "action": "Termina TLS y reenvía con X-Forwarded-Proto: https" },
        { "n": 3, "node": "tomcat", "edge": ["traefik", "tomcat"], "action": "RemoteIpValve marca la petición como segura, de ahí sale la cookie Secure" },
        { "n": 4, "node": "filters", "edge": ["tomcat", "filters"], "action": "SessionRevokerFilter y compañía inspeccionan la sesión" },
        { "n": 5, "node": "security", "edge": ["filters", "security"], "action": "CustomAuthenticationProvider valida contra UserDetailsServiceImpl" },
        { "n": 6, "node": "managers", "action": "UsersDAO consulta las credenciales" },
        { "n": 7, "node": "postgres", "edge": ["hibernate", "postgres"], "action": "Se compara el hash BCrypt almacenado" },
        { "n": 8, "node": "rabbitmq", "edge": ["security", "rabbitmq"], "action": "Se publica el evento de login" },
        { "n": 9, "node": "appServlet", "edge": ["security", "appServlet"], "action": "CustomSuccessHandler decide el destino y entrega la bandeja" }
      ]
    },
    {
      "id": "task",
      "label": "Ejecución de tarea",
      "description": "El recorrido central del producto: un usuario toma una tarea de su bandeja y la avanza en el proceso.",
      "steps": [
        { "n": 1, "node": "browser", "action": "El usuario abre la bandeja y selecciona una tarea" },
        { "n": 2, "node": "appServlet", "edge": ["traefik", "tomcat"], "action": "La petición llega al dispatcher ya autenticada" },
        { "n": 3, "node": "controllers", "edge": ["appServlet", "controllers"], "action": "El controller de taskTray recibe la acción" },
        { "n": 4, "node": "engine", "edge": ["controllers", "engine"], "action": "El motor evalúa el modelo y decide la transición" },
        { "n": 5, "node": "managers", "edge": ["engine", "managers"], "action": "Se persiste el nuevo estado de la instancia" },
        { "n": 6, "node": "postgres", "edge": ["hibernate", "postgres"], "action": "Commit de la transacción" },
        { "n": 7, "node": "rabbitmq", "edge": ["engine", "rabbitmq"], "action": "Se publican los eventos de trazabilidad" },
        { "n": 8, "node": "mail", "edge": ["engine", "mail"], "action": "Si la tarea lo define, se notifica al siguiente responsable" },
        { "n": 9, "node": "websocket", "action": "El destinatario ve la tarea aparecer sin recargar" }
      ]
    },
    {
      "id": "document",
      "label": "Carga de documento",
      "description": "Muestra la frontera con el FileManager: acoplamiento por configuración, sin código compartido.",
      "steps": [
        { "n": 1, "node": "browser", "action": "El usuario adjunta un archivo a una radicación" },
        { "n": 2, "node": "controllers", "edge": ["appServlet", "controllers"], "action": "El controller de documentManagement recibe el multipart" },
        { "n": 3, "node": "filemanager", "edge": ["controllers", "filemanager"], "action": "Se delega al FileManager, autenticando por OAuth2" },
        { "n": 4, "node": "gotenberg", "edge": ["filemanager", "gotenberg"], "action": "Si es ofimática, se convierte a PDF" },
        { "n": 5, "node": "seaweedfs", "edge": ["filemanager", "seaweedfs"], "action": "El archivo se guarda vía API S3" },
        { "n": 6, "node": "managers", "action": "El BPMS guarda solo la referencia, nunca el binario" },
        { "n": 7, "node": "solr", "edge": ["componentServices", "solr"], "action": "Se indexa el contenido para búsqueda posterior" }
      ]
    },
    {
      "id": "search",
      "label": "Búsqueda documental",
      "description": "Por qué Solr es parte del stack y no un adorno.",
      "steps": [
        { "n": 1, "node": "browser", "action": "El usuario busca por índices o texto" },
        { "n": 2, "node": "controllers", "edge": ["appServlet", "controllers"], "action": "documentSearch recibe los criterios" },
        { "n": 3, "node": "componentServices", "edge": ["controllers", "componentServices"], "action": "DocumentSearchService arma la consulta" },
        { "n": 4, "node": "solr", "edge": ["componentServices", "solr"], "action": "Solr resuelve la búsqueda sobre el índice" },
        { "n": 5, "node": "managers", "action": "Se completan los resultados con datos relacionales" },
        { "n": 6, "node": "postgres", "edge": ["hibernate", "postgres"], "action": "Consulta de apoyo" }
      ]
    },
    {
      "id": "mail",
      "label": "Envío de correo",
      "description": "La abstracción que permite cambiar de proveedor sin tocar el código que envía.",
      "steps": [
        { "n": 1, "node": "engine", "action": "Una tarea o un evento pide notificar" },
        { "n": 2, "node": "mail", "edge": ["engine", "mail"], "action": "MailProvider recibe el mensaje" },
        { "n": 3, "node": "config", "edge": ["config", "mail"], "action": "BPMS_MAIL_PROVIDER decide entre SMTP y SES" },
        { "n": 4, "node": "mailProvider", "edge": ["mail", "mailProvider"], "action": "OutboundEmailBackend entrega por el canal elegido" }
      ]
    },
    {
      "id": "boot",
      "label": "Arranque del sistema",
      "description": "El orden que importa. Si la base no responde, no arranca nada: el sessionFactory falla y arrastra toda la cadena de beans.",
      "steps": [
        { "n": 1, "node": "tomcat", "action": "Tomcat despliega ROOT.war" },
        { "n": 2, "node": "config", "action": "config.properties resuelve cada valor contra las variables de entorno" },
        { "n": 3, "node": "hibernate", "edge": ["config", "hibernate"], "action": "Se construye el sessionFactory sobre el pool c3p0" },
        { "n": 4, "node": "postgres", "edge": ["hibernate", "postgres"], "action": "Primera conexión. Si falla aquí, el contexto no levanta" },
        { "n": 5, "node": "ehcache", "edge": ["hibernate", "ehcache"], "action": "Se inicializa la caché de segundo nivel" },
        { "n": 6, "node": "managers", "action": "El Initializer busca el tenant 1" },
        { "n": 7, "node": "entities", "action": "Si no existe, siembra tenant y administrador desde el entorno" },
        { "n": 8, "node": "appServlet", "action": "El dispatcher queda listo y la aplicación responde" }
      ]
    }
  ]
}
