Commit graph

11881 commits

Author SHA1 Message Date
Johannes Millan
4f2dbcdaa7 feat(sync): handle auth errors for account deletion scenarios
When a SuperSync account is deleted, clients now properly detect
authentication failures and prompt for reconfiguration:

- Add _checkHttpStatus() helper to SuperSyncProvider that throws
  AuthFailSPError on 401/403 responses
- Clear cached credentials on auth failure to allow reconfiguration
- Add DELETE /api/test/user/:userId endpoint for E2E testing
- Add deleteTestUser() helper in supersync-helpers.ts
- Add E2E tests for account deletion and reconfiguration scenarios

The existing SyncWrapperService already handles AuthFailSPError by
showing a snackbar with "Configure" action, so no UI changes needed.
2026-01-02 15:48:17 +01:00
Johannes Millan
1982f69d22 fix(sync): sync parent.subTaskIds when task parentId changes via LWW
When LWW conflict resolution updates a task's parentId (making it a subtask
or moving it to a different parent), we must also update the corresponding
parent task's subTaskIds arrays to maintain bidirectional consistency:
- Remove task from old parent's subTaskIds (if it was a subtask)
- Add task to new parent's subTaskIds (if it becomes a subtask)

This is analogous to the existing syncProjectTaskIds, syncTagTaskIds, and
syncTodayTagTaskIds functions that handle other cross-entity relationships.

Adds 10 unit tests covering:
- Moving subtask between parents
- Task becoming a subtask
- Subtask becoming top-level
- Edge cases (deleted parent, non-existent parent, order preservation)
2026-01-02 14:03:24 +01:00
Johannes Millan
7d474d207b fix(sync): acquire lock in StaleOperationResolverService to prevent race conditions
The resolveStaleLocalOps() method writes operations via appendWithVectorClockUpdate()
but was not acquiring the sp_op_log lock. This could race with:
- Normal operation capture in operation-log.effects.ts
- Other sync operations

This could cause vector clock corruption or duplicate operations if the user
performed actions during conflict resolution.

Changes:
- Inject LockService and wrap method body in lockService.request('sp_op_log', ...)
- Add 2 unit tests verifying lock acquisition and operation ordering
2026-01-02 13:55:15 +01:00
Johannes Millan
936f374bde refactor(sync): address code review findings for operation log
Phase 1 - Fix archive circular dependency:
- Create ArchiveDbAdapter for direct IndexedDB access to archive data
- Update ArchiveOperationHandler to use ArchiveDbAdapter instead of
  lazy-injecting PfapiService, breaking the circular dependency chain
- Update tests to mock ArchiveDbAdapter

Phase 2 - Address testing gaps:
- Add 3 timeout exhaustion tests for compaction service (25s timeout)
- Add 5 clock skew edge case tests for conflict resolution:
  - Far future/past timestamps
  - Zero and negative timestamps
  - Client ID tie-breaker for identical timestamps

Phase 3 - Document cross-version migration (A.7.11):
- Add comprehensive implementation guide in architecture doc covering:
  - When to bump CURRENT_SCHEMA_VERSION
  - Operation transformation strategy
  - Conflict detection across versions
  - Backward compatibility guarantees
  - Migration rollout strategy
  - Example migration and testing requirements

All 1399 op-log tests pass.
2026-01-02 13:48:22 +01:00
Johannes Millan
ef8c8c21df fix(sync): fix sync E2E test failures and flaky encryption tests
operation-log.effects.ts:
- Move dequeue() inside the lock to prevent race condition with
  flushPendingWrites(). Previously, flushPendingWrites() could see
  queue=0 and return before IndexedDB write completed.
- Set entityId from entityIds[0] for bulk operations. Server requires
  entityId for non-full-state operations, but bulk actions like
  updateAllSimpleCounters only provide entityIds array.

supersync.page.ts:
- Fix flaky encryption checkbox tests caused by Angular Material
  checkbox starting in indeterminate "mixed" state before form loads
- Use retry logic with toPass() instead of single click + assert
- Click on label instead of touch target for more reliable interaction
- Increase timeout to 10s with smart intervals [500, 1000, 1500]ms
2026-01-01 17:14:59 +01:00
Johannes Millan
27641efeae feat(sync): add COUNTER_UPSERT action type
Add upsert action for SimpleCounter to support sync operations
that create or update counters in a single action.
2026-01-01 15:18:43 +01:00
Johannes Millan
ce7e201faa perf(test): use mock encryption in tests for faster execution
Encryption tests were slow (~0.5-2.4s each) due to Argon2id key
derivation (64MB memory, 3 iterations). This adds injectable
encryption tokens and fast mock implementations for tests.

Changes:
- Add ENCRYPT_FN/DECRYPT_FN injection tokens
- Update OperationEncryptionService to use injected functions
- Create mock-encryption.helper.ts with fast base64-based mock
- Update tests to use mock encryption via DI

Performance improvement:
- OperationEncryptionService tests: ~10s → 0.047s
- Service Logic Integration tests: ~1.7s → 0.08s

Real encryption still tested in encryption.spec.ts (3 tests, ~1s).
2026-01-01 15:14:21 +01:00
Johannes Millan
56266d28c6 fix(test): correct assertion for schema version skip limit test
The test incorrectly expected no snackbar to be shown for future version
operations within the skip limit. However, the service correctly shows a
WARNING (not ERROR) snackbar to prompt users to update when receiving ops
from newer schema versions.

Changed assertion from `not.toHaveBeenCalled()` to verify no ERROR
snackbar is shown, allowing the expected WARNING behavior.
2026-01-01 14:48:16 +01:00
Johannes Millan
91f0b6d135 feat(sync): add schema version mismatch warnings
Add user-facing notifications for schema version mismatches during sync:

- Show error and stop sync when receiving ops below MIN_SUPPORTED_SCHEMA_VERSION
- Show warning (once per session) when receiving ops from a newer schema version
- Operations from newer versions (within MAX_VERSION_SKIP) are still applied

Also adds 'WARNING' type to SnackService with appropriate styling.

Includes unit tests for all new behaviors.
2026-01-01 14:33:17 +01:00
Johannes Millan
2a9b84caf0 fix(api): add missing await in replace-token endpoint
The /replace-token endpoint was calling replaceToken() without await,
causing the response to return an unresolved Promise instead of the
actual token. This is the same class of bug as the email verification
race condition fixed in fd6499f13.

Also adds unit tests for the setCounter() fix from issue #5812:
- Tests for creating new counters with all mandatory fields
- Tests for updating existing counters (only countOnDay)
- Tests for incrementCounter and decrementCounter behavior
- Tests for error handling (invalid keys, negative values)
2026-01-01 12:24:32 +01:00
Johannes Millan
c087ffee8a fix(sync): prevent infinite rejection loop from corrupt operations
Root cause: Operations with `entityId: undefined` (corrupt ops) caused
an infinite rejection loop during sync:
1. Upload ops → Rejected with CONFLICT_CONCURRENT
2. Rejection handler creates merged ops (still with invalid entityId)
3. Upload merged ops → Rejected again
4. Loop continues forever

This prevented the sync status double checkmark from ever appearing
because hasPendingOps never became false.

Three-layer defense implemented:

1. Client: Guard time tracking dispatch (task.service.ts)
   - Changed `if (currentTask &&` to `if (currentTask?.id &&`
   - Prevents time tracking dispatch with undefined taskId

2. Client: Strengthen entityId validation (operation-log.effects.ts)
   - Check that entityId is truthy AND a string type
   - Log error with actual entityId value for debugging

3. Client: Cleanup corrupt ops on startup (operation-log-recovery.service.ts)
   - New cleanupCorruptOps() method marks corrupt ops as rejected
   - Runs during hydration to break existing infinite loops
   - Excludes bulk 'ALL' operations which legitimately lack entityId

4. Server: Validate entityId for all regular operations (validation.service.ts)
   - Reject ops with missing entityId (except full-state and bulk ops)
   - Full-state: SYNC_IMPORT, BACKUP_IMPORT, REPAIR
   - Bulk entity types: ALL, RECOVERY
   - Returns MISSING_ENTITY_ID error code (permanent rejection)
2025-12-31 17:29:25 +01:00
Johannes Millan
a86df2c356 fix(sync): prevent duplicate operation from aborting batch upload
When a duplicate operation was uploaded, PostgreSQL's unique constraint
violation (P2002) would abort the entire transaction, causing all
subsequent operations in the batch to fail with INTERNAL_ERROR.

Server-side fix:
- Add pre-check via findUnique before attempting insert
- Return DUPLICATE_OPERATION error code without aborting transaction
- Other ops in batch continue processing normally

Client-side fix:
- Handle DUPLICATE_OPERATION error code in rejected-ops-handler
- Mark duplicate as synced (not rejected) since server already has it
- Prevents infinite retry loop for legitimately uploaded ops

Adds tests for both server and client handling.
2025-12-31 16:39:55 +01:00
Johannes Millan
bb254dd07b Merge branch 'master' into feat/operation-logs
* master:
  fix(test): use dynamic date in year boundary test to avoid today collision
  fix(electron): reduce idle detection log verbosity
  fix(plugins): ensure setCounter creates valid SimpleCounter records
  feat(android): add quick add widget
  perf(android): prewarm WebView during idle time to speed up startup
  fix(ical): prevent race condition in lazy loader
  Merge branch 'master' into master

# Conflicts:
#	src/app/features/android/store/android.effects.ts
#	src/app/features/schedule/ical/get-relevant-events-from-ical.spec.ts
2025-12-31 14:20:58 +01:00
Johannes Millan
29ffa0f5d8 Merge remote-tracking branch 'origin/master'
* origin/master:
  Merge branch 'master' into master
2025-12-31 14:16:44 +01:00
Johannes Millan
a7d8a8b314 fix(today): set dueDay for tasks with dueWithTime scheduled for today (#5841)
Tasks with dueWithTime for today but no dueDay were not appearing in
the Today view because the selector only checks dueDay for membership.

Two fixes:
- Migration: Set dueDay when dueWithTime is for today during import
- Scheduler: Set dueDay conditionally instead of always clearing it

Closes #5841
2025-12-31 14:04:39 +01:00
Johannes Millan
6dea85c083 fix(test): use dynamic date in year boundary test to avoid today collision
The test was failing on Dec 31 because the hardcoded date '2025-12-31'
matched as "today", causing dateDisplay() to return just the time.
2025-12-31 14:04:22 +01:00
Johannes Millan
1529920c37 fix(plugins): ensure setCounter creates valid SimpleCounter records
The setCounter() method was using upsertSimpleCounter without providing
mandatory fields (title, type), causing DataValidationFailedError on
app reload when plugins created counters.

Now checks if counter exists first:
- Existing counters: only update countOnDay value
- New counters: create complete record with all required fields

Fixes #5812
2025-12-31 13:38:05 +01:00
Johannes Millan
4a89c05d32 feat(android): add quick add widget 2025-12-31 12:45:17 +01:00
Johannes Millan
b46e1ff83c test: fix false-positive tests and syntax error
- Fix false-positive test in short-syntax.effects.spec.ts:176-178
  The test wrapped expect() in an if(emittedAction) block, causing it
  to pass silently when no action was emitted. Now properly asserts
  that emittedAction is defined before checking its type.

- Fix syntax error in get-relevant-events-from-ical.spec.ts:830-840
  Test used await inside synchronous expect().not.toThrow() wrapper,
  which is invalid TypeScript. Removed the wrapper since async tests
  will fail if they throw anyway.
2025-12-31 11:33:37 +01:00
Johannes Millan
6d82c1982d fix(ical): prevent race condition in lazy loader
Use Promise-based singleton pattern to ensure concurrent calls share
the same loading promise instead of triggering multiple imports.

Also fix pre-existing test bug using await in sync function.
2025-12-31 11:28:09 +01:00
Johannes Millan
3307c32339 fix(tag): implement self-healing for TODAY_TAG orphaned task IDs 2025-12-31 11:27:01 +01:00
Johannes Millan
c7d37ab773 Merge branch 'refs/heads/master' into feat/operation-logs
* refs/heads/master:
  perf: lazy load ical.js to reduce initial bundle size
2025-12-30 20:24:46 +01:00
Johannes Millan
1cb1e1c742 perf: lazy load ical.js to reduce initial bundle size
Move ical.js (~76KB) to a lazy-loaded chunk via dynamic import.
The library is only loaded when calendar integration features are used.

- Create ical-lazy-loader.ts for on-demand loading
- Make getRelevantEventsForCalendarIntegrationFromIcal async
- Update calendar-integration.service.ts to use switchMap
2025-12-30 20:20:07 +01:00
Johannes Millan
06e3136dd7 fix(sync): block selector-based effects during initial startup
Fixes TODAY_TAG conflicts on app startup when effects fire before
first sync completes. Effects like preventParentAndSubTaskInTodayList$
and repairTodayTagConsistency$ were creating operations during initial
data load that conflicted with remote operations.

Changes:
- Add isInitialSyncDoneSync() to SyncTriggerService for synchronous
  state checks in RxJS filter callbacks
- Extend skipDuringSyncWindow() to also block when initial sync is
  not done, not just during active sync operations
- Add debounceDuringStartup() operator for effects that should wait
  during startup but eventually run (vs being skipped entirely)
- Add comprehensive tests (31 new tests)
2025-12-30 18:57:31 +01:00
Johannes Millan
a97a881e27
Merge pull request #5791 from dXrayb/patch-12
Merge branch 'master' into master
2025-12-30 18:46:12 +01:00
Johannes Millan
8ebf16b958 fix(sync): add missing vector clock persistence in recovery, migration, and conflict resolution
Three related bugs where vector clocks weren't properly persisted:

1. Recovery path (operation-log-recovery.service.ts):
   - recoverFromLegacyData() saved vectorClock in state_cache snapshot
   - But getVectorClock() reads from separate vector_clock store
   - Fix: Add setVectorClock() after saveStateCache()

2. Migration path (operation-log-migration.service.ts):
   - Same issue as #1 for genesis operation during migration
   - Fix: Add setVectorClock() after saveStateCache()

3. Conflict resolution (conflict-resolution.service.ts):
   - autoResolveConflictsLWW() applied remote ops but didn't merge clocks
   - Subsequent local ops would have CONCURRENT clocks instead of GREATER_THAN
   - Fix: Add mergeRemoteOpClocks() after markApplied()

Without these fixes, operations created after recovery/migration/conflict
resolution could have incomplete vector clocks, causing:
- Incorrect filtering by SyncImportFilterService
- Server rejection as CONFLICT_STALE or CONFLICT_CONCURRENT

Adds regression tests for all three fixes.
2025-12-30 15:33:57 +01:00
Johannes Millan
132ae057d3 style(progress-bar): make bottom spinner more subtle, hide during sync
- Remove explicit sync progress bar trigger from pfapi.service.ts
- Add URL filtering to HTTP interceptor to skip sync-related requests
  (dropboxapi.com, /sync/, /ops, /snapshot, /restore-points)
- Style badge: smaller font (10px), reduced padding, no shadow, opacity
- Reduce progress bar opacity to 0.3 for subtler appearance

The top nav sync button spinner is now the only sync indicator.
2025-12-30 15:33:00 +01:00
Johannes Millan
431290c170 fix(sync): prevent data loss from stale vector clocks during SYNC_IMPORT hydration
Two bugs caused data loss after app restart with SuperSync:

Bug 1: Clock merge happened AFTER loadAllData dispatch
- Operations created during loadAllData (e.g., TODAY_TAG repair) got stale clocks
- Server rejected these as CONFLICT_STALE
- Fix: Call mergeRemoteOpClocks() BEFORE store.dispatch(loadAllData())

Bug 2: CONFLICT_STALE treated as permanent rejection
- Stale operations were marked rejected instead of being resolved via merge
- Fix: Handle CONFLICT_STALE the same as CONFLICT_CONCURRENT

Adds unit tests for both bugs and E2E regression tests to prevent recurrence.
2025-12-30 14:07:56 +01:00
Johannes Millan
5474c1a38f test: fix flaky timeouts by converting to fakeAsync/tick
Convert setTimeout-based "no emission" tests to use fakeAsync/tick for
more reliable execution under CI load:

- project.effects.spec.ts: Convert 2 tests (50ms → fakeAsync)
- short-syntax.effects.spec.ts: Convert 2 tests (100ms → fakeAsync)
- operation-log.effects.spec.ts: Convert 10 tests (10-20ms → fakeAsync)

Also improve task-due.effects.spec.ts documentation explaining why tests
are skipped (pfapi-config.ts eagerly instantiates Dropbox SDK at module
load time, requires architectural refactor to lazy-load sync providers).
2025-12-30 13:19:10 +01:00
Johannes Millan
76f58eb303 test: fix unit 2025-12-29 23:20:12 +01:00
Johannes Millan
8408ac5c0e fix(lint): change hydration guard and entity registry rules from warn to error
- Change local-rules/require-hydration-guard from warn to error
- Change local-rules/require-entity-registry from warn to error
- Improve require-hydration-guard rule to skip effects with { dispatch: false }
  since they only perform side effects (audio, UI) and never dispatch actions
- Add skipWhileApplyingRemoteOps() guard to autoShowOverlay$ effect
- Add skipWhileApplyingRemoteOps() guard to triggerIdleWhenEnabled$ effect
- Add eslint-disable comments to test files that intentionally test
  unknown entity type handling
2025-12-29 22:35:40 +01:00
Johannes Millan
6c098b6eaa Merge branch 'master' into feat/operation-logs
Resolve conflict in webdav-sync-expansion.spec.ts:
- Use simplified sync verification without reload (sync updates NgRx directly)
- Test: B marks task done -> sync -> verify A sees task as done
2025-12-29 21:54:15 +01:00
Johannes Millan
c9a25a988d perf(sync): skip download when all ops fit in piggyback response
Optimize sync to reduce round-trips when remote changes are small:

- Increase PIGGYBACK_LIMIT from 100 to 500 ops on server
- Skip download call when server confirms hasMorePiggyback === false
- CRITICAL: Only skip when hasMorePiggyback is explicitly false, not undefined
  (undefined means no upload happened, so we must still download)

This reduces sync from 2 round-trips to 1 when ≤500 ops are pending,
improving perceived sync performance.

Add 5 unit tests for the skip-download optimization logic.
2025-12-29 21:52:03 +01:00
Johannes Millan
6865424f96 fix(sync): prevent removeTasksFromTodayTag with empty taskIds
The removeOverdueFormToday$ effect could dispatch removeTasksFromTodayTag
with an empty taskIds array when overdue tasks exist but none are present
in todayTaskIds (due to stale state or sync timing). This caused the
operation-log validation to fail with "missing entityId/entityIds" error,
preventing the operation from being persisted and causing missing tasks
on synced clients.

Add early return when overdueIds is empty and filter null actions.
2025-12-29 20:08:22 +01:00
Johannes Millan
016e680c5f fix(sync): fix simple counter sync and quota exceeded handling
- Add null checks in simple-counter.reducer.ts to prevent crashes when
  entity doesn't exist (fixes "Cannot read properties of undefined")
- Add missing entityIds to updateAllSimpleCounters action - was causing
  operation log to skip persistence
- Add try-catch in operation-log-upload.service.ts to show alert when
  storage quota exceeded (413 error)
- Fix supersync-network-failure.spec.ts test timing - route interception
  must happen before task creation
- Add documentation to CLAUDE.md for running supersync tests with
  real-time output using --reporter=line
2025-12-29 17:09:46 +01:00
Johannes Millan
a7cd442551 fix(op-log): fix operation flush timeout in daily-summary tests
Two fixes for supersync E2E test failures:

1. Remove race condition in operation-log.effects.ts:
   - Removed isApplyingRemoteOps() filter that caused actions enqueued
     by meta-reducer to be filtered out if sync started before effect ran
   - Meta-reducer already handles buffering during sync, so this filter
     was redundant and caused queue drain timeouts

2. Allow bulk operations with entityType 'ALL' to skip entityId validation:
   - [Archive] Flush Young to Old action has entityType 'ALL' but no entityId
   - Previous validation returned early without dequeuing, causing stuck queue
   - Now dequeues before early return and allows entityType 'ALL' to proceed

3. Fix URL pattern in supersync-daily-summary.spec.ts:
   - Changed from /(active\/tasks|tag\/TODAY\/tasks)/ to /(active\/tasks|tag\/TODAY)/
   - App sometimes navigates to tag/TODAY without /tasks suffix
2025-12-29 15:11:30 +01:00
Johannes Millan
24ef57ead1 fix(test): use valid ActionType enum values in operation-log.effects tests
Tests were using fake action types like '[Task] Update Task' which don't
exist in the ActionType enum. The effects code calls devError() for
unknown action types, which in test mode causes errors and timeouts.

Replace fake action types with valid enum values:
- TASK_SHARED_UPDATE, TASK_SHARED_ADD, TASK_SHARED_DELETE

Fixes 24 failing tests in npm run test:tz:ci
2025-12-29 13:08:51 +01:00
Johannes Millan
de192ab446 fix(op-log): add defensive validations and improve test coverage
- Add devError when action is missing entityId/entityIds (skips persist)
- Add devError when action.type is not in ActionType enum (warning only)
- Add devError when deferred actions buffer exceeds 10 items
- Add unit tests for CLIENT_ID_PROVIDER injection token

These changes catch programming errors early during development and
help identify sync issues (stuck/slow sync) via buffer size warnings.
2025-12-29 12:38:22 +01:00
Johannes Millan
0cb5cdd982 test(sync): add comprehensive tests for error handling and edge cases
Unit tests for remote-ops-processing.service.ts:
- Migration failure handling (skip failed ops, continue processing)
- Early return when all ops fail migration
- Dropped entity ID tracking for dependency warnings

E2E tests for supersync-network-failure.spec.ts:
- Rate limit exceeded (429) handling with retry
- Server storage quota exceeded gracefully
- Long offline period sync recovery (15+ tasks)
- Malformed JSON response handling
- Corrupted/invalid operations from server

These tests address gaps identified in the SuperSync implementation
review, covering error recovery, rate limiting, quota handling, and
data corruption scenarios.
2025-12-29 12:13:53 +01:00
Johannes Millan
61b8d82ad6 fix(sync): address security and reliability issues from code review
- Add payload complexity validation for full-state ops (SYNC_IMPORT,
  BACKUP_IMPORT, REPAIR) with higher thresholds (50 depth, 100k keys)
  to prevent DoS attacks via deeply nested payloads
- Make persistence failure notification sticky (duration: 0) so users
  don't miss critical errors that require page reload
- Fix misleading comment in gap detection (effectiveSinceSeq, not sinceSeq)
- Add comprehensive unit tests for StaleOperationResolverService (16 tests)
  covering vector clock merging, timestamp preservation, and edge cases
2025-12-29 12:03:49 +01:00
Johannes Millan
5d8c4787b2 test(ngrx): add unit tests for effects and selectors
Add comprehensive tests for previously untested NgRx effects and selectors:

Effects tests (31 tests):
- task-internal.effects: onAllSubTasksDone$, setDefaultEstimateIfNonGiven$, autoSetNextTask$
- work-context.effects: dismissContextScopeBannersOnContextChange, unselectSelectedTask$, switchToTodayContextOnSpecialRoutes$ (including sync guard)
- project.effects: deleteProjectRelatedData, moveAllProjectToTodayListWhenBacklogIsDisabled$
- short-syntax.effects: shortSyntax$, shortSyntaxAddNewTags$

Selector tests (9 tests):
- menu-tree.selectors: selectMenuTreeState, selectMenuTreeProjectTree, selectMenuTreeTagTree

Also removes dead code:
- Remove unused setCurrentProject action from project.actions.ts (had props<any>() and was never used)
2025-12-29 11:53:11 +01:00
Johannes Millan
8a451f8b12 fix: remove skipWhileApplyingRemoteOps from various effects for improved sync handling 2025-12-29 11:08:35 +01:00
Johannes Millan
1da70487f9 fix: address code review findings from Dec 27-29 changes
ESLint rule improvements:
- Fix require-hydration-guard to detect guards on combineLatest/forkJoin/zip
  results (eliminates 20+ false positive warnings)

Server improvements:
- Add OperationDownloadService unit tests (21 tests covering vector clock
  aggregation, gap detection, excludeClient parameter, snapshot optimization)
- Split ZIP bomb size limits: 10MB for /ops, 30MB for /snapshot
- Document storage quota update as intentionally non-atomic

E2E test improvements:
- Add waitForUISettle() helper using Angular stability instead of fixed timeouts
- Update supersync-cross-entity tests to use dynamic waits

Unit test improvements:
- Add HydrationStateService edge case tests (concurrent calls, cooldown cycles,
  timer cleanup, interleaved operations)
2025-12-29 10:37:12 +01:00
Johannes Millan
b3022c7285 fix(sync): rename skipDuringSync to skipWhileApplyingRemoteOps
- Rename operator to be more descriptive of actual behavior
- Add deprecated alias for backwards compatibility
- Remove inner guards from effects - inject() only works during class
  initialization, not inside switchMap callbacks at runtime
- Keep outer guards only (at createEffect level)
- Update ESLint rule to accept both names
- Add comprehensive unit tests for the operator (6 tests)

Key insight: The skipWhileApplyingRemoteOps() operator uses inject()
internally, which only works within Angular's injection context (during
class field initialization when createEffect() is called). Using it
inside switchMap callbacks causes NG0203 errors at runtime.

Also: currentTaskId$ is local UI state that doesn't sync between devices.
The operation log syncs entities (tasks, projects, tags), not UI state.
Therefore distinctUntilChanged() is sufficient for filtering spurious
emissions on inner observables.
2025-12-28 18:24:36 +01:00
Johannes Millan
140c46d40f fix(effects): add skipDuringSync to task-ui effects and document all usages
Add skipDuringSync() to two selector-based effects in task-ui.effects.ts:
- timeEstimateExceeded$: Prevent spurious notifications during sync
- timeEstimateExceededDismissBanner$: Prevent unexpected banner dismissal

Add explanatory comments to all skipDuringSync() usages explaining why
each is needed:
- focus-mode.effects.ts: 3 effects (autoShowOverlay$, syncTrackingStartToSession$,
  syncTrackingStopToSession$)
- work-context.effects.ts: 1 effect (switchToTodayContextOnSpecialRoutes$)
- task-ui.effects.ts: 2 effects (new additions)

These are defensive measures to prevent selector-based effects from
triggering side effects during sync/hydration when store state changes
rapidly from remote operations.
2025-12-28 17:45:21 +01:00
Johannes Millan
8905701fa5 fix(work-view): add initialValue to toSignal calls for proper initialization
The isOnTodayList, estimateRemainingToday, workingToday, and
todayRemainingInProject signals lacked initialValue, causing them to be
undefined before their observables emit.

This caused the "Later Today" section to not render because the template
condition `laterTodayTasks().length > 0 && isOnTodayList()` would fail
when isOnTodayList() was undefined (falsy).

Same pattern as the tag-list fix (d7d69fbfc).
2025-12-28 17:25:13 +01:00
Johannes Millan
446475557d fix(work-context): add skipDuringSync to prevent dispatch during sync
The switchToTodayContextOnSpecialRoutes$ effect was missing
skipDuringSync(), which could cause it to dispatch setActiveWorkContext
during startup sync if the URL matches schedule/planner/boards.

This is a defensive fix following the same pattern as the focus-mode
fix (80b925ef2).
2025-12-28 17:24:59 +01:00
Johannes Millan
d7d69fbfcc fix(tag-list): add initialValue to toSignal calls to prevent empty tags
The tag-list component's toSignal() calls for tagState and projectState
lacked initialValue, causing signals to be undefined before the store
emits. This resulted in empty tag arrays because .entities[id] lookups
on undefined returned nothing.

This was more noticeable for synced tasks since they load asynchronously
via the operation log, creating a race condition where the dialog could
render before the store emitted.

Add comprehensive unit tests (15 tests) covering:
- Initial state handling before store emits
- Tag resolution and alphabetical sorting
- tagsToHide filtering
- Work context tag filtering
- Project tag display logic
- Reactive updates when store emits
2025-12-28 17:02:38 +01:00
Johannes Millan
1997081a01 test(focus-mode): add comprehensive tests for skipDuringSync fix
Add 4 additional tests for syncTrackingStopToSession$ effect:

- Rapid currentTaskId changes during sync (freeze prevention scenario)
- Effect resumes normal behavior after sync completes
- Handle rapid sync state toggling without crashing
- Pairwise operator works correctly with skipDuringSync filtering

Total: 65 tests (59 original + 6 new for skipDuringSync)
2025-12-28 16:59:35 +01:00
Johannes Millan
f780099ef8 fix(sync): address code review findings from 2025-12-28 changes
- Add unit tests for extracted hydrator services (60 new tests):
  - operation-log-recovery.service.spec.ts (17 tests)
  - operation-log-snapshot.service.spec.ts (20 tests)
  - sync-hydration.service.spec.ts (23 tests)

- Add compressed size pre-check in sync.routes.ts:
  - New MAX_COMPRESSED_SIZE (10MB) to prevent memory exhaustion
  - Two-stage protection: pre-check compressed, post-check decompressed
  - Applied to both /ops and /snapshot upload endpoints

- Remove tombstone references from sync-server-architecture-diagrams.md:
  - Database schema, ER diagram, cleanup tasks

- Replace console.warn with Logger.warn in sync.types.ts
2025-12-28 16:56:56 +01:00