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 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 359683@bnt-lap001
Assistant-Session: eebdc939-7a9b-4e50-9d39-c8437e8a14ec
This commit is contained in:
tegwick 2026-09-21 22:07:08 +02:00
parent 39e5366abc
commit bd6c004f1a
5 changed files with 100 additions and 3 deletions

View file

@ -120,3 +120,55 @@ def review_page(page, session):
+ f'<aside class="card"><h2>What this act covers</h2><dl>{facts}<dt>Person being bound</dt>' + f'<aside class="card"><h2>What this act covers</h2><dl>{facts}<dt>Person being bound</dt>'
f'<dd>{text(memo.binding.principal.display_name)} · {text(session.subject)}</dd></dl>{evidence}</aside></div>') f'<dd>{text(memo.binding.principal.display_name)} · {text(session.subject)}</dd></dl>{evidence}</aside></div>')
return document(memo.question, content, session.subject) 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'<div class="record"><strong>{text(row.memo_id)}</strong>'
f'<p class="muted">{text(reason)}</p></div>')
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'<li>{text(d.verb.value.capitalize())} · version {d.memo_version} · {text(d.at)}'
+ (f' · {text(s["state"])}' if s else '') + '</li>' for d, s in row.history)
return (f'<div class="record"><a href="{link}"><strong>{text(row.memo.question)}</strong></a>'
f'<p class="muted">{" · ".join(facts)}</p>'
+ (f'<details><summary>Your responses</summary><ul>{history}</ul></details>' if history else '')
+ '</div>')
def overview_section(rows):
if not rows:
return '<h1>Your decisions</h1><p>No decision memos are addressed to you.</p>'
sections = ''
for group, (label, explanation) in OVERVIEW_GROUPS.items():
members = [r for r in rows if r.group == group]
if members:
sections += (f'<section class="card"><h2>{label} ({len(members)})</h2>'
f'<p class="muted">{explanation}</p>' + ''.join(map(_overview_row, members)) + '</section>')
return '<h1>Your decisions</h1>' + sections

View file

@ -18,7 +18,7 @@ from .policy import PolicyError
from .provenance import HumanControlNotDischargeable from .provenance import HumanControlNotDischargeable
from .review import ReviewError from .review import ReviewError
from .store import Conflict, EvidenceUnavailable, StoreError 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" FLOW_COOKIE = "__Host-infd-flow"
SESSION_COOKIE = "__Host-infd-session" SESSION_COOKIE = "__Host-infd-session"
@ -188,7 +188,8 @@ class App:
if method == "GET" and path == "/": if method == "GET" and path == "/":
if session: if session:
content = (f"<p>Signed in as {html.escape(session.subject)}.</p>" content = (f"<p>Signed in as {html.escape(session.subject)}.</p>"
+ ('<h1>Open a decision review</h1><p>Enter the memo identifier supplied with your review request.</p>' + (overview_section(self.review.overview(session)) if self.review is not None else '')
+ ('<h2>Open a decision review</h2><p>Enter the memo identifier supplied with your review request.</p>'
'<form method="get" action="/review"><label>Memo identifier <input name="memo_id" required maxlength="256"></label>' '<form method="get" action="/review"><label>Memo identifier <input name="memo_id" required maxlength="256"></label>'
'<button type="submit">Open review</button></form>' if self.review is not None else '<button type="submit">Open review</button></form>' if self.review is not None else
'<p>Decision review is being prepared. No approval has been recorded.</p>') + '<p>Decision review is being prepared. No approval has been recorded.</p>') +

View file

@ -34,6 +34,10 @@ try {
const session=cookies.find(c=>c.name==='__Host-infd-session'); const session=cookies.find(c=>c.name==='__Host-infd-session');
assert(session?.secure && session?.httpOnly && session?.sameSite==='Lax'); assert(session?.secure && session?.httpOnly && session?.sameSite==='Lax');
checks.push('PKCE callback and secure browser session'); 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.getByLabel('Memo identifier').fill(fixture.memo_id);
await page.getByRole('button',{name:'Open review',exact:true}).click(); await page.getByRole('button',{name:'Open review',exact:true}).click();
await page.getByRole('heading',{name:'The request',exact:true}).waitFor(); await page.getByRole('heading',{name:'The request',exact:true}).waitFor();

View file

@ -367,3 +367,43 @@ def test_overview_requires_a_live_human_session(review):
with pytest.raises(ReviewError) as error: with pytest.raises(ReviewError) as error:
controller.overview(replace(session,expires_at=0)) controller.overview(replace(session,expires_at=0))
assert error.value.code=='session_expired' 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 '<a href="/review?memo_id=memo-1"><strong>Approve the synthetic factory delivery?</strong></a>' 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 '<li>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='<img src=x onerror=alert(1)>'))
r=home(review)
assert '<img src=x' not in r['body'] and '&lt;img src=x onerror=alert(1)&gt;' in r['body']
controller.policy.transport.change=lambda d:d.update(effect='deny')
r=home(review)
assert r['status']==200 and 'Not available (2)' in r['body']
assert 'The permission service refused access to this memo.' in r['body']
assert 'Approve the synthetic factory delivery?' not in r['body'] and 'onerror' not in r['body']
def test_home_without_session_shows_sign_in_only(review):
controller,session,app,*_=review
r=call(app,'/')
assert 'Sign in with KeyCape' in r['body'] and 'Your decisions' not in r['body']
assert controller.store.policy_observations()==[]

View file

@ -95,7 +95,7 @@ policy unavailable, engine down, and each classification.
```task ```task
id: INFD-WP-0003-T03 id: INFD-WP-0003-T03
status: todo status: done
priority: high priority: high
state_hub_task_id: "b0dfae8b-e8f8-5257-b275-8d486a067c6b" state_hub_task_id: "b0dfae8b-e8f8-5257-b275-8d486a067c6b"
``` ```