Repository navigation
feat(portal): one swipe card per decision for paused runs and saves - #212
Merged
Merged
Conversation
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.
Replaces the text-heavy card with one card per decision, dark first with a light theme. Buttons stay the primary way to answer; swiping right or left is a shortcut for the same two answers. Answers are held for five seconds with Undo and nothing is queued offline. An irreversible step arms on swipe and needs a matched fact and a 1.2 s hold. Saves show presentation.fields when Flow sends them and the screenshot otherwise; Save is offered only after the frame is a PNG whose SHA-256 matches screenshot_sha256. Also fixes the save-approval bugs from #209: a bare 404 means the computer isn't attending (only Flow's closed codes mean gone), 401 and 403 go back to pairing, receipts use the receipt's decided_at, and the frame is checked against its digest. POST bodies are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Don't merge yet. Richard reviews the look first.
Supersedes #208 and #209 (left open for reference). Includes #207's save-approval relay (fast-forwarded in), so the diff shows it until #207 merges. Flow's save routes come from OpenAdaptAI/OpenAdapt#1234;
presentation.fieldscomes from a separate small Flow PR. Against an older Flow the phone shows the screenshot instead of fields.What changes
The phone shows one card per decision, for paused runs and for saves waiting for approval. It's built from the approved prototype.
risk_class: irreversible, a swipe or tap only arms. The person taps one fact from the card (a date first, then any value with a digit, else the step number) and holds for 1.2 s. There's no undo after the hold.presentation.fieldsin large monospace type, with replaced values struck through. Without fields, the card shows the screenshot itself. A malformed list is dropped whole.EMPHASIS_ENABLED): the single continuing answer is filled only when the step can be undone.--accent-verified/--inset-ok, because the palette test requires canonical values.#209 bugs fixed
404no longer means "gone" or "no saves". Only Flow's closed codes (request_not_waiting,request_unknown,already_answered) mean gone. Any other 404 shows "not attended", and a 404 save list makes the empty screen say saves aren't reaching the phone.401and403(including a refused CSRF or origin on an answer) go back to pairing.decided_at, not the phone's clock.image/pngwith a PNG signature and a SHA-256 equal topresentation.screenshot_sha256. If not, there's no picture and no approval, though Don't save still works.request_expiredis matched exactly instead of with a regex.Contract, unchanged
Bearer token on every call. POSTs carry Origin, JSON content type and
X-OpenAdapt-Portal-CSRF, and the phone refuses to send a body over 16 KiB. Save answers are exactlyaction,task_digest,task_signature, plusdecline_reason(wrong_values,wrong_record,screen_problem) on reject. Paused runs keep their/api/portal/tasksroutes and their six-field body. Only signed allowed actions are shown, and nothing goes into notifications.Screens (synthetic data, 390x844)
All 29 states in each theme: dark, light. The individual files are on the
pr-assets/portal-swipe-cardsbranch, which isn't for merging.scripts/capture_portal_scenarios.pynow drives the real shell through a stubbed transport with real taps, drags and holds, and writes every state in dark and light at 390x844. The form in the screenshot is a synthetic page the script renders.Tests
src/portalShell.test.tswas rewritten (52 tests) on the realindex.htmlwith fake timers. It covers the exact save and run bodies and headers, undo, offline drop, expiry during the hold, the 16 KiB refusal, closed decline reasons, fields vs. screenshot fallback, digest mismatch, non-PNG, swipe and keyboard, match and hold (short hold sends nothing),decided_at, every 404 case, 401 and 403 to pairing, reconciliation proof, halted, uncertain, unknown and legacy receipts.tests/test_portal/test_shell.py: no inlinestyle=(CSP), reduced motion, 44 px targets, dark by default.tests/test_portal/test_shell_palette.py: dark and light tokens against canonical tokens, plus every literal hex is canonical.pytest tests/1397 passed, 2 skipped.npm run test:ui371 passed.tsc --noEmitclean.ruff checkclean.🤖 Generated with Claude Code