synapse-hub 2.6.0 → 3.0.0

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 (143) hide show
  1. package/.agents/agents/failure-analyst.md +4 -0
  2. package/.agents/agents/live-explorer.md +2 -0
  3. package/.agents/agents/orchestrator.md +16 -0
  4. package/.agents/agents/pom-crafter.md +10 -0
  5. package/.agents/agents/quality-gatekeeper.md +5 -0
  6. package/.agents/agents/test-runner.md +8 -0
  7. package/.agents/rules/git-push-protocol.md +1 -1
  8. package/.agents/rules/graphify.md +2 -0
  9. package/.agents/rules/mandatory-playwright-live.md +9 -9
  10. package/.agents/rules/mobile_appium_rules.md +8 -1
  11. package/.agents/skills/actor-qa-swarm/SKILL.md +77 -0
  12. package/.agents/skills/actor-qa-swarm/examples/broker_sharding_sample.json +12 -0
  13. package/.agents/skills/agentless-qa-repair/SKILL.md +69 -0
  14. package/.agents/skills/agentless-qa-repair/examples/healing_report_sample.json +17 -0
  15. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +1 -1
  16. package/.agents/skills/clean-pom-crafter/SKILL.md +2 -2
  17. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +4 -4
  18. package/.agents/skills/event-stream-qa-orchestrator/SKILL.md +79 -0
  19. package/.agents/skills/event-stream-qa-orchestrator/examples/timeline_sample.jsonl +2 -0
  20. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +2 -2
  21. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +6 -21
  22. package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
  23. package/.agents/skills/lean-impact-scoreboard/SKILL.md +1 -1
  24. package/.agents/skills/mipro-prompt-compiler/SKILL.md +59 -0
  25. package/.agents/skills/mipro-prompt-compiler/examples/compiled_module_sample.json +22 -0
  26. package/.agents/skills/qa-aci-interface/SKILL.md +67 -0
  27. package/.agents/skills/qa-aci-interface/examples/surgical_edit_sample.json +12 -0
  28. package/.agents/skills/qa-memory-os/SKILL.md +80 -0
  29. package/.agents/skills/qa-memory-os/examples/archival_memory_sample.json +21 -0
  30. package/.agents/skills/repo-quality-audit/SKILL.md +1 -1
  31. package/.agents/skills/review-spec/SKILL.md +60 -0
  32. package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
  33. package/.agents/skills/senior-code-review/SKILL.md +1 -1
  34. package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
  35. package/.agents/skills/technical-debt-harvester/SKILL.md +1 -1
  36. package/.agents/skills/tot-exploratory-testing/SKILL.md +59 -0
  37. package/.agents/skills/tot-exploratory-testing/examples/tot_graph_sample.json +35 -0
  38. package/.agents/skills/wcag-audit-patterns/SKILL.md +2 -2
  39. package/.agents/skills/web-agent-gym-benchmarking/SKILL.md +62 -0
  40. package/.agents/skills/web-agent-gym-benchmarking/examples/benchmark_report_sample.json +22 -0
  41. package/CLAUDE.md +2 -2
  42. package/README.en.md +70 -291
  43. package/README.md +68 -303
  44. package/docs/ARCHITECTURE.md +54 -8
  45. package/docs/CONTRIBUTING.md +1 -1
  46. package/docs/QUALITY_GATES.md +113 -0
  47. package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
  48. package/package.json +1 -1
  49. package/scripts/package_dist.py +2 -1
  50. package/skills_manifest.json +92 -11
  51. package/synapse.config.json +1 -1
  52. package/templates/ai_bridges/CLAUDE.md +2 -2
  53. package/templates/ai_bridges/copilot-instructions.md +2 -2
  54. package/tools/TOOLS_RUNBOOK.md +1 -1
  55. package/tools/__init__.py +18 -1
  56. package/tools/_openapi_parser.py +158 -0
  57. package/tools/agent_canary.py +42 -30
  58. package/tools/agentless_patcher.py +417 -0
  59. package/tools/analyze_project.py +12 -11
  60. package/tools/api_drift_sentinel.py +4 -4
  61. package/tools/api_fuzzer.py +8 -8
  62. package/tools/auto_heal.py +23 -5
  63. package/tools/auto_heal_reflexion.py +2 -2
  64. package/tools/autogen_qa_broker.py +318 -0
  65. package/tools/browser_helpers.js +2 -2
  66. package/tools/browsergym_eval.py +236 -0
  67. package/tools/bug_reporter.py +11 -13
  68. package/tools/bump_version.py +75 -5
  69. package/tools/cache_resolver.py +78 -83
  70. package/tools/check_env.py +9 -1
  71. package/tools/check_version_sync.py +40 -3
  72. package/tools/ci_healing_hook.py +5 -7
  73. package/tools/cli.py +5 -7
  74. package/tools/config_decoupler.py +2 -3
  75. package/tools/core/__init__.py +52 -0
  76. package/tools/count_tokens.py +22 -20
  77. package/tools/dashboard_generator.py +628 -999
  78. package/tools/db_sandbox.py +69 -53
  79. package/tools/debt_harvester.py +3 -3
  80. package/tools/distill_log.py +3 -3
  81. package/tools/dspy_qa_signatures.py +3 -4
  82. package/tools/export_postman_collection.py +8 -8
  83. package/tools/flaky_detector.py +2 -2
  84. package/tools/fuzz_payloads.py +5 -5
  85. package/tools/generate_allure_report.py +1 -1
  86. package/tools/generate_jmx_plan.py +4 -4
  87. package/tools/gherkin_suite.py +1 -1
  88. package/tools/git_pre_commit_guard.py +2 -2
  89. package/tools/goldfish_review.py +2 -2
  90. package/tools/heal/__init__.py +28 -0
  91. package/tools/heal_pr_automator.py +29 -4
  92. package/tools/heuristic_locator_matcher.py +2 -2
  93. package/tools/infer_linter.py +8 -8
  94. package/tools/interview_story.py +0 -1
  95. package/tools/jtl_analyzer.py +11 -11
  96. package/tools/lint/__init__.py +30 -0
  97. package/tools/lint_ai_docs.py +42 -0
  98. package/tools/lookup_tc.py +1 -1
  99. package/tools/matrix_sharder.py +15 -13
  100. package/tools/mcp/__init__.py +16 -0
  101. package/tools/mcp_excel_server.py +2 -2
  102. package/tools/mcp_server.py +3 -4
  103. package/tools/md_to_xlsx.py +26 -26
  104. package/tools/micro_mutator.py +11 -12
  105. package/tools/mipro_assertion_tuner.py +207 -0
  106. package/tools/network_mock_shield.py +27 -27
  107. package/tools/newman_analyzer.py +2 -3
  108. package/tools/omni_emitter.py +5 -8
  109. package/tools/omni_parser.py +5 -5
  110. package/tools/omni_transpiler.py +5 -5
  111. package/tools/output_formatter.py +19 -0
  112. package/tools/perf/__init__.py +24 -0
  113. package/tools/pinpoint_failure.py +13 -1
  114. package/tools/project_state.py +4 -2
  115. package/tools/qa_event_stream.py +336 -0
  116. package/tools/repo_map.py +8 -8
  117. package/tools/report/__init__.py +32 -0
  118. package/tools/reversible_sandbox.py +5 -2
  119. package/tools/safety_firewall.py +2 -2
  120. package/tools/senior_code_review.py +2 -2
  121. package/tools/setup_project.py +10 -12
  122. package/tools/skill_collision_guard.py +3 -3
  123. package/tools/skill_vault.py +12 -3
  124. package/tools/skillspector.py +5 -7
  125. package/tools/smart_visual_masker.py +4 -6
  126. package/tools/spec_drift_sentinel.py +1 -2
  127. package/tools/swe_qa_aci.py +369 -0
  128. package/tools/synapse_qa_bench.py +3 -3
  129. package/tools/sync_agent_rules.py +18 -4
  130. package/tools/telemetry_correlator.py +3 -2
  131. package/tools/test_memory_os.py +419 -0
  132. package/tools/textgrad_mutation_optimizer.py +3 -3
  133. package/tools/tms_sync_bridge.py +5 -5
  134. package/tools/tot_exploratory_tester.py +239 -0
  135. package/tools/traffic_session_converter.py +9 -9
  136. package/tools/universal_ir.py +2 -3
  137. package/tools/update_sentinel.py +6 -9
  138. package/tools/verify_all.py +10 -0
  139. package/tools/visual_diff_canvas.py +1 -2
  140. package/tools/visual_som_helper.py +15 -13
  141. package/tools/watch_mode.py +1 -1
  142. package/tools/wcag_audit.py +22 -1
  143. package/tools/welcome_page.py +5 -6
@@ -14,7 +14,7 @@ Supports Dual-Mode:
14
14
  1. TARGET PROJECT MODE: Displays target project test suites, POM PageClasses,
15
15
  automation tech stack (Playwright/Katalon/Pytest/Cypress), in-project 10 Gates DoD,
16
16
  and active AI skill profile.
17
- 2. HUB ECOSYSTEM MODE: Displays central Synapse-Hub catalog (42 skills),
17
+ 2. HUB ECOSYSTEM MODE: Displays central Synapse-Hub catalog (51 skills),
18
18
  self-healing benchmark (25 challenges), and platform governance.
19
19
 
20
20
  Features:
@@ -34,7 +34,7 @@ import webbrowser
34
34
  import xml.etree.ElementTree as ET
35
35
  from datetime import datetime
36
36
  from pathlib import Path
37
- from typing import Dict, Any, List, Optional, Tuple
37
+ from typing import Dict, Any, Optional, List, Tuple
38
38
 
39
39
  # Enforce UTF-8 stdout encoding for Windows console compatibility
40
40
  if hasattr(sys.stdout, 'reconfigure'):
@@ -177,17 +177,19 @@ def detect_tech_stack(target_dir: str) -> Dict[str, str]:
177
177
 
178
178
 
179
179
  def load_manifest(hub_root: str) -> Dict[str, Any]:
180
+ """Loads and returns the skill manifest catalog JSON data."""
180
181
  path = os.path.join(hub_root, "skills_manifest.json")
181
182
  if os.path.exists(path):
182
183
  try:
183
184
  with open(path, "r", encoding="utf-8") as f:
184
185
  return json.load(f)
185
186
  except (OSError, json.JSONDecodeError):
186
- manifest_data = None
187
- return {"totalSkills": 42, "skills": [], "categories": {}, "profiles": {}}
187
+ _ = False
188
+ return {"totalSkills": 50, "skills": [], "categories": {}, "profiles": {}}
188
189
 
189
190
 
190
191
  def load_benchmark_report(hub_root: str) -> Dict[str, Any]:
192
+ """Parses benchmark metrics from BENCHMARK_REPORT.md into a structured dictionary."""
191
193
  candidates = [
192
194
  os.path.join(hub_root, "benchmarks", "qa_eval", "reports", "BENCHMARK_REPORT.md"),
193
195
  os.path.join(hub_root, "reports", "BENCHMARK_REPORT.md")
@@ -209,7 +211,7 @@ def load_benchmark_report(hub_root: str) -> Dict[str, Any]:
209
211
  "available": True
210
212
  }
211
213
  except (OSError, UnicodeDecodeError):
212
- report_found = False
214
+ _ = False
213
215
  return {
214
216
  "resolution_rate": "100.0%",
215
217
  "mttr": "0.0s",
@@ -220,6 +222,7 @@ def load_benchmark_report(hub_root: str) -> Dict[str, Any]:
220
222
 
221
223
 
222
224
  def load_health_report(target_dir: str) -> Dict[str, Any]:
225
+ """Parses project health statistics and quality gates status from PROJECT_HEALTH_REPORT.md."""
223
226
  candidates = [
224
227
  os.path.join(target_dir, "PROJECT_HEALTH_REPORT.md"),
225
228
  os.path.join(target_dir, "reports", "PROJECT_HEALTH_REPORT.md")
@@ -242,7 +245,7 @@ def load_health_report(target_dir: str) -> Dict[str, Any]:
242
245
  "available": True
243
246
  }
244
247
  except (OSError, UnicodeDecodeError):
245
- report_found = False
248
+ _ = False
246
249
  return {
247
250
  "dod_status": "100%_PASS",
248
251
  "classes_count": 0,
@@ -268,7 +271,7 @@ def load_latest_execution_results(target_dir: str) -> Dict[str, Any]:
268
271
  xml_candidates = list(target_path.glob("**/test-results/**/*.xml"))
269
272
 
270
273
  xml_candidates = [
271
- p for p in xml_candidates
274
+ p for p in xml_candidates
272
275
  if not any(ign in str(p) for ign in [".git", "node_modules", ".cache", "bin"])
273
276
  ]
274
277
 
@@ -277,7 +280,7 @@ def load_latest_execution_results(target_dir: str) -> Dict[str, Any]:
277
280
  latest_xml = max(xml_candidates, key=lambda p: p.stat().st_mtime)
278
281
  tree = ET.parse(latest_xml)
279
282
  root = tree.getroot()
280
-
283
+
281
284
  suite_name = html_lib.escape(root.attrib.get("name") or "Automated Test Run")
282
285
  total = int(root.attrib.get("tests", 0))
283
286
  failures = int(root.attrib.get("failures", 0))
@@ -285,15 +288,15 @@ def load_latest_execution_results(target_dir: str) -> Dict[str, Any]:
285
288
  skipped = int(root.attrib.get("skipped", 0))
286
289
  incompleted = int(root.attrib.get("incompleted", 0))
287
290
  time_sec = float(root.attrib.get("time", 0.0))
288
-
291
+
289
292
  passed = int(root.attrib.get("passed", total - (failures + errors + skipped + incompleted)))
290
293
  if passed < 0:
291
294
  passed = 0
292
-
295
+
293
296
  pass_rate = round((passed / total * 100) if total > 0 else 100.0, 1)
294
297
  mtime_str = datetime.fromtimestamp(latest_xml.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
295
298
  rel_path = html_lib.escape(str(latest_xml.relative_to(target_path)))
296
-
299
+
297
300
  return {
298
301
  "available": True,
299
302
  "suite_name": suite_name,
@@ -308,7 +311,7 @@ def load_latest_execution_results(target_dir: str) -> Dict[str, Any]:
308
311
  "report_path": rel_path
309
312
  }
310
313
  except (ET.ParseError, OSError, ValueError, KeyError):
311
- parsed_xml = False
314
+ _ = False
312
315
 
313
316
  # Check summaryTemp.txt if JUnit report not parsed
314
317
  summary_files = list(target_path.glob("**/summaryTemp.txt"))
@@ -341,7 +344,7 @@ def load_latest_execution_results(target_dir: str) -> Dict[str, Any]:
341
344
  "report_path": str(sf.relative_to(target_path))
342
345
  }
343
346
  except (OSError, ValueError):
344
- parsed_summary = False
347
+ _ = False
345
348
 
346
349
  return {
347
350
  "available": False,
@@ -364,7 +367,7 @@ def scan_target_project_assets(target_dir: str, stack_info: Dict[str, str]) -> D
364
367
 
365
368
  # 1. Discover Page Classes
366
369
  classes_list = []
367
-
370
+
368
371
  # Priority A: Check repo_map.json
369
372
  json_candidates = [
370
373
  target_path / "repo_map.json",
@@ -401,7 +404,7 @@ def scan_target_project_assets(target_dir: str, stack_info: Dict[str, str]) -> D
401
404
  if classes_list:
402
405
  break
403
406
  except (json.JSONDecodeError, OSError, KeyError):
404
- json_parsed = False
407
+ _ = False
405
408
 
406
409
  # Priority B: Parse repo_map.txt
407
410
  if not classes_list:
@@ -488,11 +491,11 @@ def scan_target_project_assets(target_dir: str, stack_info: Dict[str, str]) -> D
488
491
  "methods": m_list or ["(Defined in source)"]
489
492
  })
490
493
  except (OSError, UnicodeDecodeError):
491
- report_found = False
494
+ _ = False
492
495
 
493
496
  # 2. Discover Test Cases
494
497
  test_files = []
495
-
498
+
496
499
  # 2A. Check Scripts/
497
500
  scripts_dir = target_path / "Scripts"
498
501
  if scripts_dir.is_dir():
@@ -707,24 +710,295 @@ def build_swarm_timeline_html(default_lang: str = "vi") -> str:
707
710
  """
708
711
 
709
712
 
710
- def generate_target_project_html(
711
- target_dir: str,
712
- stack_info: Dict[str, str],
713
- health: Dict[str, Any],
714
- assets: Dict[str, Any],
715
- manifest: Dict[str, Any],
716
- execution: Optional[Dict[str, Any]] = None,
717
- default_lang: str = "vi",
713
+
714
+ # ==============================================================================
715
+ # DECOMPOSED DASHBOARD HTML RENDERING SUBSYSTEM
716
+ # ==============================================================================
717
+
718
+ DASHBOARD_COMMON_CSS = """
719
+ :root {
720
+ --bg-primary: #0b0f19;
721
+ --bg-secondary: #131b2e;
722
+ --bg-card: #1a243b;
723
+ --bg-hover: #22304e;
724
+ --border-color: #2a3b60;
725
+ --text-primary: #f8fafc;
726
+ --text-secondary: #94a3b8;
727
+ --text-muted: #64748b;
728
+ --accent-cyan: #06b6d4;
729
+ --accent-indigo: #6366f1;
730
+ --accent-emerald: #10b981;
731
+ --accent-amber: #f59e0b;
732
+ --accent-rose: #f43f5e;
733
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
734
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
735
+ }
736
+ * { margin: 0; padding: 0; box-sizing: border-box; }
737
+ @keyframes fadeInUp {
738
+ from { opacity: 0; transform: translateY(16px); }
739
+ to { opacity: 1; transform: translateY(0); }
740
+ }
741
+ @keyframes pulseGlow {
742
+ 0%, 100% { box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35); }
743
+ 50% { box-shadow: 0 4px 24px rgba(6, 182, 212, 0.55); }
744
+ }
745
+ body {
746
+ background-color: var(--bg-primary);
747
+ color: var(--text-primary);
748
+ font-family: var(--font-sans);
749
+ padding: 32px 24px;
750
+ min-height: 100vh;
751
+ line-height: 1.5;
752
+ }
753
+ .container { max-width: 1320px; margin: 0 auto; animation: fadeInUp 0.4s ease-out; }
754
+ header {
755
+ display: flex; justify-content: space-between; align-items: center;
756
+ border-bottom: 1px solid var(--border-color); padding-bottom: 24px; margin-bottom: 24px;
757
+ flex-wrap: wrap; gap: 16px;
758
+ }
759
+ .brand-box { display: flex; align-items: center; gap: 14px; }
760
+ .logo-badge {
761
+ background: linear-gradient(135deg, var(--accent-indigo), var(--accent-cyan));
762
+ color: #ffffff; width: 48px; height: 48px; border-radius: 12px;
763
+ display: flex; align-items: center; justify-content: center;
764
+ font-size: 24px; box-shadow: 0 4px 16px rgba(6, 182, 212, 0.3);
765
+ }
766
+ .brand-title h1 { font-size: 20px; font-weight: 800; color: #ffffff; letter-spacing: -0.02em; }
767
+ .brand-title p { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
768
+ .header-meta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
769
+ .meta-pill {
770
+ background: var(--bg-secondary); border: 1px solid var(--border-color);
771
+ padding: 6px 12px; border-radius: 8px; font-size: 12px; color: var(--text-secondary);
772
+ }
773
+ .meta-pill strong { color: var(--text-primary); }
774
+ .lang-switch { display: inline-flex; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 8px; padding: 2px; }
775
+ .lang-btn {
776
+ background: transparent; border: none; color: var(--text-secondary); font-size: 12px;
777
+ font-weight: 700; padding: 4px 10px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease;
778
+ }
779
+ .lang-btn.active { background: var(--accent-indigo); color: #ffffff; }
780
+
781
+ .execution-banner {
782
+ display: flex; justify-content: space-between; align-items: center;
783
+ border-radius: 12px; padding: 16px 20px; margin-bottom: 24px;
784
+ border: 1px solid; flex-wrap: wrap; gap: 16px;
785
+ }
786
+ .exec-left { display: flex; align-items: center; gap: 14px; }
787
+ .exec-badge { font-size: 12px; font-weight: 800; padding: 4px 10px; border-radius: 6px; }
788
+ .exec-stats { display: flex; gap: 16px; }
789
+ .exec-stat { font-size: 12px; color: var(--text-secondary); }
790
+ .exec-stat strong { color: #ffffff; font-size: 14px; }
791
+
792
+ .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }
793
+ .kpi-card {
794
+ background: var(--bg-card); border: 1px solid var(--border-color);
795
+ border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: space-between;
796
+ }
797
+ .kpi-label { font-size: 12px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
798
+ .kpi-value { font-size: 32px; font-weight: 800; margin: 8px 0; font-family: var(--font-mono); }
799
+ .kpi-subtext { font-size: 12px; color: var(--text-secondary); }
800
+
801
+ .section-header { display: flex; align-items: center; justify-content: space-between; margin: 32px 0 16px 0; }
802
+ .section-header h2 { font-size: 16px; font-weight: 800; color: #ffffff; display: flex; align-items: center; gap: 8px; }
803
+ .section-badge { background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-secondary); font-size: 11px; padding: 3px 8px; border-radius: 6px; font-weight: 600; }
804
+
805
+ .gates-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; margin-bottom: 24px; }
806
+ .gate-card {
807
+ background: var(--bg-secondary); border: 1px solid var(--border-color);
808
+ border-radius: 10px; padding: 16px; transition: transform 0.15s ease, border-color 0.15s ease;
809
+ }
810
+ .gate-card:hover { transform: translateY(-2px); border-color: var(--accent-cyan); }
811
+ .gate-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
812
+ .gate-id { font-family: var(--font-mono); font-size: 11px; font-weight: 800; color: var(--accent-cyan); }
813
+ .gate-title { font-size: 14px; font-weight: 700; color: #ffffff; margin-bottom: 4px; }
814
+ .gate-desc { font-size: 12px; color: var(--text-secondary); line-height: 1.4; }
815
+
816
+ .split-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
817
+ @media (max-width: 900px) { .split-grid { grid-template-columns: 1fr; } }
818
+ .panel, .sec-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; padding: 20px; }
819
+ .metric-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05); font-size: 13px; }
820
+ .metric-row:last-child { border-bottom: none; }
821
+ .metric-label { color: var(--text-secondary); }
822
+ .metric-val { font-weight: 700; font-family: var(--font-mono); }
823
+
824
+ .suites-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; margin-bottom: 24px; }
825
+ .suite-category-card { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 10px; padding: 16px; }
826
+ .suite-cat-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
827
+ .suite-cat-title { font-weight: 700; font-size: 14px; color: #ffffff; text-transform: capitalize; }
828
+ .suite-pills { display: flex; flex-wrap: wrap; gap: 6px; }
829
+ .suite-pill { background: var(--bg-card); border: 1px solid var(--border-color); font-size: 11px; font-family: var(--font-mono); color: var(--text-secondary); padding: 3px 8px; border-radius: 6px; }
830
+
831
+ .table-container, .skills-container {
832
+ background: var(--bg-card); border: 1px solid var(--border-color);
833
+ border-radius: 12px; padding: 16px; margin-bottom: 24px; overflow-x: auto;
834
+ }
835
+ .table-toolbar, .skills-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; flex-wrap: wrap; gap: 12px; }
836
+ .search-input {
837
+ background: var(--bg-primary); border: 1px solid var(--border-color);
838
+ color: #ffffff; padding: 8px 14px; border-radius: 8px; font-size: 13px;
839
+ width: 280px; outline: none;
840
+ }
841
+ .search-input:focus { border-color: var(--accent-cyan); }
842
+ table { width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; }
843
+ th {
844
+ color: var(--text-muted); font-size: 11px; text-transform: uppercase;
845
+ font-weight: 700; padding: 12px 14px; border-bottom: 2px solid var(--border-color);
846
+ }
847
+ td { padding: 12px 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); vertical-align: middle; }
848
+ tr:hover td { background: var(--bg-hover); }
849
+
850
+ .badge { display: inline-block; padding: 3px 8px; border-radius: 6px; font-size: 11px; font-weight: 700; }
851
+ .badge-pass { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }
852
+ .badge-fail { background: rgba(244, 63, 94, 0.15); color: var(--accent-rose); border: 1px solid rgba(244, 63, 94, 0.3); }
853
+ .badge-cat { background: rgba(99, 102, 241, 0.15); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.3); }
854
+ .badge-ver { background: rgba(148, 163, 184, 0.12); color: #cbd5e1; font-family: var(--font-mono); }
855
+
856
+ .filter-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
857
+ .filter-btn {
858
+ background: var(--bg-primary); border: 1px solid var(--border-color);
859
+ color: var(--text-secondary); padding: 6px 12px; border-radius: 8px;
860
+ font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s ease;
861
+ }
862
+ .filter-btn:hover { border-color: var(--accent-cyan); color: #ffffff; }
863
+ .filter-btn.active { background: var(--accent-indigo); border-color: var(--accent-indigo); color: #ffffff; }
864
+
865
+ /* FSM Swarm Stepper & Step Inspector */
866
+ .fsm-stepper { display: flex; gap: 12px; margin-bottom: 20px; overflow-x: auto; padding-bottom: 8px; }
867
+ .fsm-step {
868
+ flex: 1; min-width: 170px; background: var(--bg-secondary); border: 1px solid var(--border-color);
869
+ border-radius: 10px; padding: 12px; position: relative; transition: all 0.2s ease;
870
+ }
871
+ .fsm-step:hover { border-color: var(--accent-cyan); transform: translateY(-2px); }
872
+ .fsm-step-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
873
+ .fsm-step-num { font-size: 11px; font-weight: 800; color: var(--accent-cyan); font-family: var(--font-mono); }
874
+ .fsm-step-agent { font-size: 10px; background: rgba(99, 102, 241, 0.15); color: #818cf8; padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }
875
+ .fsm-step-title { font-size: 13px; font-weight: 700; color: #ffffff; margin-bottom: 4px; }
876
+ .fsm-step-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.3; }
877
+
878
+ .badge-llm { background: rgba(168, 85, 247, 0.18); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
879
+ .badge-tool { background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); border: 1px solid rgba(6, 182, 212, 0.3); }
880
+ .badge-gate { background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }
881
+ .step-detail-box { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); background: var(--bg-primary); padding: 4px 8px; border-radius: 4px; display: inline-block; }
882
+
883
+ footer { text-align: center; font-size: 12px; color: var(--text-muted); padding-top: 24px; border-top: 1px solid var(--border-color); }
884
+ footer a { color: var(--accent-cyan); text-decoration: none; }
885
+ footer a:hover { text-decoration: underline; }
886
+ """
887
+
888
+
889
+ def _render_gates_cards(gates: List[Dict[str, str]], default_lang: str) -> str:
890
+ """Renders Quality Gate status cards."""
891
+ cards = []
892
+ for i, g in enumerate(gates):
893
+ name_initial = g["name_vi"] if default_lang == "vi" else g["name_en"]
894
+ desc_initial = g["desc_vi"] if default_lang == "vi" else g["desc_en"]
895
+ cards.append(f"""
896
+ <div class="gate-card">
897
+ <div class="gate-header">
898
+ <span class="gate-id">{g['id']}</span>
899
+ <span class="badge badge-pass">✓ {g['status']}</span>
900
+ </div>
901
+ <div class="gate-title" data-i18n="gate_{i}_name">{name_initial}</div>
902
+ <div class="gate-desc" data-i18n="gate_{i}_desc">{desc_initial}</div>
903
+ </div>
904
+ """)
905
+ return "\n".join(cards)
906
+
907
+
908
+ def _inject_gate_translations(trans_vi: Dict[str, str], trans_en: Dict[str, str], gates: List[Dict[str, str]]) -> None:
909
+ """Injects dynamic gate name and description entries into translation maps."""
910
+ for i, g in enumerate(gates):
911
+ trans_vi[f"gate_{i}_name"] = g["name_vi"]
912
+ trans_en[f"gate_{i}_name"] = g["name_en"]
913
+ trans_vi[f"gate_{i}_desc"] = g["desc_vi"]
914
+ trans_en[f"gate_{i}_desc"] = g["desc_en"]
915
+
916
+
917
+ def _render_html_page_wrapper(
918
+ title: str,
919
+ default_lang: str,
920
+ content_html: str,
921
+ translations_vi: Dict[str, str],
922
+ translations_en: Dict[str, str],
923
+ extra_js: str = "",
718
924
  ) -> str:
719
- """Renders executive HTML dashboard tailored specifically to a Target Test Suite with bilingual support."""
720
- now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
721
- raw_proj_name = stack_info["name"] or os.path.basename(os.path.abspath(target_dir))
722
- project_name = html_lib.escape(str(raw_proj_name))
723
- if execution is None:
724
- execution = load_latest_execution_results(target_dir)
925
+ """Renders complete HTML5 page shell with common styles and interactive bilingual script."""
926
+ translations_json = f"const translations = {{ vi: {json.dumps(translations_vi, ensure_ascii=False)}, en: {json.dumps(translations_en, ensure_ascii=False)} }};"
927
+
928
+ return f"""<!DOCTYPE html>
929
+ <html lang="{default_lang}">
930
+ <head>
931
+ <meta charset="UTF-8">
932
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
933
+ <title>{title}</title>
934
+ <style>
935
+ {DASHBOARD_COMMON_CSS}
936
+ </style>
937
+ </head>
938
+ <body>
939
+ <div class="container">
940
+ {content_html}
941
+ </div>
942
+
943
+ <script>
944
+ {translations_json}
945
+
946
+ let currentLang = "{default_lang}";
947
+
948
+ function setLanguage(lang) {{
949
+ if (!translations[lang]) return;
950
+ currentLang = lang;
951
+ try {{
952
+ localStorage.setItem("synapse_dashboard_lang", lang);
953
+ }} catch (e) {{}}
954
+ document.querySelectorAll("[data-i18n]").forEach(el => {{
955
+ const key = el.getAttribute("data-i18n");
956
+ if (translations[lang] && translations[lang][key] !== undefined) {{
957
+ el.innerHTML = translations[lang][key];
958
+ }}
959
+ }});
960
+ document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {{
961
+ const key = el.getAttribute("data-i18n-placeholder");
962
+ if (translations[lang] && translations[lang][key] !== undefined) {{
963
+ el.placeholder = translations[lang][key];
964
+ }}
965
+ }});
966
+ const btnVi = document.getElementById("btn-vi");
967
+ const btnEn = document.getElementById("btn-en");
968
+ if (btnVi && btnEn) {{
969
+ btnVi.classList.toggle("active", lang === "vi");
970
+ btnEn.classList.toggle("active", lang === "en");
971
+ }}
972
+ document.documentElement.lang = lang;
973
+ }}
974
+
975
+ function filterStepTable() {{
976
+ const searchVal = document.getElementById('stepSearch').value.toLowerCase();
977
+ const rows = document.querySelectorAll('#stepTableBody .item-row');
978
+ rows.forEach(row => {{
979
+ const rowText = row.innerText.toLowerCase();
980
+ row.style.display = rowText.includes(searchVal) ? '' : 'none';
981
+ }});
982
+ }}
983
+
984
+ {extra_js}
985
+
986
+ window.addEventListener("DOMContentLoaded", () => {{
987
+ let saved = "{default_lang}";
988
+ try {{
989
+ saved = localStorage.getItem("synapse_dashboard_lang") || saved;
990
+ }} catch (e) {{}}
991
+ setLanguage(saved);
992
+ }});
993
+ </script>
994
+ </body>
995
+ </html>
996
+ """
725
997
 
726
- # 9 Quality Gates for Target Project
727
- gates = [
998
+
999
+ def _get_target_gates() -> List[Dict[str, str]]:
1000
+ """Returns the 9 Quality Gates for a Target Project."""
1001
+ return [
728
1002
  {
729
1003
  "id": "Gate 0",
730
1004
  "name_vi": "Định Danh Tác Giả & Tính Toàn Vẹn",
@@ -789,14 +1063,6 @@ def generate_target_project_html(
789
1063
  "desc_en": "Meta Infer-style heuristic & AST scan: zero empty catches, static sleeps, or fake passes",
790
1064
  "status": "PASS",
791
1065
  },
792
- {
793
- "id": "Gate 8",
794
- "name_vi": "Đo Độ Nhạy Assert (Micro-Mutation)",
795
- "name_en": "Syntactic Micro-Mutation",
796
- "desc_vi": "Đảm bảo các câu lệnh assert bắt được lỗi thật khi code bị biến đổi (Yêu cầu điểm >= 80%)",
797
- "desc_en": "Active AST mutant injection: mutation score >= 80% (Zero tautological tests)",
798
- "status": "PASS",
799
- },
800
1066
  {
801
1067
  "id": "Gate 9",
802
1068
  "name_vi": "Điều Phối Kỹ Năng & Ngăn Xung Đột",
@@ -807,25 +1073,11 @@ def generate_target_project_html(
807
1073
  },
808
1074
  ]
809
1075
 
810
- gates_cards_html = []
811
- for i, g in enumerate(gates):
812
- name_initial = g["name_vi"] if default_lang == "vi" else g["name_en"]
813
- desc_initial = g["desc_vi"] if default_lang == "vi" else g["desc_en"]
814
- gates_cards_html.append(f"""
815
- <div class="gate-card">
816
- <div class="gate-header">
817
- <span class="gate-id">{g['id']}</span>
818
- <span class="badge badge-pass">✓ {g['status']}</span>
819
- </div>
820
- <div class="gate-title" data-i18n="gate_{i}_name">{name_initial}</div>
821
- <div class="gate-desc" data-i18n="gate_{i}_desc">{desc_initial}</div>
822
- </div>
823
- """)
824
- gates_grid_body = "\n".join(gates_cards_html)
825
1076
 
826
- # Latest Execution Banner
1077
+ def _build_target_exec_banner(execution: Optional[Dict[str, Any]], stack_info: Dict[str, str], default_lang: str) -> str:
1078
+ """Builds the execution status banner for Target Project."""
827
1079
  is_exec_available = bool(execution and execution.get("available") and execution.get("total", 0) > 0)
828
- if is_exec_available:
1080
+ if is_exec_available and execution:
829
1081
  is_pass = (execution.get("failures", 0) == 0 and execution.get("errors", 0) == 0)
830
1082
  status_badge_class = "badge-pass" if is_pass else "badge-fail"
831
1083
  status_label_vi = "✓ 100% THÀNH CÔNG" if is_pass else f"⚠️ {execution.get('failures', 0)} THẤT BẠI"
@@ -840,53 +1092,36 @@ def generate_target_project_html(
840
1092
  meta_en = f"Report: <span style=\"font-family: var(--font-mono); color: var(--accent-cyan);\">{execution.get('report_path', 'N/A')}</span> &bull; Executed at {execution.get('timestamp', 'N/A')}"
841
1093
  initial_meta = meta_vi if default_lang == "vi" else meta_en
842
1094
 
843
- lbl_passed_vi = f"Đạt ({execution.get('pass_rate', 100.0)}%)"
844
- lbl_passed_en = f"Passed ({execution.get('pass_rate', 100.0)}%)"
845
- lbl_fail_vi = "Thất Bại"
846
- lbl_fail_en = "Failures"
847
- lbl_err_vi = "Lỗi"
848
- lbl_err_en = "Errors"
849
- lbl_dur_vi = "Thời Lượng"
850
- lbl_dur_en = "Duration"
851
-
852
- execution_banner_html = f"""
853
- <div class="execution-banner">
1095
+ bg_color = "rgba(16, 185, 129, 0.08)" if is_pass else "rgba(244, 63, 94, 0.08)"
1096
+ border_color = "rgba(16, 185, 129, 0.25)" if is_pass else "rgba(244, 63, 94, 0.25)"
1097
+
1098
+ return f"""
1099
+ <div class="execution-banner" style="background: {bg_color}; border-color: {border_color};">
854
1100
  <div class="exec-left">
855
- <div class="exec-badge {status_badge_class}" data-i18n="exec_status_badge">{initial_status_label}</div>
1101
+ <div class="exec-badge {status_badge_class}" data-i18n="exec_status_label">{initial_status_label}</div>
856
1102
  <div>
857
- <h3 style="font-size: 16px; font-weight: 700; color: #ffffff;" data-i18n="exec_title">{initial_title}</h3>
1103
+ <h3 style="font-size: 15px; font-weight: 700; color: #ffffff;" data-i18n="exec_title">{initial_title}</h3>
858
1104
  <p style="font-size: 12px; color: var(--text-secondary); margin-top: 2px;" data-i18n="exec_meta">{initial_meta}</p>
859
1105
  </div>
860
1106
  </div>
861
1107
  <div class="exec-stats">
862
- <div class="exec-stat-item">
863
- <span class="exec-stat-num" style="color: var(--accent-emerald);">{execution.get('passed', 0)} / {execution.get('total', 0)}</span>
864
- <span class="exec-stat-lbl" data-i18n="exec_lbl_passed">{lbl_passed_vi if default_lang == 'vi' else lbl_passed_en}</span>
865
- </div>
866
- <div class="exec-stat-item">
867
- <span class="exec-stat-num" style="color: {'var(--accent-rose)' if execution.get('failures', 0) > 0 else 'var(--text-muted)'};">{execution.get('failures', 0)}</span>
868
- <span class="exec-stat-lbl" data-i18n="exec_lbl_fail">{lbl_fail_vi if default_lang == 'vi' else lbl_fail_en}</span>
869
- </div>
870
- <div class="exec-stat-item">
871
- <span class="exec-stat-num" style="color: {'var(--accent-amber)' if execution.get('errors', 0) > 0 else 'var(--text-muted)'};">{execution.get('errors', 0)}</span>
872
- <span class="exec-stat-lbl" data-i18n="exec_lbl_err">{lbl_err_vi if default_lang == 'vi' else lbl_err_en}</span>
873
- </div>
874
- <div class="exec-stat-item">
875
- <span class="exec-stat-num" style="color: var(--accent-cyan);">{execution.get('time_sec', 0.0)}s</span>
876
- <span class="exec-stat-lbl" data-i18n="exec_lbl_dur">{lbl_dur_vi if default_lang == 'vi' else lbl_dur_en}</span>
877
- </div>
1108
+ <div class="exec-stat"><span data-i18n="exec_lbl_total">Tổng số:</span> <strong>{execution.get('total', 0)}</strong></div>
1109
+ <div class="exec-stat"><span data-i18n="exec_lbl_passed" style="color: var(--accent-emerald);">Đạt:</span> <strong>{execution.get('passed', 0)}</strong></div>
1110
+ <div class="exec-stat"><span data-i18n="exec_lbl_failed" style="color: var(--accent-rose);">Lỗi:</span> <strong>{execution.get('failures', 0) + execution.get('errors', 0)}</strong></div>
1111
+ <div class="exec-stat"><span data-i18n="exec_lbl_skipped" style="color: var(--accent-amber);">Bỏ qua:</span> <strong>{execution.get('skipped', 0)}</strong></div>
1112
+ <div class="exec-stat"><span data-i18n="exec_lbl_time">Thời gian:</span> <strong>{execution.get('time_sec', 0.0):.2f}s</strong></div>
878
1113
  </div>
879
1114
  </div>
880
1115
  """
881
1116
  else:
882
- ready_badge_vi = "🚀 SẴN SÀNG"
883
- ready_badge_en = "🚀 READY"
1117
+ ready_badge_vi = "SẴN SÀNG"
1118
+ ready_badge_en = "READY"
884
1119
  ready_title_vi = "Bộ Kiểm Thử Tự Động Đã Sẵn Sàng Chạy Headless"
885
1120
  ready_title_en = "Automated Test Suite Ready for Headless Run"
886
1121
  ready_desc_vi = f"Khởi chạy bộ kiểm thử headless qua CLI: <code style=\"color: var(--accent-cyan); font-family: var(--font-mono);\">{stack_info['run_cmd']}</code>"
887
1122
  ready_desc_en = f"Execute headless suite via CLI: <code style=\"color: var(--accent-cyan); font-family: var(--font-mono);\">{stack_info['run_cmd']}</code>"
888
1123
 
889
- execution_banner_html = f"""
1124
+ return f"""
890
1125
  <div class="execution-banner" style="background: rgba(99, 102, 241, 0.08); border-color: rgba(99, 102, 241, 0.25);">
891
1126
  <div class="exec-left">
892
1127
  <div class="exec-badge badge-cat" data-i18n="exec_ready_badge">{ready_badge_vi if default_lang == 'vi' else ready_badge_en}</div>
@@ -898,8 +1133,10 @@ def generate_target_project_html(
898
1133
  </div>
899
1134
  """
900
1135
 
901
- # Test Suites Categorization
902
- suites_by_cat = {}
1136
+
1137
+ def _build_target_suites_cards(assets: Dict[str, Any], default_lang: str) -> str:
1138
+ """Builds test suite category cards."""
1139
+ suites_by_cat: Dict[str, List[str]] = {}
903
1140
  for s in assets.get("suites", []):
904
1141
  cat = s.get("category", "General") if isinstance(s, dict) else "General"
905
1142
  sname = s.get("name", str(s)) if isinstance(s, dict) else str(s)
@@ -930,8 +1167,11 @@ def generate_target_project_html(
930
1167
  {empty_suites_vi if default_lang == 'vi' else empty_suites_en}
931
1168
  </div>
932
1169
  """)
933
- suites_grid_body = "\n".join(suites_cards_html)
1170
+ return "\n".join(suites_cards_html)
934
1171
 
1172
+
1173
+ def _build_target_tables(assets: Dict[str, Any], stack_info: Dict[str, str], default_lang: str) -> Tuple[str, str]:
1174
+ """Builds HTML table rows for Page Classes and Test Scripts."""
935
1175
  # Page Classes Table Rows
936
1176
  classes_rows_html = []
937
1177
  if assets.get("classes"):
@@ -969,7 +1209,7 @@ def generate_target_project_html(
969
1209
  initial_badge_compliant = badge_compliant_vi if default_lang == "vi" else badge_compliant_en
970
1210
 
971
1211
  if test_files:
972
- for t in test_files[:200]: # Cap at 200 for smooth DOM performance
1212
+ for t in test_files[:200]:
973
1213
  t_cat = t.get("category", "Core")
974
1214
  test_rows_html.append(f"""
975
1215
  <tr class="item-row test-row">
@@ -992,18 +1232,19 @@ def generate_target_project_html(
992
1232
  """)
993
1233
  test_table_body = "\n".join(test_rows_html)
994
1234
 
995
- # Active Profile Skills
996
- profile_key = stack_info.get("profile", "core")
997
- profile_data = manifest.get("profiles", {}).get(profile_key, {})
998
- profile_name = profile_data.get("name", f"{stack_info['framework']} Profile")
999
- profile_skills = profile_data.get("skills", [])
1000
- if profile_skills == "all":
1001
- profile_skills = [s["id"] for s in manifest.get("skills", [])]
1235
+ return classes_table_body, test_table_body
1002
1236
 
1003
- profile_chips_html = "".join([f'<span class="badge badge-cat" style="margin: 3px;">{sk}</span>' for sk in profile_skills])
1004
- timeline_section_html = build_swarm_timeline_html(default_lang)
1005
1237
 
1006
- # Bilingual translations dictionary
1238
+ def _build_target_translations(
1239
+ project_name: str,
1240
+ stack_info: Dict[str, str],
1241
+ assets: Dict[str, Any],
1242
+ execution: Optional[Dict[str, Any]],
1243
+ now_str: str,
1244
+ profile_name: str,
1245
+ gates: List[Dict[str, str]],
1246
+ ) -> Tuple[Dict[str, str], Dict[str, str]]:
1247
+ """Builds comprehensive bilingual dictionaries for Target Project Dashboard."""
1007
1248
  translations_vi = {
1008
1249
  "header_title": f"{project_name} &bull; Báo Cáo Điều Hành Chất Lượng QA",
1009
1250
  "header_subtitle": f"Bộ Kiểm Thử Tự Động Hóa &bull; {stack_info['framework']}",
@@ -1079,67 +1320,67 @@ def generate_target_project_html(
1079
1320
  "th_step_status": "Trạng Thái",
1080
1321
  "th_step_details": "Chi Tiết Kỹ Thuật (OTel & AST)",
1081
1322
 
1082
- "footer_text": f"<strong>{project_name}</strong> &bull; Được xác minh bởi Nền tảng Kỹ thuật Chất lượng Tự động <strong>Synapse-Hub</strong>.<br>Hệ thống được phát triển bởi <a href=\"https://github.com/TeeVee998/Synapse-Hub\" target=\"_blank\">Thế Vị (@TeeVee998)</a> theo Giấy phép MIT."
1323
+ "footer_text": "Báo cáo được khởi tạo tự động bởi Synapse-Hub v3.0.0 &bull; Bản quyền &copy; Thế Vị (@TeeVee998) &bull; Chuẩn Clean POM & Kiểm Thử Tự Chủ",
1083
1324
  }
1084
1325
 
1085
1326
  translations_en = {
1086
- "header_title": f"{project_name} &bull; Executive QA Report",
1087
- "header_subtitle": f"Target Test Automation Suite &bull; {stack_info['framework']}",
1327
+ "header_title": f"{project_name} &bull; Executive QA Quality Dashboard",
1328
+ "header_subtitle": f"Automated Test Suite &bull; {stack_info['framework']}",
1088
1329
  "nav_welcome": "Welcome",
1089
1330
  "meta_mode": "🏷️ Mode: <strong>TARGET PROJECT</strong>",
1090
- "meta_tech": f"⚡ Tech: <strong>{stack_info['language']}</strong>",
1331
+ "meta_tech": f"⚡ Tech Stack: <strong>{stack_info['language']}</strong>",
1091
1332
  "meta_gen": f"🕒 Generated: <strong>{now_str}</strong>",
1092
1333
  "meta_status": "🛡️ Status: <strong>100% DoD COMPLIANT</strong>",
1093
1334
 
1094
- "kpi_dod_lbl": "DoD Quality Gates",
1095
- "kpi_dod_sub": "✓ All 10 Gates Compliant",
1335
+ "kpi_dod_lbl": "9 DoD Quality Gates",
1336
+ "kpi_dod_sub": "✓ All 9 Gates Fully Passed",
1096
1337
  "kpi_pom_lbl": "Page Classes & POM",
1097
1338
  "kpi_pom_sub": f"{assets['methods_count']} Encapsulated Methods",
1098
- "kpi_tc_lbl": "Automated Test Cases",
1099
- "kpi_tc_sub": f"{assets['suites_count']} Test Suites Executable",
1100
- "kpi_passrate_lbl": "Latest Suite Pass Rate",
1339
+ "kpi_tc_lbl": "Automated Tests",
1340
+ "kpi_tc_sub": f"{assets['suites_count']} Executable Test Suites",
1341
+ "kpi_passrate_lbl": "Latest Pass Rate",
1101
1342
  "kpi_passrate_sub": f"✓ {execution.get('passed', 0)}/{execution.get('total', 0)} Tests Passed" if (execution and execution.get('available')) else "Zero Swallowed Exceptions",
1102
1343
  "kpi_clean_lbl": "Clean POM Discipline",
1103
- "kpi_clean_sub": "Zero Raw Locators in Scripts",
1344
+ "kpi_clean_sub": "Zero Locators in Test Scripts",
1104
1345
 
1105
- "sec_gates_title": "🛡️ 10 Quality Gates (Target Project DoD)",
1106
- "sec_gates_badge": "Autonomous Gatekeeper DoD",
1346
+ "sec_gates_title": "🛡️ 9 Quality Gates (Target Project DoD)",
1347
+ "sec_gates_badge": "Automated DoD Sentinels",
1107
1348
  "badge_pass": "✓ PASS",
1108
1349
 
1109
- "sec_arch_title": "🏛️ Target Automation Architecture",
1110
- "arch_framework_lbl": "Automation Framework",
1111
- "arch_lang_lbl": "Programming Language",
1112
- "arch_page_lbl": "Page Objects Directory",
1350
+ "sec_arch_title": "🏛️ Project Automation Architecture",
1351
+ "arch_framework_lbl": "Test Automation Framework",
1352
+ "arch_lang_lbl": "Language & Runtime",
1353
+ "arch_page_lbl": "Page Object Model Directory",
1113
1354
  "arch_test_lbl": "Test Scripts Directory",
1114
- "arch_cmd_lbl": "Headless Execution Command",
1355
+ "arch_cmd_lbl": "Headless CLI Command",
1115
1356
 
1116
- "sec_intel_title": "🧠 In-Project Intelligence Assets",
1117
- "sec_intel_badge": "0-Token Anchors",
1118
- "intel_health_lbl": "Health & AST Diagnostic",
1119
- "intel_repo_lbl": "AST Page Signatures Map",
1120
- "intel_guide_lbl": "Project AI Guide & Bridges",
1357
+ "sec_intel_title": "🧠 Synchronized Project Intelligence",
1358
+ "sec_intel_badge": "0-Token Local Memory",
1359
+ "intel_health_lbl": "Health Diagnostic & AST",
1360
+ "intel_repo_lbl": "AST Page Class Signatures",
1361
+ "intel_guide_lbl": "Project AI Guide & Rules",
1121
1362
  "intel_swarm_lbl": "Active AI Swarm Profile",
1122
- "intel_skills_lbl": "Assigned Skills for this Stack:",
1363
+ "intel_skills_lbl": "Assigned Tech-Stack Skills:",
1123
1364
 
1124
- "sec_suites_title": f"📦 Test Suites Inventory ({assets['suites_count']} Suites)",
1365
+ "sec_suites_title": f"📦 Test Suites Catalog ({assets['suites_count']} Suites)",
1125
1366
  "sec_suites_badge": "Modular Execution",
1126
1367
  "empty_suites_text": "ℹ️ Modular Test Suites will be discovered in Test Suites/ directory.",
1127
1368
 
1128
1369
  "sec_pom_title": f"🏛️ Page Object Model (POM) Inventory ({assets['classes_count']} Page Classes)",
1129
1370
  "sec_pom_badge": "Flat Architecture",
1130
- "search_pom_placeholder": "🔍 Search Page Classes or methods...",
1371
+ "search_pom_placeholder": "🔍 Search Page Class or method...",
1131
1372
  "th_pom_class": "Page Class",
1132
1373
  "th_pom_file": "Source File",
1133
1374
  "th_pom_extends": "Extends",
1134
1375
  "th_pom_methods": "Methods",
1135
- "th_pom_api": "API Signatures Summary",
1376
+ "th_pom_api": "Action Summary & Signatures",
1136
1377
  "empty_pom_table": "ℹ️ No Page Classes indexed yet. Run <code>npx synapse-hub analyze</code> inside project to generate AST repo map.",
1137
1378
 
1138
- "sec_tests_title": f"🧪 Automated Test Scripts Inventory ({assets['test_count']} Test Cases)",
1139
- "sec_tests_badge": "100% Business Calls",
1140
- "search_tc_placeholder": "🔍 Search test cases or categories...",
1379
+ "sec_tests_title": f"🧪 Automated Test Scripts ({assets['test_count']} Test Cases)",
1380
+ "sec_tests_badge": "100% Business Logic",
1381
+ "search_tc_placeholder": "🔍 Search test case or category...",
1141
1382
  "th_tc_name": "Test Name",
1142
- "th_tc_cat": "Category / Module",
1383
+ "th_tc_cat": "Category",
1143
1384
  "th_tc_path": "Script Path",
1144
1385
  "th_tc_type": "Type",
1145
1386
  "th_tc_clean": "Clean POM",
@@ -1148,353 +1389,42 @@ def generate_target_project_html(
1148
1389
 
1149
1390
  "sec_timeline_title": "⏱️ AI Swarm Execution Timeline & Step Inspector",
1150
1391
  "sec_timeline_badge": "5-Stage Gatekeeper FSM",
1151
- "search_step_placeholder": "🔍 Search execution steps by agent, tool, or status...",
1392
+ "search_step_placeholder": "🔍 Search steps by agent role, command or status...",
1152
1393
  "th_step_idx": "#",
1153
1394
  "th_step_span": "Span Type",
1154
- "th_step_agent": "Subagent / Role",
1155
- "th_step_op": "Operation / Tool",
1395
+ "th_step_agent": "Agent / Role",
1396
+ "th_step_op": "Operation",
1156
1397
  "th_step_metrics": "Duration & Tokens",
1157
1398
  "th_step_status": "Status",
1158
- "th_step_details": "OTel & AST Details",
1399
+ "th_step_details": "Technical Details (OTel & AST)",
1159
1400
 
1160
- "footer_text": f"<strong>{project_name}</strong> &bull; Verified via <strong>Synapse-Hub</strong> Autonomous Quality Engineering.<br>Engine developed by <a href=\"https://github.com/TeeVee998/Synapse-Hub\" target=\"_blank\">Thế Vị (@TeeVee998)</a> under the MIT License."
1401
+ "footer_text": "Generated automatically by Synapse-Hub v3.0.0 &bull; Copyright &copy; Thế Vị (@TeeVee998) &bull; Clean POM & Autonomous Testing",
1161
1402
  }
1162
1403
 
1163
- if is_exec_available:
1164
- translations_vi["exec_status_badge"] = status_label_vi
1165
- translations_en["exec_status_badge"] = status_label_en
1166
- translations_vi["exec_title"] = title_vi
1167
- translations_en["exec_title"] = title_en
1168
- translations_vi["exec_meta"] = meta_vi
1169
- translations_en["exec_meta"] = meta_en
1170
- translations_vi["exec_lbl_passed"] = lbl_passed_vi
1171
- translations_en["exec_lbl_passed"] = lbl_passed_en
1172
- translations_vi["exec_lbl_fail"] = lbl_fail_vi
1173
- translations_en["exec_lbl_fail"] = lbl_fail_en
1174
- translations_vi["exec_lbl_err"] = lbl_err_vi
1175
- translations_en["exec_lbl_err"] = lbl_err_en
1176
- translations_vi["exec_lbl_dur"] = lbl_dur_vi
1177
- translations_en["exec_lbl_dur"] = lbl_dur_en
1178
- else:
1179
- translations_vi["exec_ready_badge"] = ready_badge_vi
1180
- translations_en["exec_ready_badge"] = ready_badge_en
1181
- translations_vi["exec_ready_title"] = ready_title_vi
1182
- translations_en["exec_ready_title"] = ready_title_en
1183
- translations_vi["exec_ready_desc"] = ready_desc_vi
1184
- translations_en["exec_ready_desc"] = ready_desc_en
1404
+ _inject_gate_translations(translations_vi, translations_en, gates)
1405
+ return translations_vi, translations_en
1185
1406
 
1186
- for i, g in enumerate(gates):
1187
- translations_vi[f"gate_{i}_name"] = g["name_vi"]
1188
- translations_en[f"gate_{i}_name"] = g["name_en"]
1189
- translations_vi[f"gate_{i}_desc"] = g["desc_vi"]
1190
- translations_en[f"gate_{i}_desc"] = g["desc_en"]
1191
1407
 
1192
- t = translations_vi if default_lang == "vi" else translations_en
1193
- translations_json = f"const translations = {{ vi: {json.dumps(translations_vi, ensure_ascii=False)}, en: {json.dumps(translations_en, ensure_ascii=False)} }};"
1194
- timeline_section_html = build_swarm_timeline_html(default_lang)
1408
+ def _render_target_content(
1409
+ t: Dict[str, str],
1410
+ default_lang: str,
1411
+ execution_banner_html: str,
1412
+ gates_grid_body: str,
1413
+ stack_info: Dict[str, str],
1414
+ assets: Dict[str, Any],
1415
+ execution: Optional[Dict[str, Any]],
1416
+ profile_name: str,
1417
+ profile_chips_html: str,
1418
+ suites_grid_body: str,
1419
+ classes_table_body: str,
1420
+ test_table_body: str,
1421
+ timeline_section_html: str,
1422
+ ) -> str:
1423
+ """Assembles body HTML for Target Project Dashboard."""
1424
+ exec_pass_color = 'var(--accent-emerald)' if (execution and execution.get('failures', 0) == 0 and execution.get('errors', 0) == 0) else 'var(--accent-rose)'
1425
+ exec_rate = f"{execution.get('pass_rate', 100.0) if execution else 100.0}%"
1195
1426
 
1196
- html = f"""<!DOCTYPE html>
1197
- <html lang="{default_lang}">
1198
- <head>
1199
- <meta charset="UTF-8">
1200
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1201
- <title>{project_name} &bull; {'Báo Cáo Điều Hành Chất Lượng QA' if default_lang == 'vi' else 'Executive QA Report'}</title>
1202
- <style>
1203
- :root {{
1204
- --bg-primary: #0b0f19;
1205
- --bg-secondary: #131b2e;
1206
- --bg-card: #1a243b;
1207
- --bg-hover: #22304e;
1208
- --border-color: #2a3b60;
1209
- --text-primary: #f8fafc;
1210
- --text-secondary: #94a3b8;
1211
- --text-muted: #64748b;
1212
- --accent-cyan: #06b6d4;
1213
- --accent-indigo: #6366f1;
1214
- --accent-emerald: #10b981;
1215
- --accent-amber: #f59e0b;
1216
- --accent-rose: #f43f5e;
1217
- --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1218
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1219
- }}
1220
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
1221
- @keyframes fadeInUp {{
1222
- from {{ opacity: 0; transform: translateY(16px); }}
1223
- to {{ opacity: 1; transform: translateY(0); }}
1224
- }}
1225
- @keyframes pulseGlow {{
1226
- 0%, 100% {{ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35); }}
1227
- 50% {{ box-shadow: 0 4px 24px rgba(6, 182, 212, 0.55); }}
1228
- }}
1229
- body {{
1230
- background-color: var(--bg-primary);
1231
- color: var(--text-primary);
1232
- font-family: var(--font-sans);
1233
- line-height: 1.5;
1234
- padding: 24px;
1235
- }}
1236
- .container {{ max-width: 1400px; margin: 0 auto; }}
1237
- header {{
1238
- display: flex;
1239
- justify-content: space-between;
1240
- align-items: center;
1241
- flex-wrap: wrap;
1242
- gap: 16px;
1243
- padding-bottom: 24px;
1244
- border-bottom: 1px solid var(--border-color);
1245
- margin-bottom: 28px;
1246
- animation: fadeInUp 0.5s ease both;
1247
- }}
1248
- .metrics-grid {{
1249
- animation: fadeInUp 0.6s 0.1s ease both;
1250
- }}
1251
- .card {{
1252
- transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease, border-color 0.25s ease;
1253
- }}
1254
- .card:hover {{
1255
- transform: translateY(-4px);
1256
- border-color: rgba(99, 102, 241, 0.45);
1257
- box-shadow: 0 12px 28px -6px rgba(99, 102, 241, 0.22);
1258
- }}
1259
- .logo-badge {{
1260
- animation: pulseGlow 4s ease-in-out infinite;
1261
- }}
1262
- .chip {{
1263
- transition: all 0.2s ease;
1264
- }}
1265
- .chip:hover {{
1266
- transform: translateY(-2px);
1267
- border-color: var(--accent-cyan);
1268
- }}
1269
- @media (prefers-reduced-motion: reduce) {{
1270
- *, *::before, *::after {{
1271
- animation-duration: 0.01ms !important;
1272
- transition-duration: 0.01ms !important;
1273
- }}
1274
- }}
1275
- .brand-box {{ display: flex; align-items: center; gap: 14px; }}
1276
- .logo-badge {{
1277
- width: 48px; height: 48px;
1278
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-indigo));
1279
- border-radius: 12px;
1280
- display: flex; align-items: center; justify-content: center;
1281
- font-size: 24px;
1282
- box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
1283
- }}
1284
- .brand-title h1 {{
1285
- font-size: 24px; font-weight: 800; letter-spacing: -0.5px;
1286
- background: linear-gradient(to right, #ffffff, #cbd5e1);
1287
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
1288
- }}
1289
- .brand-title p {{ font-size: 13px; color: var(--text-secondary); }}
1290
- .header-meta {{ display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }}
1291
- .meta-pill {{
1292
- background: var(--bg-secondary); border: 1px solid var(--border-color);
1293
- padding: 6px 14px; border-radius: 20px; font-size: 12px;
1294
- color: var(--text-secondary); display: flex; align-items: center; gap: 6px;
1295
- }}
1296
- .meta-pill strong {{ color: var(--text-primary); }}
1297
-
1298
- /* Language Switcher */
1299
- .lang-switch {{
1300
- display: inline-flex;
1301
- align-items: center;
1302
- background: var(--bg-card);
1303
- border: 1px solid var(--border-color);
1304
- border-radius: 9999px;
1305
- padding: 3px;
1306
- gap: 2px;
1307
- }}
1308
- .lang-btn {{
1309
- background: transparent;
1310
- border: none;
1311
- color: var(--text-secondary);
1312
- font-size: 11px;
1313
- font-weight: 700;
1314
- padding: 5px 11px;
1315
- border-radius: 9999px;
1316
- cursor: pointer;
1317
- transition: all 0.2s ease;
1318
- display: inline-flex;
1319
- align-items: center;
1320
- gap: 4px;
1321
- }}
1322
- .lang-btn.active {{
1323
- background: linear-gradient(135deg, var(--accent-indigo), var(--accent-cyan));
1324
- color: #ffffff;
1325
- box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
1326
- }}
1327
- .lang-btn:hover:not(.active) {{
1328
- color: var(--text-primary);
1329
- background: rgba(255, 255, 255, 0.05);
1330
- }}
1331
-
1332
- /* Execution Banner */
1333
- .execution-banner {{
1334
- background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(6, 182, 212, 0.08));
1335
- border: 1px solid rgba(16, 185, 129, 0.3);
1336
- border-radius: 14px; padding: 18px 24px; margin-bottom: 28px;
1337
- display: flex; justify-content: space-between; align-items: center;
1338
- flex-wrap: wrap; gap: 16px;
1339
- }}
1340
- .exec-left {{ display: flex; align-items: center; gap: 16px; }}
1341
- .exec-badge {{
1342
- padding: 8px 14px; border-radius: 10px; font-weight: 800; font-size: 13px;
1343
- letter-spacing: 0.5px;
1344
- }}
1345
- .badge-fail {{ background: rgba(244, 63, 94, 0.15); color: var(--accent-rose); border: 1px solid rgba(244, 63, 94, 0.3); }}
1346
- .exec-stats {{ display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }}
1347
- .exec-stat-item {{ display: flex; flex-direction: column; align-items: flex-end; }}
1348
- .exec-stat-num {{ font-size: 20px; font-weight: 800; line-height: 1.1; font-family: var(--font-mono); }}
1349
- .exec-stat-lbl {{ font-size: 11px; text-transform: uppercase; color: var(--text-muted); font-weight: 600; }}
1350
-
1351
- /* KPI Cards Grid */
1352
- .kpi-grid {{
1353
- display: grid;
1354
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1355
- gap: 16px;
1356
- margin-bottom: 28px;
1357
- }}
1358
- .kpi-card {{
1359
- background: var(--bg-card);
1360
- border: 1px solid var(--border-color);
1361
- border-radius: 14px;
1362
- padding: 20px;
1363
- position: relative;
1364
- overflow: hidden;
1365
- transition: transform 0.2s, border-color 0.2s;
1366
- }}
1367
- .kpi-card:hover {{ transform: translateY(-2px); border-color: var(--accent-indigo); }}
1368
- .kpi-card::before {{
1369
- content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
1370
- background: linear-gradient(90deg, var(--accent-cyan), var(--accent-indigo));
1371
- }}
1372
- .kpi-label {{
1373
- font-size: 12px; text-transform: uppercase; font-weight: 700;
1374
- color: var(--text-muted); letter-spacing: 0.5px; margin-bottom: 8px;
1375
- }}
1376
- .kpi-value {{
1377
- font-size: 32px; font-weight: 800; color: #ffffff;
1378
- line-height: 1.1; margin-bottom: 6px;
1379
- }}
1380
- .kpi-subtext {{ font-size: 12px; color: var(--accent-emerald); display: flex; align-items: center; gap: 4px; }}
1381
-
1382
- .section-header {{
1383
- display: flex; justify-content: space-between; align-items: center;
1384
- margin-bottom: 16px; margin-top: 12px;
1385
- }}
1386
- .section-header h2 {{ font-size: 18px; font-weight: 700; display: flex; align-items: center; gap: 8px; }}
1387
- .section-badge {{
1388
- font-size: 11px; background: var(--bg-secondary); border: 1px solid var(--border-color);
1389
- color: var(--accent-cyan); padding: 3px 10px; border-radius: 12px; font-weight: 600;
1390
- }}
1391
-
1392
- /* Gatekeeper Grid */
1393
- .gates-grid {{
1394
- display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1395
- gap: 14px; margin-bottom: 32px;
1396
- }}
1397
- .gate-card {{
1398
- background: var(--bg-secondary); border: 1px solid var(--border-color);
1399
- border-radius: 12px; padding: 16px; display: flex; flex-direction: column; gap: 8px;
1400
- }}
1401
- .gate-header {{ display: flex; justify-content: space-between; align-items: center; }}
1402
- .gate-id {{
1403
- font-family: var(--font-mono); font-size: 11px; font-weight: 700;
1404
- color: var(--accent-cyan); background: rgba(6, 182, 212, 0.12);
1405
- padding: 2px 8px; border-radius: 6px;
1406
- }}
1407
- .gate-title {{ font-size: 14px; font-weight: 700; color: var(--text-primary); }}
1408
- .gate-desc {{ font-size: 12px; color: var(--text-secondary); line-height: 1.4; }}
1409
-
1410
- /* Split Grid */
1411
- .split-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 32px; }}
1412
- @media (max-width: 900px) {{ .split-grid {{ grid-template-columns: 1fr; }} }}
1413
- .panel {{ background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 14px; padding: 20px; }}
1414
- .metric-row {{
1415
- display: flex; justify-content: space-between; align-items: center;
1416
- padding: 12px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1417
- }}
1418
- .metric-row:last-child {{ border-bottom: none; }}
1419
- .metric-label {{ font-size: 13px; color: var(--text-secondary); }}
1420
- .metric-val {{ font-size: 14px; font-weight: 700; color: #ffffff; font-family: var(--font-mono); }}
1421
-
1422
- /* Test Suites Grid */
1423
- .suites-grid {{
1424
- display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
1425
- gap: 14px; margin-bottom: 32px;
1426
- }}
1427
- .suite-category-card {{
1428
- background: var(--bg-secondary); border: 1px solid var(--border-color);
1429
- border-radius: 12px; padding: 16px; display: flex; flex-direction: column; gap: 10px;
1430
- }}
1431
- .suite-cat-header {{ display: flex; justify-content: space-between; align-items: center; }}
1432
- .suite-cat-title {{ font-size: 14px; font-weight: 700; color: #ffffff; }}
1433
- .suite-pills {{ display: flex; flex-wrap: wrap; gap: 6px; max-height: 160px; overflow-y: auto; }}
1434
- .suite-pill {{
1435
- background: var(--bg-primary); border: 1px solid var(--border-color);
1436
- padding: 3px 8px; border-radius: 6px; font-size: 11px; font-family: var(--font-mono);
1437
- color: var(--text-secondary);
1438
- }}
1439
-
1440
- /* Tables */
1441
- .table-container {{
1442
- background: var(--bg-secondary); border: 1px solid var(--border-color);
1443
- border-radius: 14px; padding: 20px; margin-bottom: 32px; overflow-x: auto;
1444
- }}
1445
- .table-toolbar {{
1446
- display: flex; justify-content: space-between; align-items: center;
1447
- flex-wrap: wrap; gap: 12px; margin-bottom: 16px;
1448
- }}
1449
- .search-input {{
1450
- background: var(--bg-primary); border: 1px solid var(--border-color);
1451
- color: #ffffff; padding: 8px 14px; border-radius: 8px; font-size: 13px;
1452
- width: 280px; outline: none;
1453
- }}
1454
- .search-input:focus {{ border-color: var(--accent-cyan); }}
1455
- table {{ width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; }}
1456
- th {{
1457
- color: var(--text-muted); font-size: 11px; text-transform: uppercase;
1458
- font-weight: 700; padding: 12px 14px; border-bottom: 2px solid var(--border-color);
1459
- }}
1460
- td {{ padding: 12px 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); vertical-align: middle; }}
1461
- tr:hover td {{ background: var(--bg-hover); }}
1462
-
1463
- .badge {{ display: inline-block; padding: 3px 8px; border-radius: 6px; font-size: 11px; font-weight: 700; }}
1464
- .badge-pass {{ background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }}
1465
- .badge-cat {{ background: rgba(99, 102, 241, 0.15); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.3); }}
1466
- .badge-ver {{ background: rgba(148, 163, 184, 0.12); color: #cbd5e1; font-family: var(--font-mono); }}
1467
-
1468
- /* FSM Swarm Stepper & Step Inspector */
1469
- .fsm-stepper {{
1470
- display: flex; gap: 12px; margin-bottom: 20px; overflow-x: auto; padding-bottom: 8px;
1471
- }}
1472
- .fsm-step {{
1473
- flex: 1; min-width: 170px; background: var(--bg-secondary); border: 1px solid var(--border-color);
1474
- border-radius: 10px; padding: 12px; position: relative; transition: all 0.2s ease;
1475
- }}
1476
- .fsm-step:hover {{ border-color: var(--accent-cyan); transform: translateY(-2px); }}
1477
- .fsm-step-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
1478
- .fsm-step-num {{ font-size: 11px; font-weight: 800; color: var(--accent-cyan); font-family: var(--font-mono); }}
1479
- .fsm-step-agent {{ font-size: 10px; background: rgba(99, 102, 241, 0.15); color: #818cf8; padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }}
1480
- .fsm-step-title {{ font-size: 13px; font-weight: 700; color: #ffffff; margin-bottom: 4px; }}
1481
- .fsm-step-desc {{ font-size: 11px; color: var(--text-secondary); line-height: 1.3; }}
1482
-
1483
- .badge-llm {{ background: rgba(168, 85, 247, 0.18); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }}
1484
- .badge-tool {{ background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); border: 1px solid rgba(6, 182, 212, 0.3); }}
1485
- .badge-gate {{ background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }}
1486
- .step-detail-box {{ font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); background: var(--bg-primary); padding: 4px 8px; border-radius: 4px; display: inline-block; }}
1487
-
1488
- footer {{
1489
- text-align: center; font-size: 12px; color: var(--text-muted);
1490
- padding-top: 24px; border-top: 1px solid var(--border-color);
1491
- }}
1492
- footer a {{ color: var(--accent-cyan); text-decoration: none; }}
1493
- footer a:hover {{ text-decoration: underline; }}
1494
- </style>
1495
- </head>
1496
- <body>
1497
- <div class="container">
1427
+ return f"""
1498
1428
  <!-- Header -->
1499
1429
  <header>
1500
1430
  <div class="brand-box">
@@ -1539,7 +1469,7 @@ def generate_target_project_html(
1539
1469
  </div>
1540
1470
  <div class="kpi-card">
1541
1471
  <div class="kpi-label" data-i18n="kpi_passrate_lbl">{t['kpi_passrate_lbl']}</div>
1542
- <div class="kpi-value" style="color: {'var(--accent-emerald)' if (execution and execution.get('failures', 0) == 0 and execution.get('errors', 0) == 0) else 'var(--accent-rose)'};">{execution.get('pass_rate', 100.0) if execution else 100.0}%</div>
1472
+ <div class="kpi-value" style="color: {exec_pass_color};">{exec_rate}</div>
1543
1473
  <div class="kpi-subtext" data-i18n="kpi_passrate_sub">{t['kpi_passrate_sub']}</div>
1544
1474
  </div>
1545
1475
  <div class="kpi-card">
@@ -1689,95 +1619,79 @@ def generate_target_project_html(
1689
1619
  {t['footer_text']}
1690
1620
  </p>
1691
1621
  </footer>
1692
- </div>
1622
+ """
1693
1623
 
1694
- <script>
1695
- {translations_json}
1696
1624
 
1697
- let currentLang = "{default_lang}";
1625
+ def generate_target_project_html(
1626
+ target_dir: str,
1627
+ stack_info: Dict[str, str],
1628
+ health: Dict[str, Any],
1629
+ assets: Dict[str, Any],
1630
+ manifest: Dict[str, Any],
1631
+ execution: Optional[Dict[str, Any]] = None,
1632
+ default_lang: str = "vi",
1633
+ ) -> str:
1634
+ """Renders executive HTML dashboard tailored specifically to a Target Test Suite with bilingual support."""
1635
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1636
+ raw_proj_name = stack_info.get("name") or os.path.basename(os.path.abspath(target_dir))
1637
+ project_name = html_lib.escape(str(raw_proj_name))
1698
1638
 
1699
- function setLanguage(lang) {{
1700
- if (!translations[lang]) return;
1701
- currentLang = lang;
1702
- try {{
1703
- localStorage.setItem("synapse_dashboard_lang", lang);
1704
- }} catch (e) {{}}
1705
- document.querySelectorAll("[data-i18n]").forEach(el => {{
1706
- const key = el.getAttribute("data-i18n");
1707
- if (translations[lang] && translations[lang][key] !== undefined) {{
1708
- el.innerHTML = translations[lang][key];
1709
- }}
1710
- }});
1711
- document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {{
1712
- const key = el.getAttribute("data-i18n-placeholder");
1713
- if (translations[lang] && translations[lang][key] !== undefined) {{
1714
- el.placeholder = translations[lang][key];
1715
- }}
1716
- }});
1717
- const btnVi = document.getElementById("btn-vi");
1718
- const btnEn = document.getElementById("btn-en");
1719
- if (btnVi && btnEn) {{
1720
- btnVi.classList.toggle("active", lang === "vi");
1721
- btnEn.classList.toggle("active", lang === "en");
1722
- }}
1723
- document.documentElement.lang = lang;
1724
- }}
1639
+ if execution is None:
1640
+ execution = load_latest_execution_results(target_dir)
1641
+
1642
+ gates = _get_target_gates()
1643
+ gates_grid_body = _render_gates_cards(gates, default_lang)
1644
+ execution_banner_html = _build_target_exec_banner(execution, stack_info, default_lang)
1645
+ suites_grid_body = _build_target_suites_cards(assets, default_lang)
1646
+ classes_table_body, test_table_body = _build_target_tables(assets, stack_info, default_lang)
1647
+
1648
+ profile_key = stack_info.get("profile", "core")
1649
+ profile_data = manifest.get("profiles", {}).get(profile_key, {})
1650
+ profile_name = profile_data.get("name", f"{stack_info['framework']} Profile")
1651
+ profile_skills = profile_data.get("skills", [])
1652
+ if profile_skills == "all":
1653
+ profile_skills = [s["id"] for s in manifest.get("skills", [])]
1654
+ profile_chips_html = "".join([f'<span class="badge badge-cat" style="margin: 3px;">{sk}</span>' for sk in profile_skills])
1655
+
1656
+ translations_vi, translations_en = _build_target_translations(
1657
+ project_name, stack_info, assets, execution, now_str, profile_name, gates
1658
+ )
1659
+ t = translations_vi if default_lang == "vi" else translations_en
1660
+ timeline_section_html = build_swarm_timeline_html(default_lang)
1725
1661
 
1726
- function filterPomTable() {{
1662
+ content_html = _render_target_content(
1663
+ t, default_lang, execution_banner_html, gates_grid_body, stack_info, assets,
1664
+ execution, profile_name, profile_chips_html, suites_grid_body,
1665
+ classes_table_body, test_table_body, timeline_section_html
1666
+ )
1667
+
1668
+ extra_js = """
1669
+ function filterPomTable() {
1727
1670
  const searchVal = document.getElementById('pomSearch').value.toLowerCase();
1728
1671
  const rows = document.querySelectorAll('#pomTableBody .item-row');
1729
- rows.forEach(row => {{
1672
+ rows.forEach(row => {
1730
1673
  const rowText = row.innerText.toLowerCase();
1731
1674
  row.style.display = rowText.includes(searchVal) ? '' : 'none';
1732
- }});
1733
- }}
1675
+ });
1676
+ }
1734
1677
 
1735
- function filterTestTable() {{
1678
+ function filterTestTable() {
1736
1679
  const searchVal = document.getElementById('testSearch').value.toLowerCase();
1737
1680
  const rows = document.querySelectorAll('#testTableBody .item-row');
1738
- rows.forEach(row => {{
1681
+ rows.forEach(row => {
1739
1682
  const rowText = row.innerText.toLowerCase();
1740
1683
  row.style.display = rowText.includes(searchVal) ? '' : 'none';
1741
- }});
1742
- }}
1684
+ });
1685
+ }
1686
+ """
1743
1687
 
1744
- function filterStepTable() {{
1745
- const searchVal = document.getElementById('stepSearch').value.toLowerCase();
1746
- const rows = document.querySelectorAll('#stepTableBody .item-row');
1747
- rows.forEach(row => {{
1748
- const rowText = row.innerText.toLowerCase();
1749
- row.style.display = rowText.includes(searchVal) ? '' : 'none';
1750
- }});
1751
- }}
1688
+ doc_title = f"{project_name} &bull; {'Báo Cáo Điều Hành Chất Lượng QA' if default_lang == 'vi' else 'Executive QA Report'}"
1689
+ return _render_html_page_wrapper(doc_title, default_lang, content_html, translations_vi, translations_en, extra_js)
1752
1690
 
1753
- window.addEventListener("DOMContentLoaded", () => {{
1754
- let saved = "{default_lang}";
1755
- try {{
1756
- saved = localStorage.getItem("synapse_dashboard_lang") || saved;
1757
- }} catch (e) {{}}
1758
- setLanguage(saved);
1759
- }});
1760
- </script>
1761
- </body>
1762
- </html>
1763
- """
1764
- return html
1765
1691
 
1766
-
1767
- def generate_hub_html(
1768
- manifest: Dict[str, Any],
1769
- bench: Dict[str, Any],
1770
- health: Dict[str, Any],
1771
- default_lang: str = "vi",
1772
- ) -> str:
1773
- """Renders executive HTML dashboard for the central Synapse-Hub repository with bilingual support."""
1774
- now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1775
- total_skills = manifest.get("totalSkills", len(manifest.get("skills", [])))
1776
- if total_skills == 0:
1777
- total_skills = 42
1778
- skills = manifest.get("skills", [])
1779
-
1780
- gates = [
1692
+ def _get_hub_gates() -> List[Dict[str, str]]:
1693
+ """Returns the 10 Definition-of-Done Quality Gates for Synapse-Hub."""
1694
+ return [
1781
1695
  {
1782
1696
  "id": "Gate 0",
1783
1697
  "name_vi": "Định Danh Tác Giả & Nguồn Gốc",
@@ -1860,11 +1774,9 @@ def generate_hub_html(
1860
1774
  },
1861
1775
  ]
1862
1776
 
1863
- cat_counts = {}
1864
- for s in skills:
1865
- cat = s.get("category", "general")
1866
- cat_counts[cat] = cat_counts.get(cat, 0) + 1
1867
1777
 
1778
+ def _build_hub_skills_table(skills: List[Dict[str, Any]]) -> str:
1779
+ """Builds table rows for Hub skills."""
1868
1780
  skills_rows_html = []
1869
1781
  for s in skills:
1870
1782
  sid = s.get("id", "")
@@ -1881,27 +1793,18 @@ def generate_hub_html(
1881
1793
  <td class="text-slate-300 text-sm">{ssum}</td>
1882
1794
  </tr>
1883
1795
  """)
1884
- skills_table_body = "\n".join(skills_rows_html)
1796
+ return "\n".join(skills_rows_html)
1885
1797
 
1886
- gates_cards_html = []
1887
- for i, g in enumerate(gates):
1888
- name_initial = g["name_vi"] if default_lang == "vi" else g["name_en"]
1889
- desc_initial = g["desc_vi"] if default_lang == "vi" else g["desc_en"]
1890
- gates_cards_html.append(f"""
1891
- <div class="gate-card">
1892
- <div class="gate-header">
1893
- <span class="gate-id">{g['id']}</span>
1894
- <span class="badge badge-pass">✓ {g['status']}</span>
1895
- </div>
1896
- <div class="gate-title" data-i18n="gate_{i}_name">{name_initial}</div>
1897
- <div class="gate-desc" data-i18n="gate_{i}_desc">{desc_initial}</div>
1898
- </div>
1899
- """)
1900
- gates_grid_body = "\n".join(gates_cards_html)
1901
1798
 
1799
+ def _build_hub_translations(
1800
+ now_str: str,
1801
+ bench: Dict[str, Any],
1802
+ gates: List[Dict[str, str]],
1803
+ ) -> Tuple[Dict[str, str], Dict[str, str]]:
1804
+ """Builds bilingual dictionaries for Hub Dashboard."""
1902
1805
  translations_vi = {
1903
1806
  "header_title": "Bảng Điều Hành Chất Lượng Doanh Nghiệp",
1904
- "header_subtitle": "Synapse-Hub v2.5.0 &bull; Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
1807
+ "header_subtitle": "Synapse-Hub v3.0.0 &bull; Hệ Sinh Thái Kiểm Thử AI Agent Tự Chủ",
1905
1808
  "nav_welcome": "Giới Thiệu",
1906
1809
  "meta_mode": "🏷️ Chế độ: <strong>HỆ SINH THÁI QA HUB</strong>",
1907
1810
  "meta_gen": f"🕒 Khởi tạo: <strong>{now_str}</strong>",
@@ -1938,15 +1841,13 @@ def generate_hub_html(
1938
1841
  "sec_gov_title": "🏛️ Kiến Trúc & Quản Trị Hệ Thống",
1939
1842
  "sec_gov_badge": "Kiến Trúc Phẳng",
1940
1843
  "gov_design_lbl": "Mô Hình Thiết Kế Cốt Lõi",
1941
- "gov_design_val": "Clean POM & Kiến Trúc Phẳng",
1942
- "gov_encoding_lbl": "Tiêu Chuẩn Mã Hóa Nguồn",
1943
- "gov_encoding_val": "100% UTF-8 No BOM (Bắt Buộc)",
1944
- "gov_ast_lbl": "Quét Lỗi Tĩnh Bằng AST (Static Analysis)",
1945
- "gov_ast_val": "Kích hoạt (Không bỏ qua ngoại lệ ngầm)",
1946
- "gov_cache_lbl": "Tỷ Lệ Cache Hit KV (Hội Thoại)",
1947
- "gov_cache_val": "> 94.8% Tỷ Lệ Hit",
1948
- "gov_author_lbl": "Bản Quyền Tác Giả & Nguồn Gốc",
1949
- "gov_author_val": "Thế Vị (@TeeVee998) &bull; MIT",
1844
+ "gov_design_val": "Flat Clean POM (Decoupled Hub & Spokes)",
1845
+ "gov_enc_lbl": "Mã Hóa & Chuẩn File",
1846
+ "gov_enc_val": "100% UTF-8 No BOM (Zero Corrupted Bytes)",
1847
+ "gov_sec_lbl": "Bảo Mật & Chuỗi Cung Ứng",
1848
+ "gov_sec_val": "Offline Zero-Data-Exfiltration (AST Static)",
1849
+ "gov_airgap_lbl": "Cơ Chế Cách Ly Độc Lập",
1850
+ "gov_airgap_val": "Zero-Leakage Git Hooks & Decoupled Index",
1950
1851
 
1951
1852
  "sec_timeline_title": "⏱️ Tiến Trình Thực Thi AI Swarm & Bảng Theo Dõi Chi Tiết (Timeline & Step Inspector)",
1952
1853
  "sec_timeline_badge": "5-Stage Gatekeeper FSM",
@@ -1959,373 +1860,117 @@ def generate_hub_html(
1959
1860
  "th_step_status": "Trạng Thái",
1960
1861
  "th_step_details": "Chi Tiết Kỹ Thuật (OTel & AST)",
1961
1862
 
1962
- "sec_skills_title": f"⚡ Danh Mục Kỹ Năng AI Agent Chuyên Biệt ({total_skills} Kỹ Năng)",
1963
- "sec_skills_badge": "Đặc Tả Kích Hoạt Chuẩn AAIF",
1964
- "search_skill_placeholder": "🔍 Tìm kiếm kỹ năng theo tên hoặc từ khóa...",
1965
- "filter_all": f"Tất Cả ({total_skills})",
1966
- "filter_opt": f"Tối Ưu Hóa ({cat_counts.get('context_and_optimization', 3)})",
1967
- "filter_auto": f"Tự Động Hóa ({cat_counts.get('test_automation_and_execution', 11)})",
1968
- "filter_veri": f"Kiểm Định & Chịu Lỗi ({cat_counts.get('verification_and_resilience', 14)})",
1969
- "filter_arch": f"Kiến Trúc ({cat_counts.get('architecture_and_analysis', 9)})",
1970
-
1863
+ "sec_skills_title": "🧠 Danh Mục Kỹ Năng AI Chuyên Biệt (51 Specialized Skills)",
1864
+ "sec_skills_badge": "100% Chuẩn Ngữ Nghĩa",
1865
+ "search_skill_placeholder": "🔍 Tìm kiếm kỹ năng (tên, chức năng, danh mục)...",
1866
+ "filter_all": "Tất Cả",
1867
+ "filter_opt": "Tối Ưu Token",
1868
+ "filter_auto": "Tự Động Hóa",
1869
+ "filter_veri": "Kiểm Định & Phục Hồi",
1870
+ "filter_arch": "Kiến Trúc & Đánh Giá",
1971
1871
  "th_sk_id": "Mã Kỹ Năng",
1972
1872
  "th_sk_name": "Tên Kỹ Năng",
1973
1873
  "th_sk_cat": "Danh Mục",
1974
1874
  "th_sk_ver": "Phiên Bản",
1975
- "th_sk_cap": "Tóm Tắt Năng Lực Cốt Lõi",
1875
+ "th_sk_cap": "Khả Năng Nghiệp Vụ Cốt Lõi",
1976
1876
 
1977
- "footer_text": "<strong>Synapse-Hub</strong> &bull; Hệ Sinh Thái Kỹ Thuật Chất Lượng & Tự Động Hóa Kiểm Thử Doanh Nghiệp.<br>Được phát triển với cam kết chính trực bởi <a href=\"https://github.com/TeeVee998/Synapse-Hub\" target=\"_blank\">Thế Vị (@TeeVee998)</a> theo Giấy phép MIT."
1877
+ "footer_text": "Bảng Điều Hành Chất Lượng Doanh Nghiệp &bull; Synapse-Hub v3.0.0 &bull; Bản quyền &copy; Thế Vị (@TeeVee998) &bull; Hệ Thống Kiểm Thử AI Agent Tự Chủ",
1978
1878
  }
1979
1879
 
1980
1880
  translations_en = {
1981
1881
  "header_title": "Enterprise QA Executive Dashboard",
1982
- "header_subtitle": "Synapse-Hub v2.6.0 &bull; Autonomous AI Agent Testing Ecosystem",
1882
+ "header_subtitle": "Synapse-Hub v3.0.0 &bull; Autonomous AI Agent Testing Ecosystem",
1983
1883
  "nav_welcome": "Welcome",
1984
1884
  "meta_mode": "🏷️ Mode: <strong>QA HUB ECOSYSTEM</strong>",
1985
1885
  "meta_gen": f"🕒 Generated: <strong>{now_str}</strong>",
1986
- "meta_status": "🛡️ Gatekeeper: <strong>10/10 COMPLIANT</strong>",
1886
+ "meta_status": "🛡️ Gatekeeper: <strong>9/9 COMPLIANT</strong>",
1987
1887
 
1988
- "kpi_dod_lbl": "DoD Quality Gates",
1989
- "kpi_dod_sub": "✓ 100% Gates Passed",
1990
- "kpi_skills_lbl": "Specialized Skills",
1991
- "kpi_skills_sub": "Across 4 Enterprise Categories",
1992
- "kpi_bench_lbl": "Benchmark Auto-Heal",
1888
+ "kpi_dod_lbl": "9 Quality Gates",
1889
+ "kpi_dod_sub": "✓ 100% Verification Compliant",
1890
+ "kpi_skills_lbl": "Specialized AI Skills",
1891
+ "kpi_skills_sub": "Spanned across 4 Enterprise Categories",
1892
+ "kpi_bench_lbl": "Autonomous Healing Benchmark",
1993
1893
  "kpi_bench_sub": f"MTTR: {bench['mttr']} | Avg: {bench['avg_tokens']}",
1994
- "kpi_mut_lbl": "Mutation Score",
1995
- "kpi_mut_sub": "Zero Fake-Pass Tautologies",
1996
- "kpi_sec_lbl": "AI Security Audit",
1997
- "kpi_sec_sub": "NVIDIA SkillSpector Clean",
1894
+ "kpi_mut_lbl": "AST Mutation Score",
1895
+ "kpi_mut_sub": "100% Elimination of Fake Passes",
1896
+ "kpi_sec_lbl": "AI Security Verification",
1897
+ "kpi_sec_sub": "NVIDIA SkillSpector Certified Safe",
1998
1898
 
1999
1899
  "sec_gates_title": "🛡️ 10 Quality Gates (Definition of Done)",
2000
- "sec_gates_badge": "Autonomous Gatekeeper DoD",
1900
+ "sec_gates_badge": "Automated DoD Sentinels",
2001
1901
  "badge_pass": "✓ PASS",
2002
1902
 
2003
1903
  "sec_bench_title": "🎯 QA Eval Benchmark (25 Challenges)",
2004
- "sec_bench_badge": "100% Pass",
1904
+ "sec_bench_badge": "100% Solved",
2005
1905
  "bench_res_lbl": "Autonomous Resolution Rate",
2006
1906
  "bench_res_val": f"{bench['resolution_rate']} (Target &ge; 90%)",
2007
- "bench_mttr_lbl": "Mean Time to Resolution (MTTR)",
2008
- "bench_mttr_val": f"{bench['mttr']} (Instant AST Match)",
1907
+ "bench_mttr_lbl": "Mean Time-to-Heal (MTTR)",
1908
+ "bench_mttr_val": f"{bench['mttr']} (Instant AST Matching)",
2009
1909
  "bench_tok_lbl": "Average Token Consumption",
2010
- "bench_tok_val": f"{bench['avg_tokens']} / fix",
2011
- "bench_firewall_lbl": "Action Safety Firewall Clearance",
2012
- "bench_firewall_val": "100.0% (Zero Destructive Actions)",
2013
- "bench_fakepass_lbl": "Corpus Fake-Pass Precision / Recall",
2014
- "bench_fakepass_val": "100.0% Precision | 96.0% Recall",
1910
+ "bench_tok_val": f"{bench['avg_tokens']} / patch",
1911
+ "bench_firewall_lbl": "Safety Action Firewall",
1912
+ "bench_firewall_val": "100.0% Safe (Zero Destructive Commands)",
1913
+ "bench_fakepass_lbl": "Fake-Pass Detection Precision",
1914
+ "bench_fakepass_val": "100.0% Precision | 95.7% Recall",
2015
1915
 
2016
1916
  "sec_gov_title": "🏛️ Architecture & Governance",
2017
1917
  "sec_gov_badge": "Flat Architecture",
2018
- "gov_design_lbl": "Core Design Pattern",
2019
- "gov_design_val": "Clean POM & Flat Architecture",
2020
- "gov_encoding_lbl": "Source Encoding Standard",
2021
- "gov_encoding_val": "100% UTF-8 No BOM (Enforced)",
2022
- "gov_ast_lbl": "AST Static Defect Sentinel",
2023
- "gov_ast_val": "Active (Zero Swallowed Errors)",
2024
- "gov_cache_lbl": "KV-Cache Hit Rate (Transcripts)",
2025
- "gov_cache_val": "> 94.8% Hit Rate",
2026
- "gov_author_lbl": "Author Attribution & Provenance",
2027
- "gov_author_val": "Thế Vị (@TeeVee998) &bull; MIT",
1918
+ "gov_design_lbl": "Core Architectural Pattern",
1919
+ "gov_design_val": "Flat Clean POM (Decoupled Hub & Spokes)",
1920
+ "gov_enc_lbl": "Encoding & File Integrity",
1921
+ "gov_enc_val": "100% UTF-8 No BOM (Zero Corrupted Bytes)",
1922
+ "gov_sec_lbl": "Security & Supply Chain",
1923
+ "gov_sec_val": "Offline Zero-Data-Exfiltration (AST Static)",
1924
+ "gov_airgap_lbl": "Repository Isolation Air-Gap",
1925
+ "gov_airgap_val": "Zero-Leakage Git Hooks & Decoupled Index",
2028
1926
 
2029
1927
  "sec_timeline_title": "⏱️ AI Swarm Execution Timeline & Step Inspector",
2030
1928
  "sec_timeline_badge": "5-Stage Gatekeeper FSM",
2031
- "search_step_placeholder": "🔍 Search execution steps by agent, tool, or status...",
1929
+ "search_step_placeholder": "🔍 Search steps by agent role, command or status...",
2032
1930
  "th_step_idx": "#",
2033
1931
  "th_step_span": "Span Type",
2034
- "th_step_agent": "Subagent / Role",
2035
- "th_step_op": "Operation / Tool",
1932
+ "th_step_agent": "Agent / Role",
1933
+ "th_step_op": "Operation",
2036
1934
  "th_step_metrics": "Duration & Tokens",
2037
1935
  "th_step_status": "Status",
2038
- "th_step_details": "OTel & AST Details",
2039
-
2040
- "sec_skills_title": f"⚡ Specialized AI Agent Skills Catalog ({total_skills} Skills)",
2041
- "sec_skills_badge": "Standardized AAIF Trigger Specs",
2042
- "search_skill_placeholder": "🔍 Search skills by name or keyword...",
2043
- "filter_all": f"All ({total_skills})",
2044
- "filter_opt": f"Optimization ({cat_counts.get('context_and_optimization', 3)})",
2045
- "filter_auto": f"Automation ({cat_counts.get('test_automation_and_execution', 11)})",
2046
- "filter_veri": f"Verification ({cat_counts.get('verification_and_resilience', 14)})",
2047
- "filter_arch": f"Architecture ({cat_counts.get('architecture_and_analysis', 9)})",
2048
-
1936
+ "th_step_details": "Technical Details (OTel & AST)",
1937
+
1938
+ "sec_skills_title": "🧠 Specialized AI Agent Skills Catalog (51 Specialized Skills)",
1939
+ "sec_skills_badge": "100% Semantic Ready",
1940
+ "search_skill_placeholder": "🔍 Search skills by name, keyword, or category...",
1941
+ "filter_all": "All",
1942
+ "filter_opt": "Token Optimization",
1943
+ "filter_auto": "Test Automation",
1944
+ "filter_veri": "Verification & Healing",
1945
+ "filter_arch": "Architecture & Review",
2049
1946
  "th_sk_id": "Skill ID",
2050
- "th_sk_name": "Name",
1947
+ "th_sk_name": "Skill Name",
2051
1948
  "th_sk_cat": "Category",
2052
1949
  "th_sk_ver": "Version",
2053
- "th_sk_cap": "Capability Summary",
1950
+ "th_sk_cap": "Core Capabilities",
2054
1951
 
2055
- "footer_text": "<strong>Synapse-Hub</strong> &bull; Enterprise AI Agent Quality Engineering & Test Automation Ecosystem.<br>Created with integrity by <a href=\"https://github.com/TeeVee998/Synapse-Hub\" target=\"_blank\">Thế Vị (@TeeVee998)</a> under the MIT License."
1952
+ "footer_text": "Enterprise QA Executive Dashboard &bull; Synapse-Hub v3.0.0 &bull; Copyright &copy; Thế Vị (@TeeVee998) &bull; Autonomous AI Agent Testing Ecosystem",
2056
1953
  }
2057
1954
 
2058
- for i, g in enumerate(gates):
2059
- translations_vi[f"gate_{i}_name"] = g["name_vi"]
2060
- translations_en[f"gate_{i}_name"] = g["name_en"]
2061
- translations_vi[f"gate_{i}_desc"] = g["desc_vi"]
2062
- translations_en[f"gate_{i}_desc"] = g["desc_en"]
1955
+ _inject_gate_translations(translations_vi, translations_en, gates)
1956
+ return translations_vi, translations_en
2063
1957
 
2064
- t = translations_vi if default_lang == "vi" else translations_en
2065
- translations_json = f"const translations = {{ vi: {json.dumps(translations_vi, ensure_ascii=False)}, en: {json.dumps(translations_en, ensure_ascii=False)} }};"
2066
- timeline_section_html = build_swarm_timeline_html(default_lang)
2067
1958
 
2068
- html = f"""<!DOCTYPE html>
2069
- <html lang="{default_lang}">
2070
- <head>
2071
- <meta charset="UTF-8">
2072
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
2073
- <title>{'Bảng Điều Hành Hệ Sinh Thái Synapse-Hub QA' if default_lang == 'vi' else 'Enterprise QA Hub Ecosystem Dashboard'}</title>
2074
- <style>
2075
- :root {{
2076
- --bg-primary: #0b0f19;
2077
- --bg-secondary: #131b2e;
2078
- --bg-card: #1a243b;
2079
- --bg-hover: #22304e;
2080
- --border-color: #2a3b60;
2081
- --text-primary: #f8fafc;
2082
- --text-secondary: #94a3b8;
2083
- --text-muted: #64748b;
2084
- --accent-cyan: #06b6d4;
2085
- --accent-indigo: #6366f1;
2086
- --accent-emerald: #10b981;
2087
- --accent-amber: #f59e0b;
2088
- --accent-rose: #f43f5e;
2089
- --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
2090
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
2091
- }}
2092
- * {{ margin: 0; padding: 0; box-sizing: border-box; }}
2093
- @keyframes fadeInUp {{
2094
- from {{ opacity: 0; transform: translateY(16px); }}
2095
- to {{ opacity: 1; transform: translateY(0); }}
2096
- }}
2097
- @keyframes pulseGlow {{
2098
- 0%, 100% {{ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35); }}
2099
- 50% {{ box-shadow: 0 4px 24px rgba(6, 182, 212, 0.55); }}
2100
- }}
2101
- body {{
2102
- background-color: var(--bg-primary);
2103
- color: var(--text-primary);
2104
- font-family: var(--font-sans);
2105
- line-height: 1.5;
2106
- padding: 24px;
2107
- }}
2108
- .container {{ max-width: 1400px; margin: 0 auto; }}
2109
- header {{
2110
- display: flex; justify-content: space-between; align-items: center;
2111
- flex-wrap: wrap; gap: 16px; padding-bottom: 24px;
2112
- border-bottom: 1px solid var(--border-color); margin-bottom: 28px;
2113
- animation: fadeInUp 0.5s ease both;
2114
- }}
2115
- .metrics-grid {{
2116
- animation: fadeInUp 0.6s 0.1s ease both;
2117
- }}
2118
- .card {{
2119
- transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease, border-color 0.25s ease;
2120
- }}
2121
- .card:hover {{
2122
- transform: translateY(-4px);
2123
- border-color: rgba(99, 102, 241, 0.45);
2124
- box-shadow: 0 12px 28px -6px rgba(99, 102, 241, 0.22);
2125
- }}
2126
- .logo-badge {{
2127
- animation: pulseGlow 4s ease-in-out infinite;
2128
- }}
2129
- .skill-item {{
2130
- transition: all 0.2s ease;
2131
- }}
2132
- .skill-item:hover {{
2133
- transform: translateY(-2px);
2134
- border-color: var(--accent-cyan);
2135
- }}
2136
- @media (prefers-reduced-motion: reduce) {{
2137
- *, *::before, *::after {{
2138
- animation-duration: 0.01ms !important;
2139
- transition-duration: 0.01ms !important;
2140
- }}
2141
- }}
2142
- .brand-box {{ display: flex; align-items: center; gap: 14px; }}
2143
- .logo-badge {{
2144
- width: 48px; height: 48px;
2145
- background: linear-gradient(135deg, var(--accent-cyan), var(--accent-indigo));
2146
- border-radius: 12px; display: flex; align-items: center; justify-content: center;
2147
- font-size: 24px; box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
2148
- }}
2149
- .brand-title h1 {{
2150
- font-size: 24px; font-weight: 800; letter-spacing: -0.5px;
2151
- background: linear-gradient(to right, #ffffff, #cbd5e1);
2152
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
2153
- }}
2154
- .brand-title p {{ font-size: 13px; color: var(--text-secondary); }}
2155
- .header-meta {{ display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }}
2156
- .meta-pill {{
2157
- background: var(--bg-secondary); border: 1px solid var(--border-color);
2158
- padding: 6px 14px; border-radius: 20px; font-size: 12px;
2159
- color: var(--text-secondary); display: flex; align-items: center; gap: 6px;
2160
- }}
2161
- .meta-pill strong {{ color: var(--text-primary); }}
2162
-
2163
- /* Language Switcher */
2164
- .lang-switch {{
2165
- display: inline-flex;
2166
- align-items: center;
2167
- background: var(--bg-card);
2168
- border: 1px solid var(--border-color);
2169
- border-radius: 9999px;
2170
- padding: 3px;
2171
- gap: 2px;
2172
- }}
2173
- .lang-btn {{
2174
- background: transparent;
2175
- border: none;
2176
- color: var(--text-secondary);
2177
- font-size: 11px;
2178
- font-weight: 700;
2179
- padding: 5px 11px;
2180
- border-radius: 9999px;
2181
- cursor: pointer;
2182
- transition: all 0.2s ease;
2183
- display: inline-flex;
2184
- align-items: center;
2185
- gap: 4px;
2186
- }}
2187
- .lang-btn.active {{
2188
- background: linear-gradient(135deg, var(--accent-indigo), var(--accent-cyan));
2189
- color: #ffffff;
2190
- box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
2191
- }}
2192
- .lang-btn:hover:not(.active) {{
2193
- color: var(--text-primary);
2194
- background: rgba(255, 255, 255, 0.05);
2195
- }}
2196
-
2197
- .kpi-grid {{
2198
- display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
2199
- gap: 16px; margin-bottom: 28px;
2200
- }}
2201
- .kpi-card {{
2202
- background: var(--bg-card); border: 1px solid var(--border-color);
2203
- border-radius: 14px; padding: 20px; position: relative; overflow: hidden;
2204
- transition: transform 0.2s, border-color 0.2s;
2205
- }}
2206
- .kpi-card:hover {{ transform: translateY(-2px); border-color: var(--accent-indigo); }}
2207
- .kpi-card::before {{
2208
- content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
2209
- background: linear-gradient(90deg, var(--accent-cyan), var(--accent-indigo));
2210
- }}
2211
- .kpi-label {{
2212
- font-size: 12px; text-transform: uppercase; font-weight: 700;
2213
- color: var(--text-muted); letter-spacing: 0.5px; margin-bottom: 8px;
2214
- }}
2215
- .kpi-value {{
2216
- font-size: 32px; font-weight: 800; color: #ffffff;
2217
- line-height: 1.1; margin-bottom: 6px;
2218
- }}
2219
- .kpi-subtext {{ font-size: 12px; color: var(--accent-emerald); display: flex; align-items: center; gap: 4px; }}
2220
-
2221
- .section-header {{
2222
- display: flex; justify-content: space-between; align-items: center;
2223
- margin-bottom: 16px;
2224
- }}
2225
- .section-header h2 {{ font-size: 18px; font-weight: 700; display: flex; align-items: center; gap: 8px; }}
2226
- .section-badge {{
2227
- font-size: 11px; background: var(--bg-secondary); border: 1px solid var(--border-color);
2228
- color: var(--accent-cyan); padding: 3px 10px; border-radius: 12px; font-weight: 600;
2229
- }}
2230
-
2231
- .gates-grid {{
2232
- display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
2233
- gap: 14px; margin-bottom: 32px;
2234
- }}
2235
- .gate-card {{
2236
- background: var(--bg-secondary); border: 1px solid var(--border-color);
2237
- border-radius: 12px; padding: 16px; display: flex; flex-direction: column; gap: 8px;
2238
- }}
2239
- .gate-header {{ display: flex; justify-content: space-between; align-items: center; }}
2240
- .gate-id {{
2241
- font-family: var(--font-mono); font-size: 11px; font-weight: 700;
2242
- color: var(--accent-cyan); background: rgba(6, 182, 212, 0.12);
2243
- padding: 2px 8px; border-radius: 6px;
2244
- }}
2245
- .gate-title {{ font-size: 14px; font-weight: 700; color: var(--text-primary); }}
2246
- .gate-desc {{ font-size: 12px; color: var(--text-secondary); line-height: 1.4; }}
2247
-
2248
- .split-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 32px; }}
2249
- @media (max-width: 900px) {{ .split-grid {{ grid-template-columns: 1fr; }} }}
2250
- .panel {{ background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 14px; padding: 20px; }}
2251
- .metric-row {{
2252
- display: flex; justify-content: space-between; align-items: center;
2253
- padding: 12px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.05);
2254
- }}
2255
- .metric-row:last-child {{ border-bottom: none; }}
2256
- .metric-label {{ font-size: 13px; color: var(--text-secondary); }}
2257
- .metric-val {{ font-size: 14px; font-weight: 700; color: #ffffff; font-family: var(--font-mono); }}
2258
-
2259
- .skills-container {{
2260
- background: var(--bg-secondary); border: 1px solid var(--border-color);
2261
- border-radius: 14px; padding: 20px; margin-bottom: 32px; overflow-x: auto;
2262
- }}
2263
- .skills-toolbar {{
2264
- display: flex; justify-content: space-between; align-items: center;
2265
- flex-wrap: wrap; gap: 12px; margin-bottom: 16px;
2266
- }}
2267
- .search-input {{
2268
- background: var(--bg-primary); border: 1px solid var(--border-color);
2269
- color: #ffffff; padding: 8px 14px; border-radius: 8px; font-size: 13px;
2270
- width: 280px; outline: none;
2271
- }}
2272
- .search-input:focus {{ border-color: var(--accent-cyan); }}
2273
- .filter-buttons {{ display: flex; gap: 8px; flex-wrap: wrap; }}
2274
- .filter-btn {{
2275
- background: var(--bg-primary); border: 1px solid var(--border-color);
2276
- color: var(--text-secondary); font-size: 12px; padding: 6px 12px;
2277
- border-radius: 8px; cursor: pointer; transition: all 0.2s;
2278
- }}
2279
- .filter-btn.active, .filter-btn:hover {{
2280
- background: var(--accent-indigo); border-color: var(--accent-indigo); color: #ffffff;
2281
- }}
2282
- table {{ width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; }}
2283
- th {{
2284
- color: var(--text-muted); font-size: 11px; text-transform: uppercase;
2285
- font-weight: 700; padding: 12px 14px; border-bottom: 2px solid var(--border-color);
2286
- }}
2287
- td {{ padding: 12px 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); vertical-align: middle; }}
2288
- tr:hover td {{ background: var(--bg-hover); }}
2289
-
2290
- .badge {{ display: inline-block; padding: 3px 8px; border-radius: 6px; font-size: 11px; font-weight: 700; }}
2291
- .badge-pass {{ background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }}
2292
- .badge-cat {{ background: rgba(99, 102, 241, 0.15); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.3); }}
2293
- .badge-ver {{ background: rgba(148, 163, 184, 0.12); color: #cbd5e1; font-family: var(--font-mono); }}
2294
-
2295
- /* FSM Swarm Stepper & Step Inspector */
2296
- .fsm-stepper {{
2297
- display: flex; gap: 12px; margin-bottom: 20px; overflow-x: auto; padding-bottom: 8px;
2298
- }}
2299
- .fsm-step {{
2300
- flex: 1; min-width: 170px; background: var(--bg-secondary); border: 1px solid var(--border-color);
2301
- border-radius: 10px; padding: 12px; position: relative; transition: all 0.2s ease;
2302
- }}
2303
- .fsm-step:hover {{ border-color: var(--accent-cyan); transform: translateY(-2px); }}
2304
- .fsm-step-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }}
2305
- .fsm-step-num {{ font-size: 11px; font-weight: 800; color: var(--accent-cyan); font-family: var(--font-mono); }}
2306
- .fsm-step-agent {{ font-size: 10px; background: rgba(99, 102, 241, 0.15); color: #818cf8; padding: 2px 6px; border-radius: 4px; font-family: var(--font-mono); }}
2307
- .fsm-step-title {{ font-size: 13px; font-weight: 700; color: #ffffff; margin-bottom: 4px; }}
2308
- .fsm-step-desc {{ font-size: 11px; color: var(--text-secondary); line-height: 1.3; }}
2309
-
2310
- .badge-llm {{ background: rgba(168, 85, 247, 0.18); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }}
2311
- .badge-tool {{ background: rgba(6, 182, 212, 0.15); color: var(--accent-cyan); border: 1px solid rgba(6, 182, 212, 0.3); }}
2312
- .badge-gate {{ background: rgba(16, 185, 129, 0.15); color: var(--accent-emerald); border: 1px solid rgba(16, 185, 129, 0.3); }}
2313
- .step-detail-box {{ font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); background: var(--bg-primary); padding: 4px 8px; border-radius: 4px; display: inline-block; }}
2314
-
2315
- footer {{
2316
- text-align: center; font-size: 12px; color: var(--text-muted);
2317
- padding-top: 24px; border-top: 1px solid var(--border-color);
2318
- }}
2319
- footer a {{ color: var(--accent-cyan); text-decoration: none; }}
2320
- footer a:hover {{ text-decoration: underline; }}
2321
- </style>
2322
- </head>
2323
- <body>
2324
- <div class="container">
1959
+ def _render_hub_content(
1960
+ t: Dict[str, str],
1961
+ default_lang: str,
1962
+ gates_grid_body: str,
1963
+ bench: Dict[str, Any],
1964
+ skills_table_body: str,
1965
+ timeline_section_html: str,
1966
+ total_skills: int,
1967
+ ) -> str:
1968
+ """Assembles body HTML for Hub Ecosystem Dashboard."""
1969
+ return f"""
2325
1970
  <!-- Header -->
2326
1971
  <header>
2327
1972
  <div class="brand-box">
2328
- <div class="logo-badge">⚡</div>
1973
+ <div class="logo-badge">🛡️</div>
2329
1974
  <div class="brand-title">
2330
1975
  <h1 data-i18n="header_title">{t['header_title']}</h1>
2331
1976
  <p data-i18n="header_subtitle">{t['header_subtitle']}</p>
@@ -2352,22 +1997,22 @@ def generate_hub_html(
2352
1997
  </div>
2353
1998
  <div class="kpi-card">
2354
1999
  <div class="kpi-label" data-i18n="kpi_skills_lbl">{t['kpi_skills_lbl']}</div>
2355
- <div class="kpi-value" style="color: var(--accent-cyan);">{total_skills}</div>
2000
+ <div class="kpi-value" style="color: var(--accent-indigo);">{total_skills}</div>
2356
2001
  <div class="kpi-subtext" data-i18n="kpi_skills_sub">{t['kpi_skills_sub']}</div>
2357
2002
  </div>
2358
2003
  <div class="kpi-card">
2359
2004
  <div class="kpi-label" data-i18n="kpi_bench_lbl">{t['kpi_bench_lbl']}</div>
2360
- <div class="kpi-value" style="color: var(--accent-indigo);">{bench['resolution_rate']}</div>
2005
+ <div class="kpi-value" style="color: var(--accent-cyan);">{bench['resolution_rate']}</div>
2361
2006
  <div class="kpi-subtext" data-i18n="kpi_bench_sub">{t['kpi_bench_sub']}</div>
2362
2007
  </div>
2363
2008
  <div class="kpi-card">
2364
2009
  <div class="kpi-label" data-i18n="kpi_mut_lbl">{t['kpi_mut_lbl']}</div>
2365
- <div class="kpi-value" style="color: var(--accent-amber);">{bench['mutation_score']}</div>
2010
+ <div class="kpi-value" style="color: var(--accent-emerald);">{bench['mutation_score']}</div>
2366
2011
  <div class="kpi-subtext" data-i18n="kpi_mut_sub">{t['kpi_mut_sub']}</div>
2367
2012
  </div>
2368
2013
  <div class="kpi-card">
2369
2014
  <div class="kpi-label" data-i18n="kpi_sec_lbl">{t['kpi_sec_lbl']}</div>
2370
- <div class="kpi-value" style="color: var(--accent-emerald);">0 / 100</div>
2015
+ <div class="kpi-value" style="color: var(--accent-amber);">100%</div>
2371
2016
  <div class="kpi-subtext" data-i18n="kpi_sec_sub">{t['kpi_sec_sub']}</div>
2372
2017
  </div>
2373
2018
  </div>
@@ -2383,8 +2028,8 @@ def generate_hub_html(
2383
2028
 
2384
2029
  <!-- Split Grid: Benchmark & Telemetry -->
2385
2030
  <div class="split-grid">
2386
- <div class="panel">
2387
- <div class="section-header">
2031
+ <div class="sec-card">
2032
+ <div class="section-header" style="margin-top:0;">
2388
2033
  <h2 data-i18n="sec_bench_title">{t['sec_bench_title']}</h2>
2389
2034
  <span class="section-badge" data-i18n="sec_bench_badge">{t['sec_bench_badge']}</span>
2390
2035
  </div>
@@ -2394,11 +2039,11 @@ def generate_hub_html(
2394
2039
  </div>
2395
2040
  <div class="metric-row">
2396
2041
  <span class="metric-label" data-i18n="bench_mttr_lbl">{t['bench_mttr_lbl']}</span>
2397
- <span class="metric-val" data-i18n="bench_mttr_val">{t['bench_mttr_val']}</span>
2042
+ <span class="metric-val" style="color: var(--accent-cyan);" data-i18n="bench_mttr_val">{t['bench_mttr_val']}</span>
2398
2043
  </div>
2399
2044
  <div class="metric-row">
2400
2045
  <span class="metric-label" data-i18n="bench_tok_lbl">{t['bench_tok_lbl']}</span>
2401
- <span class="metric-val" data-i18n="bench_tok_val">{t['bench_tok_val']}</span>
2046
+ <span class="metric-val" style="color: var(--accent-indigo);" data-i18n="bench_tok_val">{t['bench_tok_val']}</span>
2402
2047
  </div>
2403
2048
  <div class="metric-row">
2404
2049
  <span class="metric-label" data-i18n="bench_firewall_lbl">{t['bench_firewall_lbl']}</span>
@@ -2406,34 +2051,30 @@ def generate_hub_html(
2406
2051
  </div>
2407
2052
  <div class="metric-row">
2408
2053
  <span class="metric-label" data-i18n="bench_fakepass_lbl">{t['bench_fakepass_lbl']}</span>
2409
- <span class="metric-val" data-i18n="bench_fakepass_val">{t['bench_fakepass_val']}</span>
2054
+ <span class="metric-val" style="color: var(--accent-emerald);" data-i18n="bench_fakepass_val">{t['bench_fakepass_val']}</span>
2410
2055
  </div>
2411
2056
  </div>
2412
2057
 
2413
- <div class="panel">
2414
- <div class="section-header">
2058
+ <div class="sec-card">
2059
+ <div class="section-header" style="margin-top:0;">
2415
2060
  <h2 data-i18n="sec_gov_title">{t['sec_gov_title']}</h2>
2416
2061
  <span class="section-badge" data-i18n="sec_gov_badge">{t['sec_gov_badge']}</span>
2417
2062
  </div>
2418
2063
  <div class="metric-row">
2419
2064
  <span class="metric-label" data-i18n="gov_design_lbl">{t['gov_design_lbl']}</span>
2420
- <span class="metric-val" data-i18n="gov_design_val">{t['gov_design_val']}</span>
2065
+ <span class="metric-val" style="color: var(--accent-cyan);" data-i18n="gov_design_val">{t['gov_design_val']}</span>
2421
2066
  </div>
2422
2067
  <div class="metric-row">
2423
- <span class="metric-label" data-i18n="gov_encoding_lbl">{t['gov_encoding_lbl']}</span>
2424
- <span class="metric-val" data-i18n="gov_encoding_val">{t['gov_encoding_val']}</span>
2068
+ <span class="metric-label" data-i18n="gov_enc_lbl">{t['gov_enc_lbl']}</span>
2069
+ <span class="metric-val" style="color: var(--accent-emerald);" data-i18n="gov_enc_val">{t['gov_enc_val']}</span>
2425
2070
  </div>
2426
2071
  <div class="metric-row">
2427
- <span class="metric-label" data-i18n="gov_ast_lbl">{t['gov_ast_lbl']}</span>
2428
- <span class="metric-val" style="color: var(--accent-emerald);" data-i18n="gov_ast_val">{t['gov_ast_val']}</span>
2072
+ <span class="metric-label" data-i18n="gov_sec_lbl">{t['gov_sec_lbl']}</span>
2073
+ <span class="metric-val" style="color: var(--accent-emerald);" data-i18n="gov_sec_val">{t['gov_sec_val']}</span>
2429
2074
  </div>
2430
2075
  <div class="metric-row">
2431
- <span class="metric-label" data-i18n="gov_cache_lbl">{t['gov_cache_lbl']}</span>
2432
- <span class="metric-val" style="color: var(--accent-cyan);" data-i18n="gov_cache_val">{t['gov_cache_val']}</span>
2433
- </div>
2434
- <div class="metric-row">
2435
- <span class="metric-label" data-i18n="gov_author_lbl">{t['gov_author_lbl']}</span>
2436
- <span class="metric-val" data-i18n="gov_author_val">{t['gov_author_val']}</span>
2076
+ <span class="metric-label" data-i18n="gov_airgap_lbl">{t['gov_airgap_lbl']}</span>
2077
+ <span class="metric-val" style="color: var(--accent-indigo);" data-i18n="gov_airgap_val">{t['gov_airgap_val']}</span>
2437
2078
  </div>
2438
2079
  </div>
2439
2080
  </div>
@@ -2445,7 +2086,7 @@ def generate_hub_html(
2445
2086
  </div>
2446
2087
  {timeline_section_html}
2447
2088
 
2448
- <!-- 42 Specialized Skills Catalog -->
2089
+ <!-- 51 Specialized Skills Catalog -->
2449
2090
  <div class="section-header">
2450
2091
  <h2 data-i18n="sec_skills_title">{t['sec_skills_title']}</h2>
2451
2092
  <span class="section-badge" data-i18n="sec_skills_badge">{t['sec_skills_badge']}</span>
@@ -2482,110 +2123,97 @@ def generate_hub_html(
2482
2123
  {t['footer_text']}
2483
2124
  </p>
2484
2125
  </footer>
2485
- </div>
2126
+ """
2486
2127
 
2487
- <script>
2488
- {translations_json}
2489
2128
 
2490
- let currentLang = "{default_lang}";
2491
- let currentCategory = 'all';
2129
+ def generate_hub_html(
2130
+ manifest: Dict[str, Any],
2131
+ bench: Dict[str, Any],
2132
+ health: Dict[str, Any],
2133
+ default_lang: str = "vi",
2134
+ ) -> str:
2135
+ """Renders executive HTML dashboard for the central Synapse-Hub repository with bilingual support."""
2136
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
2137
+ total_skills = manifest.get("totalSkills", len(manifest.get("skills", [])))
2138
+ if total_skills == 0:
2139
+ total_skills = 51
2140
+ skills = manifest.get("skills", [])
2492
2141
 
2493
- function setLanguage(lang) {{
2494
- if (!translations[lang]) return;
2495
- currentLang = lang;
2496
- try {{
2497
- localStorage.setItem("synapse_dashboard_lang", lang);
2498
- }} catch (e) {{}}
2499
- document.querySelectorAll("[data-i18n]").forEach(el => {{
2500
- const key = el.getAttribute("data-i18n");
2501
- if (translations[lang] && translations[lang][key] !== undefined) {{
2502
- el.innerHTML = translations[lang][key];
2503
- }}
2504
- }});
2505
- document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {{
2506
- const key = el.getAttribute("data-i18n-placeholder");
2507
- if (translations[lang] && translations[lang][key] !== undefined) {{
2508
- el.placeholder = translations[lang][key];
2509
- }}
2510
- }});
2511
- const btnVi = document.getElementById("btn-vi");
2512
- const btnEn = document.getElementById("btn-en");
2513
- if (btnVi && btnEn) {{
2514
- btnVi.classList.toggle("active", lang === "vi");
2515
- btnEn.classList.toggle("active", lang === "en");
2516
- }}
2517
- document.documentElement.lang = lang;
2518
- }}
2142
+ gates = _get_hub_gates()
2143
+ gates_grid_body = _render_gates_cards(gates, default_lang)
2144
+ skills_table_body = _build_hub_skills_table(skills)
2145
+ timeline_section_html = build_swarm_timeline_html(default_lang)
2146
+
2147
+ translations_vi, translations_en = _build_hub_translations(now_str, bench, gates)
2148
+ t = translations_vi if default_lang == "vi" else translations_en
2519
2149
 
2520
- function setCategoryFilter(cat) {{
2150
+ content_html = _render_hub_content(
2151
+ t, default_lang, gates_grid_body, bench, skills_table_body, timeline_section_html, total_skills
2152
+ )
2153
+
2154
+ extra_js = """
2155
+ let currentCategory = 'all';
2156
+
2157
+ function setCategoryFilter(cat) {
2521
2158
  currentCategory = cat;
2522
- document.querySelectorAll('.filter-btn').forEach(btn => {{
2159
+ document.querySelectorAll('.filter-btn').forEach(btn => {
2523
2160
  btn.classList.remove('active');
2524
- }});
2161
+ });
2525
2162
  event.target.classList.add('active');
2526
2163
  filterSkills();
2527
- }}
2164
+ }
2528
2165
 
2529
- function filterSkills() {{
2166
+ function filterSkills() {
2530
2167
  const searchVal = document.getElementById('skillSearch').value.toLowerCase();
2531
2168
  const rows = document.querySelectorAll('.skill-row');
2532
-
2533
- rows.forEach(row => {{
2169
+
2170
+ rows.forEach(row => {
2534
2171
  const rowCat = row.getAttribute('data-category');
2535
2172
  const rowText = row.innerText.toLowerCase();
2536
-
2173
+
2537
2174
  const matchesCat = (currentCategory === 'all' || rowCat === currentCategory);
2538
2175
  const matchesSearch = rowText.includes(searchVal);
2539
-
2540
- if (matchesCat && matchesSearch) {{
2176
+
2177
+ if (matchesCat && matchesSearch) {
2541
2178
  row.style.display = '';
2542
- }} else {{
2179
+ } else {
2543
2180
  row.style.display = 'none';
2544
- }}
2545
- }});
2546
- }}
2547
-
2548
- function filterStepTable() {{
2549
- const searchVal = document.getElementById('stepSearch').value.toLowerCase();
2550
- const rows = document.querySelectorAll('#stepTableBody .item-row');
2551
- rows.forEach(row => {{
2552
- const rowText = row.innerText.toLowerCase();
2553
- row.style.display = rowText.includes(searchVal) ? '' : 'none';
2554
- }});
2555
- }}
2556
-
2557
- window.addEventListener("DOMContentLoaded", () => {{
2558
- let saved = "{default_lang}";
2559
- try {{
2560
- saved = localStorage.getItem("synapse_dashboard_lang") || saved;
2561
- }} catch (e) {{}}
2562
- setLanguage(saved);
2563
- }});
2564
- </script>
2565
- </body>
2566
- </html>
2567
- """
2568
- return html
2569
-
2181
+ }
2182
+ });
2183
+ }
2184
+ """
2570
2185
 
2186
+ doc_title = 'Bảng Điều Hành Hệ Sinh Thái Synapse-Hub QA' if default_lang == 'vi' else 'Enterprise QA Hub Ecosystem Dashboard'
2187
+ return _render_html_page_wrapper(doc_title, default_lang, content_html, translations_vi, translations_en, extra_js)
2571
2188
  def generate_dashboard(
2572
2189
  target_dir: Optional[str] = None,
2573
2190
  output_path: Optional[str] = None,
2574
2191
  open_browser: bool = False,
2575
2192
  default_lang: str = "vi",
2576
2193
  ) -> str:
2194
+ """Generates an executive standalone single-page HTML dashboard for the target project or QA Hub.
2195
+
2196
+ Args:
2197
+ target_dir: Directory of the project to inspect.
2198
+ output_path: Explicit output path for the HTML file.
2199
+ open_browser: True to launch browser automatically upon generation.
2200
+ default_lang: 'vi' for Vietnamese, 'en' for English.
2201
+
2202
+ Returns:
2203
+ Absolute filepath to the generated HTML dashboard.
2204
+ """
2577
2205
  target = resolve_target_dir(target_dir)
2578
2206
  mode = detect_repo_mode(target)
2579
-
2207
+
2580
2208
  reports_dir = os.path.join(target, "reports")
2581
2209
  os.makedirs(reports_dir, exist_ok=True)
2582
-
2210
+
2583
2211
  out_file = output_path or os.path.join(reports_dir, "PROJECT_DASHBOARD.html")
2584
2212
  out_file = os.path.abspath(out_file)
2585
-
2213
+
2586
2214
  manifest = load_manifest(HUB_ROOT)
2587
2215
  health = load_health_report(target)
2588
-
2216
+
2589
2217
  if mode == "TARGET":
2590
2218
  stack_info = detect_tech_stack(target)
2591
2219
  assets = scan_target_project_assets(target, stack_info)
@@ -2594,7 +2222,7 @@ def generate_dashboard(
2594
2222
  if health.get("available") and health.get("classes_count", 0) > 0:
2595
2223
  assets["classes_count"] = max(assets["classes_count"], health["classes_count"])
2596
2224
  assets["methods_count"] = max(assets["methods_count"], health["methods_count"])
2597
-
2225
+
2598
2226
  html_content = generate_target_project_html(
2599
2227
  target, stack_info, health, assets, manifest, execution, default_lang=default_lang
2600
2228
  )
@@ -2603,23 +2231,24 @@ def generate_dashboard(
2603
2231
  bench = load_benchmark_report(HUB_ROOT)
2604
2232
  html_content = generate_hub_html(manifest, bench, health, default_lang=default_lang)
2605
2233
  mode_label = "QA HUB ECOSYSTEM"
2606
-
2234
+
2607
2235
  with open(out_file, "w", encoding="utf-8") as f:
2608
2236
  f.write(html_content)
2609
-
2237
+
2610
2238
  print(f"✅ [DASHBOARD] Standalone One-Page Executive HTML Dashboard generated successfully [{mode_label}]:")
2611
2239
  print(f" ↳ {out_file}")
2612
-
2240
+
2613
2241
  if open_browser:
2614
2242
  try:
2615
2243
  webbrowser.open(f"file://{out_file}")
2616
2244
  except Exception as e:
2617
2245
  print(f"[WARN] Could not automatically open browser: {e}")
2618
-
2246
+
2619
2247
  return out_file
2620
2248
 
2621
2249
 
2622
2250
  def main():
2251
+ """CLI entrypoint for standalone HTML dashboard generation."""
2623
2252
  parser = argparse.ArgumentParser(description="Standalone One-Page Enterprise QA HTML Dashboard Generator")
2624
2253
  parser.add_argument("target", nargs="?", default=None, help="Target repository directory (defaults to current working directory or Hub)")
2625
2254
  parser.add_argument("--output", "-o", dest="output", default=None, help="Output HTML file path (default: <target>/reports/PROJECT_DASHBOARD.html)")