mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
8 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8e810edbe7
|
fix(sync): make marked project deletions win LWW conflicts (#9009)
deleteProject cascade-deletes a project's tasks, notes, sections, repeat config, and archive data in one reducer pass. When that op lost an LWW conflict to a concurrent project edit, only the PROJECT entity was reversed: every client resurrected an empty project and the winning client's status-blind hydration replay cascaded its tasks away after a restart (live state != post-restart replay). Rather than recreate every cascaded entity (payload scales with project size and cannot restore every side effect safely), give schema-v4 deleteProject operations explicit delete-wins precedence: - new deleteProject actions carry a shared PROJECT_DELETE_WINS_MARKER; the shared LWW planner accepts a host-supplied delete-wins classifier. A marked remote delete is applied regardless of timestamps; a marked local delete is replaced with one op whose vector clock dominates both sides. - historical unmarked (schema-v3) deletions keep timestamp-based LWW; the absence of the marker (never added by the no-op v3->v4 migration) is the real discriminator, and a schema v3->v4 barrier (mirroring v2->v3) makes older clients block on the newer-schema gate instead of mis-resolving. Delete-wins plans reuse the archive-win resolution pipeline, so they inherit its atomic persistence and losing-op rejection, and disjoint merge leaves them untouched (the delete must win the whole entity). Hardening from multi-agent review: - union allTaskIds/noteIds across multiple concurrent marked deletes for the same project, so a single replacement cannot leave orphan tasks on clients that only receive it (the task reducer removes by allTaskIds). - gate the classifier on the AUTHENTICATED payload projectId matching the plaintext entityId, so a tampered/replayed delete retargeted onto a live entity cannot silently drop a concurrent edit. - guard a null/undefined delete payload in the classifier instead of throwing and wedging the conflict pass. - pin the server's legacy-misc conflict alias to the fixed v1->v2 split boundary, not CURRENT_SCHEMA_VERSION, so this bump does not fabricate false GLOBAL_CONFIG:misc/tasks conflicts during rollout. - bind the marker with a shared const (compiler-checked on producer and consumer) and rename _isArchivePlan -> _isWholeEntityWinPlan. Documents the policy as ARCHITECTURE-DECISIONS.md #7. Addresses #8997. |
||
|
|
46e98c67dd
|
fix(sync): harden passkey registration verification (#8985)
* fix(sync): preserve offline ops and harden auth * fix(sync): prevent unverified passkey replacement Keep existing credentials until email ownership is verified and scope failed-delivery cleanup to the exact pending registration. Reject negative operation timestamps while still allowing old offline operations.\n\nRefs #8961 * test(sync): cover registration races in PostgreSQL * ci(sync): run PostgreSQL integration tests * fix(sync): bind passkeys to email verification |
||
|
|
329da9b9f3
|
fix(sync): harden full-state operation recovery (#8973)
* fix(sync): prevent destructive full-state sync races Use causal ordering and server-sequence preconditions for full-state operations, surface snapshot rejection, deduplicate migrations, and retain queued WebSocket downloads.\n\nFixes #8959 * fix(sync): block uploads after rejected imports Keep incremental operations behind a durable barrier when a local import or restore is rejected. Release the barrier only after a newer full-state snapshot is accepted, and surface the blocked state instead of reporting sync success. * fix(sync): harden causal repair recovery Persist repair base cursors across the client, provider, and server so stale snapshots are rejected before quota or history mutation and legacy repairs cannot poison restore state. Atomically rebase stale repairs after downloading the missing suffix, preserve rejected-import barriers and WebSocket watermarks, and cover PostgreSQL serialization. * fix(sync): harden repair recovery edge cases Block dependent uploads after any rejected full-state operation, reset negotiated capabilities across provider config changes, and bound WebSocket download retries. Update sync test doubles for causal repairs and the expanded duplicate-operation identity. |
||
|
|
eff41c041d
|
feat(project): project completion experience (#8036)
* feat(project): add project completion with celebration + trophy view
Completing a project marks it done and archives it, with a celebration
dialog (confetti + live stats) and a prompt to resolve unfinished tasks
(move to Inbox / mark done). Completed projects show a trophy badge and
a Reopen action on the archived-projects page.
isDone stays distinct from isArchived; selectArchivedProjects is left
intact so completed projects' tasks stay filtered out of Today/Overdue.
Append/merge deferred to #8032. Plan: docs/plans/2026-06-05-project-completion.md
* refactor(project): drop Archive menu item; Complete is the retire path
Archive and Complete produced near-identical end states; collapse to one
user-facing action. Removes the 'Archive project' menu item and handler so
Complete is the single way to retire a project. The archiveProject action,
reducer and ProjectService.archive() stay (needed for op-log replay of
historical archive ops and the legacy unarchive/restore path). Wiki updated;
menu specs cover the Complete flow.
* fix(project): keep completion dialog open for the active project
MatDialog closeOnNavigation (default true) dismissed the celebration the
moment completing the currently-active project navigated to '/'. Navigate
first, then open the dialog.
* test(project): e2e for completion flow (complete, celebrate, reopen)
Covers the resolve-unfinished-tasks prompt, the celebration dialog with
stats, the trophy badge on the archived page, and reopening. Adds an
openProjectContextMenu page-object helper.
* feat(project): confirm project completion
* feat(project): show completion celebration fullscreen
* fix(project): harden completion celebration flow
* style(project): use spacing tokens in completion dialog
* fix(project): align completion screen with project context
* style(project): soften completion screen coloring
* style(project): reuse completion screen surfaces
* fix: refine project completion dialog actions
* fix: complete projects with atomic task resolution
* fix: restore archive path in project completion UI
* refactor(project): remove dead completion code
The project-level completeProject action (OpType.Update) was never
dispatched — completion goes exclusively through the atomic
TaskSharedActions.completeProject (OpType.Batch) meta-reducer. Drop the
dead action, its reducer case, the PROJECT_COMPLETE enum member and the
immutable 'PCO' op-log code (which would otherwise be permanently
reserved for an op that can never be produced); enum count 147->146.
Also remove the unused selectCompletedProjects / selectPlainArchivedProjects
selectors (no consumers) and the misspelled, unused 'angel' confetti
field, keeping the regression tests that guard selectArchivedProjects.
* fix(project): make project completion non-reversible
Completion resolves a project's open tasks (move-to-inbox / mark-done),
which reopen cannot truly restore, so the "Reopen"/"Undo" affordances on
the completion path were misleading. Drop the celebration dialog's Reopen
button and the post-complete undo snack; the fullscreen celebration is
the feedback and deliberate reactivation still lives on the
archived-projects page (Project.reopen kept for it).
Also restore the project title param on the archive confirm dialog (it
was rendering a raw {{title}} placeholder), remove the now-unused
moveTasksToInbox / markTasksDone resolution helpers (the meta-reducer
resolves tasks atomically), and drop the orphaned UNDO / S.COMPLETED
i18n keys. Updates the completion e2e to the close flow and asserts the
resolution props are forwarded to the atomic action.
* fix(tasks): cancel native reminders for project-completed tasks
Completing a project marks its unfinished tasks done inside the
meta-reducer (no per-task updateTask), so unscheduleDoneTask$'s
native-reminder cancellation is bypassed and an OS-scheduled Android
notification could still fire for a now-done task. Add a local-only
effect that cancels native reminders for the force-completed task ids.
Local-only by design: it dispatches no actions (the persistent
dismissReminderOnly/clearDeadlineReminder would each be an extra synced
op), and done tasks are already filtered from reminders$ on all
platforms — only the native Android notification needs explicit removal.
* test(project): drop TaskService spies orphaned by helper removal
getByIdWithSubTaskData$/moveToProject/setDone/setUnDone were only used
by the removed moveTasksToInbox/markTasksDone helpers and their deleted
tests.
* feat(sync): affectedEntities multi-entity conflict detection for atomic completion
WIP checkpoint of the atomic completeProject approach. The Batch op declares
every touched entity (PROJECT, INBOX, TASKs, TODAY_TAG) via a new
affectedEntities field threaded through op-log capture, conflict detection,
the sync server (+Prisma migration) and shared-schema. Per-effect
completeProject handlers (issue two-way-sync, time-block, repeat-cfg)
re-derive the task changes the atomic op bypasses.
* revert(sync): remove affectedEntities multi-entity conflict detection
Reverts
|
||
|
|
2d9988dd73
|
perf(sync): SuperSync server speed + correctness hardening (#7621)
* docs(sync): add super sync server perf plan * perf(sync): implement supersync server perf phases * fix(sync): bracket auth cache invalidation * fix(sync): avoid empty replay state stringify * fix(sync): harden supersync batch uploads * fix super sync review findings * fix(sync): guard payload bytes backfill rollout * perf(sync): speed up payload_bytes backfill and index its scan Raise the backfill batch size (DEFAULT 5->500, MAX 25->1000) so a 100M-row operations table backfills in minutes rather than tens of hours. Add a CONCURRENTLY partial index on (user_id, id) WHERE payload_bytes = 0: it drains to empty post-backfill so the boot-time backfill self-check and the BOOL_OR quota probe stop doing a full sequential scan to prove absence, and it makes the backfill's per-user keyset paging a true index seek. Wire the new concurrent-index migration into both deploy scripts' P3018 recovery path. Add migration-SQL guard tests for the ADD COLUMN (metadata-only fast path) and the new partial index. * fix(sync): bound auth cache invalidation map and bracket every delete The auth verification cache's invalidationVersions map grew one entry per lifetime-invalidated user with no eviction (unbounded heap on a long-lived single replica). Cap it at the same 10k LRU bound as the entries map, re-inserting the just-invalidated user at the MRU tail so the CAS race protection still holds for the only window that matters (one DB round trip). Bracket the passkey/magic-link registration cleanup deletes with pre+post invalidate to match the documented convention, and invalidate on verifyEmail so a freshly-verified user isn't denied for up to the cache TTL. * perf(sync): skip the redundant exact replay-state measurement The delta accounting is a proven over-estimate of the serialized state size, so when the running bound stays within the cap the true size is too and the final exact JSON.stringify is provably redundant. Skip it in that case (still measure-and-throw whenever the bound does not prove safety). This collapses the common small/incremental replay back to zero expensive full stringifications, matching the old per-op loop instead of regressing it. Name the entity-key JSON overhead constant and document that assertReplayStateSize's return value is load-bearing. * refactor(sync): split processOperationBatch into pipeline stages Extract the 297-line batch upload method into a thin orchestrator plus six named single-responsibility stage helpers (validate+clamp, intra- batch dedupe, classify existing duplicates, conflict-detect, reserve seq + insert, full-state clock). Behavior-preserving: every stage writes terminal rejections into the shared results array by index and the two empty-set guards short-circuit exactly as before. Also share the timestamp clamp, the duplicate-op SELECT, and the merged full-state clock persistence between the batch and legacy paths so they cannot silently diverge. * test(sync): pin batch error-code divergence and aggregate-once Strengthen the intra-batch duplicate test to assert same-id / different-content yields DUPLICATE_OPERATION (deliberate divergence from the legacy INVALID_OP_ID), and document the divergence in the plan. Replace the single-full-state aggregate test with two full-state ops + a spy asserting _aggregatePriorVectorClock runs exactly once and last-write-wins — the old test could not catch a per-op-aggregate regression. Add a makeOp fixture factory. Correct the plan's overstated replay-stringification numbers. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: johannesjo <1456265+johannesjo@users.noreply.github.com> |
||
|
|
5fa8260bb8 | refactor(sync): finish package extraction polish | ||
|
|
c70ced204e |
fix(sync): fix stale comments, doc refs, and preserve lastSeq on clean-slate
Remove references to deleted docs/ai/ files, update stale comments about protectedClientIds and pruning-aware comparison to reflect current REPLACE semantics, fix MAX=30→20 heading, client_0..29→19 comment, use ?? over ||, preserve lastSeq in server clean-slate path to prevent sequence reuse, and add clarifying comments for mock limitations and validation guards. |
||
|
|
bfdac6d19a |
refactor(op-log): merge LWWOperationFactory into ConflictResolutionService
- Consolidate LWWOperationFactory methods (createLWWUpdateOp, mergeAndIncrementClocks) into ConflictResolutionService to reduce service count - Remove redundant caching in SuperSyncProvider (SyncCredentialStore already caches) - Remove _onBlurWhenNotTracking$ sync trigger from SyncTriggerService - Add documentation for dueDay/dueWithTime mutual exclusivity pattern - Change docker-compose postgres port to 55432 to avoid conflicts |