feat(jobs): the job options sheet and the repeat sheet #216

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

Closes #175. Removes the last stand-in from the job sheet.

⋯ opened an inline block holding the shipped repeat editor, a convert
chooser and a hand-rolled delete confirm — the final form fragments on a
screen otherwise built from rows. It is now §6's sheet: three entries,
each something the rows cannot do, with the repeat rule's current value
shown inline because that is the entry people open to read.

Make it part of another job is conditional, not merely consequential.
SetParent refuses a job carrying a repeat rule, a deadline, list
membership or children. So the entry pre-clears what it can and says so,
and is absent when the job has sub-jobs of its own, or when it repeats and
the reader may not clear the rule — a child may SetParent but not
SetRepeat. The pre-clear travels as one queue entry through the
grouped-write primitive built for it earlier in this stack: three writes
that flush together or not at all, each carrying the value that undoes it,
captured at enqueue. A refusal restores the date and the rule and says so.

§6.1's five chips are rendered by calling the label helper on the rule
each chip would set, so chip, summary and fixture are one set of words by
construction rather than by discipline. conformance/repeat.json moves
with them, both surfaces.

The readback states one date, not four — nothing is scheduled, so a
four-date preview would be predicting the household rather than reading
the data — and it names the checklist coming back, because completing a
repeating job clones its sub-jobs. Monthly rules carry the line about the
31st landing on the 30th or the 28th.

The date maths is pinned three ways. The successor rule had to be
ported to TypeScript and Kotlin for the readback, so the same fixture
cases are now asserted by vitest, by Robolectric, and by a new Go test
against internal/domain, which is the code that actually mints
successors.

§6.2's delete counts what goes, in the question and on the button, names
the ticked work, and promises the reward returns to the rewards list —
true since the unlink landed at the bottom of this stack. §6.3 asks before
a tick mints a successor.

One widening, cited as the new rule in AGENTS.md requires: the interim
block barred moving a job that had a linked reward. setparent.go has no
such guard, and §11.B restored points on a sub-job, so the bar is dropped.

Parity gap recorded: Android has no grouped-write primitive, so its
pre-clear sends three writes with the same capture-and-restore and, offline,
simply fails on the first — the handoff's own fallback.

🤖 Generated with Claude Code

Closes #175. Removes the last stand-in from the job sheet. `⋯` opened an inline block holding the shipped repeat editor, a convert chooser and a hand-rolled delete confirm — the final form fragments on a screen otherwise built from rows. It is now §6's sheet: three entries, each something the rows cannot do, with the repeat rule's current value shown inline because that is the entry people open to *read*. **`Make it part of another job` is conditional, not merely consequential.** `SetParent` refuses a job carrying a repeat rule, a deadline, list membership or children. So the entry pre-clears what it can and says so, and is absent when the job has sub-jobs of its own, or when it repeats and the reader may not clear the rule — a child may `SetParent` but not `SetRepeat`. The pre-clear travels as **one queue entry** through the grouped-write primitive built for it earlier in this stack: three writes that flush together or not at all, each carrying the value that undoes it, captured at enqueue. A refusal restores the date and the rule and says so. **§6.1's five chips** are rendered by calling the label helper on the rule each chip would set, so chip, summary and fixture are one set of words by construction rather than by discipline. `conformance/repeat.json` moves with them, both surfaces. The readback states one date, not four — nothing is scheduled, so a four-date preview would be predicting the household rather than reading the data — and it names the checklist coming back, because completing a repeating job clones its sub-jobs. Monthly rules carry the line about the 31st landing on the 30th or the 28th. **The date maths is pinned three ways.** The successor rule had to be ported to TypeScript and Kotlin for the readback, so the same fixture cases are now asserted by vitest, by Robolectric, and by a new Go test against `internal/domain`, which is the code that actually mints successors. §6.2's delete counts what goes, in the question and on the button, names the ticked work, and promises the reward returns to the rewards list — true since the unlink landed at the bottom of this stack. §6.3 asks before a tick mints a successor. One widening, cited as the new rule in AGENTS.md requires: the interim block barred moving a job that had a linked reward. `setparent.go` has no such guard, and §11.B restored points on a sub-job, so the bar is dropped. Parity gap recorded: Android has no grouped-write primitive, so its pre-clear sends three writes with the same capture-and-restore and, offline, simply fails on the first — the handoff's own fallback. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(jobs): the job options sheet and the repeat sheet
Some checks failed
check / commits (pull_request) Successful in 46s
check / go (pull_request) Successful in 7m42s
check / web (pull_request) Failing after 6m19s
check / report (pull_request) Successful in 4s
android / report (pull_request) Has been cancelled
android / build (pull_request) Has been cancelled
5f380adfbe
The interim `⋯` block was the last of the job sheet: an inline expander
carrying the shipped RepeatEditor, a convert-to-step chooser and a delete
confirm, all in `.field`/`choice-row` form fragments on a screen built
from rows. §6 replaces it with a bottom sheet, because a menu opened from
a card has to say WHICH job is about to be deleted — its header carries
the title and a one-line summary (`Me Fox · Due Tue, Sep 8 · Every
Tuesday`), assembled from the sheet's own helpers so the two cannot
paraphrase each other.

Three entries, each 54px, each something the sheet's rows cannot do: the
repeat rule with its value inline (this is the entry people open to
READ), `Make it part of another job`, then a separator and `Delete` —
last, and the only coloured item.

`Make it part of another job` is conditional, not consequential.
SetParent refuses a candidate carrying a repeat rule, a deadline, list
membership or children, so the entry pre-clears what it can and says so
first, and is ABSENT (never disabled) on a job with sub-jobs and on a
repeating job whose reader may not clear the rule — SetParent is
Child-allowed and SetRepeat is not, so that reader would otherwise get an
entry that cannot finish. The pre-clear rides the queue's grouped-write
primitive on web: one entry, each step carrying the write that undoes it,
built from the value read at enqueue. Android has no group primitive yet,
so its three writes go straight to the API with the same compensation and
are simply unavailable offline — the handoff's own alternative, and a
recorded parity gap that a grouped OpQueue entry closes.

§6.1's chips are rendered by calling jobRepeatLabel on the rule each of
them would set, so chip, summary and `conformance/repeat.json` are one
set of words by construction rather than by discipline. Three of them
change: `Every fortnight, Tuesday` (an exact ICU `=2`, not the `two`
plural category — English has no `two` category and the chip would have
read the old generic form), `On the 8th, monthly` and `2nd Tuesday of the
month`, because `Every 2nd Tuesday` reads as either of the last two. The
ordinal comes from selectOrdinal, never an English suffix table. Chips,
both helpers and the fixture move together.

The readback states ONE date, from a port of internal/domain.NextOccurrence
against max(deadline, now) — the argument CompleteItem passes. Both ports
and the Go original are pinned to the same `next` cases in the fixture, so
a drift is a lie about what the tick will do on three surfaces at once
rather than a silent promise of a Tuesday the server will not give. The
short-month line appears only past the 28th, where the surprise actually
exists.

No weekdays strip, no yearly rule, no end date on a job — expressed as a
`kinds` filter the job caller passes to the shared editor, not as a
capability removed from it: events legitimately keep all three.

Widening cited, per the new AGENTS.md rule this change also writes down:
the interim block barred the move on any job with a linked reward.
setparent.go has no reward guard and §11.B restored points on a sub-job,
so the bar is dropped.

Form-fragment sweep, files touched:
  JobSheet.tsx           the whole interim block deleted — `.field`,
                         `choice-row`, the inline RepeatEditor and two
                         hand-rolled confirms. FIXED.
  JobSheet.tsx           quick-add `<form>` + `<input>`, inert read
                         checkbox: pre-existing, verdicted with §5 — an
                         add-and-submit is one field, drawn in the row
                         silhouette. LEFT.
  JobOptionsSheet.tsx    clean — every entry and both dialogs' answers are
                         rows; no form-actions pair.
  JobRepeatSheet.tsx     clean; its footer is the one commit button in the
                         flow and it states the RULE, not "Save".
  RepeatEditor.tsx       `type="number"` ×2 — the spinner is chrome
                         nothing else on these screens has. FIXED
                         (inputMode="numeric").
  RepeatEditor.tsx       `.field` + `Until (optional)` — render only under
                         showUntil, which is the EVENT form. A form's
                         field is right on a form; the job sheet passes
                         false. LEFT, and the boxed interval is unboxed by
                         a `.job-repeat` scope so it reads as a value in a
                         sentence here without restyling the event form.
  JobSheet.kt            interim options block deleted. FIXED.
  JobSheet.kt            BasicTextField/TextButton in the quick-add:
                         pre-existing, same verdict as web. LEFT.
  JobOptions.kt          clean.
  RepeatEditor.kt        OutlinedTextField ×2 and `Until (optional)`:
                         the Compose twin of the web verdicts above. LEFT.

New goldens ride with the code (a new case has no image to fall back on):
jobsheet-job-options, -repeat-sheet, -delete-asks-and-counts and
-ticking-a-repeating-job-asks, hearth light and dark, both surfaces. No
existing case re-recorded.

Test report

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

Coverage: 27.0%

Updated by the check workflow · commit 589eb49d1b

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1435 | ✅ pass | 1 | | Integration | 130 | ✅ pass | — | **Coverage:** 27.0% <sub>Updated by the check workflow · commit 589eb49d1bedd93bc03fb4f1f7990e9b43f0f095</sub>
nalum force-pushed feat/job-options-repeat from 5f380adfbe
Some checks failed
check / commits (pull_request) Successful in 46s
check / go (pull_request) Successful in 7m42s
check / web (pull_request) Failing after 6m19s
check / report (pull_request) Successful in 4s
android / report (pull_request) Has been cancelled
android / build (pull_request) Has been cancelled
to 4847fb1325
All checks were successful
check / commits (pull_request) Successful in 30s
check / go (pull_request) Successful in 7m15s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 13m14s
android / build (pull_request) Successful in 30m24s
android / report (pull_request) Successful in 4s
2026-08-24 19:15:33 +00:00
Compare
Author
Owner

Force-pushed: a type-only import dropped while resolving this branch's rebase
onto the card commits, plus the gate hole that let it reach a deploy.

JobSheet.tsx uses the ItemRepeat type, and the resolution took one side of a
conflicting import line and lost it. tsc is clean now.

make check never type-checked the web app. Its recipe was fmt vet lint test web-test web-i18n-check android-i18n-check tokens-check — no web-build,
so tsc --noEmit never ran. web-test catches a missing binding only where a
test happens to execute that line, which is why the earlier missing value import
was caught and this type-only one was not. The Docker build runs tsc, so the
deploy found it.

web-build is now part of check. CI's web job always ran it; this only makes
the local gate agree with the pipeline.

AGENTS.md gains the corrected description of the gate and one line beside the
stacking rule: resolving a merge conflict is writing code, so run the suite
immediately after resolving rather than at the end of the chain — taking one
side of a conflicting import silently drops what the other side added.

Force-pushed: a type-only import dropped while resolving this branch's rebase onto the card commits, plus the gate hole that let it reach a deploy. `JobSheet.tsx` uses the `ItemRepeat` type, and the resolution took one side of a conflicting import line and lost it. `tsc` is clean now. **`make check` never type-checked the web app.** Its recipe was `fmt vet lint test web-test web-i18n-check android-i18n-check tokens-check` — no `web-build`, so `tsc --noEmit` never ran. `web-test` catches a missing binding only where a test happens to execute that line, which is why the earlier missing value import was caught and this type-only one was not. The Docker build runs `tsc`, so the deploy found it. `web-build` is now part of `check`. CI's web job always ran it; this only makes the local gate agree with the pipeline. AGENTS.md gains the corrected description of the gate and one line beside the stacking rule: resolving a merge conflict is writing code, so run the suite immediately after resolving rather than at the end of the chain — taking one side of a conflicting import silently drops what the other side added.

Android test report

Suite Tests Result Skipped
Unit (debug) ❌ 1 failed

Coverage:

Updated by the android workflow · commit 589eb49d1b

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | | ❌ 1 failed | | **Coverage:** <sub>Updated by the android workflow · commit 589eb49d1bedd93bc03fb4f1f7990e9b43f0f095</sub>
nalum force-pushed feat/job-options-repeat from 4847fb1325
All checks were successful
check / commits (pull_request) Successful in 30s
check / go (pull_request) Successful in 7m15s
check / report (pull_request) Successful in 4s
check / web (pull_request) Successful in 13m14s
android / build (pull_request) Successful in 30m24s
android / report (pull_request) Successful in 4s
to 589eb49d1b
Some checks failed
check / commits (pull_request) Successful in 17s
check / go (pull_request) Successful in 2m47s
android / build (pull_request) Failing after 4m53s
check / web (pull_request) Successful in 4m13s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
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
2026-08-26 07:03:41 +00:00
Compare
nalum changed target branch from test/job-card-goldens to main 2026-08-26 18:34:10 +00:00
nalum merged commit 589eb49d1b into main 2026-08-26 18:34:13 +00:00
nalum deleted branch feat/job-options-repeat 2026-08-26 18:34:16 +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!216
No description provided.