* fix(android): restore share title derivation and dedupe shared tasks
Commit d32f7037a3 accidentally reverted the EXTRA_SUBJECT handling from
edb102534e, so the share handler stopped sending the page subject and
defaulted the title to the literal "Shared Content". The frontend's
subject -> title -> derived title chain then always fell through to that
placeholder, producing blank-looking shared tasks.
- Restore EXTRA_SUBJECT extraction; leave title/subject empty when absent
so the frontend can derive a meaningful title from the URL or note.
- Ignore empty/blank shared text instead of creating a useless task.
- Skip handleIntent() on Activity recreation (config change) so the same
share Intent isn't re-processed into a duplicate task.
- Extract buildTaskTitle/readableUrl as pure functions with unit tests
and guard the effect against empty payloads.
* fix(snack): scale error/warning snack duration with message length
Long error messages (e.g. multi-sentence sync errors) were auto-dismissed
after a fixed 8s, too short to read. Error/warning snacks now stay visible
proportional to message length (~90ms/char), clamped to 10-30s.
* feat(tasks): skip undo snack when deleting a blank task
A sub task or parent task with an empty title and no data (notes, time,
estimate, attachments, issue link, reminder, repeat, scheduling,
deadline, non-blank sub tasks) no longer shows the undo-delete snack.
* fix(sync): include archive data in REPAIR operations
validateAndRepairCurrentState built the REPAIR op from the synchronous
getStateSnapshot(), which hardcodes empty archiveYoung/archiveOld
(archives live in IndexedDB, not NgRx state). The resulting REPAIR op
carried empty archives, so every other client that applied it
overwrote its archive with nothing — wiping archived tasks on all
devices except the one that ran the repair.
- Use getStateSnapshotAsync() so the REPAIR op carries real archives.
- Extend the empty-archive overwrite guard in
ArchiveOperationHandler._handleLoadAllData() to also cover OpType.Repair
(previously only SYNC_IMPORT/BACKUP_IMPORT), as defense in depth.
* test(sync): add archive REPAIR round-trip integration test
Wires the real StateSnapshotService, ArchiveDbAdapter, ArchiveStoreService
and ArchiveOperationHandler against real IndexedDB to verify archive data
survives the REPAIR-op round-trip:
- getStateSnapshotAsync() loads IndexedDB archives; getStateSnapshot() does not
- archive round-trips from client A's IndexedDB through a REPAIR op into a
fresh client B's IndexedDB
- a REPAIR op carrying empty archives no longer wipes a client that has
archive data (empty-archive guard regression)
* perf(sync): skip archive IndexedDB reads when post-sync state is valid
validateAndRepairCurrentState validated the full async snapshot (two
IndexedDB archive reads + structured-clone deserialization) on every
Checkpoint D, even when state was valid and no repair was needed. It now
validates the cheap synchronous snapshot first and only loads the async
snapshot (with archives) when a repair is actually required — the rare
path. The REPAIR op still carries archive data.
Also addresses multi-review follow-ups:
- archive-operation-handler: reword the empty-archive guard comment so it
no longer over-promises reconciliation for REPAIR ops.
- archive-repair-roundtrip test: add isPersistent to the applied-op meta
to match the real applier; scope the file docstring accurately.
* fix(task-repeat-cfg): schedule inbox task for today when made recurring
When an Inbox task (no dueDay) was made repeatable via the dialog with a
recurrence starting today, it stayed unscheduled. The TODAY-first-occurrence
branch of updateTaskAfterMakingItRepeatable$ derived currentDueDay from
task.created as a fallback, so a task created today looked already scheduled
and dueDay was never set.
Key the decision on task.dueDay directly. Skip timed tasks and tasks that
already have dueWithTime, since dueDay/dueWithTime are mutually exclusive and
timed scheduling is handled by addRepeatCfgToTaskUpdateTask$.
Closes#7725
* fix(tasks): correct monthly first/last-day recurrence anchoring
The "Every month on the first day" and "Every month on the last day"
quick settings scheduled the first task instance in the past. Both
presets produced a backdated startDate (1st of the current month;
hardcoded January 31), which getFirstRepeatOccurrence returns verbatim
for monthly recurrences.
- MONTHLY_FIRST_DAY now anchors startDate to the next 1st-of-month
that is today or later.
- MONTHLY_LAST_DAY anchors startDate to the current month's last day
and sets a new monthlyLastDay flag, so the occurrence engine clamps
to month-end every month regardless of startDate's day-of-month.
- _normalizeMonthlyAnchor strips a stale monthlyLastDay flag when a
config leaves the preset (CUSTOM mode has no control for it).
Closes#7726
* fix(task-repeat-cfg): re-anchor start date after instance deleted
When the user moved a repeat config's startDate earlier after deleting
its only live task instance, the stale lastTaskCreationDay anchor kept
suppressing every projected/created instance between the new startDate
and the old anchor.
rescheduleTaskOnRepeatCfgUpdate$ only re-anchored lastTaskCreationDay
when a live task instance existed (the #7423 fix) — it returned early
before the re-anchoring when there was none. Hoist the
isStartDateMovedEarlier detection above that early return: when no live
instance exists but startDate moved earlier, re-anchor to the day
before the new first occurrence so it and every following day is
created and projected fresh.
Closes#7724
* test(task-repeat-cfg): cover startDate re-anchor with no live instance
Add coverage for the #7724 fix beyond the effect unit test:
- Selector integration tests: feed a config re-anchored to the day
before the new startDate through selectTaskRepeatCfgsForExactDay and
assert it projects the new startDate and every following day, while
still excluding the anchor day and earlier. Also documents that the
stale anchor suppresses the gap days.
- E2E reproduction (recurring-move-start-date-earlier-no-instance):
create a recurring task, delete its live instance, move startDate
earlier via a transparent projection, and assert the new days appear
in the planner. Verified to fail on pre-fix code.
* feat(sync): move clientId from pf into SUP_OPS for atomic rotation
Migrate the sync clientId out of the legacy `pf` IndexedDB database into
`SUP_OPS` (new `client_id` store, schema v6). The clientId write now joins
the atomic transaction in `runDestructiveStateReplacement`, so destructive
flows (clean-slate, backup-restore) rotate it atomically with
OPS/STATE_CACHE/VECTOR_CLOCK instead of a hand-rolled cross-database
two-phase commit.
- ClientIdService rewritten: SUP_OPS-backed via an independent connection,
inline one-time pf->SUP_OPS migration, error-aware resolver. Read
failures propagate (getOrGenerateClientId never mints a fresh id over a
transient error — that would orphan the device's non-regenerable
identity); loadClientId never throws.
- Delete withRotation, generateNewClientId and the CAS/rollback machinery.
- Extract pure generateClientId() + isValidClientIdFormat() into
core/util/generate-client-id.ts.
- pf becomes a read-only, one-time migration source (never written/deleted).
Closes#7732
* fix(sync): dedup SUP_OPS connection open in ClientIdService
Address multi-agent review findings on the clientId migration:
- _getSupOpsDb() shares a single in-flight open via _supOpsDbPromise;
concurrent cold-start callers previously each opened their own
SUP_OPS connection, leaking all but the last.
- _putClientIdIfAbsent() collapsed to a single tx.done / exit point.
- db-upgrade.spec.ts: cover the v6 client_id store; the createObjectStore
count assertions were stale and failing after the schema bump.
- operation-log-migration.service.ts: correct a misleading comment about
the genesis-op clientId fallback.
* refactor(sync): align ClientIdService SUP_OPS open with in-house idiom
Re-review of the connection-leak fix recommended matching
OperationLogStoreService._ensureInit's pattern:
- _getSupOpsDb() clears the in-flight promise in .catch (failure only)
instead of an unconditional finally — the resolved handle lives in
_supOpsDb, so the promise field is pure in-flight coordination state.
- close/versionchange handlers now also null _supOpsDbPromise, so a
stale (closed) connection is never re-handed-out.
- Add a regression test asserting _openSupOpsDb runs exactly once for
concurrent cold-start callers.
* docs(sync): link the single-connection follow-up to #7735
Reference the tracked follow-up issue from the ClientIdService JSDoc
and the plan's out-of-scope section, so the deliberate trade-off (one
extra SUP_OPS connection) is traceable rather than forgotten.
* test(sync): update ClientIdService spies for getOrGenerateClientId
The op-log capture effect now resolves the clientId via
getOrGenerateClientId() (was loadClientId() ?? generateNewClientId()).
These two specs still mocked only loadClientId, so the effect called an
undefined method, captured no op, and 4 tests failed in the full suite.
- task-done-replay.integration.spec.ts
- operation-log-lock-reentry.regression.spec.ts
* test(sync): open SUP_OPS versionless in e2e read helpers
DB_VERSION was bumped 5->6; five e2e helpers still opened SUP_OPS at
the hardcoded old version 5 to read state after the app had already
upgraded it to v6, throwing VersionError. Open versionless instead —
matches the ~10 other e2e files that already do, and is future-proof
against the next schema bump.
- migration/legacy-data-migration.spec.ts (x2)
- sync/supersync-legacy-migration-sync.spec.ts
- sync/webdav-legacy-migration-sync.spec.ts
- recurring/invalid-clock-string-bug-7067.spec.ts