Commit graph

263 commits

Author SHA1 Message Date
Johannes Millan
86497d24de test(sync): cover incompatible supersync password change 2026-05-13 11:08:54 +02:00
Johannes Millan
5a924eeadd test(sync): scope keep-local validation assertion 2026-05-12 18:26:08 +02:00
Johannes Millan
22dbfde12b test(sync): fix concurrent time tracking assertion 2026-05-09 19:28:19 +02:00
Johannes Millan
4b5fc3fb33 test: stabilize flaky e2e specs 2026-05-09 18:11:40 +02:00
Johannes Millan
44e0762fba
Feat/issue 7330 bc1a3f (#7522)
* 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
2026-05-09 00:12:58 +02:00
Johannes Millan
7dfc7a6efb test(e2e): match renamed time-estimate icon in daily-summary spec
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.
2026-05-08 22:30:59 +02:00
Johannes Millan
414cd7f508 fix(tasks): recover focusedTaskId when shortcut fires on a focused task
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.
2026-05-02 19:23:26 +02:00
Johannes Millan
19f1cabda0 test(e2e): use spinner cycle for SYNC_IMPORT silent-accept completion signal
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.
2026-04-29 16:17:56 +02:00
Johannes Millan
9d3cf64986 fix(sync): apply incoming SYNC_IMPORT silently with no pending ops
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
2026-04-29 16:17:56 +02:00
Johannes Millan
654f4d80ee fix(sync): break iOS WebDAV conflict-dialog loop (#7339)
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.
2026-04-25 22:36:14 +02:00
Johannes Millan
447ae68ff3 test(e2e): open attachment dialog via detail panel after #7314
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.
2026-04-25 22:36:14 +02:00
Johannes Millan
9aba039b59 test(e2e): tighten backup-export selector for renamed privacy button
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.
2026-04-21 21:50:52 +02:00
Johannes Millan
01e30b9c7e
Feat/to me it looks like there are lots of 60dd04 (#7280)
* 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)
2026-04-20 12:04:38 +02:00
Johannes Millan
de33234976 test(e2e): harden LWW archive and encryption password-change tests
- 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).
2026-04-19 19:20:34 +02:00
Johannes Millan
191d129ff3 test(e2e): restore title tolerance in LWW archive resurrection assertion 2026-04-18 23:24:19 +02:00
Johannes Millan
0e9218bd68 fix(sync): handle data validation errors and improve client ID management
- 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)
2026-04-16 17:41:39 +02:00
Johannes Millan
23c729bd9c style(e2e): fix prettier formatting in supersync spec 2026-04-14 22:09:02 +02:00
Johannes Millan
c558bcab5e test(e2e): fix failing and flaky supersync archive conflict tests
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.
2026-04-14 22:09:02 +02:00
Johannes Millan
265b44db5d test(e2e): fix flaky supersync tests by preventing premature waitForURL resolution
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.
2026-04-14 22:09:02 +02:00
Johannes Millan
fc02693287 style: fix prettier formatting errors in e2e tests and nav-item scss
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 15:41:14 +02:00
Johannes Millan
d32f7037a3 fix(sync): preserve own vector clock counter across full-state op resets
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.
2026-04-01 15:41:13 +02:00
Johannes Millan
2f2f861b9f test(e2e): block WebSocket in SuperSync tests to prevent sync race conditions
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.
2026-03-30 23:14:01 +02:00
Thorsten Klein
7fa8f12132
feat(sync): add Helm chart and WebSocket push for SuperSync (#6971)
* 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>
2026-03-30 21:34:30 +02:00
Johannes Millan
946339a035 feat(sync): replace WebDAV header-based conflict detection with content hashing
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
2026-03-30 17:33:06 +02:00
Johannes Millan
821896496b test(e2e): add day-change sync convergence test (#6992) and fix lint
Add supersync E2E test for day-change overdue sync race condition.
Fix prettier/lint issues in repeat task first occurrence spec.
2026-03-29 19:57:10 +02:00
Johannes Millan
b1afc4eae8 test(e2e): fix failing sync tests and reduce flakiness
- 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
2026-03-27 17:33:38 +01:00
Johannes Millan
4e6bf73859 fix(sync): prevent false IN_SYNC status when sync errors occur (#6571)
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
2026-03-26 17:43:04 +01:00
Johannes Millan
83432fb0f4 fix(e2e): add settle time after done-toggle to prevent sync race condition
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.
2026-03-24 20:09:02 +01:00
Johannes Millan
dbeca43876 test(e2e): fix done-toggle selector to use element instead of class
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.
2026-03-24 16:25:42 +01:00
Johannes Millan
d316a684bb fix(sync): improve move operation reliability during sync
- 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.
2026-03-24 16:25:42 +01:00
Johannes Millan
6d67d31ecb test(e2e): fix failing WebDAV and SuperSync E2E tests
- 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)
2026-03-23 11:07:25 +01:00
Johannes Millan
31480a5fab feat(tasks): replace drag handle with done toggle circle and improve mobile UX
- 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
2026-03-22 18:14:16 +01:00
Johannes Millan
2f8d871042 feat(shepherd): remove all tours except keyboard navigation
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
2026-03-22 17:00:42 +01:00
Johannes Millan
a01a511c25 feat(sync): add backup strategy with accounts-only dump and disaster recovery docs
- 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
2026-03-18 20:15:45 +01:00
Johannes Millan
f3585b5dc1 test(e2e): fix recurring future-start-date and supersync encryption tests
- 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
2026-03-18 13:14:10 +01:00
Johannes Millan
a276ccb3f4 fix: resolve pre-existing lint and prettier errors blocking commits (#6840) 2026-03-15 16:47:52 +01:00
Johannes Millan
cf7fa73e5b test(e2e): fix flaky delete-vs-update test and remove obsolete encryption tests
- 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
2026-03-07 22:32:03 +01:00
Johannes Millan
a54de5f90d test(e2e): fix failing supersync E2E tests
- 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
2026-03-07 22:32:03 +01:00
Johannes Millan
5401bb7f1d test(e2e): fix 11 failing supersync import tests
- 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
2026-03-07 13:28:20 +01:00
Johannes Millan
069873126d test(e2e): fix supersync error scenario tests and skip failing import tests
- 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
2026-03-06 23:08:37 +01:00
Johannes Millan
e8f5f1d404 test(e2e): fix supersync encryption tests for mandatory encryption
- Skip "disable encryption" test: button is hidden for SuperSync
- Skip "encryption prompt loop" tests: unencrypted SuperSync no longer possible
- Fix "wrong password" test: assert decrypt error dialog directly instead
  of checking for post-dialog snackbar/error indicators
- Fix "password change recovery" test: handle sync-import-conflict dialog
  that appears after retry decrypt due to clean slate from password change
2026-03-06 18:19:26 +01:00
Johannes Millan
cce9576946 test(e2e): fix supersync E2E tests for mandatory encryption flow
- Extract shared encryption warning helper and fix race conditions
- Restructure concurrent-import tests to import before enabling sync
- Fix encryption password preservation and change tests
- Add "Change Password" button detection in supersync page object
- Mark backup-import ID mismatch test as fixme (known limitation)
2026-03-06 16:36:38 +01:00
Johannes Millan
755cd705f5
Fix/run 22683814946 (#6740)
test(e2e): fix e2e tests
2026-03-05 19:37:48 +01:00
Johannes Millan
7164a5e44b test(e2e): fix encryption button timing and drag-handle strict mode violation
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.
2026-03-04 19:38:37 +01:00
Florian Bachmann
729d35501f
Fix immutable caldav task (#6705)
* Fix immutable CalDAV task after completion

* Linting

* Resolves Claude suggestions
2026-03-03 15:05:57 +01:00
Johannes Millan
8a6df3634d test(e2e): fix strict mode violation in encryption password dialog locator
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.
2026-03-03 11:27:19 +01:00
Johannes Millan
ff444c1b77 test(sync): add E2E tests for max retry rejection and import encryption preservation 2026-03-01 21:27:08 +01:00
Johannes Millan
baa9a46935 test(sync): add encryption and supersync e2e test coverage
Add comprehensive test coverage for the encryption and SuperSync features:

- Add e2e tests for encryption prompt loop, error scenarios, no-op sync
- Add e2e tests for provider switch, re-enable, account reset, server migration
- Add e2e tests for import clean-slate scenario
- Add unit tests for encryption dialogs, toggle, enable/disable services
- Add unit tests for sync-config, sync-wrapper, file-based-encryption
- Add unit tests for operation-log-sync, sync-import-filter, clean-slate
- Update supersync page object for mandatory encryption flows
2026-03-01 21:27:08 +01:00
overcuriousity
dc7742d354
fix: profile system bugs (#6614)
* fixes to the profile system storage which worked unreliable across multiple storage backends

* fix tests

* fix issues with tests

* address copilot reviews
2026-02-23 17:43:04 +01:00
Johannes Millan
5ab773fa26 test(e2e): fix flaky LWW delete-vs-update conflict tests
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.
2026-02-16 11:07:52 +01:00