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.
- package/.agents/agents/failure-analyst.md +4 -0
- package/.agents/agents/live-explorer.md +2 -0
- package/.agents/agents/orchestrator.md +16 -0
- package/.agents/agents/pom-crafter.md +10 -0
- package/.agents/agents/quality-gatekeeper.md +5 -0
- package/.agents/agents/test-runner.md +8 -0
- package/.agents/rules/git-push-protocol.md +1 -1
- package/.agents/rules/graphify.md +2 -0
- package/.agents/rules/mandatory-playwright-live.md +9 -9
- package/.agents/rules/mobile_appium_rules.md +8 -1
- package/.agents/skills/actor-qa-swarm/SKILL.md +77 -0
- package/.agents/skills/actor-qa-swarm/examples/broker_sharding_sample.json +12 -0
- package/.agents/skills/agentless-qa-repair/SKILL.md +69 -0
- package/.agents/skills/agentless-qa-repair/examples/healing_report_sample.json +17 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +1 -1
- package/.agents/skills/clean-pom-crafter/SKILL.md +2 -2
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +4 -4
- package/.agents/skills/event-stream-qa-orchestrator/SKILL.md +79 -0
- package/.agents/skills/event-stream-qa-orchestrator/examples/timeline_sample.jsonl +2 -0
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +2 -2
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +6 -21
- package/.agents/skills/katalon-cli-runner/SKILL.md +1 -1
- package/.agents/skills/lean-impact-scoreboard/SKILL.md +1 -1
- package/.agents/skills/mipro-prompt-compiler/SKILL.md +59 -0
- package/.agents/skills/mipro-prompt-compiler/examples/compiled_module_sample.json +22 -0
- package/.agents/skills/qa-aci-interface/SKILL.md +67 -0
- package/.agents/skills/qa-aci-interface/examples/surgical_edit_sample.json +12 -0
- package/.agents/skills/qa-memory-os/SKILL.md +80 -0
- package/.agents/skills/qa-memory-os/examples/archival_memory_sample.json +21 -0
- package/.agents/skills/repo-quality-audit/SKILL.md +1 -1
- package/.agents/skills/review-spec/SKILL.md +60 -0
- package/.agents/skills/review-spec/examples/sample_spec_review.md +33 -0
- package/.agents/skills/senior-code-review/SKILL.md +1 -1
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +2 -2
- package/.agents/skills/technical-debt-harvester/SKILL.md +1 -1
- package/.agents/skills/tot-exploratory-testing/SKILL.md +59 -0
- package/.agents/skills/tot-exploratory-testing/examples/tot_graph_sample.json +35 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +2 -2
- package/.agents/skills/web-agent-gym-benchmarking/SKILL.md +62 -0
- package/.agents/skills/web-agent-gym-benchmarking/examples/benchmark_report_sample.json +22 -0
- package/CLAUDE.md +2 -2
- package/README.en.md +70 -291
- package/README.md +68 -303
- package/docs/ARCHITECTURE.md +54 -8
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/QUALITY_GATES.md +113 -0
- package/docs/research/EXTERNAL_AGENT_SKILLS_SURVEY.md +1062 -0
- package/package.json +1 -1
- package/scripts/package_dist.py +2 -1
- package/skills_manifest.json +92 -11
- package/synapse.config.json +1 -1
- package/templates/ai_bridges/CLAUDE.md +2 -2
- package/templates/ai_bridges/copilot-instructions.md +2 -2
- package/tools/TOOLS_RUNBOOK.md +1 -1
- package/tools/__init__.py +18 -1
- package/tools/_openapi_parser.py +158 -0
- package/tools/agent_canary.py +42 -30
- package/tools/agentless_patcher.py +417 -0
- package/tools/analyze_project.py +12 -11
- package/tools/api_drift_sentinel.py +4 -4
- package/tools/api_fuzzer.py +8 -8
- package/tools/auto_heal.py +23 -5
- package/tools/auto_heal_reflexion.py +2 -2
- package/tools/autogen_qa_broker.py +318 -0
- package/tools/browser_helpers.js +2 -2
- package/tools/browsergym_eval.py +236 -0
- package/tools/bug_reporter.py +11 -13
- package/tools/bump_version.py +75 -5
- package/tools/cache_resolver.py +78 -83
- package/tools/check_env.py +9 -1
- package/tools/check_version_sync.py +40 -3
- package/tools/ci_healing_hook.py +5 -7
- package/tools/cli.py +5 -7
- package/tools/config_decoupler.py +2 -3
- package/tools/core/__init__.py +52 -0
- package/tools/count_tokens.py +22 -20
- package/tools/dashboard_generator.py +628 -999
- package/tools/db_sandbox.py +69 -53
- package/tools/debt_harvester.py +3 -3
- package/tools/distill_log.py +3 -3
- package/tools/dspy_qa_signatures.py +3 -4
- package/tools/export_postman_collection.py +8 -8
- package/tools/flaky_detector.py +2 -2
- package/tools/fuzz_payloads.py +5 -5
- package/tools/generate_allure_report.py +1 -1
- package/tools/generate_jmx_plan.py +4 -4
- package/tools/gherkin_suite.py +1 -1
- package/tools/git_pre_commit_guard.py +2 -2
- package/tools/goldfish_review.py +2 -2
- package/tools/heal/__init__.py +28 -0
- package/tools/heal_pr_automator.py +29 -4
- package/tools/heuristic_locator_matcher.py +2 -2
- package/tools/infer_linter.py +8 -8
- package/tools/interview_story.py +0 -1
- package/tools/jtl_analyzer.py +11 -11
- package/tools/lint/__init__.py +30 -0
- package/tools/lint_ai_docs.py +42 -0
- package/tools/lookup_tc.py +1 -1
- package/tools/matrix_sharder.py +15 -13
- package/tools/mcp/__init__.py +16 -0
- package/tools/mcp_excel_server.py +2 -2
- package/tools/mcp_server.py +3 -4
- package/tools/md_to_xlsx.py +26 -26
- package/tools/micro_mutator.py +11 -12
- package/tools/mipro_assertion_tuner.py +207 -0
- package/tools/network_mock_shield.py +27 -27
- package/tools/newman_analyzer.py +2 -3
- package/tools/omni_emitter.py +5 -8
- package/tools/omni_parser.py +5 -5
- package/tools/omni_transpiler.py +5 -5
- package/tools/output_formatter.py +19 -0
- package/tools/perf/__init__.py +24 -0
- package/tools/pinpoint_failure.py +13 -1
- package/tools/project_state.py +4 -2
- package/tools/qa_event_stream.py +336 -0
- package/tools/repo_map.py +8 -8
- package/tools/report/__init__.py +32 -0
- package/tools/reversible_sandbox.py +5 -2
- package/tools/safety_firewall.py +2 -2
- package/tools/senior_code_review.py +2 -2
- package/tools/setup_project.py +10 -12
- package/tools/skill_collision_guard.py +3 -3
- package/tools/skill_vault.py +12 -3
- package/tools/skillspector.py +5 -7
- package/tools/smart_visual_masker.py +4 -6
- package/tools/spec_drift_sentinel.py +1 -2
- package/tools/swe_qa_aci.py +369 -0
- package/tools/synapse_qa_bench.py +3 -3
- package/tools/sync_agent_rules.py +18 -4
- package/tools/telemetry_correlator.py +3 -2
- package/tools/test_memory_os.py +419 -0
- package/tools/textgrad_mutation_optimizer.py +3 -3
- package/tools/tms_sync_bridge.py +5 -5
- package/tools/tot_exploratory_tester.py +239 -0
- package/tools/traffic_session_converter.py +9 -9
- package/tools/universal_ir.py +2 -3
- package/tools/update_sentinel.py +6 -9
- package/tools/verify_all.py +10 -0
- package/tools/visual_diff_canvas.py +1 -2
- package/tools/visual_som_helper.py +15 -13
- package/tools/watch_mode.py +1 -1
- package/tools/wcag_audit.py +22 -1
- 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 (
|
|
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,
|
|
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
|
-
|
|
187
|
-
return {"totalSkills":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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> • Executed at {execution.get('timestamp', 'N/A')}"
|
|
841
1093
|
initial_meta = meta_vi if default_lang == "vi" else meta_en
|
|
842
1094
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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="
|
|
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:
|
|
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-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
</div>
|
|
866
|
-
<div class="exec-stat-
|
|
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 = "
|
|
883
|
-
ready_badge_en = "
|
|
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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
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
|
-
|
|
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]:
|
|
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
|
-
|
|
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
|
-
|
|
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} • 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 • {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":
|
|
1323
|
+
"footer_text": "Báo cáo được khởi tạo tự động bởi Synapse-Hub v3.0.0 • Bản quyền © Thế Vị (@TeeVee998) • Chuẩn Clean POM & Kiểm Thử Tự Chủ",
|
|
1083
1324
|
}
|
|
1084
1325
|
|
|
1085
1326
|
translations_en = {
|
|
1086
|
-
"header_title": f"{project_name} • Executive QA
|
|
1087
|
-
"header_subtitle": f"
|
|
1327
|
+
"header_title": f"{project_name} • Executive QA Quality Dashboard",
|
|
1328
|
+
"header_subtitle": f"Automated Test Suite • {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
|
|
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
|
|
1099
|
-
"kpi_tc_sub": f"{assets['suites_count']} Test Suites
|
|
1100
|
-
"kpi_passrate_lbl": "Latest
|
|
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
|
|
1344
|
+
"kpi_clean_sub": "Zero Locators in Test Scripts",
|
|
1104
1345
|
|
|
1105
|
-
"sec_gates_title": "🛡️
|
|
1106
|
-
"sec_gates_badge": "
|
|
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": "🏛️
|
|
1110
|
-
"arch_framework_lbl": "Automation Framework",
|
|
1111
|
-
"arch_lang_lbl": "
|
|
1112
|
-
"arch_page_lbl": "Page
|
|
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
|
|
1355
|
+
"arch_cmd_lbl": "Headless CLI Command",
|
|
1115
1356
|
|
|
1116
|
-
"sec_intel_title": "🧠
|
|
1117
|
-
"sec_intel_badge": "0-Token
|
|
1118
|
-
"intel_health_lbl": "Health & AST
|
|
1119
|
-
"intel_repo_lbl": "AST Page Signatures
|
|
1120
|
-
"intel_guide_lbl": "Project AI Guide &
|
|
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
|
|
1363
|
+
"intel_skills_lbl": "Assigned Tech-Stack Skills:",
|
|
1123
1364
|
|
|
1124
|
-
"sec_suites_title": f"📦 Test 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
|
|
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": "
|
|
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
|
|
1139
|
-
"sec_tests_badge": "100% Business
|
|
1140
|
-
"search_tc_placeholder": "🔍 Search test
|
|
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
|
|
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
|
|
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": "
|
|
1155
|
-
"th_step_op": "Operation
|
|
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
|
|
1399
|
+
"th_step_details": "Technical Details (OTel & AST)",
|
|
1159
1400
|
|
|
1160
|
-
"footer_text":
|
|
1401
|
+
"footer_text": "Generated automatically by Synapse-Hub v3.0.0 • Copyright © Thế Vị (@TeeVee998) • Clean POM & Autonomous Testing",
|
|
1161
1402
|
}
|
|
1162
1403
|
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
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} • {'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: {
|
|
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
|
-
|
|
1622
|
+
"""
|
|
1693
1623
|
|
|
1694
|
-
<script>
|
|
1695
|
-
{translations_json}
|
|
1696
1624
|
|
|
1697
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
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} • {'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
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
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
|
|
1807
|
+
"header_subtitle": "Synapse-Hub v3.0.0 • 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
|
|
1942
|
-
"
|
|
1943
|
-
"
|
|
1944
|
-
"
|
|
1945
|
-
"
|
|
1946
|
-
"
|
|
1947
|
-
"
|
|
1948
|
-
"gov_author_lbl": "Bản Quyền Tác Giả & Nguồn Gốc",
|
|
1949
|
-
"gov_author_val": "Thế Vị (@TeeVee998) • 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":
|
|
1963
|
-
"sec_skills_badge": "
|
|
1964
|
-
"search_skill_placeholder": "🔍 Tìm kiếm kỹ năng
|
|
1965
|
-
"filter_all":
|
|
1966
|
-
"filter_opt":
|
|
1967
|
-
"filter_auto":
|
|
1968
|
-
"filter_veri":
|
|
1969
|
-
"filter_arch":
|
|
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": "
|
|
1875
|
+
"th_sk_cap": "Khả Năng Nghiệp Vụ Cốt Lõi",
|
|
1976
1876
|
|
|
1977
|
-
"footer_text": "
|
|
1877
|
+
"footer_text": "Bảng Điều Hành Chất Lượng Doanh Nghiệp • Synapse-Hub v3.0.0 • Bản quyền © Thế Vị (@TeeVee998) • 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
|
|
1882
|
+
"header_subtitle": "Synapse-Hub v3.0.0 • 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>
|
|
1886
|
+
"meta_status": "🛡️ Gatekeeper: <strong>9/9 COMPLIANT</strong>",
|
|
1987
1887
|
|
|
1988
|
-
"kpi_dod_lbl": "
|
|
1989
|
-
"kpi_dod_sub": "✓ 100%
|
|
1990
|
-
"kpi_skills_lbl": "Specialized Skills",
|
|
1991
|
-
"kpi_skills_sub": "
|
|
1992
|
-
"kpi_bench_lbl": "Benchmark
|
|
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": "
|
|
1996
|
-
"kpi_sec_lbl": "AI Security
|
|
1997
|
-
"kpi_sec_sub": "NVIDIA SkillSpector
|
|
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": "
|
|
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%
|
|
1904
|
+
"sec_bench_badge": "100% Solved",
|
|
2005
1905
|
"bench_res_lbl": "Autonomous Resolution Rate",
|
|
2006
1906
|
"bench_res_val": f"{bench['resolution_rate']} (Target ≥ 90%)",
|
|
2007
|
-
"bench_mttr_lbl": "Mean Time
|
|
2008
|
-
"bench_mttr_val": f"{bench['mttr']} (Instant AST
|
|
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']} /
|
|
2011
|
-
"bench_firewall_lbl": "Action
|
|
2012
|
-
"bench_firewall_val": "100.0% (Zero Destructive
|
|
2013
|
-
"bench_fakepass_lbl": "
|
|
2014
|
-
"bench_fakepass_val": "100.0% Precision |
|
|
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
|
|
2019
|
-
"gov_design_val": "Clean POM &
|
|
2020
|
-
"
|
|
2021
|
-
"
|
|
2022
|
-
"
|
|
2023
|
-
"
|
|
2024
|
-
"
|
|
2025
|
-
"
|
|
2026
|
-
"gov_author_lbl": "Author Attribution & Provenance",
|
|
2027
|
-
"gov_author_val": "Thế Vị (@TeeVee998) • 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
|
|
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": "
|
|
2035
|
-
"th_step_op": "Operation
|
|
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
|
|
2039
|
-
|
|
2040
|
-
"sec_skills_title":
|
|
2041
|
-
"sec_skills_badge": "
|
|
2042
|
-
"search_skill_placeholder": "🔍 Search skills by name or
|
|
2043
|
-
"filter_all":
|
|
2044
|
-
"filter_opt":
|
|
2045
|
-
"filter_auto":
|
|
2046
|
-
"filter_veri":
|
|
2047
|
-
"filter_arch":
|
|
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": "
|
|
1950
|
+
"th_sk_cap": "Core Capabilities",
|
|
2054
1951
|
|
|
2055
|
-
"footer_text": "
|
|
1952
|
+
"footer_text": "Enterprise QA Executive Dashboard • Synapse-Hub v3.0.0 • Copyright © Thế Vị (@TeeVee998) • Autonomous AI Agent Testing Ecosystem",
|
|
2056
1953
|
}
|
|
2057
1954
|
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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"
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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="
|
|
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="
|
|
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="
|
|
2424
|
-
<span class="metric-val" data-i18n="
|
|
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="
|
|
2428
|
-
<span class="metric-val" style="color: var(--accent-emerald);" data-i18n="
|
|
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="
|
|
2432
|
-
<span class="metric-val" style="color: var(--accent-
|
|
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
|
-
<!--
|
|
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
|
-
|
|
2126
|
+
"""
|
|
2486
2127
|
|
|
2487
|
-
<script>
|
|
2488
|
-
{translations_json}
|
|
2489
2128
|
|
|
2490
|
-
|
|
2491
|
-
|
|
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
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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)")
|