* fix(sync): strip same-batch-archived task IDs from TAG/PROJECT LWW payloads
Issue #7330. lwwUpdateMetaReducer's orphan filter only sees taskState as it
is when each op runs. A TAG LWW Update applied before its sibling archive
op in the same bulk batch escapes the filter, leaving TODAY_TAG (or any
tag/project) referencing a task the very next op removes — user-visible
as "archived tasks reappear in today's view" on hibernate-wake.
bulkOperationsMetaReducer already collects archivingOrDeletingEntityIds
for the wholesale TASK LWW Update skip; this commit reuses that set to
pre-clean taskIds (and PROJECT-only backlogTaskIds) on TAG/PROJECT LWW
Update payloads before convertOpToAction. Cross-batch ordering is not
addressed — see open issue.
* fix(sync): surface post-sync validation failure as ERROR, not IN_SYNC
Issue #7330. The reporter saw "State validation failed after sync. Some
data may be inconsistent." while sync simultaneously reported status
IN_SYNC. validateAfterSync's boolean was discarded above the snackbar
layer (#6571 only added the snackbar; nothing flowed up to the status
pipeline).
Plumb the boolean through:
- validateAfterSync: void -> boolean
- _validateAndRepairAfterResolution: void -> boolean
- autoResolveConflictsLWW + processRemoteOps: add validationFailed
- DownloadOutcome.ops_processed + UploadOutcome.completed: add
validationFailed
- sync-wrapper: if either result.validationFailed, setSyncStatus('ERROR')
and return HANDLED_ERROR before the IN_SYNC mark
Tests: assert validateAfterSync returns the boolean, assert
processRemoteOps surfaces validationFailed, assert sync-wrapper sets
ERROR (not IN_SYNC) when either download or upload reports
validationFailed.
* fix(sync): always set top-level id on LWW Update payloads
lwwUpdateMetaReducer drops LWW Updates whose payload lacks a top-level
id ("Entity data has no id"). Two creation paths could produce such
payloads:
- createLWWUpdateOp passed entityState through unchanged, so a malformed
selector result silently produced an unusable LWW op.
- _convertToLWWUpdatesIfNeeded built mergedEntity by spreading baseEntity
and updateChanges; when baseEntity lacked id (corrupt DELETE payload)
and updateChanges had id stripped, the merge had no id either.
Both sites now force the canonical entityId onto the payload. (#7330)
* fix(sync): close 3 secondary gaps in #7330 fixes
Multi-agent review surfaced three additional code paths mirroring the
same defects we fixed at the primary path:
G1: bulkOperationsMetaReducer's pre-scan only saw op.entityIds /
op.entityId for archive/delete ops. moveToArchive declares only
top-level task IDs, but the reducer cascades to subtasks via
[t.id, ...t.subTasks.map(st => st.id)]. deleteTask cascades the same
way via taskToDelete.subTaskIds. Same-batch TAG/PROJECT LWW Updates
referencing those subtasks slipped through the strip. Add
collectCascadedSubTaskIds to harvest subtask IDs from archive/delete
payloads.
G2: SyncWrapperService's LWW re-upload retry loop captured only
reuploadResult.localWinOpsCreated, throwing away validationFailed.
A retry-pass piggybacked download with failing post-sync validation
still reported IN_SYNC. Track reuploadValidationFailed across
retries and OR into the gate before IN_SYNC.
G3: OperationLogSyncService's downloadCallback (used by the
rejected-op handler) converted the download outcome into
DownloadResultForRejection, which has no validationFailed field.
A nested download triggering validation failure was lost before
uploadPendingOps returned. Route the boolean via the existing
piggybackValidationFailed flag in the closure scope.
Also fixes one cosmetic logging field (reuploadValidationFailed
included alongside download/upload in the ERROR log).
* fix(sync): defense-in-depth id derivation in lwwUpdateMetaReducer
Multi-agent review surfaced two follow-up improvements for #7330:
1. Consumer-side id fallback: a future LWW Update producer that
forgets to backfill payload.id would silently regress the
"Entity data has no id" bail. Recover from action.meta.entityId,
which convertOpToAction always populates from the canonical
op.entityId. Now a single consumer guards the whole class of
missing-id producer regressions.
2. Tighten 7 conflict-resolution.service.spec assertions from
jasmine.objectContaining({ localWinOpsCreated: N }) back to
strict toEqual({...}). The loosening (added when validationFailed
was introduced) hid future stray-field regressions; the validation
path now returns a deterministic shape per code branch, so we can
assert it.
* fix(sync): close issues found in follow-up review (#7521)
Multi-agent review of the prior #7330 follow-up commits surfaced four
real issues. All addressed here:
CRITICAL — singleton id pollution at producer + consumer
Singletons use entityId='*' as a sentinel. Both the producer-side
payload-id backfill (createLWWUpdateOp + _convertToLWWUpdatesIfNeeded)
and the consumer-side meta.entityId fallback in lwwUpdateMetaReducer
injected `id: '*'` into payloads. For singletons (GLOBAL_CONFIG,
app-state, time-tracking) this leaks a synthetic `id: '*'` field
into singleton feature state when the reducer spreads entityData.
Gate id-injection on entityId \!== '*' at all three sites and move
the consumer fallback past the singleton branch.
WARNING — null-safety in collectCascadedSubTaskIds
`'actionPayload' in payload` check could pass for malformed
payload {actionPayload: null}, then crash on the next property
access. Tighten to a typeof check.
WARNING — retries-exhausted path bypasses validationFailed gate
When the LWW re-upload loop exits via MAX retries with pending
ops still > 0, sync-wrapper used UNKNOWN_OR_CHANGED without
consulting reuploadValidationFailed. Validation failure during
a retry pass is now elevated to ERROR — a more honest signal
than UNKNOWN_OR_CHANGED, and consistent with the user-visible
"State validation failed" snackbar they already saw.
SUGGESTION — devError on consumer fallback
Producer regressions should scream loudly in dev, not slip
through silently. The fallback now emits devError + applies
the recovery, so a future LWW producer that forgets payload.id
surfaces in dev rather than only-when-the-bail-fires.
* fix(sync): close validationFailed gap in USE_REMOTE and DELETE_MULTIPLE cascade
Three issues found by multi-agent review of the #7330 sync fixes:
1. forceDownloadRemoteState() discarded the validationFailed result
from processRemoteOps. A USE_REMOTE conflict-resolution path that
applied corrupt remote state would still mark sync IN_SYNC despite
the snackbar. Now returns { validationFailed } and propagates it
through _handleSyncImportConflict and DownloadOutcome.no_new_ops
to SyncWrapperService, plus the two direct callers (manual force
download and first-sync conflict resolution).
2. sync-wrapper retry-exhaustion priority: when LWW retries exhausted
AND the initial download/upload had reported validationFailed, the
wrapper returned UNKNOWN_OR_CHANGED instead of ERROR. The hoisted
downloadValidationFailed/uploadValidationFailed are now checked
inside the retry-exhaustion branch alongside reuploadValidationFailed.
3. bulk-hydration pre-scan handled DELETE_MULTIPLE in the loop but the
collectCascadedSubTaskIds helper early-returned for that action type.
Since deleteTasks payload only carries flat taskIds, subtask cascade
must be derived from initial batch state (mirroring what
handleDeleteTasks does at apply time). Co-batched TAG/PROJECT LWW
Updates referencing those subtasks could otherwise still leak.
Adds regression tests for each path.
* refactor(sync): centralize LWW id backfill, extract singleton sentinel constant
Follow-up simplifications from the multi-agent review of #7330 fixes.
- Extract SINGLETON_ENTITY_ID = '*' + isSingletonEntityId() helper in
entity-registry.ts. Replaces the bare '*' literal across
conflict-resolution.service.ts, lww-update.meta-reducer.ts,
validate-operation-payload.ts, operation-log-recovery.service.ts,
and operation-log-migration.service.ts. (S2)
- Add LWW Update payload.id backfill at the apply boundary in
convertOpToAction. Every applied LWW op now has its top-level id set
from op.entityId (excluding singletons). The redundant defense-in-depth
block in lwwUpdateMetaReducer (which derived id from meta.entityId) is
removed — the converter is now the single chokepoint, with producers
(createLWWUpdateOp, _convertToLWWUpdatesIfNeeded) keeping their
enforcement for an explicit on-disk shape. (S1)
- stripBatchArchivedTaskIdsFromLwwPayload: drop non-string entries
rather than preserving them, and skip the cleaned[] allocation when
no rewrite is needed (two-pass with early exit on first hit). (S5+S7)
* refactor(sync): extract bulk-archive util, unify orphan-task filters, type validation propagation
Continued review follow-ups for #7330. Behavior unchanged.
- Move payload-archaeology helpers (collectCascadedSubTaskIds,
stripBatchArchivedTaskIdsFromLwwPayload) out of bulk-hydration.meta-reducer
into a co-located bulk-archive-filter.util.ts. The meta-reducer body
drops back to its dispatcher role; the helpers gain an independent
test surface and clearer naming. (S3)
- Add a shared filterTaskIdArraysFromTagOrProjectPayload helper. Both
filterOrphanedTaskIdsFromEntityData (lww-update.meta-reducer; predicate:
not in live state) and stripBatchArchivedTaskIdsFromLwwPayload (bulk
meta-reducer; predicate: in same-batch archive set) now wrap it. The
two callers run at different layers because their predicates resolve
at different times — the shared helper is the array-walking +
payload-cloning + warn-logging core. (S4)
- Replace the closure-captured piggybackValidationFailed boolean in
uploadPendingOps with typed plumbing: validationFailed flows through
DownloadResultForRejection and RejectionHandlingResult. The closure
smuggle was fragile; the typed return is grep-able and survives
refactors. The wider session-latch refactor proposed in review is
deferred — current typed plumbing is well-tested. (S6 minimal)
* refactor(sync): replace validationFailed plumbing with session latch + integration test
Final review follow-up for #7330. Behavior unchanged; surface area shrinks.
The validation-failed signal previously rode through 7 typed boundaries:
DownloadOutcome.{ops_processed,no_new_ops}.validationFailed,
UploadOutcome.completed.validationFailed, processRemoteOps return,
forceDownloadRemoteState return, _handleSyncImportConflict return,
DownloadResultForRejection.validationFailed, RejectionHandlingResult
.validationFailed, plus a closure-captured piggybackValidationFailed in
uploadPendingOps. Threading it correctly required every call site to
remember to forward the boolean — a new variant or path that forgot
would silently let IN_SYNC ride over corrupt state.
- Add SyncSessionValidationService — a no-dep singleton with reset() /
setFailed() / hasFailed(). RemoteOpsProcessingService.validateAfterSync
and ConflictResolutionService.autoResolveConflictsLWW flip the latch
when validation reports corruption. SyncWrapperService resets it at
every entry point (sync(), _forceDownload(), resolveSyncConflict
USE_REMOTE) and reads it once before deciding IN_SYNC vs ERROR.
- Drop validationFailed from DownloadOutcome variants, UploadOutcome,
DownloadResultForRejection, RejectionHandlingResult, processRemoteOps
return, autoResolveConflictsLWW return, forceDownloadRemoteState
return, and _handleSyncImportConflict return. ~120 LOC of plumbing
collapsed to single latch reads.
- Add a focused integration test
(post-sync-validation.integration.spec.ts) that wires real
RemoteOpsProcessingService + ConflictResolutionService against a
stubbed ValidateStateService. Asserts the latch flips on validation
failure and survives discarded-boolean callers — catching plumbing
regressions a future code path could otherwise sneak past.
Net change vs current branch: ~120 LOC deleted from production sources,
~270 LOC added in new service + tests (latch unit tests + integration).
* fix(sync): close 3 latch-bypass gaps from codex review
1. SyncHydrationService.hydrateFromRemoteSync runs validateAndRepair()
directly and previously dropped the result on failure. Snapshot
hydration (file-based providers, USE_REMOTE force-download) would
silently accept corrupt remote data — the wrapper would see a clean
latch and report IN_SYNC. Now flips
SyncSessionValidationService.setFailed() when isValid is false.
2. WsTriggeredDownloadService called downloadRemoteOps() outside the
wrapper session contract, so any validation failure during a realtime
apply was either dropped (next sync()'s reset cleared it) or leaked
into the next session. The service now resets the latch up-front and
reads it after the download, surfacing failures as
setSyncStatus('ERROR').
3. convertOpToAction only backfilled payload.id when missing. A
malformed/older remote LWW op with payload.id \!= op.entityId would
slip through and update the WRONG entity in lwwUpdateMetaReducer
(which trusts entityData.id). Now forces id from op.entityId for
non-singleton LWW ops, making "entityId is canonical" a hard
invariant at the apply boundary.
Adds regression tests for each: integration test for the snapshot path,
two new tests on ws-triggered-download (latch flip → ERROR; latch reset
between sessions), and a converter test for the entityId-mismatch case.
* test(sync): unstick three failing supersync e2e tests
Three independent flakes surfaced in the same run; root-caused from
playwright traces:
- daily-summary: time-estimate row icon was renamed timer→hourglass_empty
on master (eca8d211a) but the selector was never updated on this branch.
Cherry-pick of master's 4be835017 selector update.
- multi-migration: 120s test budget is too tight for 3 sequential
setupSuperSync + 4 syncAndWait cycles under parallel @supersync load.
Trace shows test reaches the post-sync 1.5s grace wait — Client B has
already received all 3 tasks — and times out there. Bumped just this
test to 180s.
- lww-conflict notification: done-toggle dispatches updateTask
asynchronously; without an explicit wait, sync runs as a no-op (latch
trace shows uploaded=0, status=IN_SYNC at 2459705) and the queued
updateTask lands ~13ms later, flipping hasNoPendingOps back and hiding
the check icon → syncCheckIcon waitFor times out. Added the same
expect(...).toHaveClass(/isDone/) wait that the file's other LWW tests
(lines 83, 376, 389) already use.
* refactor(sync): self-enforcing session-validation contract via withSession()
The previous SyncSessionValidationService API was reset() / setFailed() /
hasFailed() with a doc-comment contract: "every sync entry point must call
reset() before doing work." A new entry point added later (e.g., a
background download path) that forgot the reset would inherit a
leaked-failed latch from a prior session, and #7330's IN_SYNC-vs-ERROR
decision would misfire silently. The maintenance hazard was the largest
reason this code wasn't self-protecting.
Replace with a callback API:
withSession<T>(work: () => Promise<T>): Promise<T>
Resets the latch at entry, marks a session active for the duration of
work, clears the marker on completion or error. setFailed() and reset()
log a noisy SyncLog.err if called outside an active session — a runtime
guard for "validation fired without an entry point opening a session."
Nested withSession() calls are detected and run in the outer session's
context (no inner reset clobbers outer state).
Three production entry points migrated:
- SyncWrapperService._sync() — body extracted to _syncBody(providerId)
to avoid a 400-line indent diff
- SyncWrapperService._forceDownload()
- WsTriggeredDownloadService._downloadOps()
The fourth caller of reset() — the USE_REMOTE branch in
_handleLocalDataConflict — keeps using reset(), now correctly within the
session opened by _sync()'s withSession. It's a sub-scope re-scoping
inside an existing session, not a top-level entry point. The new
reset() docs make this distinction explicit.
Test surface:
- sync-session-validation.service.spec.ts rewritten: 12 tests cover the
callback semantics, nested-session guard, and outside-session warnings.
- post-sync-validation.integration.spec.ts wraps each scenario in
withSession() (mirrors production); replaces the old "reset() between
sessions" test with one that asserts withSession() entry resets state.
- sync-wrapper.service.spec.ts unchanged — its tests fire setFailed()
inside mocked download/upload callbacks, which now run inside _sync()'s
session. 107/107 still pass.
- WsTriggeredDownloadService spec: latch.reset() in setup → _resetForTest()
to avoid the new outside-session warning; the "stale latch" test seeds
internal state via the test-only helper.
reset() and setFailed() are still public — they're load-bearing for
the USE_REMOTE sub-scope and for validation services that need to flip
the latch from inside session-wrapped flows. The contract is now:
top-level entry points use withSession; validation sites use setFailed
unchanged; only USE_REMOTE recovery uses reset.
#7330
* fix(sync): warn when convertOpToAction rewrites a mismatched payload.id
The id-rewrite guard added in 41b47be4 silently fixes producer/wire bugs
(an LWW op whose payload.id disagrees with op.entityId would otherwise
update the wrong entity in lwwUpdateMetaReducer). Correct in direction,
but invisible — if the assumption that "no legitimate code path produces
mismatched ops" ever breaks, we'd only learn from a user-reported corrupt
entity.
Log a SyncLog.warn with ids only (actionType, entityType, entityId,
payloadId) when the rewrite fires. Never log payload content — op log is
exportable and #7330 already caused us to audit that surface for user
data leaks.
Two new tests: warning fires with the expected id pair on mismatch; no
warning on the happy path. Sanity assertion verifies payload content
(title) doesn't appear in the log call args.
#7330
* test(sync): static check enumerates withSession() entry-point allow-list
CI-time guard for the latch maintenance hazard: greps the eight production
sync sources for `.withSession(` callers and asserts the count matches an
explicit allow-list (3 today: _sync, _forceDownload, _downloadOps).
Brittle on purpose. A future contributor adding a 5th sync entry point
will see this fail and have to read the contract in
sync-session-validation.service.ts before updating ALLOWED_ENTRY_POINTS.
That's the friction the runtime guards (the new withSession callback
API + outside-session warnings) can't deliver on their own — they catch
forgotten resets, but not "added a new top-level entry point without
considering whether it's a session boundary at all."
Wired into `npm run lint` via a new lint:sync-sessions step. Tried a
Karma spec first, but Karma doesn't serve the workspace at /base/ paths
without explicit config — a Node script via existing tools/ infrastructure
is simpler and runs the same in CI.
Verified by appending a fake 4th caller to operation-log-sync.service.ts
and confirming the script exits 1 with a contributor-friendly message
pointing at the contract file. Reverted before this commit.
#7330
* fix(sync): wrap ImmediateUploadService._performUpload in withSession()
Fourth #7330 entry point. uploadPendingOps() processes piggybacked
remote ops, which run validateAfterSync(); on corruption, validation
flips the SyncSessionValidationService latch. Without an explicit
withSession() wrapper here, the latch flip would either fire outside
any session (logged as a contract violation, dropped by the next
normal sync's reset) or — worse — go unread while _performUpload
claimed IN_SYNC based purely on result.uploadedCount. That reproduces
the exact #7330 surface on the immediate-upload path.
Wrap _performUpload's body in latch.withSession(...) and read
hasFailed() before any IN_SYNC / deferred-checkmark decision. On
failure (including a thrown upload after the latch flipped) emit
ERROR; transient errors with no latch flip remain silent.
Allow-list: ImmediateUploadService._performUpload added to
tools/check-sync-session-entry-points.js (now 4 entry points).
Tests: 5 new specs cover failure during piggybacked-op processing,
clean upload, LWW re-upload pass, latch reset between sessions, and
upload-throws-after-latch-flip.
Plan: docs/plans/2026-05-08-sync-run-service-refactor.md proposes a
type-enforced runner to replace the contract+lint pair. Deferred —
this PR closes the user-visible bug; the runner refactor is
value-over-time.
* chore(sync): drop withSession() entry-point lint check + deferred runner plan
The static check enumerates withSession() *callers* and asserts the set
matches an allow-list. It catches "added a withSession call without
updating the list" but not the inverse — "added a sync entry point
that should call withSession() but doesn't." _performUpload() was the
existence proof that the lint was silent on the actual failure mode.
Net: false confidence + maintenance churn for a problem nobody has.
The 4-entry-point contract is small enough for code review.
The deferred runner-refactor plan doc proposes a SyncRunService that
mints a SyncRunContext to enforce the contract via types. Two reviews
converged on "don't do it":
- net more code (re-introduces the typed plumbing the latch was
chosen to avoid in b3cbdbd41),
- 3 of 4 entry points keep their own status logic (UNKNOWN_OR_CHANGED,
deferred-checkmark) so the runner shell adds little structural value,
- the hazard it closes (forgot withSession on entry point #5) is
theoretical until that contributor exists.
Drop the doc rather than leave a Status:Proposal file rotting in repo.
withSession() itself stays — reset-and-only-reset at session entry is
load-bearing against leaked-failed latches; cheap insurance.
#7330
* docs(sync): correct entry-point list in SyncSessionValidationService
The top-of-file docstring listed `resolveSyncConflict() (USE_REMOTE
branch)` as a 4th entry point and omitted `ImmediateUploadService
._performUpload()` (added in 22f68027). Both inaccurate after the
ImmediateUploadService wrap and contradict the inline `reset()`
docs at line 104, which correctly identify USE_REMOTE as a sub-scope
inside `_sync()`'s session, not a session boundary.
Replace with the actual four entry points and call out USE_REMOTE
separately as a sub-scope re-scope.
#7330
The time-estimate row icon in task-detail-panel was renamed from
`timer` to `hourglass_empty` in eca8d211a; update the supersync
daily-summary selector so the click target resolves again.
After commit 2105bbc8 switched task focus tracking from `focus`/`blur` to
`focusin`/`focusout`, the focusout from a textarea blur (e.g. closing
inline title-edit on Enter) clears focusedTaskId before the paired
focusin from the subsequent host.focus() refocus rebinds it. When the
host was already document.activeElement, the refocus is a no-op and no
focusin fires — keystrokes then silently drop because the shortcut
handler bails on `\!focusedTaskId`.
Recover by deriving the id from `document.activeElement.closest('task')`
when the signal is null. Guard `_handleTaskShortcut` so the recovery
never delegates to a stale lastFocusedTaskComponent that points at a
different task than the active element.
E2E `markTaskDone` helper additionally blurs the active element before
.focus() so the focus call always fires events even when the host is
already active. Defense in depth — the production fix is sufficient on
its own, but this keeps the helper representative of real user input.
Fixes the deterministic CI failure of supersync-archive-subtasks.spec.ts
"Add subtask then immediately archive syncs correctly" since 2026-05-01.
Replace the syncCheckIcon-based completion race with a spinner visible→hidden
cycle. The check icon may be stale from a prior sync, which forced the test
to add a "wait for the new sync to start" guard; the spinner toggles per-sync
and is unambiguous. The conflict dialog still races against completion, so
the test fails fast if a regression brings the dialog back.
Receiving clients with only already-synced data (no unsynced pending
changes) used to see a conflict dialog when an incoming SYNC_IMPORT
arrived. If the user picked USE_LOCAL — a natural reaction to "your
data may be lost" — forceUploadLocalState() re-uploaded the pre-import
state as a new SYNC_IMPORT, rolling back the import (e.g. encryption
change) for every device.
The originating device already gates the SYNC_IMPORT behind a strong
warning (D_SERVER_MIGRATION_CONFIRM, b761efd8). The receiving-side
dialog is now scoped to the case where unsynced pending user changes
would actually be lost; already-synced store data is no longer treated
as a conflict.
Switches the gate from _hasAnyMeaningfulData (pending OR store) to
_hasMeaningfulPendingOps (pending only) in both the download and
piggyback paths. Drops the now-redundant isEncryptionOnlyChange
short-circuit — under the new gate, PASSWORD_CHANGED SYNC_IMPORTs
without pending ops fall through to silent acceptance for free.
- New unit tests for the silent-accept path on both code paths
- New e2e regression guard (supersync-import-conflict-dialog) — fails
if the gate ever reverts to including store contents
- supersync-scenarios.md D.1 / D.6 and the flowchart gate updated
Two compounding bugs trapped iOS WebDAV users in a per-minute conflict
dialog that no button could resolve:
1. FileBasedSyncAdapter's snapshotReplacement heuristic re-fires
gapDetected on every sync from a non-writing client (clientId \!=
excludeClient is true forever), so the download keeps coming back
with snapshotState and OperationLogSyncService keeps throwing
LocalDataConflictError despite the local clock already dominating
the remote snapshot. Skip hydration and conflict when
compareVectorClocks(local, remote) is EQUAL or GREATER_THAN, gated
on both clocks being non-empty so a fresh client still hydrates a
legacy/clockless snapshot.
2. SyncWrapperService._openConflictDialog$ filtered undefined out of
the afterClosed() stream, so a programmatic close (iOS WebView
lifecycle, re-entry) collapsed the observable and firstValueFrom
threw EmptyError — the user's Use Local/Use Remote/Cancel click
never reached the resolution branches. Drop the filter so undefined
flows through to the existing cancellation path.
The dominate-skip deliberately does NOT append result.newOps to the
op log: VectorClockService.getEntityFrontier is last-write-wins by
seq, and writing historical remote ops at the current tail would
regress per-entity frontiers and let future LWW resolution overwrite
local data. Trade-off documented inline.
Adds an adapter-level integration reproducer that asserts gapDetected
re-fires forever for a non-writing client (the upstream loop trigger),
a 3-client WebDAV e2e that reproduces the loop end-to-end against a
real provider, plus service-level tests for the dominate-skip, the
empty-clock guard, the concurrent-clock conservative path, and
consecutive-sync loop prevention.
The attach-dialog entry point moved out of the task context menu in
PR #7314 and now lives in the detail panel. Update the WebDAV sync
attachment test to use openTaskDetailPanel() and click the attachment
input-item instead of right-clicking and looking for an "Attach" menu
item that no longer exists.
The exportBackup helper used `file-imex button:has-text("Export")`, which
matched both the backup Export button and the newly-text-labeled
"Export Data (anonymized)" privacy button introduced in #7141. Playwright
strict mode on the subsequent click() threw, causing both non-skipped
tests in this file to fail. Switch to an exact name match so only the
intended button is targeted.
* fix(issue): prevent crash from orphan issueProviderId (#7135)
The Jira image-headers effect in task-detail-panel subscribed to
selectIssueProviderById without an error handler, so a task with an
issueProviderId pointing at a deleted provider (e.g. after sync
convergence where taskIdsToUnlink didn't cover all local tasks)
propagated the selector throw to Zone.js as a crash dialog. Wrap the
inner selector observable in catchError that logs and falls back to
of(null); the downstream jiraCfg?.isEnabled guard handles the fallback.
Also drop IssueLog.log(issueProviderKey, issueProvider) from the
throwing variant of the selector: providers may carry credentials
(host, token, apiKey) and IssueLog history is exportable.
* fix(focus-mode): sync tray countdown with in-app timer during breaks
Tray title was rebuilt from a cached currentFocusSessionTime that only
refreshed when CURRENT_TASK_UPDATED fired. addTimeSpent is gated on an
active current task, so during focus-mode breaks or task-less focus
sessions the cache froze while the in-app timer kept ticking.
Add the tick action to taskChangeElectron$ so the cache refreshes every
second whenever the focus timer is running.
Fixes#7278
* fix(ci): restore GitHub Actions SHA pins undone by 0e9218bd68
Commit 0e9218bd68 silently reverted PR #7212 (github-actions-minor group
bump) along with its stated sync/client-id work. This restores the 15
workflow files to their pre-revert state.
Actions restored to newer pinned SHAs:
- actions/upload-artifact v7.0.0 -> v7.0.1
- step-security/harden-runner v2.16.1 -> v2.17.0
- softprops/action-gh-release v2.6.1 -> v3.0.0
- signpath/github-action-submit-signing-request v2.0 -> v2.1
- anthropics/claude-code-action v1.0.89 -> v1.0.93
- docker/build-push-action v7.0.0 -> v7.1.0
- easingthemes/ssh-deploy v5.1.1 -> v6.0.3
* fix: restore i18n, UI, and docs work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following work alongside its stated
sync/client-id changes. Files where later master commits (fec7b25f23, etc.)
already re-applied the reverted work are intentionally left untouched.
Restored:
- #7232 docs/long-term-plans/location-based-reminders.md (513 lines)
- #7199 Romanian i18n phase 3 (ro.json + ro-md.json, ~1168 lines)
- #7049 Polish translation improvements
- #7143 planner component styling (4 scss files)
- #7211 add-task-bar preserve time estimate when typing title
- #7208 task.reducer roll-up estimates for added subtasks
- #6767 focus-mode pomodoro reset button
- #7205 plugin-dev github-issue-provider TOKEN description
- #7231 mobile-bottom-nav FAB fix
- a4fe03272 iOS keyboard accessory bar (global-theme + dialog-fullscreen-markdown)
- 309670db3 ShortSyntaxEffects undefined guard
- 667a7986f Dropbox PKCE auth comment/behavior
* fix(electron): restore electron + e2e work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted the following electron/e2e work.
Files already re-fixed by later master commits are left as-is:
- e2e/tests/sync/supersync-archive-conflict.spec.ts (de33234976 + 191d129ff3)
Restored:
- e8a3e156eb fix(electron): Linux autostart IDB backing-store recovery
(re-adds electron/clear-stale-idb-locks.ts + start-app.ts wiring)
- 5ce78a5b63 fix(electron): macOS Cmd+Q / Dock > Quit hang
(setIsQuiting + before-quit delegate to close-handler)
- 46e0fa2d01 fix(sync): FILE_SYNC_LIST_FILES IPC contract
(electronAPI.d.ts + local-file-sync + preload + ipc-events)
- ea1ef16307 fix(android): session-only SAF permissions on OEM devices
- 8865dc0a50 test(e2e): supersync parallel-worker stampede guard
(SUPERSYNC_SERVER_HEALTHY env-var fallback + goto retry loop)
- af7c7687e2 test(e2e): block WS-triggered downloads in non-WS specs
- 265b44db5d test(e2e): premature waitForURL on daily-summary
(this is literally the fix the bad commit's message claimed to add)
Conflict resolutions:
- e2e/utils/supersync-helpers.ts: kept the refined getDoneTaskElement
checks from d64014d086 (later than c558bcab5e) while restoring the
goto retry loop from 8865dc0a50.
- electron/start-app.ts: unioned imports (setIsQuiting +
clearStaleLevelDbLocks from theirs, fs from ours).
* fix(sync): restore sync-core work undone by 0e9218bd68
Commit 0e9218bd68 silently reverted parts of several sync fixes. Most
sync-core reverts have already been re-addressed differently on master
by later commits (1f5184f6e7, 05cd875dd6, 09f5ced2c9, 7df43358ab,
d9158d6adb, 32dbc95ed9, 8c3b08e016, f89fe1ebc3) — those files are
intentionally left untouched to avoid reverting master's newer work.
This PR restores only the pieces that are genuinely still missing:
- e8a3e156eb fix(electron): IDB backing-store autoreload (in-app piece)
operation-log-hydrator.service.ts + .spec.ts (the electron/clear-
stale-idb-locks.ts piece was restored in the prior commit)
Plus three low-risk documentation/cleanup restorations:
- operation-sync.util.ts — add "Nextcloud" to isFileBasedProvider JSDoc
- dropbox.ts — restore improved _getRedirectUri JSDoc (667a7986fb)
- dialog-get-and-enter-auth-code.component.ts — restore isNativePlatform
comment explaining why manual code entry flow is used (667a7986fb)
- file-adapter.interface.ts — remove stray "// NEW" comment (46e0fa2d01)
Intentionally NOT restored (master's newer work covers or supersedes):
- sync-trigger.service.ts / sync.effects.ts (05cd875dd6)
- sync-wrapper.service.ts (1f5184f6e7)
- sync-errors.ts (1f5184f6e7 re-added LegacySyncFormatDetectedError)
- file-based-sync-adapter.service.ts + spec (1f5184f6e7 + d9158d6adb)
- file-based-sync.types.ts (1f5184f6e7)
- operation-log.const.ts (32dbc95ed9 bumped IDB_OPEN_RETRIES to 5)
- dialog-sync-initial-cfg.component.ts (f89fe1ebc3)
- supersync-archive-conflict: add closing B sync to the convergence
rounds (A→B→A→B) so B is not one op behind A's final conflict-
resolution state; bump active-list assertion timeout to 15s to
cover the async reducer pipeline + sync-replay event-loop yield.
- supersync.page: fix race in changeEncryptionPassword where a stale
check icon from the previous syncAndWait() caused the method to
return before the server wipe + re-upload finished, leaving the
next client to race against partially-uploaded server state.
Now captures the check-icon state at entry and waits for it to
clear (same pattern syncAndWait uses).
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper
- Handle corrupted remote data gracefully instead of throwing
- Add getOrGenerateClientId() as unified entry point, eliminating dual injection
of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption,
and sync-hydration services
- Use crypto.getRandomValues() instead of Math.random() for client ID generation
- Warn user when stored client ID is invalid and must be regenerated
- Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)
Three separate root causes addressed:
1. markTaskDone/markSubtaskDone race with 200ms animation delay:
toggleDoneWithAnimation uses window.setTimeout(200ms) before
dispatching isDone:true. Tests proceeded before the state settled,
causing subsequent assertions to fail. Fix: wait for isDone CSS class
after clicking done-toggle. Also use .first() on the task locator to
avoid Playwright strict-mode violations during CDK drag animation,
where the same task briefly exists as two DOM elements.
2. Premature waitForURL resolution in supersync.spec.ts test 5.1:
waitForURL(/tag\/TODAY/) matched the current /tag/TODAY/daily-summary
URL immediately. Fix: add negative lookahead (?!\/daily-summary).
3. LWW worklog title nondeterminism in archive-conflict test:
After archive-wins conflict resolution and multiple sync rounds, the
archived task title on both clients depends on sync timing — it may be
the original name or the renamed name. Fix: accept either title using
hasTaskInWorklog OR, rather than asserting a specific title.
The URL pattern /(active\/tasks|tag\/TODAY)/ was matching the current
/daily-summary page URL (which contains "tag/TODAY"), causing waitForURL
to resolve immediately without waiting for navigation back to the work
view. This meant syncAndWait() fired before archive operations were
fully uploaded, so Client B received no archived tasks.
Fix: add negative lookahead (?!\/daily-summary) to all three waitForURL
calls (two inline in supersync-daily-summary.spec.ts, one in the shared
archiveDoneTasks() helper). Also updates the helper to accept tag/TODAY
without /tasks suffix, consistent with the rest of the test suite.
When a full-state op (SYNC_IMPORT/BACKUP_IMPORT) arrived from another
client, mergeRemoteOpClocks reset the local clock to a "minimal" form
but only preserved the current client's counter from the incoming op's
clock. If the current client had issued ops (e.g. GLOBAL_CONFIG) not
reflected in the incoming full-state op's clock, its counter was dropped,
causing subsequent ops to reuse the same counter value. Downstream clients
then saw these ops as EQUAL (duplicate) and skipped them silently.
Fix: take max(mergedClock[clientId], currentClock[clientId]) when
rebuilding the clock after a full-state op reset.
Also add __SP_E2E_BLOCK_WS_DOWNLOAD flag to WsTriggeredDownloadService
to allow E2E tests to block automatic WS-triggered downloads during
concurrent conflict scenarios.
Fix archive conflict test by blocking WS downloads on Client A during
the concurrent edit phase so it doesn't auto-receive B's rename via
WebSocket before archiving (restoring the intended conflict scenario).
Fix LWW singleton test to assert convergence rather than specific winner.
Fix renameTask helper to avoid Playwright/Angular re-render races.
Fix shepherd.js import paths that broke the Angular dev server build.
The WebSocket push feature (7fa8f12132) introduced background downloads
via WsTriggeredDownloadService that race with E2E test assertions.
Tests assume controlled, sequential sync via syncAndWait() but WS push
causes data to arrive before tests expect it, breaking conflict scenarios
and route interception.
Block the /api/sync/ws endpoint by default in setupSuperSync() and
opt-in only for the realtime-push test that specifically verifies WS.
* feat(sync): add Helm chart for SuperSync Kubernetes deployment
Includes Deployment, StatefulSet (PostgreSQL), Service, Ingress,
ConfigMap, Secret, HPA, PDB, NetworkPolicy, and test templates.
Supports both bundled PostgreSQL and external database configurations.
* feat(sync): add WebSocket push notifications for near-realtime sync
Server: Fastify WebSocket plugin with connection manager, app-level
heartbeat (30s), debounced notifications, and per-user routing.
Client: WebSocket service with exponential backoff reconnection,
WS-triggered download service, and reduced polling when connected.
* fix(sync): improve WebSocket error handling and reactivity
Make syncInterval$ reactive to WS connection state, fix Set/Map
mutation during heartbeat iteration, add error handling to WS route
handler, separate JSON parsing from message handling, detect auth
failures in WS-triggered downloads, and add logging to all empty
catch blocks.
* fix(sync): address PR review findings for WebSocket and Helm
Fix race condition in WS-triggered download pipeline by moving
isSyncInProgress filter after debounce and adding guard in
_downloadOps. Add logging to remaining empty catch blocks, fix
missing $NODE_IP in Helm NOTES.txt, and correct inaccurate
comments in values.yaml and ws-triggered-download.service.ts.
* fix(sync): add rate limiting to WebSocket upgrade endpoint
Limit WS connection attempts to 10 per minute per IP to prevent
connection flooding, matching the rate-limit pattern used by other
sync endpoints.
* fix(sync): extract shared constants, fix debounce correctness, replace deprecated toPromise
Extract CLIENT_ID_REGEX and MAX_CLIENT_ID_LENGTH into sync.const.ts
to prevent drift between sync.routes.ts and websocket.routes.ts.
Fix debounce in notifyNewOps to accumulate excluded client IDs across
rapid calls from different clients, preventing self-notifications.
Replace deprecated toPromise() with firstValueFrom in connectWebSocket
and _sync methods. Add .catch() to reconnect path and logging to
silent early returns in _downloadOps.
* fix(build): restore correct package-lock.json and fix upstream lint errors
* revert: restore upstream HTML formatting to match CI prettier config
* fix(sync): use DownloadOutcome discriminated union in WsTriggeredDownloadService
* fix(sync): narrow TokenVerificationResult before accessing userId
* fix(sync): await async getProviderById in connectWebSocket
Missing await caused the Promise object to be cast to
SuperSyncProvider, making getWebSocketParams undefined.
* feat(sync): add SEED_USERS env var to create verified users on startup
For self-hosted single-user setups: set SEED_USERS=email1,email2 to
create verified users on boot and log their access tokens. Skips
existing users. Removes need for SMTP/magic link registration.
Also fix Dockerfile to use npm install instead of npm ci for lockfile
compatibility.
* fix(sync): address PR review feedback for Helm chart and server
Security:
- Remove seed-users.ts (logged full JWT tokens to stdout)
- Add fail assertions for missing jwtSecret and postgresql.password
- Add smtp.user/smtp.password values fields
- Add from: selector to NetworkPolicy ingress
- Add egress rule for external database when postgresql.enabled=false
Correctness:
- Fix PostgreSQL StatefulSet indentation for non-persistent mode
- Fix NOTES.txt panic on empty tls list (use len instead of index)
- Restore npm ci in Dockerfile by using node:24-alpine (ships npm 11)
- Add Recreate deployment strategy when using RWO PVC
Operational:
- Add fail guard preventing HPA maxReplicas > 1 (in-memory WS state)
- Fix PDB to use maxUnavailable instead of minAvailable
- Add WebSocket ingress timeout annotation examples
- Add Prisma db push init container for schema migrations
- Default serviceAccount.automount to false
- Add Chart.yaml maintainers, home, sources metadata
* feat(sync): add ALLOWED_EMAILS env var to restrict registration
Supports fully qualified emails (user@example.com) and domain
wildcards (*@example.com). When unset, all emails are allowed.
Applied to all three registration endpoints (passkey options,
passkey verify, magic link).
* fix(sync): exempt health endpoint from rate limiting
Kubernetes liveness/readiness probes hit /health every 5-15s,
exhausting the global rate limit (100 req/15min) and causing
429 responses that trigger container restarts.
* fix(sync): harden WebSocket, Helm chart, and sync services from multi-agent review
- Pin prisma@5.22.0 in init container and use migrate deploy instead of db push
- Add per-user WebSocket connection limit (max 10) to prevent resource exhaustion
- Add replicaCount > 1 fail guard in deployment template (in-memory WS state)
- Set maxPayload: 1024 on WebSocket plugin (only pong messages expected)
- Remove premature IN_SYNC status from download-only WS-triggered sync
- Fix double removeConnection on error+close events (let close handle cleanup)
- DRY debounce logic in notifyNewOps and store latestSeq on pending entry
- Remove dead fromClientId from NewOpsNotification and WsMessage interfaces
- Add takeUntilDestroyed to _wsProviderCleanup subscription
- Simplify email-allowlist.ts to eager init (eliminate mutable state)
- Guard connectWebSocket at call site for non-SuperSync providers
- Add distinctUntilChanged to syncInterval$ to prevent unnecessary resets
- Add container-level securityContext to PostgreSQL StatefulSet
- Fix HPA maxReplicas default to 1 (matches single-replica constraint)
* fix(sync): restore migration in Dockerfile CMD for non-Helm Docker users
Helm users get migration via init container (runs first, CMD becomes no-op).
Docker-compose/plain Docker users get automatic migration back on startup.
* fix(boards): add missing drag delay for touch and extract shared signal
Add cdkDragStartDelay to board-panel drag items, which was missing
entirely. Extract the repeated `isTouchActive() ? DRAG_DELAY_FOR_TOUCH : 0`
expression into a shared `dragDelayForTouch` computed signal and refactor
all 8 components to use it.
* test(sync): add comprehensive WebSocket test coverage
Add unit tests for the new WebSocket real-time sync notification pipeline:
- WebSocketConnectionService (server): connection lifecycle, max-per-user
limits, notification debouncing, heartbeat, graceful shutdown (13 tests)
- WebSocket routes validation: token/clientId validation, close codes,
error handling, validation ordering (18 tests)
- SuperSyncWebSocketService (frontend): reconnection with exponential
backoff, heartbeat, disconnect cleanup, URL conversion (6 tests)
- WsTriggeredDownloadService: auth error handling, pipeline resilience,
start() idempotency (3 tests)
- SyncWrapperService: connectWebSocket guards for non-SuperSync, null
params, and already-connected cases (3 tests)
- E2E realtime push: verifies WS-triggered download between two clients
Quality fixes:
- Replace waitForTimeout with syncAndWait in E2E
- Add explanatory comment for microtask flushing in sync-wrapper spec
- Use getter for isSyncInProgress mock in ws-triggered-download spec
---------
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Replace HTTP-header-based conflict detection (If-Unmodified-Since, If-Match,
Last-Modified, ETag) with application-level content hashing (MD5), reducing
WebDAV server requirements to just GET, PUT, and MKCOL.
This mirrors the proven pattern from the LocalFile sync provider and eliminates
compatibility issues with WebDAV servers that don't properly support conditional
headers, strip response headers via reverse proxies, or return inconsistent
PROPFIND XML.
Changes:
- download() now computes MD5 hash of response body as rev
- upload() uses GET-compare-PUT instead of conditional PUT headers
- Remove testConditionalHeaders(), _cleanRev(), _getFileMetaViaHead()
- Remove WebdavServerCapabilities, WebdavServerType, basicCompatibilityMode
- Remove conditional header warning dialog from config page
- Remove legacyRev from provider interface
- Simplify webdav.const.ts (remove unused headers/methods/statuses)
- Add E2E test for near-simultaneous two-client sync
- Add unit test for rev stability across downloads
- Fix SuperSync add-button click interception by hovering the group
header first (activates pointer-events) and targeting the button
element instead of mat-icon, with force-click fallback
- Fix WebDAV sync-expansion project lookup by using the proven
navigateToProjectByName helper instead of a fragile manual
sidebar locator
- Reduce flakiness in rapid-sync test by adding explicit timeouts
to post-loop verification assertions
Multiple error paths in the sync pipeline silently swallowed errors,
allowing sync to report IN_SYNC while clients had diverged state.
This caused permanent data loss — tasks missing on one device but
present on another, with no indication anything was wrong.
Fixes:
1. Propagate download failure: when OperationLogDownloadService
returns success=false, throw instead of treating as "no new ops".
Prevents lastServerSeq from advancing past failed downloads.
2. Throw on LWW conflict apply failure: autoResolveConflictsLWW
now throws when applyOperations fails, matching the behavior of
applyNonConflictingOps. Prevents lastServerSeq from advancing
past failed conflict resolutions.
3. Propagate rejected ops handler errors: rethrow instead of
swallowing in the uploadPendingOps catch block, so the
sync-wrapper can set ERROR status.
4. Surface validation failure: validateAfterSync now checks the
return value from validateAndRepairCurrentState and shows an
error snackbar when validation fails.
5. Set ERROR status in sync-wrapper catch-all: the generic error
handler now calls setSyncStatus('ERROR') so the sync icon shows
the red error state.
Fixes#6571
The stale client reconnection test was flaky because syncAndWait()
was called immediately after clicking done-toggle, racing with the
200ms animation delay before isDone is dispatched to the store.
Use markTaskDone helper and wait for state to persist before syncing.
The done-toggle was refactored from an inline SVG with class="done-toggle"
into a standalone Angular component <done-toggle>. Update all e2e selectors
from '.done-toggle' (class) to 'done-toggle' (element) to match.
- moveItemAfterAnchor: preserve current position when anchor is
concurrently deleted instead of appending to end (which corrupted
ordering on remote clients). For cross-list moves, append to end
as fallback to prevent data loss.
- Prevent double-write of deferred actions: track buffered actions in
a WeakSet and filter them in the effect so they are only written
once by processDeferredActions().
- Propagate skipDequeue through handleQuotaExceeded retry path to
prevent queue desync when deferred actions hit storage quota.
- Add e2e tests for concurrent delete + reorder sync scenarios.
- Suppress onboarding overlay for fresh Client B in WebDAV legacy
migration test (onboarding-backdrop was blocking sync button clicks)
- Update play indicator selector from .play-icon-indicator to
.play-indicator after done-toggle circle refactor removed the mat-icon
- Use markTaskDoneByKey in archive conflict tests for reliable done
state verification (click-based markTaskDone was silently failing)
- Replace drag handle with SVG done-toggle circle with checkmark draw-on animation
- Improve mobile UX: cdkDragStartDelay for touch, bottom-sheet context menu
- Move issue/repeat indicators from drag handle to tag-list as chips
- Add isFinishDayEnabled config toggle
- Reorder context menu: add sub task before duplicate, deadline as own entry
- Fix context menu not opening on swipe left
- Fix race condition in done toggle animation timeout
Remove the full welcome tour (Welcome, AddTask, DeleteTask, Projects,
Sync, IssueProviders, ProductivityHelper, FinalCongrats, StartTourAgain)
and keep only the KeyboardNav tour, triggered from the Help menu.
- Delete ShepherdComponent (auto-start on load)
- Strip shepherd-steps.const.ts to KeyboardNav steps only
- Simplify ShepherdService with _initPromise guard and fresh tour on re-open
- Remove 3 tour menu items from Help menu, keep keyboard tour
- Remove waitForEl/waitForElObs$ helpers (unused by KeyboardNav)
- Delete e2e/utils/tour-helpers.ts and all tour dismissal calls
- Clean up stale tour comments across e2e tests
- Fix pre-existing build error: add isFinishDayEnabled to AppFeaturesConfig
type, defaults, form toggle, component signal, and translations
- Add accounts-only pg_dump (users + passkeys) to backup script for
lightweight disaster recovery when clients still have data
- Add pipefail to backup script to catch silent dump failures
- Add test endpoint to simulate partial server revert (ops-after/:serverSeq)
- Add 6 e2e tests covering all disaster recovery scenarios:
complete data loss, partial revert, accounts-only restore (API + SQL),
full dump restore + reset account, and all-clients-lost recovery
- Add backup-and-recovery.md with setup, recovery procedures, and
decision tree
- Rewrite recurring-future-start-date test to set startDate via the
repeat dialog's model signal instead of the broken schedule-dialog
approach (mat-datepicker signals don't work from page.evaluate)
- Fix supersync encryption password-change test by clicking syncBtn
directly instead of triggerSync() which throws on error state before
the async decrypt dialog renders
- Fix flaky "Concurrent Delete vs. Update" test with deterministic
delete-wins assertion instead of permissive either-outcome check
- Delete supersync-encryption-enable-disable.spec.ts (4 tests) and
supersync-encryption-prompt-loop.spec.ts (2 tests) — these tested
optional encryption flows that no longer exist since encryption
became mandatory for SuperSync
- Wrong-password overwrite: remove Client A sync blocked by decrypt dialog
- Complex chain (4.1): rewrite without unreliable renameTask helper
- Delete vs Update race: fix assertions to expect delete wins
- Deleted task dueDay=today: same delete-wins fix, rename test
- Subtask conflicts: restructure so A marks done subtask from B
- Import tests: add syncImportChoice 'local' to preserve imported data
- Various other test fixes for reliability
- Fix importBackupFile() to wait for actual import completion via console
event instead of unreliable URL polling
- Fix setupSuperSync to respect syncImportChoice config in all code paths
- Add .first() to task locators that resolve to multiple elements on
project pages (backlog + active sections)
- Add decryptionFailedPassword config to handle "Decryption Failed" dialog
when server has old encrypted ops from before encryption changes
- Skip 1 test (unencrypted→encrypted) that needs server-side isCleanSlate fix
- Update error scenario mocks to use new OpUploadResponse format
({ results: OpUploadResult[] } instead of { piggybacked, rejectedOps })
- Use route.fetch() to get real op IDs from server for mock responses
- Add syncImportChoice option to SuperSyncConfig for import test control
- Skip 3 import-clean-server-state tests pending server-side isCleanSlate fix
Two root causes for failing E2E tests in CI:
1. Encryption button not visible after provider selection: The async
provider change listener loads config from IndexedDB and updates the
Formly model, but the 1000ms waitForTimeout was insufficient in CI.
Replace with polling (200ms intervals, 10s timeout) that waits for
either enable or disable encryption button to become visible.
Also removes ~130 lines of leftover debug logging from disableEncryption().
2. Drag-handle strict mode violation on parent tasks with subtasks:
task.locator('.drag-handle') matched both the parent's drag handle
and nested subtask drag handles. Add .first() to all drag-handle
locator usages to target only the parent task's own handle.
The dialog-enter-encryption-password now has two mat-flat-button buttons
when SuperSync is active (Save & Sync + Force Overwrite). The e2e locator
'button[mat-flat-button]' resolved to 2 elements causing Playwright strict
mode failures across all encryption-related SuperSync tests. Added
[color="primary"] to uniquely target the Save & Sync button.
* fixes to the profile system storage which worked unreliable across multiple storage backends
* fix tests
* fix issues with tests
* address copilot reviews
Fix deleteTask helper clicking on task-title, which entered edit mode
and caused Backspace to delete text instead of triggering the delete
shortcut. Use drag-handle click target instead.
Also add delete verification, resilient title assertions for known LWW
race, increased persistence waits, and extra sync rounds.