eva4j 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/AGENTS.md +314 -10
  2. package/COMMAND_EVALUATION.md +15 -16
  3. package/DOMAIN_YAML_GUIDE.md +576 -10
  4. package/FUTURE_FEATURES.md +1627 -1168
  5. package/README.md +318 -13
  6. package/bin/eva4j.js +34 -0
  7. package/config/defaults.json +1 -0
  8. package/design-system.md +797 -0
  9. package/docs/commands/EVALUATE_SYSTEM.md +994 -0
  10. package/docs/commands/GENERATE_ENTITIES.md +795 -6
  11. package/docs/commands/INDEX.md +10 -1
  12. package/examples/domain-endpoints-relations.yaml +353 -0
  13. package/examples/domain-endpoints-versioned.yaml +144 -0
  14. package/examples/domain-endpoints.yaml +135 -0
  15. package/examples/domain-events.yaml +166 -20
  16. package/examples/domain-listeners.yaml +212 -0
  17. package/examples/domain-one-to-many.yaml +1 -0
  18. package/examples/domain-one-to-one.yaml +1 -0
  19. package/examples/domain-ports.yaml +414 -0
  20. package/examples/domain-soft-delete.yaml +47 -44
  21. package/examples/system/notification.yaml +147 -0
  22. package/examples/system/product.yaml +185 -0
  23. package/examples/system/system.yaml +112 -0
  24. package/examples/system-report.html +971 -0
  25. package/examples/system.yaml +332 -0
  26. package/package.json +2 -1
  27. package/src/commands/build.js +714 -0
  28. package/src/commands/create.js +7 -3
  29. package/src/commands/detach.js +1 -0
  30. package/src/commands/evaluate-system.js +610 -0
  31. package/src/commands/generate-entities.js +1331 -49
  32. package/src/commands/generate-http-exchange.js +2 -0
  33. package/src/commands/generate-kafka-event.js +98 -11
  34. package/src/generators/base-generator.js +8 -1
  35. package/src/generators/postman-generator.js +188 -0
  36. package/src/generators/shared-generator.js +10 -0
  37. package/src/utils/config-manager.js +54 -0
  38. package/src/utils/context-builder.js +1 -0
  39. package/src/utils/domain-diagram.js +192 -0
  40. package/src/utils/domain-validator.js +970 -0
  41. package/src/utils/fake-data.js +376 -0
  42. package/src/utils/naming.js +3 -2
  43. package/src/utils/system-validator.js +434 -0
  44. package/src/utils/yaml-to-entity.js +302 -8
  45. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  46. package/templates/aggregate/AggregateRepository.java.ejs +8 -2
  47. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
  48. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  49. package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
  50. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  51. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  52. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  53. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  54. package/templates/base/gradle/build.gradle.ejs +3 -2
  55. package/templates/base/root/AGENTS.md.ejs +306 -45
  56. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
  57. package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
  58. package/templates/base/root/system.yaml.ejs +97 -0
  59. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  60. package/templates/crud/Controller.java.ejs +4 -4
  61. package/templates/crud/CreateCommand.java.ejs +4 -0
  62. package/templates/crud/CreateItemDto.java.ejs +4 -0
  63. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  64. package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
  65. package/templates/crud/EndpointsController.java.ejs +178 -0
  66. package/templates/crud/FindByQuery.java.ejs +17 -0
  67. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  68. package/templates/crud/ListQuery.java.ejs +1 -1
  69. package/templates/crud/ListQueryHandler.java.ejs +8 -8
  70. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  71. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  72. package/templates/crud/ScaffoldQuery.java.ejs +13 -0
  73. package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
  74. package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
  75. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  76. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  77. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  78. package/templates/crud/TransitionCommand.java.ejs +9 -0
  79. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  80. package/templates/crud/UpdateCommand.java.ejs +4 -0
  81. package/templates/evaluate/report.html.ejs +1363 -0
  82. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  83. package/templates/kafka-event/Event.java.ejs +16 -0
  84. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  85. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  86. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  87. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  88. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
  89. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  90. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  91. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  92. package/templates/mock/MockEvent.java.ejs +10 -0
  93. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  94. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  95. package/templates/mock/SpringEventListener.java.ejs +61 -0
  96. package/templates/ports/PortDomainModel.java.ejs +35 -0
  97. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  98. package/templates/ports/PortFeignClient.java.ejs +45 -0
  99. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  100. package/templates/ports/PortInterface.java.ejs +45 -0
  101. package/templates/ports/PortNestedType.java.ejs +28 -0
  102. package/templates/ports/PortRequestDto.java.ejs +30 -0
  103. package/templates/ports/PortResponseDto.java.ejs +28 -0
  104. package/templates/postman/Collection.json.ejs +1 -1
  105. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  106. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
@@ -0,0 +1,971 @@
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>test-eva — eva4j Architecture Validator</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
10
+ <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
11
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
12
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
13
+ <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
14
+ <style>
15
+ * { box-sizing: border-box; margin: 0; padding: 0; }
16
+ body { background: #0a0a0f; color: #e8e8f0; font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif; }
17
+ ::-webkit-scrollbar { width: 6px; }
18
+ ::-webkit-scrollbar-track { background: #0a0a0f; }
19
+ ::-webkit-scrollbar-thumb { background: #2e2e50; border-radius: 3px; }
20
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
21
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
22
+ @keyframes shimmer { 0% { box-shadow: 0 0 0 0 rgba(230,57,80,0.4); } 70% { box-shadow: 0 0 0 8px rgba(230,57,80,0); } 100% { box-shadow: 0 0 0 0 rgba(230,57,80,0); } }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div id="root"></div>
27
+
28
+ <script>
29
+ (function () {
30
+ var _d = 'eyJzeXN0ZW1OYW1lIjoidGVzdC1ldmEiLCJtb2R1bGVzIjpbeyJpZCI6Im1vdmllcyIsImxhYmVsIjoiTW92aWVzIiwiaWNvbiI6IvCfjqwiLCJjb2xvciI6IiM0YTllZmYiLCJkZXNjIjoiQ2F0w6Fsb2dvIGRlIHBlbMOtY3VsYXM6IHTDrXR1bG9zLCBnw6luZXJvcywgY2xhc2lmaWNhY2lvbmVzLCBzaW5vcHNpcyB5IGR1cmFjacOzbiJ9LHsiaWQiOiJ0aGVhdGVycyIsImxhYmVsIjoiVGhlYXRlcnMiLCJpY29uIjoi8J+Pm++4jyIsImNvbG9yIjoiIzliNmRmZiIsImRlc2MiOiJTYWxhcyBkZSBjaW5lOiBjb25maWd1cmFjacOzbiBkZSBjYXBhY2lkYWQsIHRpcG8gZGUgc2FsYSAoMkQsIDNELCBJTUFYKSB5IG1hcGEgZGUgYXNpZW50b3MifSx7ImlkIjoic2NyZWVuaW5ncyIsImxhYmVsIjoiU2NyZWVuaW5ncyIsImljb24iOiLwn4+b77iPIiwiY29sb3IiOiIjZjVjODQyIiwiZGVzYyI6IkZ1bmNpb25lcyBwcm9ncmFtYWRhczogYXNvY2lhIHBlbMOtY3VsYSArIHNhbGEgKyBob3JhcmlvLCBnZXN0aW9uYSBkaXNwb25pYmlsaWRhZCwgcHJlY2lvcyB5IGJsb3F1ZW8gZXhjbHVzaXZvIHBhcmEgZXZlbnRvcyBwcml2YWRvcyJ9LHsiaWQiOiJjdXN0b21lcnMiLCJsYWJlbCI6IkN1c3RvbWVycyIsImljb24iOiLwn5GkIiwiY29sb3IiOiIjMmRjYzhmIiwiZGVzYyI6IlJlZ2lzdHJvIHkgcGVyZmlsIGRlIGNsaWVudGVzOiBkYXRvcyBwZXJzb25hbGVzLCBoaXN0b3JpYWwgZGUgY29tcHJhcyBlIGhpc3RvcmlhbCBkZSBwdW50b3MifSx7ImlkIjoicmVzZXJ2YXRpb25zIiwibGFiZWwiOiJSZXNlcnZhdGlvbnMiLCJpY29uIjoi8J+On++4jyIsImNvbG9yIjoiI2ZmOGM0MiIsImRlc2MiOiJDaWNsbyBkZSB2aWRhIGRlIGxhIHJlc2VydmE6IHNlbGVjY2nDs24geSBibG9xdWVvIGRlIGFzaWVudG9zLCBjb25maXJtYWNpw7NuLCBjYW5jZWxhY2nDs24geSByZXNlcnZhIGV4Y2x1c2l2YSBkZSBzYWxhIGNvbXBsZXRhIHBhcmEgZXZlbnRvcyBwcml2YWRvcyJ9LHsiaWQiOiJwYXltZW50cyIsImxhYmVsIjoiUGF5bWVudHMiLCJpY29uIjoi8J+SsyIsImNvbG9yIjoiI2U2Mzk1MCIsImRlc2MiOiJQcm9jZXNhbWllbnRvIGRlIHBhZ29zOiBpbnRlZ3JhY2nDs24gY29uIHBhc2FyZWxhIGV4dGVybmEsIGFwcm9iYWNpb25lcyB5IHJlZW1ib2xzb3MifSx7ImlkIjoibm90aWZpY2F0aW9ucyIsImxhYmVsIjoiTm90aWZpY2F0aW9ucyIsImljb24iOiLwn5SUIiwiY29sb3IiOiIjNDBjNGQwIiwiZGVzYyI6IkVudsOtbyBkZSBub3RpZmljYWNpb25lcyBwb3IgZW1haWwgeSBTTVM6IGNvbmZpcm1hY2lvbmVzLCB0aWNrZXRzIFFSLCByZWNvcmRhdG9yaW9zIHkgYWxlcnRhcyBkZSBjYW5jZWxhY2nDs24ifV0sImV2ZW50cyI6W3siZXZlbnQiOiJSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsInByb2R1Y2VyIjoicmVzZXJ2YXRpb25zIiwidG9waWMiOiJSRVNFUlZBVElPTl9DUkVBVEVEIiwiY29uc3VtZXJzIjpbInBheW1lbnRzIiwibm90aWZpY2F0aW9ucyJdfSx7ImV2ZW50IjoiUGF5bWVudEFwcHJvdmVkRXZlbnQiLCJwcm9kdWNlciI6InBheW1lbnRzIiwidG9waWMiOiJQQVlNRU5UX0FQUFJPVkVEIiwiY29uc3VtZXJzIjpbInJlc2VydmF0aW9ucyIsIm5vdGlmaWNhdGlvbnMiXX0seyJldmVudCI6IlBheW1lbnRSZWplY3RlZEV2ZW50IiwicHJvZHVjZXIiOiJwYXltZW50cyIsInRvcGljIjoiUEFZTUVOVF9SRUpFQ1RFRCIsImNvbnN1bWVycyI6WyJyZXNlcnZhdGlvbnMiLCJub3RpZmljYXRpb25zIl19LHsiZXZlbnQiOiJSZXNlcnZhdGlvbkNvbmZpcm1lZEV2ZW50IiwicHJvZHVjZXIiOiJyZXNlcnZhdGlvbnMiLCJ0b3BpYyI6IlJFU0VSVkFUSU9OX0NPTkZJUk1FRCIsImNvbnN1bWVycyI6WyJub3RpZmljYXRpb25zIiwiY3VzdG9tZXJzIl19LHsiZXZlbnQiOiJSZXNlcnZhdGlvbkNhbmNlbGxlZEV2ZW50IiwicHJvZHVjZXIiOiJyZXNlcnZhdGlvbnMiLCJ0b3BpYyI6IlJFU0VSVkFUSU9OX0NBTkNFTExFRCIsImNvbnN1bWVycyI6WyJwYXltZW50cyIsInNjcmVlbmluZ3MiLCJub3RpZmljYXRpb25zIl19LHsiZXZlbnQiOiJSZXNlcnZhdGlvbkV4cGlyZWRFdmVudCIsInByb2R1Y2VyIjoicmVzZXJ2YXRpb25zIiwidG9waWMiOiJSRVNFUlZBVElPTl9FWFBJUkVEIiwiY29uc3VtZXJzIjpbInNjcmVlbmluZ3MiLCJub3RpZmljYXRpb25zIl19LHsiZXZlbnQiOiJTY3JlZW5pbmdDYW5jZWxsZWRFdmVudCIsInByb2R1Y2VyIjoic2NyZWVuaW5ncyIsInRvcGljIjoiU0NSRUVOSU5HX0NBTkNFTExFRCIsImNvbnN1bWVycyI6WyJyZXNlcnZhdGlvbnMiLCJub3RpZmljYXRpb25zIl19LHsiZXZlbnQiOiJQcml2YXRlRXZlbnRSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsInByb2R1Y2VyIjoicmVzZXJ2YXRpb25zIiwidG9waWMiOiJQUklWQVRFX0VWRU5UX1JFU0VSVkFUSU9OX0NSRUFURUQiLCJjb25zdW1lcnMiOlsic2NyZWVuaW5ncyIsInBheW1lbnRzIiwibm90aWZpY2F0aW9ucyJdfSx7ImV2ZW50IjoiVGhlYXRlckxvY2tlZEZvclByaXZhdGVFdmVudEV2ZW50IiwicHJvZHVjZXIiOiJzY3JlZW5pbmdzIiwidG9waWMiOiJUSEVBVEVSX0xPQ0tFRF9GT1JfUFJJVkFURV9FVkVOVCIsImNvbnN1bWVycyI6WyJub3RpZmljYXRpb25zIl19XSwic3luY0ludGVncmF0aW9ucyI6W3siY2FsbGVyIjoic2NyZWVuaW5ncyIsImNhbGxzIjoibW92aWVzIiwicG9ydCI6Ik1vdmllU2VydmljZSIsImVuZHBvaW50cyI6WyJHRVQgL21vdmllcy97aWR9Il19LHsiY2FsbGVyIjoic2NyZWVuaW5ncyIsImNhbGxzIjoidGhlYXRlcnMiLCJwb3J0IjoiVGhlYXRlclNlcnZpY2UiLCJlbmRwb2ludHMiOlsiR0VUIC90aGVhdGVycy97aWR9Il19LHsiY2FsbGVyIjoicmVzZXJ2YXRpb25zIiwiY2FsbHMiOiJzY3JlZW5pbmdzIiwicG9ydCI6IlNjcmVlbmluZ1NlcnZpY2UiLCJlbmRwb2ludHMiOlsiR0VUIC9zY3JlZW5pbmdzL3tpZH0iLCJHRVQgL3NjcmVlbmluZ3Mve2lkfS9zZWF0cyIsIkdFVCAvc2NyZWVuaW5ncy97aWR9L3ByaXZhdGUtZXZlbnQtYXZhaWxhYmlsaXR5Il19LHsiY2FsbGVyIjoicmVzZXJ2YXRpb25zIiwiY2FsbHMiOiJjdXN0b21lcnMiLCJwb3J0IjoiQ3VzdG9tZXJTZXJ2aWNlIiwiZW5kcG9pbnRzIjpbIkdFVCAvY3VzdG9tZXJzL3tpZH0iXX0seyJjYWxsZXIiOiJwYXltZW50cyIsImNhbGxzIjoicmVzZXJ2YXRpb25zIiwicG9ydCI6IlJlc2VydmF0aW9uU2VydmljZSIsImVuZHBvaW50cyI6WyJHRVQgL3Jlc2VydmF0aW9ucy97aWR9Il19XSwiZW5kcG9pbnRzIjp7Im1vdmllcyI6WyJHRVQgL21vdmllcyIsIkdFVCAvbW92aWVzL3tpZH0iLCJQT1NUIC9tb3ZpZXMiLCJQVVQgL21vdmllcy97aWR9IiwiREVMRVRFIC9tb3ZpZXMve2lkfSJdLCJ0aGVhdGVycyI6WyJHRVQgL3RoZWF0ZXJzIiwiR0VUIC90aGVhdGVycy97aWR9IiwiUE9TVCAvdGhlYXRlcnMiLCJQVVQgL3RoZWF0ZXJzL3tpZH0iXSwic2NyZWVuaW5ncyI6WyJHRVQgL3NjcmVlbmluZ3MiLCJHRVQgL3NjcmVlbmluZ3Mve2lkfSIsIkdFVCAvc2NyZWVuaW5ncy97aWR9L3NlYXRzIiwiUE9TVCAvc2NyZWVuaW5ncyIsIlBVVCAvc2NyZWVuaW5ncy97aWR9L2NhbmNlbCIsIkdFVCAvc2NyZWVuaW5ncy97aWR9L3ByaXZhdGUtZXZlbnQtYXZhaWxhYmlsaXR5IiwiUFVUIC9zY3JlZW5pbmdzL3tpZH0vbG9jayJdLCJjdXN0b21lcnMiOlsiUE9TVCAvY3VzdG9tZXJzIiwiR0VUIC9jdXN0b21lcnMve2lkfSIsIkdFVCAvY3VzdG9tZXJzIiwiUFVUIC9jdXN0b21lcnMve2lkfSJdLCJyZXNlcnZhdGlvbnMiOlsiUE9TVCAvcmVzZXJ2YXRpb25zIiwiR0VUIC9yZXNlcnZhdGlvbnMve2lkfSIsIkdFVCAvcmVzZXJ2YXRpb25zIiwiUFVUIC9yZXNlcnZhdGlvbnMve2lkfS9jb25maXJtIiwiUFVUIC9yZXNlcnZhdGlvbnMve2lkfS9jYW5jZWwiLCJQVVQgL3Jlc2VydmF0aW9ucy97aWR9L2V4cGlyZSIsIlBPU1QgL3Jlc2VydmF0aW9ucy9wcml2YXRlLWV2ZW50cyJdLCJwYXltZW50cyI6WyJQT1NUIC9wYXltZW50cyIsIkdFVCAvcGF5bWVudHMve2lkfSIsIkdFVCAvcGF5bWVudHMiLCJQVVQgL3BheW1lbnRzL3tpZH0vYXBwcm92ZSIsIlBVVCAvcGF5bWVudHMve2lkfS9yZWplY3QiLCJQVVQgL3BheW1lbnRzL3tpZH0vcmVmdW5kIl0sIm5vdGlmaWNhdGlvbnMiOltdfSwiZmxvd3MiOlt7ImlkIjoiUmVzZXJ2YXRpb25DcmVhdGVkRXZlbnQiLCJsYWJlbCI6IlJlc2VydmF0aW9uIENyZWF0ZWQiLCJpY29uIjoi8J+On++4jyIsImRlc2NyaXB0aW9uIjoicmVzZXJ2YXRpb25zIOKGkiBbcGF5bWVudHMsIG5vdGlmaWNhdGlvbnNdIHbDrWEgdG9waWMgUkVTRVJWQVRJT05fQ1JFQVRFRCIsImNvbG9yIjoiI2ZmOGM0MiIsInN0ZXBzIjpbeyJpZCI6MSwidHlwZSI6Imh0dHAiLCJmcm9tIjoiY2xpZW50IiwidG8iOiJyZXNlcnZhdGlvbnMiLCJsYWJlbCI6IlBPU1QgL3Jlc2VydmF0aW9ucyIsImRlc2MiOiJJbmljaWFyIHJlc2VydmE6IHNlbGVjY2lvbmFyIGZ1bmNpw7NuIHkgYXNpZW50b3MsIGJsb3F1ZW8gdGVtcG9yYWwgKDE1IG1pbikiLCJzeW5jQ2FsbHMiOlt7InRvIjoic2NyZWVuaW5ncyIsImxhYmVsIjoiR0VUIC9zY3JlZW5pbmdzL3tpZH0iLCJwb3J0IjoiU2NyZWVuaW5nU2VydmljZSJ9LHsidG8iOiJjdXN0b21lcnMiLCJsYWJlbCI6IkdFVCAvY3VzdG9tZXJzL3tpZH0iLCJwb3J0IjoiQ3VzdG9tZXJTZXJ2aWNlIn1dfSx7ImlkIjoyLCJ0eXBlIjoiZXZlbnQiLCJmcm9tIjoicmVzZXJ2YXRpb25zIiwiZXZlbnQiOiJSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsInRvcGljIjoiUkVTRVJWQVRJT05fQ1JFQVRFRCIsInRvIjpbInBheW1lbnRzIiwibm90aWZpY2F0aW9ucyJdLCJkZXNjIjoiUmVzZXJ2YXRpb25DcmVhdGVkRXZlbnQgcHVibGljYWRvIGVuIEthZmthICh0b3BpYzogUkVTRVJWQVRJT05fQ1JFQVRFRCkifSx7ImlkIjozLCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6InBheW1lbnRzIiwidG8iOiJwYXltZW50cyIsImxhYmVsIjoiUHJvY2VzYSBSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsImRlc2MiOiJwYXltZW50cyByZWFjY2lvbmEgYWwgZXZlbnRvIFJlc2VydmF0aW9uQ3JlYXRlZEV2ZW50In0seyJpZCI6NCwidHlwZSI6ImFjdGlvbiIsImZyb20iOiJub3RpZmljYXRpb25zIiwidG8iOiJub3RpZmljYXRpb25zIiwibGFiZWwiOiJQcm9jZXNhIFJlc2VydmF0aW9uQ3JlYXRlZEV2ZW50IiwiZGVzYyI6Im5vdGlmaWNhdGlvbnMgcmVhY2Npb25hIGFsIGV2ZW50byBSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCJ9XX0seyJpZCI6IlBheW1lbnRBcHByb3ZlZEV2ZW50IiwibGFiZWwiOiJQYXltZW50IEFwcHJvdmVkIiwiaWNvbiI6IvCfkrMiLCJkZXNjcmlwdGlvbiI6InBheW1lbnRzIOKGkiBbcmVzZXJ2YXRpb25zLCBub3RpZmljYXRpb25zXSB2w61hIHRvcGljIFBBWU1FTlRfQVBQUk9WRUQiLCJjb2xvciI6IiNlNjM5NTAiLCJzdGVwcyI6W3siaWQiOjEsInR5cGUiOiJodHRwIiwiZnJvbSI6ImNsaWVudCIsInRvIjoicGF5bWVudHMiLCJsYWJlbCI6IlBVVCAvcGF5bWVudHMve2lkfS9hcHByb3ZlIiwiZGVzYyI6IlJlZ2lzdHJhciBhcHJvYmFjacOzbiBkZSBwYWdvIHJlY2liaWRhIGRlc2RlIGxhIHBhc2FyZWxhIiwic3luY0NhbGxzIjpbeyJ0byI6InJlc2VydmF0aW9ucyIsImxhYmVsIjoiR0VUIC9yZXNlcnZhdGlvbnMve2lkfSIsInBvcnQiOiJSZXNlcnZhdGlvblNlcnZpY2UifV19LHsiaWQiOjIsInR5cGUiOiJldmVudCIsImZyb20iOiJwYXltZW50cyIsImV2ZW50IjoiUGF5bWVudEFwcHJvdmVkRXZlbnQiLCJ0b3BpYyI6IlBBWU1FTlRfQVBQUk9WRUQiLCJ0byI6WyJyZXNlcnZhdGlvbnMiLCJub3RpZmljYXRpb25zIl0sImRlc2MiOiJQYXltZW50QXBwcm92ZWRFdmVudCBwdWJsaWNhZG8gZW4gS2Fma2EgKHRvcGljOiBQQVlNRU5UX0FQUFJPVkVEKSJ9LHsiaWQiOjMsInR5cGUiOiJhY3Rpb24iLCJmcm9tIjoicmVzZXJ2YXRpb25zIiwidG8iOiJyZXNlcnZhdGlvbnMiLCJsYWJlbCI6IlByb2Nlc2EgUGF5bWVudEFwcHJvdmVkRXZlbnQiLCJkZXNjIjoicmVzZXJ2YXRpb25zIHJlYWNjaW9uYSBhbCBldmVudG8gUGF5bWVudEFwcHJvdmVkRXZlbnQifSx7ImlkIjo0LCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6Im5vdGlmaWNhdGlvbnMiLCJ0byI6Im5vdGlmaWNhdGlvbnMiLCJsYWJlbCI6IlByb2Nlc2EgUGF5bWVudEFwcHJvdmVkRXZlbnQiLCJkZXNjIjoibm90aWZpY2F0aW9ucyByZWFjY2lvbmEgYWwgZXZlbnRvIFBheW1lbnRBcHByb3ZlZEV2ZW50In1dfSx7ImlkIjoiUGF5bWVudFJlamVjdGVkRXZlbnQiLCJsYWJlbCI6IlBheW1lbnQgUmVqZWN0ZWQiLCJpY29uIjoi8J+SsyIsImRlc2NyaXB0aW9uIjoicGF5bWVudHMg4oaSIFtyZXNlcnZhdGlvbnMsIG5vdGlmaWNhdGlvbnNdIHbDrWEgdG9waWMgUEFZTUVOVF9SRUpFQ1RFRCIsImNvbG9yIjoiI2U2Mzk1MCIsInN0ZXBzIjpbeyJpZCI6MSwidHlwZSI6Imh0dHAiLCJmcm9tIjoiY2xpZW50IiwidG8iOiJwYXltZW50cyIsImxhYmVsIjoiUFVUIC9wYXltZW50cy97aWR9L3JlamVjdCIsImRlc2MiOiJSZWdpc3RyYXIgcmVjaGF6byBkZSBwYWdvIHJlY2liaWRvIGRlc2RlIGxhIHBhc2FyZWxhIiwic3luY0NhbGxzIjpbeyJ0byI6InJlc2VydmF0aW9ucyIsImxhYmVsIjoiR0VUIC9yZXNlcnZhdGlvbnMve2lkfSIsInBvcnQiOiJSZXNlcnZhdGlvblNlcnZpY2UifV19LHsiaWQiOjIsInR5cGUiOiJldmVudCIsImZyb20iOiJwYXltZW50cyIsImV2ZW50IjoiUGF5bWVudFJlamVjdGVkRXZlbnQiLCJ0b3BpYyI6IlBBWU1FTlRfUkVKRUNURUQiLCJ0byI6WyJyZXNlcnZhdGlvbnMiLCJub3RpZmljYXRpb25zIl0sImRlc2MiOiJQYXltZW50UmVqZWN0ZWRFdmVudCBwdWJsaWNhZG8gZW4gS2Fma2EgKHRvcGljOiBQQVlNRU5UX1JFSkVDVEVEKSJ9LHsiaWQiOjMsInR5cGUiOiJhY3Rpb24iLCJmcm9tIjoicmVzZXJ2YXRpb25zIiwidG8iOiJyZXNlcnZhdGlvbnMiLCJsYWJlbCI6IlByb2Nlc2EgUGF5bWVudFJlamVjdGVkRXZlbnQiLCJkZXNjIjoicmVzZXJ2YXRpb25zIHJlYWNjaW9uYSBhbCBldmVudG8gUGF5bWVudFJlamVjdGVkRXZlbnQifSx7ImlkIjo0LCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6Im5vdGlmaWNhdGlvbnMiLCJ0byI6Im5vdGlmaWNhdGlvbnMiLCJsYWJlbCI6IlByb2Nlc2EgUGF5bWVudFJlamVjdGVkRXZlbnQiLCJkZXNjIjoibm90aWZpY2F0aW9ucyByZWFjY2lvbmEgYWwgZXZlbnRvIFBheW1lbnRSZWplY3RlZEV2ZW50In1dfSx7ImlkIjoiUmVzZXJ2YXRpb25Db25maXJtZWRFdmVudCIsImxhYmVsIjoiUmVzZXJ2YXRpb24gQ29uZmlybWVkIiwiaWNvbiI6IvCfjp/vuI8iLCJkZXNjcmlwdGlvbiI6InJlc2VydmF0aW9ucyDihpIgW25vdGlmaWNhdGlvbnMsIGN1c3RvbWVyc10gdsOtYSB0b3BpYyBSRVNFUlZBVElPTl9DT05GSVJNRUQiLCJjb2xvciI6IiNmZjhjNDIiLCJzdGVwcyI6W3siaWQiOjEsInR5cGUiOiJodHRwIiwiZnJvbSI6ImNsaWVudCIsInRvIjoicmVzZXJ2YXRpb25zIiwibGFiZWwiOiJQVVQgL3Jlc2VydmF0aW9ucy97aWR9L2NvbmZpcm0iLCJkZXNjIjoiQ29uZmlybWFyIGxhIHJlc2VydmEgZGVzcHXDqXMgZGUgcmVjaWJpciBwYWdvIGFwcm9iYWRvIiwic3luY0NhbGxzIjpbeyJ0byI6InNjcmVlbmluZ3MiLCJsYWJlbCI6IkdFVCAvc2NyZWVuaW5ncy97aWR9IiwicG9ydCI6IlNjcmVlbmluZ1NlcnZpY2UifSx7InRvIjoiY3VzdG9tZXJzIiwibGFiZWwiOiJHRVQgL2N1c3RvbWVycy97aWR9IiwicG9ydCI6IkN1c3RvbWVyU2VydmljZSJ9XX0seyJpZCI6MiwidHlwZSI6ImV2ZW50IiwiZnJvbSI6InJlc2VydmF0aW9ucyIsImV2ZW50IjoiUmVzZXJ2YXRpb25Db25maXJtZWRFdmVudCIsInRvcGljIjoiUkVTRVJWQVRJT05fQ09ORklSTUVEIiwidG8iOlsibm90aWZpY2F0aW9ucyIsImN1c3RvbWVycyJdLCJkZXNjIjoiUmVzZXJ2YXRpb25Db25maXJtZWRFdmVudCBwdWJsaWNhZG8gZW4gS2Fma2EgKHRvcGljOiBSRVNFUlZBVElPTl9DT05GSVJNRUQpIn0seyJpZCI6MywidHlwZSI6ImFjdGlvbiIsImZyb20iOiJub3RpZmljYXRpb25zIiwidG8iOiJub3RpZmljYXRpb25zIiwibGFiZWwiOiJQcm9jZXNhIFJlc2VydmF0aW9uQ29uZmlybWVkRXZlbnQiLCJkZXNjIjoibm90aWZpY2F0aW9ucyByZWFjY2lvbmEgYWwgZXZlbnRvIFJlc2VydmF0aW9uQ29uZmlybWVkRXZlbnQifSx7ImlkIjo0LCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6ImN1c3RvbWVycyIsInRvIjoiY3VzdG9tZXJzIiwibGFiZWwiOiJQcm9jZXNhIFJlc2VydmF0aW9uQ29uZmlybWVkRXZlbnQiLCJkZXNjIjoiY3VzdG9tZXJzIHJlYWNjaW9uYSBhbCBldmVudG8gUmVzZXJ2YXRpb25Db25maXJtZWRFdmVudCJ9XX0seyJpZCI6IlJlc2VydmF0aW9uQ2FuY2VsbGVkRXZlbnQiLCJsYWJlbCI6IlJlc2VydmF0aW9uIENhbmNlbGxlZCIsImljb24iOiLwn46f77iPIiwiZGVzY3JpcHRpb24iOiJyZXNlcnZhdGlvbnMg4oaSIFtwYXltZW50cywgc2NyZWVuaW5ncywgbm90aWZpY2F0aW9uc10gdsOtYSB0b3BpYyBSRVNFUlZBVElPTl9DQU5DRUxMRUQiLCJjb2xvciI6IiNmZjhjNDIiLCJzdGVwcyI6W3siaWQiOjEsInR5cGUiOiJodHRwIiwiZnJvbSI6ImNsaWVudCIsInRvIjoicmVzZXJ2YXRpb25zIiwibGFiZWwiOiJQVVQgL3Jlc2VydmF0aW9ucy97aWR9L2NhbmNlbCIsImRlc2MiOiJDYW5jZWxhciBsYSByZXNlcnZhIHkgbGliZXJhciBsb3MgYXNpZW50b3Mgc2VsZWNjaW9uYWRvcyIsInN5bmNDYWxscyI6W3sidG8iOiJzY3JlZW5pbmdzIiwibGFiZWwiOiJHRVQgL3NjcmVlbmluZ3Mve2lkfSIsInBvcnQiOiJTY3JlZW5pbmdTZXJ2aWNlIn0seyJ0byI6ImN1c3RvbWVycyIsImxhYmVsIjoiR0VUIC9jdXN0b21lcnMve2lkfSIsInBvcnQiOiJDdXN0b21lclNlcnZpY2UifV19LHsiaWQiOjIsInR5cGUiOiJldmVudCIsImZyb20iOiJyZXNlcnZhdGlvbnMiLCJldmVudCI6IlJlc2VydmF0aW9uQ2FuY2VsbGVkRXZlbnQiLCJ0b3BpYyI6IlJFU0VSVkFUSU9OX0NBTkNFTExFRCIsInRvIjpbInBheW1lbnRzIiwic2NyZWVuaW5ncyIsIm5vdGlmaWNhdGlvbnMiXSwiZGVzYyI6IlJlc2VydmF0aW9uQ2FuY2VsbGVkRXZlbnQgcHVibGljYWRvIGVuIEthZmthICh0b3BpYzogUkVTRVJWQVRJT05fQ0FOQ0VMTEVEKSJ9LHsiaWQiOjMsInR5cGUiOiJhY3Rpb24iLCJmcm9tIjoicGF5bWVudHMiLCJ0byI6InBheW1lbnRzIiwibGFiZWwiOiJQcm9jZXNhIFJlc2VydmF0aW9uQ2FuY2VsbGVkRXZlbnQiLCJkZXNjIjoicGF5bWVudHMgcmVhY2Npb25hIGFsIGV2ZW50byBSZXNlcnZhdGlvbkNhbmNlbGxlZEV2ZW50In0seyJpZCI6NCwidHlwZSI6ImFjdGlvbiIsImZyb20iOiJzY3JlZW5pbmdzIiwidG8iOiJzY3JlZW5pbmdzIiwibGFiZWwiOiJDYW5jZWxTY3JlZW5pbmcgKFBVVCAvc2NyZWVuaW5ncy97aWR9L2NhbmNlbCkiLCJkZXNjIjoic2NyZWVuaW5ncyByZWFjY2lvbmEgYWwgZXZlbnRvIFJlc2VydmF0aW9uQ2FuY2VsbGVkRXZlbnQifSx7ImlkIjo1LCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6Im5vdGlmaWNhdGlvbnMiLCJ0byI6Im5vdGlmaWNhdGlvbnMiLCJsYWJlbCI6IlByb2Nlc2EgUmVzZXJ2YXRpb25DYW5jZWxsZWRFdmVudCIsImRlc2MiOiJub3RpZmljYXRpb25zIHJlYWNjaW9uYSBhbCBldmVudG8gUmVzZXJ2YXRpb25DYW5jZWxsZWRFdmVudCJ9XX0seyJpZCI6IlJlc2VydmF0aW9uRXhwaXJlZEV2ZW50IiwibGFiZWwiOiJSZXNlcnZhdGlvbiBFeHBpcmVkIiwiaWNvbiI6IvCfjp/vuI8iLCJkZXNjcmlwdGlvbiI6InJlc2VydmF0aW9ucyDihpIgW3NjcmVlbmluZ3MsIG5vdGlmaWNhdGlvbnNdIHbDrWEgdG9waWMgUkVTRVJWQVRJT05fRVhQSVJFRCIsImNvbG9yIjoiI2ZmOGM0MiIsInN0ZXBzIjpbeyJpZCI6MSwidHlwZSI6Imh0dHAiLCJmcm9tIjoiY2xpZW50IiwidG8iOiJyZXNlcnZhdGlvbnMiLCJsYWJlbCI6IlBVVCAvcmVzZXJ2YXRpb25zL3tpZH0vZXhwaXJlIiwiZGVzYyI6IkV4cGlyYXIgcmVzZXJ2YSBubyBwYWdhZGEgZGVudHJvIGRlbCB0aWVtcG8gbMOtbWl0ZSBkZSBibG9xdWVvIiwic3luY0NhbGxzIjpbeyJ0byI6InNjcmVlbmluZ3MiLCJsYWJlbCI6IkdFVCAvc2NyZWVuaW5ncy97aWR9IiwicG9ydCI6IlNjcmVlbmluZ1NlcnZpY2UifSx7InRvIjoiY3VzdG9tZXJzIiwibGFiZWwiOiJHRVQgL2N1c3RvbWVycy97aWR9IiwicG9ydCI6IkN1c3RvbWVyU2VydmljZSJ9XX0seyJpZCI6MiwidHlwZSI6ImV2ZW50IiwiZnJvbSI6InJlc2VydmF0aW9ucyIsImV2ZW50IjoiUmVzZXJ2YXRpb25FeHBpcmVkRXZlbnQiLCJ0b3BpYyI6IlJFU0VSVkFUSU9OX0VYUElSRUQiLCJ0byI6WyJzY3JlZW5pbmdzIiwibm90aWZpY2F0aW9ucyJdLCJkZXNjIjoiUmVzZXJ2YXRpb25FeHBpcmVkRXZlbnQgcHVibGljYWRvIGVuIEthZmthICh0b3BpYzogUkVTRVJWQVRJT05fRVhQSVJFRCkifSx7ImlkIjozLCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6InNjcmVlbmluZ3MiLCJ0byI6InNjcmVlbmluZ3MiLCJsYWJlbCI6IlByb2Nlc2EgUmVzZXJ2YXRpb25FeHBpcmVkRXZlbnQiLCJkZXNjIjoic2NyZWVuaW5ncyByZWFjY2lvbmEgYWwgZXZlbnRvIFJlc2VydmF0aW9uRXhwaXJlZEV2ZW50In0seyJpZCI6NCwidHlwZSI6ImFjdGlvbiIsImZyb20iOiJub3RpZmljYXRpb25zIiwidG8iOiJub3RpZmljYXRpb25zIiwibGFiZWwiOiJQcm9jZXNhIFJlc2VydmF0aW9uRXhwaXJlZEV2ZW50IiwiZGVzYyI6Im5vdGlmaWNhdGlvbnMgcmVhY2Npb25hIGFsIGV2ZW50byBSZXNlcnZhdGlvbkV4cGlyZWRFdmVudCJ9XX0seyJpZCI6IlNjcmVlbmluZ0NhbmNlbGxlZEV2ZW50IiwibGFiZWwiOiJTY3JlZW5pbmcgQ2FuY2VsbGVkIiwiaWNvbiI6IvCfj5vvuI8iLCJkZXNjcmlwdGlvbiI6InNjcmVlbmluZ3Mg4oaSIFtyZXNlcnZhdGlvbnMsIG5vdGlmaWNhdGlvbnNdIHbDrWEgdG9waWMgU0NSRUVOSU5HX0NBTkNFTExFRCIsImNvbG9yIjoiI2Y1Yzg0MiIsInN0ZXBzIjpbeyJpZCI6MSwidHlwZSI6Imh0dHAiLCJmcm9tIjoiY2xpZW50IiwidG8iOiJzY3JlZW5pbmdzIiwibGFiZWwiOiJQVVQgL3NjcmVlbmluZ3Mve2lkfS9jYW5jZWwiLCJkZXNjIjoiQ2FuY2VsYXIgdW5hIGZ1bmNpw7NuIHByb2dyYW1hZGEgeSBsaWJlcmFyIHRvZGFzIGxhcyByZXNlcnZhcyIsInN5bmNDYWxscyI6W3sidG8iOiJtb3ZpZXMiLCJsYWJlbCI6IkdFVCAvbW92aWVzL3tpZH0iLCJwb3J0IjoiTW92aWVTZXJ2aWNlIn0seyJ0byI6InRoZWF0ZXJzIiwibGFiZWwiOiJHRVQgL3RoZWF0ZXJzL3tpZH0iLCJwb3J0IjoiVGhlYXRlclNlcnZpY2UifV19LHsiaWQiOjIsInR5cGUiOiJldmVudCIsImZyb20iOiJzY3JlZW5pbmdzIiwiZXZlbnQiOiJTY3JlZW5pbmdDYW5jZWxsZWRFdmVudCIsInRvcGljIjoiU0NSRUVOSU5HX0NBTkNFTExFRCIsInRvIjpbInJlc2VydmF0aW9ucyIsIm5vdGlmaWNhdGlvbnMiXSwiZGVzYyI6IlNjcmVlbmluZ0NhbmNlbGxlZEV2ZW50IHB1YmxpY2FkbyBlbiBLYWZrYSAodG9waWM6IFNDUkVFTklOR19DQU5DRUxMRUQpIn0seyJpZCI6MywidHlwZSI6ImFjdGlvbiIsImZyb20iOiJyZXNlcnZhdGlvbnMiLCJ0byI6InJlc2VydmF0aW9ucyIsImxhYmVsIjoiQ2FuY2VsUmVzZXJ2YXRpb24gKFBVVCAvcmVzZXJ2YXRpb25zL3tpZH0vY2FuY2VsKSIsImRlc2MiOiJyZXNlcnZhdGlvbnMgcmVhY2Npb25hIGFsIGV2ZW50byBTY3JlZW5pbmdDYW5jZWxsZWRFdmVudCJ9LHsiaWQiOjQsInR5cGUiOiJhY3Rpb24iLCJmcm9tIjoibm90aWZpY2F0aW9ucyIsInRvIjoibm90aWZpY2F0aW9ucyIsImxhYmVsIjoiUHJvY2VzYSBTY3JlZW5pbmdDYW5jZWxsZWRFdmVudCIsImRlc2MiOiJub3RpZmljYXRpb25zIHJlYWNjaW9uYSBhbCBldmVudG8gU2NyZWVuaW5nQ2FuY2VsbGVkRXZlbnQifV19LHsiaWQiOiJQcml2YXRlRXZlbnRSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsImxhYmVsIjoiUHJpdmF0ZSBFdmVudCBSZXNlcnZhdGlvbiBDcmVhdGVkIiwiaWNvbiI6IvCfjp/vuI8iLCJkZXNjcmlwdGlvbiI6InJlc2VydmF0aW9ucyDihpIgW3NjcmVlbmluZ3MsIHBheW1lbnRzLCBub3RpZmljYXRpb25zXSB2w61hIHRvcGljIFBSSVZBVEVfRVZFTlRfUkVTRVJWQVRJT05fQ1JFQVRFRCIsImNvbG9yIjoiI2ZmOGM0MiIsInN0ZXBzIjpbeyJpZCI6MSwidHlwZSI6Imh0dHAiLCJmcm9tIjoiY2xpZW50IiwidG8iOiJyZXNlcnZhdGlvbnMiLCJsYWJlbCI6IlBPU1QgL3Jlc2VydmF0aW9ucyIsImRlc2MiOiJJbmljaWFyIHJlc2VydmE6IHNlbGVjY2lvbmFyIGZ1bmNpw7NuIHkgYXNpZW50b3MsIGJsb3F1ZW8gdGVtcG9yYWwgKDE1IG1pbikiLCJzeW5jQ2FsbHMiOlt7InRvIjoic2NyZWVuaW5ncyIsImxhYmVsIjoiR0VUIC9zY3JlZW5pbmdzL3tpZH0iLCJwb3J0IjoiU2NyZWVuaW5nU2VydmljZSJ9LHsidG8iOiJjdXN0b21lcnMiLCJsYWJlbCI6IkdFVCAvY3VzdG9tZXJzL3tpZH0iLCJwb3J0IjoiQ3VzdG9tZXJTZXJ2aWNlIn1dfSx7ImlkIjoyLCJ0eXBlIjoiZXZlbnQiLCJmcm9tIjoicmVzZXJ2YXRpb25zIiwiZXZlbnQiOiJQcml2YXRlRXZlbnRSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsInRvcGljIjoiUFJJVkFURV9FVkVOVF9SRVNFUlZBVElPTl9DUkVBVEVEIiwidG8iOlsic2NyZWVuaW5ncyIsInBheW1lbnRzIiwibm90aWZpY2F0aW9ucyJdLCJkZXNjIjoiUHJpdmF0ZUV2ZW50UmVzZXJ2YXRpb25DcmVhdGVkRXZlbnQgcHVibGljYWRvIGVuIEthZmthICh0b3BpYzogUFJJVkFURV9FVkVOVF9SRVNFUlZBVElPTl9DUkVBVEVEKSJ9LHsiaWQiOjMsInR5cGUiOiJhY3Rpb24iLCJmcm9tIjoic2NyZWVuaW5ncyIsInRvIjoic2NyZWVuaW5ncyIsImxhYmVsIjoiUHJvY2VzYSBQcml2YXRlRXZlbnRSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsImRlc2MiOiJzY3JlZW5pbmdzIHJlYWNjaW9uYSBhbCBldmVudG8gUHJpdmF0ZUV2ZW50UmVzZXJ2YXRpb25DcmVhdGVkRXZlbnQifSx7ImlkIjo0LCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6InBheW1lbnRzIiwidG8iOiJwYXltZW50cyIsImxhYmVsIjoiUHJvY2VzYSBQcml2YXRlRXZlbnRSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCIsImRlc2MiOiJwYXltZW50cyByZWFjY2lvbmEgYWwgZXZlbnRvIFByaXZhdGVFdmVudFJlc2VydmF0aW9uQ3JlYXRlZEV2ZW50In0seyJpZCI6NSwidHlwZSI6ImFjdGlvbiIsImZyb20iOiJub3RpZmljYXRpb25zIiwidG8iOiJub3RpZmljYXRpb25zIiwibGFiZWwiOiJQcm9jZXNhIFByaXZhdGVFdmVudFJlc2VydmF0aW9uQ3JlYXRlZEV2ZW50IiwiZGVzYyI6Im5vdGlmaWNhdGlvbnMgcmVhY2Npb25hIGFsIGV2ZW50byBQcml2YXRlRXZlbnRSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCJ9XX0seyJpZCI6IlRoZWF0ZXJMb2NrZWRGb3JQcml2YXRlRXZlbnRFdmVudCIsImxhYmVsIjoiVGhlYXRlciBMb2NrZWQgRm9yIFByaXZhdGUgRXZlbnQiLCJpY29uIjoi8J+Pm++4jyIsImRlc2NyaXB0aW9uIjoic2NyZWVuaW5ncyDihpIgW25vdGlmaWNhdGlvbnNdIHbDrWEgdG9waWMgVEhFQVRFUl9MT0NLRURfRk9SX1BSSVZBVEVfRVZFTlQiLCJjb2xvciI6IiNmNWM4NDIiLCJzdGVwcyI6W3siaWQiOjEsInR5cGUiOiJodHRwIiwiZnJvbSI6ImNsaWVudCIsInRvIjoic2NyZWVuaW5ncyIsImxhYmVsIjoidHJpZ2dlciAvdGhlYXRlcmxvY2tlZGZvcnByaXZhdGVldmVudCIsImRlc2MiOiJBY2Npw7NuIHF1ZSBkZXNlbmNhZGVuYSBlbCBldmVudG8ifSx7ImlkIjoyLCJ0eXBlIjoiZXZlbnQiLCJmcm9tIjoic2NyZWVuaW5ncyIsImV2ZW50IjoiVGhlYXRlckxvY2tlZEZvclByaXZhdGVFdmVudEV2ZW50IiwidG9waWMiOiJUSEVBVEVSX0xPQ0tFRF9GT1JfUFJJVkFURV9FVkVOVCIsInRvIjpbIm5vdGlmaWNhdGlvbnMiXSwiZGVzYyI6IlRoZWF0ZXJMb2NrZWRGb3JQcml2YXRlRXZlbnRFdmVudCBwdWJsaWNhZG8gZW4gS2Fma2EgKHRvcGljOiBUSEVBVEVSX0xPQ0tFRF9GT1JfUFJJVkFURV9FVkVOVCkifSx7ImlkIjozLCJ0eXBlIjoiYWN0aW9uIiwiZnJvbSI6Im5vdGlmaWNhdGlvbnMiLCJ0byI6Im5vdGlmaWNhdGlvbnMiLCJsYWJlbCI6IlByb2Nlc2EgVGhlYXRlckxvY2tlZEZvclByaXZhdGVFdmVudEV2ZW50IiwiZGVzYyI6Im5vdGlmaWNhdGlvbnMgcmVhY2Npb25hIGFsIGV2ZW50byBUaGVhdGVyTG9ja2VkRm9yUHJpdmF0ZUV2ZW50RXZlbnQifV19XSwidmFsaWRhdGlvbiI6eyJlcnJvcnMiOltdLCJ3YXJuaW5ncyI6WyInbm90aWZpY2F0aW9ucycgbm8gdGllbmUgZW5kcG9pbnRzIGV4cHVlc3RvcyAoZXhwb3Nlc1tdIHZhY8OtbyBvIGF1c2VudGUpIiwiR2FwIGRlIGNvbXBvcnRhbWllbnRvOiAnRXhwaXJlUmVzZXJ2YXRpb24nIChQVVQgL3Jlc2VydmF0aW9ucy97aWR9L2V4cGlyZSkgZW4gJ3Jlc2VydmF0aW9ucycgbm8gdGllbmUgbmluZ8O6biBldmVudG8gbmkgbGxhbWFkYSBzw61uY3JvbmEgcXVlIGxvIGFjdGl2ZS4gUHVlZGUgbmVjZXNpdGFyIHVuIHNjaGVkdWxlciBvIGpvYiBwZXJpw7NkaWNvLiIsIkdhcCBkZSBjb21wb3J0YW1pZW50bzogJ1Byb2Nlc3NSZWZ1bmQnIChQVVQgL3BheW1lbnRzL3tpZH0vcmVmdW5kKSBlbiAncGF5bWVudHMnIG5vIHRpZW5lIG5pbmfDum4gZXZlbnRvIG5pIGxsYW1hZGEgc8OtbmNyb25hIHF1ZSBsbyBhY3RpdmUuIFB1ZWRlIG5lY2VzaXRhciB1biBzY2hlZHVsZXIgbyBqb2IgcGVyacOzZGljby4iLCJBY29wbGFtaWVudG8gYXNpbcOpdHJpY286ICdyZXNlcnZhdGlvbnMnIGxsYW1hIHPDrW5jcm9uYW1lbnRlIGEgJ3NjcmVlbmluZ3MnLCBtaWVudHJhcyAnc2NyZWVuaW5ncycgcmVzcG9uZGUgdsOtYSBldmVudG9zIGFzw61uY3Jvbm9zIChTY3JlZW5pbmdDYW5jZWxsZWRFdmVudCkuIENvbnNpZGVyYXIgcGFzYXIgbG9zIGRhdG9zIG5lY2VzYXJpb3MgZGlyZWN0YW1lbnRlIGVuIGVsIGV2ZW50byBwYXJhIGVsaW1pbmFyIGxhIGxsYW1hZGEgc8OtbmNyb25hLiIsIkFjb3BsYW1pZW50byBhc2ltw6l0cmljbzogJ3BheW1lbnRzJyBsbGFtYSBzw61uY3JvbmFtZW50ZSBhICdyZXNlcnZhdGlvbnMnLCBtaWVudHJhcyAncmVzZXJ2YXRpb25zJyByZXNwb25kZSB2w61hIGV2ZW50b3MgYXPDrW5jcm9ub3MgKFJlc2VydmF0aW9uQ3JlYXRlZEV2ZW50LCBSZXNlcnZhdGlvbkNhbmNlbGxlZEV2ZW50LCBQcml2YXRlRXZlbnRSZXNlcnZhdGlvbkNyZWF0ZWRFdmVudCkuIENvbnNpZGVyYXIgcGFzYXIgbG9zIGRhdG9zIG5lY2VzYXJpb3MgZGlyZWN0YW1lbnRlIGVuIGVsIGV2ZW50byBwYXJhIGVsaW1pbmFyIGxhIGxsYW1hZGEgc8OtbmNyb25hLiJdLCJvayI6WyJQcm9kdWN0b3IgJ3Jlc2VydmF0aW9ucycgZGVsIGV2ZW50byAnUmVzZXJ2YXRpb25DcmVhdGVkRXZlbnQnIGV4aXN0ZSDinJMiLCJQcm9kdWN0b3IgJ3BheW1lbnRzJyBkZWwgZXZlbnRvICdQYXltZW50QXBwcm92ZWRFdmVudCcgZXhpc3RlIOKckyIsIlByb2R1Y3RvciAncGF5bWVudHMnIGRlbCBldmVudG8gJ1BheW1lbnRSZWplY3RlZEV2ZW50JyBleGlzdGUg4pyTIiwiUHJvZHVjdG9yICdyZXNlcnZhdGlvbnMnIGRlbCBldmVudG8gJ1Jlc2VydmF0aW9uQ29uZmlybWVkRXZlbnQnIGV4aXN0ZSDinJMiLCJQcm9kdWN0b3IgJ3Jlc2VydmF0aW9ucycgZGVsIGV2ZW50byAnUmVzZXJ2YXRpb25DYW5jZWxsZWRFdmVudCcgZXhpc3RlIOKckyIsIlByb2R1Y3RvciAncmVzZXJ2YXRpb25zJyBkZWwgZXZlbnRvICdSZXNlcnZhdGlvbkV4cGlyZWRFdmVudCcgZXhpc3RlIOKckyIsIlByb2R1Y3RvciAnc2NyZWVuaW5ncycgZGVsIGV2ZW50byAnU2NyZWVuaW5nQ2FuY2VsbGVkRXZlbnQnIGV4aXN0ZSDinJMiLCJQcm9kdWN0b3IgJ3Jlc2VydmF0aW9ucycgZGVsIGV2ZW50byAnUHJpdmF0ZUV2ZW50UmVzZXJ2YXRpb25DcmVhdGVkRXZlbnQnIGV4aXN0ZSDinJMiLCJQcm9kdWN0b3IgJ3NjcmVlbmluZ3MnIGRlbCBldmVudG8gJ1RoZWF0ZXJMb2NrZWRGb3JQcml2YXRlRXZlbnRFdmVudCcgZXhpc3RlIOKckyIsIlRvZG9zIGxvcyBjb25zdW1pZG9yZXMgZGUgZXZlbnRvcyBlc3TDoW4gZGVjbGFyYWRvcyBjb21vIG3Ds2R1bG9zIOKckyIsIlRvZG9zIGxvcyBlbmRwb2ludHMgdXNhZG9zIGVuIGludGVncmFjaW9uZXMgc8OtbmNyb25hcyBlc3TDoW4gZGVjbGFyYWRvcyBlbiBsb3MgbcOzZHVsb3MgZGVzdGlubyDinJMiLCJObyBzZSBkZXRlY3Rhcm9uIGNpY2xvcyBuaSBhY29wbGFtaWVudG8gc8OtbmNyb25vIGJpZGlyZWNjaW9uYWwg4pyTIiwiJ2N1c3RvbWVycycgZXMgY29uc3VtaWRvciBwdXJvIGRlIGV2ZW50b3MgKGNvcnJlY3RvOiBubyBwcm9kdWNlIGV2ZW50b3MgcHJvcGlvcykiLCInbm90aWZpY2F0aW9ucycgZXMgY29uc3VtaWRvciBwdXJvIGRlIGV2ZW50b3MgKGNvcnJlY3RvOiBubyBwcm9kdWNlIGV2ZW50b3MgcHJvcGlvcykiLCInbm90aWZpY2F0aW9ucycgbm8gZXhwb25lIGVuZHBvaW50cyBSRVNUIGRpcmVjdGFtZW50ZSAobcOzZHVsbyBkZSBpbnRlZ3JhY2nDs24pIiwiJ3NjcmVlbmluZ3MnIHRpZW5lIGVuZHBvaW50cyBhY2Nlc2libGVzIHRhbnRvIHPDrW5jcm9uYW1lbnRlIGNvbW8gdsOtYSBldmVudG9zIChkaXNlw7FvIGR1YWwg4oCUIGludGVuY2lvbmFsKSIsIidjdXN0b21lcnMnIHRpZW5lIGVuZHBvaW50cyBhY2Nlc2libGVzIHRhbnRvIHPDrW5jcm9uYW1lbnRlIGNvbW8gdsOtYSBldmVudG9zIChkaXNlw7FvIGR1YWwg4oCUIGludGVuY2lvbmFsKSIsIidyZXNlcnZhdGlvbnMnIHRpZW5lIGVuZHBvaW50cyBhY2Nlc2libGVzIHRhbnRvIHPDrW5jcm9uYW1lbnRlIGNvbW8gdsOtYSBldmVudG9zIChkaXNlw7FvIGR1YWwg4oCUIGludGVuY2lvbmFsKSJdLCJzY29yZSI6ODh9LCJnZW5lcmF0ZWRBdCI6IjIwMjYtMDMtMTFUMjM6MTk6NTEuNTcxWiJ9';
31
+ var bytes = Uint8Array.from(atob(_d), function(c) { return c.charCodeAt(0); });
32
+ window.__EVA_DATA__ = JSON.parse(new TextDecoder('utf-8').decode(bytes));
33
+ })();
34
+ </script>
35
+
36
+ <script type="text/babel">
37
+ const { useState, useRef, useEffect } = React;
38
+
39
+ // ─── Injected data ──────────────────────────────────────────────────────
40
+ const {
41
+ systemName,
42
+ modules: MODULES_LIST,
43
+ events: EVENTS,
44
+ syncIntegrations: SYNC_INTEGRATIONS,
45
+ endpoints: ENDPOINTS,
46
+ flows: FLOWS_LIST,
47
+ validation: VALIDATION,
48
+ generatedAt,
49
+ } = window.__EVA_DATA__;
50
+
51
+ // Convert arrays to maps for fast lookup
52
+ const MODULES = Object.fromEntries(MODULES_LIST.map(m => [m.id, m]));
53
+ const FLOWS = Object.fromEntries(FLOWS_LIST.map(f => [f.id, f]));
54
+
55
+ // ─── Design system ──────────────────────────────────────────────────────
56
+ const C = {
57
+ bg: "#0a0a0f",
58
+ surface: "#12121a",
59
+ surfaceHover: "#1a1a28",
60
+ border: "#1e1e30",
61
+ borderBright: "#2e2e50",
62
+ accent: "#e63950",
63
+ gold: "#f5c842",
64
+ green: "#2dcc8f",
65
+ blue: "#4a9eff",
66
+ purple: "#9b6dff",
67
+ orange: "#ff8c42",
68
+ text: "#e8e8f0",
69
+ textMuted: "#8c8caa",
70
+ textDim: "#b4b4cc",
71
+ };
72
+
73
+ // ─── Primitive components ───────────────────────────────────────────────
74
+ const Tag = ({ color, children }) => (
75
+ <span style={{
76
+ background: color + "22", color, border: `1px solid ${color}44`,
77
+ borderRadius: 4, padding: "1px 8px", fontSize: 11, fontWeight: 600,
78
+ fontFamily: "'JetBrains Mono', 'Courier New', monospace", letterSpacing: 0.3, display: "inline-block",
79
+ }}>
80
+ {children}
81
+ </span>
82
+ );
83
+
84
+ const Badge = ({ color, children }) => (
85
+ <span style={{
86
+ background: color + "33", color, borderRadius: 20,
87
+ padding: "2px 10px", fontSize: 11, fontWeight: 700,
88
+ }}>
89
+ {children}
90
+ </span>
91
+ );
92
+
93
+ // ─── ModuleCard ─────────────────────────────────────────────────────────
94
+ function ModuleCard({ id, selected, onClick }) {
95
+ const mod = MODULES[id];
96
+ if (!mod) return null;
97
+ return (
98
+ <div onClick={onClick} style={{
99
+ background: selected ? mod.color + "22" : C.surface,
100
+ border: `1px solid ${selected ? mod.color : C.border}`,
101
+ borderRadius: 10, padding: "10px 14px", cursor: "pointer",
102
+ transition: "all 0.18s", display: "flex", alignItems: "center", gap: 10,
103
+ boxShadow: selected ? `0 0 14px ${mod.color}33` : "none",
104
+ }}>
105
+ <span style={{ fontSize: 20 }}>{mod.icon}</span>
106
+ <div>
107
+ <div style={{ fontWeight: 700, color: selected ? mod.color : C.text, fontSize: 13 }}>{mod.label}</div>
108
+ <div style={{ color: C.textMuted, fontSize: 10, marginTop: 1 }}>{mod.desc}</div>
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ // ─── ValidationTab ──────────────────────────────────────────────────────
115
+ function ValidationTab() {
116
+ const [expanded, setExpanded] = useState({ errors: true, warnings: true, ok: false });
117
+
118
+ const score = VALIDATION.score;
119
+ const scoreColor = score > 80 ? C.green : score > 60 ? C.gold : C.accent;
120
+
121
+ function Section({ title, items, color, icon, sectionKey }) {
122
+ const isOpen = expanded[sectionKey];
123
+ return (
124
+ <div style={{ marginBottom: 16 }}>
125
+ <div
126
+ onClick={() => setExpanded(e => ({ ...e, [sectionKey]: !e[sectionKey] }))}
127
+ style={{
128
+ display: "flex", alignItems: "center", gap: 10, cursor: "pointer",
129
+ padding: "10px 16px", background: C.surface, border: `1px solid ${C.border}`,
130
+ borderRadius: isOpen ? "8px 8px 0 0" : 8, userSelect: "none",
131
+ }}
132
+ >
133
+ <span style={{ fontSize: 18 }}>{icon}</span>
134
+ <span style={{ fontWeight: 700, color, flex: 1 }}>{title}</span>
135
+ <Badge color={color}>{items.length}</Badge>
136
+ <span style={{ color: C.textMuted, fontSize: 12 }}>{isOpen ? "▲" : "▼"}</span>
137
+ </div>
138
+ {isOpen && items.length > 0 && (
139
+ <div style={{ border: `1px solid ${C.border}`, borderTop: "none", borderRadius: "0 0 8px 8px", overflow: "hidden" }}>
140
+ {items.map((item, i) => (
141
+ <div key={i} style={{
142
+ padding: "10px 16px",
143
+ borderBottom: i < items.length - 1 ? `1px solid ${C.border}` : "none",
144
+ display: "flex", alignItems: "flex-start", gap: 10,
145
+ background: i % 2 === 0 ? C.bg : C.surface,
146
+ }}>
147
+ <span style={{ color, marginTop: 1, flexShrink: 0 }}>•</span>
148
+ <span style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
149
+ {typeof item === "string" ? item : item.msg}
150
+ </span>
151
+ </div>
152
+ ))}
153
+ </div>
154
+ )}
155
+ {isOpen && items.length === 0 && (
156
+ <div style={{ border: `1px solid ${C.border}`, borderTop: "none", borderRadius: "0 0 8px 8px", padding: "12px 16px", background: C.bg }}>
157
+ <span style={{ color: C.textMuted, fontSize: 12 }}>— ninguno —</span>
158
+ </div>
159
+ )}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ return (
165
+ <div>
166
+ {/* Score cards */}
167
+ <div style={{ display: "flex", gap: 16, marginBottom: 24, flexWrap: "wrap" }}>
168
+ <div style={{
169
+ flex: 1, minWidth: 160, background: C.surface, border: `1px solid ${scoreColor}44`,
170
+ borderRadius: 12, padding: 20, textAlign: "center",
171
+ boxShadow: `0 0 24px ${scoreColor}22`,
172
+ }}>
173
+ <div style={{ fontSize: 52, fontWeight: 900, color: scoreColor, fontFamily: "monospace", lineHeight: 1 }}>
174
+ {score}%
175
+ </div>
176
+ <div style={{ color: C.textMuted, fontSize: 11, marginTop: 6, letterSpacing: 1 }}>SCORE DE CALIDAD</div>
177
+ </div>
178
+ {[
179
+ { label: "Errores", count: VALIDATION.errors.length, color: C.accent, icon: "🔴" },
180
+ { label: "Advertencias", count: VALIDATION.warnings.length, color: C.gold, icon: "🟡" },
181
+ { label: "Validados", count: VALIDATION.ok.length, color: C.green, icon: "🟢" },
182
+ ].map(s => (
183
+ <div key={s.label} style={{
184
+ flex: 1, minWidth: 130, background: C.surface, border: `1px solid ${C.border}`,
185
+ borderRadius: 12, padding: 20, textAlign: "center",
186
+ }}>
187
+ <div style={{ fontSize: 38, fontWeight: 900, color: s.color, fontFamily: "monospace" }}>{s.count}</div>
188
+ <div style={{ color: C.textMuted, fontSize: 11, marginTop: 6, letterSpacing: 0.5 }}>
189
+ {s.icon} {s.label.toUpperCase()}
190
+ </div>
191
+ </div>
192
+ ))}
193
+ </div>
194
+
195
+ <Section title="Errores críticos" items={VALIDATION.errors} color={C.accent} icon="🔴" sectionKey="errors" />
196
+ <Section title="Advertencias" items={VALIDATION.warnings} color={C.gold} icon="🟡" sectionKey="warnings" />
197
+ <Section title="Validaciones pasadas" items={VALIDATION.ok} color={C.green} icon="🟢" sectionKey="ok" />
198
+ </div>
199
+ );
200
+ }
201
+
202
+ // ─── FlowSimulator ──────────────────────────────────────────────────────
203
+ function FlowSimulator() {
204
+ const defaultFlow = FLOWS_LIST[0]?.id || null;
205
+ const [selectedFlowId, setSelectedFlowId] = useState(defaultFlow);
206
+ const [currentStep, setCurrentStep] = useState(-1);
207
+ const [running, setRunning] = useState(false);
208
+ const [completed, setCompleted] = useState(false);
209
+ const intervalRef = useRef(null);
210
+
211
+ const flow = FLOWS[selectedFlowId] || FLOWS_LIST[0];
212
+
213
+ if (!flow) {
214
+ return (
215
+ <div style={{ color: C.textMuted, padding: 40, textAlign: "center" }}>
216
+ No hay flujos de eventos declarados en integrations.async
217
+ </div>
218
+ );
219
+ }
220
+
221
+ const reset = () => {
222
+ clearInterval(intervalRef.current);
223
+ setCurrentStep(-1);
224
+ setRunning(false);
225
+ setCompleted(false);
226
+ };
227
+
228
+ const selectFlow = (id) => {
229
+ reset();
230
+ setSelectedFlowId(id);
231
+ };
232
+
233
+ const run = () => {
234
+ if (running) return;
235
+ reset();
236
+ setRunning(true);
237
+ let step = 0;
238
+ setCurrentStep(0);
239
+ intervalRef.current = setInterval(() => {
240
+ step++;
241
+ if (step >= flow.steps.length) {
242
+ clearInterval(intervalRef.current);
243
+ setRunning(false);
244
+ setCompleted(true);
245
+ setCurrentStep(flow.steps.length);
246
+ } else {
247
+ setCurrentStep(step);
248
+ }
249
+ }, 1100);
250
+ };
251
+
252
+ useEffect(() => () => clearInterval(intervalRef.current), []);
253
+
254
+ const getStepIcon = (step) => {
255
+ if (step.type === "event") return "⚡";
256
+ if (step.type === "external") return "🌐";
257
+ if (step.type === "action") return "📤";
258
+ return "→";
259
+ };
260
+
261
+ const getStepColor = (step) => {
262
+ if (step.type === "event") return C.gold;
263
+ if (step.type === "external") return C.purple;
264
+ if (step.type === "action") return C.green;
265
+ return C.blue;
266
+ };
267
+
268
+ const getActorColor = (actor) => {
269
+ if (MODULES[actor]) return MODULES[actor].color;
270
+ if (actor === "client" || actor === "organizer") return C.green;
271
+ if (actor === "admin") return C.purple;
272
+ if (actor === "gateway") return C.textDim;
273
+ return C.textMuted;
274
+ };
275
+
276
+ return (
277
+ <div>
278
+ {/* Flow selector tabs */}
279
+ <div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
280
+ {FLOWS_LIST.map(f => (
281
+ <button key={f.id} onClick={() => selectFlow(f.id)} style={{
282
+ background: selectedFlowId === f.id ? f.color + "22" : C.surface,
283
+ border: `1px solid ${selectedFlowId === f.id ? f.color : C.border}`,
284
+ color: selectedFlowId === f.id ? f.color : C.textMuted,
285
+ borderRadius: 8, padding: "7px 12px", cursor: "pointer",
286
+ fontWeight: selectedFlowId === f.id ? 700 : 400, fontSize: 12,
287
+ transition: "all 0.15s", display: "flex", alignItems: "center", gap: 6,
288
+ boxShadow: selectedFlowId === f.id ? `0 0 12px ${f.color}33` : "none",
289
+ fontFamily: "inherit",
290
+ }}>
291
+ <span>{f.icon}</span>{f.label}
292
+ </button>
293
+ ))}
294
+ </div>
295
+
296
+ {/* Flow header */}
297
+ <div style={{
298
+ background: C.surface, border: `1px solid ${flow.color}44`, borderRadius: 10,
299
+ padding: 16, marginBottom: 20, display: "flex", alignItems: "center", gap: 16,
300
+ }}>
301
+ <span style={{ fontSize: 32 }}>{flow.icon}</span>
302
+ <div style={{ flex: 1 }}>
303
+ <div style={{ fontWeight: 700, color: flow.color, fontSize: 16 }}>{flow.label}</div>
304
+ <div style={{ color: C.textDim, fontSize: 12, marginTop: 3 }}>{flow.description}</div>
305
+ </div>
306
+ <div style={{ display: "flex", gap: 8 }}>
307
+ <button onClick={run} disabled={running} style={{
308
+ background: running ? C.border : flow.color, color: "#fff",
309
+ border: "none", borderRadius: 8, padding: "10px 20px",
310
+ cursor: running ? "not-allowed" : "pointer", fontWeight: 700, fontSize: 14,
311
+ transition: "all 0.15s", opacity: running ? 0.6 : 1, fontFamily: "inherit",
312
+ }}>
313
+ {running ? "⏳ Ejecutando..." : completed ? "▶ Re-ejecutar" : "▶ Simular"}
314
+ </button>
315
+ <button onClick={reset} style={{
316
+ background: "transparent", color: C.textMuted,
317
+ border: `1px solid ${C.border}`, borderRadius: 8,
318
+ padding: "10px 14px", cursor: "pointer", fontFamily: "inherit",
319
+ }}>⟳</button>
320
+ </div>
321
+ </div>
322
+
323
+ {/* Steps timeline */}
324
+ <div style={{ position: "relative" }}>
325
+ {flow.steps.map((step, i) => {
326
+ const isActive = currentStep === i;
327
+ const isDone = currentStep > i;
328
+ const stepColor = getStepColor(step);
329
+
330
+ return (
331
+ <div key={step.id} style={{ display: "flex", gap: 16, marginBottom: 8, alignItems: "flex-start" }}>
332
+ {/* Timeline dot */}
333
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 36, flexShrink: 0 }}>
334
+ <div style={{
335
+ width: 36, height: 36, borderRadius: "50%",
336
+ display: "flex", alignItems: "center", justifyContent: "center",
337
+ background: isDone ? C.green + "22" : isActive ? stepColor + "33" : C.surface,
338
+ border: `2px solid ${isDone ? C.green : isActive ? stepColor : C.border}`,
339
+ color: isDone ? C.green : isActive ? stepColor : C.textMuted,
340
+ fontWeight: 700, fontSize: 13, transition: "all 0.3s",
341
+ boxShadow: isActive ? `0 0 16px ${stepColor}44` : "none",
342
+ }}>
343
+ {isDone ? "✓" : step.id}
344
+ </div>
345
+ {i < flow.steps.length - 1 && (
346
+ <div style={{
347
+ width: 2, flex: 1, minHeight: 24,
348
+ background: isDone ? C.green + "44" : C.border,
349
+ marginTop: 4,
350
+ }} />
351
+ )}
352
+ </div>
353
+
354
+ {/* Step card */}
355
+ <div style={{
356
+ flex: 1,
357
+ background: isActive ? stepColor + "11" : isDone ? C.green + "08" : C.surface,
358
+ border: `1px solid ${isActive ? stepColor + "66" : isDone ? C.green + "33" : C.border}`,
359
+ borderRadius: 10, padding: "12px 16px", marginBottom: 4,
360
+ transition: "all 0.3s",
361
+ opacity: currentStep === -1 ? 0.55 : isDone || isActive ? 1 : 0.4,
362
+ }}>
363
+ {/* Step header */}
364
+ <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
365
+ <span style={{ fontSize: 16 }}>{getStepIcon(step)}</span>
366
+ {step.from && (
367
+ <>
368
+ <Tag color={getActorColor(step.from)}>{step.from}</Tag>
369
+ <span style={{ color: C.textMuted, fontSize: 12 }}>→</span>
370
+ </>
371
+ )}
372
+ {step.to && !Array.isArray(step.to) && (
373
+ <Tag color={getActorColor(step.to)}>{step.to}</Tag>
374
+ )}
375
+ {Array.isArray(step.to) && step.to.map(t => (
376
+ <Tag key={t} color={getActorColor(t)}>{t}</Tag>
377
+ ))}
378
+ {step.label && (
379
+ <code style={{
380
+ background: C.bg, color: stepColor,
381
+ padding: "2px 8px", borderRadius: 4, fontSize: 12,
382
+ border: `1px solid ${C.border}`,
383
+ }}>
384
+ {step.label}
385
+ </code>
386
+ )}
387
+ {step.event && <Tag color={C.gold}>⚡ {step.event}</Tag>}
388
+ {step.topic && <Tag color={C.purple}>kafka:{step.topic}</Tag>}
389
+ </div>
390
+
391
+ <div style={{ color: C.textDim, fontSize: 12, marginTop: 6, lineHeight: 1.6 }}>
392
+ {step.desc}
393
+ </div>
394
+
395
+ {/* Sync sub-calls (shown when step is active) */}
396
+ {step.syncCalls && step.syncCalls.length > 0 && isActive && (
397
+ <div style={{ marginTop: 10, paddingTop: 10, borderTop: `1px dashed ${C.border}` }}>
398
+ <div style={{ color: C.textMuted, fontSize: 10, marginBottom: 6, fontWeight: 700, letterSpacing: 1 }}>
399
+ LLAMADAS SÍNCRONAS:
400
+ </div>
401
+ {step.syncCalls.map((sc, j) => (
402
+ <div key={j} style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
403
+ <span style={{ color: C.blue, fontSize: 11 }}>⟶</span>
404
+ <Tag color={getActorColor(sc.to)}>{sc.to}</Tag>
405
+ <code style={{
406
+ background: C.bg, color: C.blue,
407
+ padding: "1px 6px", borderRadius: 3, fontSize: 11,
408
+ }}>{sc.label}</code>
409
+ <span style={{ color: C.textMuted, fontSize: 10 }}>via {sc.port}</span>
410
+ </div>
411
+ ))}
412
+ </div>
413
+ )}
414
+ </div>
415
+ </div>
416
+ );
417
+ })}
418
+ </div>
419
+
420
+ {completed && (
421
+ <div style={{
422
+ background: C.green + "15", border: `1px solid ${C.green}44`,
423
+ borderRadius: 10, padding: 16, textAlign: "center", marginTop: 8,
424
+ animation: "fadeIn 0.3s",
425
+ }}>
426
+ <span style={{ fontSize: 24 }}>✅</span>
427
+ <div style={{ color: C.green, fontWeight: 700, marginTop: 4 }}>Flujo completado exitosamente</div>
428
+ <div style={{ color: C.textMuted, fontSize: 12, marginTop: 2 }}>
429
+ {flow.steps.length} pasos · {flow.label}
430
+ </div>
431
+ </div>
432
+ )}
433
+ </div>
434
+ );
435
+ }
436
+
437
+ // ─── ArchitectureTab ────────────────────────────────────────────────────
438
+ function ArchitectureTab() {
439
+ const [selected, setSelected] = useState(null);
440
+
441
+ const moduleInfo = (id) => ({
442
+ produces: EVENTS.filter(e => e.producer === id),
443
+ consumes: EVENTS.filter(e => (e.consumers || []).includes(id)),
444
+ callsSync: SYNC_INTEGRATIONS.filter(s => s.caller === id),
445
+ calledBy: SYNC_INTEGRATIONS.filter(s => s.calls === id),
446
+ endpoints: ENDPOINTS[id] || [],
447
+ });
448
+
449
+ return (
450
+ <div>
451
+ {/* Module grid */}
452
+ <div style={{ marginBottom: 20 }}>
453
+ <div style={{ color: C.textMuted, fontSize: 11, marginBottom: 10, fontWeight: 700, letterSpacing: 1 }}>
454
+ MÓDULOS — clic para explorar dependencias
455
+ </div>
456
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 10 }}>
457
+ {MODULES_LIST.map(m => (
458
+ <ModuleCard
459
+ key={m.id}
460
+ id={m.id}
461
+ selected={selected === m.id}
462
+ onClick={() => setSelected(selected === m.id ? null : m.id)}
463
+ />
464
+ ))}
465
+ </div>
466
+ </div>
467
+
468
+ {/* Selected module detail */}
469
+ {selected && (() => {
470
+ const mod = MODULES[selected];
471
+ const info = moduleInfo(selected);
472
+ if (!mod) return null;
473
+ return (
474
+ <div style={{
475
+ background: C.surface, border: `1px solid ${mod.color}44`,
476
+ borderRadius: 12, padding: 20, marginBottom: 20,
477
+ animation: "fadeIn 0.2s",
478
+ }}>
479
+ <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}>
480
+ <span style={{ fontSize: 28 }}>{mod.icon}</span>
481
+ <div>
482
+ <div style={{ fontWeight: 800, color: mod.color, fontSize: 18 }}>{mod.label}</div>
483
+ <div style={{ color: C.textMuted, fontSize: 12 }}>{mod.desc}</div>
484
+ </div>
485
+ </div>
486
+
487
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))", gap: 16 }}>
488
+ {[
489
+ { title: "Produce eventos", items: info.produces, color: C.gold, icon: "📤", render: e => e.event },
490
+ { title: "Consume eventos", items: info.consumes, color: C.blue, icon: "📥", render: e => e.event },
491
+ { title: "Llama síncronamente", items: info.callsSync, color: C.purple, icon: "⟶", render: s => `→ ${s.calls} (${s.port})` },
492
+ { title: "Es llamado por", items: info.calledBy, color: C.green, icon: "⟵", render: s => `← ${s.caller} (${s.port})` },
493
+ { title: "Endpoints expuestos", items: info.endpoints, color: mod.color, icon: "🔌", render: e => e },
494
+ ].filter(sec => sec.items.length > 0).map(section => (
495
+ <div key={section.title}>
496
+ <div style={{ color: section.color, fontSize: 10, fontWeight: 700, marginBottom: 8, letterSpacing: 0.5 }}>
497
+ {section.icon} {section.title.toUpperCase()}
498
+ </div>
499
+ {section.items.map((item, i) => (
500
+ <div key={i} style={{
501
+ color: C.textDim, fontSize: 12, padding: "5px 0",
502
+ borderBottom: `1px solid ${C.border}`, fontFamily: "monospace",
503
+ }}>
504
+ {section.render(item)}
505
+ </div>
506
+ ))}
507
+ </div>
508
+ ))}
509
+ </div>
510
+ </div>
511
+ );
512
+ })()}
513
+
514
+ {/* Sync integrations graph */}
515
+ {SYNC_INTEGRATIONS.length > 0 && (
516
+ <div style={{ marginBottom: 24 }}>
517
+ <div style={{ color: C.textMuted, fontSize: 11, marginBottom: 10, fontWeight: 700, letterSpacing: 1 }}>
518
+ DEPENDENCIAS SÍNCRONAS ({SYNC_INTEGRATIONS.length} puertos)
519
+ </div>
520
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))", gap: 10 }}>
521
+ {SYNC_INTEGRATIONS.map((s, i) => {
522
+ const callerMod = MODULES[s.caller];
523
+ const calleeMod = MODULES[s.calls];
524
+ return (
525
+ <div key={i} style={{
526
+ background: C.surface, border: `1px solid ${C.border}`,
527
+ borderRadius: 10, padding: "12px 16px",
528
+ }}>
529
+ <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 6, marginBottom: 8 }}>
530
+ <Tag color={callerMod?.color || C.textMuted}>{s.caller}</Tag>
531
+ <span style={{ color: C.textMuted, fontSize: 14, fontWeight: 700 }}>→</span>
532
+ <Tag color={calleeMod?.color || C.textMuted}>{s.calls}</Tag>
533
+ <Tag color={C.purple}>{s.port}</Tag>
534
+ </div>
535
+ <div style={{ marginLeft: 4 }}>
536
+ {(s.endpoints || []).map((ep, j) => (
537
+ <div key={j} style={{ color: C.textDim, fontSize: 11, fontFamily: "monospace", padding: "2px 0" }}>
538
+ · {ep}
539
+ </div>
540
+ ))}
541
+ </div>
542
+ </div>
543
+ );
544
+ })}
545
+ </div>
546
+ </div>
547
+ )}
548
+
549
+ {/* Kafka topics */}
550
+ <div>
551
+ <div style={{ color: C.textMuted, fontSize: 11, marginBottom: 10, fontWeight: 700, letterSpacing: 1 }}>
552
+ KAFKA TOPICS ({EVENTS.length} eventos)
553
+ </div>
554
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 8 }}>
555
+ {EVENTS.map(e => (
556
+ <div key={e.event} style={{
557
+ background: C.surface, border: `1px solid ${C.border}`,
558
+ borderRadius: 8, padding: "10px 14px",
559
+ }}>
560
+ <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }}>
561
+ <Tag color={C.gold}>{e.topic}</Tag>
562
+ </div>
563
+ <div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
564
+ <Tag color={MODULES[e.producer]?.color || C.textMuted}>{e.producer}</Tag>
565
+ <span style={{ color: C.textMuted, fontSize: 11 }}>→</span>
566
+ {(e.consumers || []).map(c => (
567
+ <Tag key={c} color={MODULES[c]?.color || C.textMuted}>{c}</Tag>
568
+ ))}
569
+ </div>
570
+ </div>
571
+ ))}
572
+ </div>
573
+ </div>
574
+ </div>
575
+ );
576
+ }
577
+
578
+ // ─── DiagramTab ──────────────────────────────────────────────────────────
579
+ function DiagramTab() {
580
+ const containerRef = useRef(null);
581
+ const networkRef = useRef(null);
582
+ const edgesDataRef = useRef(null);
583
+ const edgeGroupMapRef = useRef({});
584
+ const eventEdgesMapRef = useRef({});
585
+ const edgeLabelMapRef = useRef({});
586
+ const [physicsOn, setPhysicsOn] = useState(true);
587
+ const [hoveredNode, setHoveredNode] = useState(null);
588
+ const [hoveredEvent, setHoveredEvent] = useState(null); // { name, producer, consumers[] }
589
+
590
+ // Build vis datasets from injected data
591
+ function buildDatasets() {
592
+ const visNodes = MODULES_LIST.map(mod => ({
593
+ id: mod.id,
594
+ label: mod.icon + "\n" + mod.label,
595
+ title: mod.desc,
596
+ color: {
597
+ background: mod.color + "22",
598
+ border: mod.color,
599
+ highlight: { background: mod.color + "44", border: mod.color },
600
+ hover: { background: mod.color + "33", border: mod.color },
601
+ },
602
+ font: { color: C.text, size: 13, face: "'Plus Jakarta Sans', sans-serif", multi: false },
603
+ shape: "box",
604
+ borderWidth: 2,
605
+ borderWidthSelected: 3,
606
+ margin: 10,
607
+ }));
608
+
609
+ const visEdges = [];
610
+
611
+ // Sync edges — solid blue
612
+ SYNC_INTEGRATIONS.forEach((s, i) => {
613
+ visEdges.push({
614
+ id: "sync-" + i,
615
+ from: s.caller,
616
+ to: s.calls,
617
+ label: s.port,
618
+ dashes: false,
619
+ color: { color: C.blue, highlight: C.blue, hover: C.blue },
620
+ font: { color: C.blue, size: 10, face: "'JetBrains Mono', monospace",
621
+ background: C.bg, strokeWidth: 0, align: "middle" },
622
+ arrows: { to: { enabled: true, scaleFactor: 0.7 } },
623
+ width: 2,
624
+ smooth: { enabled: true, type: "curvedCW", roundness: 0.15 },
625
+ });
626
+ });
627
+
628
+ // Broker node — shown only if there are async events
629
+ if (EVENTS.length > 0) {
630
+ const brokerLabel = ((window.__EVA_DATA__.brokerName || "Kafka") + "\nBroker");
631
+ visNodes.push({
632
+ id: "__broker__",
633
+ label: "⚡ " + brokerLabel,
634
+ title: "Message broker — retransmite eventos asíncronos entre módulos",
635
+ color: {
636
+ background: C.gold, border: "#c49a00",
637
+ highlight: { background: "#ffd84d", border: "#c49a00" },
638
+ hover: { background: "#ffd84d", border: "#c49a00" },
639
+ },
640
+ font: { color: "#0d1f3c", size: 13, face: "'Plus Jakarta Sans', sans-serif", bold: true },
641
+ shape: "box", borderWidth: 2, borderWidthSelected: 3, margin: 12,
642
+ });
643
+ }
644
+
645
+ // Async edges — routed through broker + group maps for hover highlighting
646
+ const edgeGroupMap = {}; // edgeId → eventIndex
647
+ const eventEdgesMap = {}; // eventIndex → [edgeIds]
648
+ const edgeLabelMap = {}; // edgeId → original label
649
+ EVENTS.forEach((ev, i) => {
650
+ const shortLabel = ev.event.replace(/Event$/, "");
651
+ eventEdgesMap[i] = [];
652
+ // producer → broker
653
+ const pubId = "async-pub-" + i;
654
+ edgeGroupMap[pubId] = i;
655
+ eventEdgesMap[i].push(pubId);
656
+ edgeLabelMap[pubId] = shortLabel;
657
+ visEdges.push({
658
+ id: pubId,
659
+ from: ev.producer,
660
+ to: "__broker__",
661
+ label: shortLabel,
662
+ dashes: [6, 4],
663
+ color: { color: C.gold + "cc", highlight: C.gold, hover: C.gold },
664
+ font: { color: C.gold, size: 10, face: "'JetBrains Mono', monospace",
665
+ background: C.bg, strokeWidth: 0, align: "middle" },
666
+ arrows: { to: { enabled: true, scaleFactor: 0.6 } },
667
+ width: 1.5,
668
+ smooth: { enabled: true, type: "dynamic" },
669
+ });
670
+ // broker → each consumer
671
+ (ev.consumers || []).forEach((consumer, j) => {
672
+ const subId = "async-sub-" + i + "-" + j;
673
+ edgeGroupMap[subId] = i;
674
+ eventEdgesMap[i].push(subId);
675
+ edgeLabelMap[subId] = "";
676
+ visEdges.push({
677
+ id: subId,
678
+ from: "__broker__",
679
+ to: consumer,
680
+ label: "",
681
+ dashes: [4, 4],
682
+ color: { color: C.gold + "77", highlight: C.gold, hover: C.gold },
683
+ font: { color: "rgba(0,0,0,0)", strokeWidth: 0 },
684
+ arrows: { to: { enabled: true, scaleFactor: 0.5 } },
685
+ width: 1.5,
686
+ smooth: { enabled: true, type: "dynamic" },
687
+ });
688
+ });
689
+ });
690
+
691
+ return { visNodes, visEdges, edgeGroupMap, eventEdgesMap, edgeLabelMap };
692
+ }
693
+
694
+ function initNetwork(phys) {
695
+ if (!containerRef.current) return;
696
+ if (networkRef.current) { networkRef.current.destroy(); }
697
+
698
+ const { visNodes, visEdges, edgeGroupMap, eventEdgesMap, edgeLabelMap } = buildDatasets();
699
+ edgeGroupMapRef.current = edgeGroupMap;
700
+ eventEdgesMapRef.current = eventEdgesMap;
701
+ edgeLabelMapRef.current = edgeLabelMap;
702
+ const edgesDS = new vis.DataSet(visEdges);
703
+ edgesDataRef.current = edgesDS;
704
+ const data = {
705
+ nodes: new vis.DataSet(visNodes),
706
+ edges: edgesDS,
707
+ };
708
+ const options = {
709
+ physics: {
710
+ enabled: phys,
711
+ solver: "forceAtlas2Based",
712
+ forceAtlas2Based: { gravitationalConstant: -60, springLength: 160, springConstant: 0.05, damping: 0.5 },
713
+ stabilization: { iterations: 400, updateInterval: 25 },
714
+ },
715
+ interaction: { dragNodes: true, zoomView: true, hover: true, tooltipDelay: 150 },
716
+ edges: { selectionWidth: 2 },
717
+ nodes: { chosen: true },
718
+ layout: { randomSeed: 42 },
719
+ };
720
+
721
+ const net = new vis.Network(containerRef.current, data, options);
722
+ net.once("stabilizationIterationsDone", () => {
723
+ net.setOptions({ physics: { enabled: false } });
724
+ setPhysicsOn(false);
725
+ });
726
+ net.on("hoverNode", (p) => setHoveredNode(p.node));
727
+ net.on("blurNode", () => setHoveredNode(null));
728
+ net.on("hoverEdge", (p) => {
729
+ const groupIdx = edgeGroupMapRef.current[p.edge];
730
+ if (groupIdx === undefined || !edgesDataRef.current) return;
731
+ const groupIds = eventEdgesMapRef.current[groupIdx] || [];
732
+ const allIds = Object.keys(edgeGroupMapRef.current);
733
+ const RED = "#ff6b6b";
734
+ const GREEN = "#4ade80";
735
+ const FONT_PUB = { color: C.gold, size: 10, face: "'JetBrains Mono', monospace", background: C.bg, strokeWidth: 0, align: "middle" };
736
+ const FONT_HID = { color: "rgba(0,0,0,0)", background: "rgba(0,0,0,0)", strokeWidth: 0 };
737
+ edgesDataRef.current.update(allIds.map(id => {
738
+ const inGroup = groupIds.includes(id);
739
+ const isPub = id.startsWith("async-pub-");
740
+ return inGroup
741
+ ? { id,
742
+ color: { color: RED, highlight: RED, hover: RED },
743
+ font: isPub ? { ...FONT_PUB, color: GREEN, background: "rgba(0,0,0,0)", vadjust: -14 } : FONT_HID,
744
+ label: edgeLabelMapRef.current[id],
745
+ width: 1.5 }
746
+ : { id,
747
+ color: { color: C.gold + "18", highlight: C.gold + "22", hover: C.gold + "22" },
748
+ font: FONT_HID,
749
+ label: "",
750
+ width: 1.5 };
751
+ }));
752
+ // update event tooltip
753
+ const ev = EVENTS[groupIdx];
754
+ if (ev) setHoveredEvent({ name: ev.event, producer: ev.producer, consumers: ev.consumers || [] });
755
+ });
756
+ net.on("blurEdge", () => {
757
+ setHoveredEvent(null);
758
+ if (!edgesDataRef.current) return;
759
+ const allIds = Object.keys(edgeGroupMapRef.current);
760
+ const FONT_PUB = { color: C.gold, size: 10, face: "'JetBrains Mono', monospace", background: C.bg, strokeWidth: 0, align: "middle" };
761
+ const FONT_SUB = { color: "rgba(0,0,0,0)", strokeWidth: 0 };
762
+ edgesDataRef.current.update(allIds.map(id => {
763
+ const isPub = id.startsWith("async-pub-");
764
+ return {
765
+ id,
766
+ color: isPub
767
+ ? { color: C.gold + "cc", highlight: C.gold, hover: C.gold }
768
+ : { color: C.gold + "77", highlight: C.gold, hover: C.gold },
769
+ font: isPub ? FONT_PUB : FONT_SUB,
770
+ label: edgeLabelMapRef.current[id],
771
+ width: 1.5,
772
+ };
773
+ }));
774
+ });
775
+ networkRef.current = net;
776
+ }
777
+
778
+ useEffect(() => {
779
+ initNetwork(true);
780
+ return () => { if (networkRef.current) networkRef.current.destroy(); };
781
+ }, []);
782
+
783
+ const togglePhysics = () => {
784
+ const next = !physicsOn;
785
+ setPhysicsOn(next);
786
+ if (networkRef.current) networkRef.current.setOptions({ physics: { enabled: next } });
787
+ };
788
+
789
+ const fitView = () => networkRef.current && networkRef.current.fit({ animation: { duration: 400, easingFunction: "easeInOutQuad" } });
790
+ const resetNet = () => initNetwork(true);
791
+
792
+ const hoveredMod = (hoveredNode && hoveredNode !== "__broker__") ? MODULES[hoveredNode] : null;
793
+ const hoveredBroker = hoveredNode === "__broker__";
794
+ // overlay priority: hoveredEvent > node tooltips
795
+ const showOverlay = hoveredEvent || hoveredMod || hoveredBroker;
796
+
797
+ return (
798
+ <div>
799
+ {/* Toolbar */}
800
+ <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexWrap: "wrap" }}>
801
+ <button onClick={togglePhysics} style={{
802
+ background: physicsOn ? C.blue + "22" : C.surface,
803
+ border: `1px solid ${physicsOn ? C.blue : C.border}`,
804
+ color: physicsOn ? C.blue : C.textMuted,
805
+ borderRadius: 8, padding: "7px 14px", cursor: "pointer",
806
+ fontFamily: "inherit", fontSize: 12, fontWeight: 600,
807
+ transition: "all 0.15s",
808
+ }}>
809
+ {physicsOn ? "⏸ Detener física" : "▶ Activar física"}
810
+ </button>
811
+ <button onClick={fitView} style={{
812
+ background: C.surface, border: `1px solid ${C.border}`,
813
+ color: C.textMuted, borderRadius: 8, padding: "7px 14px",
814
+ cursor: "pointer", fontFamily: "inherit", fontSize: 12,
815
+ }}>⊞ Ajustar vista</button>
816
+ <button onClick={resetNet} style={{
817
+ background: C.surface, border: `1px solid ${C.border}`,
818
+ color: C.textMuted, borderRadius: 8, padding: "7px 14px",
819
+ cursor: "pointer", fontFamily: "inherit", fontSize: 12,
820
+ }}>⟳ Reiniciar</button>
821
+ <div style={{ flex: 1 }} />
822
+ </div>
823
+
824
+ {/* Canvas */}
825
+ <div style={{
826
+ position: "relative",
827
+ background: C.surface, border: `1px solid ${C.border}`,
828
+ borderRadius: 12, overflow: "hidden",
829
+ }}>
830
+ <div ref={containerRef} style={{ width: "100%", height: 720 }} />
831
+ {showOverlay && (
832
+ <div style={{
833
+ position: "absolute", bottom: 12, left: 12,
834
+ background: C.bg + "ee",
835
+ border: `1px solid ${
836
+ hoveredEvent ? "#4ade80"
837
+ : hoveredBroker ? C.gold
838
+ : hoveredMod.color}66`,
839
+ borderRadius: 8, padding: "6px 12px", backdropFilter: "blur(4px)",
840
+ fontSize: 12, fontWeight: 600,
841
+ animation: "fadeIn 0.15s", pointerEvents: "none",
842
+ maxWidth: "80%",
843
+ }}>
844
+ {hoveredEvent
845
+ ? <span style={{ color: "#4ade80" }}>
846
+ ⬡ {hoveredEvent.name}
847
+ <span style={{ color: C.textMuted, fontWeight: 400 }}> — </span>
848
+ <span style={{ color: C.textDim, fontWeight: 400 }}>
849
+ {hoveredEvent.producer} → [{hoveredEvent.consumers.join(", ")}]
850
+ </span>
851
+ </span>
852
+ : hoveredBroker
853
+ ? <span style={{ color: C.gold }}>⚡ Kafka Broker — <span style={{ color: C.textDim, fontWeight: 400 }}>Retransmite eventos asíncronos entre módulos</span></span>
854
+ : <span style={{ color: hoveredMod.color }}>{hoveredMod.icon} {hoveredMod.label} — <span style={{ color: C.textDim, fontWeight: 400 }}>{hoveredMod.desc}</span></span>
855
+ }
856
+ </div>
857
+ )}
858
+ </div>
859
+
860
+ {/* Legend */}
861
+ <div style={{
862
+ display: "flex", gap: 24, marginTop: 14, flexWrap: "wrap",
863
+ alignItems: "center", paddingLeft: 4,
864
+ }}>
865
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
866
+ <svg width="36" height="12">
867
+ <line x1="0" y1="6" x2="36" y2="6"
868
+ stroke={C.blue} strokeWidth="2" strokeLinecap="round" />
869
+ <polygon points="32,3 36,6 32,9" fill={C.blue} />
870
+ </svg>
871
+ <span style={{ color: C.textMuted, fontSize: 12 }}>Síncrono (HTTP)</span>
872
+ </div>
873
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
874
+ <svg width="36" height="12">
875
+ <line x1="0" y1="6" x2="36" y2="6"
876
+ stroke={C.gold} strokeWidth="1.5" strokeDasharray="6 4" strokeLinecap="round" />
877
+ <polygon points="32,3 36,6 32,9" fill={C.gold} />
878
+ </svg>
879
+ <span style={{ color: C.textMuted, fontSize: 12 }}>Asíncrono (Kafka)</span>
880
+ </div>
881
+ <div style={{ color: C.textMuted, fontSize: 11, marginLeft: "auto" }}>
882
+ {MODULES_LIST.length} módulos · {SYNC_INTEGRATIONS.length} puertos sync · {EVENTS.length} eventos
883
+ </div>
884
+ </div>
885
+ </div>
886
+ );
887
+ }
888
+
889
+ // ─── App root ───────────────────────────────────────────────────────────
890
+ function App() {
891
+ const [tab, setTab] = useState("validation");
892
+
893
+ const tabs = [
894
+ { id: "validation", label: "Validación", icon: "🔍" },
895
+ { id: "flows", label: "Simulador de flujos", icon: "▶" },
896
+ { id: "architecture", label: "Arquitectura", icon: "🗺️" },
897
+ { id: "diagram", label: "Diagrama", icon: "◈" },
898
+ ];
899
+
900
+ const sys = window.__EVA_DATA__;
901
+ const tech = [];
902
+ // Detect tech from systemConfig embedded in data
903
+ if (sys.events && sys.events.length > 0) tech.push({ label: "Kafka", color: C.gold });
904
+
905
+ return (
906
+ <div style={{ background: C.bg, minHeight: "100vh", color: C.text, fontFamily: "'Plus Jakarta Sans', system-ui, -apple-system, sans-serif" }}>
907
+
908
+ {/* Header */}
909
+ <div style={{ borderBottom: `1px solid ${C.border}`, padding: "0 24px" }}>
910
+ <div style={{ maxWidth: 1100, margin: "0 auto", display: "flex", alignItems: "center", gap: 16, height: 64 }}>
911
+ <div>
912
+ <span style={{ fontWeight: 900, fontSize: 18, color: C.accent, letterSpacing: -0.5 }}>eva4j</span>
913
+ <span style={{ fontWeight: 400, fontSize: 14, color: C.textMuted, marginLeft: 8 }}>/ architecture validator</span>
914
+ </div>
915
+ <div style={{ height: 20, width: 1, background: C.border }} />
916
+ <div style={{ fontWeight: 700, fontSize: 16, color: C.text }}>{systemName}</div>
917
+ <div style={{ flex: 1 }} />
918
+ <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
919
+ {sys.events && sys.events.length > 0 && (
920
+ <Tag color={C.gold}>Kafka · {sys.events.length} events</Tag>
921
+ )}
922
+ {sys.syncIntegrations && sys.syncIntegrations.length > 0 && (
923
+ <Tag color={C.blue}>Sync · {sys.syncIntegrations.length} ports</Tag>
924
+ )}
925
+ <Tag color={C.purple}>{sys.modules.length} modules</Tag>
926
+ </div>
927
+ </div>
928
+ </div>
929
+
930
+ {/* Tabs */}
931
+ <div style={{ borderBottom: `1px solid ${C.border}`, padding: "0 24px" }}>
932
+ <div style={{ maxWidth: 1100, margin: "0 auto", display: "flex", gap: 4 }}>
933
+ {tabs.map(t => (
934
+ <button key={t.id} onClick={() => setTab(t.id)} style={{
935
+ background: "transparent", border: "none",
936
+ color: tab === t.id ? C.text : C.textMuted,
937
+ padding: "16px 20px", cursor: "pointer",
938
+ fontWeight: tab === t.id ? 700 : 400,
939
+ borderBottom: `2px solid ${tab === t.id ? C.accent : "transparent"}`,
940
+ fontSize: 13, transition: "all 0.15s", fontFamily: "inherit",
941
+ display: "flex", alignItems: "center", gap: 8,
942
+ }}>
943
+ <span>{t.icon}</span> {t.label}
944
+ </button>
945
+ ))}
946
+ <div style={{ flex: 1 }} />
947
+ <div style={{ display: "flex", alignItems: "center" }}>
948
+ <span style={{ color: C.textMuted, fontSize: 10 }}>
949
+ generated {new Date(generatedAt).toLocaleString()}
950
+ </span>
951
+ </div>
952
+ </div>
953
+ </div>
954
+
955
+ {/* Tab content */}
956
+ <div style={{ maxWidth: 1100, margin: "0 auto", padding: "28px 24px" }}>
957
+ {tab === "validation" && <ValidationTab />}
958
+ {tab === "flows" && <FlowSimulator />}
959
+ {tab === "architecture" && <ArchitectureTab />}
960
+ {tab === "diagram" && <DiagramTab />}
961
+ </div>
962
+ </div>
963
+ );
964
+ }
965
+
966
+ // Mount
967
+ const root = ReactDOM.createRoot(document.getElementById("root"));
968
+ root.render(React.createElement(App, null));
969
+ </script>
970
+ </body>
971
+ </html>