feat: sub-verb matrix rows, child standalone jobs, pinned sheet errors (ADR-0031) #89

Merged
nalum merged 9 commits from feat/matrix-sub-verbs into main 2026-08-16 12:41:08 +00:00
Owner

Stacked on #88 (merge that first; Forgejo retargets this to main).

A child testing the Android app saw the New job button, pressed Save, and got "Not authorized to create an item outside a list". The button read the ItemService/Create matrix row, but the standalone-vs-in-list rule lived only as a hard-coded role check inside the handler — invisible to GetPermissionMatrix, so web and Android both showed an affordance the server refused.

ADR-0031: sub-verb matrix entries. A role-only resource guard inside a handler now gets a dotted matrix row. The handler consults it via roles.CheckPermission, GetPermissionMatrix serves it, and clients gate the affordance on it — one row, no drift. Two rows land here:

  • ItemService/Create.standalone (Admin, Member, Child) — who may mint a job outside a list. Policy change: children may now create standalone jobs.
  • ItemService/AssignUser.other (Admin, Member) — who may set a job's owner to someone other than themselves. AssignUser reads it in place of its hard-coded check, and Create now enforces it when owner_uid rides the request. That closes a real hole: with standalone create open to children, the owner riding the create bypassed the ADR-0014 take-only guard entirely — a child could mint a job assigned to anyone.

Clients follow the rows: New job appears exactly when Create.standalone allows it, and the owner picker offers only Me to takers, so the form can never stage a create the server refuses.

Pinned sheet errors. The same testing session showed failure banners clipping off the top of scrolled sheets. The web Sheet grew an error slot rendered with the footer, and 18 sheet forms hand their banner there (three needed error state hoisted out of child components). Android moved ErrorLine beside the action row in 14 bottom-sheet forms. The message now appears next to the buttons that caused it.

Verified: make check green, Android assembleDebug green, deployed to the kind cluster and exercised — the live server serves both sub-verb rows, and the child flow was retested on-device.

🤖 Generated with Claude Code

Stacked on #88 (merge that first; Forgejo retargets this to main). A child testing the Android app saw the New job button, pressed Save, and got "Not authorized to create an item outside a list". The button read the `ItemService/Create` matrix row, but the standalone-vs-in-list rule lived only as a hard-coded role check inside the handler — invisible to `GetPermissionMatrix`, so web and Android both showed an affordance the server refused. **ADR-0031: sub-verb matrix entries.** A role-only resource guard inside a handler now gets a dotted matrix row. The handler consults it via `roles.CheckPermission`, `GetPermissionMatrix` serves it, and clients gate the affordance on it — one row, no drift. Two rows land here: - `ItemService/Create.standalone` (Admin, Member, Child) — who may mint a job outside a list. Policy change: children may now create standalone jobs. - `ItemService/AssignUser.other` (Admin, Member) — who may set a job's owner to someone other than themselves. `AssignUser` reads it in place of its hard-coded check, and `Create` now enforces it when `owner_uid` rides the request. That closes a real hole: with standalone create open to children, the owner riding the create bypassed the ADR-0014 take-only guard entirely — a child could mint a job assigned to anyone. Clients follow the rows: New job appears exactly when `Create.standalone` allows it, and the owner picker offers only Me to takers, so the form can never stage a create the server refuses. **Pinned sheet errors.** The same testing session showed failure banners clipping off the top of scrolled sheets. The web `Sheet` grew an `error` slot rendered with the footer, and 18 sheet forms hand their banner there (three needed error state hoisted out of child components). Android moved `ErrorLine` beside the action row in 14 bottom-sheet forms. The message now appears next to the buttons that caused it. Verified: `make check` green, Android `assembleDebug` green, deployed to the kind cluster and exercised — the live server serves both sub-verb rows, and the child flow was retested on-device. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The New job affordance was gated on ItemService/Create, but the
standalone-vs-in-list rule lived only as a hard-coded role check inside
the handler — invisible to GetPermissionMatrix, so both web and Android
showed a child the button and the server then refused. The rule now
lives in the matrix as a Create.standalone sub-verb entry: the handler
consults it, GetPermissionMatrix serves it, and clients gate on it — one
source of truth, no drift (ADR-0031).

Policy also changes: CHILD joins the sub-verb entry, so children may now
mint standalone jobs, not only list entries and steps under their own
jobs.
The button was gated on ItemService/Create, which CHILD passes for
in-list creation — so a child saw New job and then hit the server's
standalone guard. Reading the sub-verb entry keeps the affordance and
the guard on the same matrix row (ADR-0031).
Same fix as the web reference: the button read ItemService/Create,
which CHILD passes for in-list creation, so a child saw New job and the
server refused the standalone create. The sub-verb entry (ADR-0031) is
the row the server guard reads, so the affordance now matches it.
ADR-0031: a role-only resource guard inside a handler is named in the
PermissionMatrix as a sub-verb entry (ItemService/Create.standalone) so
the guard, GetPermissionMatrix and every client affordance read the one
row. The first entry also flips policy: CHILD may now create standalone
jobs.
Opening standalone create to children exposed a hole: the owner rides
CreateItemRequest, so the ADR-0014 take-only guard in AssignUser never
ran and a child could mint a job assigned to anyone. The role list for
'may hand a job to someone else' now lives as the AssignUser.other
sub-verb row (ADR-0031); AssignUser reads it in place of its hard-coded
Admin/Member check, and Create enforces it whenever owner_uid names
someone other than the requester. The self-vs-other comparison stays in
the handlers — it needs the requester's identity, which a matrix row
cannot express.
The picker read from the assignable family list regardless of role, so
a child could stage a job for someone else and only learn at Save that
the server refuses. The AssignUser.other sub-verb row (ADR-0031) now
decides who sees other marks — takers get just their own, matching what
the create will accept.
Same fix as the web reference: the AssignUser.other sub-verb row
(ADR-0031) decides who sees other members' marks in the New job owner
picker — takers get just their own, matching what the create accepts.
A failure banner at the top of a sheet body scrolls out of view — on a
long form the user presses Save at the bottom and the answer renders
where they are not looking, clipped by the scroll (seen on New job).
Sheet grows an error slot pinned with the footer, and every sheet form
hands its banner there instead of the body top: the message appears
beside the buttons that caused it, always visible.
fix(android): pin form errors beside the sheet actions
All checks were successful
check / commits (pull_request) Successful in 8s
check / web (pull_request) Successful in 1m39s
check / go (pull_request) Successful in 2m42s
check / report (pull_request) Successful in 3s
android / build (pull_request) Successful in 5m36s
android / report (pull_request) Successful in 4s
1261818adc
The web reference just moved sheet-form failure banners out of the
scrollable body and down to the action row, so the message appears
beside the buttons that caused it instead of clipped off the top
(seen on New job). The Compose bottom-sheet forms follow: ErrorLine
moves from under the heading to just above each form's action row.

Test report

Suite Tests Result Skipped
Unit 1332 ✅ pass 1
Integration 83 ✅ pass —

Coverage: 27.6%

Updated by the check workflow · commit 8b66ab9746

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1332 | ✅ pass | 1 | | Integration | 83 | ✅ pass | — | **Coverage:** 27.6% <sub>Updated by the check workflow · commit 8b66ab97466f69bca00370eb60d681db557fe8bb</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 31 ✅ pass 0

Updated by the android workflow · commit 8b66ab9746

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 31 | ✅ pass | 0 | <sub>Updated by the android workflow · commit 8b66ab97466f69bca00370eb60d681db557fe8bb</sub>
nalum force-pushed feat/matrix-sub-verbs from 1261818adc
All checks were successful
check / commits (pull_request) Successful in 8s
check / web (pull_request) Successful in 1m39s
check / go (pull_request) Successful in 2m42s
check / report (pull_request) Successful in 3s
android / build (pull_request) Successful in 5m36s
android / report (pull_request) Successful in 4s
to 8b66ab9746
All checks were successful
check / commits (pull_request) Successful in 7s
check / web (pull_request) Successful in 1m56s
check / go (pull_request) Successful in 2m34s
android / build (pull_request) Successful in 6m39s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
2026-08-16 12:16:29 +00:00
Compare
nalum force-pushed feat/matrix-sub-verbs from 8b66ab9746
All checks were successful
check / commits (pull_request) Successful in 7s
check / web (pull_request) Successful in 1m56s
check / go (pull_request) Successful in 2m34s
android / build (pull_request) Successful in 6m39s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
to 4d347ee624
Some checks failed
android / build (push) Has been cancelled
android / report (push) Has been cancelled
check / commits (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
check / commits (pull_request) Has been cancelled
check / go (pull_request) Has been cancelled
check / report (pull_request) Has been cancelled
check / web (pull_request) Has been cancelled
android / report (pull_request) Has been cancelled
android / build (pull_request) Has been cancelled
2026-08-16 12:40:38 +00:00
Compare
nalum changed target branch from feat/push-foreground-service to main 2026-08-16 12:41:04 +00:00
nalum merged commit 4d347ee624 into main 2026-08-16 12:41:08 +00:00
nalum deleted branch feat/matrix-sub-verbs 2026-08-16 12:41:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!89
No description provided.