diff --git a/src/opal/web/routes.py b/src/opal/web/routes.py
index e083c2e..f1dfc6b 100644
--- a/src/opal/web/routes.py
+++ b/src/opal/web/routes.py
@@ -2903,7 +2903,6 @@ def risks_table(
rows = [
{
"risk": r,
- "badge": DISPOSITION_BADGES.get(r.disposition, "draft"),
"reviewed_age": _relative_age(r.last_reviewed_at) if r.last_reviewed_at else "—",
"reviewed_iso": r.last_reviewed_at.strftime("%Y-%m-%dT%H:%M:%SZ")
if r.last_reviewed_at
@@ -2974,8 +2973,11 @@ def risks_new(request: Request, db: DbSession) -> HTMLResponse:
@router.get("/risks/{risk_id}", response_class=HTMLResponse)
-def risks_detail(request: Request, db: DbSession, risk_id: int) -> HTMLResponse:
- """Risk detail page — the generated statement is the masthead."""
+def risks_detail(
+ request: Request, db: DbSession, risk_id: int, edit: bool = Query(False)
+) -> HTMLResponse:
+ """Risk detail page — the generated statement is the masthead. Opens
+ read-only; ?edit=1 renders the in-place editors, DONE returns to view."""
from opal.risks.lint import lint_risk_row
from opal.risks.readiness import readiness
from opal.web.lint_markup import statement_lint_html
@@ -2991,15 +2993,14 @@ def risks_detail(request: Request, db: DbSession, risk_id: int) -> HTMLResponse:
findings = lint_risk_row(risk)
linked_issue_ids = [link.issue_id for link in risk.issue_links]
- parts = (
- db.query(Part).filter(Part.deleted_at.is_(None)).order_by(Part.name).limit(200).all()
- )
+ parts = db.query(Part).filter(Part.deleted_at.is_(None)).order_by(Part.name).limit(200).all()
# The dropdown is capped; the set asset must still render as selected.
if risk.asset_part is not None and risk.asset_part not in parts:
parts.append(risk.asset_part)
context = get_base_context(request, db, f"{risk.risk_number} - OPAL")
context["risk"] = risk
+ context["editing"] = edit
context["badge"] = DISPOSITION_BADGES.get(risk.disposition, "draft")
context["dispositions"] = [d.value for d in RiskDisposition]
context["roles"] = [r.value for r in RiskIssueRole]
@@ -3021,7 +3022,9 @@ def risks_detail(request: Request, db: DbSession, risk_id: int) -> HTMLResponse:
@router.get("/risks/{risk_id}/acceptance-panel", response_class=HTMLResponse)
-def risks_acceptance_panel(request: Request, db: DbSession, risk_id: int) -> HTMLResponse:
+def risks_acceptance_panel(
+ request: Request, db: DbSession, risk_id: int, edit: bool = Query(False)
+) -> HTMLResponse:
"""Acceptance panel partial — re-fetched after field saves."""
from opal.risks.readiness import readiness
@@ -3033,6 +3036,7 @@ def risks_acceptance_panel(request: Request, db: DbSession, risk_id: int) -> HTM
{
"request": request,
"risk": risk,
+ "editing": edit,
"readiness": readiness(db, risk),
"current_user": _get_current_user(request, db),
},
diff --git a/src/opal/web/static/js/risks.js b/src/opal/web/static/js/risks.js
index 1240983..51b646c 100644
--- a/src/opal/web/static/js/risks.js
+++ b/src/opal/web/static/js/risks.js
@@ -88,7 +88,8 @@ async function refreshAcceptancePanel() {
// confirm dialog mid-signature.
const confirmEl = document.getElementById('accept-confirm');
const confirmWasOpen = confirmEl && confirmEl.style.display !== 'none';
- const r = await fetch(`/risks/${riskId}/acceptance-panel`);
+ const editing = typeof EDITING !== 'undefined' && EDITING;
+ const r = await fetch(`/risks/${riskId}/acceptance-panel${editing ? '?edit=1' : ''}`);
if (r.ok) container.innerHTML = await r.text();
if (confirmWasOpen && document.getElementById('accept-confirm')) openAcceptConfirm();
} catch (e) { /* panel refresh is cosmetic; the next save retries */ }
diff --git a/src/opal/web/templates/risks/_acceptance_panel.html b/src/opal/web/templates/risks/_acceptance_panel.html
index bd66b6d..23c7216 100644
--- a/src/opal/web/templates/risks/_acceptance_panel.html
+++ b/src/opal/web/templates/risks/_acceptance_panel.html
@@ -1,48 +1,35 @@
-{# Acceptance readiness panel — mirrors requirements/_baseline_panel.html and
- reuses its CSS. Rendered server-side from opal.risks.readiness, re-fetched
- via GET /risks/{id}/acceptance-panel after field saves. All checks are
- hard — acceptance is a signature. #}
-
-
ACCEPTANCE READINESS
- {% for check in readiness.checks %}
-
- [{{ 'x' if check.passed else ' ' }}]
- {{ check.label }}
- {% if not check.passed and check.detail %}
- ← {{ check.detail }}
- {% endif %}
-
- {% endfor %}
-
──────────────────────────
- {% if risk.disposition == 'accepted' %}
+{# Acceptance — the ACCEPT button carries the gate: grayed until every hard
+ check passes, the tooltip names what is missing (opal.risks.readiness is
+ the one source; the gate and accept() cannot drift). Re-fetched via
+ GET /risks/{id}/acceptance-panel?edit=… after field saves. Signature
+ facts (ACCEPTED · RATIONALE) live in the detail rail once signed. #}
+{% if risk.disposition not in ('accepted', 'realized') %}
+{% if editing %}
+
+
+
+
+{% set short = {'scenario_complete': 'scenario', 'owner_assigned': 'owner', 'scored': 'score', 'rationale_recorded': 'rationale', 'lint_clean': 'lint'} %}
+{% set failing = readiness.checks | rejectattr('passed') | map(attribute='key') | list %}
+
+