Skip to content

feat(nag): sync recurring task instance completion via ActiveSync#30

Merged
TDannhauer merged 2 commits into
FRAMEWORK_6_0from
fix/activesync-recurring-task-completion
Jun 24, 2026
Merged

feat(nag): sync recurring task instance completion via ActiveSync#30
TDannhauer merged 2 commits into
FRAMEWORK_6_0from
fix/activesync-recurring-task-completion

Conversation

@TDannhauer

Copy link
Copy Markdown
Contributor

Summary

  • Apply single-instance completion, un-completion, and series-master updates from EAS clients (iOS Reminders, Outlook) to the recurring Nag task, keeping one canonical row per series.

Motivation

Completing one occurrence of a recurring task on a device did not stick in Nag and could spawn duplicate task rows: clients send a dead-occurrence Add (often reusing a client id) plus a master Modify, which the previous import/replace path mishandled. Un-completing an occurrence (including the final one) did not reopen the series correctly.

Changes

  • Nag_Task::fromASTask() — route dead-occurrence Adds and master Modifys to dedicated recurrence handlers; record completions[] instead of calling toggleComplete().
    • _applyActiveSyncRecurrenceInstance() — complete/uncomplete a single occurrence, with rollback of the master due date when reopening a past instance.
    • _applyActiveSyncRecurrenceMasterChange() — apply next-due advance and reopen the final instance of a fully-complete series; preserve the total RRULE COUNT (clients send the remaining occurrence count, not the series total).
    • _setActiveSyncCompletion() — shared completion-flag/date helper.
  • Nag_Api::replace() / import() — resolve the series master for instance messages and merge dead-occurrence Adds into the master UID (across editable tasklists); expose resolveActiveSyncSeriesMasterUid().
  • Nag_Driver::modifyFromHash() — persist recurrence/completion state without toggleComplete() side effects.
  • Export (toASTask()) reports the remaining occurrence count and next due date.

Companion PRs

  • horde/core: resolveTaskSeriesMasterUid() bridge.
  • horde/activesync: master-change detection and importer master resolution.

Test plan

  • vendor/bin/phpunit -c vendor/horde/nag/phpunit.xml.dist --bootstrap vendor/autoload.php vendor/horde/nag/test/Nag/Unit/Task/ActiveSyncRecurrenceTest.php (29 tests)
  • Manual device test (iOS 16.1): complete instances, uncomplete an earlier instance, complete whole series, reopen final instance — Nag stays in sync, no duplicate rows.

Apply single-instance completion, un-completion, and series-master
updates from EAS clients (iOS, Outlook) to the recurring Nag task
without spawning duplicate rows.

- fromASTask(): route dead-occurrence Adds and master Modifies to
  dedicated recurrence handlers; track completions[] instead of toggling
- Api::replace()/import(): resolve the series master for instance
  messages and merge dead-occurrence Adds into the master UID
- Driver::modifyFromHash(): persist recurrence/completion state without
  toggleComplete() side effects
- preserve the total RRULE COUNT on master Modify (clients send the
  remaining-occurrence count, not the series total)

Adds Nag_Unit_Task_ActiveSyncRecurrenceTest covering completion,
un-completion, master advance, final-instance reopen, and count
preservation.
@TDannhauer TDannhauer requested a review from ralflang June 24, 2026 15:07
Recurring tasks whose series is fully complete (or whose due date is
implausible) return null from Nag_Task::getNextDue(). The task detail
view and the portal Summary block dereferenced the result directly,
causing "Call to a member function timestamp() on null". Guard both
call sites.
@TDannhauer TDannhauer merged commit 8a6ff3e into FRAMEWORK_6_0 Jun 24, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants