Commit graph

824 commits

Author SHA1 Message Date
Johannes Millan
431bc50da9
fix(sync): help Nextcloud users find their user ID + auto-detect it (#7617) (#8547)
* docs(sync): point Nextcloud user-ID lookup at the WebDAV URL (#7617)

The field hint, the 404 test-connection message, and the wiki all told
users to find their user ID under 'Settings -> Personal info' / 'your
Files URL'. Both are unreliable: Personal info does not clearly surface
the uid, and a folder's address bar shows a folder ID, not the user ID
(reporter followed it and got a folder ID). Redirect all three to the
authoritative source: Files -> settings gear (bottom-left) -> the WebDAV
URL '.../remote.php/dav/files/<user-id>/'.

* feat(sync): auto-detect Nextcloud user ID via OCS (#7617)

The Nextcloud WebDAV files path needs the account's internal user ID,
which differs from the email/login name people enter and is awkward to
find by hand — the root cause of the recurring '404 / connection test
failed' reports. Add a 'Detect user ID' button to the Nextcloud sync
config that asks the server for it.

- packages/sync-providers: discoverNextcloudUserId() calls the OCS
  endpoint /ocs/v2.php/cloud/user (OCS-APIRequest header) authenticated
  with login + app password and returns ocs.data.id. A 401 reports bad
  credentials (cleanly distinct from the 404 wrong-user-id path); a 200
  that isn't an OCS payload reports 'not a Nextcloud/OCS server'. A
  missing/wrong URL scheme is refused up front (matches the provider's
  own _cfgOrError check) so credentials never hit a schemeless host.
- Dialog: button fills the Username field with the detected ID. To keep
  auth working, if 'Login name' was empty and the user had typed their
  login into 'Username', that login is preserved into 'Login name'
  before Username is overwritten with the ID. Result handling split into
  _applyDetectedUserIdResult for unit-testability.
- Additive and optional: generic WebDAV and the save/sync path are
  untouched, no synced data shapes change.
- Tests: 7 package specs (success, trailing-slash, login fallback, 401,
  non-OCS 200, missing id, scheme guard) + 6 dialog specs (login guard,
  fill+confirm, 3 login-preservation cases, failure).
2026-06-23 11:56:15 +02:00
John Costa
0a0723521b
feat(azure-devops): add optional WIQL override for auto backlog import (#8516)
* feat(azure-devops): add optional WIQL override for auto backlog import

Adds an optional autoImportWiql config field to the Azure DevOps issue
provider plugin. When set, it fully replaces the generated auto-import
backlog query, so the user controls scope, state filtering and ordering
(e.g. to filter by iteration path, area path or work item type, or to
match custom done-state names). When empty, behavior is unchanged from
today's scope-based query, so it is fully backward compatible.

This mirrors the Jira provider's autoAddBacklogJqlQuery (a full query the
user owns) rather than appending a fragment, which can only narrow the
hard-coded English done-state exclusion and cannot accept a real exported
WIQL Select..From..Where statement.

Adds a vitest suite covering the default scopes, quote escaping, the
verbatim override and the blank-fallback, wires the plugin into the
plugin-tests CI matrix, and updates the issue integration comparison wiki.

Closes #7674

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DY1ESWymmU9x9ykLNaRHGH

* test(azure-devops): use node test env, drop unneeded jsdom dependency

The plugin tests exercise no DOM API, so vitest's default node
environment is sufficient (matching the clickup and google-calendar
provider plugins). Removing jsdom prunes ~530 lines of transitive
devDependencies from the lockfile.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-22 17:38:49 +02:00
Mohamed Abdeltawab
32aec63023
Cleanup/remove dead super sync surface (#8526)
* cleanup: remove unused latestSnapshotSeq from HTTP response and client types

latestSnapshotSeq was computed on every download but never read by any
client code. sibling snapshotVectorClock IS consumed — only this field
is dead. The server still computes it internally for the snapshot
optimization logic, but it no longer serializes it in the response.

Removed from:
- DownloadOpsResponse type and response object (sync.routes.ts)
- Zod schema (supersync-http-contract.ts) + associated test
- OpDownloadResponseBase interface (sync-providers)
- Client validator test + stale comment references
- Server integration test assertions

* cleanup: remove deprecated CONFLICT_STALE error code

Server never emits CONFLICT_STALE (fully migrated to
CONFLICT_SUPERSEDED).

* cleanup: remove unused SyncConfig fields (maxOpsPerUpload, downloadLimit, downloadRateLimit)

These three config fields were declared and defaulted but never read by
any code. Routes hardcode their own limits:

- maxOpsPerUpload: gate is MAX_OPS_PER_BATCH constant
(sync.routes.payload.ts)
- downloadLimit: hardcoded Math.min(limit, 1000) in sync.routes.ts
- downloadRateLimit: hardcoded max:200 in route rateLimit config

* cleanup: remove dead/drifted duplicate types from sync.types.ts

Removed 6 types with zero references repo-wide:

- SnapshotResponse: orphaned after previous PR removed its importers
- UploadSnapshotRequest: zero uses, missing 7 fields vs actual Zod
schema
- RestorePointType: zero uses, included dead 'DAILY_BOUNDARY' value
- RestorePoint: zero uses (live version in snapshot.service.ts)
- RestorePointsResponse: zero uses
- RestoreSnapshotResponse: zero uses
2026-06-22 14:34:18 +02:00
Symon Baikov
d0f71d18b9
docs(sync): clarify upload rate-limit layers (#8497) 2026-06-22 12:19:56 +02:00
Johannes Millan
9faf0a50d5
fix(sync): clarify Nextcloud connection-test 404 (wrong user ID) (#7617) (#8513)
* fix(plainspace): don't tint claim-list link icon with accent color

* fix(sync): clarify Nextcloud connection-test 404 (wrong user ID) (#7617)

A base-root 404 in the WebDAV connection test means auth succeeded but the
DAV path /remote.php/dav/files/<userName>/ does not exist — i.e. the
"Username" holds an email/display name instead of the account's user ID
(Nextcloud accepts email/login for auth but the files path needs the uid).

Previously this surfaced as a bare scrubbed hostname in the snack, which
users misread as a stripped URL. Now:

- WebdavApi.testConnection maps thrown errors to a readable, privacy-safe
  message plus an HTTP errorCode discriminator (404/401/status).
- The Nextcloud "Test Connection" path shows a specific hint on 404
  explaining that "Username" must be the user ID, not email/display name.
- Wiki + field guidance clarified accordingly.
2026-06-20 15:18:53 +02:00
Johannes Millan
933b274afe
feat(plugins): migrate Azure DevOps issue provider to a plugin (#8509)
* fix(plainspace): don't tint claim-list link icon with accent color

* feat(plugins): migrate Azure DevOps issue provider to a plugin

Move the built-in Azure DevOps provider out of core into a bundled plugin
(packages/plugin-dev/azure-devops-issue-provider), mirroring the prior
GitHub/ClickUp/Gitea/Linear/Trello migrations.

- Read-only provider: WIQL search + backlog import (scope all / created-by-me
  / assigned-to-me), two-step work-item fetch, getById with description.
- PAT auth via HTTP Basic (empty username); allowPrivateNetwork enabled so
  self-hosted Azure DevOps Server hosts keep working.
- getIssueLink builds the work-item URL from host + project + id without a
  request; isDone maps (pullOnly) from Closed/Done/Removed/Resolved states.
- issue-provider.reducer migrates legacy AZURE_DEVOPS providers to plugin
  shape on load (project listed first so the tooltip/initials keep showing
  the project); auto-enable already covers migrated keys generically.
- AZURE_DEVOPS moves from BuiltInIssueProviderKey to MigratedIssueProviderKey;
  the synced issueProviderKey union member is preserved for forward-compat.
- i18n: ported the translated F.AZURE_DEVOPS.FORM.* config labels (de/en/uk
  localized, English fallback elsewhere) and shared issue-content display
  labels to all 28 app locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(plugins): tidy Azure DevOps plugin issue field mapping

Post-review cleanup of the field mappers (no behavior change for the
common case, and closer to the Linear/Gitea reference plugins):

- mapReduced no longer emits a redundant `state` field; the typed `status`
  already drives the search-list done indicator (is-issue-done), and dropping
  `state` keeps isDone unset on add-from-search — matching the built-in, which
  never set isDone in getAddTaskData.
- mapIssue keeps `state` as the single canonical status field (used by
  issueDisplay, the isDone fieldMapping and extractSyncValues) and no longer
  duplicates it as `status`; the display row now points at `state`.
- Compute the work-item summary once per mapper instead of twice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(plugins): remove dead Azure DevOps form i18n keys after plugin migration

The built-in Azure DevOps config form was deleted in the plugin migration,
so the F.AZURE_DEVOPS.FORM.* keys in t.const.ts and the app locale files
(en/de/uk) are now unreferenced. The plugin ships its own bundled i18n.
Removing them matches the GitHub/Trello/Linear/ClickUp migration cleanup.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:06:45 +02:00
Mohamed Abdeltawab
0b4bc79354
refactor: retire GET /api/sync/snapshot, re-scope GET /api/sync/status as diagnostic (#8496)
Retire GET /api/sync/snapshot (attack-surface reduction):
- Remove route handler from sync.routes.ts (no production client caller)
- Keep internal generateSnapshot() and all downstream code
- Update tests: delete GET /snapshot describe block, rework isolation
  test (sync.routes.spec.ts), remove GET assertion (sync-fixes.spec.ts),
  remove SimulatedClient.getSnapshot() helper + rework 2 integration tests
  to use GET /api/sync/ops instead (multi-client-sync.integration.spec.ts)

Re-scope GET /api/sync/status as diagnostic:
- Add doc comment to route handler marking it diagnostic
- Update all documentation (README, API wiki, architecture diagrams)

Documentation updates across 5 files remove GET /snapshot references
and label GET /status as diagnostic.

Breaking: self-hosters with external tooling relying on GET /snapshot
must migrate — no shipped client version ever called this endpoint.
2026-06-20 12:02:58 +02:00
Symon Baikov
c0b8f30214
refactor(sync): remove dead SyncService facade methods (#8498)
* refactor(sync): remove dead SyncService facade methods

* test(sync): update encrypted snapshot cache assertion
2026-06-20 12:00:10 +02:00
Mohamed Abdeltawab
1423409a13
refactor: remove unused isCleanSlate from POST /ops contract and handler (#8491)
isCleanSlate was accepted and forwarded by the /ops endpoint, but the
production client never sends it — clean slate flows exclusively via
POST /snapshot. Removing it from the schema, the handler, and the
UploadOpsRequest interface for defense-in-depth. Zod's default strip
mode silently ignores it if an old/rogue client sends the field.

The snapshot schema (SuperSyncUploadSnapshotRequestSchema) and the
internal uploadOps(..., isCleanSlate?) parameter used by the snapshot
handler are unchanged.
2026-06-19 14:23:37 +02:00
dependabot[bot]
29936d03c8
chore(deps): bump nodemailer (#8478)
Bumps the npm_and_yarn group with 1 update in the / directory: [nodemailer](https://github.com/nodemailer/nodemailer).


Updates `nodemailer` from 8.0.9 to 9.0.1
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.9...v9.0.1)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 9.0.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 10:51:54 +02:00
dependabot[bot]
b89878e8bc
chore(deps): bump the npm_and_yarn group across 2 directories with 2 updates (#8477)
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/caldav-calendar-provider directory: [undici](https://github.com/nodejs/undici).
Bumps the npm_and_yarn group with 1 update in the /packages/super-sync-server directory: [nodemailer](https://github.com/nodemailer/nodemailer).


Updates `undici` from 7.27.0 to 7.28.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.27.0...v7.28.0)

Updates `nodemailer` from 8.0.11 to 9.0.0
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.28.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nodemailer
  dependency-version: 9.0.0
  dependency-type: direct:production
  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-06-18 20:22:52 +02:00
Symon Baikov
d0da0aea93
Codex/issue 8081 keep subtask input open (#8423)
* fix(tasks): keep subtask input open after add

* fix(tasks): keep subtask input open after add

* perf(tasks): avoid task row effect reactivity

* fix(tasks): address subtask input review

* feat(tasks): ease in the inline subtask input instead of popping

* fix(tasks): consume inline subtask-input request to prevent stale focus-steal

Address multi-review findings on the inline subtask input:

- The open request was held in a signal that was never cleared, so a task row re-created with the same id (e.g. navigating away from a project and back) re-ran its open effect on init and re-opened the input, stealing focus with no user action. Reset via consume() once the row acts on it.

- Drop the redundant requestId counter (a fresh request value already re-fires the effect); request payload is now just the parentId string.

- Add an aria-label to the input (placeholder is not an accessible name).

- _commit() returns void (its boolean result was unused).

* feat(tasks): animate the inline subtask input out and tighten its top gap

- Use the expandFade animation (enter + leave) so the input collapses out instead of vanishing. Caveat: when the input is the only thing in .sub-tasks (adding the first subtask, then cancelling without creating any), the enclosing @if collapses in the same tick and Angular skips the child leave animation, so it's instant in that one case.

- Reduce the input's top margin from --s-half (4px) to --s-quarter (2px).

* feat(tasks): refocus the task row when the subtask draft is cancelled with Escape

The inline subtask input's 'closed' output now reports why it closed ('escape' vs 'blur'). On Escape (a keyboard cancel) the host task calls focusSelf() so keyboard navigation continues from that row; on blur it doesn't, since focus already moved elsewhere. focusSelf() is a no-op on touch.

Covered by unit tests (reason emitted, host refocus only on escape) and an e2e assertion that the task row is focused after Escape.

* feat(tasks): return focus to the task the subtask draft was opened from

Escape previously refocused the parent row that hosts the input. Capture the originating task (which may be a subtask) when opening the draft — before focus moves into the input and the parent row claims focusedTaskId — and restore that on Escape. Falls back to the host row when no origin was captured; no-op on touch.

Focus-by-id prefers the last #t-<id> instance (the detail side-panel one) to match the existing inline-edit focus convention when a task renders twice.

Covered by unit tests and an e2e proving focus returns to the originating subtask, not its parent.

* test(tasks): update subtask e2e for the inline draft input flow

The add-subtask UX changed from 'press a -> empty subtask title focused for edit' to an inline draft input (type + Enter to commit, stays open for rapid entry). Update every e2e site that added subtasks via the old textarea selector:

- WorkViewPage.addSubTask helper: wait for .e2e-add-subtask-input, fill + Enter, then Escape to close the draft for a clean post-state (fixes simple-subtask, add-to-today, drag-task-into-subtask, finish-day-quick-history, and the sync specs that use the helper).

- add-subtask-with-detail-panel-open.spec.ts: assert the draft input opens focused from panel/main-list focus; rewrite the multi-subtask case to repeated Enter (the new rapid-entry path).

- supersync-archive-subtasks + supersync-lww-conflict: same inline-draft flow (not runnable in-sandbox; fixed by analogy).

Verified locally: detail-panel-open 3/3, simple-subtask, add-to-today 5/5, drag-into-subtask, finish-day-quick-history all green.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-18 17:58:45 +02:00
Johannes Millan
be50775c9c feat(plugins): migrate Trello issue provider to a plugin
Move the built-in Trello provider out of core into a bundled plugin
(packages/plugin-dev/trello-issue-provider), mirroring the prior
GitHub/ClickUp/Gitea/Linear migrations.

- Board selection is now a dynamic `select` config field backed by
  `loadOptions` (member + organization boards, merged) instead of the
  bespoke `trello_additional_cfg` board-picker component, which is
  deleted along with its `@case('TRELLO')` in the edit dialog.
- Credentials are sent via Trello's `Authorization: OAuth` header rather
  than `key`/`token` URL query params, so they never appear in request
  URLs or error logs (keeps the redaction from #8459 intact).
- Card attachments are surfaced as markdown links in the issue display;
  the plugin model has no attachment hook, so the previous inline
  preview / attachment-count is dropped.
- `getIssueLink` derives `https://trello.com/c/<shortLink>` directly;
  isDone maps from a card being archived (closed).
- issue-provider.reducer migrates legacy TRELLO providers to plugin
  shape on load (boardName listed first so the tooltip keeps showing
  the board name); auto-enable already covers migrated keys generically.
- i18n: the built-in config form was hard-coded English, but the
  issue-content display labels used shared translated keys. Ship all 28
  app locales — DISPLAY.* and the token-help link sourced from the
  shared F.ISSUE.ISSUE_CONTENT.* / HOW_TO_GET_A_TOKEN translations, with
  the never-translated config-field labels kept in English per locale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:37:39 +02:00
Johannes Millan
2cf67cee71 18.11.0 2026-06-18 13:37:47 +02:00
Mohamed Abdeltawab
c2bf7b26a0
refactor(sync-core): drop shared-schema vector-clock compat re-export anda app enum (#8441)
* feat(sync-core): drop shared-schema vector-clock compat re-export and app enum

- Remove the shared-schema → sync-core compatibility re-export of
vector-clock types/functions; retarget app files
(vector-clock.ts,operation-log.const.ts) and the server (sync.types.ts)
to import from @sp/sync-core directly
- Add @sp/sync-core to super-sync-server/package.json deps (it was
load-bearing through the re-export)
- Convert VectorClockComparison from a bare type to an as const object +
derived type in sync-core,drop the app-side enum copy and the as cast
- Update spec imports and pa ckage-boundaries.md

* docs: remove stale shared-schema vector-clock references

- Update comments in vector-clocks.md, client vector-clock.ts, and
  server sync.types.ts to reference @sp/sync-core directly
- Remove @sp/sync-core dependency from shared-schema/package.json
- Regenerate package-lock.json to reflect the removed edge

* docs(sync): fix orphaned VectorClockComparison comment

The comment block describing VectorClockComparison was left dangling
above no declaration after the app-side enum was removed, and still
claimed 'Uses enum for client-side ergonomics'. Move it above the
re-export it documents and correct the wording.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-17 16:21:26 +02:00
Johannes Millan
c1ce4f5c7b test(sync): pin #8334 divergent-scalar conflict, fix PGlite SQL drift
The entity-ids-conflict PGlite spec hand-copied the conflict-detection SQL
but had drifted to the old mutually-exclusive
`CASE WHEN cardinality(entity_ids) > 0 THEN entity_ids ELSE ARRAY[entity_id]`
form, while production conflict.ts now uses the
`entity_ids || ...ARRAY[entity_id]` UNION (the silent-data-loss fix in
560757b88f). The drifted spec passed with either form, so it gave false
assurance, and the only test that actually discriminated the fix lived in a
DATABASE_URL-gated, vitest-excluded integration spec that runs in no CI job.

Sync both unnest sites to the production union SQL and add a divergent-scalar
regression to detectConflictForEntities and prefetchLatestEntityOpsForBatch:
an op whose scalar entity_id (task-Z) is NOT a member of its own entity_ids
must still be found. Verified the new cases fail on the old CASE SQL and pass
on the union (12/12 green).
2026-06-17 16:07:53 +02:00
dependabot[bot]
bf664bb1d5
chore(deps): bump esbuild in the npm_and_yarn group across 0 directory (#8451)
Updates `esbuild` from 0.25.12 to 0.27.3
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/v0.27.3/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.12...v0.27.3)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.27.3
  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-06-17 14:41:32 +02:00
dependabot[bot]
59c4b75d7b
chore(deps): bump the npm_and_yarn group across 1 directory with 6 updates (#8438)
Bumps the npm_and_yarn group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `8.0.7` | `8.0.9` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `6.4.2` | `6.4.3` |
| [form-data](https://github.com/form-data/form-data) | `4.0.5` | `4.0.6` |
| [hono](https://github.com/honojs/hono) | `4.12.22` | `4.12.25` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.1.1` | `4.2.0` |
| [tar](https://github.com/isaacs/node-tar) | `7.5.15` | `7.5.16` |



Updates `nodemailer` from 8.0.7 to 8.0.9
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.7...v8.0.9)

Updates `vite` from 6.4.2 to 6.4.3
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.3/packages/vite)

Updates `form-data` from 4.0.5 to 4.0.6
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6)

Updates `hono` from 4.12.22 to 4.12.25
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.22...v4.12.25)

Updates `js-yaml` from 4.1.1 to 4.2.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/commits)

Updates `tar` from 7.5.15 to 7.5.16
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.15...v7.5.16)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 8.0.9
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 6.4.3
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: form-data
  dependency-version: 4.0.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: hono
  dependency-version: 4.12.25
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 4.2.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tar
  dependency-version: 7.5.16
  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-06-17 13:16:00 +02:00
Mohamed Abdeltawab
863c7347a3
refactor: remove SyncConfigPort dead port and ConflictUiPort.notify is dropped (#8408)
* refactor: remove SyncConfigPort dead port and ConflictUiPort.notify is dropped

* refactor: remove orphaned doc comment and type
2026-06-16 11:07:34 +02:00
dependabot[bot]
2d1b775995
chore(deps): bump markdown-it (#8425)
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/doc-mode directory: [markdown-it](https://github.com/markdown-it/markdown-it).


Updates `markdown-it` from 14.1.1 to 14.2.0
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.1...14.2.0)

---
updated-dependencies:
- dependency-name: markdown-it
  dependency-version: 14.2.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-06-16 10:43:09 +02:00
Johannes Millan
904d523e8a
fix(sync): Caddy WS-token log redaction (#8343) + batched failed-op retry (#8305) (#8404)
* fix(sync): redact WebSocket auth token from Caddy access logs (#8343)

The WS endpoint authenticates via ?token=<JWT> (browsers can't set
headers on a WebSocket), and that JWT is valid for 365 days with no
rotation. The shipped Caddyfile enables access logging, so every WS
upgrade wrote a year-valid, full-sync credential to stdout / docker
logs. Use a filter log encoder to redact the token query param from the
logged URI while keeping the request line for observability.

* fix(sync): retry failed remote ops as one seq-ordered batch (#8305)

retryFailedRemoteOps applied each failed op in its own single-op
applyOperations call. That turned every retry into a single-op batch, so
the same-batch archive pre-scan (collectArchivingOrDeletingEntityIdsFrom
Batch) no-op'd and silently weakened the #7330 orphan-resurrection
guard. Retry all failed ops as one batch instead, ordered by seq, and
mirror the primary remote-apply path's applied/slice-from-failure result
handling (keeping the MAX_CONFLICT_RETRY_ATTEMPTS reject cap).

* fixup! fix(sync): redact WebSocket auth token from Caddy access logs (#8343)

* refactor(sync): share slice-from-failure op-id logic across apply paths

Extract getFailedOpIdsFromBatch() — the 'failed op plus every op after it'
slice that retryFailedRemoteOps, ConflictResolutionService, and the
sync-core remote-apply path each derived inline. Removes the app-side
duplication flagged in review and fixes a latent bug: conflict-resolution
sliced from the findIndex result with no -1 guard, so an unfound failed op
would slice(-1) and mark only the last op failed. The shared helper guards
that case (marks just the failed op). Behavior is otherwise unchanged.

* test(sync): integration-test failed-op retry against real IndexedDB (#8305)

The retryFailedRemoteOps unit spec mocks the store, so it can't verify the
real status transitions the retry relies on. Add an integration spec that
drives retryFailedRemoteOps through the real OperationLogStoreService with
only the applier controlled, asserting: full-batch success clears ops to
applied, the batch is applied in one call, partial failure re-marks the
failed op plus every op after it, and a permanently-failing op is rejected
at MAX_CONFLICT_RETRY_ATTEMPTS across reboots and then no longer returned.
2026-06-15 17:40:20 +02:00
Meh-S-Eze
3db96fd8a3
fix(plugins): expose focused task API to iframe plugins (#8291)
Co-authored-by: Shem Freeze <whatamehs@Shems-MacBook-Air.local>
2026-06-15 14:57:34 +02:00
Mohamed Abdeltawab
a4c0dc7d46
refactor: remove dead SyncStateCorruptedError, compat exports, and 0 byte sync-providers barrel #8328 (#8396)
* refactor: remove dead SyncStateCorruptedError, compat exports, and 0-byte sync-providers barrel #8328

* docs(sync): drop stale SyncStateCorruptedError/fail-fast references (#8328)

The fail-fast dependency-resolution subsystem (DependencyResolverService +
SyncStateCorruptedError throw) was removed earlier; OperationApplierService now
bulk-dispatches ops in causal arrival order and returns a failedOp for the caller
to re-validate/retry. Update the sync architecture docs, archive-operations
diagram, and user-data wiki to match. Completes the dead-code cleanup for #8396.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-15 14:16:14 +02:00
Johannes Millan
560757b88f fix(sync): cover scalar entity_id in multi-entity conflict lookup (#8334)
The two raw-SQL latest-writer lookups unnested a mutually-exclusive
`CASE WHEN cardinality(entity_ids) > 0 THEN entity_ids ELSE ARRAY[entity_id]`,
so a stored op whose scalar entity_id is NOT a member of its own entity_ids
(producible via getStoredEntityIds dedup) was invisible to the batch lookup -
a later concurrent op touching that entity was wrongly accepted (silent data
loss). This only bites once batch upload is enabled in prod, but is a real
latent divergence between the single-entity (Prisma OR) and batch (raw SQL)
paths.

Fold the scalar into the set with a union:
  entity_ids || CASE WHEN entity_id IS NULL THEN '{}'::text[] ELSE ARRAY[entity_id] END
(entity_id is nullable; the guard prevents array||NULL nulling the whole
array). DISTINCT ON dedupes the common entity_id = entityIds[0] overlap. The
WHERE prefilter is unchanged, so GIN(entity_ids) + entity_id btree usage is
preserved. Update the conflict-detection.spec mock to union too, and add a
DATABASE_URL-gated PG integration test exercising the real detectConflict /
prefetchLatestEntityOpsForBatch (divergent-scalar case fails under the old
CASE); excluded from the default vitest run like the sibling SQL integration test.
2026-06-13 16:57:01 +02:00
Johannes Millan
490d990e3e
fix(sync): replay all entityIds on multi-entity DELETE (#8340) (#8384)
* test(sync): cover multi-entity conflict SQL against real Postgres (#8334)

Add an in-process Postgres (PGlite) spec that runs the actual unnest(CASE...)
conflict-detection SQL from conflict.ts, covering the non-first-entity case,
latest-per-entity selection, scalar fallback for pre-migration rows, and the
NULL-entity_id full-state op. Unlike the DATABASE_URL-gated integration specs
(excluded from the default config), this runs in the normal npm test job.

* fix(sync): delete all entityIds on multi-entity DEL replay (#8340)

Server op-replay deleted only the scalar entityId, so a batch delete
(deleteTasks stores its full set in entityIds, scalar = entityIds[0]) left
entities 2..n alive in restore snapshots, which feed back to clients. Thread
entityIds (now persisted, #8334) through replay and delete the full set, with
the same prototype-pollution guard and scalar fallback for single-entity and
pre-migration ops. Add entityIds to REPLAY_OPERATION_SELECT.
2026-06-13 16:08:30 +02:00
Johannes Millan
d30fd5f434
fix(sync): conflict-check all entities of multi-entity ops (#8334) (#8377)
* fix(sync): conflict-check all entities of multi-entity ops #8334

Multi-entity ops (deleteTasks, moveToArchive, __updateMultipleTaskSimple,
round-time-spent, batch board/issue-provider actions) carry entityIds[], but
the server operations table only persisted the scalar entity_id (= entityIds[0]).
Once such an op was stored, only its first entity took part in future conflict
lookups, so a later stale write to a non-first entity found no prior writer and
was wrongly accepted instead of rejected as CONFLICT_SUPERSEDED/CONCURRENT.

- Add an entity_ids text[] column (populated for multi-entity ops only via
  getStoredEntityIds; single-entity ops store [] and use the scalar) + a GIN
  index. Migration is metadata-only with no backfill: pre-migration rows fall
  back to entity_id, so the fix is forward-only (entities 2..n of already-stored
  ops were never persisted and stay unrecoverable).
- detectConflictForEntity now runs two ordered LIMIT-1 lookups (scalar btree +
  entity_ids GIN) and takes the higher server_seq, preserving the fast ordered
  hot path instead of an OR's BitmapOr+sort.
- detectConflictForEntities / prefetchLatestEntityOpsForBatch match an entity as
  the scalar entity_id OR a member of entity_ids (unnest CASE + && / = ANY).
- Harden validateOp to bound entityIds (length + per-element), mirroring entityId.

Raw SQL validated against Postgres (PGlite) incl. GIN usage and two-query
correctness; single path + validation + migrations covered by unit tests. The
full conflict-detection.spec needs a generated Prisma client (CI), and the
hot-path round-trip tradeoff should be confirmed with a real-PG EXPLAIN.

* fix(sync): store entity_ids when a batch op dedups off the scalar #8334

Multi-review follow-up. getStoredEntityIds gated on `length > 1`, so a batch op
whose entityIds dedup to a single value that differs from entityId (the server
does not enforce entity_id === entityIds[0]) stored []  and that entity became
invisible to conflict lookups — reintroducing #8334 for it. Gate on "is the set
exactly [entity_id]?" instead, and cover it with unit tests.

Also: correct the array-branch comment/doc — a GIN(entity_ids) lookup has no
server_seq so it match-all-then-sorts (cheap only because multi-entity ops are
rare), it is not an ordered walk; drop a stale "OR filter" test docstring; add a
counter-note on getConflictEntityIds vs getStoredEntityIds to prevent swapping.

* refactor(sync): single OR lookup for entity conflict detection #8334

Third multi-review follow-up. Revert detectConflictForEntity from the two ordered
findFirst lookups back to a single Prisma OR [{entityId}, {entityIds:{has}}]. The
two-query split optimized the OR's BitmapOr+sort, but that is bounded by op-log
pruning (sub-ms in practice) while the split added a guaranteed extra round-trip on
the common single-entity path (doubled by the FIX-1.5 re-check) — a net-negative for
the median. The OR is simpler, fully typed/testable, and likely faster in aggregate;
a code comment documents the split as the escalation if a real-PG EXPLAIN ever shows
the OR is a problem.

Review polish (no behaviour change): correct the array-branch/getStoredEntityIds
comments (a GIN @> is match-all-then-sort, not an ordered walk; multi-entity-only
storage's win is GIN size + keeping single-entity inserts off the GIN, not sort
depth); note the batch unnest paths as the first EXPLAIN candidates under load; keep
the two batch queries' CASE/prefilter SQL inline (a shared fragment would shift the
positional params the conflict-detection.spec mock relies on) with a keep-in-sync
note; replace a non-ASCII <= in a client-facing validation error string.

* test(sync): update db mocks for OR + prefetch entity_ids lookups #8334

Running the full super-sync-server suite (with a generated Prisma client) surfaced
two specs whose inline db mocks hadn't tracked the new conflict-detection SQL:

- time-tracking-operations.spec: findFirst now models the single-entity lookup's
  OR: [{entityId}, {entityIds:{has}}] shape (it previously only matched the scalar
  entityId, so a concurrent single-entity update was wrongly "accepted").
- sync.service.spec: the prefetch $queryRaw mock parsed userId as the last param and
  flattened all params into the touched pairs; the #8334 prefilter adds idArray
  params after userId, so it now finds userId by type and reads the touched pairs
  from the VALUES join fragment only.

Production code unchanged; these are test-mock fidelity fixes. Full suite: 800 passing.
2026-06-13 15:40:10 +02:00
dependabot[bot]
76d6419fa8
chore(deps): bump the npm_and_yarn group across 9 directories with 1 update (#8366)
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/ai-productivity-prompts directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/caldav-calendar-provider directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/clickup-issue-provider directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/doc-mode directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/gitea-issue-provider directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/github-issue-provider directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/google-calendar-provider directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/linear-issue-provider directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /packages/plugin-dev/sync-md directory: [esbuild](https://github.com/evanw/esbuild).


Removes `esbuild`

Updates `esbuild` from 0.25.12 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

Updates `esbuild` from 0.25.12 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

Updates `esbuild` from 0.25.12 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

Updates `esbuild` from 0.25.12 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

Updates `esbuild` from 0.25.12 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

Updates `esbuild` from 0.25.12 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

Updates `esbuild` from 0.25.12 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

Updates `esbuild` from 0.25.11 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/commits)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version:
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  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-06-13 14:37:36 +02:00
Johannes Millan
e30cf89745
fix(sync): measure op payload size limit in UTF-8 bytes, not UTF-16 units (#8368)
validateOp enforced the per-payload size limit with JSON.stringify().length
(UTF-16 code units) while the quota gate, the persisted payloadBytes column,
and the storage counter all measure UTF-8 bytes (Buffer.byteLength). Non-ASCII
payloads undercount in the check, so a payload could pass validation yet be
charged more everywhere else.

Switch the check to Buffer.byteLength(...,'utf8') and reuse the measured size
so a large payload is stringified once for sizing instead of repeatedly per
upload:
- computeOpStorageBytes gains an optional cachedPayloadBytes arg.
- validateOp returns payloadBytes; both upload paths reuse it at the persist
  site (the vector clock is still measured there since it is pruned after
  validation, so the stored clock differs from the validation/gate-time clock).
- processOperation returns { result, storageBytes, fallback } so the serial
  loop reuses the size instead of recomputing it, mirroring the batch path's
  acceptedDeltaBytes.
Net payload stringifies per accepted op: serial 4->2, batch 3->2 (the
pre-validation quota gate remains the one unavoidable measure).

Behavior change: heavily non-ASCII full-state imports above 20MB UTF-8
(previously accepted under the looser UTF-16 count) are now correctly rejected
with PAYLOAD_TOO_LARGE, matching the documented byte limit.
2026-06-13 14:29:00 +02:00
Johannes Millan
e1d6174f1d
fix(sync): don't cache transaction-failure results in request dedup (#8370)
A rolled-back upload (INTERNAL_ERROR) was cached under the deterministic
requestId, so retries of the same batch were served the cached failure for
the 5-min dedup TTL — defeating retries. Skip caching INTERNAL_ERROR results
in both the ops and snapshot handlers. #8332
2026-06-13 12:50:32 +02:00
Johannes Millan
5e63eeb294
fix(plugins): stop leaked timers on plugin disable via onUnload hook (#8286)
* feat(sync): show actionable error on persistent WebDAV 409

When a WebDAV PUT keeps returning 409 Conflict even after the parent
collection is created, the Base URL / Sync Folder Path is misconfigured
(the classic Synology / raw-WebDAV setup mistake). Previously the raw
"HTTP 409 Conflict" (or a bare "Unknown error") reached the user with no
hint at the cause.

Throw a dedicated WebDavSyncFolderUnusableSPError with a privacy-safe,
actionable message (no path, no response body) at the spot that already
detects this condition. Mirrors NetworkUnavailableSPError: a fixed
user-facing message matched by instanceof.

* ci(release): revive contributors section in release notes

* fix(plugins): stop leaked timers on plugin disable via onUnload hook

* fix(plugins): harden onUnload teardown hook after review

* refactor(plugins): group lifecycle registers into options object

* fix(plugins): close onReady stale-guard gap and timer interleave race
2026-06-12 17:09:28 +02:00
Johannes Millan
22e5884371
fix(caldav): discover calendars via principal/calendar-home-set #8259 (#8278)
* fix(caldav): discover calendars via principal/calendar-home-set #8259

The CalDAV Events plugin populated its calendar dropdowns with a single
Depth:1 PROPFIND on the exact URL the user entered, keeping only
responses whose resourcetype is <calendar>. Users paste the advertised
CalDAV root (Nextcloud /remote.php/dav, Fastmail /dav/), where calendars
live one or two collection levels deeper, so the request succeeds but
lists no calendars. The dialog then showed a green 'Options loaded' tick
with empty dropdowns (loadOptions resolved with an empty array, no
error), and the Schedule showed no events because no calendar could be
selected.

Rewrite discoverCalendars to do RFC 4791 service discovery: try the
entered URL directly (back-compat for a pasted calendar-home), else
PROPFIND current-user-principal, then calendar-home-set, then enumerate
the home collection.

Discovery now follows server-controlled hrefs, so harden resolveHref to
resolve via the URL constructor and refuse any off-origin target (the
SSRF boundary, since credentials attach to every request), and drop
untrusted server data from thrown error messages. De-duplicate the XML
parse-error guard into a shared parseXmlDoc.

Add discovery test coverage (previously none), including cross-origin
href refusal.

* test(caldav): assert request origin instead of URL substring

CodeQL flagged the cross-origin discovery test's url.includes('evil...')
check (js/incomplete-url-substring-sanitization). Parse each requested
URL and assert its origin equals the entered server origin instead — a
stronger, alert-free assertion that no credentialed request escaped
off-origin.
2026-06-12 13:01:32 +02:00
Miklos
67d1e32ffc
feat(focus-mode): focus screen UX overhaul (#7586)
* feat(focus-mode): focus screen UX overhaul

Major rework of the focus mode timer screens (#7349):

- Shared <focus-clock-face> drives Pomodoro / Flowtime / Countdown /
  Break with a single visual chrome; size tokens scale fluidly via
  clamp() + vmin/vh, no discrete breakpoints.
- Single source of truth: --clock-time-size and --control-offset
  derive from --clock-face-size.
- Countdown: click-to-edit duration, draggable handle on the ring,
  hybrid 5-min/15-min snap with 6h-per-rotation above 1h
  ([useFlexibleIncrement] on input-duration-slider, opt-in for other
  consumers).
- Pomodoro prep inherits the click-to-type input; drag handle hidden
  so dragging the ring can't silently shift the value.
- Pause keeps the selected task on screen (displayedTask falls back
  to the paused task while currentTask is null).
- Notes panel acts as a modal: clock stays put, backdrop dims and
  closes on outside-click.
- Session controls row below the circle (pause / complete / reset
  cycles); buttons fade via shared --revealed-opacity gated on host
  hover + document.hasFocus().
- Break screen mirrors focus-mode-main layout; cycle counter inside
  the circle on both focus and break; back-to-planning unified.
- Flowtime settings dialog: all fields render with proper
  disable/enable, stable dialog width when switching modes.
- arrow_backward -> arrow_back: fixes glitched glyph in repeat-type
  context menus (boards, simple counters, take-a-break, flowtime).

* fix(focus-mode): silence naming-convention lint on formly 'props.disabled'

Formly's expressionProperties path-string keys ('props.disabled') aren't
camelCase and the rule has no requiresQuotes exemption; matches the
existing pattern in src/app/features/issue/common-issue-form-stuff.const.ts.

* test(focus-mode): mock pomodoroConfig signal on FocusModeService

The component's initialization effect now reads
focusModeService.pomodoroConfig() in Pomodoro+Preparation mode, but the
three mocked FocusModeService instances in the spec didn't provide it,
causing TypeError in 47 tests on CI (somehow not surfaced locally).

* test(focus-mode): align specs with unified back-to-planning flow

- focus-mode-break.spec: exitBreakToPlanning -> cancelFocusSession
- focus-mode-session-done.spec: drop obsolete hideFocusOverlay assertion
  (cancelFocusSession now handles both clearing tracking and hiding the
  overlay, matching the production component)
- focus-mode-main.spec: storeSpy gains selectSignal returning a signal,
  needed by the displayedTask paused-task fallback

* fix(focus-mode): restore E2E selector hooks on refactored controls

The shared <focus-clock-face> refactor moved pause/complete buttons out
of the clock face into a new .circle-controls row but didn't carry the
class names forward; 23 E2E specs key off them. Also re-add
.task-title-placeholder on the no-task FAB so prep-state checks find it.

- .pause-resume-btn on pause/resume in focus-mode-main + focus-mode-break
- .complete-session-btn on the done_all button in focus-mode-main
- .task-title-placeholder added to .select-task-cta FAB

* fix(focus-mode): aria-label icon buttons; align break E2Es with new flow

- focus-mode-break: pause/resume/skip/reset icon buttons now carry
  [attr.aria-label] in addition to matTooltip — icon ligature alone
  isn't an accessible name and breaks getByRole locators.
- pomodoro-break-timing-bug-6044.spec: skipButton uses getByRole with
  accessible name rather than hasText (mat-icon ligature is "skip_next",
  not "skip break").
- focus-mode-break.spec: "exit break to planning and change timer mode"
  and "Back to Planning should NOT auto-start next session" now match
  the unified back-to-planning flow — overlay closes on click, user
  re-opens focus mode to change settings or verify prep state.

* fix(focus-mode): address PR #7586 review feedback

Maintainer review (johannesjo):
- Debounce the Pomodoro work-duration write so editing it emits one
  synced config op instead of one per keystroke; flush on session start
  so a value typed inside the debounce window is not lost. (A1)
- Replace the local ::ng-deep restyling of the shared input-duration-slider
  with opt-in [bareRing] and [hideHandle] inputs that own the chrome
  overrides in the slider's own styles; the four other consumers keep the
  default look. (A2)
- Add unit tests for the flexible drag math (_setValueFromRotationFlex):
  the A<->B boundary anchoring at 55/60 min and both +/-180 degree wrap
  branches. (A3)
- Delete the orphaned exitBreakToPlanning action, its
  stopTrackingOnExitBreakToPlanning$ effect, reducer case and specs;
  cancelFocusSession already unsets the current task. (B1)
- Drop the unreferenced CONTINUE_TO_NEXT_SESSION and BREAK_RELAX_MSG
  i18n keys. (B2)
- Collapse the duplicated clock-size clamp() into a single
  --clock-face-size-default token. (B4)

Smaller focus-screen fixes (beerkumquatpome):
- Hide the break task title when "pause tracking during breaks" is on. (C7)
- Commit and close the duration editor on Enter. (C9)
- Rename "Back to Planning" to "Exit focus session". (C11)
- Show Flowtime breaks as a neutral "Break". (C13)

Break-circle vertical alignment (C1) is only partially addressed here
(matched the top reservation); exact alignment is a follow-up.

* refactor(focus-mode): share a layout shell across timer screens and auto-start Flowtime breaks

Extract a presentational focus-mode-layout component (4-row content-projection
skeleton: [fmTop]/[fmTask]/[fmClock]/[fmBottom]) shared by the focus-session and
break screens, so both keep a stable clock baseline across the focus<->break and
prep<->in-progress transitions. focus-mode-main and focus-mode-break now consume
the shell instead of each maintaining their own absolute layout.

Replace the Flowtime "break offer" step with an auto-started break, mirroring
Pomodoro:
- Remove the BreakOffer UI state and the offerFlowtimeBreak action/reducer.
- endFlowtimeSession now dispatches completeFocusSession(isManual:false) +
  startBreak (unsetting the task first when tracking-pause-on-break is on), so
  the break starts automatically and the session is logged exactly once via
  logFocusSession$.

Also reorder the bottom controls (Back to Planning leftmost), restore the
BACK_TO_PLANNING label to "Back to Planning", and drop the now-orphaned
FLOWTIME_BREAK_TITLE / START_BREAK i18n keys.

* test(layout): restore document.activeElement after focus-restoration specs

The LayoutService "Focus restoration" tests override document.activeElement
with Object.defineProperty, which shadows the native (inherited) getter with an
own property on document. The afterEach only removed the mock DOM node, so the
override leaked into later specs: once it ran, document.activeElement was frozen
at the mock element and subsequent .focus() calls could no longer move it.

Depending on Karma's spec order this broke the task.service focusTaskById tests
(#7120), which then saw the stale activeElement instead of the element they
focused. Delete the shadowing own property in afterEach to restore native
behavior.

Repro: ng test --include layout.service.spec.ts --include task.service.spec.ts

* refactor(focus-mode): add interactive tracking widget and polish timer-screen layout

- Replace the read-only task-tracking-info with focus-mode-task-tracking
  (vertical time stack + play/pause), wired through the shared layout shell
- Center the task title and floor the task-row height so the clock baseline
  stays aligned across focus <-> break
- Spacing polish: task-title-row and layout gaps to --s2,
  segmented-button-group padding to 0
- Drop redundant safe-area-bottom padding on the action row (the overlay
  already reserves it for the fixed shell)
- Add "Take a moment to relax" break message and a clock-digit edit affordance

* refactor(focus-mode): share timer/break layout, drop dead tracking toggle

- Extract a shared <focus-mode-layout> skeleton and <focus-mode-task-row> used by both the focus session and the break.

- Remove the in-view tracking play/pause toggle (start/stop stays on the global header button); strip focus-mode-task-tracking to read-only and drop RESUME_TRACKING.

- Pin the mode selector out of flow and center the task·clock·bottom group; equal reserved task/bottom rows keep the clock vertically centered, with the selector kept on top.

- Tighten sizing: horizontal selector segments, settings cog matched to the in-session controls, and a fluid clock clamp.
2026-06-12 11:59:56 +02:00
Johannes Millan
ecd5ec7077 Merge master and address PR review findings
Harden LocalFile saves, keep legacy file image IPC removed, avoid pre-save cached-image deletion, and add user-facing reselect warnings for LocalFile/background images.
2026-06-10 19:17:54 +02:00
Johannes Millan
86bb66b3b4 fix(electron): surface sync-folder pick persist failures to renderer 2026-06-10 17:02:48 +02:00
johannesjo
2b628a0a50 refactor(electron): consistency + UX nits from pre-merge review
Pre-merge review of #8228 flagged four real items in the otherwise
solid security boundary. All non-blocking; rolled into one pass:

1) IMAGE_PICK_AND_IMPORT was the only renderer-callable handler that
   threw raw Errors back across IPC. The FS handlers all funnel
   through createSafeIpcError, which strips e.stack so main-bundle
   paths can't leak via the renderer's error-handling. Switch
   IMAGE_PICK_AND_IMPORT to the same shape: returns Error on
   validation failure, null on user cancel, value on success.
   Renderer adapter uses `instanceof Error` (parity with fileSyncLoad
   et al). New test asserts the returned error has no path content
   and no stack.

2) The image picker button was not disabled while the IPC was in
   flight. A second click would queue a second IPC, opening a second
   dialog after the first resolves; because `replacesId` is computed
   from the form value at click time, the first import's id would
   never be GC'd. Add an `isPickerBusy` signal and bind the button's
   `disabled` to it. New spec asserts the second concurrent click is a
   no-op and the busy flag clears in the `finally`.

3) Stale comment in `local-file.model.ts` pointed at
   `electron/sync-folder-store.ts`, which doesn't exist (the storage
   was inlined into local-file-sync.ts in Phase 2.5). Update to
   describe the actual current shape and clarify that the field is
   read once on upgrade for the migration breadcrumb and never written
   from new code paths.

4) Image input placeholder advertised `file://` URLs as a valid
   input. Phase 4 removed the IPC that resolved them, so the
   placeholder was a footgun. Drop the `file://` half.

68/68 electron security tests; 8/8 formly-image-input karma; lint +
tsc clean.
2026-06-10 00:17:48 +02:00
johannesjo
38d2b16a82 refactor(electron): close IMAGE_CACHE_IMPORT trigger gap + review nits
End-to-end review of the #8228 sweep flagged one real residual risk
and several smaller items. This commit addresses them in one pass.

The IMAGE_CACHE_IMPORT IPC accepted a renderer-supplied absolute path
with no link to a recent SHOW_OPEN_DIALOG signal. A compromised
renderer could call importImage() with any image-extension path under
5 MB outside userData — same information-disclosure shape the legacy
READ_LOCAL_IMAGE_AS_DATA_URL had, just one-shot instead of repeatable.

Fix: merge the dialog and the import into a single atomic IPC,
IMAGE_PICK_AND_IMPORT. Main opens the picker inline and only calls
importImage() with the file the user actually clicked. The renderer
no longer holds a path at any point in the flow; an attacker who
controls the renderer cannot trigger an image read without a real
user dialog interaction.

Shape change:
- Return null on user-cancel; throw on validation failure. This
  preserves the existing UX where cancel is silent and an unreadable
  image shows a snack. Renderer-side openFileExplorer is a try/catch.
- Optional { replacesId } arg garbage-collects the previously
  cached image when the user picks a new one. Closes the disk-fills
  follow-up the review raised (#3) without an extra IPC round-trip.

Smaller items from the review folded in:
- Extract _resolveRelative helper in local-file-sync.ts; the five
  FS handlers had the same three-line incantation copy-pasted. The
  Phase 2.5 simplification over-corrected by inlining everything.
- Drop the dead __resetSyncFolderCacheForTests export; no caller, and
  the tests reset via require.cache surgery anyway.
- image-cache.ts getExt now uses path.extname so Windows backslash
  paths are handled correctly (the old slash-only split worked by
  accident).
- resolveBgImageToDataUrl: short-circuit on empty id, and log a
  console.warn when a legacy file:// is detected (no resolvable IPC
  remains; user must re-pick). i18n snack is a documented follow-up.
- Package LocalFileSyncElectron.isReady now logs a critical line via
  the injected sync logger when isReady=false but the renderer's
  privateCfg still has the legacy syncFolderPath — dev console
  surfaces the "needs re-pick" state without dragging i18n into the
  security PR.
- Stale comment in electron-file-adapter.ts referenced a deleted file
  (sync-folder-store.ts is inlined into local-file-sync.ts now).

Tests:
- electron/local-file-sync.test.cjs gains four IPC-level cases:
  legacy IMAGE_CACHE_IMPORT not registered, pick+import round-trip,
  cancel returns null, validation failure throws, replacesId GCs the
  prior cached file.
- formly-image-input.component.spec rewritten for the new flow:
  asserts imagePickAndImport is called with replacesId when one is in
  state, snack on rejection but not on null, no setValue on either.

68/68 electron security tests pass; 7/7 formly-image-input karma;
5/5 resolve-bg-image karma; 17/17 sync-providers package
local-file specs. Lint + tsc clean.

What this commit does NOT do (called out so it isn't forgotten):
- CLIPBOARD_COPY_IMAGE_FILE has the same renderer-supplied-path shape
  for clipboard images. Parallel issue; out of scope for #8228.
- PICK_DIRECTORY has no defaultPath — could nudge users toward a
  dedicated sync subfolder vs picking ~/Documents. Cosmetic.
- Symlink TOCTOU between resolveSyncPath and writeFileSync stands;
  O_NOFOLLOW-on-open is the proper fix and needs cross-platform care.
- Migration UX is console-only; a user-visible snack needs i18n
  infrastructure not appropriate for the security commit.
2026-06-09 23:57:45 +02:00
johannesjo
0c21649fde feat(electron): own sync folder main-side and take only relative paths
Phase 2 of #8228. Closes the renderer-supplied-absolute-path hole: the
file-sync IPCs now take a relative path; main resolves it against a
sync folder it owns and never trusts the renderer's copy of.

Main side
- sync-folder-store: persists the path in simple-store (still inside
  userData, still renderer-unreachable via assertPathOutside) with an
  in-memory cache so per-IPC lookup doesn't stat the file every call.
  initSyncFolderStore() is fire-and-forget at adapter init; each handler
  awaits it defensively because it's idempotent via _loadOnce.
- FILE_SYNC_SAVE/LOAD/REMOVE: accept { relativePath, ... }, pass through
  resolveSyncPath, write/read/delete the vetted absolute path.
- CHECK_DIR_EXISTS and FILE_SYNC_LIST_FILES: accept an optional
  relativePath; default to the sync root itself so the existing
  "is the sync folder reachable?" check still works.
- PICK_DIRECTORY: now persists the chosen folder via setSyncFolderPath
  before returning the display string to the renderer. The renderer
  receiving the string is no longer load-bearing — it's display only.
- New GET_SYNC_FOLDER_PATH IPC returns the current value for display
  (settings form, "sync folder: …" labels).
- TO_FILE_URL: pulls in the assertPathOutside(userData) backstop that
  was missing on master, so a userData path cannot be laundered into a
  file:// URL that later flows through READ_LOCAL_IMAGE_AS_DATA_URL or
  the navigation guard.

Package (@sp/sync-providers/local-file)
- LocalFileSyncElectron.isReady now asks main (getMainSyncFolderPath
  dep) instead of reading the renderer's privateCfg. This drives the
  migration UX: an existing install with a legacy privateCfg value but
  no main-side path lands on "not configured" and gets re-prompted by
  the picker. A one-cycle "please pick again" is the migration cost; we
  decided against silently promoting the renderer's persisted value
  because it could be tampered with.
- getFilePath returns the *relative* path (no absolute folder prefix).
  The leading-slash strip is preserved so existing call sites that
  prepend '/' still work.
- pickDirectory no longer writes to renderer privateCfg — main owns
  persistence. The dep contract still returns the display string.
- The privateCfg.syncFolderPath field is marked @deprecated; the
  field is kept for migration of older configs and may be removed in a
  later pass once the migration has rolled out.

Renderer
- ElectronFileAdapter forwards the relative path verbatim to the IPC.
- The renderer's LocalFileSyncElectron deps wire pickDirectory and the
  new getMainSyncFolderPath to the bridge.

Tests
- electron/local-file-sync.test.cjs rewritten for the new signatures:
  happy path, traversal escapes, absolute-relativePath rejection,
  sync-folder-equals-userData rejection (grant-file forgery defense),
  PICK persists main-side, GET returns the configured path, TO_FILE_URL
  refuses userData.
- electron/sync-folder-store.test.cjs covers persistence,
  init-before-get throw, null/empty handling, idempotent set.
- The package's local-file-sync-electron.spec.ts is updated for the new
  deps shape and asserts the package no longer writes to the renderer
  credential store.

All 56 electron-side tests pass; 374 sync-providers tests pass.
2026-06-09 22:40:50 +02:00
Johannes Millan
7f029b1798
fix(plugins): harden nodeExecution grants (#8205)
* fix(plugins): harden node execution grants

* fix(plugins): harden iframe bridge boundaries (#8208)

* fix(plugins): harden iframe bridge boundaries

* fix(plugins): tighten iframe bridge follow-up

* test(plugins): make node-executor electron test hermetic

The new plugin-node-executor test read the real built-in plugin manifest
(src/assets/bundled-plugins/sync-md/manifest.json), which is a build
artifact absent when 'npm run test:electron' runs in CI (before the
frontend/plugin build). Stub the manifest read, scoped to the executor
module via Module._load, so the grant/token/webContents assertions no
longer depend on built plugin assets.

* fix(plugins): allow iframe formatDate & getCurrentLanguage i18n methods

The iframe API allow-list gate added in #8208 only listed a subset of
the i18n methods that master's #8146 exposes to iframe plugins. Without
this, plugin calls to formatDate/getCurrentLanguage (and translate) are
rejected with 'Unknown API method'. Add all three so the merged gate
matches the methods createBoundMethods/createPluginApiScript expose.

* docs(plugins): document node-exec handoff bootstrap-ordering invariant

The one-shot consumePluginNodeExecutionApi() handoff is defended by
construction ordering, not structural isolation: PluginBridgeService must
consume it before any plugin 'new Function' code runs (both share window.ea
in one renderer realm). Document the invariant at the consumption site so a
future lazy-service/early-plugin-load refactor can't silently regress it.
Surfaced by the post-merge security review (latent finding; not currently
exploitable).

* fix(plugins): use static iframe sandbox attribute to avoid NG0910

Binding a security-sensitive iframe attribute (sandbox) via [attr.sandbox]
makes Angular throw RuntimeError NG0910 and tear down the iframe, crashing
the plugin view to the global error screen. This broke the plugin-iframe,
plugin-loading and plugin-lifecycle e2e tests.

Restore the static sandbox attribute (still without allow-same-origin, so
the opaque-origin isolation from #8208 is preserved) and drop the now-unused
iframeSandbox binding/import.
2026-06-09 18:16:41 +02:00
Johannes Millan
4c869c2e15
feat(plugins): migrate Gitea and Linear issue providers to plugins (#8204)
* feat(plugins): migrate Gitea and Linear issue providers to plugins

* feat(plugins): localize Gitea issue provider config form

Port the existing Gitea translations (F.GITEA.* + shared issue-content keys)
into the plugin's own i18n so non-English users keep localized config labels
after the plugin migration. Generated for all 28 app locales.

* docs(plugins): note Linear teamId/projectId filter is now active

The built-in Linear provider defined teamId/projectId config fields but never
passed them to the search, so they were inert. The plugin honors them as the
field labels promise; document this as a deliberate behavior change.

* test(issue): fix unknown casts in issue-provider migration spec

The spec compiled clean under the app tsconfig (which excludes *.spec.ts)
but failed CI's tsconfig.spec.json build: casting state.entities[id]
(IssueProvider | undefined) straight to Record<string, unknown> is a
TS2352 non-overlap error, and .toBe(already) on a bare literal is a
TS2345. Add the intermediate 'as unknown' so the spec type-checks.

* ci(lighthouse): raise total resource-count budget to 260

Migrating the Gitea and Linear issue providers to bundled plugins adds
two more entries to BUNDLED_PLUGIN_PATHS. Each bundled plugin is
discovered at startup, fetching its manifest.json plus icon.svg, which
pushed the Lighthouse total request count to 251, one over the previous
250 budget. The increase is an inherent, accepted consequence of the
built-in -> bundled-plugin migration (same as github/clickup); bump the
budget to 260 to reflect the new baseline with modest headroom.
2026-06-09 15:03:20 +02:00
felix bear
d844559c6f
fix(plugins): translate yesterday tasks plugin #5103 (#8146)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-09 14:55:05 +02:00
Johannes Millan
3d2c811e78 chore(task-repeat): revert RRULE Phase 1 from master (#7948) Develop the full RFC-5545 RRULE epic on the long-running feat/rrule-epic branch and merge once complete and testable in final form, rather than landing 13 phases into master one half-state at a time. Work preserved on feat/rrule-epic. Not yet shipped (post-v18.9.1), so no user impact. 2026-06-09 13:56:38 +02:00
dependabot[bot]
4ccba1f578
chore(deps): bump @types/supertest from 6.0.3 to 7.2.0 (#7430)
Bumps [@types/supertest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/supertest) from 6.0.3 to 7.2.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/supertest)

---
updated-dependencies:
- dependency-name: "@types/supertest"
  dependency-version: 7.2.0
  dependency-type: direct:development
  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>
2026-06-09 11:54:04 +02:00
Johannes Millan
7eabbc34b9 fix(sync): make super-sync-server migrations buildable from scratch
The Prisma migration chain began with an ALTER on the operations table, so
a fresh database could not be initialized from migrations alone (the first
migration failed with no table to ALTER). Separately, the login_token /
login_token_expires_at columns and index existed in schema.prisma and were
used at runtime by src/auth.ts (magic-link login) but were never created by
any migration, so a migrate-only database crashed with
"column users.login_token does not exist".

- Add a 0_init baseline migration that creates the base tables (the
  pre-2025-12-12 shape), sorting before the first incremental migration so
  the existing ALTER migrations apply cleanly on top.
- Add 20260601000000_add_login_token to create the missing magic-link
  columns and index (IF NOT EXISTS, safe on installs that already have them
  from a prior db push).
- Add migration_lock.toml (provider = postgresql), the standard companion to
  a real migration baseline.
- Document baselining for existing databases: `migrate resolve --applied
  0_init` for installs with migration history, and resolving the whole chain
  for db-push installs with none (covers the Helm/Docker unattended paths).
- Add regression tests asserting the baseline exists and that every @map
  column in schema.prisma stays backed by a migration (guards the drift class
  that caused this bug, not just login_token).

Verified by replaying all migrations on a fresh Postgres: the chain applies
cleanly and the resulting schema matches schema.prisma exactly.

Closes #8187
2026-06-09 11:36:44 +02:00
felix bear
a562b24b82
feat(github): configure issue provider api base url #5749 (#8183)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-09 11:28:05 +02:00
Filip
1718b0a8b8
feat(task-repeat): RFC 5545 RRULE recurring schedules — EPIC · Phase 1/13 (Closes #4020) (#7948)
* feat(task-repeat): add cron / natural-language recurring schedules

Add a CRON repeat cycle so a single recurring task can express schedules
the day/week/month/year cycles cannot (e.g. every Saturday, March through
November). The cron expression drives occurrence generation; all other
schedule fields are ignored for CRON cfgs.

- Occurrence engine: getNewestPossibleCronDueDate / getNextCronOccurrence in
  cron-occurrence.util.ts (cron-parser), wired into the existing
  getNewestPossibleDueDate / getNextRepeatOccurrence dispatchers. Recurrence
  stays day-granular: sub-daily crons create at most one task per day.
- English to cron via the crono-eng WASM module (src/assets/crono-eng.wasm,
  loaded lazily), with a builtin regex parser as fallback until/if the module
  is unavailable. naturalLanguageToCron() also passes raw cron through.
- Cron to english live preview under the input via cronstrue; the field shows
  the interpreted cron + humanized reading as the user types and warns when an
  expression is sub-daily.
- Invalid / unrecognized input shows an error snack and blocks save.
- Add-task bar: @+<cron-or-phrase> short syntax attaches a CRON repeat cfg.
- tools/build-crono-wasm.js regenerates the WASM asset from the sibling
  crono-eng Zig project (committed asset is the source of truth for builds).

* fix(task-repeat): cron edge cases, clearer warnings, and corpus tests

Hardening + UX pass on the cron repeat cycle, driven by verifying the full
crono-eng corpus (415 phrases) end to end.

Fixes
- Silent never-fires: crono-eng can translate phrases (a specific year, "nearest
  weekday"/W, "n-to-last day"/L-n, biweekly #-lists) into Quartz forms the
  recurrence engine (cron-parser) cannot run. These passed UI validation yet a
  task would never be created. naturalLanguageToCron now gates its output on
  cron-parser, so such input is rejected at the field instead.
- Off-by-one for midnight crons: cron-parser's next() is exclusive of an
  exact-boundary currentDate, so getNextCronOccurrence skipped the first
  eligible day for 00:00 schedules (e.g. first occurrence landed a day late).
  Seed the search one ms before the lower-bound day so a midnight fire stays
  eligible; now matches the non-cron daily convention.

UX
- Live preview shows the interpreted cron + plain-English reading below the
  field, and warns when the time of day is ignored (day-granular engine) or the
  schedule is sub-daily ("runs at most once per day").
- Friendlier validation/snack message with examples instead of terse jargon.

Tests
- tools/test-crono-wasm.js (npm run test:crono): corpus-driven harness asserting
  the committed WASM matches all 415 corpus crons, and that cron-parser +
  cronstrue accept them — classifying the 27 known engine-unsupported forms so
  it stays a regression guard.
- cron-occurrence.util.spec.ts: occurrence engine across daily/weekly/monthly/
  month-range/sub-daily, varied times, start-date and last-creation gating.
- parse-natural-cron.util.spec.ts: raw passthrough, phrase recognition, null
  cases, loader resilience.
- task-repeat-cfg-form.cron.spec.ts: field validator + live-preview/time-warning.

* fix(task-repeat): live cron preview + @+ title strip; add E2E

Fixes found by adding end-to-end coverage of the cron feature.

- Live preview did not render: Formly's mat-hint does not reflect a dynamic
  expressionProperties.description, so the interpreted-cron/English preview never
  updated as the user typed. Move it into the dialog template, driven by a
  computed signal off the form's valueChanges (getCronPreview in cron-preview.util),
  so it updates live and persists after applying. The time-of-day-ignored and
  sub-daily warnings now render as proper translated lines.
- "@+<phrase>" short syntax left the clause in the title: shortSyntax set
  taskChanges.title for the cron strip, but the later parseTimeSpentChanges
  reassignment wiped it, so a bare "Foo @+every day" submitted the raw title.
  Strip into the working title and emit the cleaned title at the end.
- shortSyntax returned cronExpression unconditionally (undefined when absent),
  breaking 38 exact-match assertions; only include the key when set.

Tests
- e2e/tests/recurring/cron-repeat.spec.ts: @+ short-syntax attaches a CRON cfg
  (title stripped); full dialog flow (phrase → live preview + time warning →
  save → CRON cfg persisted).
- short-syntax.spec: @+ extraction/stripping, with and without other syntax.
- getCronPreview unit tests (interpreted cron, English, timed/sub-daily flags).

* test(task-repeat): property/fuzz/metamorphic/edge cron tests; fix first-occurrence

Large second testing pass (≈+58 unit tests, +2 E2E, new property harness),
which surfaced one gap and one upstream quirk.

Fix
- getFirstRepeatOccurrence returned null for CRON cfgs (no case → default), so a
  timed CRON task's first instance and the startDate-moved-earlier path fell
  back to today instead of the cron's first fire. Add getFirstCronOccurrence
  (first fire on/after startDate) and route CRON through it.

Tests
- cron-occurrence.invariants.spec: property/invariant battery over many crons
  (no-throw, result strictly-after / on-or-before bounds, determinism,
  monotonicity), calendar edges (leap-day, year rollover, DST spring/fall),
  pathological "Feb 30" (null, no hang), and getFirstCronOccurrence /
  getFirstRepeatOccurrence routing.
- parse-natural-cron.metamorphic.spec: relational tests (case/whitespace/order/
  irrelevant-text invariance, idempotence, determinism, raw passthrough).
- short-syntax.spec: more @+ cases (raw cron after @+, bare @ is not cron,
  unrecognized phrase ignored, clause stops at the next delimiter).
- tools/test-crono-properties.js (npm run test:crono:props): WASM determinism,
  marshalling fuzz (empty/oversized/unicode/boundary), English→cron→English
  round-trip, and occurrence simulation across every runnable corpus cron.
- e2e: invalid phrase blocks save + shows inline error (Save disabled);
  raw cron expression preview + save. Shared dialog-open helper.

Note: documented a cron-parser DST quirk — prev() skips the spring-forward
midnight, so "newest due" can land a day earlier on that date (day-granular,
once a year; lastTaskCreationDay prevents duplicates).

* test(task-repeat): selector integration, serialization, and WASM buffer-reuse

- selectors.spec: CRON cases for the selectors that drive task creation —
  selectAllUnprocessedTaskRepeatCfgs (due today, overdue, already-created,
  paused, future start, invalid expr, deleted instance) and
  selectTaskRepeatCfgsForExactDay (exact-day match). Proves a CRON cfg is
  picked up by addAllDueToday's selector path.
- cron-occurrence.invariants.spec: a CRON cfg survives a JSON round-trip with
  identical occurrence behavior (sync / backup integrity).
- test-crono-properties.js: buffer-reuse checks — a short translation after a
  long one is uncontaminated, and results stay deterministic across a 600-call
  interleaved loop (the WASM module reuses static input/output buffers).

* test(task-repeat): cross-timezone day-resolution harness

The Karma suite only runs in the host timezone (Chrome on Windows ignores the
TZ env var — the reason the pre-existing *.tz.spec fails), so cron day-resolution
was never verified across zones. Add a Node harness (npm run test:crono:tz) that
spawns a child process per timezone — Node honors TZ — and asserts day-class
invariants in each: weekly-Monday resolves to a Monday, monthly day-15 to the
15th, daily to the next calendar day, time-of-day never shifts the day, and a
full-year daily iteration is monotonic with 366 distinct days (no DST skip in
next()). Covers fractional offsets (India +05:30, Chatham +12:45/+13:45) and
Southern-hemisphere DST (Sydney). Mirrors getNextCronOccurrence's core math.

* fix(task-repeat): DST-safe newest-due; verify real engine across timezones

Closes the time-based reliability gaps from the previous testing pass.

Fix
- getNewestPossibleCronDueDate no longer uses cron-parser's prev(), which skips
  the spring-forward midnight in DST zones (a daily cron then looked like it
  didn't fire that day). It now walks days backward and asks "does the cron fire
  during this local day?" via a next()-based probe (next() is monotonic across
  DST). Spring-forward and fall-back now resolve the exact day.

Tests
- main.ts exposes the real occurrence utils on __e2eTestHelpers.cron (dev/stage
  only, stripped from production).
- e2e/cron-timezone.spec: runs the REAL engine under five forced browser
  timezones (Playwright timezoneId — reliable regardless of host OS, unlike the
  Karma TZ env). Each asserts the timezone was actually applied (live UTC
  offset) AND that day-class results are correct, incl. fractional offsets
  (Chatham +12:45) and the spring-forward day.
- test-crono-tz.js: also asserts a daily cron fires on every calendar day of the
  year in all 8 zones (regression guard for the prev() skip).
- invariants.spec: spring-forward / fall-back now assert the exact day for both
  next() and newest (no longer hedged).
- effects.spec: a CRON cfg's first occurrence is computed through the real
  updateTaskAfterMakingItRepeatable$ effect path (live new Date()).

* test(task-repeat): dev jumpDay clock helper + live day-change e2e

Adds __e2eTestHelpers.jumpDay(n) / resetClock() (dev/stage only, stripped from
production) so recurring/day-change behavior can be fast-forwarded from the
DevTools console without touching the OS clock: jumpDay shifts Date.now() by a
cumulative day offset and forces the day-change re-sample so addAllDueToday()
runs. New e2e asserts jumpDay(1) creates the next daily instance — covering the
live day-change -> creation path end to end.

* feat(task-repeat): add "create a task for each missed occurrence" option

By default, opening the app after several scheduled occurrences were
missed creates only a single instance for the most recent occurrence.
When enabled (advanced section), a separate overdue task is created for
every missed occurrence instead, oldest first, capped at the 30 most
recent to avoid flooding the list / emitting a large sync batch.

- new getAllMissedDueDates() util reuses getNextRepeatOccurrence so the
  per-cycle and CRON occurrence logic stays single-sourced and DST-safe
- service multi-spawn path is gated to the catch-up flow (target day is
  today or earlier) and is mutually exclusive with skipOverdue /
  waitForCompletion; the single newest-occurrence path is unchanged
- form checkbox hides when "skip overdue" is set, and vice versa

Part of #4020

* fix(task-repeat): re-anchor lastTaskCreationDay when cron expression changes

cronExpression was missing from SCHEDULE_AFFECTING_FIELDS, so editing a
CRON schedule did not re-run rescheduleTaskOnRepeatCfgUpdate$. A
previously computed (often future) lastTaskCreationDay then stuck around
and silently suppressed every occurrence until real time caught up to it
— e.g. a "Jan-Aug" cron set while the clock was past August anchored to
Jan 1 next year and produced no tasks in between.

Adding 'cronExpression' makes a cron edit re-anchor from the current
date, consistent with repeatCycle / repeatEvery / weekday edits.

Part of #4020

* feat(tasks): surface @+ recurring short syntax in autocomplete and help

The `@+<schedule>` add-task short syntax existed but was undiscoverable.

- add recurring examples to the `@` autocomplete suggestions: typing `@+`
  now autocompletes phrases like `@+every monday` or
  `@+every saturday from march through november`
- document `@+` in the Settings -> Short Syntax help text

Part of #4020

* fix(tasks): remove @+ recurring autocomplete entries

The `@+` examples added under the `@` mention trigger kept the suggestion
dropdown open while typing `@+<phrase>`, so pressing Enter selected a
suggestion instead of submitting the task — breaking the headline
type-and-go flow. The `@+` syntax stays documented in Settings -> Short
Syntax; only the dropdown entries are removed.

Part of #4020

* refactor(task-repeat): show cron only inside Custom recurring config

Cron appeared twice in the recurring-config dropdown: as a top-level
"Cron / natural language" quick-setting AND as a cycle inside "Custom
recurring config". Drop the duplicate top-level option; cron is now
reached via Custom -> repeatCycle = Cron.

- remove the CRON quick-setting option from the dropdown builder
- keep the repeatCycle select visible when CRON is chosen (only hide
  repeatEvery) so the user can switch back
- map existing cron configs (incl. legacy quickSetting 'CRON') to CUSTOM
  on load so the dropdown matches the stored cycle
- update the cron E2E to drive the dialog via Custom -> Cron

Part of #4020

* test(task-repeat): make @+ short-syntax e2e date-independent

The test asserted the `@+every monday` task appears in the Today view, but
a weekly schedule's first instance is the next Monday — so on any non-Monday
the task is correctly scheduled for a future day and isn't in Today, making
the test fail depending on the day it runs. Verify the attached CRON config
and the stripped title via the store instead, which is view- and
date-independent.

Part of #4020

* feat(task-repeat): RFC 5545 RRULE recurring schedules [WIP]

Overhaul recurring tasks from the bespoke repeatCycle format to RFC 5545
RRULE as the canonical recurrence representation (legacy fields kept
populated for old-client forward-compat). Adds a structured RRULE builder,
RRULE-backed quick presets, per-instance due-date derivation, multiple end
conditions (COUNT / UNTIL / after-N-completions), an occurrence heatmap with
completion simulation, natural-language @+ short syntax, and a REST API for
recurring tasks. Migration is lazy (on dialog open) so existing configs keep
firing untouched until edited.

WIP: some required features and tests are still outstanding, and it needs
much more real-world (user-based) testing before it is merge-ready.

* feat(task-repeat): per-occurrence overrides, due-date control move, @+ preview, NL fix [WIP]

- RECURRENCE-ID: per-instance overrides (move / re-time / re-title) surfaced to
  the engine as RDATE + EXDATE so every projection stays consistent.
- Move the due-date derivation control out of the rrule builder into the dialog
  so it applies to all recurring configs (presets + Custom).
- Live @+ recurrence preview (humanized rule + next occurrence date) in the
  add-task-bar, so a far-off rule is obvious before submit.
- Fix @+ "every other <weekday> in <month>" mis-parsing to YEARLY;INTERVAL=2
  ("every 2 years"); weekly-style interval + weekdays now stays WEEKLY.

* refactor(task-repeat): trim PR to engine+builder+migration+preview; fix curator review

Trim the WIP recurring-schedules PR to its reviewable core — RFC 5545 occurrence
engine, structured RRULE builder, legacy<->RRULE migration (both directions),
live text preview, quick-setting presets, and tests — and defer the rest to
follow-up sub-MRs: natural-language @+, due-date derivation, ends-after-N-
completions, missed-occurrence backfill, heatmap preview + simulation, REST
recurring creation, and RECURRENCE-ID per-instance overrides. The full
implementation is preserved on branch feat/recurring-full.

Curator review fixes:
- quickSetting forward-compat: never persist a value outside the released
  (master) union. The newer preset literals AND 'RRULE' map to 'CUSTOM' at every
  persist boundary (dialog save, add-task-bar, data-repair downgrade); the rich
  value drives the dialog UI in-memory only and the builder reconstructs from the
  opaque rrule on open. Fixes old/mobile typia sync-validation treating such
  tasks as corrupt.
- Malformed rrule now falls back to the legacy schedule fields (guarded by
  isRRuleValid) instead of silently stopping the task.
- Reverse converter maps a BYDAY-less FREQ=WEEKLY onto the start weekday, so the
  legacy WEEKLY engine still fires on old clients.
- Stop logging the raw rule body (the raw-override field makes it free-text user
  input; log history is exportable).
- DRY: share normalizeWeekdays / toNumArray between the two converters.

* refactor(task-repeat): clamp quickSetting at the action creators

The op-log replays the action payload, not reduced state
(operation-capture.service.ts), so a reducer-level clamp would not keep an
out-of-union quickSetting off the wire. Clamp in the addTaskRepeatCfgToTask /
updateTaskRepeatCfg / updateTaskRepeatCfgs creators instead -- the single
boundary every dispatcher passes through, so old/mobile clients always replay a
value their typia union accepts. The newer preset literals and 'RRULE' stay
in-memory in the dialog form only.

Removes the now-redundant per-call-site clamps in the dialog save path and the
add-task-bar; the @+ and REST paths inherit the clamp for free when their phases
return. Adds an action-creator spec covering the clamp at each entry point.

* feat(task-repeat): nth-weekday rows support multiple weekdays per ordinal

Each ordinal row (1st/2nd/3rd/4th/last) now multi-selects weekdays via toggle
buttons, so one line can mean "the 1st Monday and the 1st Tuesday" -> BYDAY=1MO,1TU.
The per-row ordinal dropdown excludes positions already used by other rows (each
ordinal anchors at most one line), and the add button hides once all are used.
Parsing groups weekdays that share an ordinal back into one row. Applies to both
the monthly and yearly nth-weekday modes; updates the helper copy accordingly.

* test(task-repeat): complex RRULE coverage + day-by-day create-loop sim

Adds high-complexity coverage for the occurrence engine and builder:
- engine variants x settings: per-day ordinals, BYSETPOS last-weekday,
  BYMONTHDAY=-1, seasonal BYMONTH, BYWEEKNO/BYYEARDAY, leap years, mixed with
  COUNT / UNTIL / EXDATE / lastTaskCreationDay
- builder forward + mode-detection + lossless round-trips (incl. multi-weekday
  ordinals and sub-daily raw-override)
- cfg->engine routing for complex rules with deletedInstanceDates and the
  malformed-rrule legacy fallback
- a "day-march" simulator that drives getNewestPossibleDueDate one day at a time
  with lastTaskCreationDay fed back like the service, asserting the created
  stream has no dupes/skips, honors EXDATE/COUNT, is idempotent within a day,
  and documents Phase-1 app-closed catch-up (newest missed only)

Also trims the deferred @+ short-syntax e2e (returns with its phase) and fixes
the dialog-flow e2e to target the current .rrule-result preview selector.

* feat(task-repeat): custom ordinal input for nth-weekday rows and BYSETPOS

The nth-weekday ordinal dropdown and the weekday-set 'which occurrence'
dropdown each gain a 'custom…' option that reveals a free-form input,
allowing any non-zero ordinal (e.g. -2 = 2nd-to-last, 5FR) and
comma-separated BYSETPOS lists (e.g. 2,-1). Parsed rules with such values
now render structurally instead of falling back to the raw override.

* feat(task-repeat): make weekday-set occurrence (BYSETPOS) a multi-select

Replace the single-value 'which occurrence' dropdown with toggle buttons
matching the weekday toggles, so several occurrences can combine (e.g.
first + last weekday = BYSETPOS=1,-1). 'Every' clears the narrowing; the
custom input stays for values without a toggle.

* fix(task-repeat): address review findings on rrule migration fidelity

- Yearly builder rules seed BYMONTH from the start month: a bare
  FREQ=YEARLY;BYMONTHDAY=n expands across every month (RFC 5545) and
  fired monthly for fresh yearly configs.
- quickSetting change handler passes the selected start date, so
  date-writing presets no longer overwrite a user-picked future anchor
  with today.
- Remove the createForEachMissed checkbox + copy: the engine has no
  backfill support yet, so the setting silently did nothing (and wrote
  an undeclared field into synced state).
- rruleToLegacyTaskRepeatCfg always resets the monthly anchor fields so
  stale nth-weekday/last-day anchors can't survive the merge, sets
  monthlyLastDay only for a pure BYMONTHDAY=-1 rule, and aligns
  startDate to the rule's first occurrence for date-anchored
  monthly/yearly rules (legacy clients read day/month from startDate);
  save() re-derives the fallback so later startDate edits stay aligned.
- _normalizeMonthlyAnchor keeps monthlyLastDay on RRULE saves — it is
  the derived old-client fallback for BYMONTHDAY=-1, not a stale preset
  flag.
- Legacy CUSTOM migration preserves clamp semantics: day > 28 anchors
  emit BYMONTHDAY=<d>,-1;BYSETPOS=1 (the day, or the last day of
  shorter months) instead of a plain BYMONTHDAY that skips such months;
  the builder serializer emits BYSETPOS in day-of-month modes so these
  rules round-trip structurally.
- Regenerate package-lock.json to drop stale cron-parser/cronstrue
  entries left over from the earlier cron approach.

* fix(task-repeat): harden rrule builder + legacy fallback after deep review

Correctness:
- Clear bySetPos (and its custom-input state) on monthly/yearly mode and
  frequency switches: a BYSETPOS left over from the weekday-set mode
  silently narrowed day-of-month rules (BYMONTHDAY=15;BYSETPOS=2 never
  fires) with no UI to see or clear it.
- Move startDate alignment out of rruleToLegacyTaskRepeatCfg into a new
  arithmetic getAlignedStartDate, applied once at save and only when the
  schedule actually changed: the occurrence-search version corrupted the
  clamp idiom (aligned a day-31 rule onto Feb 28/29 so old clients fired
  on the wrong day forever), collapsed multi-day BYMONTHDAY lists to
  their earliest day, rewrote the visible start-date field live on every
  builder interaction, cost ~60ms/keystroke for sparse rules, and put
  startDate into the change diff on title-only edits — retriggering the
  issue-#7373 reschedule. Weekday-anchored rules are no longer aligned.
- Emit the monthly-anchor resets as null/false instead of undefined (in
  the converter AND the presets' MONTHLY_ANCHOR_RESET): JSON.stringify
  drops undefined keys from op-log payloads, so the reset never reached
  remote clients and stale nth-weekday/last-day anchors survived there.
  The anchor model fields now allow null; the nth-weekday engine guard
  strips it.
- Enforce the YEARLY BYMONTH guard in the serializer: yearly date mode
  without months now emits a plain FREQ=YEARLY (anniversary) instead of
  a bare BYMONTHDAY that fires monthly; parsed bare yearly rules fall
  back to the raw override, preserving their semantics verbatim.
- Drop the auto-seeded BYMONTH when switching away from YEARLY (it
  silently constrained the new rule to one month) and seed from the
  CURRENT start date rather than the ngOnInit-captured month.
- Clamp custom nth ordinals per frequency (±5 monthly, ±53 yearly) —
  BYDAY=10MO is valid RFC but matches nothing, ever — and reject an
  ordinal another row already anchors (duplicate rows collapse on
  reload). Re-clamp poses when switching into MONTHLY.
- Drop BYSETPOS=0 on parse (parseString accepts it; re-emitting creates
  a rule the occurrence engine silently treats as dead).
- Predefined set-position toggles close the explicitly-opened custom
  input (both rendered active with contradictory state).

Cleanup:
- Extract parseIntList (4 cloned int-list sanitizers), pushBySetPos /
  pushByDaySet (4 copy-pasted emit blocks), _clampedMonthDayPart
  (monthly/yearly clamp duplication); share the monthly/yearly
  weekday-set template block via ngTemplateOutlet; parse BYSETPOS via a
  computed signal instead of per-CD string parsing.
- Correct the BYMONTHDAY hint: it claimed short months clamp to their
  last day, but plain BYMONTHDAY skips them.

* test: make timezone demo specs host-independent

Six .tz.spec demonstration tests branched on the host's CURRENT
getTimezoneOffset() (or a literal 'America/Los_Angeles' name check)
while asserting against January test instants. In DST-observing zones
the summer offset differs from the January one (e.g. New York: EDT 240
vs EST 300), so the wrong branch was taken and the suite failed every
summer on US-east machines, blocking pre-push hooks.

Branch on the offset AT the test instant instead, with the correct
longitude threshold for each instant (07:00 UTC flips the local day at
UTC-7, 06:00 UTC at UTC-6, midnight UTC at UTC±0) — the tests now pass
in any host timezone year-round.

* test: pin browser timezone to Europe/Berlin in karma config

Re-enable the previously commented-out TZ pin so timezone-sensitive
specs run deterministically where Chrome honors the env var
(Linux/macOS, incl. CI). Verified empirically that headless Chrome on
Windows IGNORES TZ and keeps the host zone — the *.tz.spec.ts files
keep their offset-at-test-instant branching as the Windows backstop.

* test: make supersync dump/restore helpers cross-platform

createFullDump/restoreFullDump used POSIX-only shell constructs (output
redirection to /tmp, 'cat | psql', 'rm -f'). On Windows the restore
pipeline failed AFTER the 'DROP SCHEMA public CASCADE' had already
succeeded, leaving the test database without any tables — every
subsequent spec in the run then failed in createTestUser with 'table
public.users does not exist' (5 cascading failures + dozens of
never-ran tests).

Capture pg_dump via execSync stdout + fs.writeFileSync, feed the
restore through stdin (execSync input), use os.tmpdir() and
fs.unlinkSync. Verified: full dump-restore spec plus the three spec
files it previously poisoned all pass on Windows (14/14).

* test(e2e): round-trip coverage for new rrule builder widgets

Covers the four gaps hand-testing would otherwise need to catch, using
the dialog's live result band as the oracle (.rrule-result__expr =
exact assembled rule, .rrule-result__next = engine-computed upcoming
dates — no waiting for real time):

- custom nth ordinal (-2 = 2nd-to-last weekday) emits the right BYDAY
  and persists verbatim
- BYSETPOS multi-select (first + last) emits BYSETPOS=1,-1 and persists
- mode switch drops a weekday-set BYSETPOS instead of leaking it into a
  day-of-month rule (dead-rule regression)
- switching to YEARLY seeds BYMONTH and the upcoming dates are strictly
  one per year

Persistence asserted via the NgRx store: saving a recurring cfg
re-plans the task onto its first occurrence (usually not today), so a
UI reopen from the work view is date-dependent; the parse-to-widget
rendering side is covered by the dialog/builder unit specs.

* fix(add-task-bar): open the repeat dialog for the custom recurring option

The repeat quick-setting menu emits the 'RRULE' value for 'Custom
recurring config', but the add-task-bar still branched on the legacy
'CUSTOM' value — picking the option fell through to the preset path and
silently created a weekly-fallback cfg instead of opening the dialog.

Route both 'RRULE' and 'CUSTOM' through the dialog path, preselect the
RRULE builder in the dialog via a new initialQuickSetting dialog input
(the user explicitly asked for a custom rule), and show the tune icon
for the menu entry again. Covered by a new e2e: menu pick → task
created → builder dialog opens → saved rule persists verbatim.

* fix(task-repeat): address review on rrule alignment + anchor sync safety

- getAlignedStartDate re-anchors onto the rule's actual first occurrence
  (occurrence-set-neutral for INTERVAL/COUNT/UNTIL) instead of pure date
  math that shifted INTERVAL cadence and skipped valid clamped occurrences
- save() always re-derives the legacy fallback fields against the final
  startDate, not only when alignment moved it
- monthly anchors never persist null (released clients' typia schema only
  allows absent-or-numeric): undefined resets everywhere, null stripped at
  the action-creator boundary, model reverted to master signature
- round-trip guard ignores BYSETPOS=0 so the cleaned rule is emitted
  instead of being preserved verbatim via raw override

* docs(wiki): fix MD024 duplicate headings on repeating-tasks page

* fix(task-repeat): harden rrule save guards + restore preset labels on reopen

Correctness (from deep review of the branch diff):
- reject COUNT combined with repeatFromCompletionDate at save: completion
  re-anchors startDate/lastTaskCreationDay, restarting the COUNT window, so
  the series would never terminate
- reject parseable rules that yield zero occurrences (e.g.
  FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=30): they persisted as silently dead
  recurrences with the legacy fallback bypassed
- map the builder's single-weekday BYSETPOS form (BYDAY=FR;BYSETPOS=-1,
  'last Friday') onto the legacy nth-weekday anchor; old clients previously
  fell back to startDate's day-of-month — a wrong recurrence

Polish:
- infer the preset back from the stored rrule when quickSetting was
  wire-clamped to CUSTOM, so Weekends & co. reopen under their label
  instead of the raw builder; new QUICK_SETTING_PRESETS single source
  replaces the hand-maintained KNOWN_PRESETS set
- extract shared safeParseRRuleOptions + FREQ_TO_CYCLE (six drifted
  hand-rolled parse wrappers); memoize isRRuleValid (per-cfg routing guard
  on every overdue scan); share noonUtc/toLocalNoon between engine and
  preview; fold toMonthArray onto toNumArray; merge the duplicate
  _toPersisted action-creator helpers
- trim the wiki section documenting the create-for-each-missed feature
  that was deliberately cut from this PR

* fix(task-repeat): address curator review on anchor validity + fallback phase

- never emit/persist an out-of-union monthlyWeekOfMonth: range-guard the
  BYDAY ordinal in rruleToLegacyTaskRepeatCfg (the builder's custom input
  allows ±5, the synced model only 1..4|-1), sanitize null AND out-of-range
  anchors at the action-creator boundary, and mirror the repair in
  data-repair — an out-of-union value trips released clients' typia
  validation/repair flow
- reject sub-daily frequencies (raw override FREQ=HOURLY/...) at save: the
  day-granular engine would accept but silently collapse them to ~daily,
  and they have no legacy repeatCycle for old clients
- log hygiene: the update-all-instances flow now logs changed keys + task
  ids instead of raw changes/task objects (title, notes and rrule body are
  user content; the log history is exportable); engine warns log the error
  name only, since rrule.js messages can embed the free-text rule body
- align single-BYDAY weekly rules onto the engine's first occurrence: the
  engine groups weeks by WKST while the legacy fallback counts rolling
  7-day blocks from startDate, so biweekly cfgs whose start is off the
  pattern day fired on OPPOSITE alternating weeks on old vs new clients
  (and WKST shifted the engine's phase, which legacy cannot express);
  after re-anchoring the cadence is exactly interval*7 days in both
- diff dialog saves against the PROCESSED cfg: the lazy legacy->rrule
  migration (and preset inference) no longer leak into the change set of a
  title-only edit — rrule is schedule-affecting, so that leak relocated
  today's live instance on unrelated edits; empty change sets skip the
  update dispatch entirely instead of creating a no-op sync op

* fix(task-repeat): keep presets rrule-backed + builder mode for completion cfgs

- stop stripping the preset-generated rrule at save: getQuickSettingUpdates
  OVERWRITES the rule with the preset's canonical one, so the old 'clear on
  switch away from builder' branch broke the every-cfg-carries-its-rrule
  contract — and an rrule:undefined clear would not even propagate (dropped
  by the op-log JSON wire, leaving remote clients on the old rule); the spec
  asserting the stale behavior is inverted
- completion-relative cfgs always open in builder mode: the schedule-type
  toggle only exists there, so preset inference (or a stored faithful preset
  label) would hide the one control that explains how the cfg fires
- monthly anchor clears: document the wire gap properly instead of flipping
  values again — null trips released clients' blocking data-repair confirm
  dialog on every sync (typia has no null on these fields), undefined clears
  only locally; durable clears require shipping '| null' on both fields in a
  release FIRST, then switching the reset value (sequenced migration note in
  the model + an op-log JSON round-trip spec pinning current behavior)
- replace 6 hardcoded rrule-builder placeholders with translation keys
- revert the settings help text advertising the deferred @+ short syntax

* test(e2e): drop CUSTOM weekday-checkbox spec superseded by rrule builder

The #8025 e2e (merged in from master) drives the legacy CUSTOM recurring
form's cycle select + weekday checkboxes, which this branch removes — the
RRULE builder replaces that UI and its weekday toggles are covered by
rrule-builder-roundtrip.spec.ts. The #8025 failure mode (a saved weekly
cfg that never fires) is blocked generally at save by the first-occurrence
probe.

* test(e2e): de-flake worklog history day-row wait

The worklog is rebuilt from the archive on the history navigation, so the
day row only appears once that async load + month-expand animation settles.
The bare `waitFor` fell back to the 15s action timeout, which CI load
(prod build, 3 workers) could exceed. Use a web-first `expect().toBeVisible()`
with 30s headroom instead.

* fix(task-repeat-cfg): clear repeat-from-completion when switching to a preset

The "from completion" schedule-type toggle exists only inside the RRULE
builder. Selecting a preset hides it, but the flag stuck on the cfg, so a
preset could keep firing relative to completion with no visible control.

Clear it at the save() edit boundary, but only when actually set, so an
untouched preset save stays an empty-diff no-op (#7373 class) rather than
dispatching a spurious undefined->false op. The ADD path already starts from
a false default, so the dialog is the only path that needs it.

Also reword the monthly-anchor clear comments: the cross-client divergence on
an nth-weekday -> day-of-month switch is an inherent, unfixable limitation for
pre-rrule clients (no in-schema "no anchor" value; a `| null` migration would
help an empty client band), not a deferred TODO. Make the round-trip test pin
this explicitly and start the monthlyLastDay case from `true` so it proves the
`false` clear survives the wire instead of passing vacuously.
2026-06-09 11:25:35 +02:00
Johannes Millan
8d43c50631 fix(plugins): dedupe Google Calendar auto-time-block events in schedule #8171
With Auto time blocking on, scheduling a task writes a mirror event to
Google Calendar. When the read calendar overlaps the time-block calendar,
that mirror was fetched back and rendered next to the local task that
created it — a duplicate. Filter SP-owned events (tagged with
extendedProperties.private.spTaskId) out of all plugin read paths.
2026-06-09 10:50:07 +02:00
felix bear
62f8141b82
fix(supersync): improve account badge contrast (#8186)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-09 00:13:33 +02:00
felix bear
fe65d4b9a6
fix(plugin): refresh procrastination buster i18n #5102 (#8145)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 20:44:43 +02:00
Johannes Millan
aa2ad88ff1
fix(caldav-plugin): make recurring-occurrence edits/deletes safe and quiet #7492 (#8149)
* build: drop unused elevate.exe from Windows build #8135

elevate.exe is bundled by electron-builder's NSIS target solely so
electron-updater can apply privileged per-machine updates. We ship no
in-app auto-updater (electron-updater is not a dependency; the autoUpdater
block in electron/start-app.ts is commented out), so the binary is unused
dead weight and a frequent AV false-positive. Set packElevateHelper: false
to stop bundling it; safe because perMachine is not true.

* fix(caldav-plugin): make recurring-occurrence edits/deletes safe and quiet #7492

Expanded RRULE occurrences all share one master .ics, so operating on a
single occurrence hit the whole series: updateIssue rewrote the master
(and the next poll pulled it onto every sibling), deleteIssue deleted the
master (the entire series), and getById returned the master's DTSTART,
collapsing every instance onto the first one's time.

Plugin:
- parseCompoundId surfaces occurrenceMs instead of discarding it
- getById re-anchors start/end onto the requested occurrence (timed via
  toIcalUtcDateTime, all-day via toIcalDate matching ical.js local-midnight)
- updateIssue/deleteIssue refuse occurrence writes with a marked error
  (isExpectedSyncSkip), so the series is never mutated

Host two-way sync:
- editing or deleting a task linked to one occurrence now stays silent (the
  user changed their task, not the calendar) and the local change still
  applies. Explicit agenda reschedule/delete keep surfacing the honest
  "can't change a single occurrence" message (no false success).

Single non-recurring events are unaffected. Full per-occurrence editing
(RECURRENCE-ID overrides + EXDATE) remains a follow-up; it needs an If-Match
primitive and recurrence-value preservation.
2026-06-08 16:05:52 +02:00
Johannes Millan
0d1869263f docs: remove outdated and implemented plan docs
Delete 29 plan/design docs whose work has shipped or been superseded
(SuperSync slices, sync-core extraction, encryption-at-rest drafts,
document-mode/Stage-A persistence, calendar/CalDAV concepts, focus-mode
time-tracking sync, etc.).

Kept the still-forward-looking docs (e.g. supersync-encryption-at-rest,
sync-core-simplification-roadmap, calendar-two-way-sync-technical-analysis).

Source comments that cited deleted docs are rewritten into self-contained
inline rationale so no "see docs/..." reference dangles.
2026-06-08 12:38:51 +02:00