Repository navigation
dashboard: /stand-card Stand Card page (re-land of #3765 on #3604's head, with the script-escape fix) - #4033
qingyun-wu wants to merge 8 commits into
Conversation
… data Vendors the owner's reference (sutando-stand-card_1.html) as src/dashboard-stand-card.html with two additive patches: an adaptStandJson() bridge from the current `bin/sutando stand --json` shape (stand_id / owners[] / evidence blocks) to the card's VIEW shape, and a source badge that says live vs sample. The /stand-card route runs the CLI with a 6s timeout and injects the JSON; on any failure the page falls back to its baked Aug-23 sample and labels itself so — the route never 500s on a dead daemon. (cherry picked from commit fadd9f1499aee0f72a3b2b7a5bb524f514e76410)
The matrix, CLI-gaps and JSON-view sections were the mockup's prototype commentary; the served card now ends at Resolve. Their render JS and the introducing divider go with them. (cherry picked from commit 96763bc9a0b596f93d04215e5e80bf05238715df)
Owner request: the Stand-proof note and Snapshot chips leave the rail; a Workers block lists the four named workers (name, seat, runtime, live dot), from window.SUTANDO_WORKERS when injected, baked names otherwise. (cherry picked from commit 9eaf35e7d5a6539a21fc836d4c8b6a9c8c3080b8)
…emoved Owner iteration: workers render as a full inspector section in the Channels row grammar rather than a rail block; the Devices section, its tile renderer and its tally slot go with it. (cherry picked from commit c40c3163a2dce74b149599fcc4275fce5f08b5bf)
Workers -> Owner -> Channels; Resolve section + demo resolver removed. Worker rows open the drawer; a stand-profile postMessage from the hosting client applies names, avatar, base color and About from the identity document, and the avatar tap posts an edit-profile hop back up — the card renders, the client owns writes. (cherry picked from commit d9dc420c55995ecc96674787f63c21249fe59001)
Avatar tap asks the host to open a picker (host owns the API write) and toasts upload status; style/decorators/DM chips render from the posted identity; the visibility switcher, its wiring and the channel advisory are removed; profile posts merge instead of replace. (cherry picked from commit 64ae7d4d253c29269af36e718efefe1f3bb17346)
`json.dumps` leaves `/` alone, so a `</script>` inside any stand field ended the inlined <script> element early and the rest of the value ran as markup. `<\/` is the same JSON to the parser, so the page receives an identical payload with no token that can close the element. Adds the route's first test: the escape, the two fallback paths (runtime missing, error object), and /stand-card through the real handler.
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 88%. Diff CoverageDiff: origin/rescue/pool-uncommitted-2026-08-26...HEAD, staged and unstaged changes
Summary
|
…ling thread The coverage gate reported the route branch uncovered even though the loopback round trip exercised it: its tracer did not follow the server thread. Serving one request through Handler on an in-memory socket runs the same lines on the test's own thread, so they count under any core. Also covers the 404 when the page file is absent.
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested at 96c2a032. COMMENT is used because this review account is also the author. The JSON </script> escape is repaired, but the complete new page has three blockers:
-
[P1] Authenticate profile messages and avoid the avatar HTML sink —
src/dashboard-stand-card.html:794–803. The message listener acceptsstand-profilefrom any origin/source, then interpolatesprofile.avatarHttpintoinnerHTML. I loaded the actual page HTML produced bystand_card_response()in Chrome, framed it from a different origin, and sentavatarHttp: 'x" onerror="document.body.dataset.reviewXss=location.origin'. The frame's marker changed from absent to its own dashboard origin. No owner interaction or backend mutation was needed. Restrict messages to the trusted hosting client (source AND configured origin) and assign a validated avatar URL through DOM properties. The server's new script escape does not cover this second sink. Add a foreign-origin rejection test and an accepted-profile hostile-URL test. -
[P1] Do not report verification/link/revocation without performing it —
src/dashboard-stand-card.html:845–876. With injected live channel data explicitly lacking a credential, clicking Verify and then approving Link produces✓ 1 verified entrance; Revoke then showsRevoked. Chrome recorded zero network requests during those actions. These handlers only changeVIEWand fabricate verification time/subject. On a page labeled live state, an owner can believe access was revoked while durable authorization is untouched. Bind the actions to the real governed APIs and update from their results, or disable them until implemented. Test success, refusal, and persistence after reload. -
[P2] Render live identity and workers instead of mixing in the sample —
src/dashboard-stand-card.html:313–325,521–529,552–563. I supplied stand@review-fixture:example.test, display nameReview Stand, inactive status, and ownerFixture Owner. The page announces live state but its main card still says Sutando / Qingyun Wu /@sutando-qingyun-001:ag2.space, Active, and four sample workers all active. The copy/QR code uses the supplied ID while the printed QR URIs remain the sample. Populate every visible identity/share field from one validated view; show unreported/empty workers until actual worker data is available. Add a non-sample identity and empty-worker browser case.
Browser proof used actual production-rendered HTML with only the runtime JSON response substituted, two distinct HTTP origins, and all network requests intercepted; it touched no live services or accounts. Output:
input Review Stand / Fixture Owner -> card Sutando / Qingyun Wu; four sample active workers
verify + link -> 1 verified entrance; requests=0
revoke -> Verified / Revoked; requests=0
foreign-origin avatar message -> marker=http://127.0.0.1:19401
Reviewed the full three-file child diff and surrounding dashboard mutation gates, response routing, CLI identity source, and frontend interactions. The parent #3604 head 6c0b416e is an ancestor. All 13 dashboard suites pass; the late 96c2a032 commit adds test-only 404/inline-handler coverage, and its expanded suite passes. Production HTML/server code is unchanged from the browser-tested ae460603. Added-path/root/Python-prose/whitespace checks pass with a forbidden-path positive control; the scan does not catch the clone-specific ~/Documents/github/sutando/... strings in the new adapter, which should also be derived from the runtime rather than guessed.
Worst-case disruption is attacker-controlled execution in the dashboard origin plus misleading authorization and identity state. Those are blocking despite the green route tests. Latest CI is still running on the new head. Keep merge order #3604 then this PR, rebase/recheck after the parent lands, and provide the required real deployment/restart witness before approval. No live restart or provider send was performed for this review.
|
CLA check did not register on 96c2a03 (SHA-bound stall); close+reopen to retrigger it. |
|
Closing per owner: the stand card is being removed. Branch deleted; commits remain reachable through this PR's history. |
Re-lands the Stand Card dashboard from #3604's local line as its own reviewable PR, stacked on
rescue/pool-uncommitted-2026-08-26(base =6c0b416e, #3604's current head). Supersedes #3765, which carried the same six commits on an older base and was closed after @sonichi's review asked for one change before landing.What is in here
Six original stand-card commits, cherry-picked with
-xin order (tree identical to #3765's tipcfea8948d;git diff cfea8948d fcd49c7b2 -- src/dashboard.py src/dashboard-stand-card.htmlis empty), plus two new commits — the review fix, and a coverage follow-up after the gate's first run reported the route branch uncovered because its tracer did not follow the loopback server thread (the same lines are now also driven on the calling thread; locally every changed line reports covered):10e49e052dashboard: /stand-card serves the owner's Stand Card design with live dataa3638fafestand-card: drop the three design-notes sections (owner request)f0067be5astand-card: rail swaps proof/snapshot blocks for a workers rostere9e5d8574stand-card: Workers inspector section replaces Devices; rail roster removedfe5c9f290stand-card: reorder sections, worker drawers, live identity intakefcd49c7b2stand-card: in-place avatar pick, identity chips, owner-only viewae4606032stand-card: escape</in the JSON inlined into the page script ← new96c2a0326test(stand-card): cover the 404 path and drive /stand-card on the calling thread ← newDeliberately not included: the pin-honoring leaderless fallback (
3726b6d1+ follow-ups) anddocs/interaction-semantics.md— the former sits in the per-worker loop #3604 has been asked to remove; the latter documents the A2UI interaction vocabulary, not this page.The review finding (#3765) and its fix
Before (
fcd49c7b2,src/dashboard.py:891) — a</script>inside any stand field closes the inlined element:After (
ae4606032):<\/is the same JSON to the parser, so the page receives an identical payload; there is no longer a token that can end the script element. Unconditional, not gated on which fields are attacker-influenced today (per the review).Evidence at HEAD
python3 tests/dashboard-stand-card-escape.test.py(new; covers the escape, both fallback paths, and/stand-cardthrough the realHandler— the lines the #3765 coverage gate listed as uncovered):All 13
tests/dashboard-*.test.pysuites pass on this head. Repo gate on the cumulative diff vs6c0b416e:Not addressed here
The
except Exception: passnit from the same review (a live-data outage leaves no trace) is left as-is — it was explicitly a nit, and the page labels itselfsample snapshoton fallback, which the review confirmed holds.Stacked on #3604; merge order: #3604 first, then this.
🤖 Generated with Claude Code