Skip to content

feat(portal): approve a save with one tap on the phone - #209

Closed
abrichr wants to merge 9 commits into
mainfrom
feat/phone-save-approval
Closed

abrichr wants to merge 9 commits into
mainfrom
feat/phone-save-approval

Conversation

@abrichr

@abrichr abrichr commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Do not merge yet. Richard reviews the look first. Stacked on #208 (phone decision card) and #207 (save-approval relay, merged in). It targets main so CI runs, which means the diff also shows those two PRs. Merge them first; then this diff is only the save-approval screens. Depends on Flow's save-approval console routes in OpenAdaptAI/OpenAdapt#1234 (merge freeze). Against an older console the save routes return 404, and the phone treats that as "no saves".

What changes

The approvals design is final: a workflow is admitted once, by a person or by a delegated agent under a policy a person signed, and after that runs stop only on problems. Approving every save stays as the strictest setting. This PR lets the paired phone answer that save with one tap, using the same card as a paused run.

  • Waiting saves appear in Needs you next to paused runs. A single one opens by itself.
  • Card: "Approve a save · Step 5", the stakes from risk_class, Save this form?, the workflow's step name, the exact screenshot ("Exactly what will be saved", zoomable), what happens next, then the answers.
  • Approve and save is one tap. It is filled under the same rule as a halt: not when the save cannot be undone.
  • Don't save asks one more question, "Why shouldn't it save?", and offers only Flow's closed reasons that the phone has words for. There is no free text.
  • No picture, no approval: if the exact screenshot fails to load or is not a PNG, Approve is not offered.
  • The POST body is exactly action, task_digest, task_signature, plus decline_reason on a decline. Flow forbids extra fields, and the payload matches them.
  • Receipts: Approved: saving after one more check / Nothing was saved, each with the answer, the reason, "On this phone" and the time, and the decision digest prefix. request_not_waiting, request_unknown and already_answered show "This save is no longer waiting". task_changed shows "This save changed" and offers to look again. Expired requests open as expired. Offline sends nothing.
  • The empty state now explains when the phone is asked: "Runs stop here only when something needs a person, or before a save if you asked to approve each one."

Fail-closed rules are unchanged: only signed allowed actions are offered, the digest and signature are echoed, Flow writes the answer once and the run re-reads the form before saving, expiry is enforced, nothing is queued offline, and no PHI goes into notifications. No contract fields were added.

Screens

The generator now renders save approvals as real shell states, not previews: request, cannot be undone, no picture, decline reasons, sending, approved, declined, no longer waiting, changed, not accepted, and connection lost, each in light and dark. The docs set adds save-approval and save-approved in both schemes. Business decision is still the only preview.

Tests

  • src/portalShell.test.ts: 12 new tests (one-tap payload, decline reasons closed and filtered, no picture means no approve, irreversible means no emphasis, gone, changed, expired, offline, wrong task_kind, older runner without save routes)
  • tests/test_portal/test_phone_captures.py: the save fixtures match Flow's task, and save screens are shell states rather than previews
  • Local: pytest tests/ 1399 passed, 1 skipped · npm run test:ui 371 passed · npm run build ok · ruff check clean

abrichr and others added 8 commits October 17, 2026 23:16
Allowlist Flow's /api/save-approvals routes (list, signed task detail, exact
screenshot, decision) behind /api/portal/save-approvals with the same session,
origin and CSRF checks as attended decisions. Desktop forwards the phone's
answer verbatim and adds no authority. Contract documented in
docs/DECISION_PORTAL.md for the portal UI work to build on.
@abrichr
abrichr changed the base branch from feat/phone-decision-card to main October 18, 2026 03:59
@abrichr

abrichr commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Closing: superseded by #212, merged.

@abrichr abrichr closed this Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant