From bd6c004f1a0c2ac017d203856c73bcaa589fe88e Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 21 Sep 2026 22:07:08 +0200 Subject: [PATCH] Render the my-decisions overview on the signed-in home page (INFD-WP-0003 T03) Groups the reviewer's memos (needs attention, open, accepted, declined, returned, closed, not available) above the manual memo-id form. Server rendered and escaped; refused rows show only the memo id. The Chromium harness gains a home-overview check (13 checks pass). Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 359683@bnt-lap001 Assistant-Session: eebdc939-7a9b-4e50-9d39-c8437e8a14ec --- informed_decision/ui.py | 52 +++++++++++++++++++ informed_decision/web.py | 5 +- tests/browser_review.mjs | 4 ++ tests/test_review_controller.py | 40 ++++++++++++++ .../INFD-WP-0003-my-decisions-overview.md | 2 +- 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/informed_decision/ui.py b/informed_decision/ui.py index 998ba66..a57321b 100644 --- a/informed_decision/ui.py +++ b/informed_decision/ui.py @@ -120,3 +120,55 @@ def review_page(page, session): + f'') return document(memo.question, content, session.subject) + + +OVERVIEW_GROUPS = { + "attention": ("Needs attention", "An approval entry may have reached Approval Engine without confirmation. Do not submit again; open the original record."), + "open": ("Open for you", "These memos await your response."), + "accepted": ("Accepted", "Approval Engine confirmed your approval entry. An approval does not authorize execution."), + "declined": ("Declined", "You declined these memos."), + "returned": ("Returned or in discussion", "You returned these memos or asked for discussion. A revised version reopens the question."), + "closed": ("Closed without your response", "The approval closed before you responded."), + "unavailable": ("Not available", "These memos are addressed to you but cannot be shown now."), +} +OVERVIEW_REASONS = { + "policy_denied": "The permission service refused access to this memo.", + "renderer_changed": "This memo names an earlier review interface.", + "unsupported_review_profile": "This memo requires a different review profile.", + "missing_act_binding": "This memo is not bound to an approval.", + "binding_changed": "The approval no longer matches this memo.", + "engine_unavailable": "Approval status is unavailable. The review checks it again before you act.", +} + + +def _overview_row(row): + link = "/review?memo_id=" + quote(row.memo_id, safe="") + if row.memo is None: + reason = OVERVIEW_REASONS.get(row.reason, "The permission check could not complete. Reload later.") + return (f'
{text(row.memo_id)}' + f'

{text(reason)}

') + facts = [f'Memo {text(row.memo_id)} · version {row.memo.version}'] + if row.engine_status: + facts.append(f'Approval status: {text(row.engine_status)}') + elif row.reason: + facts.append(text(OVERVIEW_REASONS.get(row.reason, "Approval status is unavailable."))) + if row.intent and row.intent.get("approved_at"): + facts.append(f'Entry recorded {text(row.intent["approved_at"])}') + history = ''.join(f'
  • {text(d.verb.value.capitalize())} · version {d.memo_version} · {text(d.at)}' + + (f' · {text(s["state"])}' if s else '') + '
  • ' for d, s in row.history) + return (f'
    {text(row.memo.question)}' + f'

    {" · ".join(facts)}

    ' + + (f'
    Your responses
      {history}
    ' if history else '') + + '
    ') + + +def overview_section(rows): + if not rows: + return '

    Your decisions

    No decision memos are addressed to you.

    ' + sections = '' + for group, (label, explanation) in OVERVIEW_GROUPS.items(): + members = [r for r in rows if r.group == group] + if members: + sections += (f'

    {label} ({len(members)})

    ' + f'

    {explanation}

    ' + ''.join(map(_overview_row, members)) + '
    ') + return '

    Your decisions

    ' + sections diff --git a/informed_decision/web.py b/informed_decision/web.py index 0b3d211..6730947 100644 --- a/informed_decision/web.py +++ b/informed_decision/web.py @@ -18,7 +18,7 @@ from .policy import PolicyError from .provenance import HumanControlNotDischargeable from .review import ReviewError from .store import Conflict, EvidenceUnavailable, StoreError -from .ui import STYLES, document, error_page, review_page +from .ui import STYLES, document, error_page, overview_section, review_page FLOW_COOKIE = "__Host-infd-flow" SESSION_COOKIE = "__Host-infd-session" @@ -188,7 +188,8 @@ class App: if method == "GET" and path == "/": if session: content = (f"

    Signed in as {html.escape(session.subject)}.

    " - + ('

    Open a decision review

    Enter the memo identifier supplied with your review request.

    ' + + (overview_section(self.review.overview(session)) if self.review is not None else '') + + ('

    Open a decision review

    Enter the memo identifier supplied with your review request.

    ' '
    ' '
    ' if self.review is not None else '

    Decision review is being prepared. No approval has been recorded.

    ') + diff --git a/tests/browser_review.mjs b/tests/browser_review.mjs index 153837b..e176ba7 100644 --- a/tests/browser_review.mjs +++ b/tests/browser_review.mjs @@ -34,6 +34,10 @@ try { const session=cookies.find(c=>c.name==='__Host-infd-session'); assert(session?.secure && session?.httpOnly && session?.sameSite==='Lax'); checks.push('PKCE callback and secure browser session'); + await page.getByRole('heading',{name:/^Open for you \(1\)$/}).waitFor(); + assert.equal(await page.locator('a[href="/review?memo_id='+encodeURIComponent(fixture.memo_id)+'"]').count(),1); + assert.equal(await page.locator('img,script').count(),0); + checks.push('Home overview lists the open memo addressed to the reviewer'); await page.getByLabel('Memo identifier').fill(fixture.memo_id); await page.getByRole('button',{name:'Open review',exact:true}).click(); await page.getByRole('heading',{name:'The request',exact:true}).waitFor(); diff --git a/tests/test_review_controller.py b/tests/test_review_controller.py index 004b8e9..eda21b9 100644 --- a/tests/test_review_controller.py +++ b/tests/test_review_controller.py @@ -367,3 +367,43 @@ def test_overview_requires_a_live_human_session(review): with pytest.raises(ReviewError) as error: controller.overview(replace(session,expires_at=0)) assert error.value.code=='session_expired' + + +def home(review): + return call(review[2],'/',cookie=SESSION_COOKIE+'=fixture-session') + + +def test_home_lists_open_then_accepted_memos_and_keeps_the_manual_form(review): + controller,session,app,memo,engine,transport=review + r=home(review) + assert r['status']==200 and 'Open for you (1)' in r['body'] and 'Accepted (' not in r['body'] + assert 'Approve the synthetic factory delivery?' in r['body'] + assert 'Approval status: requested' in r['body'] and 'Open a decision review' in r['body'] + assert 'private-brief-sentinel' not in r['body'] and 'private-packet-sentinel' not in r['body'] + assert len(controller.store.evidence())==0 + p=opened(review).presentation;controller.acknowledge(session,p.id,['h-1']) + controller.act(session,p.id,Verb.ACCEPT,operation_id=str(uuid.uuid4())) + r=home(review) + assert 'Accepted (1)' in r['body'] and 'Open for you' not in r['body'] + assert 'Approval status: approved' in r['body'] and 'Entry recorded ' in r['body'] + assert '
  • Accept · version 1 · ' in r['body'] and 'confirmed' in r['body'] + + +def test_home_escapes_memo_text_and_redacts_refused_rows(review): + controller,session,app,memo,engine,transport=review + hostile=add_memo(review,'hostile') + controller.store.save_memo(replace(hostile,version=2,question='')) + r=home(review) + assert '