feat(jobs): one surface for the job detail sheet #213

Merged
nalum merged 1 commit from feat/job-detail-sheet into main 2026-08-26 18:34:00 +00:00
Owner

Closes #172 and #173. First screen built on the field row (#211).

A job sheet you read, then press Edit, then fill in a form, then Save,
asks a household to make every decision at once to change one thing.
Changing who does the bins meant opening an editor over the title, the
note, the date and the points, and a half-finished form was a decision
about all of them. Rows that are their own controls ask for one change at
a time and commit it where it is shown.

Structure follows §5: the title in place, the description as the job's own
words rather than a labelled field, the progress block, then Who / When /
Worth as picker rows, the To do list, and a footer that names the next
thing the reader can actually do — four states, because CompleteItem
requires the owner and a button that cannot do what it says is worse than
no button.

Why the pickers are in this PR

The first version of this branch wired the three chevron rows to the
pickers the app already had, leaving #173 to replace them. Deployed and
looked at, that was plainly wrong: the app's existing pickers are inline
form fragments with Save buttons, so reusing them re-imported the exact
grammar this redesign exists to delete. The sheet broke two of the five
laws it was built to satisfy — a chevron unfolded a form instead of
opening a picker, and three explicit Saves sat on a screen whose premise
is commit-on-blur.

So #173 is folded in. A chevron now opens a real picker (§8): Who
commits and closes on a tap, because a single-select list has nothing to
confirm; When confirms, because the date and the time are two
decisions, and its footer states the outcome rather than saying Done;
Worth re-filters when the owner changes, since the server's
eligibility rule depends on who the job belongs to. No Save button
remains on this screen
, and repricing a linked reward commits in place
like every other value.

Sheets also gained a grab handle and a stack, so a picker over a sheet
dismisses itself rather than the screen beneath it — that one is a real
bug found by building the thing.

Nothing shipped was dropped

Points on a sub-job, removing one, creating a reward from inside the job
and repricing a linked one all survive, in the places §5 puts them. Taking
an unassigned job folded into the Who picker, which is the server's taker
rule rather than a second affordance for it. Ticking a sub-job with points
now celebrates instead of passing in silence, and the sheet honours the
admin bypass the server has always had on completion. Android gained
sub-job promotion, which that surface never had.

A completed repeating job takes §6.3's shape: title struck, successor
named and tappable, and a refusal to reopen that says which job took over
instead of saying no.

The queue debt this collected

Carrying spawned_next into Android's read cache needed a schema bump,
which exposed something worse than the feature: CacheDb.onUpgrade
dropped pending_op along with the caches. The queue is not a cache. It
holds writes that never reached the server — a job renamed on the bus, a
sub-job ticked with no signal — and dropping them loses a household's work
with no symptom at all. The comment in that file had been warning about
this since v12.

It now survives an upgrade, with a test that was checked to fail against
the old behaviour rather than merely pass against the new one.

Left thinner, on purpose

The repeat rule and delete keep their current affordances inside an
interim options block until #175 and #176. That block is now the only
stand-in in the sheet.

conformance/render-job-sheet.json and render-job-pickers.json pin the
sheet's and the pickers' string inventories across both surfaces. No
screenshot golden moved: none photographs a Sheet.

Jobs.tsx 2,565 → 903 lines; ItemSheets.kt 1,434 → 327.

🤖 Generated with Claude Code

Closes #172 **and #173**. First screen built on the field row (#211). A job sheet you read, then press Edit, then fill in a form, then Save, asks a household to make every decision at once to change one thing. Changing who does the bins meant opening an editor over the title, the note, the date and the points, and a half-finished form was a decision about all of them. Rows that are their own controls ask for one change at a time and commit it where it is shown. Structure follows §5: the title in place, the description as the job's own words rather than a labelled field, the progress block, then Who / When / Worth as picker rows, the To do list, and a footer that names the next thing the reader can actually do — four states, because `CompleteItem` requires the owner and a button that cannot do what it says is worse than no button. ## Why the pickers are in this PR The first version of this branch wired the three chevron rows to the pickers the app already had, leaving #173 to replace them. Deployed and looked at, that was plainly wrong: the app's existing pickers are inline form fragments with Save buttons, so reusing them re-imported the exact grammar this redesign exists to delete. The sheet broke two of the five laws it was built to satisfy — a chevron unfolded a form instead of opening a picker, and three explicit Saves sat on a screen whose premise is commit-on-blur. So #173 is folded in. A chevron now opens a real picker (§8): **Who** commits and closes on a tap, because a single-select list has nothing to confirm; **When** confirms, because the date and the time are two decisions, and its footer states the outcome rather than saying Done; **Worth** re-filters when the owner changes, since the server's eligibility rule depends on who the job belongs to. **No Save button remains on this screen**, and repricing a linked reward commits in place like every other value. Sheets also gained a grab handle and a stack, so a picker over a sheet dismisses itself rather than the screen beneath it — that one is a real bug found by building the thing. ## Nothing shipped was dropped Points on a sub-job, removing one, creating a reward from inside the job and repricing a linked one all survive, in the places §5 puts them. Taking an unassigned job folded into the Who picker, which is the server's taker rule rather than a second affordance for it. Ticking a sub-job with points now celebrates instead of passing in silence, and the sheet honours the admin bypass the server has always had on completion. Android gained sub-job promotion, which that surface never had. A completed repeating job takes §6.3's shape: title struck, successor named and tappable, and a refusal to reopen that says which job took over instead of saying no. ## The queue debt this collected Carrying `spawned_next` into Android's read cache needed a schema bump, which exposed something worse than the feature: `CacheDb.onUpgrade` dropped `pending_op` along with the caches. The queue is not a cache. It holds writes that never reached the server — a job renamed on the bus, a sub-job ticked with no signal — and dropping them loses a household's work with no symptom at all. The comment in that file had been warning about this since v12. It now survives an upgrade, with a test that was checked to fail against the old behaviour rather than merely pass against the new one. ## Left thinner, on purpose The repeat rule and delete keep their current affordances inside an interim options block until #175 and #176. That block is now the only stand-in in the sheet. `conformance/render-job-sheet.json` and `render-job-pickers.json` pin the sheet's and the pickers' string inventories across both surfaces. No screenshot golden moved: none photographs a `Sheet`. `Jobs.tsx` 2,565 → 903 lines; `ItemSheets.kt` 1,434 → 327. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(jobs): one surface for the job detail sheet
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 2m50s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m6s
android / build (pull_request) Successful in 6m47s
android / report (pull_request) Successful in 3s
90ef92aea9
The sheet was two screens pretending to be one: a read-only view and,
behind an Edit button, a form where every field waited on a single
Save. A job changes one thing at a time — a handover, a slipped
deadline, a reworded title — so the daily edit paid a mode switch and
a whole-form commit, and a failed Save refused four fields to refuse
one. Viewing and editing are now states of the same surface: title
and note edit in place, Who / When / Worth are chevron rows that open
a picker and commit on return, each through its single-purpose RPC
(AssignUser, SetDeadline, LinkItem; the partial-safe Update carries
the in-place text), with the FieldRow grammar supplying blur-commit,
the flash, the one snackbar, Undo-dequeues and the invalid trap.

Sub-jobs each expand — one at a time — into Who, Worth, their own
note, "Make it its own job" and "Remove it"; there is no deadline row,
which the server refuses through both doors. The footer derives from
CompleteItem's owner rule (admin bypasses, unowned is open to all):
Done, Tick "<next>", an unfilled Waiting-on line, or nothing — it only
ever names something the reader can actually do. Empty means absent:
no zero counters, no empty progress block, and "Break this up" stands
in for an empty list. Every affordance gates on the matrix verb it
performs, so a child sees fewer rows, never disabled ones.

Nothing the shipped sheet could do is lost: points on a sub-job,
removing one, minting a points reward from inside the job, repricing
a linked reward, take/put-back (now inside the Who picker), promotion
(new to Android, via a new items.setParent queue op), and
make-a-step-of + repeats + delete relocated into an interim ⋯ block.
Deliberately left for the tickets that follow: the designed pickers
(#173 — the chevrons open the existing MemberSelect, date/time fields
and grant/link block), the designed ⋯ options and repeat sheets
(#175), the restricted child sheets (#178) and the card (#174). The
sheet's cross-surface strings are pinned by a new
conformance/render-job-sheet.json family on both semantic-walk
suites; no screenshot-golden cases were added, so the committed
goldens stand untouched.

Test report

Suite Tests Result Skipped
Unit 1434 ✅ pass 1
Integration 130 ✅ pass —

Coverage: 27.0%

Updated by the check workflow · commit 846dee7442

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1434 | ✅ pass | 1 | | Integration | 130 | ✅ pass | — | **Coverage:** 27.0% <sub>Updated by the check workflow · commit 846dee74423ae40e6c387bbc79c65f2f8fa51998</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) ❌ 1 failed

Coverage:

Updated by the android workflow · commit 846dee7442

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | | ❌ 1 failed | | **Coverage:** <sub>Updated by the android workflow · commit 846dee74423ae40e6c387bbc79c65f2f8fa51998</sub>
nalum force-pushed feat/job-detail-sheet from 90ef92aea9
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 2m50s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m6s
android / build (pull_request) Successful in 6m47s
android / report (pull_request) Successful in 3s
to 4b39b5957b
All checks were successful
check / commits (pull_request) Successful in 10s
check / go (pull_request) Successful in 2m52s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m10s
android / build (pull_request) Successful in 6m30s
android / report (pull_request) Successful in 3s
2026-08-24 14:08:25 +00:00
Compare
nalum force-pushed feat/job-detail-sheet from 4b39b5957b
All checks were successful
check / commits (pull_request) Successful in 10s
check / go (pull_request) Successful in 2m52s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m10s
android / build (pull_request) Successful in 6m30s
android / report (pull_request) Successful in 3s
to 3fab3be704
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 2m49s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 4m5s
android / build (pull_request) Successful in 6m32s
android / report (pull_request) Successful in 4s
2026-08-24 14:59:04 +00:00
Compare
Author
Owner

Force-pushed: four defects found by deploying it and using it, all fixed in
this commit.

Closing a picker closed the whole sheet. React synthetic events propagate
through the React tree, not the DOM tree, so a picker rendered inside the
sheet's children is a React descendant of the sheet's own element — and the
picker's leave animation bubbled into the sheet's onAnimationEnd, which read
sheet-down and closed it. Guarded with e.target === e.currentTarget in
Sheet.tsx, so it holds for every sheet in the app. The overlay's backdrop
click turned out to be unguarded too, and only accidentally safe; it now guards
itself instead of leaning on a child's stopPropagation.

This one was unreachable from a test: jsdom ships no AnimationEvent
constructor, and React skips registering animationend when it is missing. A
shim now makes that class of bug catchable.

The linked reward rendered as a broken row — its name in the label column,
its value as a bare number, unstyled Unlink and Delete beneath. And the Worth
picker never showed what was already linked
, because it lists eligible
rewards and eligibility excludes anything already attached.

Those were one mistake seen twice. The picker now owns the answer: the linked
reward first and drawn as selected, its amount editing in place, Unlink and
Delete beside it, then the eligible rewards, Make a new one, Worth nothing.
The block under the row is deleted, component and CSS, on both surfaces. The
Worth row is §5's shape again — a value and a chevron.

The add-a-sub-job row was the shipped quick-add form, never restyled. It is
now one more row on the list's 48px rhythm, keeping the name-and-price-in-one-
gesture capability.

Also fixed while in there: unticked checkboxes drew a ghosted tick at 15%
opacity. §5 wants an empty box with a 2px muted border.

Every web fix has a test confirmed to fail against the previous commit. Android
did not have the first defect — Compose does not bubble — and the report says so
rather than claiming a fix that was not made.

Known parity gap, left deliberately: Android's checkbox is Material's 20dp/2dp
rather than §5's 22dp/7dp. Replacing Material's toggleable semantics over 2dp is
not worth it; #178's pass can settle it.

Force-pushed: four defects found by deploying it and using it, all fixed in this commit. **Closing a picker closed the whole sheet.** React synthetic events propagate through the React tree, not the DOM tree, so a picker rendered inside the sheet's children is a React descendant of the sheet's own element — and the picker's leave animation bubbled into the sheet's `onAnimationEnd`, which read `sheet-down` and closed it. Guarded with `e.target === e.currentTarget` in `Sheet.tsx`, so it holds for every sheet in the app. The overlay's backdrop click turned out to be unguarded too, and only accidentally safe; it now guards itself instead of leaning on a child's `stopPropagation`. This one was **unreachable from a test**: jsdom ships no `AnimationEvent` constructor, and React skips registering `animationend` when it is missing. A shim now makes that class of bug catchable. **The linked reward rendered as a broken row** — its name in the label column, its value as a bare number, unstyled Unlink and Delete beneath. And **the Worth picker never showed what was already linked**, because it lists eligible rewards and eligibility excludes anything already attached. Those were one mistake seen twice. The picker now owns the answer: the linked reward first and drawn as selected, its amount editing in place, Unlink and Delete beside it, then the eligible rewards, `Make a new one`, `Worth nothing`. The block under the row is deleted, component and CSS, on both surfaces. The Worth row is §5's shape again — a value and a chevron. **The add-a-sub-job row was the shipped quick-add form**, never restyled. It is now one more row on the list's 48px rhythm, keeping the name-and-price-in-one- gesture capability. Also fixed while in there: unticked checkboxes drew a ghosted tick at 15% opacity. §5 wants an empty box with a 2px muted border. Every web fix has a test confirmed to fail against the previous commit. Android did not have the first defect — Compose does not bubble — and the report says so rather than claiming a fix that was not made. Known parity gap, left deliberately: Android's checkbox is Material's 20dp/2dp rather than §5's 22dp/7dp. Replacing Material's toggleable semantics over 2dp is not worth it; #178's pass can settle it.
nalum force-pushed feat/job-detail-sheet from 3fab3be704
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 2m49s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 4m5s
android / build (pull_request) Successful in 6m32s
android / report (pull_request) Successful in 4s
to 7451a4bbc0
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 2m53s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m14s
android / build (pull_request) Successful in 6m49s
android / report (pull_request) Successful in 4s
2026-08-24 15:29:51 +00:00
Compare
Author
Owner

Force-pushed again: the Worth picker's last three styling defects, found on the
same deploy.

  • Make a new one opened a raw form field — a bordered number input with
    browser spinner arrows, between rows that are all clean 56px picker rows. It
    is now one more row, with a digit-filtered text field and no spinner. Android
    got the same shape, built from PickerRow's layout rather than an
    OutlinedTextField.
  • The footer announced Worth 0 points, disabled, while the field was
    empty. A reward worth nothing is not something the server accepts, so the
    button was stating an outcome that could not happen — and a disabled control
    is what law 5 bans anyway. There is now no footer until the amount is one the
    picker could commit, and then it states the real outcome, the way the When
    picker's footer states Due Tuesday 09:45.
  • Unlink and Delete this reward were bare underlined links, with the
    underline running under the trash icon. They are quiet buttons in the
    picker's own voice now. Android needed no change here — Compose's
    TextButton never underlined, and already matched the sheet's delete-trigger
    grammar.

Each fix has a test proven to fail first, by swapping the pre-fix sources back
in with jj file show and watching the assertions break, on both surfaces.

All four styling defects on this screen came from the same place: the shipped
app's form fragments reused inside the new row grammar. The sheet is clean now;
the next ticket to touch one of those surfaces should check for it rather than
wait for a deploy to find it.

Force-pushed again: the Worth picker's last three styling defects, found on the same deploy. - **`Make a new one` opened a raw form field** — a bordered number input with browser spinner arrows, between rows that are all clean 56px picker rows. It is now one more row, with a digit-filtered text field and no spinner. Android got the same shape, built from `PickerRow`'s layout rather than an `OutlinedTextField`. - **The footer announced `Worth 0 points`**, disabled, while the field was empty. A reward worth nothing is not something the server accepts, so the button was stating an outcome that could not happen — and a disabled control is what law 5 bans anyway. There is now no footer until the amount is one the picker could commit, and then it states the real outcome, the way the When picker's footer states `Due Tuesday 09:45`. - **`Unlink` and `Delete this reward` were bare underlined links**, with the underline running under the trash icon. They are quiet buttons in the picker's own voice now. Android needed no change here — Compose's `TextButton` never underlined, and already matched the sheet's delete-trigger grammar. Each fix has a test proven to fail first, by swapping the pre-fix sources back in with `jj file show` and watching the assertions break, on both surfaces. All four styling defects on this screen came from the same place: the shipped app's form fragments reused inside the new row grammar. The sheet is clean now; the next ticket to touch one of those surfaces should check for it rather than wait for a deploy to find it.
nalum force-pushed feat/job-detail-sheet from 7451a4bbc0
All checks were successful
check / commits (pull_request) Successful in 9s
check / go (pull_request) Successful in 2m53s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m14s
android / build (pull_request) Successful in 6m49s
android / report (pull_request) Successful in 4s
to aac0e632be
All checks were successful
check / commits (pull_request) Successful in 10s
check / go (pull_request) Successful in 3m14s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m28s
android / build (pull_request) Successful in 8m23s
android / report (pull_request) Successful in 4s
2026-08-24 16:29:14 +00:00
Compare
Author
Owner

Force-pushed: two more form fragments fixed, the whole screen swept, and the
goldens recorded.

The sweep found a data-loss bug. Remove it on a sub-job hard-deleted on
the first tap — no question, no undo. Delete is one of exactly two acts the laws
send to a dialog, precisely because it cannot be taken back. It now asks
Delete "Wash up"? This one can't be undone.

The two defects reported from the deploy: the When picker's Time was a boxed
field with a clock button beside it and is now a row (24-hour kept, and clearing
moved into the list as Any time, so the empty row names its emptiness instead
of going blank); Make it its own job and Remove it were underlined links and
are now the last two of §5's five things, as rows.

Nineteen hits swept, each with a verdict. Nine fixed, including several nobody
reported: the ⋯ entries and both delete confirms were still buttons in a
form-actions block, Android tinted Unlink with the accent where web uses
ink-soft, and a sub-job's checkbox was disabled-and-faded when it belongs to
someone else — law 5 wants a fact drawn at full strength and simply not offered.

Two left deliberately: the ⋯ repeat block and the convert chooser are the last
.field fragments here, and #175 replaces that whole block with the designed
repeat sheet. Styling them now is work thrown away; their triggers are rows.

One reported, not fixed: Jobs.tsx still holds the shipped create form with
four (optional) labels. That is §8's create screen — #177 owns it.

Goldens recorded: 12 cases, 24 images — the sheet with sub-jobs, a sub-job
expanded, a plain job, and all three pickers. Hearth light and dark, because
every colour in them comes from components whose own goldens already cross all
19 families; what these pin is layout. The When cases pose on the frozen e2e
anchor, or a month grid with today ringed would re-record itself every midnight.

Thirteen tests, each proven to fail against the previous commit first.

AGENTS.md now carries the form-fragment sweep as a hand-off condition with its
hunt table, so the next screen built from rows gets it without being asked.

Force-pushed: two more form fragments fixed, the whole screen swept, and the goldens recorded. **The sweep found a data-loss bug.** `Remove it` on a sub-job hard-deleted on the first tap — no question, no undo. Delete is one of exactly two acts the laws send to a dialog, precisely because it cannot be taken back. It now asks `Delete "Wash up"? This one can't be undone.` The two defects reported from the deploy: the When picker's `Time` was a boxed field with a clock button beside it and is now a row (24-hour kept, and clearing moved into the list as `Any time`, so the empty row names its emptiness instead of going blank); `Make it its own job` and `Remove it` were underlined links and are now the last two of §5's five things, as rows. Nineteen hits swept, each with a verdict. Nine fixed, including several nobody reported: the `⋯` entries and both delete confirms were still buttons in a `form-actions` block, Android tinted `Unlink` with the accent where web uses ink-soft, and a sub-job's checkbox was disabled-and-faded when it belongs to someone else — law 5 wants a fact drawn at full strength and simply not offered. Two left deliberately: the `⋯` repeat block and the convert chooser are the last `.field` fragments here, and #175 replaces that whole block with the designed repeat sheet. Styling them now is work thrown away; their triggers are rows. One reported, not fixed: `Jobs.tsx` still holds the shipped create form with four `(optional)` labels. That is §8's create screen — #177 owns it. **Goldens recorded**: 12 cases, 24 images — the sheet with sub-jobs, a sub-job expanded, a plain job, and all three pickers. Hearth light and dark, because every colour in them comes from components whose own goldens already cross all 19 families; what these pin is layout. The When cases pose on the frozen e2e anchor, or a month grid with today ringed would re-record itself every midnight. Thirteen tests, each proven to fail against the previous commit first. `AGENTS.md` now carries the form-fragment sweep as a hand-off condition with its hunt table, so the next screen built from rows gets it without being asked.
nalum force-pushed feat/job-detail-sheet from aac0e632be
All checks were successful
check / commits (pull_request) Successful in 10s
check / go (pull_request) Successful in 3m14s
check / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m28s
android / build (pull_request) Successful in 8m23s
android / report (pull_request) Successful in 4s
to 298dcb9275
All checks were successful
check / commits (pull_request) Successful in 22s
check / go (pull_request) Successful in 3m5s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 4m37s
android / build (pull_request) Successful in 7m37s
android / report (pull_request) Successful in 3s
2026-08-24 17:10:19 +00:00
Compare
Author
Owner

Force-pushed: a behaviour regression this branch introduced, now reverted.

What broke. The rebuild widened the tick rule to isAdmin || owner === me || owner === "", on both surfaces, for sub-jobs and for the job's own footer. So
an admin was offered a tick on everyone's work. It was done on purpose, with the
reasoning that the server's RequireOwner bypasses for an admin and the shipped
web "denied a tick the server allows".

That reasoning is backwards. What the server permits is not what the interface
should offer. The bypass exists so a parent can correct data through the API or
after reassigning — not so they can tick a child's chore off on their behalf.
And since completing a sub-job with points banks them to its owner, an admin
ticking someone else's sub-job credits that person for work they may not have
done, which quietly breaks what the reward system is for.

The design already said so, and this branch had already implemented the other
half of it: §5's footer shows Waiting on Ali, unfilled and not a button, for a
sub-job that belongs to someone else, and §7b calls a sub-job the reader owns
"the only interactive row on the sheet". The footer states were right; the
checkbox rule under them was not, so the same screen contradicted itself.

Fixed by dropping isAdmin from canTickJob/tickableStep (web) and
canTickJob/tickable (Android). The server guard is untouched.

One correction to the report that prompted this. The household remembered
1.8 as letting the job's owner tick an unassigned sub-job. It did not — it let
anyone: v1.8.1 web/src/pages/Jobs.tsx:1677 reads (stepOwner === me || stepOwner === ""), with no parent-owner check and no admin clause, and Android
agreed. §7b says the same thing, because that is what CompleteItem does with an
unowned item. So 1.8's rule is restored exactly, including that row. Narrowing it
to the job's owner would be a new decision, not a restoration, and is left open.

The isAdmin capability is kept on the Android caps object, deliberately unused,
so a test that really flags an admin pins this rather than the absence of a line
nobody would notice coming back.

Force-pushed: a behaviour regression this branch introduced, now reverted. **What broke.** The rebuild widened the tick rule to `isAdmin || owner === me || owner === ""`, on both surfaces, for sub-jobs and for the job's own footer. So an admin was offered a tick on everyone's work. It was done on purpose, with the reasoning that the server's `RequireOwner` bypasses for an admin and the shipped web "denied a tick the server allows". That reasoning is backwards. What the server permits is not what the interface should offer. The bypass exists so a parent can correct data through the API or after reassigning — not so they can tick a child's chore off on their behalf. And since completing a sub-job with points banks them to its owner, an admin ticking someone else's sub-job credits that person for work they may not have done, which quietly breaks what the reward system is for. The design already said so, and this branch had already implemented the other half of it: §5's footer shows `Waiting on Ali`, unfilled and not a button, for a sub-job that belongs to someone else, and §7b calls a sub-job the reader owns "the only interactive row on the sheet". The footer states were right; the checkbox rule under them was not, so the same screen contradicted itself. **Fixed** by dropping `isAdmin` from `canTickJob`/`tickableStep` (web) and `canTickJob`/`tickable` (Android). The server guard is untouched. **One correction to the report that prompted this.** The household remembered 1.8 as letting the *job's owner* tick an unassigned sub-job. It did not — it let anyone: `v1.8.1 web/src/pages/Jobs.tsx:1677` reads `(stepOwner === me || stepOwner === "")`, with no parent-owner check and no admin clause, and Android agreed. §7b says the same thing, because that is what `CompleteItem` does with an unowned item. So 1.8's rule is restored exactly, including that row. Narrowing it to the job's owner would be a new decision, not a restoration, and is left open. The `isAdmin` capability is kept on the Android caps object, deliberately unused, so a test that really flags an admin pins this rather than the absence of a line nobody would notice coming back.
nalum force-pushed feat/job-detail-sheet from 298dcb9275
All checks were successful
check / commits (pull_request) Successful in 22s
check / go (pull_request) Successful in 3m5s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 4m37s
android / build (pull_request) Successful in 7m37s
android / report (pull_request) Successful in 3s
to 846dee7442
Some checks failed
check / commits (pull_request) Successful in 13s
android / build (pull_request) Failing after 4m35s
check / go (pull_request) Successful in 3m18s
check / web (pull_request) Successful in 4m39s
android / report (pull_request) Successful in 4s
check / report (pull_request) Successful in 4s
android / report (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
android / build (push) Has been cancelled
check / commits (push) Has been cancelled
2026-08-26 07:03:40 +00:00
Compare
nalum changed target branch from feat/field-row to main 2026-08-26 18:33:58 +00:00
nalum merged commit 846dee7442 into main 2026-08-26 18:34:00 +00:00
nalum deleted branch feat/job-detail-sheet 2026-08-26 18:34:01 +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!213
No description provided.