E2E test improvements:
- Increase timeouts for sync button and add task bar visibility checks
- Add retry logic for sync button wait in setupSuperSync
- Handle dialog close race conditions in save button click
- Fix simple counter test to work with collapsible sections and inline forms
Build fixes:
- Add es2022 lib/target and baseUrl to electron tsconfig
- Include window-ea.d.ts for proper type resolution
- Add @ts-ignore for import.meta.url in reminder service for Electron build
Extract server migration detection and handling from OperationLogSyncService
to a new ServerMigrationService for improved maintainability.
Extracted functionality:
- checkAndHandleMigration: Detects empty server with previously synced ops
- handleServerMigration: Creates SYNC_IMPORT with full validated state
- _isEmptyState: Checks if state has meaningful data to sync
- _hasNoUserCreatedTags: Excludes system tags from empty state check
Changes:
- Create ServerMigrationService (257 lines)
- Create server-migration.service.spec.ts (19 tests)
- Update OperationLogSyncService to delegate to new service
- Remove ~200 lines from OperationLogSyncService (1714 → 1521 lines)
All existing tests continue to pass. Conflict resolution logic untouched.
The applyLanguageFromState$ effect is intentionally designed to fire
during sync - UI language config should apply regardless of source.
Safe because it uses dispatch: false and is idempotent.
Extract _filterOpsInvalidatedBySyncImport from OperationLogSyncService
into a new SyncImportFilterService for better testability and separation
of concerns.
- Create SyncImportFilterService with 143 LOC for filter logic
- Add comprehensive tests (16 tests) for the new service
- Update OperationLogSyncService to delegate to the new service
- Remove old tests from OperationLogSyncService spec (moved to new file)
Add local-rules/require-hydration-guard ESLint rule that warns when
selector-based NgRx effects lack hydration guards (skipDuringSync() or
isApplyingRemoteOps()).
The rule correctly identifies:
- Effects that START with this.store.select() as primary source
- Does NOT flag selectors in withLatestFrom (secondary sources)
- Does NOT flag selectors inside operator callbacks (already guarded)
This prevents duplicate operations during sync replay where selector-based
effects would fire on intermediate states.
Install eslint-plugin-local-rules to enable the rule.
- Show error snackbar when server migration validation fails, informing
user to try importing a backup instead of silently failing
- Skip clearing operations when backup fails during import, keeping old
ops as fallback to prevent potential data loss
- Update tests to verify both behaviors
- Extract extractActionPayload() to operation.types.ts to eliminate
duplication between operation-converter.util.ts and
dependency-resolver.service.ts
- Standardize archive-operation-handler.service.ts JSDoc with consistent
@localBehavior/@remoteBehavior tags documenting when each handler
executes vs skips for local/remote operations
Add storage quota system to prevent single users from consuming
excessive disk space on the sync server.
Changes:
- Add storageQuotaBytes and storageUsedBytes fields to User model
- Add STORAGE_QUOTA_EXCEEDED error code for quota violations
- Add quota calculation and enforcement methods to SyncService
- Block uploads (POST /ops, POST /snapshot) when quota exceeded
- Allow downloads to continue when over quota (so users can delete data)
- Return storage usage/quota in GET /status response
Default quota: 100MB per user
- Click counters now sync immediately with absolute values instead of
being batched every 5 minutes
- Stopwatch counters now sync absolute values instead of relative
durations, fixing issue where remote clients would add duration to
their existing value (e.g., 0:20 became 0:23)
- Remove _modifiedClickCounters batching mechanism (no longer needed)
- Add comprehensive unit tests for immediate sync behavior
- Add e2e test for simple counter sync between multiple clients
Trigger WorklogService.refreshWorklog() after remote sync operations
that affect archive data (moveToArchive, restoreTask, updateTask,
flushYoungToOld, deleteProject, deleteTag, etc.).
- Use isArchiveAffectingAction() helper to detect archive operations
- Only trigger reload for remote operations, not local hydration
- Use lazy injection to avoid circular dependency
- Add unit tests for archive reload trigger behavior
- Disable server rate limiting in E2E test mode to prevent sync timeouts
- Improve SuperSync configuration dropdown stability in page object
- Add explicit waits for UI elements in daily summary tests
- Handle rate limit errors in sync wait logic defensively
Add comprehensive tests for the new concurrent modification handling:
1. VectorClockService tests:
- Test getFullVectorClock() method that queries all ops from seq 0
- Verify it includes clock entries missing from snapshot
2. OperationLogDownloadService tests:
- Test forceFromSeq0 option downloads from seq 0
- Test allOpClocks is populated with all clocks (including duplicates)
- Test allOpClocks includes clocks from filtered duplicate ops
3. OperationLogSyncService tests:
- Test force download is triggered when normal download returns 0 ops
- Test allOpClocks are passed to _resolveStaleLocalOps
- Add getCurrentVectorClock to spy setup
When the server rejects operations with "Concurrent modification" but
normal download returns 0 new ops, the client's computed vector clock
is likely missing entries that the server has (due to compaction or
snapshot issues).
This fix adds:
1. forceFromSeq0 option to downloadRemoteOps that downloads from seq 0
and captures ALL op clocks (including duplicates)
2. When concurrent modification persists after normal download,
trigger a force download to get all op clocks from the server
3. Merge all downloaded clocks into the global clock before creating
merged operations
4. Add getFullVectorClock() to VectorClockService as a fallback
This ensures merged operations have clocks that dominate the server's
frontier, fixing the issue where Today list reordering wasn't syncing.
The previous fix only did ONE re-upload after creating merged ops.
If that re-upload also failed due to concurrent modification, the
newly created merged ops would not be uploaded, leaving changes unsynced.
The fix adds a loop that continues re-uploading until either:
1. No more merged ops are created (server accepted all ops)
2. Max attempts (5) reached (safety limit to prevent infinite loops)
This ensures that even with persistent concurrent modification conflicts,
the client will keep creating merged ops with incrementing clocks until
the server accepts them.
The previous fix used getEntityFrontier() which only includes ops after
the last snapshot. If the conflicting remote op was compacted into the
snapshot, its clock would NOT be included in the entity frontier.
This caused the merged op's clock to potentially NOT dominate the server's
clock, leading to another rejection and an infinite loop of:
1. Upload merged op
2. Server rejects (clock doesn't dominate)
3. Create another merged op
4. Repeat...
The fix uses getCurrentVectorClock() which includes the snapshot clock
plus all subsequent ops - the complete knowledge of all known clocks.
This ensures the merged op's clock will ALWAYS dominate when uploaded.
The previous fix created merged update operations when concurrent modification
rejections occurred, but these ops just sat in the log waiting for the next
sync cycle. This meant the Today list reorder would not sync until some other
change triggered a sync.
The fix:
1. _resolveStaleLocalOps now returns the count of merged ops created
2. _handleRejectedOps now returns the total count of merged ops
3. uploadPendingOps adds this count to localWinOpsCreated
4. ImmediateUploadService already triggers a follow-up upload when
localWinOpsCreated > 0, so merged ops get uploaded immediately
This ensures Today list reordering (and any other concurrent modifications)
sync immediately after conflict resolution, not on the next sync cycle.
The previous fix incorrectly checked `localWinOpsCreated === 0` to determine
if download returned new ops. This was wrong because:
- `localWinOpsCreated` indicates LWW conflict resolution creating local-win ops
- Download returning ops where remote wins all conflicts = 0 local-win ops
- This caused the code to incorrectly try to "resolve locally" even when
download had already processed remote ops
The fix:
1. Add `newOpsCount` to downloadRemoteOps return value
2. Check `newOpsCount === 0` to detect when download got no new ops
3. Also check for still-pending ops AFTER download with new ops, in case
downloaded ops were for different entities than our pending ops
This ensures that Today list reordering conflicts are properly resolved
when one client's changes are rejected due to concurrent modification.
When multiple clients modify the same entity (e.g., TODAY_TAG), the server
may reject operations with "Concurrent modification detected". Previously,
these were marked as rejected and silently discarded, causing user changes
to be lost.
Now concurrent modification rejections are handled properly:
1. Try downloading new remote ops first
2. If download returns new ops, normal conflict detection handles them
3. If download returns nothing (we already have the conflicting ops):
- Mark old pending ops as rejected (their clocks are stale)
- Create NEW ops with current state and merged vector clocks
- The new ops will be uploaded on next sync cycle
This ensures user changes are preserved by creating new operations with
vector clocks that dominate all existing clocks (both local and remote).
Changes:
- Add _resolveStaleLocalOps() to create merged update ops
- Make getCurrentEntityState() public in ConflictResolutionService
- Await download before checking if local resolution is needed
- Add unit tests for concurrent modification handling
Remove unused persistence services that have been replaced by the
operation log system:
- android-db-adapter.service.ts
- database.service.ts
- db-adapter.model.ts
- indexed-db-adapter.service.ts
- persistence.service.spec.ts
Also remove related references from migration service, startup service,
and storage keys.
Add edge case tests for import backup functionality:
- Complex nested data structures preservation
- Backup survives clearAllOperations
- Independence from state_cache
- clearAllOperations doesn't affect import_backup
- Continue import even if backup save fails
Replace tmpBackupService with a dedicated import_backup object store in
the SUP_OPS IndexedDB database. This avoids triggering the stray backup
recovery prompt on startup.
Changes:
- Add import_backup store to SUP_OPS schema
- Add saveImportBackup/loadImportBackup/clearImportBackup/hasImportBackup methods
- Update pfapi.service.ts to use new backup mechanism
- Add tests for import backup functionality
SYNC_IMPORT operations contain the full app state (10-30MB+) and
accumulate in IndexedDB because imports don't delete old operations.
This causes IndexedDB to grow to 29MB+ even with minimal actual data.
Changes:
- Add clearAllOperations() method to OperationLogStoreService
- Before import, backup current state to tmpBackupService
- Clear all old operations before creating new SYNC_IMPORT
- Add tests for new functionality
The backup allows recovery if something goes wrong during import.
- Delay initial sync by 500ms for SuperSync only (data already local)
- Pre-warm privateCfg cache in setActiveSyncProvider (reduces IDB reads)
- Add unit tests for new behavior
- Fix tag.effects.spec.ts missing provider mocks
Add validation and repair before creating SYNC_IMPORT operations
during server migration. This prevents corrupted state (e.g.,
orphaned menuTree references) from propagating to other clients.
- If state is invalid and can't be repaired, abort SYNC_IMPORT
- If state was repaired, dispatch loadAllData to update local store
- Ensures SYNC_IMPORT only contains valid, consistent state
Add post-sync repair for TODAY_TAG.taskIds divergence caused by per-entity
conflict resolution. When "Add to today" and "Snooze" operations conflict,
the TASK entity may resolve to one client's values while TODAY_TAG gets
different values, causing persistent state divergence.
Implementation follows existing pattern in tag.effects.ts:
- selectTodayTagRepair selector detects inconsistencies between
TODAY_TAG.taskIds and tasks with dueDay=today
- repairTodayTagConsistency$ effect watches for inconsistencies and
dispatches updateTag to repair, protected by skipDuringSync()
This approach is cleaner than the previous service-based implementation
as it consolidates sync-related repairs in tag.effects.ts alongside
the existing preventParentAndSubTaskInTodayList$ effect.
Docker-compose merges arrays by default, so both port 1900 and 1901
were being mapped. Using !override ensures only port 1901 is used
for e2e tests, allowing them to run alongside the dev server on 1900.
Informational snackbars like "Deleted task X Undo" and "addCreated task X"
were being incorrectly detected as sync errors. Now only snackbars containing
actual error keywords (error, failed, problem, could not, unable to) are
treated as sync failures.
The nav-item component renders a button with class 'nav-link', not an anchor tag.
Changed locators from '.nav-sidenav nav-item a:has-text(...)' to
'.nav-sidenav .nav-link:has-text(...)'.
- Use more specific nav-sidenav locators for project navigation
- Add retry logic for marking tasks as done
- Add waitForTask after task creation before syncing
- Increase settling time between operations
- Add debug logging throughout tests
- Add wait calls after task creation for UI stability
- Add debug logging for test troubleshooting
- Add timeout after sync for UI to settle
- Improve test assertions with better waits
- Tags test: Use right-click context menu approach instead of 'g' shortcut
to avoid typing into editable task title
- Tags test: Add robust dismissAllOverlays helper with multiple Escape
presses and backdrop click fallback
- Late-join test: Improve conflict dialog handling with retry loop and
wait for dialog to close
- Late-join test: Add extra sync cycle after conflict resolution for
more reliable data propagation
- All files: Fix incorrect port in warning messages (1900 -> 1901)
All 48 supersync e2e tests pass consistently.
- SimpleCounterService: fix memory leak by cleaning up accumulators on counter
deletion, flush pending data in ngOnDestroy, clear accumulators on type change
- BatchedTimeSyncAccumulator: add clearOne() method for cleanup without dispatch
- getRepeatableTaskId: add input validation with descriptive error messages
- ArchiveService: fix race condition by reusing loaded archive data instead of
reloading, add logging for missing parent task edge case, clarify lastFlush
semantics with comments
- DailySummaryComponent: add try/catch for sync operations with error snackbar
- LockService: add graceful degradation when Web Locks API unavailable with
clear warning about multi-tab data loss risks
All fixes include comprehensive test coverage (518+ lines added).
- Add test for task deletion syncing between clients
- Document that scheduled task tests use dueDay, not actual repeat configs
- Reference integration tests for full repeat config sync testing
Extract common batched time sync logic from TaskService and
SimpleCounterService into a shared utility class.
- Create BatchedTimeSyncAccumulator with accumulate/flush/flushOne/shouldFlush methods
- Add 15 unit tests for the utility
- Refactor SimpleCounterService to use the shared accumulator
- Refactor TaskService to use the shared accumulator
- Context tracking (_unsyncedContexts) remains domain-specific in TaskService
Click counters now sync every 5 minutes like stopwatch counters,
instead of immediately after each click. This reduces sync traffic
and aligns with task time tracking behavior.
- Modified increaseCounterToday/decreaseCounterToday to track IDs
- Updated _flushAccumulatedTime to flush click counter changes
- Updated tests to verify batched behavior
Add tests to verify that increaseCounterToday and decreaseCounterToday:
- Dispatch the local UI update action (non-persistent)
- Dispatch setSimpleCounterCounterToday with absolute value (persistent)
- Actions are dispatched in correct order
Simplify click counter sync by using setSimpleCounterCounterToday
(absolute value) instead of increment operations:
- Make increaseSimpleCounterCounterToday non-persistent (local UI only)
- Make decreaseSimpleCounterCounterToday non-persistent (local UI only)
- Service now dispatches setSimpleCounterCounterToday after increment
to sync the absolute value
- Remove isRemote checks from reducer (no longer needed)
This fixes the issue where click counters weren't syncing properly
because increment operations are relative, not absolute.
When two devices create a repeatable task for the same day simultaneously,
they now generate the same task ID (rpt_{repeatCfgId}_{dueDay}), allowing
conflict resolution to work correctly instead of creating duplicates.
Also adds hydration guards to TaskDueEffects to prevent selector-based
effects from firing during sync replay.
- StopWatch counters now use batched sync (every 5 min) instead of
syncing on every 1-second tick, reducing sync operations by 99.7%
- Add tickSimpleCounterLocal (non-persistent) for immediate UI updates
- Add syncSimpleCounterTime (persistent) for batched sync
- ClickCounter clicks still sync immediately
- Make isOn state local-only (toggle/on/off actions non-persistent)
- Fix double-counting bug for click counters on remote clients by
checking isRemote flag in increase/decrease reducers
- Add flush on counter stop, visibility change, and app close
The drag preview was showing larger than the actual event duration because
it used task.timeEstimate (total) instead of event.timeLeftInHours (which
accounts for timeSpent).
The updateAllSimpleCounters action was missing isPersistent metadata,
causing simple counter settings (including isEnabled) to not persist
across page reloads when changed via the config form.
Previously, flushYoungToOld was dispatched as an action and handled by
an NgRx effect. This caused a race condition during finish day:
1. Action dispatched, effect queued
2. Method returned, sync started, DB locked
3. Effect ran, tried to write, blocked by DB lock
Fix follows the same pattern as moveToArchive:
- Perform the flush synchronously in ArchiveService before dispatching
- Dispatch action for op-log capture only (syncs to other clients)
- Handler skips local operations (only runs for remote)
Also adds comprehensive unit tests and e2e test for this scenario.
The server migration check was incorrectly creating a SYNC_IMPORT when
a fresh client (with local data but no sync history) synced to an empty
server. This caused operations from other clients to be filtered out as
"invalidated by SYNC_IMPORT" because they were CONCURRENT with it.
Now _checkAndHandleServerMigration() checks for previously synced ops
before triggering migration, correctly distinguishing between:
- Fresh client (only local ops) → uploads ops normally
- Server migration (has sync history) → creates SYNC_IMPORT
Also adds npm scripts for debugging supersync E2E tests.