Commit graph

42 commits

Author SHA1 Message Date
Johannes Millan
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.
2026-07-14 19:58:33 +02:00
Johannes Millan
91a1f0eda9
fix(tasks): keep REST project moves atomic across replay (#9001)
* fix(tasks): make REST project moves atomic

Capture affected subtasks in the persisted update action so project moves replay consistently, and repair stale project and section references during archive and restore.

Fixes #8983

* fix(tasks): harden project move replay

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-14 17:25:37 +02:00
Johannes Millan
51bf689bd5
fix(sync): preserve multi-entity conflict recovery (#8990)
* chore: add project-scoped Angular MCP

* chore: update npm for release-age policy

* fix(sync): preserve LWW outcomes across clients

Distinguish replacement snapshots from partial merge operations, protect device-local sync configuration, recreate winning deletes, and resolve every conflicted entity in bulk operations.

Fixes #8956

* fix(sync): harden mixed LWW conflict replay

Preserve unaffected remote and local bulk intents, keep device-local sync settings during replacement replay, and gate replacement semantics behind schema v3.

* fix(sync): recover subtask subtree and harden LWW replay follow-ups

Follow-up hardening for #8956 after multi-agent review:

- Recreate a locally-winning parent's subtasks when a remote bulk delete
  is a mixed winner. The full remote delete is applied (cascade-deleting
  the parent's subtasks via handleDeleteTasks) but only the parent had a
  compensation op, so the subtree was silently lost across devices.
- extractUpdateChanges: scan array-valued payload props instead of guessing
  `${payloadKey}s`, so irregular bulk keys (e.g. taskUpdates) no longer
  return {} and drop a remote winner's changes.
- Degrade gracefully instead of throwing when a remote update wins over a
  local delete with no reconstructable base entity, matching the
  single-entity path (a permanent sync wedge is worse than the bounded
  divergence it already accepts).
- Remove dead code (unused `deleting` set + zero-caller wrapper), use the
  Set-based scoped-bulk-delete filter, type meta via LwwUpdateMode, and
  restore the withLocalOnlySyncSettings rationale comment.

Adds regression tests for the subtree-recovery and irregular-bulk-key paths.

* fix(sync): preserve conflict outcomes during replay

Persist replacement LWW operations and bulk-delete snapshots so reconstructed state matches the result applied live. Bump the op-log DB version to prevent older clients from opening the incompatible schema.

* fix(sync): persist conflict outcomes atomically

Write remote losers, local compensations, and final remote winners in one IndexedDB transaction so crashes cannot expose a partial replay order. Add real-store coverage for live/replay equivalence and transaction rollback.

* fix(sync): preserve multi-entity conflict recovery

* fix(sync): close review gaps in multi-entity conflict recovery

- recreate a winning parent's subtasks when a remote DELETE loses
  outright (single-entity or all-local-win bulk), so clients that
  applied the delete and status-blind hydration replay converge
- apply the combined resolution batch in durable seq order so a
  pending row reused from a prior failed attempt replays identically
  live and after a crash
- restamp converted remote updates carrying the v3 replacement
  envelope to the current schema version
- strip the virtual TODAY tag from LWW task payloads and shallow-merge
  patch-mode singleton payloads instead of replacing feature state
- pin the server snapshot fast-path spec to CURRENT_SCHEMA_VERSION
  (fixes the CI failure from the v2-to-v3 bump)

* test(sync): pin outright-losing delete convergence across clients

Three-way real-reducer/real-store convergence for the pure-loser path
(live == restart replay == originating client), covering both the
same-batch recreate exemption and the cross-batch recreate path.
Verified to fail against the pre-fix service.
2026-07-14 14:10:52 +02:00
Johannes Millan
bd67174863
fix(sync): make task and time replay deterministic (#8979)
* fix(sync): make task replay values deterministic

Capture logical dates and timestamps in task actions and backfill legacy operations. Carry per-day task totals so own-operation replay is idempotent while foreign time remains additive.

Fixes #8957

* fix(sync): make time snapshots replay-safe

Exclude pending task-time batches from op-log and file-sync snapshots so their later additive operations cannot overlap snapshot state. Preserve concurrent direct credits and normalize legacy replay dates deterministically.

Fixes #8957

* fix(sync): align snapshots with queued task time

Exclude accumulator and in-flight task-time deltas from operation-log snapshots so later delta operations cannot double-count them. Capture file and direct-upload snapshots at the same operation-log boundary, and flush or clear queued time around destructive state replacement.

* fix(tasks): flush queued time at task boundaries

Persist queued timer deltas before absolute short-syntax edits, and clear them whenever project, schedule, or repeat cleanup deletes the owning task. This prevents stale batches from recreating or overwriting removed task time.

* fix(sync): preserve concurrent task time deltas

Treat concurrent task-time batches as commuting updates on both client and server, while retaining causal stale-operation checks. Reject malformed identities, dates, durations, and unsafe timestamps before replay or persistence.

* test(sync): cover task time snapshot replay

Exercise seeded snapshot and restart invariants plus a real three-client SuperSync convergence path with the initial time inside the snapshot.

* fix(sync): select action type for legacy conflicts
2026-07-13 21:53:46 +02:00
Johannes Millan
762b3c5d8d
feat(plugins): add Todoist import plugin with Import/Export launcher (#8882)
* feat(plugins): add Todoist import plugin with Import/Export launcher

* fix(plugins): fix cross-chunk subtask loss in todoist-import + hardening

* fix(plugins): clamp non-finite Todoist durations + review polish

* feat(plugins): add Eisenhower priority mapping to Todoist import

Replace the single "map priorities to p1-p3 tags" checkbox with one
mutually-exclusive control (Nothing / p1-p3 tags / Eisenhower matrix).

The new Eisenhower option reuses SP's built-in urgent/important tags by
title (p1 -> urgent+important, p2 -> important, p3 -> urgent, p4 -> none),
so imported tasks populate the Eisenhower Matrix board with no new tag
and no new plugin API. Collapsing Todoist's single priority axis onto the
2-D matrix is opinionated, so it stays opt-in and off by default.

Requested in the review of #8882.

* fix(plugins): harden Todoist import data integrity

* fix(plugins): improve Todoist import feedback
2026-07-10 13:30:03 +02:00
Johannes Millan
dc0378edd6
[codex] Fix sync import conflict from startup example tasks (#7976)
* fix(sync): ignore startup example tasks during import

* fix(sync): defer startup example tasks until initial sync completes

Switch ExampleTasksService to afterInitialSyncDoneStrict$ so example tasks
are not created before the first remote import lands. On a fresh synced
client the imported tasks are then already in the store and the length===0
guard skips creation entirely — closing the same conflict for file-based
providers (Dropbox/WebDAV), which the op-log marker gate does not cover.

The isExampleTask marker + gate exclusion stay as a safety net for the
narrow case where example tasks are created on a still-empty server and an
import arrives before they are uploaded.

Also dedupe the two markRejected call sites into _discardExampleTaskOps and
document the local-only read (a remote isExampleTask flag can never bypass
the conflict dialog) and the intentional mixed-case behavior.

* test(sync): cover mixed-pending and empty-discard import gate cases

Add a SyncImportConflictGateService test for the mixed case (real pending
work + startup example tasks): the conflict dialog is still shown, but the
example-task id is reported in discardablePendingOpIds and intentionally
left for the caller to keep on USE_LOCAL — locking the documented invariant.

Also assert markRejected is not called on the config-only silent-accept path,
pinning the empty-array guard in _discardExampleTaskOps.

* test(sync): integration coverage for example-task import gate

Run the real OperationLogStoreService + SyncImportConflictGateService against
IndexedDB (no mocks) to cover the seam the unit specs stub:
- example-task creates persisted with their real multi-entity payload are
  recognized as non-meaningful and reported as discardable
- after markRejected they are actually excluded from getUnsynced() (never
  uploaded), while a pending config op is preserved
- real user work alongside example tasks still triggers the dialog
- a remote-sourced example-task op is never discardable (gate reads local
  pending only)

Verified load-bearing: removing the gate exclusion turns the first case red.

* fix(sync): mark onboarding done when example tasks are skipped

EXAMPLE_TASKS_CREATED was only written after creating example tasks, so a
synced client that skipped creation because real tasks already existed never
set the flag — and could recreate onboarding tasks on a later startup when
the task list happened to be empty.

Set the flag whenever tasks already exist at the initial-sync gate, so
onboarding runs at most once per client.

* docs(sync): note known limits of the example-task import gate

Document two accepted, narrow residuals of syncing onboarding example tasks:
- upload→piggyback path: example ops accepted in the same upload round are
  already synced and not in the discard list; state stays correct because the
  SYNC_IMPORT filter drops them as CONCURRENT on receivers.
- file-based snapshot path: hasMeaningfulStoreData() counts example tasks (no
  in-state marker), so a fresh Dropbox/WebDAV client that made example tasks
  while sync was disabled can still see the conflict dialog.

* test(sync): cover fresh-client example-task import gate (e2e)

Add a SuperSync e2e proving a fresh client with first-run example tasks accepts
an incoming SYNC_IMPORT without a conflict dialog, plus a backward-compatible
{ allowExampleTasks } opt-in to createSimulatedClient (the harness otherwise
pre-sets SUP_EXAMPLE_TASKS_CREATED).

Uses waitForInitialSync:false + a race between the conflict dialog and sync
completion so the test actually fails when the dialog appears (pre-fix), and
asserts all four onboarding titles are absent. Guards the op-log isExampleTask
marker/gate path (not the afterInitialSyncDoneStrict$ timing, which a fresh
e2e client cannot exercise since sync is disabled at boot).

* docs(sync): link example-task import-gate limitations to #7985
2026-06-03 15:57:53 +02:00
Maikel Hajiabadi
e0645f57c3
Feat/task creation due date (#7966)
* feat(tasks): add deadline support to add-task-bar

* test(tasks): add unit tests for deadline creation

* docs(wiki): document deadline button and shortcuts

* feat(tasks): shift deadline creation to input short-syntax

* test(tasks): fix expectation in mentions spec for deadline trigger

* build(tasks): drop unused DialogDeadlineComponent import

* fix(tasks): prevent title corruption when combining schedule and deadline

* refactor(tasks): restrict preceding space guard to deadline trigger

* fix(tasks): prevent hasDeadlineTime leaking onto task during short syntax edits

* fix(tasks): auto-plan tasks when deadline is set to today via short syntax
2026-06-03 14:46:43 +02:00
Johannes Millan
34d4aa0bbf
feat(tasks): allow dragging tasks into subtask lists (#7962)
* feat(tasks): allow dragging tasks into subtask lists

* fix(tasks): support childless subtask drop targets

* fix(tasks): support dragging subtasks back to main list

* fix(tasks): preserve nested subtask sorting

* fix(tasks): address subtask drag review feedback

* fix(tasks): address drag conversion review

* test: skip onboarding in migration fresh-start e2e

* fix(tasks): harden convertToSubTask guards and dedupe tag cleanup

Address review findings on the drag-to-subtask feature (#7905):

- Keep the section and crud meta-reducer guards in lock-step via a shared
  canApplyConvertToSubTask(). Previously the section reducer stripped a task
  from its section even when the crud reducer rejected the convert (missing
  target parent or self-target), leaving the task top-level yet dropped from
  section ordering on a replayed/concurrent op.
- Reject nesting under a target that is itself a subtask. The UI renders only
  two levels, so deeper nesting would orphan the task and leave the
  grandparent's time aggregation stale.
- Tighten op-log payload validation to require string taskId/targetParentId.
- Dedupe the "remove task ids from all tags" logic shared by
  convertToSubTask/deleteTask/deleteTasks into removeTasksFromAllTags().
- Collapse the tri-state afterTaskId positioning in handleConvertToMainTask
  (undefined and null both prepend via moveItemAfterAnchor).
- Name the DragPointer type in DropListService.

Adds reducer specs for the rejected-target-parent cases.

* fix(tasks): remove empty subtask drop-target for childless parents

The dashed empty sub-task drop zone that appeared under childless parent
tasks during a drag was unwanted UI. Remove it along with its supporting
machinery (the subTaskDropCandidate signal in ScheduleExternalDragService,
the pointerdown candidate-arming in TaskListComponent, the
isEmptySubTaskDropTargetMounted computed, and the related template/SCSS).

Consequence: a task can now be nested by dragging only onto a parent that
already has a subtask list. Dragging a subtask back out to a main list
(convertToMainTask) and nesting into an existing subtask list
(convertToSubTask) are unchanged.

* fix(tasks): reliably convert subtasks dragged to the top-level list

Two issues prevented dragging a subtask out to the top-level list to
convert it into a main task:

- CDK only caches a sibling drop-list's geometry when its enterPredicate
  passes at drag start (_startReceiving). The pointer is always over the
  source subtask list then, so the top-level list was never cached and
  conversion silently failed until an unrelated parent drag warmed it.
  Open a one-microtask accept window at subtask drag start so CDK caches
  the top-level lists' geometry; the pointer guard resumes afterwards.

- An expanded neighbour's subtask list "caught" the drag in the dead-band
  just above the next parent (sibling order resolves subtask lists before
  the top-level list), silently re-parenting the subtask instead of
  converting it, and growing/sticking once entered. Treat only an actual
  subtask row as "inside" a foreign subtask list; its trailing padding now
  falls through to the top-level list for conversion. The source list
  still blocks anywhere, so in-list sorting is unaffected.

* fix(tasks): use midpoint-crossing sort for drag preview placement

CDK's SingleAxisSortStrategy swaps siblings as soon as the cursor enters
any part of their clientRect. For task lists whose item rects span the
parent's full element (header + its expanded subtask list), that swap
displaces the drop preview far below the cursor — landing it past the
target's subtasks instead of where the user is pointing.

Monkey-patch the strategy's `_getItemIndexFromPointerPosition` with a
relative-position midpoint rule (the pattern used by dnd-kit and
react-beautiful-dnd): a sibling only swaps with the dragged item once
the cursor crosses into the half of the sibling that's on the dragged
item's approach side. `enter()` keeps CDK's first-inside semantics so
initial placement still lands somewhere sensible.

Also extend `_pointerSubTaskList()` with a `.sub-tasks` wrapper fallback
so the leading strip between a parent's header and its first subtask
routes the drag into that SUB list at index 0 instead of falling through
to the top-level list and converting at the parent's slot.

* test(tasks): align drag predicate spec data

* test(tasks): reconcile createMockDrop calls after combining drag PRs

Combining the two #7905 PR lineages crossed a stale signature:
the midpoint-sort commit added enterPredicate cases calling the old
3-arg createMockDrop(modelId, filteredTasks, listId), while keilogic's
spec-alignment commit narrowed it to (modelId, listId) since
enterPredicate never reads filteredTasks. Drop the vestigial arrays
from the three affected cases so they match the simplified helper.

* fix(tasks): widen first/last subtask drop targets

Dragging a task to the first/last position of a subtask list was hard:
the midpoint sort patch gives each slot a half-row trigger, and the
two end slots have no neighbouring row to borrow the other half from,
so they were only half of the edge row. The existing "easier dragging"
padding lived on :host, outside the `.task-list-inner` cdkDropList, so
it never grew the CDK hit-rect.

Move that padding inside the sub-task drop list so the strip above the
first and below the last subtask is part of the drop rect. CDK's
enter() then drops a task arriving there as the first/last child
(SingleAxisSortStrategy._shouldEnterAsFirstChild). The matching :host
padding is dropped for sub-lists so the visible gap stays compact.

* refactor(tasks): apply multi-review follow-ups to subtask drag

Hardening and fixes surfaced by the multi-agent review of the
drag-into-subtask feature:

- Scope the CDK midpoint sort patch to VERTICAL lists. It mutates the
  shared SingleAxisSortStrategy prototype app-wide, so horizontal lists
  (boards, issue panel) were silently swept into the new hit-test;
  gating to vertical keeps them on CDK's stock behaviour and sidesteps
  the right-to-left index-inversion question. Update the spec to assert
  the horizontal fall-back.
- Keep first-child re-parent in a sub-list's leading pad. The drop-target
  CSS fix moved the SUB list's top padding inside `.task-list-inner`, so
  part of the header→first-row strip now lives in the drop rect; report
  the leading pad as a row (re-parent) while the trailing pad stays a
  convert-to-main dead-band. Doc comment updated to match.
- Memoise the per-pointer subtask-list hit-test in DropListService so the
  several enterPredicate calls CDK fires per pointer move reuse one
  document.elementFromPoint.
- Tighten convertToSubTask op-log validation to isValidEntityId (rejects
  '' / 'undefined' / 'null') instead of a bare typeof string check.
- Log ids, not full task objects, in the drop handler (titles/notes must
  not reach the exportable log).
- Drop the dead _resetMidpointSortPatchForTests export; note why the
  seeded drag pointer is inert after a plain tap.
- Add direct unit tests for the canConvert/canApply guard pair.

* test(tasks): cover drop()-to-convert dispatch and the drag e2e

Close the two coverage gaps flagged in review:

- Unit: drive the public drop() handler with CdkDragDrop events and
  assert the resulting convertToSubTask / convertToMainTask dispatch,
  including the newIds→afterTaskId placement math (after-anchor,
  first-slot null anchor) and the isDone flag for the DONE list. This
  was previously untested — only _move() and the reducer were.
- E2E: a real CDK drag of a top-level task onto a subtask row, asserting
  it converts to a subtask. Uses the manual stepped-mouse gesture (CDK
  ignores HTML5 dragTo), matching work-view/sections.spec.ts. Verified
  green 3× in a row.

* fix(tasks): cut drop snap-back flicker by emitting default list sync

On drop, CDK tears down its preview/placeholder but never moves the real
DOM node (the list is NgRx-driven), so the un-moved task is briefly
revealed at its old slot until the store re-render lands. customizeUndone
Tasks() pushed *every* work-view list emission through
observeOn(animationFrameScheduler), adding a guaranteed extra frame to
that re-render and widening the visible snap-back.

Keep the frame-defer only for the customized (sort/group/filter) path —
it does heavier work and is driven by CD-bound signals, where a sync emit
can re-enter change detection. The default path is store-driven only, so
emit it on the same tick: the list re-renders without the extra frame and
the dropped task lands in place with far less (ideally no) snap-back.

task-view-customizer spec (43) green; drag-into-subtask e2e green.

* fix(tasks): scope CDK midpoint sort patch to task-list instances

Patch the dragged list's own SingleAxisSortStrategy instance instead of
the shared prototype. registerDropList is only called by task lists, so
the prototype mutation silently changed the hit-test of every other
vertical CDK list (planner, notes, boards, tree-dnd). Shadowing the
method per instance keeps the midpoint-crossing rule scoped to task
lists and leaves all other lists on CDK's stock behaviour.

The CDK-rename guard and 'this' binding are unchanged; the global
isPatched flag is dropped since each instance now patches independently.

* fix(sync): validate afterTaskId in convertToSubTask payload

The convertToSubTask op-log validation branch accepted any payload with
valid taskId/targetParentId, ignoring afterTaskId. Require it to be a
string or null (the action type's contract) so a crafted/malformed sync
payload is rejected at the validation boundary rather than treated as a
not-found anchor downstream.

* docs(tasks): clarify customizer frame-defer and drag-to-done intent

Comment-only. Correct the task-view-customizer note: the customized
path stays on the animation-frame scheduler to batch the signal-driven
emission burst on context switch (commit fddedf3fa6), not because a sync
emit re-enters change detection (the consumer is toSignal). Document
that dragging a subtask onto the DONE list intentionally converts it to
a main task done today, even outside the Today context.

* fix(tasks): keep midpoint sort patch alive across CDK strategy recreation

The previous commit scoped the patch by shadowing the method on the
strategy *instance*. That regressed subtask drag 100% of the time: CDK
rebuilds the SingleAxisSortStrategy on every drag start
(DropListRef.withOrientation runs in its beforeStarted hook), so the
instance shadow was discarded before the first pointer move and task
lists fell back to CDK's stock first-inside hit-test — the exact tall-
item preview misplacement the patch exists to prevent.

Patch the shared prototype instead (survives recreation) but apply the
midpoint rule only when the strategy's container is a registered task
list, delegating to CDK's original otherwise. This keeps the scoping
goal (planner/notes/boards/tree-dnd stay on stock behaviour) without the
recreation fragility. Extract the scoping dispatcher as a pure function
and unit-test it.

---------

Co-authored-by: kei <keletrh@gmail.com>
2026-06-02 19:43:41 +02:00
Het Savani
b80e81f3e3
feat(tasks): auto add tasks with deadlines today to Today view (#7650)
* feat(tasks): auto add tasks with deadlines today to Today view

* fix(tasks): preserve schedule when completing tasks

* fix(tasks): cover scheduled completion edge cases

* chore(deps)(deps): bump cloudflare/wrangler-action from 3.15.0 to 4.0.0 (#7653)

Bumps [cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action) from 3.15.0 to 4.0.0.
- [Release notes](https://github.com/cloudflare/wrangler-action/releases)
- [Changelog](https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md)
- [Commits](9acf94ace1...ebbaa15849)

---
updated-dependencies:
- dependency-name: cloudflare/wrangler-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps)(deps): bump the github-actions-minor group with 4 updates (#7652)

Bumps the github-actions-minor group with 4 updates: [step-security/harden-runner](https://github.com/step-security/harden-runner), [browser-actions/setup-chrome](https://github.com/browser-actions/setup-chrome), [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `step-security/harden-runner` from 2.19.1 to 2.19.3
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](a5ad31d6a1...ab7a9404c0)

Updates `browser-actions/setup-chrome` from 2.1.1 to 2.1.2
- [Release notes](https://github.com/browser-actions/setup-chrome/releases)
- [Changelog](https://github.com/browser-actions/setup-chrome/blob/master/CHANGELOG.md)
- [Commits](4f8e94349a...2e1d749697)

Updates `anthropics/claude-code-action` from 1.0.111 to 1.0.123
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](fefa07e9c6...51ea8ea73a)

Updates `github/codeql-action` from 4.35.3 to 4.35.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e46ed2cbd0...9e0d7b8d25)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.19.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-minor
- dependency-name: browser-actions/setup-chrome
  dependency-version: 2.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-minor
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.123
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-minor
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(plugins): allow iframe-only plugin installs

* fix(config): validate start of next day boundary

* fix(sync): support separate Nextcloud login name

Refs #7617

* test(tasks): add scheduled completion round trips

* fix(focus-mode): allow active timers to switch to flowtime

* test(tasks): expand completion replay matrix

* perf(sync): use indexed findFirst for op-download minSeq

Prisma operation.aggregate({ _min: { serverSeq } }) compiles to MIN()
over a `SELECT ... OFFSET 0` subquery. The OFFSET is a Postgres planner
optimization fence, so the (user_id, server_seq) index could not serve a
first-row seek and the query degraded to a per-user O(N) scan. Under a
client reconnect stampede this ran minutes inside the 60s interactive
download transaction, blowing the tx timeout (500s) and exhausting the
connection pool (cascading upload+download failures).

Replace it with findFirst ordered by serverSeq asc, which compiles to
ORDER BY server_seq ASC LIMIT 1 — a guaranteed index seek on the existing
unique (user_id, server_seq) index. Behaviour-preserving: same minSeq
value and same null-when-empty semantics.

Update both consuming specs (operation-download.service, gap-detection)
to the two-findFirst-call shape and add a regression test asserting the
indexed query is used and the aggregate path is not reintroduced.

* fix(supersync): use 127.0.0.1 in caddy healthcheck to avoid ::1 refusal

busybox wget in caddy:2.11-alpine resolves `localhost` to ::1 first, but Caddy binds its admin API to IPv4 127.0.0.1:2019. The healthcheck got "connection refused" and marked a healthy Caddy (serving prod traffic with valid TLS) as unhealthy, making deploy.sh report a false "Container startup failed\!".

* test(focus-mode): update skip break e2e assertion

* test(focus-mode): update skip break expectation

* docs(supersync): align caddy admin healthcheck note to 127.0.0.1

Comment-only follow-up to the docker-compose healthcheck fix: the Caddyfile note still said localhost:2019; align it to the literal IP the probe now uses.

* test(focus-mode): restore mode selector locator

* refactor(tasks): make deadline auto-planning atomic

* test(focus-mode): restore mode selector locator

* fix(tasks): prevent duplicate subtask links

* fix(tasks): remove noisy warning chips

* fix(schedule): position over-budget badge correctly in day panel

* feat(tasks): auto add tasks with deadlines today to Today view

* refactor(tasks): make deadline auto-planning atomic

* Checkpoint deadline auto-plan review work

* fix(tasks): make deadline auto-planning atomic

* fix(tasks): clear orphaned remindAt when deadline auto-plan clears dueWithTime

Overdue tasks moved to Today via deadline auto-planning had their
dueWithTime cleared but kept remindAt, leaving a reminder anchored to a
time the task no longer has. Clear remindAt in the same atomic reducer
pass (one op), matching the planTasksForToday/dismissReminderOnly
convention. Document the auto-plan policy and deliberate decisions on
getDeadlineAutoPlanDecision.

* perf(tasks): O(1) Today-membership lookups in deadline auto-plan

Multi-review follow-up:
- getDeadlineAutoPlanDecision takes ReadonlySet instead of array; the
  defensive effect now passes its accumulating Set directly and hoists a
  Set for the due-task filters, removing the O(N*M) array-includes scan
  on the date-rollover path.
- Drop redundant isTodayWithOffset import; reuse the local
  getDateStrWithOffset helper (identical under the positive-finite
  timestamp guard).
- Un-export isTaskDueTodayBySchedule (file-internal only).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 21:27:40 +02:00
Johannes Millan
f8a737761a feat(sync): extract sync-core ports and fix replay date preservation
- Add SyncConfigPort and ConflictUiPort contracts in packages/sync-core
  with contract tests, implement adapters in GlobalConfigService and
  SyncImportConflictDialogService
- Sanitize validation/repair logging (typia auto-fix, menu tree repair,
  related-model validation, validation-fn) so op-log error paths never
  include user content; add validation-log-meta helper
- Preserve originating today and startOfNextDayDiffMs in
  planTasksForToday so replay on other devices keeps the same dueDay;
  legacy ops fall back to op timestamp
- Preserve doneOn timestamp during task-done replay
- Add integration specs for plan-today and task-done replay
2026-05-12 19:14:48 +02:00
Johannes Millan
3aa3ba3aa5 fix(sync): prevent day-change overdue removal from conflicting with user actions (#6992)
The automated removeOverdueFormToday$ effect created persistent sync operations
that could win LWW conflicts against explicit user actions (planTasksForToday)
on other devices, causing tasks to remain overdue after sync.

Introduce localRemoveOverdueFromToday — a non-persistent action that applies the
same state change locally but is invisible to the operation log. Since every
device runs this effect independently, syncing the removal is redundant.
2026-03-29 19:57:10 +02:00
Johannes Millan
02bc3e88e3 feat(two-way-sync): add plugin OAuth, two-way field sync, and remote issue deletion
Add OAuth support for plugins with PKCE, token persistence in local-only
IndexedDB, and Electron/web redirect handling. Extend two-way sync with
dueDay, dueWithTime, and timeEstimate field mappings including mutually
exclusive field clearing. Support remote issue deletion on task delete
and remote deletion detection during polling.

Add Google Calendar plugin (disabled from bundled builds pending legal
review) as a development reference for the plugin OAuth and two-way
sync APIs.

Additional fixes:
- Restore accidentally deleted focus-mode translation keys
- Remove full Task[] from deleteTasks action to prevent op-log bloat
- Add dueDay/deadlineDay format validation guards (#6908)
- Fix Android reminder alarm cancel intent matching
- Validate dueDay format to prevent false overdue from corrupted data
- Fix PKCE test expectation after utility consolidation
2026-03-22 13:02:41 +01:00
Johannes Millan
d401e6169e feat(tasks): add deadlines feature with reminders and planner integration
Add deadline support for tasks with date-only and time-specific deadlines.

Core:
- Add deadlineDay, deadlineWithTime, deadlineRemindAt fields to task model
- Add NgRx actions (setDeadline, removeDeadline, clearDeadlineReminder)
- Add meta-reducer with mutual exclusivity and input validation
- Register deadline actions in ActionType enum and op-log codes

UI:
- Create deadline dialog with calendar, time input, and reminder options
- Add deadline badge to task list row with overdue warning color
- Add deadline item to task detail panel with flag icon
- Add deadline options to task context menu
- Show deadlines in scheduled list page

Reminders:
- Add deadline reminder effects and selectors
- Show reminder dialog with grouped deadline/schedule sections
- Handle deadline reminder clearing on dismiss, done, add-to-today
- Cancel Android native deadline notifications on delete/archive

Planner:
- Show deadline tasks in planner day view and overdue section
- Create dedicated planner-deadline-task component
- Optimize deadline grouping with O(N) single-pass selector

Other:
- Add deadline-today banner effect for unplanned deadline tasks
- Add translation keys for all deadline UI strings
- Add E2E tests for deadline reminder flows
- Add unit tests for deadline reducer and overdue utility
- Extract shared isDeadlineOverdue utility function
- Guard app-state selectors against undefined during teardown
2026-03-15 18:48:00 +01:00
Johannes Millan
572330454b feat(tasks): add NgRx actions for task deadlines 2026-03-14 13:04:44 +01:00
Johannes Millan
0bd1bafcef fix(sync): prevent orphaned repeatCfgId during conflict resolution
Change meta-reducer to scan local state instead of using synced payload.
This prevents cross-client divergence when deleting repeat configs.

- Meta-reducer now scans NgRx state for all tasks with matching repeatCfgId
- Remove taskIdsToUnlink from action payload (deterministic cleanup)
- Simplify service method (no longer needs to query tasks)
- Enhanced validation error logging with task ID and state location
2026-01-21 14:30:24 +01:00
Johannes Millan
853bbcf268 fix(reminders): clear scheduled time when adding to today from dialog
When user clicks "Add to Today" in the reminder dialog, the task's
scheduled time is now always cleared, even if it was originally
scheduled for a specific time today (e.g., "2:30 PM today").

This ensures tasks become "sometime today" without a specific hour
when added from the reminder dialog, while preserving the existing
behavior for other flows (context menu, drag-drop).

- Add isClearScheduledTime parameter to planTasksForToday action
- Update reminder dialog to pass isClearScheduledTime: true
- Update meta-reducer logic to clear dueWithTime when flag is set
2026-01-20 17:07:24 +01:00
Johannes Millan
d6dcc86ca0 refactor: reorganize operation-log files into src/app/op-log/
Move operation-log from src/app/core/persistence/operation-log/ to
src/app/op-log/ with improved subdirectory organization:

- core/: Types, constants, errors, entity registry
- capture/: Write path (Actions → Operations)
  - operation-capture.meta-reducer.ts
  - operation-capture.service.ts
  - operation-log.effects.ts
- apply/: Read path (Operations → State)
  - bulk-hydration.action.ts/.meta-reducer.ts
  - operation-applier.service.ts
  - operation-converter.util.ts
  - hydration-state.service.ts
  - archive-operation-handler.service.ts/.effects.ts
- store/: IndexedDB persistence
  - operation-log-store.service.ts
  - operation-log-hydrator.service.ts
  - operation-log-compaction.service.ts
  - schema-migration.service.ts
- sync/: Server sync (SuperSync)
  - operation-log-sync.service.ts
  - operation-log-upload.service.ts
  - operation-log-download.service.ts
  - conflict-resolution.service.ts
  - sync-import-filter.service.ts
  - vector-clock.service.ts
  - operation-encryption.service.ts
- validation/: State validation
  - validate-state.service.ts
  - validate-operation-payload.ts
- util/: Shared utilities
  - entity-key.util.ts
  - client-id.provider.ts
- testing/: Integration tests and benchmarks

This reorganization:
- Places op-log at the same level as pfapi for better visibility
- Groups files by responsibility (write path vs read path)
- Makes the sync architecture more discoverable
- Improves navigation for developers new to the codebase
2025-12-27 17:52:11 +01:00
Johannes Millan
055dabe2e7 fix(sync): pass isIgnoreDBLock for remote archive operations
When remote sync operations triggered archive modifications (like
deleteIssueProvider, deleteProject, deleteTag, deleteTaskRepeatCfg),
the archive saves were silently dropped because the database was
locked during sync processing.

Changes:
- Add isIgnoreDBLock option to TaskArchiveService methods:
  - removeAllArchiveTasksForProject()
  - removeTagsFromAllTasks()
  - removeRepeatCfgFromArchiveTasks()
  - unlinkIssueProviderFromArchiveTasks()
  - _execActionBoth() (private helper)

- Update ArchiveOperationHandler to pass { isIgnoreDBLock: true }
  for remote operations in:
  - _handleDeleteProject()
  - _handleDeleteTags()
  - _handleDeleteTaskRepeatCfg()
  - _handleDeleteIssueProvider()
  - _handleDeleteIssueProviders()

- Add updateTasks batch action for efficient bulk archive updates
- Add _handleUpdateTasks handler for remote batch operations

All 112 tests pass (37 task-archive + 75 archive-operation-handler)
2025-12-26 13:31:50 +01:00
Johannes Millan
61ab6c4568 fix(sync): make undo task delete sync across devices
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)
2025-12-13 17:03:19 +01:00
Johannes Millan
0fe28303b2 refactor(issue): consolidate deleteIssueProvider actions to TaskSharedActions
Move deleteIssueProviders from IssueProviderActions to TaskSharedActions for
consistency with deleteIssueProvider. Both actions affect task state (unlinking
issue fields) and need meta-reducer handling.

- Add TaskSharedActions.deleteIssueProviders with taskIdsToUnlink support
- Remove unused IssueProviderActions.deleteIssueProvider/deleteIssueProviders
- Update issue-provider reducer to use new action
- Update meta-reducer to handle bulk task unlinking
- Update archive-operation-handler to use consolidated action
2025-12-12 20:48:13 +01:00
Johannes Millan
c45a3e6327 feat(sync): add atomic applyShortSyntax action for short syntax operations
- Create applyShortSyntax compound action in task-shared.actions.ts
- Add short-syntax-shared.reducer.ts meta-reducer for atomic state updates
- Refactor short-syntax.effects.ts to use single compound action instead
  of multiple separate actions (updateTask, planTaskForDay, moveToOtherProject)
- Add action mapping to state-change-capture.service.ts
- Add multi-entity-atomicity.integration.spec.ts tests
- Add short-syntax-shared.reducer.spec.ts with 15 unit tests
- Update task-shared.reducer.spec.ts for board-style pattern compatibility

This ensures short syntax operations (project move + scheduling + tag updates)
are captured as a single atomic operation for sync consistency.
2025-12-12 20:47:48 +01:00
Johannes Millan
919c8381ab refactor(oplog): revert moveToArchive to full payload, add batch chunking
Revert moveToArchive from taskIds back to full TaskWithSubTasks[] payload.
Remote clients need full task data because archived tasks sync via
archiveYoung (daily) not the operation log.

Also adds MAX_BATCH_OPERATIONS_SIZE (50) and chunking logic in
plugin-bridge.service to prevent oversized batch operations.

See docs/operation-payload-optimization-discussion.md for full analysis.
2025-12-12 20:47:41 +01:00
Johannes Millan
4c493e67b3 perf(oplog): change moveToArchive action to pass taskIds instead of full tasks
Reduce operation payload size by changing moveToArchive action signature
from { tasks: TaskWithSubTasks[] } to { taskIds: string[] }.

Reducers now look up full task data from state using the provided IDs.
This significantly reduces payload size when archiving many tasks
(e.g., 20 tasks with subtasks: ~50KB -> ~1KB).

Changes:
- task-shared.actions.ts: Change moveToArchive to accept taskIds
- task.reducer.ts: Look up task from state before calling deleteTaskHelper
- task-shared-lifecycle.reducer.ts: Build TaskWithSubTasks from state
- task.service.ts: Pass task IDs instead of full task objects
- Update tests to ensure task entities exist in state for lookup
2025-12-12 20:47:41 +01:00
Johannes Millan
91a9c60cff feat(plugins): add rate limiting and size validation for plugin data
- Add MAX_PLUGIN_DATA_SIZE (1MB) to prevent large data storage
- Add MIN_PLUGIN_PERSIST_INTERVAL_MS (1s) rate limit per plugin
- Validate data size and rate limit in PluginUserPersistenceService
- Rethrow descriptive errors for better plugin debugging
- Add unit tests for size and rate limit validation
2025-12-12 20:47:41 +01:00
Johannes Millan
04a9ef1e34 refactor(sync): migrate move operations to anchor-based positioning
Replace newOrderedIds (full list) with afterTaskId (anchor) for sync-friendly
drag-drop operations. Concurrent moves now merge correctly instead of
conflicting.

Actions migrated:
- moveProjectTaskInBacklogList
- moveProjectTaskToBacklogList
- moveProjectTaskToRegularList
- moveTaskInTodayList (PROJECT and TAG contexts)

Also includes:
- deleteProject payload slim-down (projectId + noteIds instead of full Project)
- Helper functions: moveItemAfterAnchor(), getAnchorFromDragDrop()
- Comprehensive tests for all anchor-based moves
- Documentation updates in todo.md
2025-12-12 20:46:41 +01:00
Johannes Millan
3129c1dbca test(oplog): add persistent-action tests and fix compilation/lint errors
- Add unit tests for isPersistentAction type guard.

- Fix compilation errors in task scheduling components caused by removed reminderId/removeReminderFromTask.

- Fix type error in create-sorted-blocker-blocks.spec.ts.

- Fix lint errors in various files.
2025-12-12 20:46:27 +01:00
Johannes Millan
b383025fc1 chore: modernize type assertions to use satisfies 2025-12-12 20:46:26 +01:00
Johannes Millan
88cdfb13d8 refactor: improve op log persistence and update addTagToTask signature
- Refactor TaskSharedActions.addTagToTask to accept tagId instead of full Tag object to separate concerns.
- Update reducers and components to dispatch addTag separately when creating new tags.
- Add batch append support to OperationLogStore.
- Add extensive tests for OperationLogUploadService and effects.
2025-12-12 20:46:26 +01:00
Johannes Millan
1ed936ab23 feat(sync): optimize tag addition to task
- Consolidate 3 separate operations ([Tag] Add Tag, [Task Shared] Update Task, [MenuTree] Update Tag Tree) into a single [Task Shared] Add Tag To Task operation.
- This reduces operation log overhead and improves sync efficiency.
- Add 'addTagToTask' action to TaskSharedActions.
- Update Tag, Task, and MenuTree reducers to handle the new atomic action.
- Refactor DialogEditTagsForTaskComponent to dispatch the new action.
- Add 'createTagObject' helper to TagService.
- Add ACTION_TYPE_ALIASES support to operation converter (infra).
2025-12-12 20:46:26 +01:00
Johannes Millan
a1f7284b02 fix(op-log): add persistence meta to all TaskSharedActions
- Convert action-blacklist to use actual action imports instead of strings
- Remove 3 non-existent actions from blacklist (dead code)
- Add meta.isPersistent to all TaskSharedActions that were missing it:
  - convertToMainTask, moveToArchive, restoreTask
  - scheduleTaskWithTime, reScheduleTaskWithTime, unscheduleTask
  - dismissReminderOnly, moveToOtherProject, deleteProject
  - planTasksForToday, removeTasksFromTodayTag
  - removeTagsForAllTasks, moveTaskInTodayTagList
  - batchUpdateForProject

This fixes the issue where adding tasks to today (and other scheduling
operations) didn't trigger persistence to the operation log.
2025-12-12 20:46:13 +01:00
Johannes Millan
be494ec7c4 feat: Implement Operation Log & Multi-Tab Coordination (Phase 1-3)
This commit introduces the foundation for an operation-based (event sourcing) sync model,
implementing Phases 1, 2, and initial aspects of Phase 3 as outlined in `docs/ai/operation-log-sync.md`.

Key changes include:

- **Local Operation Logging (Phase 1):**
  - Created core services: `OperationLogStoreService` (IndexedDB), `LockService` (Web Locks + LocalStorage fallback), `OperationLogEffects` (NgRx Effect to capture persistent actions), `OperationLogHydratorService` (for startup hydration), and `OperationLogCompactionService`.
  - Defined essential types: `Operation`, `PersistentActionMeta`, `OpType`, `EntityType`.
  - Implemented a UUID v7 generator (`uuid-v7.ts`).
  - Configured an action blacklist (`action-whitelist.ts`) to exclude UI-only actions from persistence.

- **Multi-Tab Coordination (Phase 2):**
  - Implemented `MultiTabCoordinatorService` using `BroadcastChannel` to synchronize operations across multiple browser tabs/windows in real-time.
  - Integrated this service into `OperationLogEffects` to broadcast newly persisted operations.
  - Extracted `convertOpToAction` into a shared utility for reuse.

- **Dependency Resolution (Phase 3 - initial):**
  - Introduced `DependencyResolverService` to identify and check dependencies of operations (e.g., Task depending on Project or Parent Task). This lays the groundwork for robust replay and conflict resolution.

- **Action Annotation & Blacklisting:**
  - Refactored core NgRx action creators for `TaskSharedActions`, `ProjectActions`, and `TagActions` to include `PersistentActionMeta`. This allows the `OperationLogEffects` to correctly capture and store state-changing operations while maintaining compatibility with existing reducers/effects.
  - The implementation adheres to a blacklisting approach, where actions with metadata are persisted unless explicitly blacklisted.
2025-12-12 20:46:05 +01:00
Johannes Millan
27b714bf91 feat(scheduleRightPanel): improve on drag out to unschedule 2025-10-17 12:25:52 +02:00
Johannes Millan
24fced4617 feat(plugins): update plugin infrastructure and cleanup 2025-07-10 15:06:48 +02:00
Johannes Millan
88814795c7 feat(sync-md): new approach and make it work better 2025-07-07 18:39:14 +02:00
Johannes Millan
63ed3fae3e feat: add option to dismiss reminder without removing from Today #4601
- Add new dismissReminderOnly action that only clears dueWithTime
- Keep task in Today view when dismissing reminder this way
- Add "Dismiss Reminder (Keep in Today)" option to reminder dialog
- Implement for both single and multiple task reminder dialogs

Closes #4601
2025-06-28 15:51:58 +02:00
Johannes Millan
cfba4c1f51 refactor(task): migrate remaining tag.reducer.ts logic to task-shared.reducer.ts
- Migrate PlannerActions.transferTask handler
- Migrate PlannerActions.planTaskForDay handler
- Migrate PlannerActions.moveBeforeTask handler
- Add moveTaskInTodayTagList action and handler to TaskSharedActions
- All tag-related task logic now centralized in task-shared meta-reducer
2025-06-13 21:17:31 +02:00
Johannes Millan
a1666f9793 refactor(task): migrate scheduling actions to TaskSharedActions and remove deprecated functions 2025-06-13 21:17:31 +02:00
Johannes Millan
4972ecb03e refactor(task): migrate removeTagsForAllTasks action to TaskSharedActions 2025-06-13 09:00:37 +02:00
Johannes Millan
daa62dda9b refactor(task): replace moveToArchive_ action with TaskSharedActions.moveToArchive 2025-06-13 08:30:49 +02:00
Johannes Millan
f0a00f20e5 refactor(task): extract moveToOtherProject to task-shared meta-reducer
- Move moveToOtherProject action from task.reducer to task-shared meta-reducer
- Add comprehensive project task list management (removes from old, adds to new)
- Handle both regular taskIds and backlogTaskIds in project transitions
- Add 5 comprehensive tests covering all edge cases:
  - Basic task move between projects
  - Moving task with subtasks
  - Moving from backlog
  - Non-existent source project
  - Non-existent target project
- All tests passing (580/580)

Key improvements:
- Centralized cross-cutting project management logic
- Proper handling of project task lists during moves
- Maintains all task relationships and subtask hierarchies
2025-06-12 16:28:10 +02:00
Johannes Millan
be047b4c3d refactor(task): extract updateTask with enhanced tag handling
- Move updateTask action from task.reducer to task-shared meta-reducer
- Enhance updateTask to handle tag changes when tagIds are updated
- Remove updateTaskTags action completely - updateTask now handles this
- Add comprehensive tests for new enhanced functionality
- All tests passing with proper task and tag state updates

Key improvements:
- Single action to update both task properties and tags
- Proper tag relationship management in meta-reducer
- Maintains all existing task update functionality
- Eliminates need for separate updateTaskTags action
2025-06-12 15:53:20 +02:00
Johannes Millan
d9db8570e0 refactor(task): migrate actions to modern createActionGroup syntax
- Move all actions to task-shared.actions.ts using createActionGroup
- Use camelCase naming convention for action names
- Update task-shared.reducer.ts to use new action references
- Update tests to use new TaskSharedActions
- All tests passing and linting clean
2025-06-12 15:27:03 +02:00