eva4j 1.0.16 β†’ 1.0.18

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 (151) hide show
  1. package/AGENTS.md +220 -5
  2. package/DOMAIN_YAML_GUIDE.md +188 -3
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/QUICK_REFERENCE.md +8 -4
  5. package/bin/eva4j.js +70 -2
  6. package/config/defaults.json +1 -0
  7. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  8. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  9. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  10. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  11. package/docs/commands/EVALUATE_SYSTEM.md +290 -10
  12. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  13. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  15. package/docs/commands/INDEX.md +27 -3
  16. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  17. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  18. package/docs/prototype/system/RISKS.md +277 -0
  19. package/docs/prototype/system/customers.yaml +133 -0
  20. package/docs/prototype/system/inventory.yaml +109 -0
  21. package/docs/prototype/system/notifications.yaml +131 -0
  22. package/docs/prototype/system/orders.yaml +241 -0
  23. package/docs/prototype/system/payments.yaml +256 -0
  24. package/docs/prototype/system/products.yaml +168 -0
  25. package/docs/prototype/system/system.yaml +269 -0
  26. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  27. package/examples/domain-events.yaml +26 -0
  28. package/examples/domain-read-models.yaml +113 -0
  29. package/examples/system/customer.yaml +89 -0
  30. package/examples/system/orders.yaml +119 -0
  31. package/examples/system/product.yaml +27 -0
  32. package/examples/system/system.yaml +80 -0
  33. package/package.json +1 -1
  34. package/read-model-spec.md +664 -0
  35. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  36. package/src/agents/design-gap-analyst.agent.md +383 -0
  37. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  38. package/src/agents/design-reviewer.agent.md +34 -5
  39. package/src/agents/implement-use-cases.prompt.md +179 -0
  40. package/src/agents/ux-gap-analyst.agent.md +412 -0
  41. package/src/commands/add-rabbitmq-client.js +261 -0
  42. package/src/commands/add-temporal-client.js +22 -2
  43. package/src/commands/build.js +267 -11
  44. package/src/commands/evaluate-system.js +700 -13
  45. package/src/commands/generate-entities.js +560 -24
  46. package/src/commands/generate-http-exchange.js +3 -0
  47. package/src/commands/generate-kafka-event.js +3 -0
  48. package/src/commands/generate-kafka-listener.js +3 -0
  49. package/src/commands/generate-rabbitmq-event.js +665 -0
  50. package/src/commands/generate-rabbitmq-listener.js +205 -0
  51. package/src/commands/generate-record.js +2 -2
  52. package/src/commands/generate-resource.js +4 -1
  53. package/src/commands/generate-temporal-activity.js +970 -33
  54. package/src/commands/generate-temporal-flow.js +98 -38
  55. package/src/commands/generate-temporal-system.js +708 -0
  56. package/src/commands/generate-usecase.js +4 -1
  57. package/src/skills/build-system-yaml/SKILL.md +343 -2
  58. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
  59. package/src/skills/build-system-yaml/references/module-spec.md +90 -9
  60. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  61. package/src/skills/build-temporal-system/SKILL.md +752 -0
  62. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  63. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  64. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  65. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  66. package/src/skills/implement-use-case/SKILL.md +350 -0
  67. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  68. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  69. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  70. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  71. package/src/utils/bounded-context-diagram.js +844 -0
  72. package/src/utils/config-manager.js +4 -2
  73. package/src/utils/domain-validator.js +495 -17
  74. package/src/utils/naming.js +20 -0
  75. package/src/utils/system-validator.js +169 -11
  76. package/src/utils/system-yaml-parser.js +318 -0
  77. package/src/utils/temporal-validator.js +497 -0
  78. package/src/utils/validator.js +3 -1
  79. package/src/utils/yaml-to-entity.js +281 -9
  80. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  81. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  82. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  83. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  84. package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
  85. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  86. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  87. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  88. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  89. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  90. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  91. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  92. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  93. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  94. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  95. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  96. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  97. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  98. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  99. package/templates/base/root/AGENTS.md.ejs +1 -1
  100. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  101. package/templates/crud/EndpointsController.java.ejs +1 -1
  102. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  103. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  104. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  105. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  106. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  107. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  108. package/templates/evaluate/report.html.ejs +1447 -90
  109. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  110. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  111. package/templates/ports/PortAclMapper.java.ejs +35 -0
  112. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  113. package/templates/ports/PortFeignClient.java.ejs +4 -0
  114. package/templates/ports/PortResponseDto.java.ejs +1 -1
  115. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  116. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  117. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  118. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  119. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  120. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  121. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  122. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  123. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  124. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  125. package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
  126. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  127. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  128. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  129. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
  130. package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
  131. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  132. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  133. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  134. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  135. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  136. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  137. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  138. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  139. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  140. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  141. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  142. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  143. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  144. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  145. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  146. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  147. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  148. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  149. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  150. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  151. package/COMMAND_EVALUATION.md +0 -911
@@ -48,6 +48,16 @@
48
48
  validation: VALIDATION,
49
49
  domainValidation: DOMAIN_VALIDATION,
50
50
  generatedAt,
51
+ isTemporalMode: IS_TEMPORAL_MODE,
52
+ orchestration: TEMPORAL_ORCHESTRATION,
53
+ workflows: TEMPORAL_WORKFLOWS,
54
+ localWorkflows: LOCAL_WORKFLOWS,
55
+ sagaWorkflows: SAGA_WORKFLOWS,
56
+ activityCatalog: ACTIVITY_CATALOG,
57
+ moduleRoles: MODULE_ROLES,
58
+ queueTopology: QUEUE_TOPOLOGY,
59
+ externalTypeDeps: EXTERNAL_TYPE_DEPS,
60
+ temporalValidation: TEMPORAL_VALIDATION,
51
61
  } = window.__EVA_DATA__;
52
62
 
53
63
  // Convert arrays to maps for fast lookup
@@ -583,13 +593,15 @@
583
593
  function DiagramTab() {
584
594
  const containerRef = useRef(null);
585
595
  const networkRef = useRef(null);
586
- const edgesDataRef = useRef(null);
587
- const edgeGroupMapRef = useRef({});
588
- const eventEdgesMapRef = useRef({});
589
- const edgeLabelMapRef = useRef({});
590
- const [physicsOn, setPhysicsOn] = useState(true);
591
- const [hoveredNode, setHoveredNode] = useState(null);
592
- const [hoveredEvent, setHoveredEvent] = useState(null); // { name, producer, consumers[] }
596
+ const edgesDataRef = useRef(null);
597
+ const edgeGroupMapRef = useRef({});
598
+ const eventEdgesMapRef = useRef({});
599
+ const edgeLabelMapRef = useRef({});
600
+ const temporalEdgeActivitiesRef = useRef({}); // edgeId β†’ { activities[], color, wfLabel }
601
+ const [physicsOn, setPhysicsOn] = useState(true);
602
+ const [hoveredNode, setHoveredNode] = useState(null);
603
+ const [hoveredEvent, setHoveredEvent] = useState(null); // { name, producer, consumers[] }
604
+ const [hoveredTemporalEdge, setHoveredTemporalEdge] = useState(null); // { activities[], color, wfLabel }
593
605
 
594
606
  // Build vis datasets from injected data
595
607
  function buildDatasets() {
@@ -629,6 +641,120 @@
629
641
  });
630
642
  });
631
643
 
644
+ // Temporal workflow edges β€” central Temporal Server hub (mirrors Kafka broker pattern)
645
+ if (IS_TEMPORAL_MODE && Array.isArray(TEMPORAL_WORKFLOWS)) {
646
+ // Build camelCase β†’ vis node id lookup (handles hyphenated names like "shopping-carts")
647
+ const camelToNodeId = {};
648
+ MODULES_LIST.forEach(m => {
649
+ camelToNodeId[m.id] = m.id;
650
+ const cc = m.id
651
+ .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
652
+ .replace(/^(.)/, c => c.toLowerCase());
653
+ camelToNodeId[cc] = m.id;
654
+ });
655
+
656
+ // Central Temporal Server node
657
+ const temporalTarget = (TEMPORAL_ORCHESTRATION && TEMPORAL_ORCHESTRATION.target) || "localhost:7233";
658
+ const temporalNs = (TEMPORAL_ORCHESTRATION && TEMPORAL_ORCHESTRATION.namespace) || "default";
659
+ visNodes.push({
660
+ id: "__temporal__",
661
+ label: "⏱️ Temporal\n" + temporalNs,
662
+ title: `Temporal Server: ${temporalTarget} β€” orquesta todos los workflows`,
663
+ color: {
664
+ background: "#3b1f6e",
665
+ border: C.purple,
666
+ highlight: { background: "#5a2fa8", border: C.purple },
667
+ hover: { background: "#4a2890", border: C.purple },
668
+ },
669
+ font: { color: "#e8e8f0", size: 13, face: "'Plus Jakarta Sans', sans-serif", bold: true },
670
+ shape: "ellipse", borderWidth: 3, borderWidthSelected: 4, margin: 14,
671
+ });
672
+
673
+ const WF_COLORS = [C.blue, C.orange, C.green, C.accent, C.gold, "#c084fc"];
674
+ const temporalEdgeActivities = {}; // populated below, stored in ref after build
675
+
676
+ TEMPORAL_WORKFLOWS.forEach((wf, wfIdx) => {
677
+ const orchId = camelToNodeId[wf.triggerModule];
678
+ if (!orchId) return;
679
+ const color = WF_COLORS[wfIdx % WF_COLORS.length];
680
+ const shortLabel = (wf.name || "").replace(/Workflow$/, "");
681
+
682
+ // Fix 2: one trigger edge PER WORKFLOW (not per unique orchestrator module)
683
+ // Uses workflow color + shortLabel so both Checkout and CancelOrder are distinct
684
+ visEdges.push({
685
+ id: `temporal-trigger-${wfIdx}`,
686
+ from: orchId,
687
+ to: "__temporal__",
688
+ label: shortLabel,
689
+ dashes: [5, 3],
690
+ color: { color: color + "cc", highlight: color, hover: color },
691
+ font: { color, size: 9, face: "'JetBrains Mono', monospace",
692
+ background: C.bg, strokeWidth: 0, align: "middle" },
693
+ arrows: { to: { enabled: true, scaleFactor: 0.6 } },
694
+ width: 1.5,
695
+ smooth: { enabled: true, type: "dynamic" },
696
+ });
697
+
698
+ // Mejora 4: determine dashes per target (all-async β†’ dashed, else solid)
699
+ // Fix 1: removed `|| tgtId === orchId` — allow self-calls (e.g. Temporal→ShoppingCarts)
700
+ // Build per-target activity list for Mejora 3 (tooltip)
701
+ const wfTargetActivities = {}; // tgtId β†’ [{activity, roleClass, type, compensation}]
702
+ (wf.steps || []).forEach((step) => {
703
+ if (step._isWait || !step.target) return;
704
+ const tgtId = camelToNodeId[step.target];
705
+ if (!tgtId) return;
706
+ if (!wfTargetActivities[tgtId]) wfTargetActivities[tgtId] = [];
707
+ wfTargetActivities[tgtId].push({
708
+ activity: step.activity || step.activityPascal || "",
709
+ roleClass: step.roleClass || "",
710
+ type: step.type || "sync",
711
+ compensation: step.compensation || null,
712
+ });
713
+ });
714
+
715
+ // Emit one dispatch edge per (wf, target) pair
716
+ Object.entries(wfTargetActivities).forEach(([tgtId, activities]) => {
717
+ const allAsync = activities.every(a => a.type === "async");
718
+ const edgeId = `temporal-dispatch-${wfIdx}-${tgtId}`;
719
+ temporalEdgeActivities[edgeId] = { activities, color, wfLabel: shortLabel };
720
+ visEdges.push({
721
+ id: edgeId,
722
+ from: "__temporal__",
723
+ to: tgtId,
724
+ label: shortLabel,
725
+ dashes: allAsync ? [4, 3] : false,
726
+ color: { color, highlight: color, hover: color },
727
+ font: { color, size: 10, face: "'JetBrains Mono', monospace",
728
+ background: C.bg, strokeWidth: 0, align: "middle" },
729
+ arrows: { to: { enabled: true, scaleFactor: 0.7 } },
730
+ width: 2,
731
+ smooth: { enabled: true, type: "curvedCW", roundness: 0.12 + wfIdx * 0.18 },
732
+ });
733
+
734
+ // Mejora 5: compensation edges (red dashed, Temporal β†’ same target, thin)
735
+ activities.forEach((act, aIdx) => {
736
+ if (!act.compensation) return;
737
+ const compId = `temporal-comp-${wfIdx}-${tgtId}-${aIdx}`;
738
+ visEdges.push({
739
+ id: compId,
740
+ from: "__temporal__",
741
+ to: tgtId,
742
+ label: "↩ " + act.compensation,
743
+ dashes: [3, 4],
744
+ color: { color: C.accent + "99", highlight: C.accent, hover: C.accent },
745
+ font: { color: C.accent, size: 8, face: "'JetBrains Mono', monospace",
746
+ background: C.bg, strokeWidth: 0, align: "middle" },
747
+ arrows: { to: { enabled: true, scaleFactor: 0.5 } },
748
+ width: 1,
749
+ smooth: { enabled: true, type: "curvedCCW", roundness: 0.35 },
750
+ });
751
+ });
752
+ });
753
+ });
754
+
755
+ temporalEdgeActivitiesRef.current = temporalEdgeActivities;
756
+ }
757
+
632
758
  // Broker node β€” shown only if there are async events
633
759
  if (EVENTS.length > 0) {
634
760
  const brokerLabel = ((window.__EVA_DATA__.brokerName || "Kafka") + "\nBroker");
@@ -725,11 +851,18 @@
725
851
  const net = new vis.Network(containerRef.current, data, options);
726
852
  net.once("stabilizationIterationsDone", () => {
727
853
  net.setOptions({ physics: { enabled: false } });
854
+ net.fit({ animation: { duration: 300, easingFunction: "easeInOutQuad" } });
728
855
  setPhysicsOn(false);
729
856
  });
730
857
  net.on("hoverNode", (p) => setHoveredNode(p.node));
731
858
  net.on("blurNode", () => setHoveredNode(null));
732
859
  net.on("hoverEdge", (p) => {
860
+ // Mejora 3: temporal dispatch edges have their own tooltip
861
+ const temporalInfo = temporalEdgeActivitiesRef.current[p.edge];
862
+ if (temporalInfo) {
863
+ setHoveredTemporalEdge(temporalInfo);
864
+ return;
865
+ }
733
866
  const groupIdx = edgeGroupMapRef.current[p.edge];
734
867
  if (groupIdx === undefined || !edgesDataRef.current) return;
735
868
  const groupIds = eventEdgesMapRef.current[groupIdx] || [];
@@ -759,6 +892,7 @@
759
892
  });
760
893
  net.on("blurEdge", () => {
761
894
  setHoveredEvent(null);
895
+ setHoveredTemporalEdge(null);
762
896
  if (!edgesDataRef.current) return;
763
897
  const allIds = Object.keys(edgeGroupMapRef.current);
764
898
  const FONT_PUB = { color: C.gold, size: 10, face: "'JetBrains Mono', monospace", background: C.bg, strokeWidth: 0, align: "middle" };
@@ -793,10 +927,11 @@
793
927
  const fitView = () => networkRef.current && networkRef.current.fit({ animation: { duration: 400, easingFunction: "easeInOutQuad" } });
794
928
  const resetNet = () => initNetwork(true);
795
929
 
796
- const hoveredMod = (hoveredNode && hoveredNode !== "__broker__") ? MODULES[hoveredNode] : null;
797
- const hoveredBroker = hoveredNode === "__broker__";
798
- // overlay priority: hoveredEvent > node tooltips
799
- const showOverlay = hoveredEvent || hoveredMod || hoveredBroker;
930
+ const hoveredMod = (hoveredNode && hoveredNode !== "__broker__" && hoveredNode !== "__temporal__") ? MODULES[hoveredNode] : null;
931
+ const hoveredBroker = hoveredNode === "__broker__";
932
+ const hoveredTemporal = hoveredNode === "__temporal__";
933
+ // overlay priority: hoveredTemporalEdge > hoveredEvent > node tooltips
934
+ const showOverlay = hoveredTemporalEdge || hoveredEvent || hoveredMod || hoveredBroker || hoveredTemporal;
800
935
 
801
936
  return (
802
937
  <div>
@@ -837,15 +972,48 @@
837
972
  position: "absolute", bottom: 12, left: 12,
838
973
  background: C.bg + "ee",
839
974
  border: `1px solid ${
840
- hoveredEvent ? "#4ade80"
975
+ hoveredTemporalEdge ? hoveredTemporalEdge.color
976
+ : hoveredEvent ? "#4ade80"
841
977
  : hoveredBroker ? C.gold
842
- : hoveredMod.color}66`,
843
- borderRadius: 8, padding: "6px 12px", backdropFilter: "blur(4px)",
978
+ : hoveredTemporal ? C.purple
979
+ : hoveredMod ? hoveredMod.color : C.borderBright}66`,
980
+ borderRadius: 8, padding: "8px 14px", backdropFilter: "blur(4px)",
844
981
  fontSize: 12, fontWeight: 600,
845
982
  animation: "fadeIn 0.15s", pointerEvents: "none",
846
983
  maxWidth: "80%",
847
984
  }}>
848
- {hoveredEvent
985
+ {hoveredTemporalEdge
986
+ ? <div>
987
+ <div style={{ color: hoveredTemporalEdge.color, marginBottom: 6 }}>
988
+ ⏱️ <span style={{ fontWeight: 700 }}>{hoveredTemporalEdge.wfLabel}</span>
989
+ <span style={{ color: C.textMuted, fontWeight: 400, marginLeft: 6, fontSize: 11 }}>actividades en este mΓ³dulo</span>
990
+ </div>
991
+ <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
992
+ {hoveredTemporalEdge.activities.map((act, i) => {
993
+ const ROLE_COLORS = { READ: C.blue, WRITE: C.green, COMPENSATE: C.accent, NOTIFY: C.gold };
994
+ const roleColor = ROLE_COLORS[act.roleClass] || C.textMuted;
995
+ return (
996
+ <div key={i} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11 }}>
997
+ <span style={{
998
+ background: roleColor + "22", color: roleColor,
999
+ border: `1px solid ${roleColor}44`,
1000
+ borderRadius: 3, padding: "0 5px", fontSize: 9, fontWeight: 700,
1001
+ fontFamily: "'JetBrains Mono', monospace", letterSpacing: 0.4,
1002
+ minWidth: 70, textAlign: "center",
1003
+ }}>{act.roleClass || "STEP"}</span>
1004
+ <span style={{ color: C.textDim, fontFamily: "'JetBrains Mono', monospace" }}>{act.activity}</span>
1005
+ {act.type === "async" && (
1006
+ <span style={{ color: C.gold, fontSize: 9, fontWeight: 700, letterSpacing: 0.3 }}>async</span>
1007
+ )}
1008
+ {act.compensation && (
1009
+ <span style={{ color: C.accent, fontSize: 9 }}>↩ {act.compensation}</span>
1010
+ )}
1011
+ </div>
1012
+ );
1013
+ })}
1014
+ </div>
1015
+ </div>
1016
+ : hoveredEvent
849
1017
  ? <span style={{ color: "#4ade80" }}>
850
1018
  ⬑ {hoveredEvent.name}
851
1019
  <span style={{ color: C.textMuted, fontWeight: 400 }}> β€” </span>
@@ -853,6 +1021,8 @@
853
1021
  {hoveredEvent.producer} β†’ [{hoveredEvent.consumers.join(", ")}]
854
1022
  </span>
855
1023
  </span>
1024
+ : hoveredTemporal
1025
+ ? <span style={{ color: C.purple }}>⏱️ Temporal Server β€” <span style={{ color: C.textDim, fontWeight: 400 }}>Orquesta la ejecuciΓ³n de workflows y despacha actividades a los mΓ³dulos worker</span></span>
856
1026
  : hoveredBroker
857
1027
  ? <span style={{ color: C.gold }}>⚑ Kafka Broker β€” <span style={{ color: C.textDim, fontWeight: 400 }}>Retransmite eventos asΓ­ncronos entre mΓ³dulos</span></span>
858
1028
  : <span style={{ color: hoveredMod.color }}>{hoveredMod.icon} {hoveredMod.label} β€” <span style={{ color: C.textDim, fontWeight: 400 }}>{hoveredMod.desc}</span></span>
@@ -866,25 +1036,67 @@
866
1036
  display: "flex", gap: 24, marginTop: 14, flexWrap: "wrap",
867
1037
  alignItems: "center", paddingLeft: 4,
868
1038
  }}>
869
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
870
- <svg width="36" height="12">
871
- <line x1="0" y1="6" x2="36" y2="6"
872
- stroke={C.blue} strokeWidth="2" strokeLinecap="round" />
873
- <polygon points="32,3 36,6 32,9" fill={C.blue} />
874
- </svg>
875
- <span style={{ color: C.textMuted, fontSize: 12 }}>SΓ­ncrono (HTTP)</span>
876
- </div>
877
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
878
- <svg width="36" height="12">
879
- <line x1="0" y1="6" x2="36" y2="6"
880
- stroke={C.gold} strokeWidth="1.5" strokeDasharray="6 4" strokeLinecap="round" />
881
- <polygon points="32,3 36,6 32,9" fill={C.gold} />
882
- </svg>
883
- <span style={{ color: C.textMuted, fontSize: 12 }}>AsΓ­ncrono (Kafka)</span>
884
- </div>
885
- <div style={{ color: C.textMuted, fontSize: 11, marginLeft: "auto" }}>
886
- {MODULES_LIST.length} mΓ³dulos Β· {SYNC_INTEGRATIONS.length} puertos sync Β· {EVENTS.length} eventos
887
- </div>
1039
+ {IS_TEMPORAL_MODE ? (
1040
+ <>
1041
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1042
+ <svg width="36" height="12">
1043
+ <line x1="0" y1="6" x2="36" y2="6"
1044
+ stroke={C.blue} strokeWidth="1.5" strokeDasharray="5 3" strokeLinecap="round" />
1045
+ <polygon points="32,3 36,6 32,9" fill={C.blue} />
1046
+ </svg>
1047
+ <span style={{ color: C.textMuted, fontSize: 12 }}>Trigger (por workflow)</span>
1048
+ </div>
1049
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1050
+ <svg width="36" height="12">
1051
+ <line x1="0" y1="6" x2="36" y2="6"
1052
+ stroke={C.blue} strokeWidth="2" strokeLinecap="round" />
1053
+ <polygon points="32,3 36,6 32,9" fill={C.blue} />
1054
+ </svg>
1055
+ <span style={{ color: C.textMuted, fontSize: 12 }}>Dispatch sync</span>
1056
+ </div>
1057
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1058
+ <svg width="36" height="12">
1059
+ <line x1="0" y1="6" x2="36" y2="6"
1060
+ stroke={C.blue} strokeWidth="2" strokeDasharray="4 3" strokeLinecap="round" />
1061
+ <polygon points="32,3 36,6 32,9" fill={C.blue} />
1062
+ </svg>
1063
+ <span style={{ color: C.textMuted, fontSize: 12 }}>Dispatch async</span>
1064
+ </div>
1065
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1066
+ <svg width="36" height="12">
1067
+ <line x1="0" y1="6" x2="36" y2="6"
1068
+ stroke={C.accent} strokeWidth="1" strokeDasharray="3 4" strokeLinecap="round" />
1069
+ <polygon points="32,3 36,6 32,9" fill={C.accent} />
1070
+ </svg>
1071
+ <span style={{ color: C.textMuted, fontSize: 12 }}>CompensaciΓ³n</span>
1072
+ </div>
1073
+ <div style={{ color: C.textMuted, fontSize: 11, marginLeft: "auto" }}>
1074
+ {MODULES_LIST.length} mΓ³dulos Β· {(TEMPORAL_WORKFLOWS || []).length} workflows Β· hover edges para ver actividades
1075
+ </div>
1076
+ </>
1077
+ ) : (
1078
+ <>
1079
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1080
+ <svg width="36" height="12">
1081
+ <line x1="0" y1="6" x2="36" y2="6"
1082
+ stroke={C.blue} strokeWidth="2" strokeLinecap="round" />
1083
+ <polygon points="32,3 36,6 32,9" fill={C.blue} />
1084
+ </svg>
1085
+ <span style={{ color: C.textMuted, fontSize: 12 }}>SΓ­ncrono (HTTP)</span>
1086
+ </div>
1087
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1088
+ <svg width="36" height="12">
1089
+ <line x1="0" y1="6" x2="36" y2="6"
1090
+ stroke={C.gold} strokeWidth="1.5" strokeDasharray="6 4" strokeLinecap="round" />
1091
+ <polygon points="32,3 36,6 32,9" fill={C.gold} />
1092
+ </svg>
1093
+ <span style={{ color: C.textMuted, fontSize: 12 }}>AsΓ­ncrono (Kafka)</span>
1094
+ </div>
1095
+ <div style={{ color: C.textMuted, fontSize: 11, marginLeft: "auto" }}>
1096
+ {MODULES_LIST.length} mΓ³dulos Β· {SYNC_INTEGRATIONS.length} puertos sync Β· {EVENTS.length} eventos
1097
+ </div>
1098
+ </>
1099
+ )}
888
1100
  </div>
889
1101
  </div>
890
1102
  );
@@ -911,17 +1123,76 @@
911
1123
  }
912
1124
 
913
1125
  // ─── DiagramPanel ────────────────────────────────────────────────────────────────
914
- function DiagramPanel({ moduleKey, diagramText }) {
1126
+ // Props:
1127
+ // moduleKey β€” unique key for re-render detection
1128
+ // diagramText β€” Mermaid syntax string
1129
+ // onNodeClick β€” optional callback(nodeLabel) when a click-enabled node is clicked
1130
+ function DiagramPanel({ moduleKey, diagramText, onNodeClick }) {
915
1131
  const containerRef = useRef(null);
916
1132
  const wrapperRef = useRef(null);
917
1133
  const [renderState, setRenderState] = useState('idle');
918
1134
  const [errorMsg, setErrorMsg] = useState('');
919
- const [wrapperJustify, setWrapperJustify] = useState('flex-start');
1135
+ const [zoom, setZoom] = useState(1);
1136
+ const [isPanning, setIsPanning] = useState(false);
1137
+ const [panOrigin, setPanOrigin] = useState({ x: 0, y: 0 });
1138
+ const [scrollOrigin, setScrollOrigin] = useState({ x: 0, y: 0 });
1139
+
1140
+ function zoomIn() { setZoom(function(z) { return Math.min(z + 0.2, 3); }); }
1141
+ function zoomOut() { setZoom(function(z) { return Math.max(z - 0.2, 0.3); }); }
1142
+ function zoomFit() {
1143
+ if (!wrapperRef.current || !containerRef.current) return;
1144
+ var svgEl = containerRef.current.querySelector('svg');
1145
+ if (!svgEl) return;
1146
+ var vb = svgEl.getAttribute('viewBox');
1147
+ if (!vb) { setZoom(1); return; }
1148
+ var intrinsicW = parseFloat(vb.split(/\s+/)[2]);
1149
+ var containerW = wrapperRef.current.clientWidth - 40;
1150
+ if (intrinsicW > 0 && containerW > 0) {
1151
+ setZoom(Math.min(containerW / intrinsicW, 2));
1152
+ }
1153
+ }
1154
+ function zoomReset() { setZoom(1); }
1155
+
1156
+ // Wheel zoom β€” use native listener via useEffect so we can set { passive: false }
1157
+ // (React onWheel is passive by default, so preventDefault is ignored)
1158
+ useEffect(function() {
1159
+ var el = wrapperRef.current;
1160
+ if (!el) return;
1161
+ function onWheel(e) {
1162
+ if (!e.ctrlKey && !e.metaKey) return;
1163
+ e.preventDefault();
1164
+ setZoom(function(z) {
1165
+ var delta = e.deltaY > 0 ? -0.1 : 0.1;
1166
+ return Math.max(0.3, Math.min(z + delta, 3));
1167
+ });
1168
+ }
1169
+ el.addEventListener('wheel', onWheel, { passive: false });
1170
+ return function() { el.removeEventListener('wheel', onWheel); };
1171
+ });
1172
+
1173
+ // Pan via mouse drag
1174
+ function handleMouseDown(e) {
1175
+ if (e.button !== 0) return;
1176
+ setIsPanning(true);
1177
+ setPanOrigin({ x: e.clientX, y: e.clientY });
1178
+ setScrollOrigin({
1179
+ x: wrapperRef.current ? wrapperRef.current.scrollLeft : 0,
1180
+ y: wrapperRef.current ? wrapperRef.current.scrollTop : 0,
1181
+ });
1182
+ }
1183
+ function handleMouseMove(e) {
1184
+ if (!isPanning || !wrapperRef.current) return;
1185
+ wrapperRef.current.scrollLeft = scrollOrigin.x - (e.clientX - panOrigin.x);
1186
+ wrapperRef.current.scrollTop = scrollOrigin.y - (e.clientY - panOrigin.y);
1187
+ }
1188
+ function handleMouseUp() { setIsPanning(false); }
920
1189
 
921
1190
  useEffect(() => {
922
1191
  if (!diagramText) return;
923
1192
  let cancelled = false;
924
1193
  setRenderState('loading');
1194
+ setZoom(1);
1195
+
925
1196
  (async () => {
926
1197
  try {
927
1198
  // Wait up to 4s for mermaid to load (deferred script)
@@ -931,31 +1202,57 @@
931
1202
  waited += 100;
932
1203
  }
933
1204
  if (!ensureMermaid()) throw new Error('Mermaid no estΓ‘ disponible');
1205
+
1206
+ // Enable click callbacks via securityLevel loose
1207
+ window.mermaid.initialize({
1208
+ startOnLoad: false,
1209
+ theme: 'dark',
1210
+ securityLevel: 'loose',
1211
+ themeVariables: {
1212
+ background: '#0a0a0f',
1213
+ primaryColor: '#1e1e2e',
1214
+ primaryBorderColor: '#4a4a8a',
1215
+ lineColor: '#8c8caa',
1216
+ fontFamily: "'JetBrains Mono', monospace",
1217
+ fontSize: '13px',
1218
+ },
1219
+ });
1220
+ _mermaidReady = true;
1221
+
934
1222
  if (cancelled) return;
935
1223
  const id = 'mmd-' + moduleKey.replace(/[^a-zA-Z0-9]/g, '-') + '-' + Date.now();
936
1224
  const { svg } = await window.mermaid.render(id, diagramText);
937
1225
  if (cancelled || !containerRef.current) return;
938
1226
  containerRef.current.innerHTML = svg;
1227
+
1228
+ // Manually bind click handlers to CMD_ and Q_ nodes in the SVG.
1229
+ // Mermaid v11 click directives are unreliable across versions,
1230
+ // so we bypass them and attach listeners directly to the DOM.
1231
+ if (onNodeClick) {
1232
+ containerRef.current.querySelectorAll('[id*="flowchart-CMD_"], [id*="flowchart-Q_"]').forEach(function(node) {
1233
+ var match = (node.id || '').match(/flowchart-(CMD|Q)_([^-]+)/);
1234
+ if (match) {
1235
+ var ucName = match[2];
1236
+ node.style.cursor = 'pointer';
1237
+ node.addEventListener('click', function(e) {
1238
+ e.stopPropagation();
1239
+ onNodeClick(ucName);
1240
+ });
1241
+ }
1242
+ });
1243
+ }
1244
+
939
1245
  const svgEl = containerRef.current.querySelector('svg');
940
1246
  if (svgEl) {
941
1247
  svgEl.removeAttribute('height');
942
1248
  svgEl.removeAttribute('width');
943
1249
 
944
- // Measure intrinsic width from viewBox to decide sizing strategy
1250
+ // Render at natural intrinsic size; zoom handles scaling
945
1251
  const vb = svgEl.getAttribute('viewBox');
946
1252
  const intrinsicW = vb ? parseFloat(vb.split(/\s+/)[2]) : 0;
947
- const containerW = wrapperRef.current ? wrapperRef.current.clientWidth - 40 : 0;
948
-
949
- if (intrinsicW > 0 && intrinsicW < containerW * 0.65) {
950
- // Small diagram (1-2 classes): render at natural size and center
1253
+ if (intrinsicW > 0) {
951
1254
  svgEl.style.width = intrinsicW + 'px';
952
- svgEl.style.maxWidth = '100%';
953
- setWrapperJustify('center');
954
- } else {
955
- // Large diagram: expand to fill; overflow: auto handles horizontal scroll
956
- svgEl.style.width = Math.max(intrinsicW, containerW) + 'px';
957
1255
  svgEl.style.maxWidth = 'none';
958
- setWrapperJustify('flex-start');
959
1256
  }
960
1257
  }
961
1258
  setRenderState('done');
@@ -976,26 +1273,293 @@
976
1273
  </div>
977
1274
  );
978
1275
  }
1276
+
1277
+ var zoomPct = Math.round(zoom * 100) + '%';
1278
+
979
1279
  return (
980
- <div
981
- ref={wrapperRef}
982
- style={{
983
- background: '#0d0d15', borderRadius: 10, padding: 20,
984
- overflow: 'auto', border: `1px solid ${C.border}`, minHeight: 200,
985
- display: 'flex', justifyContent: wrapperJustify,
986
- }}
987
- >
988
- {renderState === 'loading' && (
989
- <div style={{ color: C.textMuted, fontSize: 13, textAlign: 'center', padding: 30, width: '100%' }}>
990
- ⏳ Renderizando diagrama...
1280
+ <div style={{ position: 'relative' }}>
1281
+ {/* Zoom toolbar */}
1282
+ {renderState === 'done' && (
1283
+ <div style={{
1284
+ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 8,
1285
+ background: C.surface, border: '1px solid ' + C.border,
1286
+ borderRadius: 6, padding: '4px 8px', width: 'fit-content',
1287
+ }}>
1288
+ <button onClick={zoomOut} title="Zoom out" style={{
1289
+ background: 'transparent', color: C.textMuted,
1290
+ border: '1px solid ' + C.border, borderRadius: 4,
1291
+ padding: '3px 10px', fontSize: 13, cursor: 'pointer',
1292
+ fontFamily: 'inherit', fontWeight: 600,
1293
+ }}>βˆ’</button>
1294
+ <button onClick={zoomReset} title="Reset zoom" style={{
1295
+ background: 'transparent', color: C.textMuted,
1296
+ border: '1px solid ' + C.border, borderRadius: 4,
1297
+ padding: '3px 10px', fontSize: 13, cursor: 'pointer',
1298
+ fontFamily: "'JetBrains Mono', monospace", fontWeight: 600,
1299
+ minWidth: 48, textAlign: 'center',
1300
+ }}>{zoomPct}</button>
1301
+ <button onClick={zoomIn} title="Zoom in" style={{
1302
+ background: 'transparent', color: C.textMuted,
1303
+ border: '1px solid ' + C.border, borderRadius: 4,
1304
+ padding: '3px 10px', fontSize: 13, cursor: 'pointer',
1305
+ fontFamily: 'inherit', fontWeight: 600,
1306
+ }}>+</button>
1307
+ <button onClick={zoomFit} title="Fit to width" style={{
1308
+ background: 'transparent', color: C.textMuted,
1309
+ border: '1px solid ' + C.border, borderRadius: 4,
1310
+ padding: '3px 10px', fontSize: 13, cursor: 'pointer',
1311
+ fontFamily: 'inherit', fontWeight: 600,
1312
+ }}>β€’</button>
1313
+ <span style={{ fontSize: 11, color: C.textMuted, marginLeft: 8 }}>Ctrl+scroll Β· drag to pan</span>
991
1314
  </div>
992
1315
  )}
993
- {renderState === 'error' && (
994
- <div style={{ color: C.accent, fontSize: 12, padding: 10, fontFamily: "'JetBrains Mono', monospace" }}>
995
- ⚠ {errorMsg}
1316
+
1317
+ {/* Diagram viewport */}
1318
+ <div
1319
+ ref={wrapperRef}
1320
+ onMouseDown={handleMouseDown}
1321
+ onMouseMove={handleMouseMove}
1322
+ onMouseUp={handleMouseUp}
1323
+ onMouseLeave={handleMouseUp}
1324
+ style={{
1325
+ background: '#0d0d15', borderRadius: 10, padding: 20,
1326
+ overflow: 'auto', border: '1px solid ' + C.border, minHeight: 200,
1327
+ cursor: isPanning ? 'grabbing' : 'grab',
1328
+ }}
1329
+ >
1330
+ {renderState === 'loading' && (
1331
+ <div style={{ color: C.textMuted, fontSize: 13, textAlign: 'center', padding: 30, width: '100%' }}>
1332
+ ⏳ Renderizando diagrama...
1333
+ </div>
1334
+ )}
1335
+ {renderState === 'error' && (
1336
+ <div style={{ color: C.accent, fontSize: 12, padding: 10, fontFamily: "'JetBrains Mono', monospace" }}>
1337
+ ⚠ {errorMsg}
1338
+ </div>
1339
+ )}
1340
+ <div
1341
+ ref={containerRef}
1342
+ style={{
1343
+ display: renderState === 'done' ? 'block' : 'none',
1344
+ transform: 'scale(' + zoom + ')',
1345
+ transformOrigin: 'top left',
1346
+ transition: isPanning ? 'none' : 'transform 0.15s ease',
1347
+ }}
1348
+ />
1349
+ </div>
1350
+ </div>
1351
+ );
1352
+ }
1353
+
1354
+ // ─── UseCaseModal ─────────────────────────────────────────────────────────────
1355
+ function UseCaseModal({ uc, onClose }) {
1356
+ if (!uc) return null;
1357
+
1358
+ var typeColor = uc.type === 'command' ? '#E67E22' : '#4A90D9';
1359
+ var typeLabel = uc.type === 'command' ? 'βš™οΈ Command' : 'πŸ” Query';
1360
+ var badgeBg = uc.isStandard ? '#27AE6033' : '#E67E2233';
1361
+ var badgeBorder = uc.isStandard ? '#27AE6066' : '#E67E2266';
1362
+ var badgeColor = uc.isStandard ? '#27AE60' : '#E67E22';
1363
+ var badgeText = uc.isStandard ? 'EstΓ‘ndar' : 'Custom';
1364
+
1365
+ return (
1366
+ <div onClick={onClose} style={{
1367
+ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
1368
+ background: 'rgba(0,0,0,0.65)', zIndex: 9999,
1369
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
1370
+ animation: 'fadeIn 0.15s ease',
1371
+ }}>
1372
+ <div onClick={function(e) { e.stopPropagation(); }} style={{
1373
+ background: '#12121f', border: '1px solid #2a2a4a',
1374
+ borderRadius: 14, padding: 0, maxWidth: 560, width: '90%',
1375
+ maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
1376
+ }}>
1377
+ {/* Header */}
1378
+ <div style={{
1379
+ padding: '20px 24px 16px', borderBottom: '1px solid #1e1e3a',
1380
+ display: 'flex', alignItems: 'flex-start', gap: 12,
1381
+ }}>
1382
+ <div style={{ flex: 1 }}>
1383
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
1384
+ <span style={{ color: typeColor, fontSize: 12, fontWeight: 700,
1385
+ background: typeColor + '22', border: '1px solid ' + typeColor + '44',
1386
+ borderRadius: 4, padding: '2px 10px',
1387
+ }}>{typeLabel}</span>
1388
+ <span style={{ color: badgeColor, fontSize: 11, fontWeight: 600,
1389
+ background: badgeBg, border: '1px solid ' + badgeBorder,
1390
+ borderRadius: 4, padding: '2px 8px',
1391
+ }}>{badgeText}</span>
1392
+ </div>
1393
+ <div style={{ fontSize: 20, fontWeight: 800, color: '#e2e2f0',
1394
+ fontFamily: "'JetBrains Mono', monospace",
1395
+ }}>{uc.name}</div>
1396
+ <div style={{ fontSize: 12, color: '#8c8caa', marginTop: 4,
1397
+ fontFamily: "'JetBrains Mono', monospace",
1398
+ }}>{uc.name + (uc.type === 'command' ? 'CommandHandler' : 'QueryHandler')}</div>
1399
+ </div>
1400
+ <button onClick={onClose} style={{
1401
+ background: 'transparent', border: '1px solid #2a2a4a', borderRadius: 6,
1402
+ color: '#8c8caa', fontSize: 16, cursor: 'pointer', padding: '4px 10px',
1403
+ fontFamily: 'inherit',
1404
+ }}>βœ•</button>
996
1405
  </div>
997
- )}
998
- <div ref={containerRef} style={{ display: renderState === 'done' ? 'block' : 'none' }} />
1406
+
1407
+ {/* Body */}
1408
+ <div style={{ padding: '16px 24px 20px' }}>
1409
+ {/* Description */}
1410
+ <div style={{
1411
+ background: '#0d0d18', borderRadius: 8, padding: '12px 16px', marginBottom: 16,
1412
+ border: '1px solid #1e1e3a', fontSize: 13, color: '#b0b0cc', lineHeight: 1.6,
1413
+ }}>{uc.description}</div>
1414
+
1415
+ {/* Endpoint */}
1416
+ {uc.endpoint && (
1417
+ <div style={{ marginBottom: 14 }}>
1418
+ <div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Endpoint</div>
1419
+ <div style={{
1420
+ display: 'flex', alignItems: 'center', gap: 8,
1421
+ background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
1422
+ }}>
1423
+ <span style={{
1424
+ fontWeight: 800, fontSize: 12, color: uc.endpoint.method === 'GET' ? '#4A90D9' : uc.endpoint.method === 'POST' ? '#27AE60' : uc.endpoint.method === 'DELETE' ? '#E74C3C' : '#E67E22',
1425
+ fontFamily: "'JetBrains Mono', monospace",
1426
+ }}>{uc.endpoint.method}</span>
1427
+ <span style={{ fontSize: 13, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace" }}>{uc.endpoint.path}</span>
1428
+ {uc.endpoint.version && (
1429
+ <span style={{ fontSize: 11, color: '#8c8caa', marginLeft: 'auto',
1430
+ background: '#1e1e3a', borderRadius: 4, padding: '1px 6px',
1431
+ }}>{uc.endpoint.version}</span>
1432
+ )}
1433
+ </div>
1434
+ </div>
1435
+ )}
1436
+
1437
+ {/* Triggered by event */}
1438
+ {uc.triggeredBy && (
1439
+ <div style={{ marginBottom: 14 }}>
1440
+ <div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Disparado por evento</div>
1441
+ <div style={{
1442
+ background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
1443
+ display: 'flex', alignItems: 'center', gap: 8,
1444
+ }}>
1445
+ <span style={{ color: '#27AE60', fontSize: 13 }}>πŸ“₯</span>
1446
+ <span style={{ fontSize: 13, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace" }}>{uc.triggeredBy.event}</span>
1447
+ {uc.triggeredBy.producer && (
1448
+ <span style={{ fontSize: 11, color: '#8c8caa', marginLeft: 'auto' }}>← {uc.triggeredBy.producer}</span>
1449
+ )}
1450
+ </div>
1451
+ {uc.triggeredBy.topic && (
1452
+ <div style={{ fontSize: 11, color: '#666', marginTop: 4, fontFamily: "'JetBrains Mono', monospace" }}>topic: {uc.triggeredBy.topic}</div>
1453
+ )}
1454
+ </div>
1455
+ )}
1456
+
1457
+ {/* State transitions */}
1458
+ {uc.stateTransitions && uc.stateTransitions.length > 0 && (
1459
+ <div style={{ marginBottom: 14 }}>
1460
+ <div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Transiciones de estado</div>
1461
+ {uc.stateTransitions.map(function(t, i) {
1462
+ return (
1463
+ <div key={i} style={{
1464
+ background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
1465
+ marginBottom: 4, display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, flexWrap: 'wrap',
1466
+ }}>
1467
+ <span style={{ color: '#B07CC6', fontFamily: "'JetBrains Mono', monospace", fontWeight: 600 }}>{t.method}()</span>
1468
+ <span style={{ color: '#8c8caa' }}>:</span>
1469
+ <span style={{ color: '#E67E22', fontFamily: "'JetBrains Mono', monospace" }}>{t.from}</span>
1470
+ <span style={{ color: '#8c8caa' }}>β†’</span>
1471
+ <span style={{ color: '#27AE60', fontFamily: "'JetBrains Mono', monospace" }}>{t.to}</span>
1472
+ {t.guard && (
1473
+ <span style={{ fontSize: 11, color: '#E74C3C', marginLeft: 'auto' }}>guard: {t.guard}</span>
1474
+ )}
1475
+ </div>
1476
+ );
1477
+ })}
1478
+ </div>
1479
+ )}
1480
+
1481
+ {/* Events emitted */}
1482
+ {uc.eventsEmitted && uc.eventsEmitted.length > 0 && (
1483
+ <div style={{ marginBottom: 14 }}>
1484
+ <div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Eventos emitidos</div>
1485
+ {uc.eventsEmitted.map(function(ev, i) {
1486
+ return (
1487
+ <div key={i} style={{
1488
+ background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
1489
+ marginBottom: 4,
1490
+ }}>
1491
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
1492
+ <span style={{ color: '#E67E22', fontSize: 13 }}>πŸ“€</span>
1493
+ <span style={{ fontSize: 13, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace", fontWeight: 600 }}>{ev.name}</span>
1494
+ </div>
1495
+ {ev.fields.length > 0 && (
1496
+ <div style={{ marginTop: 4, fontSize: 11, color: '#8c8caa', fontFamily: "'JetBrains Mono', monospace" }}>
1497
+ {ev.fields.join(' Β· ')}
1498
+ </div>
1499
+ )}
1500
+ </div>
1501
+ );
1502
+ })}
1503
+ </div>
1504
+ )}
1505
+
1506
+ {/* Request fields */}
1507
+ {uc.requestFields && uc.requestFields.length > 0 && (
1508
+ <div style={{ marginBottom: 14 }}>
1509
+ <div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Campos del request</div>
1510
+ <div style={{
1511
+ background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
1512
+ }}>
1513
+ {uc.requestFields.map(function(f, i) {
1514
+ return (
1515
+ <div key={i} style={{
1516
+ display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0',
1517
+ borderBottom: i < uc.requestFields.length - 1 ? '1px solid #1a1a2e' : 'none',
1518
+ }}>
1519
+ <span style={{ fontSize: 12, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace", flex: 1 }}>{f.name}</span>
1520
+ <span style={{ fontSize: 11, color: '#8c8caa', fontFamily: "'JetBrains Mono', monospace" }}>{f.type}</span>
1521
+ {f.required && (
1522
+ <span style={{ fontSize: 10, color: '#E74C3C', fontWeight: 700 }}>REQ</span>
1523
+ )}
1524
+ </div>
1525
+ );
1526
+ })}
1527
+ </div>
1528
+ </div>
1529
+ )}
1530
+
1531
+ {/* Available ports */}
1532
+ {uc.availablePorts && uc.availablePorts.length > 0 && (
1533
+ <div style={{ marginBottom: 14 }}>
1534
+ <div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Puertos sΓ­ncronos disponibles</div>
1535
+ {uc.availablePorts.map(function(p, i) {
1536
+ return (
1537
+ <div key={i} style={{
1538
+ background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
1539
+ marginBottom: 4,
1540
+ }}>
1541
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
1542
+ <span style={{ color: '#16A085', fontSize: 13 }}>πŸ”—</span>
1543
+ <span style={{ fontSize: 13, color: '#e2e2f0', fontWeight: 600 }}>{p.service}</span>
1544
+ {p.target && (
1545
+ <span style={{ fontSize: 11, color: '#8c8caa', marginLeft: 'auto' }}>β†’ {p.target}</span>
1546
+ )}
1547
+ </div>
1548
+ <div style={{ marginTop: 4, fontSize: 11, color: '#8c8caa', fontFamily: "'JetBrains Mono', monospace" }}>
1549
+ {p.methods.join(' Β· ')}
1550
+ </div>
1551
+ </div>
1552
+ );
1553
+ })}
1554
+ </div>
1555
+ )}
1556
+
1557
+ {/* Aggregate */}
1558
+ <div style={{ fontSize: 11, color: '#666', borderTop: '1px solid #1e1e3a', paddingTop: 12, marginTop: 8 }}>
1559
+ Agregado: <strong style={{ color: '#B07CC6' }}>{uc.aggregate}</strong>
1560
+ </div>
1561
+ </div>
1562
+ </div>
999
1563
  </div>
1000
1564
  );
1001
1565
  }
@@ -1005,16 +1569,19 @@
1005
1569
  const [expandedChecks, setExpandedChecks] = useState({});
1006
1570
  const [selectedModule, setSelectedModule] = useState('all');
1007
1571
  const [view, setView] = useState('findings');
1572
+ const [selectedUC, setSelectedUC] = useState(null); // use case detail object for modal
1008
1573
  if (!DOMAIN_VALIDATION) return null;
1009
1574
 
1010
- const { summary, categories, diagrams } = DOMAIN_VALIDATION;
1575
+ const { summary, categories, diagrams, blueprints, useCaseDetails } = DOMAIN_VALIDATION;
1011
1576
 
1012
1577
  // Collect all unique module names that have at least one finding
1013
- const allModules = [...new Set(
1014
- categories.flatMap(cat =>
1015
- cat.checks.flatMap(check => check.findings.map(f => f.module))
1016
- )
1017
- )].sort();
1578
+ // Also include modules that have diagrams/blueprints even without findings
1579
+ const findingsModules = categories.flatMap(cat =>
1580
+ cat.checks.flatMap(check => check.findings.map(f => f.module))
1581
+ );
1582
+ const diagramModules = diagrams ? Object.keys(diagrams).filter(k => diagrams[k]) : [];
1583
+ const blueprintModules = blueprints ? Object.keys(blueprints).filter(k => blueprints[k]) : [];
1584
+ const allModules = [...new Set([...findingsModules, ...diagramModules, ...blueprintModules])].sort();
1018
1585
 
1019
1586
  const SEV_COLOR = {
1020
1587
  error: C.accent,
@@ -1110,13 +1677,13 @@
1110
1677
  )}
1111
1678
  </div>
1112
1679
 
1113
- {/* View toggle pill (only rendered when diagrams are available) */}
1114
- {diagrams && Object.keys(diagrams).length > 0 && (
1680
+ {/* View toggle pill (only rendered when diagrams or blueprints are available) */}
1681
+ {((diagrams && Object.keys(diagrams).length > 0) || (blueprints && Object.keys(blueprints).length > 0)) && (
1115
1682
  <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 20 }}>
1116
- {[{ id: 'findings', label: 'πŸ“‹ Hallazgos' }, { id: 'diagram', label: 'πŸ—Ί Diagrama' }].map(v => (
1683
+ {[{ id: 'findings', label: 'πŸ“‹ Hallazgos' }, { id: 'diagram', label: 'πŸ—Ί Diagrama' }, { id: 'blueprint', label: 'πŸ— Blueprint' }].map(v => (
1117
1684
  <button
1118
1685
  key={v.id}
1119
- onClick={() => setView(v.id)}
1686
+ onClick={() => { setView(v.id); }}
1120
1687
  style={{
1121
1688
  background: view === v.id ? C.purple + '33' : 'transparent',
1122
1689
  color: view === v.id ? C.purple : C.textMuted,
@@ -1143,6 +1710,31 @@
1143
1710
  </div>
1144
1711
  )}
1145
1712
 
1713
+ {/* Blueprint view */}
1714
+ {view === 'blueprint' && (
1715
+ <div>
1716
+ {selectedModule === 'all' ? (
1717
+ <div style={{ padding: 32, textAlign: 'center', background: C.surface, borderRadius: 10, border: `1px solid ${C.border}`, color: C.textMuted, fontSize: 13 }}>
1718
+ Selecciona un mΓ³dulo en el filtro de arriba para ver su blueprint de bounded context
1719
+ </div>
1720
+ ) : (
1721
+ <DiagramPanel
1722
+ moduleKey={selectedModule + '-bp'}
1723
+ diagramText={blueprints && blueprints[selectedModule]}
1724
+ onNodeClick={function(label) {
1725
+ var moduleUCs = useCaseDetails && useCaseDetails[selectedModule];
1726
+ if (moduleUCs && moduleUCs[label]) {
1727
+ setSelectedUC(moduleUCs[label]);
1728
+ }
1729
+ }}
1730
+ />
1731
+ )}
1732
+ </div>
1733
+ )}
1734
+
1735
+ {/* Use case detail modal */}
1736
+ {selectedUC && <UseCaseModal uc={selectedUC} onClose={function() { setSelectedUC(null); }} />}
1737
+
1146
1738
  {/* Category cards */}
1147
1739
  {view === 'findings' && categories.map(cat => {
1148
1740
  // Apply module filter to each check's findings
@@ -1276,22 +1868,765 @@
1276
1868
  </div>
1277
1869
  );
1278
1870
  }
1871
+ // ─── Temporal: role badge colors ────────────────────────────────────────
1872
+ const ROLE_STYLE = {
1873
+ READ: { color: C.blue, label: "READ", bg: C.blue + "22" },
1874
+ WRITE: { color: C.green, label: "WRITE", bg: C.green + "22" },
1875
+ COMPENSATE: { color: C.accent, label: "COMPENSATE", bg: C.accent + "22" },
1876
+ NOTIFY: { color: C.gold, label: "NOTIFY", bg: C.gold + "22" },
1877
+ };
1878
+
1879
+ const PRIMARY_ROLE_STYLE = {
1880
+ Orchestrator: { color: C.purple, icon: "🎯" },
1881
+ Executor: { color: C.green, icon: "βš™οΈ" },
1882
+ DataProvider: { color: C.blue, icon: "πŸ“¦" },
1883
+ Reactor: { color: C.gold, icon: "⚑" },
1884
+ Standalone: { color: C.textMuted, icon: "β—ˆ" },
1885
+ };
1886
+
1887
+ function RoleBadge({ role, small }) {
1888
+ const rs = ROLE_STYLE[role] || { color: C.textMuted, label: role, bg: C.surface };
1889
+ return (
1890
+ <span style={{
1891
+ background: rs.bg, color: rs.color, border: `1px solid ${rs.color}55`,
1892
+ borderRadius: 4, padding: small ? "0px 5px" : "2px 8px",
1893
+ fontSize: small ? 9 : 11, fontWeight: 700, letterSpacing: 0.5,
1894
+ fontFamily: "'JetBrains Mono', monospace", display: "inline-block",
1895
+ }}>{rs.label}</span>
1896
+ );
1897
+ }
1898
+
1899
+ // ─── WorkflowsTab ───────────────────────────────────────────────────────
1900
+ function WorkflowsTab() {
1901
+ const allWfs = [...(TEMPORAL_WORKFLOWS || []), ...(LOCAL_WORKFLOWS || [])];
1902
+ const [selectedIdx, setSelectedIdx] = useState(0);
1903
+ const [expandedStep, setExpandedStep] = useState(null);
1904
+ const wf = allWfs[selectedIdx] || null;
1905
+
1906
+ if (!wf) return (
1907
+ <div style={{ color: C.textMuted, padding: 60, textAlign: "center" }}>
1908
+ <div style={{ fontSize: 40, marginBottom: 16 }}>⏱️</div>
1909
+ No hay workflows Temporal declarados en system.yaml
1910
+ </div>
1911
+ );
1912
+
1913
+ const typeIcon = (type, actType) => {
1914
+ if (type === "async") return "⚑";
1915
+ if (actType === "heavy") return "πŸ‹οΈ";
1916
+ return "β–Ά";
1917
+ };
1918
+
1919
+ return (
1920
+ <div>
1921
+ {/* Workflow selector */}
1922
+ <div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
1923
+ {allWfs.map((w, i) => (
1924
+ <button key={i} onClick={() => { setSelectedIdx(i); setExpandedStep(null); }} style={{
1925
+ background: i === selectedIdx ? C.purple + "22" : C.surface,
1926
+ border: `1px solid ${i === selectedIdx ? C.purple : C.border}`,
1927
+ color: i === selectedIdx ? C.purple : C.textMuted,
1928
+ borderRadius: 8, padding: "7px 14px", cursor: "pointer",
1929
+ fontWeight: i === selectedIdx ? 700 : 400, fontSize: 12,
1930
+ transition: "all 0.15s", fontFamily: "inherit",
1931
+ display: "flex", alignItems: "center", gap: 6,
1932
+ }}>
1933
+ {w.saga && <span title="Saga">πŸ”„</span>}
1934
+ {w._isLocal && <span title="Local">πŸ“Œ</span>}
1935
+ {w.name}
1936
+ </button>
1937
+ ))}
1938
+ </div>
1939
+
1940
+ {/* Workflow header */}
1941
+ <div style={{
1942
+ background: C.surface, border: `1px solid ${C.purple}44`, borderRadius: 12,
1943
+ padding: 20, marginBottom: 20,
1944
+ }}>
1945
+ <div style={{ display: "flex", alignItems: "flex-start", gap: 16 }}>
1946
+ <div style={{ flex: 1 }}>
1947
+ <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
1948
+ <span style={{ fontWeight: 800, fontSize: 18, color: C.text }}>{wf.name}</span>
1949
+ {wf.saga && <Tag color={C.accent}>πŸ”„ SAGA</Tag>}
1950
+ {wf._isLocal && <Tag color={C.gold}>πŸ“Œ LOCAL</Tag>}
1951
+ </div>
1952
+ {wf.trigger && (
1953
+ <div style={{ marginTop: 10, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
1954
+ <span style={{ color: C.textMuted, fontSize: 11 }}>TRIGGER</span>
1955
+ <Tag color={wf.triggerModuleColor || C.blue}>
1956
+ {wf.triggerModuleIcon} {wf.triggerModuleLabel}
1957
+ </Tag>
1958
+ {wf.trigger.on && (
1959
+ <>
1960
+ <span style={{ color: C.textMuted, fontSize: 11 }}>evento</span>
1961
+ <code style={{ background: C.bg, color: C.gold, padding: "2px 8px", borderRadius: 4, fontSize: 11, border: `1px solid ${C.border}` }}>{wf.trigger.on}</code>
1962
+ </>
1963
+ )}
1964
+ </div>
1965
+ )}
1966
+ <div style={{ marginTop: 10, display: "flex", gap: 16, flexWrap: "wrap" }}>
1967
+ <span style={{ color: C.textMuted, fontSize: 11 }}>
1968
+ <span style={{ color: C.textDim, fontWeight: 700 }}>{wf.stepCount}</span> pasos
1969
+ </span>
1970
+ {wf.compensableStepCount > 0 && (
1971
+ <span style={{ color: C.textMuted, fontSize: 11 }}>
1972
+ <span style={{ color: C.accent, fontWeight: 700 }}>{wf.compensableStepCount}</span> compensables
1973
+ </span>
1974
+ )}
1975
+ {wf.asyncStepCount > 0 && (
1976
+ <span style={{ color: C.textMuted, fontSize: 11 }}>
1977
+ <span style={{ color: C.gold, fontWeight: 700 }}>{wf.asyncStepCount}</span> async (fire-and-forget)
1978
+ </span>
1979
+ )}
1980
+ {wf.taskQueue && (
1981
+ <span style={{ color: C.textMuted, fontSize: 11 }}>
1982
+ queue: <code style={{ color: C.purple, fontSize: 10 }}>{wf.taskQueue}</code>
1983
+ </span>
1984
+ )}
1985
+ </div>
1986
+ </div>
1987
+ </div>
1988
+ </div>
1989
+
1990
+ {/* Steps timeline */}
1991
+ <div style={{ marginBottom: 24 }}>
1992
+ {(wf.steps || []).filter(s => !s._isWait).map((step, i) => {
1993
+ const expanded = expandedStep === i;
1994
+ const roleSt = ROLE_STYLE[step.roleClass] || {};
1995
+ const actColor = roleSt.color || C.blue;
1996
+ return (
1997
+ <div key={i} style={{ display: "flex", gap: 14, marginBottom: 6 }}>
1998
+ {/* Timeline */}
1999
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 40, flexShrink: 0 }}>
2000
+ <div style={{
2001
+ width: 38, height: 38, borderRadius: "50%",
2002
+ display: "flex", alignItems: "center", justifyContent: "center",
2003
+ background: actColor + "22", border: `2px solid ${actColor}55`,
2004
+ color: actColor, fontWeight: 800, fontSize: 13,
2005
+ }}>{step._stepNum}</div>
2006
+ {i < (wf.steps || []).filter(s => !s._isWait).length - 1 && (
2007
+ <div style={{ width: 2, flex: 1, minHeight: 16, background: C.border, marginTop: 3 }} />
2008
+ )}
2009
+ </div>
2010
+ {/* Card */}
2011
+ <div style={{ flex: 1 }}>
2012
+ <div
2013
+ onClick={() => setExpandedStep(expanded ? null : i)}
2014
+ style={{
2015
+ background: expanded ? actColor + "11" : C.surface,
2016
+ border: `1px solid ${expanded ? actColor + "66" : C.border}`,
2017
+ borderRadius: expanded ? "10px 10px 0 0" : 10,
2018
+ padding: "11px 16px", cursor: "pointer", transition: "all 0.15s",
2019
+ }}
2020
+ >
2021
+ <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
2022
+ <span style={{ fontSize: 15 }}>{typeIcon(step.type, step.activityType)}</span>
2023
+ <span style={{ fontWeight: 700, color: C.text, fontSize: 14 }}>{step.activity}</span>
2024
+ <RoleBadge role={step.roleClass} />
2025
+ {step.type === "async" && <Tag color={C.gold}>async</Tag>}
2026
+ {step.activityType === "heavy" && <Tag color={C.orange}>heavy</Tag>}
2027
+ <div style={{ flex: 1 }} />
2028
+ <Tag color={step.targetColor || C.blue}>{step.targetIcon} {step.targetLabel || step.target}</Tag>
2029
+ <span style={{ color: C.textMuted, fontSize: 11 }}>{expanded ? "β–²" : "β–Ό"}</span>
2030
+ </div>
2031
+ {step.roleDesc && (
2032
+ <div style={{ color: C.textMuted, fontSize: 11, marginTop: 5, lineHeight: 1.5 }}>{step.roleDesc}</div>
2033
+ )}
2034
+ </div>
2035
+ {expanded && (
2036
+ <div style={{ border: `1px solid ${actColor}66`, borderTop: "none", borderRadius: "0 0 10px 10px", background: C.bg, padding: 16 }}>
2037
+ <div style={{ display: "flex", gap: 20, flexWrap: "wrap" }}>
2038
+ {/* Inputs */}
2039
+ {step.inputs && step.inputs.length > 0 && (
2040
+ <div style={{ flex: 1, minWidth: 200 }}>
2041
+ <div style={{ color: C.textMuted, fontSize: 10, fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>ENTRADAS</div>
2042
+ {step.inputs.map((inp, j) => (
2043
+ <div key={j} style={{ marginBottom: 4 }}>
2044
+ <code style={{ color: C.blue, fontSize: 11, background: C.surface, padding: "1px 6px", borderRadius: 3 }}>{inp.name}</code>
2045
+ <span style={{ color: C.textMuted, fontSize: 10, marginLeft: 6 }}>← {inp.source}</span>
2046
+ </div>
2047
+ ))}
2048
+ </div>
2049
+ )}
2050
+ {/* Outputs */}
2051
+ {step.outputs && step.outputs.length > 0 && (
2052
+ <div style={{ flex: 1, minWidth: 200 }}>
2053
+ <div style={{ color: C.textMuted, fontSize: 10, fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>SALIDAS</div>
2054
+ {step.outputs.map((out, j) => (
2055
+ <div key={j} style={{ marginBottom: 4 }}>
2056
+ <code style={{ color: C.green, fontSize: 11, background: C.surface, padding: "1px 6px", borderRadius: 3 }}>{out.name}</code>
2057
+ {out.consumers && out.consumers.length > 0 && (
2058
+ <span style={{ color: C.textMuted, fontSize: 10, marginLeft: 6 }}>β†’ {out.consumers.join(", ")}</span>
2059
+ )}
2060
+ </div>
2061
+ ))}
2062
+ </div>
2063
+ )}
2064
+ {/* Compensation */}
2065
+ {step.compensation && (
2066
+ <div style={{ flex: 1, minWidth: 200 }}>
2067
+ <div style={{ color: C.textMuted, fontSize: 10, fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>COMPENSACIΓ“N</div>
2068
+ <code style={{ color: C.accent, fontSize: 11, background: C.surface, padding: "1px 6px", borderRadius: 3 }}>{step.compensation}</code>
2069
+ </div>
2070
+ )}
2071
+ </div>
2072
+ {/* Retry / timeout */}
2073
+ <div style={{ display: "flex", gap: 12, marginTop: 10, flexWrap: "wrap" }}>
2074
+ {step.timeout && (
2075
+ <span style={{ color: C.textMuted, fontSize: 10 }}>
2076
+ ⏱ timeout: <code style={{ color: C.textDim }}>{step.timeout}</code>
2077
+ </span>
2078
+ )}
2079
+ {step.retryPolicy && (
2080
+ <span style={{ color: C.textMuted, fontSize: 10 }}>
2081
+ πŸ” retry: max {step.retryPolicy.maximumAttempts || "∞"} Β· backoff {step.retryPolicy.initialInterval || "1s"}
2082
+ </span>
2083
+ )}
2084
+ {!step.retryPolicy && step.activityType === "heavy" && (
2085
+ <span style={{ color: C.accent, fontSize: 10 }}>⚠ heavy activity sin retryPolicy declarado</span>
2086
+ )}
2087
+ </div>
2088
+ </div>
2089
+ )}
2090
+ </div>
2091
+ </div>
2092
+ );
2093
+ })}
2094
+ </div>
2095
+
2096
+ {/* Data flow table */}
2097
+ {wf.dataFlowTable && wf.dataFlowTable.length > 0 && (
2098
+ <div style={{ marginTop: 4 }}>
2099
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>TABLA DE FLUJO DE DATOS</div>
2100
+ <div style={{ overflowX: "auto" }}>
2101
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>
2102
+ <thead>
2103
+ <tr style={{ background: C.surface }}>
2104
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, fontWeight: 700, textAlign: "left" }}>Campo</th>
2105
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.blue, fontWeight: 700, textAlign: "left" }}>Origen</th>
2106
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.green, fontWeight: 700, textAlign: "left" }}>Consumido por</th>
2107
+ </tr>
2108
+ </thead>
2109
+ <tbody>
2110
+ {wf.dataFlowTable.map((row, i) => (
2111
+ <tr key={i} style={{ background: i % 2 === 0 ? C.bg : C.surface }}>
2112
+ <td style={{ border: `1px solid ${C.border}`, padding: "7px 12px" }}>
2113
+ <code style={{ color: C.text, fontSize: 11 }}>{row.field}</code>
2114
+ </td>
2115
+ <td style={{ border: `1px solid ${C.border}`, padding: "7px 12px", color: C.textDim, fontSize: 11 }}>{row.source}</td>
2116
+ <td style={{ border: `1px solid ${C.border}`, padding: "7px 12px" }}>
2117
+ {row.consumed && row.consumed.length > 0
2118
+ ? row.consumed.map((c, j) => (
2119
+ <span key={j} style={{ color: C.textDim, fontSize: 11, marginRight: 6 }}>{c.activity}</span>
2120
+ ))
2121
+ : <span style={{ color: C.textMuted, fontSize: 11 }}>β€” no consumido β€”</span>
2122
+ }
2123
+ </td>
2124
+ </tr>
2125
+ ))}
2126
+ </tbody>
2127
+ </table>
2128
+ </div>
2129
+ </div>
2130
+ )}
2131
+ </div>
2132
+ );
2133
+ }
2134
+
2135
+ // ─── SagaAnalysisTab ────────────────────────────────────────────────────
2136
+ function SagaAnalysisTab() {
2137
+ const sagas = SAGA_WORKFLOWS || [];
2138
+ const [selectedIdx, setSelectedIdx] = useState(0);
2139
+ const [failAtStep, setFailAtStep] = useState(null);
2140
+ const saga = sagas[selectedIdx] || null;
2141
+
2142
+ if (!sagas.length) return (
2143
+ <div style={{ color: C.textMuted, padding: 60, textAlign: "center" }}>
2144
+ <div style={{ fontSize: 40, marginBottom: 16 }}>πŸ”„</div>
2145
+ No hay workflows con <code style={{ color: C.accent }}>saga: true</code> declarados
2146
+ </div>
2147
+ );
2148
+
2149
+ const chain = saga ? (saga.sagaChain || []) : [];
2150
+ const lifo = [...chain].reverse();
2151
+
2152
+ // Simulate failure: steps after failAtStep get rolled back
2153
+ const isRolledBack = (stepNum) => failAtStep !== null && stepNum > failAtStep;
2154
+ const isFailPoint = (stepNum) => stepNum === failAtStep;
2155
+ const wasExecuted = (stepNum) => failAtStep === null || stepNum <= failAtStep;
2156
+
2157
+ return (
2158
+ <div>
2159
+ {/* Saga selector */}
2160
+ {sagas.length > 1 && (
2161
+ <div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
2162
+ {sagas.map((s, i) => (
2163
+ <button key={i} onClick={() => { setSelectedIdx(i); setFailAtStep(null); }} style={{
2164
+ background: i === selectedIdx ? C.accent + "22" : C.surface,
2165
+ border: `1px solid ${i === selectedIdx ? C.accent : C.border}`,
2166
+ color: i === selectedIdx ? C.accent : C.textMuted,
2167
+ borderRadius: 8, padding: "7px 14px", cursor: "pointer",
2168
+ fontWeight: i === selectedIdx ? 700 : 400, fontSize: 12,
2169
+ transition: "all 0.15s", fontFamily: "inherit",
2170
+ }}>πŸ”„ {s.name}</button>
2171
+ ))}
2172
+ </div>
2173
+ )}
2174
+
2175
+ {/* Saga header */}
2176
+ <div style={{ background: C.surface, border: `1px solid ${C.accent}44`, borderRadius: 12, padding: 20, marginBottom: 24 }}>
2177
+ <div style={{ fontWeight: 800, fontSize: 18, color: C.text, marginBottom: 8 }}>πŸ”„ {saga.name}</div>
2178
+ <div style={{ display: "flex", gap: 20, flexWrap: "wrap" }}>
2179
+ <span style={{ color: C.textMuted, fontSize: 12 }}>
2180
+ <span style={{ color: C.text, fontWeight: 700 }}>{chain.length}</span> pasos en la cadena
2181
+ </span>
2182
+ <span style={{ color: C.textMuted, fontSize: 12 }}>
2183
+ <span style={{ color: C.accent, fontWeight: 700 }}>{chain.filter(s => s.isCompensable).length}</span> compensables
2184
+ </span>
2185
+ <span style={{ color: C.textMuted, fontSize: 12 }}>
2186
+ <span style={{ color: C.gold, fontWeight: 700 }}>{chain.filter(s => !s.isCompensable && s.type === "async").length}</span> async (no compensables)
2187
+ </span>
2188
+ </div>
2189
+ </div>
2190
+
2191
+ {/* Fail simulator */}
2192
+ <div style={{ background: C.surface, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16, marginBottom: 24 }}>
2193
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>
2194
+ πŸ”΄ SIMULAR FALLO EN PASO:
2195
+ </div>
2196
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
2197
+ <button onClick={() => setFailAtStep(null)} style={{
2198
+ background: failAtStep === null ? C.green + "22" : C.bg,
2199
+ border: `1px solid ${failAtStep === null ? C.green : C.border}`,
2200
+ color: failAtStep === null ? C.green : C.textMuted,
2201
+ borderRadius: 6, padding: "5px 12px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
2202
+ }}>βœ… Sin fallo</button>
2203
+ {chain.filter(s => s.canFail).map((s) => (
2204
+ <button key={s.stepNum} onClick={() => setFailAtStep(s.stepNum)} style={{
2205
+ background: failAtStep === s.stepNum ? C.accent + "22" : C.bg,
2206
+ border: `1px solid ${failAtStep === s.stepNum ? C.accent : C.border}`,
2207
+ color: failAtStep === s.stepNum ? C.accent : C.textMuted,
2208
+ borderRadius: 6, padding: "5px 12px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
2209
+ }}>Paso {s.stepNum}</button>
2210
+ ))}
2211
+ </div>
2212
+ {failAtStep !== null && (
2213
+ <div style={{ marginTop: 12, padding: "10px 14px", background: C.accent + "11", border: `1px solid ${C.accent}44`, borderRadius: 8 }}>
2214
+ <div style={{ color: C.accent, fontWeight: 700, fontSize: 12 }}>
2215
+ πŸ’₯ Fallo en paso {failAtStep} β€” Se ejecutan compensaciones LIFO:
2216
+ </div>
2217
+ <div style={{ color: C.textDim, fontSize: 11, marginTop: 4, lineHeight: 1.7 }}>
2218
+ {lifo.filter(s => s.stepNum < failAtStep && s.isCompensable).map((s, i) => (
2219
+ <span key={i}>
2220
+ <code style={{ color: C.accent, background: C.surface, padding: "1px 5px", borderRadius: 3 }}>{s.compensationName}</code>
2221
+ {" en "}<span style={{ color: C.textDim }}>{s.targetModule}</span>
2222
+ {i < lifo.filter(ss => ss.stepNum < failAtStep && ss.isCompensable).length - 1 ? " β†’ " : ""}
2223
+ </span>
2224
+ ))}
2225
+ {lifo.filter(s => s.stepNum < failAtStep && s.isCompensable).length === 0 && (
2226
+ <span style={{ color: C.gold }}>⚠ Ningún paso previo tiene compensación declarada</span>
2227
+ )}
2228
+ </div>
2229
+ </div>
2230
+ )}
2231
+ </div>
2232
+
2233
+ {/* Execution chain (forward) */}
2234
+ <div style={{ marginBottom: 20 }}>
2235
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>
2236
+ β–Ά CADENA DE EJECUCIΓ“N (orden cronolΓ³gico)
2237
+ </div>
2238
+ {chain.map((step, i) => {
2239
+ const rolledBack = isRolledBack(step.stepNum);
2240
+ const failPoint = isFailPoint(step.stepNum);
2241
+ const executed = wasExecuted(step.stepNum);
2242
+ const color = failPoint ? C.accent : rolledBack ? C.textMuted : step.isCompensable ? C.green : C.gold;
2243
+
2244
+ return (
2245
+ <div key={i} style={{ display: "flex", gap: 12, marginBottom: 6, opacity: rolledBack ? 0.35 : 1 }}>
2246
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 36, flexShrink: 0 }}>
2247
+ <div style={{
2248
+ width: 34, height: 34, borderRadius: "50%",
2249
+ display: "flex", alignItems: "center", justifyContent: "center",
2250
+ background: color + "22", border: `2px solid ${color}55`,
2251
+ color, fontWeight: 800, fontSize: 12,
2252
+ }}>
2253
+ {failPoint ? "πŸ’₯" : rolledBack ? "β—‹" : step.stepNum}
2254
+ </div>
2255
+ {i < chain.length - 1 && (
2256
+ <div style={{ width: 2, flex: 1, minHeight: 12, background: C.border, marginTop: 3 }} />
2257
+ )}
2258
+ </div>
2259
+ <div style={{
2260
+ flex: 1, background: failPoint ? C.accent + "11" : C.surface,
2261
+ border: `1px solid ${failPoint ? C.accent : rolledBack ? C.border : color + "44"}`,
2262
+ borderRadius: 8, padding: "10px 14px",
2263
+ }}>
2264
+ <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
2265
+ <span style={{ fontWeight: 700, color: failPoint ? C.accent : C.text, fontSize: 13 }}>
2266
+ {step.activityName}
2267
+ </span>
2268
+ {step.isCompensable && <Tag color={C.green}>compensable</Tag>}
2269
+ {step.type === "async" && !step.isCompensable && <Tag color={C.gold}>async Β· no compensable</Tag>}
2270
+ {failPoint && <Tag color={C.accent}>FALLO AQUÍ</Tag>}
2271
+ <div style={{ flex: 1 }} />
2272
+ <span style={{ color: C.textMuted, fontSize: 11 }}>{step.targetModule}</span>
2273
+ {step.timeout && <code style={{ color: C.textMuted, fontSize: 10 }}>⏱ {step.timeout}</code>}
2274
+ </div>
2275
+ {step.isCompensable && step.compensationName && (
2276
+ <div style={{ marginTop: 5, color: C.textMuted, fontSize: 11 }}>
2277
+ ↩ compensaciΓ³n: <code style={{ color: C.accent, background: C.bg, padding: "1px 5px", borderRadius: 3 }}>{step.compensationName}</code>
2278
+ </div>
2279
+ )}
2280
+ </div>
2281
+ </div>
2282
+ );
2283
+ })}
2284
+ </div>
2285
+
2286
+ {/* LIFO compensation chain */}
2287
+ <div>
2288
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>
2289
+ ↩ CADENA DE COMPENSACIΓ“N LIFO
2290
+ </div>
2291
+ {lifo.filter(s => s.isCompensable).length === 0 && (
2292
+ <div style={{ color: C.gold, fontSize: 12, padding: 16, background: C.gold + "11", border: `1px solid ${C.gold}44`, borderRadius: 8 }}>
2293
+ ⚠ Ningún paso de este workflow tiene compensación declarada. Considera agregar <code>compensation:</code> en cada paso crítico de la saga.
2294
+ </div>
2295
+ )}
2296
+ {lifo.filter(s => s.isCompensable).map((step, i, arr) => (
2297
+ <div key={i} style={{ display: "flex", gap: 12, marginBottom: 6 }}>
2298
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 36, flexShrink: 0 }}>
2299
+ <div style={{
2300
+ width: 34, height: 34, borderRadius: "50%",
2301
+ display: "flex", alignItems: "center", justifyContent: "center",
2302
+ background: C.accent + "22", border: `2px solid ${C.accent}55`,
2303
+ color: C.accent, fontWeight: 800, fontSize: 12,
2304
+ }}>{i + 1}</div>
2305
+ {i < arr.length - 1 && (
2306
+ <div style={{ width: 2, flex: 1, minHeight: 12, background: C.accent + "33", marginTop: 3 }} />
2307
+ )}
2308
+ </div>
2309
+ <div style={{ flex: 1, background: C.accent + "08", border: `1px solid ${C.accent}33`, borderRadius: 8, padding: "10px 14px" }}>
2310
+ <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
2311
+ <span style={{ color: C.accent, fontWeight: 700, fontSize: 12 }}>↩ {step.compensationName}</span>
2312
+ <span style={{ color: C.textMuted, fontSize: 11 }}>en <span style={{ color: C.textDim }}>{step.targetModule}</span></span>
2313
+ <span style={{ color: C.textMuted, fontSize: 11 }}>
2314
+ (revierte paso {step.stepNum}: {step.activityName})
2315
+ </span>
2316
+ </div>
2317
+ </div>
2318
+ </div>
2319
+ ))}
2320
+ </div>
2321
+ </div>
2322
+ );
2323
+ }
2324
+
2325
+ // ─── ActivityCatalogTab ─────────────────────────────────────────────────
2326
+ function ActivityCatalogTab() {
2327
+ const catalog = ACTIVITY_CATALOG || [];
2328
+ const [roleFilter, setRoleFilter] = useState("ALL");
2329
+ const [moduleFilter, setModuleFilter] = useState("ALL");
2330
+ const [showOrphans, setShowOrphans] = useState(false);
2331
+
2332
+ const modules = [...new Set(catalog.map(a => a.module))].sort();
2333
+ const roles = ["ALL", "READ", "WRITE", "COMPENSATE", "NOTIFY"];
2334
+
2335
+ const filtered = catalog.filter(a => {
2336
+ if (roleFilter !== "ALL" && a.role !== roleFilter) return false;
2337
+ if (moduleFilter !== "ALL" && a.module !== moduleFilter) return false;
2338
+ if (showOrphans && !a.isOrphan) return false;
2339
+ return true;
2340
+ });
2341
+
2342
+ const orphanCount = catalog.filter(a => a.isOrphan).length;
2343
+
2344
+ return (
2345
+ <div>
2346
+ {/* Stats bar */}
2347
+ <div style={{ display: "flex", gap: 12, marginBottom: 20, flexWrap: "wrap" }}>
2348
+ {[
2349
+ { label: "Total", count: catalog.length, color: C.text },
2350
+ { label: "READ", count: catalog.filter(a => a.role === "READ").length, color: C.blue },
2351
+ { label: "WRITE", count: catalog.filter(a => a.role === "WRITE").length, color: C.green },
2352
+ { label: "COMPENSATE", count: catalog.filter(a => a.role === "COMPENSATE").length, color: C.accent },
2353
+ { label: "NOTIFY", count: catalog.filter(a => a.role === "NOTIFY").length, color: C.gold },
2354
+ { label: "Orphans", count: orphanCount, color: C.orange },
2355
+ ].map(stat => (
2356
+ <div key={stat.label} style={{
2357
+ background: C.surface, border: `1px solid ${C.border}`,
2358
+ borderRadius: 10, padding: "12px 18px", textAlign: "center", minWidth: 90,
2359
+ }}>
2360
+ <div style={{ fontSize: 24, fontWeight: 900, color: stat.color, fontFamily: "monospace" }}>{stat.count}</div>
2361
+ <div style={{ color: C.textMuted, fontSize: 10, marginTop: 2, fontWeight: 700, letterSpacing: 0.5 }}>{stat.label}</div>
2362
+ </div>
2363
+ ))}
2364
+ </div>
2365
+
2366
+ {/* Filters */}
2367
+ <div style={{ display: "flex", gap: 10, marginBottom: 16, flexWrap: "wrap", alignItems: "center" }}>
2368
+ <span style={{ color: C.textMuted, fontSize: 11 }}>Rol:</span>
2369
+ {roles.map(r => (
2370
+ <button key={r} onClick={() => setRoleFilter(r)} style={{
2371
+ background: roleFilter === r ? (ROLE_STYLE[r] || { color: C.text }).color + "22" : C.surface,
2372
+ border: `1px solid ${roleFilter === r ? (ROLE_STYLE[r] || { color: C.border }).color : C.border}`,
2373
+ color: roleFilter === r ? (ROLE_STYLE[r] || { color: C.text }).color : C.textMuted,
2374
+ borderRadius: 6, padding: "4px 10px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
2375
+ }}>{r}</button>
2376
+ ))}
2377
+ <span style={{ color: C.border }}>|</span>
2378
+ <span style={{ color: C.textMuted, fontSize: 11 }}>MΓ³dulo:</span>
2379
+ <select value={moduleFilter} onChange={e => setModuleFilter(e.target.value)} style={{
2380
+ background: C.surface, border: `1px solid ${C.border}`, color: C.textDim,
2381
+ borderRadius: 6, padding: "4px 10px", fontSize: 11, fontFamily: "inherit",
2382
+ }}>
2383
+ <option value="ALL">Todos</option>
2384
+ {modules.map(m => <option key={m} value={m}>{m}</option>)}
2385
+ </select>
2386
+ <span style={{ color: C.border }}>|</span>
2387
+ <button onClick={() => setShowOrphans(!showOrphans)} style={{
2388
+ background: showOrphans ? C.orange + "22" : C.surface,
2389
+ border: `1px solid ${showOrphans ? C.orange : C.border}`,
2390
+ color: showOrphans ? C.orange : C.textMuted,
2391
+ borderRadius: 6, padding: "4px 10px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
2392
+ }}>⚠ Solo orphans ({orphanCount})</button>
2393
+ </div>
2394
+
2395
+ {/* Table */}
2396
+ <div style={{ overflowX: "auto" }}>
2397
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>
2398
+ <thead>
2399
+ <tr style={{ background: C.surface }}>
2400
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Actividad</th>
2401
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>MΓ³dulo</th>
2402
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Rol</th>
2403
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Tipo</th>
2404
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>DescripciΓ³n</th>
2405
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Usado en</th>
2406
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "center" }}>Retry</th>
2407
+ </tr>
2408
+ </thead>
2409
+ <tbody>
2410
+ {filtered.map((act, i) => {
2411
+ const roleSt = ROLE_STYLE[act.role] || { color: C.textMuted };
2412
+ return (
2413
+ <tr key={i} style={{ background: i % 2 === 0 ? C.bg : C.surface }}>
2414
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
2415
+ <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
2416
+ <code style={{ color: C.text, fontSize: 12 }}>{act.name}</code>
2417
+ {act.isOrphan && <span title="No usado en ningún workflow" style={{ color: C.orange, fontSize: 10 }}>⚠</span>}
2418
+ </div>
2419
+ </td>
2420
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textDim, fontSize: 11 }}>{act.moduleLabel || act.module}</td>
2421
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
2422
+ <RoleBadge role={act.role} small />
2423
+ </td>
2424
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
2425
+ <Tag color={act.type === "heavy" ? C.orange : C.textMuted}>{act.type}</Tag>
2426
+ </td>
2427
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, fontSize: 11, maxWidth: 280, lineHeight: 1.5 }}>
2428
+ {act.description}
2429
+ </td>
2430
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
2431
+ {act.usedInWorkflows && act.usedInWorkflows.length > 0
2432
+ ? act.usedInWorkflows.map((w, j) => (
2433
+ <div key={j} style={{ color: C.purple, fontSize: 10, marginBottom: 2 }}>{w}</div>
2434
+ ))
2435
+ : <span style={{ color: C.orange, fontSize: 11 }}>⚠ orphan</span>
2436
+ }
2437
+ </td>
2438
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", textAlign: "center" }}>
2439
+ {act.hasRetryPolicy
2440
+ ? <span style={{ color: C.green, fontSize: 14 }} title={`max: ${act.retryPolicy?.maximumAttempts}`}>βœ“</span>
2441
+ : act.type === "heavy"
2442
+ ? <span style={{ color: C.accent, fontSize: 14 }} title="Heavy activity sin retryPolicy">⚠</span>
2443
+ : <span style={{ color: C.border, fontSize: 12 }}>β€”</span>
2444
+ }
2445
+ </td>
2446
+ </tr>
2447
+ );
2448
+ })}
2449
+ </tbody>
2450
+ </table>
2451
+ {filtered.length === 0 && (
2452
+ <div style={{ color: C.textMuted, padding: 20, textAlign: "center", fontSize: 12 }}>Sin resultados para los filtros seleccionados</div>
2453
+ )}
2454
+ </div>
2455
+
2456
+ {/* External type deps */}
2457
+ {EXTERNAL_TYPE_DEPS && EXTERNAL_TYPE_DEPS.length > 0 && (
2458
+ <div style={{ marginTop: 28 }}>
2459
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>
2460
+ DEPENDENCIAS DE TIPOS EXTERNOS
2461
+ </div>
2462
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
2463
+ {EXTERNAL_TYPE_DEPS.map((dep, i) => (
2464
+ <div key={i} style={{
2465
+ background: C.surface, border: `1px solid ${C.purple}44`, borderRadius: 8,
2466
+ padding: "8px 14px", fontSize: 11,
2467
+ }}>
2468
+ <span style={{ color: C.text, fontWeight: 700 }}>{dep.consumerModule}</span>
2469
+ <span style={{ color: C.textMuted }}>.</span>
2470
+ <span style={{ color: C.blue }}>{dep.activityName}</span>
2471
+ <span style={{ color: C.textMuted }}> usa </span>
2472
+ <code style={{ color: C.purple }}>{dep.typeName}</code>
2473
+ <span style={{ color: C.textMuted }}> de </span>
2474
+ <span style={{ color: C.green }}>{dep.sourceModule}</span>
2475
+ </div>
2476
+ ))}
2477
+ </div>
2478
+ </div>
2479
+ )}
2480
+ </div>
2481
+ );
2482
+ }
2483
+
2484
+ // ─── TemporalArchitectureTab ─────────────────────────────────────────────
2485
+ function TemporalArchitectureTab() {
2486
+ const roles = Object.values(MODULE_ROLES || {});
2487
+ const queues = Object.values(QUEUE_TOPOLOGY || {});
2488
+
2489
+ return (
2490
+ <div>
2491
+ {/* Orchestration info */}
2492
+ {TEMPORAL_ORCHESTRATION && (
2493
+ <div style={{ background: C.surface, border: `1px solid ${C.purple}44`, borderRadius: 12, padding: 20, marginBottom: 24 }}>
2494
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>TEMPORAL SERVER</div>
2495
+ <div style={{ display: "flex", gap: 24, flexWrap: "wrap" }}>
2496
+ <span style={{ fontSize: 12 }}>
2497
+ <span style={{ color: C.textMuted }}>target: </span>
2498
+ <code style={{ color: C.purple }}>{TEMPORAL_ORCHESTRATION.target}</code>
2499
+ </span>
2500
+ <span style={{ fontSize: 12 }}>
2501
+ <span style={{ color: C.textMuted }}>namespace: </span>
2502
+ <code style={{ color: C.green }}>{TEMPORAL_ORCHESTRATION.namespace}</code>
2503
+ </span>
2504
+ </div>
2505
+ </div>
2506
+ )}
2507
+
2508
+ {/* Module roles grid */}
2509
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>ROL POR MΓ“DULO</div>
2510
+ <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 12, marginBottom: 28 }}>
2511
+ {roles.map((mod, i) => {
2512
+ const rs = PRIMARY_ROLE_STYLE[mod.primaryRole] || PRIMARY_ROLE_STYLE["Standalone"];
2513
+ return (
2514
+ <div key={i} style={{
2515
+ background: C.surface, border: `1px solid ${rs.color}44`,
2516
+ borderRadius: 12, padding: 16,
2517
+ boxShadow: `0 0 16px ${rs.color}12`,
2518
+ }}>
2519
+ <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
2520
+ <span style={{ fontSize: 22 }}>{rs.icon}</span>
2521
+ <div>
2522
+ <div style={{ fontWeight: 700, color: C.text, fontSize: 14 }}>{mod.label || mod.name}</div>
2523
+ <div style={{ color: rs.color, fontSize: 10, fontWeight: 700, letterSpacing: 0.5 }}>{mod.roleLabel}</div>
2524
+ </div>
2525
+ </div>
2526
+ <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
2527
+ {(mod.roles || []).map((r, j) => {
2528
+ const rrs = PRIMARY_ROLE_STYLE[r] || {};
2529
+ return (
2530
+ <span key={j} style={{
2531
+ background: (rrs.color || C.textMuted) + "22", color: rrs.color || C.textMuted,
2532
+ border: `1px solid ${(rrs.color || C.textMuted)}44`,
2533
+ borderRadius: 4, padding: "1px 7px", fontSize: 10, fontWeight: 700,
2534
+ }}>{r}</span>
2535
+ );
2536
+ })}
2537
+ </div>
2538
+ </div>
2539
+ );
2540
+ })}
2541
+ </div>
2542
+
2543
+ {/* Queue topology */}
2544
+ {queues.length > 0 && (
2545
+ <div>
2546
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>TASK QUEUES POR MΓ“DULO</div>
2547
+ <div style={{ overflowX: "auto" }}>
2548
+ <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 11 }}>
2549
+ <thead>
2550
+ <tr style={{ background: C.surface }}>
2551
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>MΓ³dulo</th>
2552
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.purple, textAlign: "left" }}>Workflow Queue</th>
2553
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.orange, textAlign: "left" }}>Heavy Task Queue</th>
2554
+ <th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.green, textAlign: "left" }}>Light Task Queue</th>
2555
+ </tr>
2556
+ </thead>
2557
+ <tbody>
2558
+ {queues.map((q, i) => (
2559
+ <tr key={i} style={{ background: i % 2 === 0 ? C.bg : C.surface }}>
2560
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", fontWeight: 700, color: C.text }}>{q.label || q.name}</td>
2561
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
2562
+ <code style={{ color: C.purple, fontSize: 10 }}>{q.flowQueue}</code>
2563
+ </td>
2564
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
2565
+ <code style={{ color: C.orange, fontSize: 10 }}>{q.heavyQueue}</code>
2566
+ </td>
2567
+ <td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
2568
+ <code style={{ color: C.green, fontSize: 10 }}>{q.lightQueue}</code>
2569
+ </td>
2570
+ </tr>
2571
+ ))}
2572
+ </tbody>
2573
+ </table>
2574
+ </div>
2575
+ </div>
2576
+ )}
2577
+
2578
+ {/* Cross-workflow module interaction matrix */}
2579
+ <div style={{ marginTop: 28 }}>
2580
+ <div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>PARTICIPACIΓ“N EN WORKFLOWS</div>
2581
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
2582
+ {(TEMPORAL_WORKFLOWS || []).map((wf, i) => (
2583
+ <div key={i} style={{
2584
+ background: C.surface, border: `1px solid ${C.purple}33`, borderRadius: 10,
2585
+ padding: "12px 16px", minWidth: 200,
2586
+ }}>
2587
+ <div style={{ fontWeight: 700, color: C.purple, fontSize: 13, marginBottom: 8 }}>
2588
+ {wf.saga ? "πŸ”„ " : "⏱️ "}{wf.name}
2589
+ </div>
2590
+ {[...new Set((wf.steps || []).filter(s => !s._isWait).map(s => s.target || wf.triggerModule))].map((mod, j) => {
2591
+ const rs = PRIMARY_ROLE_STYLE[(MODULE_ROLES[mod] || {}).primaryRole] || { color: C.textMuted, icon: "β—ˆ" };
2592
+ return (
2593
+ <div key={j} style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
2594
+ <span style={{ fontSize: 12 }}>{rs.icon}</span>
2595
+ <span style={{ color: C.textDim, fontSize: 11 }}>{(MODULE_ROLES[mod] || {}).label || mod}</span>
2596
+ </div>
2597
+ );
2598
+ })}
2599
+ </div>
2600
+ ))}
2601
+ </div>
2602
+ </div>
2603
+ </div>
2604
+ );
2605
+ }
2606
+
1279
2607
  // ─── App root ───────────────────────────────────────────────────────────
1280
2608
  function App() {
1281
2609
  const [tab, setTab] = useState("validation");
1282
2610
 
1283
- const tabs = [
1284
- { id: "validation", label: "ValidaciΓ³n", icon: "πŸ”" },
1285
- { id: "flows", label: "Simulador de flujos", icon: "β–Ά" },
1286
- { id: "architecture", label: "Arquitectura", icon: "πŸ—ΊοΈ" },
1287
- { id: "diagram", label: "Diagrama", icon: "β—ˆ" },
1288
- ...(DOMAIN_VALIDATION ? [{ id: "domain", label: "Dominio", icon: "πŸ›οΈ" }] : []),
1289
- ];
2611
+ const tabs = IS_TEMPORAL_MODE
2612
+ ? [
2613
+ { id: "validation", label: "ValidaciΓ³n", icon: "πŸ”" },
2614
+ { id: "workflows", label: "Workflows", icon: "⏱️" },
2615
+ ...(SAGA_WORKFLOWS && SAGA_WORKFLOWS.length > 0 ? [{ id: "saga", label: "AnΓ‘lisis Saga", icon: "πŸ”„" }] : []),
2616
+ { id: "activities", label: "Actividades", icon: "πŸ—‚οΈ" },
2617
+ { id: "architecture", label: "Arquitectura", icon: "πŸ—ΊοΈ" },
2618
+ { id: "diagram", label: "Diagrama", icon: "β—ˆ" },
2619
+ ...(DOMAIN_VALIDATION ? [{ id: "domain", label: "Dominio", icon: "πŸ›οΈ" }] : []),
2620
+ ]
2621
+ : [
2622
+ { id: "validation", label: "ValidaciΓ³n", icon: "πŸ”" },
2623
+ { id: "flows", label: "Simulador de flujos", icon: "β–Ά" },
2624
+ { id: "architecture", label: "Arquitectura", icon: "πŸ—ΊοΈ" },
2625
+ { id: "diagram", label: "Diagrama", icon: "β—ˆ" },
2626
+ ...(DOMAIN_VALIDATION ? [{ id: "domain", label: "Dominio", icon: "πŸ›οΈ" }] : []),
2627
+ ];
1290
2628
 
1291
2629
  const sys = window.__EVA_DATA__;
1292
- const tech = [];
1293
- // Detect tech from systemConfig embedded in data
1294
- if (sys.events && sys.events.length > 0) tech.push({ label: "Kafka", color: C.gold });
1295
2630
 
1296
2631
  return (
1297
2632
  <div style={{ background: C.bg, minHeight: "100vh", color: C.text, fontFamily: "'Plus Jakarta Sans', system-ui, -apple-system, sans-serif" }}>
@@ -1307,11 +2642,26 @@
1307
2642
  <div style={{ fontWeight: 700, fontSize: 16, color: C.text }}>{systemName}</div>
1308
2643
  <div style={{ flex: 1 }} />
1309
2644
  <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
1310
- {sys.events && sys.events.length > 0 && (
1311
- <Tag color={C.gold}>Kafka Β· {sys.events.length} events</Tag>
1312
- )}
1313
- {sys.syncIntegrations && sys.syncIntegrations.length > 0 && (
1314
- <Tag color={C.blue}>Sync Β· {sys.syncIntegrations.length} ports</Tag>
2645
+ {IS_TEMPORAL_MODE ? (
2646
+ <>
2647
+ <Tag color={C.purple}>⏱️ Temporal</Tag>
2648
+ {TEMPORAL_ORCHESTRATION && (
2649
+ <Tag color={C.textMuted}>{TEMPORAL_ORCHESTRATION.namespace}</Tag>
2650
+ )}
2651
+ {TEMPORAL_WORKFLOWS && <Tag color={C.blue}>{TEMPORAL_WORKFLOWS.length} workflows</Tag>}
2652
+ {SAGA_WORKFLOWS && SAGA_WORKFLOWS.length > 0 && (
2653
+ <Tag color={C.accent}>πŸ”„ {SAGA_WORKFLOWS.length} sagas</Tag>
2654
+ )}
2655
+ </>
2656
+ ) : (
2657
+ <>
2658
+ {sys.events && sys.events.length > 0 && (
2659
+ <Tag color={C.gold}>Kafka Β· {sys.events.length} events</Tag>
2660
+ )}
2661
+ {sys.syncIntegrations && sys.syncIntegrations.length > 0 && (
2662
+ <Tag color={C.blue}>Sync Β· {sys.syncIntegrations.length} ports</Tag>
2663
+ )}
2664
+ </>
1315
2665
  )}
1316
2666
  <Tag color={C.purple}>{sys.modules.length} modules</Tag>
1317
2667
  </div>
@@ -1346,8 +2696,15 @@
1346
2696
  {/* Tab content */}
1347
2697
  <div style={{ maxWidth: 1100, margin: "0 auto", padding: "28px 24px" }}>
1348
2698
  {tab === "validation" && <ValidationTab />}
1349
- {tab === "flows" && <FlowSimulator />}
1350
- {tab === "architecture" && <ArchitectureTab />}
2699
+ {/* Temporal-specific tabs */}
2700
+ {IS_TEMPORAL_MODE && tab === "workflows" && <WorkflowsTab />}
2701
+ {IS_TEMPORAL_MODE && tab === "saga" && <SagaAnalysisTab />}
2702
+ {IS_TEMPORAL_MODE && tab === "activities" && <ActivityCatalogTab />}
2703
+ {IS_TEMPORAL_MODE && tab === "architecture" && <TemporalArchitectureTab />}
2704
+ {/* Broker-mode tabs */}
2705
+ {!IS_TEMPORAL_MODE && tab === "flows" && <FlowSimulator />}
2706
+ {!IS_TEMPORAL_MODE && tab === "architecture" && <ArchitectureTab />}
2707
+ {/* Shared tabs */}
1351
2708
  {tab === "diagram" && <DiagramTab />}
1352
2709
  {tab === "domain" && DOMAIN_VALIDATION && <DomainTab />}
1353
2710
  </div>