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
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).
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.
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.
Add "Forgot Password?" link and form to the SuperSync server login page.
The backend endpoints already existed but had no UI entry point.
- Add forgot password button under login form
- Add email input form with "Send Reset Link" button
- Show generic success message (prevents email enumeration)
- Auto-return to login form after submission
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)
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)
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.
Replace inline onclick handlers with JavaScript event listeners to
comply with Content Security Policy (CSP). The helmet middleware's
scriptSrc directive blocks inline event handlers, preventing the
register tab and other buttons from working.
* 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
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
Add test verifying that when a task scheduled for today is deleted on
one client while updated on another, the recreated task (via LWW Update)
correctly appears in the TODAY view. This validates that
syncTodayTagTaskIds properly updates TODAY_TAG.taskIds during LWW
conflict resolution.
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
Security fixes implemented:
1. CRITICAL: Fix missing await in email verification (pages.ts:139)
- verifyEmail() was called without await, causing race condition
- Response was sent before verification completed
2. CRITICAL: Fix XSS vulnerability in password reset page (pages.ts)
- Added safeJsonForScript() to properly escape tokens in JS context
- JSON.stringify alone doesn't escape </script> sequences
- Now escapes <, >, & as unicode (\u003c, \u003e, \u0026)
3. CRITICAL: Fix XSS in privacy HTML template (server.ts)
- Added escapeHtml() function for all template interpolations
- Prevents XSS if environment variables contain malicious content
4. HIGH: Enable Content Security Policy (server.ts)
- CSP was disabled (contentSecurityPolicy: false)
- Now enabled with strict directives:
- default-src 'self', object-src 'none', frame-ancestors 'none'
5. HIGH: Block wildcard CORS in production (config.ts)
- CORS_ORIGINS=* with credentials is a security vulnerability
- Now throws error in production, warns in development
6. HIGH: Add password reset flow (auth.ts, api.ts, email.ts, schema.prisma)
- Secure token generation with crypto.randomBytes(32)
- 1-hour expiry, one-time use tokens
- Revokes all sessions on password reset
- Prevents email enumeration (same response for all cases)
- Rate limited: 5 requests/15min for forgot-password
- Rate limited: 10 requests/15min for reset-password
Test coverage:
- 45 new tests across 4 test files
- Tests for XSS prevention, CORS blocking, CSP headers
- Tests for password reset flow (API and unit level)
- Fixed pre-existing flaky boundary test in retention-config.spec.ts
Load WebView native libraries during main thread idle time using
IdleHandler and WebSettings.getDefaultUserAgent(). This reduces
the 200-300ms freeze on first WebView creation.
- 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.
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.
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
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)
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.
- 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.
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.
After simulating network failure and restoring network, a dialog backdrop
could remain visible and intercept clicks on the sync button. This caused
the retry sync to fail with a timeout.
Add check for `.cdk-overlay-backdrop` and press Escape to dismiss any
open dialogs before triggering the retry sync.
- 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
Two test fixes:
1. webdav-sync-full.spec.ts - "should sync data between two clients"
- After multiple reloads in the deletion test section, Client B's
sync state became stale and couldn't receive new tasks
- Fix: Create a fresh Client B context for the conflict resolution
section instead of reusing the stale one
2. sync.page.ts - setupWebdavSync combobox selector
- The role-based selector getByRole('combobox', { name: 'Sync Provider' })
was unreliable across different test runs
- Fix: Use the existing providerSelect locator which was already
validated to be visible, instead of creating a new role-based selector
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
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.
- Add retry loop for deletion sync to handle eventual consistency
- Fix done state sync test to handle Done Tasks section
- Add task order sync test
- Add time tracking sync test (skipped - complex persistence)
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.
- 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
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
The tests were failing because:
1. The backup file has tasks with dueDay in the past (2024-12-01)
2. TODAY tag is a "virtual tag" where membership is determined by
task.dueDay === today, not by taskIds
3. When all tasks are done, planning mode activates and hides the
Finish Day button
Fix by:
- Navigate to INBOX project to view and mark tasks as done
- Navigate to TODAY tag to access the Finish Day button
- Exit planning mode by clicking "Ready to work!" if visible
- Then proceed with Finish Day flow
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
- 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.