Update Conflict Management section to show auto-resolve flow:
- Both DELETE same entity → auto-resolved as "remote"
- Identical payload (same opType + payload) → auto-resolved as "remote"
- Real conflicts → show user dialog
Add auto-resolve note to Key Implementation Details.
Replace fragmented sections 2, 2.1, 2.2, 2.3 with single comprehensive
master diagram showing:
- Client-side: sync loop, download/upload flow, conflict management
- Server-side: all 6 API endpoints with request/response details
- Server processing: validation, conflict detection, persistence
- PostgreSQL: all 4 tables with columns and write operations
- Connections: API -> processing -> database with operation types
Add quick reference tables for API endpoints and PostgreSQL tables.
- Update first diagram to show ArchiveModel contains both task: TaskArchive
and timeTracking: State (not just time tracking data)
- Add flushYoungToOld action flow from OpEffects to archive
- Expand Archive Data Flow Notes with flush mechanism details
- Update Section 8.2 dual-database diagram with full ArchiveModel structure
Replace incorrect "Multi-Tab Coordinator" with actual behavior:
- Single instance enforcement via BroadcastChannel (startup.service.ts)
- Web Locks API for critical sync operations (lock.service.ts)
Add section 8.2 showing the two separate IndexedDB databases:
- SUP_OPS: Operation log (ops table, state_cache)
- PFAPI: Archive data (archiveYoung, archiveOld), time tracking, legacy sync
Update archive operations flow diagram to explicitly show:
- PFAPI IndexedDB as the storage destination for archive data
- SUP_OPS IndexedDB for operation log entries
- Clear separation between the two storage systems
Add Archive Storage section to the first architecture diagram showing:
- ArchiveService writing to IndexedDB BEFORE dispatch
- Two-tier archive storage (archiveYoung/archiveOld)
- Time tracking data (timeSpentOnDay entries)
- Notes explaining the archive data flow and sync handling
- Fix silent save failures during sync by adding snackbar notification
- Fix fresh client race condition by using synchronous window.confirm()
- Simplify LockService to Web Locks API only (97%+ browser support)
- Add monotonicity warning when server sequence decreases
- Document vector clock corruption limitation and ModelCtrl cache bypass
- Add unit tests for UUID timestamp extraction from UUIDv7
- Update hybrid-manifest-architecture.md status to Implemented
- Add documentation index (README.md) for operation-log docs
- Add index for docs/ai/sync/ with document categorization
- Replace duplicate docs with redirect stubs to canonical locations
- Add effect rules, multi-entity rules, and config constants to operation-rules.md
- Update vector-clocks.md with operation log integration section
- Expand super-sync-server README with API details and security features
- Update pfapi sync README with operation log integration info
- Extract `filterNewOps()` to OperationLogStoreService for batch duplicate filtering
- Refactor `detectConflicts()` into smaller helpers: `_checkOpForConflicts()`,
`_checkEntityForConflict()`, `_buildEntityFrontier()`, `_adjustForClockCorruption()`
- Add UUID validation to `_extractTimestampFromUuidv7()` with clear error messages
- Extract `_isValidBackupId()` helper in SyncSafetyBackupService
- Add `hasTask()` method to TaskArchiveService, replace exception-based control flow
- Cache reducer chain in TaskArchiveService to avoid recreation per call
- Add `SYSTEM_TAG_IDS` constant to tag.const.ts for filtering system tags
- Extract `_hasNoUserCreatedTags()` helper using SYSTEM_TAG_IDS
- Add iteration limit (MAX_DOWNLOAD_ITERATIONS) to download loop
- Add user notification for archive operation failures
- Add JSDoc for SyncSafetyBackup interface and backup slot strategy comments
- Use i18n keys for error messages in restore dialog
Add ability to compress archived tasks by:
- Deleting subtask entities and merging timeSpent/timeSpentOnDay to parent
- Clearing notes from tasks older than 1 year
- Clearing non-essential issue fields (keeps issueId and issueType)
The compression operation is synced across clients via the operation log.
Users can trigger compression from Settings → Import/Export with a preview
showing affected tasks and estimated space savings.
When tasks scheduled for today with a time triggered the reminder dialog,
clicking "Add all to today" did not clear remindAt, causing the reminder
to repeatedly pop up. Now remindAt is always cleared when explicitly
adding tasks to today.
Two bugs prevented legacy data from being migrated to operation logs:
1. Migration services called getAllSyncModelData() which uses NgRx store
delegate. Since NgRx store is empty during init, this returned empty data.
Fix: Use getAllSyncModelDataFromModelCtrls() to read from 'pf' database.
2. Effects dispatching actions during app init created orphan operations
before migration ran. The lastSeq > 0 check then returned early, thinking
migration already happened.
Fix: Check for snapshot first, then check for Genesis operation, and
clear orphan operations if found before proceeding with migration.
Show single checkmark when local ops are synced to remote, and double
checkmark (done_all icon) when we also recently checked remote for
updates (within 1 minute). This applies only to Super Sync.
- Add SuperSyncStatusService to track remote check freshness and
pending ops status
- Update OperationLogDownloadService to mark remote checks
- Update OperationLogSyncService to track pending ops status
- Update SyncWrapperService to expose combined status observable
- Update main-header component to conditionally show done_all vs check
When filtering operations after a SYNC_IMPORT, the previous string
comparison `op.id > latestImport.id` could incorrectly filter out
operations created in the same millisecond due to random bits in UUIDv7.
This caused repeatable task instances to not sync when created
immediately after an import operation.
Fix: Extract the 48-bit timestamp from UUIDv7 and use >= comparison
to ensure same-millisecond operations are kept.
When receiving a SYNC_IMPORT operation from another client (e.g., after
a backup import), the archive data (archiveYoung, archiveOld) was only
loaded into NgRx state but never persisted to IndexedDB. On app restart,
IndexedDB had the old data, causing time tracking loss.
Fix adds _handleLoadAllData handler to ArchiveOperationHandler that
writes archive data to IndexedDB for remote SYNC_IMPORT operations.
Fixes: 1594 hours on Client A -> 70 hours on Client B after sync
- Remove ~750 lines of unused file-based sync code from upload/download services
- Delete OperationLogManifest interface (no longer needed)
- Remove manifest service references from integration tests
- Add unit tests for HydrationStateService
- Test TaskArchiveService.updateTask dispatches persistent action
- Test TaskArchiveService.updateTasks dispatches actions for each update
- Test isSkipDispatch option prevents dispatch
- Test isIgnoreDBLock is passed through to save
- Test ArchiveOperationHandler.handleUpdateTask for remote operations
- Test updateTask is now archive-affecting action
TaskArchiveService.updateTask() and updateTasks() now dispatch
persistent actions after saving to IndexedDB, enabling sync.
ArchiveOperationHandler handles remote updateTask operations
by updating the archive with isSkipDispatch to prevent loops.
Fix moveToArchive operations being rejected by server:
- deriveOpType() now uses action's declared opType instead of deriving from entity changes
- moveToArchive declares opType: Update but was being changed to Delete based on entity changes
- Server rejected because DEL operations require singular entityId, but bulk ops use entityIds
Additional sync improvements:
- Use OpLog.warn instead of devError for old snapshot format (expected transitional state)
- Add lock-based synchronization for conflict detection (replaces flushPendingWrites)
- Handle TODAY_TAG correctly when checking if client state is empty
- Add partial apply failure handling with validation and user notification
- Server: Add race condition prevention with REPEATABLE_READ isolation and re-check after seq allocation
- Server: Add concurrent snapshot generation lock to prevent duplicate work
Update tests to use LockService instead of OperationWriteFlushService
When both local and remote operations have the same effect (e.g., both
DELETE the same entity or both UPDATE with identical payloads), the
conflict is now auto-resolved as "remote" without showing the dialog
to the user. This reduces unnecessary user interaction when conflicts
are semantically identical.
- Add isIdenticalConflict() method to detect identical conflicts
- Add _deepEqual() helper for payload comparison
- Modify presentConflicts() to separate and auto-resolve identical conflicts
- Add comprehensive tests for identical conflict detection
Replace vector clock comparison with UUIDv7 timestamp comparison in
_replayLocalSyncedOpsAfterImport. This fixes state divergence after
restore/import operations.
When restoring with isForceConflict=true, a fresh vector clock with a
new client ID is created. Comparing clocks with no overlapping client
IDs resulted in CONCURRENT (not LESS_THAN), causing old ops to be
incorrectly replayed on top of restored state.
UUIDv7 IDs are time-ordered, so op.id < syncImportOp.id correctly
identifies ops created before the restore/import that should be
filtered out.
- Add server endpoints for restore points (GET /api/sync/restore-points, GET /api/sync/restore/:serverSeq)
- Add RestorePoint types and RestoreCapable interface
- Add getRestorePoints() and generateSnapshotAtSeq() to SyncService
- Add SuperSyncRestoreService for client-side restore orchestration
- Add DialogRestorePointComponent for restore point selection UI
- Add "Restore from History" button to sync settings (visible when Super Sync is active)
- Fix circular dependency in SyncSafetyBackupService using lazy injection
- Update docker-compose.yml to support local development (configurable NODE_ENV, PUBLIC_URL)
When restoring a deleted task, we must read from the updated state
(after tag associations are restored) to get the current project
entity, not the original state.
The HTTP Headers API requires ISO-8859-1 characters. Tokens copied from
web pages may contain invisible Unicode characters (zero-width spaces,
etc.) that cause "Failed to execute 'set' on 'Headers'" errors.
- Await all saves in updateLocalMainModelsFromRemoteMetaFile() using
Promise.all() to ensure data is fully persisted before sync completes
- Remove unreliable early return optimization in sync() that compared
metaRev timestamps, causing incorrect InSync returns
- Add cache-busting (cache: 'no-store') to WebDAV HTTP adapter to
prevent stale responses
- Fix E2E tests by removing page reloads that broke sync provider
re-initialization
- Improve waitForSync test helper to properly detect new sync cycles
- Add debug logging for vector clock comparisons in sync status checks
The sync now properly detects remote changes via vector clock comparison
and uploads/downloads data correctly between multiple clients.
Add RETRY_DELAY_BASE_MS injection token to make retry delays configurable.
Tests now provide 0ms delay for instant retries, reducing test time from
7+ seconds to ~0.06 seconds for the download failure handling tests.
Add comprehensive test suite that validates state remains valid after
every state-changing action. Tests check Typia schema validation,
cross-model relationship validation, and entity state consistency.
Includes:
- Test utilities for building valid AppDataCompleteNew states
- 30 tests covering TaskSharedActions, project, tag, and note actions
- Complex scenario tests for multi-step operations
Unit tests (8 new tests in task-shared-crud.reducer.spec.ts):
- Preserve timeSpentOnDay data when restoring
- Preserve attachments when restoring
- Preserve notes, issueId, reminderId, dueWithTime
- Idempotent project restore (no duplicates on replay)
- Subtasks with their own tags restored correctly
- Position-aware restore (middle position)
- Position clamping when array shrinks
E2E test (supersync-edge-cases.spec.ts):
- Undo task delete syncs restored task to other client
- Tests full flow: create→sync→delete→undo→sync→verify
The restore logic was REPLACING taskIds arrays entirely with captured
snapshots, which could cause data loss if tasks were added between
delete and undo (e.g., during sync replay on another device).
Changes:
- Add mergeTaskIdsAtPositions() helper for position-aware merging
- Restore inserts at original position instead of replacing arrays
- Only restore task IDs that were in the captured array for each tag
- Add 5 tests for merge behavior (preserving new tasks, position, idempotency)
The previous implementation used a payload-less undoDeleteTask action that
relied on locally cached state. When synced, other devices received an empty
operation and couldn't restore the task.
Changes:
- Add restoreDeletedTask action with full payload (task, project/tag/parent context)
- Simplify meta-reducer to capture-only, export getLastDeletePayload() getter
- Move restore logic to shared reducer (sync-aware)
- Update snackbar effect to use actionFn with captured payload
- Remove obsolete undoDeleteTask action
- Add comprehensive tests (17 new tests for restore, 15 for capture)
- Add unit tests for deleteTag/deleteTags in tag-shared.reducer.spec.ts:
- Orphaned task deletion when tag is sole assignment
- Cascade delete subtasks of orphaned parents
- Cleanup task repeat configs referencing deleted tags
- Cleanup time tracking state for deleted tags
- Add unit tests for project time tracking cleanup in project-shared.reducer.spec.ts
- Add E2E tests in supersync-advanced-edge-cases.spec.ts:
- Bulk task creation (10 tasks) syncs without data loss
- Stale client reconnection after many changes
- Special characters in task names sync correctly
Note: Complex cascading delete E2E tests were removed as the scenarios
are better covered by unit tests due to UI timing fragility.
When a client switches to a new/empty server, the full state snapshot
(SYNC_IMPORT) was not being uploaded because ImmediateUploadService
was calling OperationLogUploadService.uploadPendingOps() directly,
bypassing the migration detection callback in OperationLogSyncService.
Changes:
- Update OperationLogSyncService.uploadPendingOps() to return UploadResult | null
- Change ImmediateUploadService to call _syncService.uploadPendingOps()
instead of _uploadService.uploadPendingOps() directly
- Update unit tests to reflect the new call chain
- Re-enable server-migration E2E tests (now passing)
This ensures the migration detection callback runs during immediate upload,
creating a SYNC_IMPORT when switching to a new/empty server so that other
clients joining the new server receive all data.
Fix race condition where sync could start before local operation writes
completed to IndexedDB, causing conflict detection to miss pending ops.
- Add OperationWriteFlushService that uses sp_op_log lock to ensure all
pending writes complete before reading from IndexedDB
- Call flushPendingWrites() before conflict detection in sync service
- Add retry with exponential backoff to createTestUser for rate limiting
- Add unit tests for flush service and sync ordering
The flush mechanism works by acquiring the same lock used by writeOperation().
Since effects use concatMap and Web Locks API guarantees FIFO ordering,
acquiring the lock means all prior writes have completed.
Problem: Importing backups (~8.5MB) failed with "Transaction rolled back
due to internal error". The Prisma transaction had no explicit timeout,
defaulting to 5 seconds which was insufficient for large JSON payloads.
The client then marked the operation as permanently rejected instead of
retrying.
Server-side fix:
- Add 60s timeout to uploadOps transaction (matching generateSnapshot)
- Improve error message to indicate timeout vs other errors
- Include actual error details for debugging
Client-side fix:
- Extend transient error detection to include server errors:
- "transaction rolled back"
- "internal server error"
- "server busy" / "please retry"
- HTTP 500/502/503/504 errors
- Transient errors are NOT marked as rejected, allowing retry
Tests:
- Add 6 tests for transient vs permanent error handling
- Verify network errors, timeouts, and server errors are retriable
- Verify validation errors remain permanently rejected
- Add Section 2c to architecture diagrams with mermaid diagrams
explaining the late-joiner problem and solution
- Add Section C.7 to main architecture doc with code examples
- Add Example 4 to vector-clocks.md explaining dominance filtering
- Update Last Updated dates to December 12, 2025
Split the operation processing into two passes:
1. First pass: Initialize hardDependencies and opById for ALL operations
2. Second pass: Extract dependencies and build the graph
This fixes a bug where when processing operation A, if its dependency
entity is being deleted by operation B that comes LATER in the array,
hardDependencies.get(deleterOp.id) would be undefined because B hadn't
been processed yet.
Fixes: TypeError: Cannot read properties of undefined (reading 'add')