test(observability): guard sidebar navigation design
Some checks failed
CD Pipeline / deploy (push) Failing after 2m11s
Some checks failed
CD Pipeline / deploy (push) Failing after 2m11s
This commit is contained in:
@@ -63,6 +63,9 @@ Guard 會檢查:
|
|||||||
- 圖表 `style="height:..."` 與靜態 `style="width:..."`。
|
- 圖表 `style="height:..."` 與靜態 `style="width:..."`。
|
||||||
- 過大標題 clamp 與純白 hover/card surface。
|
- 過大標題 clamp 與純白 hover/card surface。
|
||||||
- 觀測台 CSS 必要 token / utility class 是否仍存在。
|
- 觀測台 CSS 必要 token / utility class 是否仍存在。
|
||||||
|
- 側欄是否維持 `AI 中樞 → AI 觀測台 → 分組頁面` 的收納結構。
|
||||||
|
- 側欄是否維持暖深咖啡背景與第二/三層足夠對比。
|
||||||
|
- Topbar 是否仍載入觀測台 CSS 與健康 indicator。
|
||||||
|
|
||||||
### 2. Production 10 頁 HTTP 巡檢
|
### 2. Production 10 頁 HTTP 巡檢
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ TEMPLATE_PATHS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
CSS_PATH = Path("static/css/observability-system.css")
|
CSS_PATH = Path("static/css/observability-system.css")
|
||||||
|
SHELL_PATH = Path("templates/components/_ewoooc_shell.html")
|
||||||
|
BASE_PATH = Path("templates/ewoooc_base.html")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -92,6 +94,40 @@ REQUIRED_CSS_SNIPPETS = [
|
|||||||
".obs-table-shell",
|
".obs-table-shell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
REQUIRED_SHELL_SNIPPETS = [
|
||||||
|
"AI 中樞",
|
||||||
|
"AI 觀測台",
|
||||||
|
"戰情室",
|
||||||
|
"系統與成本",
|
||||||
|
"RAG 與品質",
|
||||||
|
"momo-nav-tree",
|
||||||
|
"momo-nav-subtree",
|
||||||
|
"momo-nav-subtitle",
|
||||||
|
"momo-nav-sublink",
|
||||||
|
"rgba(255, 248, 238, 0.72)",
|
||||||
|
"linear-gradient(180deg",
|
||||||
|
]
|
||||||
|
|
||||||
|
FORBIDDEN_SHELL_PATTERNS = [
|
||||||
|
Rule(
|
||||||
|
"pure_black_sidebar",
|
||||||
|
re.compile(r"\.momo-sidebar\s*\{[^}]*background\s*:\s*(#000|black)\b", re.IGNORECASE | re.DOTALL),
|
||||||
|
"側欄不得回退成純黑背景;必須維持暖深咖啡漸層。",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
"low_contrast_sublink",
|
||||||
|
re.compile(r"\.momo-nav-sublink\s*\{[^}]*color\s*:\s*rgba\([^)]*,\s*0\.(?:[0-5][0-9]|60)\)", re.IGNORECASE | re.DOTALL),
|
||||||
|
"第二/三層導覽文字透明度過低,會看不清楚;需維持足夠對比。",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
REQUIRED_BASE_SNIPPETS = [
|
||||||
|
"observability-system.css",
|
||||||
|
"momo-observability-mode",
|
||||||
|
"/observability/overview",
|
||||||
|
"/observability/api/health_indicator",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def line_number(text: str, index: int) -> int:
|
def line_number(text: str, index: int) -> int:
|
||||||
return text.count("\n", 0, index) + 1
|
return text.count("\n", 0, index) + 1
|
||||||
@@ -129,11 +165,43 @@ def scan_css() -> list[str]:
|
|||||||
return findings
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def scan_required_snippets(relative_path: Path, snippets: list[str], label: str) -> list[str]:
|
||||||
|
path = ROOT / relative_path
|
||||||
|
if not path.exists():
|
||||||
|
return [f"{relative_path}: missing required {label} file"]
|
||||||
|
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
findings: list[str] = []
|
||||||
|
for snippet in snippets:
|
||||||
|
if snippet not in text:
|
||||||
|
findings.append(f"{relative_path}: missing required {label} snippet `{snippet}`")
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def scan_shell() -> list[str]:
|
||||||
|
path = ROOT / SHELL_PATH
|
||||||
|
if not path.exists():
|
||||||
|
return [f"{SHELL_PATH}: missing required shell file"]
|
||||||
|
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
findings = scan_required_snippets(SHELL_PATH, REQUIRED_SHELL_SNIPPETS, "sidebar/nav")
|
||||||
|
for rule in FORBIDDEN_SHELL_PATTERNS:
|
||||||
|
for match in rule.pattern.finditer(text):
|
||||||
|
line = line_number(text, match.start())
|
||||||
|
snippet = match.group(0).replace("\n", " ")[:90]
|
||||||
|
findings.append(
|
||||||
|
f"{SHELL_PATH}:{line}: [{rule.code}] {rule.message} ({snippet})"
|
||||||
|
)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
findings: list[str] = []
|
findings: list[str] = []
|
||||||
for template_path in TEMPLATE_PATHS:
|
for template_path in TEMPLATE_PATHS:
|
||||||
findings.extend(scan_file(Path(template_path)))
|
findings.extend(scan_file(Path(template_path)))
|
||||||
findings.extend(scan_css())
|
findings.extend(scan_css())
|
||||||
|
findings.extend(scan_shell())
|
||||||
|
findings.extend(scan_required_snippets(BASE_PATH, REQUIRED_BASE_SNIPPETS, "base/topbar"))
|
||||||
|
|
||||||
if findings:
|
if findings:
|
||||||
print("Observability UI guard: FAIL")
|
print("Observability UI guard: FAIL")
|
||||||
@@ -144,6 +212,8 @@ def main() -> int:
|
|||||||
print("Observability UI guard: PASS")
|
print("Observability UI guard: PASS")
|
||||||
print(f"- templates checked: {len(TEMPLATE_PATHS)}")
|
print(f"- templates checked: {len(TEMPLATE_PATHS)}")
|
||||||
print(f"- css guardrails checked: {len(REQUIRED_CSS_SNIPPETS)}")
|
print(f"- css guardrails checked: {len(REQUIRED_CSS_SNIPPETS)}")
|
||||||
|
print(f"- sidebar/nav guardrails checked: {len(REQUIRED_SHELL_SNIPPETS)}")
|
||||||
|
print(f"- base/topbar guardrails checked: {len(REQUIRED_BASE_SNIPPETS)}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user