Chores: due dates, calendar repeats and a per-chore "show from" - #159
Conversation
A chore can now carry a due date and repeat in days, weeks, months or
years ("house insurance, due 1 Oct 2026, every year"). The stored date is
the anchor; the next due date is derived from it and the latest log, so a
log a little early or late moves the chore to the next occurrence and
undoing a log needs no extra code.
Schema: tags gains nullable due_date and repeat_unit (CHECK week/month/
year; null means days). interval_days keeps the approximate length so the
web app and cadence buckets keep working. The edit PATCH names the new
columns only when they hold or held a value, so plain edits still save on
a database that has not had schema.sql applied yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H5p1rGPAJ2yaLENgnsDJUo
A chore can now carry lead_days: keep it in the hidden section until that many days before it is due. It wins over both the smart-visibility cadence lead and the legacy 60-day distant rule; null leaves those in charge. A pinned chore still always shows. The sheet gains a "Show from" row (Auto / Days before due…). Repeat, due date and lead days now travel together as ChoreSchedule from the sheet through the view model to the repository. Schema adds tags.lead_days (CHECK >= 0) with the same only-send-when-used PATCH rule as due_date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5p1rGPAJ2yaLENgnsDJUo
The per-chore lead time moves out of Supabase into ChoreLeadStore (DataStore, tag id to days), alongside swipe-to-snooze and the bucket lead times: when a chore shows up is a per-phone display choice, so one person's setting no longer hides it for the rest of the household. tags.lead_days, its CHECK and the PATCH handling are removed again; ChoreSchedule is back to repeat and due date. The list state reads the overrides from leadOverrides, the sheet opens with this phone's value and saves it through the view model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5p1rGPAJ2yaLENgnsDJUo
mapgie
left a comment
There was a problem hiding this comment.
Reviewed the full diff (all 18 files) and traced the core scheduling logic against its tests. a11y_check.py and check_changelog_fragment.py both pass locally. No correctness bugs found; a few minor observations below.
What's well done
- The "never rewrite
due_date" design (LESSONS #64).nextChoreDueDatederives the next occurrence from the anchor plus the latest log, so all four undo paths and web-app logs work with no extra code. Occurrences are counted from the anchor (anchor.plusMonths(k)), never chained, which correctly avoids month-end drift (31 Jan then 28 Feb then 31 Mar). The bracket-and-nearest loop is monotonic and terminates, and the "before the anchor never comes due" clamp (maxOf(ticked + 1, 0)) is right. - Backward-compatible schema.
ADD COLUMN IF NOT EXISTSplus the re-asserted CHECKs in "Constraint sync" keepsschema.sqlidempotent, andSchemaSyncTestnow guardsrepeat_unitand the ALTERs. Keepinginterval_daysas the approximate length preserves the web app and cadence buckets. - PATCH gating.
chorePatch(..., includeSchedule)names the new columns only when there is a value or the row already had one, so plain edits still save against a project that has not applied the migration.ChorePayloadTestcovers the explicit-null-clear and lead-days-alone cases. - Tests read as behaviours, not methods, with dates sat mid-window so a run cannot straddle a boundary.
dueSoonDays, the Overdue chip, the DUE sort, and all three "show from" precedence rules are covered, and the newValueChip/ dropdown rows carry a11y roles.
Minor observations (non-blocking)
- Extra Supabase read per shared edit.
updateTag'sSTAY_SHAREDpath now callsfindShared(tagId)before everypatchShared, purely to computehadSchedule. That is a new round-trip on every plain chore edit, and it is only actually needed during the pre-migration window; once the columns exist you would always send them. Reasonable tradeoff, but worth a comment noting it can be dropped when the schema is known-applied. ChoreRepeat.fromdisplay mismatch on externally-edited rows. Ifinterval_daysis edited elsewhere to a non-multiple of the unit (e.g. 45 on arepeat_unit='month'row), the app falls back toevery 45din the caption while the storedrepeat_unitstill saysmonth. Harmless and documented in the KDoc, just flagging that stored unit and shown unit can disagree in that case.- A done one-off dated chore stays in the main list reading "done" (status FRESH, not hidden) until archived. This is stated as intended in the PR body, so not a defect; it is just the one place a dated chore does not self-tidy the way a repeating one does.
Nothing here needs a change before merge.
Generated by Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5p1rGPAJ2yaLENgnsDJUo
|
Following up on the review:
Generated by Claude Code |
Every chore repeats, so a due date now only counts alongside a repeat. The sheet shows the Due date row only once a repeat is set and drops a leftover date when the repeat is cleared. A row that still has a date but no repeat (from the web app, say) is treated as undated and timed from its last log. The one-off rules go: ONE_OFF_EARLY_DAYS, the "done" badge and the nullable next due date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5p1rGPAJ2yaLENgnsDJUo
README: chores bullet, project map (ChoreSchedule.kt, ChoreLeadStore), where "Show from" lives, a "When a chore is due" section, and a note to re-run schema.sql for the new tags columns. In-app help mentions due dates and the per-chore Show from row. CLAUDE.md gains a map row for the due-date seam and the facts that every chore repeats and Show from is on-device. ChoreLeadStore's string codec moves into a pure ChoreLeadCodec with its own JVM test (round trip, '|' in a tag id, bad entries, back to Auto). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5p1rGPAJ2yaLENgnsDJUo
|
Follow-up on the on-device "Show from" store: The KDoc says it follows the same scheme as
Two low-cost options, either is fine:
Option 1 is the smaller change and keeps the store honest even if a future delete path forgets to clean up. Generated by Claude Code |
ChoreLeadStore kept a <tagId>|<days> entry forever once written, unlike ChoreSnoozeStore, which self-prunes stale entries on read. A hard-deleted chore (or a future delete path) would leave its "show from" behind, and because a tag id is an NFC id reused across the id space, that setting could later reattach to a different chore landing on the same id. ChoreLeadStore.retainOnly drops entries whose tag id is not among the chores that still exist, reconciled after every successful load against active + archived (the complete shared and private set). It writes only when there is something to prune. The pure ChoreLeadCodec.retainOnly is tested on the JVM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dZB2iMJJxM2AjbJTMEpbu
Repeating chores can now have a due date. The repeat can be counted in days, weeks, months or years, e.g. "House insurance, due 1 Oct 2026, every year". Each chore also gets its own "Show from" setting, kept on this phone only. There are no one-off chores: a due date only applies when the chore also has a repeat.
What changes for the user
1 y,2 w.0means "on the day".admin · yearly · due 1 Oct. The badge counts whole days:9d left,due today,4d over.min(7, repeat/2)days before it. A dated chore that hasn't been logged yet is not shown as "never done".Next due date
The stored
due_dateis the date the user entered, and it is never rewritten.nextChoreDueDate(anchor, repeat, lastLog)works out the next due date from it and the latest log:Logging never writes to the chore, so every undo path works with no extra code (LESSONS #64). A row with a date but no repeat, e.g. one set from the web app, is treated as undated and timed from its last log.
Where things are stored
tags.due_date dateandtags.repeat_unit text CHECK (repeat_unit IN ('week','month','year')). A null unit means days.ADD COLUMN IF NOT EXISTS, and the CHECK is re-asserted in the constraint sync.interval_daysstill holds the approximate length (a year is stored as 365), so the web app and the cadence buckets keep working.chorePatchonly names the new columns when they hold a value or held one before. That way plain edits still save before the schema is applied. The extra read this needs is commented as removable later.ChoreLeadStore(DataStore, keyed by tag id) and reaches the list throughChoreUiState.leadOverrides.Tests
ChoreScheduleTest:ChoreUiStateTest:0meaning the due day, pinned choresChorePayloadTest: which columns are sent, explicit nulls on clear, andlead_daysnever sent.SchemaSyncTest:RepeatUnitvalues are allowed by the CHECK, and the ALTERs are present.SheetDraftTest:Changelog fragment:
minor.🤖 Generated with Claude Code
https://claude.ai/code/session_01H5p1rGPAJ2yaLENgnsDJUo