diff --git a/src/generate_repo_overview/_policy_sync_html.py b/src/generate_repo_overview/_policy_sync_html.py
index d08f110..054343a 100644
--- a/src/generate_repo_overview/_policy_sync_html.py
+++ b/src/generate_repo_overview/_policy_sync_html.py
@@ -61,14 +61,14 @@ def render_policy_sync_section(
'
Evaluation status
\n'
' \n'
+ _policy_stat(summary.compliant, "Compliant", "compliant", "✓")
- + _policy_stat(summary.drifted, "Changes Needed", "changes-required", "!")
+ + _policy_stat(summary.drifted, "Changes Needed", "changes-required", "X")
+ _policy_stat(
summary.not_applicable, "Not Applicable", "not-applicable", "N/A"
)
+ _optional_policy_stat(
- summary.evaluation_failures, "Evaluation Errors", "error", "!"
+ summary.evaluation_failures, "Evaluation Errors", "error", "X"
)
- + _optional_policy_stat(summary.sync_failures, "Sync Failures", "error", "!")
+ + _optional_policy_stat(summary.sync_failures, "Sync Failures", "error", "X")
+ _optional_policy_stat(summary.skipped, "Skipped", "not-evaluated", "—")
+ "
\n"
" \n"
@@ -254,6 +254,10 @@ def _matrix_cell(outcome: PolicySyncOutcome | None) -> str:
if automated:
label = "Compliant (automated PR)"
marker = "✓✓"
+ if status_class == "changes-required" and outcome.policy_pr_status == "open":
+ link = _safe_external_url(outcome.pull_request_url or "")
+ if link:
+ return _policy_pr_badge(outcome.policy_pr_status, href=link)
content = (
f'{marker}'
@@ -306,13 +310,13 @@ def _status_display(outcome: PolicySyncOutcome) -> tuple[str, str, str]:
"pull-request-recreated",
"pull-request-recreated-no-changes",
}:
- return "changes-required", "Changes required", "!"
+ return "changes-required", "Changes required", "X"
if outcome.status == "not-applicable":
return "not-applicable", "Not applicable", "N/A"
if outcome.status in {"skipped", "sync-error"}:
return "not-evaluated", "Not evaluated", "—"
if outcome.status == "error":
- return "error", "Error", "!"
+ return "error", "Error", "X"
return "unknown", outcome.status or "Unknown", "?"
diff --git a/src/generate_repo_overview/templates/styles.css b/src/generate_repo_overview/templates/styles.css
index 12f0b41..7371ba2 100644
--- a/src/generate_repo_overview/templates/styles.css
+++ b/src/generate_repo_overview/templates/styles.css
@@ -504,7 +504,8 @@ th[data-tooltip]:focus::after, td[data-tooltip]:focus::after { opacity: 1; }
.policy-status.compliant { color: var(--green); background: #23863633; }
.policy-status.changes-required { color: var(--red); background: #da363333; }
.policy-status.not-applicable, .policy-status.not-evaluated { color: var(--muted); background: #8b949e22; }
-.policy-status.error, .policy-status.unknown { color: var(--orange); background: #d2992233; }
+.policy-status.error { color: var(--red); background: #da363333; }
+.policy-status.unknown { color: var(--orange); background: #d2992233; }
.policy-pr-badge {
display: inline-flex;
align-items: center;
@@ -518,7 +519,7 @@ th[data-tooltip]:focus::after, td[data-tooltip]:focus::after { opacity: 1; }
}
.policy-pr-badge:hover { text-decoration: none; filter: brightness(1.15); }
.policy-pr-badge svg { width: 12px; height: 12px; }
-.policy-pr-open { color: #3fb950; background: #2ea04333; border-color: #2ea04399; }
+.policy-pr-open { color: var(--orange); background: #e3702d33; border-color: #e3702d99; }
.policy-pr-merged { color: #a371f7; background: #8250df33; border-color: #8250df99; }
.policy-pr-closed { color: #8b949e; background: #6e778133; border-color: #6e778199; }
.policy-pr-none, .policy-pr-unknown { color: var(--muted); background: #8b949e22; border-color: #8b949e66; }
diff --git a/tests/test_policy_sync.py b/tests/test_policy_sync.py
index 4569aa8..64037e9 100644
--- a/tests/test_policy_sync.py
+++ b/tests/test_policy_sync.py
@@ -350,7 +350,7 @@ def test_policy_sync_tab_uses_repository_groups_and_pr_states() -> None:
'✓' in page
)
assert (
- '!'
+ 'X'
in page
)
assert (
@@ -371,6 +371,11 @@ def test_policy_sync_tab_uses_repository_groups_and_pr_states() -> None:
assert "Actions in this run" not in page
assert 'class="policy-pr-badge policy-pr-open"' in page
assert ">Open" in page
+ assert (
+ 'X ✓✓'
in page
@@ -382,6 +387,35 @@ def test_policy_sync_tab_uses_repository_groups_and_pr_states() -> None:
assert 'href="https://github.com/org/score/pull/3"' not in page
+def test_policy_sync_error_outcomes_render_red_x() -> None:
+ """Error outcomes use a red X in both the summary and matrix."""
+ report = PolicySyncReport(
+ schema_version=2,
+ summary=PolicySyncSummary(evaluation_failures=1),
+ outcomes=(
+ PolicySyncOutcome(
+ policy_id="policy-error",
+ repository="repo-error",
+ applicable="yes",
+ status="error",
+ error="evaluation failed",
+ ),
+ ),
+ )
+
+ page = render_index_page(_minimal_snapshot(), report)
+
+ assert (
+ 'X'
+ in page
+ )
+ assert (
+ 'X'
+ in page
+ )
+ assert ".policy-status.error { color: var(--red);" in page
+
+
def test_render_details_discovers_configured_report_and_publishes_raw_json(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: