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:
parent
39e5366abc
commit
bd6c004f1a
5 changed files with 100 additions and 3 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>') +
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 '<img src=x onerror=alert(1)>' 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()==[]
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue