Commit graph

21395 commits

Author SHA1 Message Date
Johannes Millan
ffca0f1397 fix(sync): guard rebuild backup identity and keep undo across reload
The pre-replace import backup and the USE_REMOTE rebuild recovery
marker were matched by timestamp only, so a concurrent capture or a
reload between rebuild completion and snack dismissal could clear or
restore the wrong backup.

- Import backups carry an opaque backupId (uuidv7); clearImportBackup,
  replaceAllForRawRebuild and applyRemoteReplaceWithSnapshot verify the
  expected identity inside their transactions before acting.
- Completing a raw rebuild atomically replaces the incomplete marker
  with a durable raw_rebuild_recovery entry, so the "restore previous
  data" Undo survives a reload; StartupService re-offers it at boot and
  dismissal retires exactly the offered backup.
- SnackService treats a sticky recovery/update action as a single
  persistent slot: unrelated transient snacks no longer destroy a
  visible Undo, and dismissal awaits the snack's dismissFn.
2026-07-11 18:00:30 +02:00
Johannes Millan
bdb0fef9a9 fix(sync): make remote reducer checkpoint atomic with its clock merge
A committed reducer must never be durable without its vector clock, and
a merged clock must never be durable without its reducer checkpoint —
either mismatch lets the next local operation be causally older than
state already visible in NgRx after a crash.

- markArchivePending + separate mergeRemoteOpClocks are replaced by one
  markReducersCommittedAndMergeClocks transaction (ops + vector_clock);
  the clock math is extracted into a pure calculateRemoteClockMerge so
  the standalone merge path keeps identical full-state-reset semantics.
- The sync-core RemoteOperationApplyStorePort gains an
  onRemoteClocksDurable hook so deferred local actions drain exactly
  when clocks are durable, not merely when ops were applied; a
  checkpoint rejection can no longer mask the primary apply error.
- Conflict resolution's local-wins path writes remote losers and rebased
  local compensations in one appendMixedSourceBatchSkipDuplicates
  transaction, so synthetic ops cannot reuse or regress the client
  counter.
- DB_VERSION 7 -> 8 as a deliberate downgrade barrier: released v7
  readers only understand 'failed' and would silently overlook
  outstanding 'archive_pending' work.

op-log suite and sync-core suite cover the checkpoint rollback, atomic
clock merge, mixed-source ordering and drain failure matrix.
2026-07-11 17:59:42 +02:00
Johannes Millan
f9610530c9 fix(sync): converge rebuild capture races, unwedge archive recovery
Remediate the findings of the 5-agent necessity review:

- USE_REMOTE: a local capture racing the locked rebuild now throws a
  typed CaptureRacedRebuildError, and forceDownloadRemoteState retries
  phase 2 in-call (bounded, 3 attempts) through the existing
  crash-resume branch — raced ops fold into preservedLocalOps and the
  already-downloaded history is reused (WS downloads and immediate
  uploads stay gated by the marker, so no re-download is needed).
  Previously every attempt aborted while e.g. time tracking dispatched
  continuously, re-downloading the full history per sync trigger and
  churning the Undo snack (now shown on final failure only).
- Hydrator archive retry: pass skipDeferredLocalActions and drain
  explicitly with a caught error. A drain throw from the coordinator's
  finally used to mask the archive result and escalate out of
  hydrateStore() into attemptRecovery(), which can import stale legacy
  data over a correctly hydrated store.
- Incomplete-remote gate: run one in-session archive-only retry when
  the only blockers are quarantined failed/archive_pending ops, so a
  transient archive failure self-heals on the next sync instead of
  wedging until app restart. Never attempted while the rebuild marker
  is set or reducer-uncommitted pending rows exist.
- Boot recovery: StartupService surfaces the pre-replace backup's
  persistent restore snack when a stranded raw_rebuild_incomplete
  marker is found, covering users who boot offline or disable sync
  after finding the app "emptied" by a mid-rebuild crash.
- Snack correctness: latch the USE_REMOTE newer-schema warning once per
  session; guard _notifyBlockedOp and the LWW apply-failure snack with
  hasPendingPersistentAction() so they cannot destroy a visible Undo;
  drop the useless "Update app" action for below-minimum data.
- Strings: MIGRATION_FAILED / VERSION_UNSUPPORTED now describe the
  blocking semantics instead of the removed skip behavior.
- Store: getPendingRemoteOps excludes rejected rows (parity with
  getFailedRemoteOps) so a rejected-but-pending row cannot trip the
  sync gate for a session.
- Server: compute the upload request fingerprint eagerly after the
  rate-limit gate — identical cost to the lazy closure in every path,
  minus the memoization machinery; laziness remains in the snapshot
  handler where it skips hashing multi-MB states.

op-log suite 3004/3004, super-sync-server 831/831, checkFile clean on
all touched files. Nine regression tests pin the new behavior.
2026-07-11 14:49:08 +02:00
Johannes Millan
01d12cacb7 test(sync): target transient rejection assertion 2026-07-11 13:18:03 +02:00
Johannes Millan
eaa15fe575 fix(sync): handle initial provider setup safely 2026-07-11 11:25:57 +02:00
Johannes Millan
bfdc63e0c6 fix(sync): preserve actionable recovery state 2026-07-10 22:50:33 +02:00
Johannes Millan
eabfd84436 fix(sync): migrate legacy remote failures once 2026-07-10 22:35:30 +02:00
Johannes Millan
8aa0b8ca50 fix(sync): gate incomplete rebuild recovery 2026-07-10 22:35:17 +02:00
Johannes Millan
aa6e74bd34 fix(supersync): preserve occupied ids during cleanup 2026-07-10 22:35:05 +02:00
Johannes Millan
62e9e34b29 fix(sync): preserve incomplete recovery work 2026-07-10 22:11:11 +02:00
Johannes Millan
e3093a416c fix(sync-core): validate remote apply results 2026-07-10 22:11:00 +02:00
Johannes Millan
1edcb1bfac fix(supersync): isolate invalid upload operations 2026-07-10 22:10:52 +02:00
Johannes Millan
7866e16b56 docs(sync): document recovery checkpoints 2026-07-10 19:38:03 +02:00
Johannes Millan
5d568dcf8c test(sync): verify non-task conflict convergence 2026-07-10 19:37:55 +02:00
Johannes Millan
0939f5af69 fix(sync): enforce upload completion contracts 2026-07-10 19:35:37 +02:00
Johannes Millan
3480d749d7 fix(sync): block on incomplete local persistence 2026-07-10 19:29:23 +02:00
Johannes Millan
87353d4b0a fix(sync): preserve edits across rebuild resume 2026-07-10 19:23:07 +02:00
Johannes Millan
de8a2cb7b0 fix(sync): preserve fresh-client recovery state 2026-07-10 19:18:20 +02:00
Johannes Millan
ab9e1d38c9 fix(sync): exempt fresh-client startup ops from conflict gate
Fresh clients generate startup config and genesis operations before their first sync. Treat those operations as setup state until a prior sync exists, while continuing to protect later config changes and all ordinary user work from authoritative snapshot replacement.
2026-07-10 19:08:38 +02:00
Johannes Millan
be0d6e7dd0 fix(sync): surface Undo when an interrupted USE_REMOTE resume can't finish
Multi-review remediation for the USE_REMOTE rebuild path.

- Crash-resume recovery gap (confirmed by 2 independent reviewers): when an
  interrupted-rebuild resume aborts in its download/validate phase (empty or
  newer-schema remote, or a persistent download failure), forceDownloadRemoteState
  threw before it could offer Undo. The prior attempt had already committed the
  destructive baseline, so the pre-replace backup was stranded with no restore
  affordance — reading as total data loss. downloadRemoteOps now surfaces the
  recovery Undo on a resume abort (deduped via hasPendingPersistentAction so
  repeated auto/WS syncs don't respawn it). Covered by two new unit tests.

- SnackService: collapse the redundant dual write of the persistent-action flag
  (set in open() AND recomputed in _openSnack) to one authoritative write in
  open(); _openSnack keeps only the on-dismiss clear (3-reviewer consensus).

- Document the archive-retry idempotency invariant on ARCHIVE_AFFECTING_ACTION_TYPES:
  the hydrator retry re-runs archive side effects with skipReducerDispatch even
  after a fully-successful run, so they must stay idempotent (overwrite, never
  additive) to avoid double-counting time-tracking/counter deltas.
2026-07-10 18:41:09 +02:00
Johannes Millan
a3272ed138 fix(sync): keep USE_REMOTE recovery Undo visible after routine sync
After a "Use Server Data" replace shows the persistent Undo recovery
snack (#8107), a follow-up routine sync-success snack must not silently
replace it. SnackService now tracks a pending persistent action
(actionStr + duration 0); the header sync() skips its success feedback
while one is showing. Unblocks the committed USE_REMOTE crash-resume e2e
(supersync-use-remote-crash-resume.spec.ts), whose Undo assertion
depends on the recovery snack surviving the sync that resumed the rebuild.
2026-07-10 17:38:47 +02:00
Johannes Millan
19b6fed044 test(e2e): cover USE_REMOTE interrupted rebuild crash resume
Reloads client B at the exact atomic-baseline-commit cutoff, then
verifies the next sync detects the interrupted rebuild, redoes the raw
download, keeps the first attempt's pre-replace backup, finishes on the
remote state, and offers an Undo that restores B's original import.
2026-07-10 17:15:51 +02:00
Johannes Millan
360f95ed7a fix(sync): preserve device-local sync settings in rebuild baseline
USE_REMOTE's atomic rebuild synthesizes a globalConfig shell from
DEFAULT_GLOBAL_CONFIG (getDefaultMainModelData omits globalConfig), then
re-applies the device's local-only sync settings (provider, isEnabled,
isEncryptionEnabled, syncInterval, isManualSyncOnly) onto the baseline.

Without this, an interrupted rebuild committed a baseline whose sync
config was pure defaults, so the resumed client could lose the provider
and schedule it needs to sync again. Adds a unit test asserting the
device-local settings survive into runRemoteStateReplacement's baseline.
2026-07-10 17:15:13 +02:00
Johannes Millan
17ab963c8f Merge remote-tracking branch 'origin/feat/sync-55222e' into feat/sync-55222e
* origin/feat/sync-55222e:
  test(e2e): identify counter without the flaky hover tooltip
2026-07-10 17:03:57 +02:00
Johannes Millan
16af839641 test(sync): align example-task gate spec with widened conflict gate
The integration spec still pinned the pre-branch semantics where a
pending GLOBAL_CONFIG op counted as non-meaningful. The widened gate
(79f91e36fe) deliberately treats config changes as meaningful user work
(entity-wide exemptions are unsafe — GLOBAL_CONFIG carries synced
preferences), pinned by the gate unit spec but missed here, so the full
suite failed since that commit. Split the case: example-only pending
stays silent + discardable; config pending now expects the dialog.
2026-07-10 16:56:50 +02:00
Johannes Millan
fc20f81c2c test(sync): force the upload-piggyback conflict race 2026-07-10 16:55:18 +02:00
Johannes Millan
a8f29a3f62 test(sync): avoid conflict-gate e2e deadlock 2026-07-10 16:49:07 +02:00
Johannes Millan
0df3e81155 fix(sync): remediate multi-agent review findings
- USE_REMOTE: fix guaranteed self-deadlock — flushPendingWrites() was
  called while already holding the non-reentrant sp_op_log lock its own
  Phase 2 re-acquires, so every "Use Server Data" hung 30s and aborted.
  New OperationWriteFlushService.flushThenRunExclusive owns the bounded
  flush->lock->recheck barrier; ServerMigrationService reuses it, which
  also bounds its previously unbounded snapshot-cutoff retry loop.
- USE_REMOTE: make the rebuild crash-resumable. A raw_rebuild_incomplete
  META marker is set atomically with the baseline replacement and cleared
  after the replay commits; the next sync redoes the raw rebuild instead
  of the normal download (which excludes own ops server-side and would
  silently lose the un-replayed suffix). The resume keeps the first
  attempt's pre-replace backup instead of overwriting the single slot
  with the partial baseline.
- Deferred actions: serialize overlapping drains (two concurrent drains
  could mint two ops for one user intent), stop the drain at the first
  transient failure instead of persisting successors out of order (the
  older edit would win LWW everywhere via clock inversion), abandon
  permanently invalid actions after one attempt (typed
  PermanentDeferredWriteError) instead of retrying + snacking on every
  sync forever, and latch the buffer-size warnings to once per stuck
  window (previously a blocking dev dialog per action past 100); drop
  the no-op 5000 "hard cap" tier.
- writeOperation: post-append bookkeeping failures no longer propagate
  into the deferred retry loop, which re-appended the same action under
  a fresh op id (double-apply of additive payloads).
- remote-apply: full-state cleanup retains every full-state op of the
  current batch, so an archive_pending import can no longer be deleted
  before its archive retry (startup replay would lose a change already
  visible at runtime).
- Hydration: sanitize a malformed stored schemaVersion per-op instead of
  failing the whole boot into recovery; strict parsing stays on the
  receive/upload paths (0 still parses so a below-minimum remote op
  surfaces as VERSION_UNSUPPORTED, not a generic migration failure).
- Server: request fingerprints are computed lazily — only when a dedup
  entry exists, and otherwise after the rate-limit/pre-quota gates but
  before uploadOps mutates the parsed ops. Fixes the pre-gate hashing of
  full payloads (authenticated DoS-cost regression) and repairs three
  sync-fixes retry tests to model true identical-body retries.
- Delete dead code: failed-op-ids util (+spec) and MAX_VERSION_SKIP (the
  removed forward-compat band); fix the stale clock-merge parity comment.

sync-core 210/210, shared-schema 50/50, super-sync-server 825/825, and
all touched Angular specs pass.
2026-07-10 16:47:29 +02:00
Johannes Millan
a09ef864cf test(e2e): identify counter without the flaky hover tooltip
getNamedClickCounter hovered the counter button and waited for its
matTooltip title, but matTooltip does not open on Playwright's synthetic
hover in headless CI — the assertion timed out at PHASE 1 before the gate
logic ran (run 29098937743, SuperSync 1/6). Each client has exactly one
counter at every interaction point here, so assert the button's rendered
initial instead of hovering for a tooltip.
2026-07-10 16:43:23 +02:00
Johannes Millan
eecf33a4e8 fix(sync): remediate multi-agent review findings
- USE_REMOTE: fix guaranteed self-deadlock — flushPendingWrites() was
  called while already holding the non-reentrant sp_op_log lock its own
  Phase 2 re-acquires, so every "Use Server Data" hung 30s and aborted.
  New OperationWriteFlushService.flushThenRunExclusive owns the bounded
  flush->lock->recheck barrier; ServerMigrationService reuses it, which
  also bounds its previously unbounded snapshot-cutoff retry loop.
- USE_REMOTE: make the rebuild crash-resumable. A raw_rebuild_incomplete
  META marker is set atomically with the baseline replacement and cleared
  after the replay commits; the next sync redoes the raw rebuild instead
  of the normal download (which excludes own ops server-side and would
  silently lose the un-replayed suffix). The resume keeps the first
  attempt's pre-replace backup instead of overwriting the single slot
  with the partial baseline.
- Deferred actions: serialize overlapping drains (two concurrent drains
  could mint two ops for one user intent), stop the drain at the first
  transient failure instead of persisting successors out of order (the
  older edit would win LWW everywhere via clock inversion), abandon
  permanently invalid actions after one attempt (typed
  PermanentDeferredWriteError) instead of retrying + snacking on every
  sync forever, and latch the buffer-size warnings to once per stuck
  window (previously a blocking dev dialog per action past 100); drop
  the no-op 5000 "hard cap" tier.
- writeOperation: post-append bookkeeping failures no longer propagate
  into the deferred retry loop, which re-appended the same action under
  a fresh op id (double-apply of additive payloads).
- remote-apply: full-state cleanup retains every full-state op of the
  current batch, so an archive_pending import can no longer be deleted
  before its archive retry (startup replay would lose a change already
  visible at runtime).
- Hydration: sanitize a malformed stored schemaVersion per-op instead of
  failing the whole boot into recovery; strict parsing (floor now 1,
  matching the server contract) stays on the receive/upload paths.
- Server: request fingerprints are computed lazily — only when a dedup
  entry exists, and otherwise after the rate-limit/pre-quota gates but
  before uploadOps mutates the parsed ops. Fixes the pre-gate hashing of
  full payloads (authenticated DoS-cost regression) and repairs three
  sync-fixes retry tests to model true identical-body retries.
- Delete dead code: failed-op-ids util (+spec) and MAX_VERSION_SKIP (the
  removed forward-compat band); fix the stale clock-merge parity comment.

sync-core 210/210, shared-schema 50/50, super-sync-server 825/825, and
all touched Angular specs pass.
2026-07-10 16:39:37 +02:00
Johannes Millan
d212740254 test(e2e): resolve A's post-import conflict as USE_LOCAL in gate test
Client A already synced a populated state, so importing a backup diverges
from the server and A's own sync raises the sync-import conflict gate.
syncAndWait() defaulted to USE_REMOTE, which discarded A's import before it
reached the server, leaving Client B with nothing to conflict against, so
the PHASE 5 conflict dialog never appeared (run 29090279243, SuperSync 1/6).

Pass { useLocal: true } so A force-uploads the import as a new SYNC_IMPORT
the server keeps, letting B's pending simple-counter change trigger the
conflict dialog as intended.
2026-07-10 16:09:35 +02:00
Johannes Millan
f38342eeb9 fix(sync): harden replay and conflict recovery
Make remote replay and authoritative state replacement crash-resumable and atomic. Preserve pending changes until piggyback processing commits, and stop cleanly on incompatible operations. Validate sync identities and schemas while strengthening request deduplication and privacy-safe diagnostics.
2026-07-10 15:24:51 +02:00
Johannes Millan
6e6ce34c15 test(e2e): incoming SYNC_IMPORT must prompt on pending non-task work
Regression test for the widened incoming-full-state conflict gate and the
piggyback pre-upload pending-snapshot fix: a pending SIMPLE_COUNTER change
(non-TASK/PROJECT/TAG/NOTE) must trigger the conflict dialog instead of
being silently discarded by an incoming SYNC_IMPORT from another client.

Not yet run (SuperSync docker stack is unavailable in this sandbox); run via
npm run e2e:supersync:file or the E2E Tests (Scheduled) workflow.
2026-07-10 13:36:52 +02:00
Johannes Millan
79f91e36fe fix(sync): remediate sync-correctness review findings
Address findings from a full sync-system review (blockers + high/medium):

- USE_REMOTE ("Use Server Data") is now a true download-first rebuild:
  fetches the complete server history (incl. own/already-known ops via a
  raw-download mode) and validates it before any local mutation; aborts
  untouched on download failure, empty remote, or newer-schema ops.
- Widen the incoming full-state conflict gate to treat all user work as
  meaningful (not just TASK/PROJECT/TAG/NOTE CUD); fix the piggyback race
  by judging against a pre-upload pending snapshot.
- Stop advancing the server cursor past ops blocked by schema/migration
  failures so they are retried after an app update; block any op from a
  newer schema version (no forward-compat band).
- Retry of failed remote ops runs archive side effects only, never
  re-dispatching reducers whose effects already committed (fixes additive
  double-apply of time-tracking/counter deltas across hydration+retry).
- Use local monotonic seq (not lexical UUIDv7) to decide which ops are
  covered by an uploaded snapshot, preventing dropped unsynced ops under
  clock rollback.
- Server: include entityIds in duplicate-operation equality.
- Capture buffer no longer silently drops accepted actions at 100 (warns
  to reload); drop only past a 5000 pathological cap.

Adds regression coverage for each fix. sync-core 209/209,
super-sync-server 817/817, and all touched Angular specs pass.
2026-07-10 13:25:30 +02:00
Johannes Millan
6e20c09e25 chore: stop tracking .claude/skills symlink (runtime-managed)
The Claude Code runtime scaffolds .claude/ each session and materializes
.claude/skills as a real directory, clobbering the committed symlink
(-> ../.agents/skills). Git then reported a perpetual 'D .claude/skills' in
every worktree/session. Skill sources remain tracked under .agents/skills/;
let the runtime own .claude/skills and ignore it via the existing /.claude/*
rule.
2026-07-10 10:46:38 +02:00
Johannes Millan
83e52853ec chore(deps): pin npm version to remove lockfile drift
Contributors on npm 10 (Node 22's bundled default) vs npm 11 reconcile the
app-builder-lib.minimatch override differently, rewriting package-lock.json on
npm install. Pin npm 11.7.0 via the standard packageManager field (Corepack)
and complete the existing volta block so volta users pick it up automatically.

Ref: discussion #8869
2026-07-10 10:34:53 +02:00
Yehonatan Avrahimi
18c48d5ea8
feat(tasks): add task through eml file hover (#8282)
* feat(eml): add EML file parsing and task creation on drop

Currently a simple skeleton, needs more work, but should be a good POC before refining.

Fixes #518

* fix(main-header): correct email message formatting and handle EML upload errors

while this commit looks bad, its a first commit before refining this solution and sending the final PR.

Fixes #518

* refactor(eml): move eml-parser to util and tighten isFileEml check

* feat(eml): on eml hover error parse error to log and show a snack message

* bugfix(eml-typecheck): tighten type for 'sender' to not have bugs

* refactor(eml-drop): updated file locations for each file drop

* bugfix(eml-drop): use stable npm package for parsing eml files

* bugfix(eml-drop): remove relevant "dev" flag

* refactor(eml-drop): drop empty emails with a snack instead of throwing

* test(eml-drop): add isFileEml and parseEml unit tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* rafactor(eml-service): refactor code to be consistent  as written in issue number 3

* bugfix(eml-drop): remove file name when geting an error

as written in 1. inside the pr

* docs: update task creation instructions to include .eml file support

* refactor(eml-parser): update import statement for PostalMime to dynamic import

* refactor(eml-drop): enhance file drop handling and improve error logging

* test(eml-drop): add unit tests for EmlDropDirective and EmlDropService

* feat(tasks): save eml body to task notes and harden eml parsing

- capture the parsed plain-text email body into task notes (text/plain
  only, never HTML, to avoid injecting untrusted markup into markdown notes)
- pass the File/Blob straight to postal-mime so it applies the message's
  own charset/transfer-encoding instead of forcing UTF-8 via file.text()
- use relative import paths to match repo convention
- document why the drop handler relies on the global dragover preventDefault
- update wiki wording + tests for the new add() signature and notes behavior

* fix(tasks): harden eml-to-task against untrusted subject/size

Follow-up to the eml-drop feature from a multi-agent review:

- suppress short-syntax on imported email subjects: email content is
  untrusted external input, but TaskService.add() dispatches addTask which
  ShortSyntaxEffects parses for #tag/@date/+project/URL tokens — stripping
  them from the intended literal "sender: subject" title and mutating
  tags/scheduling/attachments. Thread isIgnoreShortSyntax through add()
  (matching the existing plugin-bridge pattern) and set it here.
- guard against oversized .eml files (postal-mime parses synchronously on
  the main thread and the body syncs to every device) with a 10MB cap +
  EML_TOO_LARGE snack.
- trim sender/subject so whitespace-only headers don't bypass the empty
  check or block the address fallback.
- drop the stale "add attachment" TODO.

* fix(tasks): log bounded eml parse reason, not the raw error

The source is untrusted email content and the log history is exportable
(rule #9). Log only e.message (postal-mime's throw messages are structural,
so no email content leaks) instead of the raw error object + stack.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-07-09 17:58:35 +02:00
Johannes Millan
df88d43f22
refactor(i18n): unify Due/Deadline/Planned/Scheduled labels (#8819) (#8878)
Resolve the terminology collision where "Due" labeled both the deadline
and the planned-day concept. Standardize UI state labels on "Deadline"
(deadlineDay/deadlineWithTime) and "Planned" (dueDay/dueWithTime), and
move the deadline-passed label off "Overdue!" to "Past deadline!" so it
no longer clashes with the planned-past Overdue list.

en.json values only; keys unchanged (t.const.ts regenerates identically).
The "Schedule" action verb/keybinding and the issue-provider "Due Date"
field are intentionally left. The behavioral question of what "Overdue"
should mean is split out to #8877.
2026-07-09 17:12:59 +02:00
Johannes Millan
e11b5ec5a2
feat(tasks): create a task from a link dropped on the app (#8879)
* feat(tasks): create "Check <url>" task from links dropped on empty app chrome

Dropping a web link anywhere on the app except onto a task, note, or panel
now creates a "Check <url>" task with a success snack, mirroring the Android
share-a-link flow. Task/note/panel drop handlers already stopPropagation, so
the document-level handler only sees links dropped on empty chrome.

- add pure getDroppedUrl() helper (http(s) only; rejects files, plain text,
  non-web schemes, and whitespace/multiline selections)
- wire the existing global onDrop in AppComponent to create the task
- add APP.DROP_LINK translations

* fix(tasks): robustly extract dropped link URL; stop notes double-fire

The dropped-link → task feature could no-op for real link drags: getDroppedUrl
only read text/plain and rejected anything with inner whitespace, but browsers
often deliver the URL only in text/uri-list, or as "<url>\n<title>" in
text/plain (and Electron cross-app drops fill only one). Now scan both types
line by line and take the first exact http(s) URL.

Also fix a duplicate-task bug found in review: dropping a non-image link on the
Notes panel created BOTH a note and a "Check" task, because NoteService only
calls stopPropagation after an await (too late). Claim the event synchronously
in the notes drop handler.

* feat(tasks): give dropped-link tasks a readable title + clickable attachment

Mirror the Android share flow more closely: the dropped-link task now gets a
human-readable title ("Check example.com: some article") plus the raw URL as a
clickable LINK attachment, instead of a raw URL crammed into the title.

Extract the shared readableUrl() helper out of android.effects into
util/readable-url.ts (now used by both the Android share effect and the desktop
drop handler) with its own spec.
2026-07-09 17:11:06 +02:00
Johannes Millan
36aba1c5c5 fix(ci): relax yamllint line-length for SHA-pinned wiki-sync workflow
SHA-pinning the actions in wiki-sync.yml (#8868) pushed the
setup-python `uses:` line to 84 chars, tripping yamllint's default
80-char line-length rule and failing the Lint job on every wiki push
and every PR touching wiki paths.

Relax the yamllint step to accept the 40-char SHA pins and their
single-space `# vX.Y.Z` version comments. Tightening the comments to
two spaces instead would push the already-80-char pin lines over the
limit, so raising the ceiling is the correct fix.
2026-07-09 17:03:51 +02:00
Lane Sawyer
0f397cd9b0
fix(nav): prevent icon jump on sidebar close (#8866) 2026-07-09 16:21:56 +02:00
dependabot[bot]
84bedacd22
chore(deps-dev): bump ws (#8876)
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/sync-md directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 8.18.3 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.21.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 15:57:08 +02:00
John Costa
24c799ee13
fix(work-context): make isTodayListActive reactive to context changes (#8863)
task.component's `isTodayListActive` computed read the plain mutable
`WorkContextService.isTodayList` boolean, which is not a signal producer,
so the computed never invalidated and returned its first value for the
component's lifetime.

Expose `isTodayListSignal` (a toSignal mirror of isTodayList$) on the
service and read that from the computed. The plain boolean stays for the
synchronous reader in history.component.

#8843
2026-07-09 15:35:59 +02:00
aakhter
8a1105bbb2
perf(tasks): content-stable scheduling snapshot for collection task selectors (#8858)
* perf(tasks): content-stable scheduling snapshot for collection selectors

The collection selectors (overdue, due-time-sorted, deadline-sorted,
due-day, later-today, all-with-subtasks, and today-task-ids) all derive
from selectAllTasks, which returns a new array every second while a task
tracks time. None of them read timeSpent, yet each re-ran its O(n)
filter/sort and reallocated every tick.

Introduce selectTaskSchedulingSnapshot: a per-entity ref-cached, array-
ref-stable projection of the scheduling fields only (id, isDone,
dueDay/dueWithTime, deadlineDay/deadlineWithTime, parentId, subTaskIds),
so a timeSpent-only tick yields the identical snapshot ref. Each
collection selector now derives its filter/sort DECISION (an ordered id
list) from the snapshot — skipped by NgRx memoization when the snapshot
is stable — and a per-selector memoized re-map turns the stable id list
back into live task entities, keeping the existing output types and
returning the previous array ref unless a genuine member changed. No
consumer/API changes; task refs stay live (no staleness).

selectTimeConflictTaskIds is intentionally left on the live sorted
selector because getTimeLeftForTask reads timeSpent.

SPAP-20

* test(tasks): clear leaked selectAllTasksWithDueTimeSorted override between tests

dialog-schedule-task.* and time-block-sync.effects specs call
store.overrideSelector(selectAllTasksWithDueTimeSorted, ...). overrideSelector
uses setResult(), which sticks on the singleton selector across spec files and
is only reset by clearResult() (not release()). When one of those specs ran
before task.selectors.spec in the full karma bundle, the new
selectAllTasksWithDueTimeSorted(mockState) test got the leaked value instead of
computing from mockState — "Expected [ ] to contain 'task5'". Single-file runs
passed, so it only surfaced in CI's full run.

Add the selector to the existing beforeEach clearResult()/release() defensive
block (mirrors selectOverdueTasks etc.). Repro: dialog-schedule-task +
task.selectors together went 1 FAILED -> 90 SUCCESS.

SPAP-20
2026-07-09 15:32:22 +02:00
Lane Sawyer
87f15c8c65
chore(config): adopt AGENTS.md as shared AI-agent config with skills (#8864)
Make AGENTS.md the single source of guidance read natively by Claude
Code, OpenAI Codex, and GitHub Copilot. CLAUDE.md becomes a symlink to
it. Extract commit-message guidance into a reusable Agent Skill under
.agents/skills (read by Codex and Copilot); .claude/skills symlinks to
it for Claude Code, and .gitignore is narrowed to /.claude/* so only
that skills pointer is tracked. Drop the now-redundant
.github/copilot-instructions.md symlink since Copilot reads AGENTS.md.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 15:30:56 +02:00
Lane Sawyer
c2ab4fe973
ci(actions): SHA-pin remaining workflow actions (#8868)
* ci(actions): SHA-pin remaining workflow actions

Replace the mutable derekprior/add-autoresponse@master ref in
issue-open-auto-reply.yml with an inline actions/github-script snippet
to remove the third-party code-injection risk in CI. SHA-pin the 4
remaining tag-pinned actions in wiki-sync.yml (checkout, setup-python)
to match the rest of the workflows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* wrong sha for the github-script v9

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 15:28:59 +02:00
Johannes Millan
7f604f68f7
feat(task-repeat): default skip-overdue for new everyday recurring tasks (#8861)
* feat(task-repeat): default skip-overdue for new everyday recurring tasks

New recurring configs now default skipOverdue ("Don't let overdue instances
pile up") to ON only for a plain everyday schedule — the "Daily" preset or a
CUSTOM every-single-day cycle. That is the one case where the option is both
useful and provably safe: everyday tasks are the only schedule that actually
piles up (one empty overdue copy per missed day), and today is always
scheduled so a missed instance regenerates the same day and can never
silently vanish (it cannot even drop to zero).

Every other schedule stays OFF — workday/weekly/monthly/yearly and every-N-day
custom cycles keep their one missed occurrence visible, so a real obligation
(pay rent on the 1st, renew the domain) never disappears until its next
occurrence. Deriving the default purely from the effective schedule means the
"Daily" preset and a CUSTOM every-day cycle (the same schedule entered two
ways) get the same default — no "same schedule, different default" surprise.

The default is seeded from the chosen schedule in both config-creation paths:
the repeat dialog (re-derived from the final schedule on save, and skipped
when the user explicitly toggled the Advanced checkbox) and the inline
add-task-bar recurrence. Existing configs and DEFAULT_TASK_REPEAT_CFG
(skipOverdue: false) are unchanged; only newly created configs are affected.

Supersedes the broader daily+Mon-Fri variant, dropping the schedule-change
checkbox re-sync machinery: an Advanced checkbox opened on a Daily config and
then switched may briefly show a stale ON, but save always persists the safe
re-derived value.

* docs(task-repeat): clarify custom every-day default; signpost baseline

Multi-review follow-ups (no behavior change):
- Wiki 2.06 + 4.13: a CUSTOM every-single-day cycle also defaults skipOverdue
  ON (it is the same schedule as the Daily preset); "off" now reads "custom
  cycles longer than a day" so the docs match getDefaultSkipOverdue.
- Comment on DEFAULT_TASK_REPEAT_CFG.skipOverdue pointing to the schedule-aware
  creation default, so the model's `false` baseline is not mistaken for the
  effective default.
- Tighten the save() display-gap comment: the cosmetic seeded-ON/persist-OFF
  gap applies to any new non-Daily config whose Advanced panel is opened
  untouched, not only a Daily→switch.

* refactor(task-repeat): use type-only import in skip-overdue predicate

TaskRepeatCfgCopy is used only in type position; match the codebase's import type convention (58 other files). No behavior change.
2026-07-08 19:55:37 +02:00
John Costa
5edb659a65
fix(archive): write compressed archives in a single atomic transaction (#8862)
compressArchive() persisted archiveYoung and archiveOld via two
independent writes (Promise.all). A crash between them left a
half-compressed archive, and because compression is op-replayed on
other clients, a torn local result diverged from replicas.

Route both writes through the existing saveArchivesAtomic API (one
IndexedDB transaction over both stores), matching the flush-young-to-old
path in archive-operation-handler.service.ts. Adds a regression spec.

#8843
2026-07-08 19:54:30 +02:00
Johannes Millan
6236dc7458
fix(tasks): add sub-task submit button and commit-on-blur on touch (#8860)
Android/mobile soft keyboards hide the Enter key or deliver it as a composing
keydown that onKeydown filters out (to protect CJK candidate confirmation), so
the inline sub-task draft could not be committed at all: neither Enter, tapping
away, nor any on-screen control saved the text.

- Add an always-visible submit button beside the input as an accessible,
  discoverable commit target (mouse-clickable, screen-reader labelled). Its
  mousedown is preventDefaulted so a desktop click keeps input focus, commits,
  and keeps the field open for rapid entry — mirroring the main add-task bar.
  On touch the tap commits via the blur path below.
- Commit the draft on blur, but only on touch (isTouchActive): the natural
  mobile "done" gesture is tapping away, and the soft-keyboard Enter is
  unreliable there (#8791). Desktop keeps click-away-to-cancel — Enter and the
  button are the reliable commit paths — so blur no longer silently creates a
  task, and a user can move to the button without the draft being committed out
  from under them. Escape still discards on all devices.

Both commit paths read the live input value, so IME/predictive-text buffering
no longer strands the text.

Closes #8856. Supersedes the standalone #8791 commit-on-blur branch.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:16:42 +02:00
Johannes Millan
420292e37c feat(notes): allow outlook: + note/task-app deep-links in URL-scheme allowlist (#8859)
Restore outlook: (reported in #8859) plus the same-class note/task apps
notion:, things:, omnifocus:, bear:, joplin: that the GHSA-hr87 fix also
silently broke. All low-risk registered-app deep-links, same class as the
already-allowed obsidian:/logseq:/zotero:.
2026-07-08 17:12:09 +02:00