When concurrent sync triggers occur (especially on Android), a race condition
between filterNewOps() and appendBatch() can cause ConstraintError due to
duplicate operation IDs in the byId unique index.
Instead of failing sync, catch ConstraintError and return empty array - the
ops were already inserted by a concurrent sync.
Fixes#6213
Regular menu items (like tag toggle buttons) were having their clicks
blocked by the touch fix directive. This caused tag assignment from
context menu to not work on mobile devices.
The click blocking is now limited to elements with matMenuTriggerFor
attribute, which are the actual submenu triggers that need protection
against accidental selection when submenus open under the finger.
Empty entityChanges is intentional for most actions since the
actionPayload is sufficient for replay and sync. The warning
was triggering constantly for normal operations like updateTask.
On Android, Capacitor serves the app from http://localhost which is not
a secure context. WebCrypto's crypto.subtle API is unavailable in
insecure contexts, causing Dropbox OAuth PKCE code generation to fail.
Use hash-wasm (already a dependency for Argon2id) as fallback when
crypto.subtle is unavailable.
Move unsynced operations check inside runWithSyncBlocked() to prevent
race condition where background operations could add unsynced ops
between the check and acquiring the lock.
Also add verification step after createCleanSlate() to ensure
SYNC_IMPORT was stored before proceeding, and keep new password
config on upload failure (reverting would encrypt with wrong password).
Fixes flaky E2E test in CI.
Move the max-height constraint from global mat-dialog-content to the
dialog-schedule-task component specifically, since it has no header
and needs different sizing.
Fields progressOnDone, transitionConfig, and availableTransitions were added
after some users already had OpenProject configs, causing Typia validation
errors. Making them optional allows legacy data to load without errors.
When the app goes to background on Android, accumulated time tracking
data was not being flushed to IndexedDB. If Android terminated the app
while backgrounded, tracked time would be lost.
This fix adds a flushOnPause$ effect that:
- Syncs elapsed time from the native foreground service
- Flushes accumulated time from the BatchedTimeSyncAccumulator
- Writes pending operations to IndexedDB
This follows the same pattern already used for notification button
clicks (handlePauseAction$, handleDoneAction$).
Mock validateAndRepair directly instead of relying on global state
(environment.production, window.confirm spy) that could be affected
by test pollution when running the full test suite.
When isManualBreakStart was enabled, the detectSessionCompletion$ effect
was blocking the completeFocusSession action entirely. This prevented:
- Screen transition from Main to SessionDone
- Completion sound from playing
- Window focus on Electron
- "Start break" button from appearing
The isManualBreakStart check was incorrectly placed in detectSessionCompletion$.
It belongs only in autoStartBreakOnSessionComplete$ (which already has it)
to prevent breaks from auto-starting while still allowing session completion.
Re-enable the data repair system with user confirmation before execution.
When invalid data is detected, show a confirmation dialog asking if the
user wants to repair, giving users control and visibility.
Changes:
- Add translated confirmation dialog before repair executes
- Add isRemote flag to loadAllData dispatch in sync contexts to prevent
LOCAL_ACTIONS effects from firing
- Add user notification when repair fails after confirmation
- Add comprehensive tests for validateAndRepairCurrentState()
- Add documentation for implicit contracts and limitations
- Improve type safety (replace `as any` with proper casts)
Add optional chaining for cfg()?.tasks to prevent "Cannot read
properties of undefined (reading 'defaultProjectId')" error during
Dropbox sync when config is not yet loaded.
Fixes#6199
Update link renderer tests to provide tokens array and mock parser
context as required by marked v17. Remove URL auto-linking tests
since gfm:true handles this automatically in the lexer now.
Use CapacitorHttp instead of fetch() on iOS/Android to avoid
issues with WebKit and WebView handling of HTTP requests. Native
platform tests are skipped as CapacitorHttp cannot be properly
mocked in Jasmine.
Subtasks appearing as top-level items in Today/Tag views (when their
parent is not in the view) were snapping back to original position
when reordered via drag-drop.
Root cause: selectTodayTagRepair selector was excluding ALL subtasks
from valid taskIds, even those that should appear as top-level items.
After reordering, the repair effect immediately "fixed" the order by
removing the subtask.
Changes:
- Update selectTodayTagRepair to include subtasks whose parent is NOT
in the Today list (these appear as top-level items)
- Update enterPredicate to check if subtask's parent is in target
list's tasks for more reliable detection
- Add guard for OVERDUE to BACKLOG edge case in _move method
- Add comprehensive tests for enterPredicate (26 tests)
- Add edge case tests for selectTodayTagRepair (38 tests)
Change DEFAULT_GITLAB_CFG.project from '' to null for semantic
correctness. The type GitlabCfg.project is string | null, so null
properly represents "not configured" rather than an empty string.
Fixes#6138
Add safe appFeatures selector and signal to prevent crash when
config.appFeatures is missing during data migration or sync.
The crash "Cannot read properties of undefined (reading
'isPlannerEnabled')" occurred when cfg() returned a partial config
object without the appFeatures property.
Changes:
- Add selectAppFeaturesConfig selector with fallback to defaults
- Add appFeatures signal to GlobalConfigService with safe initial value
- Fix selectIsFocusModeEnabled to use optional chaining on appFeatures
- Update all usages to use appFeatures() instead of cfg()?.appFeatures
Fixes#6182
The upgrade to marked v17 (commit 4bdd94cbf) broke several markdown
features:
- Bold/italic text not rendering
- Link syntax [text](url) not working
- Plain URLs not becoming clickable
Root causes:
1. In marked v17, renderer methods receive token objects with a
`tokens` array. To get rendered HTML with inline formatting,
you must call `this.parser.parseInline(tokens)` instead of
using the raw `text` property.
2. The redundant `provideMarkdown()` call in main.ts was overriding
the `MarkdownModule.forRoot()` configuration that included the
custom markedOptionsFactory.
3. The custom renderer.text URL linkifier was causing double-processing
since marked v17 with gfm:true already auto-links URLs at the
lexer level.
Changes:
- Convert listitem, link, paragraph renderers to regular functions
to access `this.parser.parseInline(tokens)` for inline rendering
- Restore image renderer with sizing syntax support
- Restore preprocessing hooks for image sizing (=WIDTHxHEIGHT)
- Remove custom URL linkifier (GFM handles it natively)
- Remove redundant provideMarkdown() from main.ts
Fixes#6181
Fill missing tasks config fields with defaults when loading data from
older app versions or synced snapshots. This prevents state validation
errors for recently added fields like isAutoMarkParentAsDone,
isAutoAddWorkedOnToToday, isTrayShowCurrent,
isMarkdownFormattingInNotesEnabled, and notesTemplate.
The mobile navigation drawer was opening from the wrong side (left instead
of right) and immediately closing when a background image was set via Unsplash.
Root cause: backdrop-filter was incorrectly applied to the HOST element
(<magic-side-nav>) which has width: 0 on mobile. Per CSS spec, backdrop-filter
creates a new containing block for position: fixed descendants, causing
.nav-sidenav with right: 0 to be positioned relative to the zero-width host
instead of the viewport.
Fix: Move backdrop-filter styles from HOST to .nav-sidenav using correct
:host-context() & syntax, ensuring the milkglass effect is applied to the
fixed-position sidebar that has its own containing block (the viewport).
Fixes#6177Fixes#6133
Sync was incorrectly reporting ERROR status for CONFLICT_CONCURRENT
rejections even when conflicts were successfully resolved via LWW merge.
Changes:
- Add RejectionHandlingResult interface with mergedOpsCreated and
permanentRejectionCount fields
- Update handleRejectedOps to return both counts
- Check permanentRejectionCount instead of rejectedCount in sync-wrapper
- Add fallback to rejectedCount for backward compatibility
- Suppress MatFormField onContainerClick timing errors during sync
This change makes the function of the button accurate.
But because it's a change people will certainly notice, I made it a separate PR from other changes (if we need to discuss).
- Add 3 unit tests verifying setProtectedClientIds is called for
SYNC_IMPORT and BACKUP_IMPORT, and not called for regular ops
- Fix inconsistent hideExpression patterns in sync-form.const.ts
using safe field?.model access pattern
- Improve E2E test uniqueness with testRunId + timestamp + random suffix
- Increase error snackbar detection timeout from 2000ms to 5000ms
- Fix hideExpression patterns in sync-form.const.ts that could show
buttons when field is undefined by adding ?? false fallback
- Replace fixed waitForTimeout in supersync.page.ts with proper
Promise.race that waits for sync signals (spinner, error, dialog)
- Remove redundant timeouts after checkbox click and password fill,
use proper toBeEnabled assertion instead
Previously, when a DELETE operation was rejected due to concurrent
modification, the service tried to create a replacement UPDATE op by
fetching current entity state. Since the entity was deleted locally,
getCurrentEntityState() returned undefined, causing the DELETE to be
discarded and sync to never complete.
Now, DELETE operations are detected and handled separately by creating
a new DELETE operation with the merged vector clock, preserving the
original actionType and payload from the stale op.
Validation was trying to self-heal TODAY_TAG orphaned IDs by mutating
the frozen NgRx state, causing "Cannot assign to read only property"
errors after SYNC_IMPORT on fresh clients. Validation should be
read-only - it now logs a warning without mutating. Orphaned IDs in
TODAY_TAG are harmless since it's a virtual tag (membership is via
task.dueDay, taskIds only stores ordering).
- Add E2E tests for SuperSync vector clock pruning scenarios:
- Tasks created after receiving SYNC_IMPORT sync correctly
- Multiple sync cycles after SYNC_IMPORT maintain consistency
- These tests verify the end-to-end flow that would fail without the fix
The E2E tests use the actual SuperSync server and verify that:
1. Client A imports backup (creates SYNC_IMPORT)
2. Client B receives import and creates new tasks
3. Client B's tasks sync back to Client A correctly
4. No sync errors occur (tasks aren't filtered incorrectly)
Vector clock pruning was removing the SYNC_IMPORT client ID during hydration,
causing new operations to appear CONCURRENT with the import instead of
GREATER_THAN. This led to SyncImportFilterService incorrectly discarding
valid ops as "invalidated by SYNC_IMPORT".
Root cause: setProtectedClientIds() was only called in RemoteOpsProcessingService
(for live sync) but NOT during app startup hydration when loading from snapshot.
Changes:
- Add migration in OperationLogHydratorService to restore protectedClientIds
from existing SYNC_IMPORT ops in the ops log (handles old data)
- Fix setVectorClock() to preserve existing protectedClientIds
- Fix appendWithVectorClockUpdate() to preserve protectedClientIds
- Add setProtectedClientIds calls in all hydration code paths
- Add comprehensive unit tests for all scenarios
The migration runs early during hydration and scans the ops log for existing
full-state ops (SYNC_IMPORT/BACKUP_IMPORT/REPAIR), setting their client ID
as protected if protectedClientIds was never set (old data from before fix).
When encryption is enabled, clients could get stuck in infinite conflict
resolution loops because they couldn't create vector clocks that dominate
the server's state. The server already computed the existingClock during
conflict detection but didn't return it to the client.
This fix:
- Adds existingClock to UploadResult type (server)
- Returns conflict.existingClock in CONFLICT_CONCURRENT/CONFLICT_STALE rejections
- Passes existingClock through client upload service to rejected ops handler
- Merges entity clocks into extraClocks for stale operation resolution
Includes E2E tests for encryption + conflict resolution scenarios.
On Android, tapping the hamburger menu caused the menu to flash and
immediately close. This happened because the touch event from the
button was captured by the drawer's swipe handler during its enter
animation. Fixed by temporarily disabling swipe detection for 300ms
when opening the menu.
- Clear wrapped provider cache when encryption is enabled/disabled/changed
to ensure new encryption settings take effect immediately
- Add hard limit (100) for deferred actions buffer to prevent unbounded
memory growth if sync gets stuck
- Limit vector clock size to prevent unbounded growth with many clients
- Prevent circular references when moving subtasks to avoid infinite loops
- Fix import-sync merge test: use goto instead of reload for reliability
with service workers
- Fix tag management test: add proper menu close handling with multiple
Escape presses and fallback click to dismiss overlays
- Fix encryption enable/disable tests: use fresh client contexts instead
of reconfiguring existing clients to avoid clean slate conflicts
- Fix encryption password change tests: correct wait states and method
names
- Add encryption password field to SuperSync form with e2e class selector