fix(ui): /m approval card sends the tapped option as the decision — select → note → explicit Send, alert Got it, real type labels; one respond-payload builder shared with desktop (#2370) - #2378
Open
webmixgamer wants to merge 3 commits into
Conversation
…elect → note → explicit Send, alert Got it, real type labels; one respond-payload builder shared with desktop (#2370) The mobile admin's queue card POSTed `{response: 'approved', response_text: <tapped option>}` for EVERY answer — so a Deny was recorded as an approval, a typed answer became a note, the card answered on one tap with no note and no explicit submit, and the type line read a nonexistent `request_type` (blank). The agent reads `response` as the decision (sync write-back + the ent#329 resume framing), so the field is load-bearing. Live artifact: the two cards answered on /m during the #1927 eyeball sit in the agent's queue file as `response='approved'`, `response_text='Approve'`. - `utils/operatorQueue.js` (new): the ONE home of the respond payload (`queueResponseBody`, `buildQueueResponse`), the controls-kind switch (`queueResponseKind`, by item TYPE — an approval with usable options → buttons, alert → Got it, everything else → text answer), the `optionsOf` normaliser (the DB stores any JSON the agent wrote), the shared type labels and the attribution-free 409/400/404 "no longer pending" copy + predicate. - `stores/operatorQueue.js`: builds its body through the shared builder (note trimmed, empty → null; a blank decision never reaches the wire); 400/404 join 409 with the shared copy. - `QueueCard.vue`: imports the shared type label. - `MobileAdmin.vue`: approval = select (aria-pressed, inset ring) → restated consequence (role=status) → optional note (no Enter-submit) → `Cancel` (focused on reveal, p19) + `Send: <option>`; question = text + Send; alert = Got it; title line; real type label (and the type line off gray-500). Success drops the card locally before the refetch (fetchQueue swallows its own errors); a monotonic guard stops a stale poll from resurrecting an answered card; 5xx/transport → per-card InlineError with selection + note kept (p18); 409/400/404 → card dropped + persistent page-level notice scrolled into view. Per-card state keyed by id, pruned on every poll, cleared on logout. Colour-free CSS only (raw-colour ratchet: MobileAdmin 0/4/128 vs baseline 0/4/130). - Tests: `tests/unit/operatorQueueResponse.spec.js` (decision table, behavioural store test with axios mocked, source-wiring/residue guards) + `e2e/mobile-admin-queue-respond.spec.js` (@smoke; one method-aware route captures the real POST body; proved to fail on the pre-fix code with `captured POSTs: [{"response":"approved","response_text":"Deny"}]`). - Docs: mobile-admin-pwa.md, operating-room.md, requirements §27.3, flows index, 2 learnings entries; CSO diff report (0 findings). - Follow-ups filed: #2375 (Workspace asks panel — same inversion, different endpoint), #2376 (server-side `response ∈ options` check), #2377 (desktop never shows the not-recorded copy). Fixes #2370 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…odeQL js/incomplete-multi-character-sanitization) The helper removes comments from repo source text before the residue assertions; a single-pass replace of `<!--…-->` is what the rule flags. Loop until stable — the documented remediation — so the red check clears without weakening the assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, no quoted old literals in comments (clears CodeQL) CodeQL flags any replace() whose regex contains `<!--` (js/incomplete-multi-character-sanitization), fixpoint loop or not. The helper existed only so two of this fix's own comments could quote the old `request_type` / `response: 'approved'` literals; reword the comments instead and assert on raw source, which is stricter — the literals may not return even in a comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
/mmobile admin's queue card POSTed{ response: 'approved', response_text: <tapped option> }for every answer — a Deny was recorded as an approval, a typed answer became a note, the card answered on one tap with no note and no explicit submit (desktopQueueCardis select → optional note → Send), and the type line read a nonexistentrequest_type(blank). The agent readsresponseas the decision (sync write-back + the ent#329 resume framing), so the field is load-bearing. Live artifact: the two cards answered on/mduring the bug: background polls re-flash loaded content and reset UI state (design-system p13/p14) #1927 eyeball sit in the agent's queue file asresponse='approved',response_text='Approve'.utils/operatorQueue.jsis now the ONE builder of the respond payload, shared by the desktop store and/m(the bug class was a second hand-built producer)./mreaches desktop parity: select (aria-pressed, inset ring) → restated consequence → optional note (no Enter-submit) →Cancel(focused on reveal, p19) +Send: <option>; question = text + Send; alert =Got it; title line; real type labels (Needs approval/Question/Heads up, shared withQueueCard).fetchQueueswallows its own errors) and a monotonic guard stops a stale poll from resurrecting an answered card; a 5xx/transport failure renders anInlineErrorinside the card (p18) with the selection + note kept for a retry; a 409/400/404 "no longer pending" refusal drops the card and shows a persistent page-level notice, scrolled into view. Per-card state is keyed by item id, pruned on every poll, cleared on logout. Colour-free CSS only.Changes
src/frontend/src/utils/operatorQueue.js(new) —queueResponseBody/buildQueueResponse/queueResponseKind(by item type: approval-with-options → buttons, alert → Got it, else text answer) /optionsOf(the DB stores whatever JSON the agent wrote) /queueTypeLabel/QUEUE_RESPONSE_NOT_RECORDED+respondRefusedAsNotPending(409/400/404)src/frontend/src/stores/operatorQueue.js— body via the shared builder (note trimmed, empty →null; a blank decision never reaches the wire); 400/404 join 409 with the shared attribution-free copysrc/frontend/src/components/operator/QueueCard.vue— imports the shared type labelsrc/frontend/src/views/MobileAdmin.vue— the card rebuild above;.ops-card-typeoff gray-500 (the contract's dark-ink floor);data-testidhookssrc/frontend/tests/unit/operatorQueueResponse.spec.js(new) — decision table, behavioural store test (vi.mock('axios')), source-wiring + residue guardssrc/frontend/e2e/mobile-admin-queue-respond.spec.js(new,@smoke) — one method-awarepage.routeserves the GET and captures the real POST body; "no POST on tap" is proved by order (tap → form → Send → exactly one captured body)feature-flows/mobile-admin-pwa.md,feature-flows/operating-room.md,requirements/public-access.md§27.3, flows index, 2learnings.mdentries, CSO diff reportTest Plan
vitest run— 57 files / 1224 tests (45 new)/m(5) + Operations (3) regressiona tap must not answer; captured POSTs: [{"id":"e2e-2370-approval","body":{"response":"approved","response_text":"Deny"}}]vite build✓ ·check:tokens✓ · loading-gate ratchet ✓ · raw-colour scan (review-enforced, not CI):views/MobileAdmin.vue0 / 4 / 128 (baseline 0 / 4 / 130),QueueCard.vue28 / 59 / 0,stores/operatorQueue.js4 / 0 / 0 — all unchanged or smaller/m(three seeded cards), then the agent's own queue file:approval → response='Approve', response_text='test'·question → response='test answer'·alert → response='acknowledged'— never'approved'/review— 0 critical, 7 low items applied ·/cso --diff— 0 findings (docs/security-reports/cso-diff-2026-08-21-2370-mobile-approval-payload.md)Notes for reviewers
reportActionFailurebanner — 5xx/transport → per-cardInlineError(design-system p18: next to the control; also drops the untrue "Nothing was changed" for a timed-out POST), 409/400/404 → the persistent page banner (the card is about to leave). Honest failure is preserved and more visible." "→null) and shares the refusal copy;QueueCardrenders the same label set. No desktop UX change.response ∈ optionscheck — the guard that would have made this a 422 five months ago), bug: Operations feed never shows "your response was not recorded" after a respond race — fetchItems() wipes the copy and the UI only renders it as a refresh-failure detail #2377 (desktop never shows the not-recorded copy;fetchItems()wipes it). Residual registered in trinity-devDEBT_INBOX(options size-cap marker selectable on both surfaces — parity). Notes left on refactor: decompose oversized Vue views (Settings 3061 lines, MobileAdmin, Agents, AgentDetail, ...) #1030 and refactor: route destructive confirms through ConfirmDialog — eliminate native confirm() (design-system p19) #1924.Fixes #2370
🤖 Generated with Claude Code