diff --git a/services/ai_surface_html_readback_service.py b/services/ai_surface_html_readback_service.py
index db46154..b1fd223 100644
--- a/services/ai_surface_html_readback_service.py
+++ b/services/ai_surface_html_readback_service.py
@@ -416,8 +416,30 @@ def _route_sources_for_template(root: Path, template_name: str) -> list[str]:
def _evaluate_site_template(root: Path, path: Path) -> dict[str, Any]:
relpath = _relative_template_path(root, path)
- html = path.read_text(encoding="utf-8")
high_priority = _is_high_priority_template(relpath)
+ try:
+ html = path.read_text(encoding="utf-8")
+ except UnicodeDecodeError as exc:
+ return {
+ "template": relpath,
+ "family": _template_family(relpath),
+ "priority": "P1" if high_priority else "P2",
+ "status": "critical",
+ "route_sources": _route_sources_for_template(root, relpath),
+ "guardrails": {
+ "compact_marker_present": False,
+ "benchmark_marker_present": False,
+ "professional_marker_present": False,
+ },
+ "finding_count": 1,
+ "findings": [{
+ "type": "template_encoding_error",
+ "severity": "critical",
+ "encoding": "utf-8",
+ "error": str(exc)[:300],
+ }],
+ "next_machine_action": "build_sitewide_ui_ux_repair_package",
+ }
compact_marker_present = "data-density-guardrail=" in html
benchmark_marker_present = "data-benchmark-guardrail=" in html
professional_marker_present = any(marker in html for marker in PROFESSIONAL_GUARDRAIL_MARKERS)
@@ -569,6 +591,15 @@ def build_sitewide_ui_ux_repair_package(
"Add first-viewport status, next action, compact density marker, and evidence-on-demand pattern."
),
})
+ if finding.get("type") == "template_encoding_error":
+ actions.append({
+ "action": "normalize_template_encoding_utf8",
+ "target_template": surface.get("template"),
+ "safe_apply_hint": (
+ "Convert the template to UTF-8, preserve visible Traditional Chinese copy, "
+ "then rerun sitewide UI/UX inventory."
+ ),
+ })
repair_items.append({
"template": surface.get("template"),
"family": surface.get("family"),
diff --git a/tests/test_ai_automation_smoke_service.py b/tests/test_ai_automation_smoke_service.py
index e5dbe5f..9e3d231 100644
--- a/tests/test_ai_automation_smoke_service.py
+++ b/tests/test_ai_automation_smoke_service.py
@@ -394,6 +394,29 @@ def test_sitewide_ui_ux_repair_package_prioritizes_controlled_template_repairs()
assert package["repair_items"][0]["controlled_actions"]
+def test_sitewide_ui_ux_agent_reports_non_utf8_templates(tmp_path):
+ from services.ai_surface_html_readback_service import (
+ build_sitewide_ui_ux_agent_inventory,
+ build_sitewide_ui_ux_repair_package,
+ )
+
+ template_dir = tmp_path / "templates"
+ template_dir.mkdir()
+ (template_dir / "legacy_dashboard.html").write_bytes(b"\xa3 legacy dashboard")
+
+ inventory = build_sitewide_ui_ux_agent_inventory(root=tmp_path)
+ issue = inventory["issue_surfaces"][0]
+ package = build_sitewide_ui_ux_repair_package(source_inventory=inventory)
+
+ assert inventory["status"] == "warning"
+ assert issue["status"] == "critical"
+ assert issue["findings"][0]["type"] == "template_encoding_error"
+ assert package["repair_items"][0]["controlled_actions"][0]["action"] == (
+ "normalize_template_encoding_utf8"
+ )
+ assert package["safety"]["writes_database"] is False
+
+
def test_surface_html_readback_check_is_part_of_ai_smoke(monkeypatch):
from services import ai_automation_smoke_service as smoke