Commit graph

21123 commits

Author SHA1 Message Date
rugk
5c66615fa3
docs(readme): fix typo/missing words (#8453)
A small grammar fix here.
2026-06-17 16:02:08 +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
Johannes Millan
7eeea37b7a ci(e2e): retry npm install to absorb Prisma postinstall ECONNRESET 2026-06-17 14:35:12 +02:00
Johannes Millan
797451427e test(daily-summary): guard /active/daily-summary route resolution (#8449)
Add an e2e regression guard asserting the context-resolved
/active/daily-summary route opens the daily summary instead of falling
through to the wildcard and redirecting to the start page. The unit spec
only pins the navigation string; this verifies the string is a navigable
route.
2026-06-17 14:29:12 +02:00
Johannes Millan
07d3e1d722 fix(finish-day): open daily summary from before-close dialog (#8449)
The before-close confirm dialog's 'Finish Day' option navigated to a bare
'/daily-summary' route, which does not exist at the top level. It fell
through to the '**' wildcard and redirected to the start page, so the
daily summary never opened and the app appeared to do nothing.

Navigate to '/active/daily-summary' instead — resolved by
ActiveWorkContextGuard to the current context, matching the in-app
Finish Day button.
2026-06-17 14:29:12 +02:00
Johannes Millan
9b35c30f9c fix(nextcloud-deck): normalize card done to boolean to prevent isDone: null
The Deck REST API returns a card's done field as a nullable completion
timestamp, not a boolean (proven by the write path that sends an ISO
string). The raw value was copied straight into task.isDone, so not-done
cards stored isDone: null, failing typia validation and triggering the
data-repair dialog on every startup/poll. In the batch poll path done
cards are filtered out, so every synced card had done: null.

Coerce to a real boolean at the API mapping boundary and correct the
DeckCardResponse.done type to string | null to reflect the API.

Fixes #8436
2026-06-17 13:31:02 +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
Maikel Hajiabadi
ec16757c82
fix(keyboard): resolve macOS global shortcut layout mismatch (#8378) (#8381)
* fix(keyboard): resolve macOS global shortcut layout mismatch (#8378)

* fix(keyboard-layout): log layout-detection failure and resolve layoutReady with map copy

* fix(keyboard-shortcut): remove debug console logs and add macOS scope comment

* fix(keyboard-shortcut): preserve modifier separator when mapping plus key shortcut

* refactor(keyboard-shortcut): export mapping helpers and avoid as any cast in configuration mapping

* test(keyboard-shortcut): add unit tests for layout shortcut translation logic

* test(keyboard-shortcut): use correct KeyboardConfig type instead of as any in test fixture

* docs(keyboard-shortcut): hoist macOS physical shortcut layout comments to helper JSDoc

* refactor(config): extract global shortcut keys and mapping helpers

* test: add test helper capability for IS_ELECTRON and IS_MAC

* test: add comprehensive integration unit tests for global shortcut effects

* feat(electron): eagerly trigger layout detection on Electron startup for macOS timing fix

* refactor(config): InjectionToken migration, layout detection hardening, and startup optimization

* refactor: address non-blocking suggestions for layout detection and DI tokens

* build(electron): move keyboard-config.model to shared-with-frontend to fix ASAR require

* fix(client-id): increase entropy to 6 chars and fix related test regressions
2026-06-17 12:57:47 +02:00
Symon Baikov
807d3bf5e5
perf(sync): cache latest full-state op lookup (#8427)
* perf(sync): cache latest full-state op lookup

* fix(sync): harden full-state op metadata

* refactor(sync): dedupe full-state-ops meta helper + SQLite tests

Extract the FullStateOpRef/FullStateOpsMetaEntry shape and the
"latest = max UUIDv7" rule into full-state-ops-meta.ts, shared by the
upgrade-time seed (db-upgrade.ts) and the runtime maintenance
(operation-log-store.service.ts) so the two copies can't drift. The
shared builder always copies refs, removing the prior store-vs-upgrade
copy inconsistency (call sites already pass fresh arrays, so behavior is
unchanged).

Add store-service tests driving the full-state metadata pointer over a
real SQLite engine (sql.js), including the rebuild-on-read fallback that
keeps the pointer correct on SQLite, where the IndexedDB-only
populate-on-upgrade seed never runs.

* fix(sync): prune full-state meta atomically in clearFullStateOpsExcept

Read the meta pointer INSIDE the delete transaction instead of taking a
snapshot before it. The prior out-of-tx read could be clobbered by a
full-state append committed between the read and the write, dropping that
op's ref from the pointer while the op stayed in OPS — risking a stale
filtering baseline on the sync-receive path (packages/sync-core caller).
The OPERATION_LOG lock masks this today, but the method is now atomic and
self-consistent regardless of lock scope, matching deleteOpsWhere.

Also route the two hand-built meta writes (clearAllOperations,
runDestructiveStateReplacement) through buildFullStateOpsMeta so `latest`
is always derived from refs, never hand-asserted.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-17 12:55:31 +02:00
Symon Baikov
d50c74eda7
feat(menu-tree): order project and tag dropdowns by tree (#8100)
* feat(menu-tree): order project and tag dropdowns by tree

Add tree-order list helpers to MenuTreeService for projects and tags. The helpers flatten the stored menu tree depth-first, skip folder nodes, preserve tree sibling order, and append items missing from the tree so selectable projects and tags are never dropped.

Expose tree-ordered project and tag lists from ProjectService and TagService while keeping the existing sorted APIs for non-dropdown consumers such as short-syntax suggestions.

Switch project and tag selection dropdowns to the new tree-order APIs across task context menus, task rows, add-task bar controls, note project moves, formly project selection, start-page project selection, and tag edit/toggle menus.

Cover the ordering behavior with MenuTreeService, ProjectService, TagService, add-task-bar actions, and task context menu specs.

* fix(task-view-customizer): merge duplicate-titled tags when grouping

Distinct tag entities can share a title. _groupByTag keyed buckets by
tag.title but assigned with =, so a second same-titled tag overwrote the
first tag's tasks, dropping them from the grouped task view. Merge into
the title bucket instead, restoring the previous title-keyed grouping and
matching _getTagTitleOrderMap's single-slot contract.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-17 12:46:22 +02:00
Maksym Dilanian
f7ccfb540d
feat(i18n): update Ukrainian tranlation (#8410)
* feat(i18n): update Ukrainian tranlation

* fix(i18n): revert translation of interpolation token 'date'

* fix(i18n): restore placeholder interpolation for some strings
2026-06-17 10:55:43 +02:00
Maikel Hajiabadi
dd2a89770d
Fix visuals (#8412)
* style(ui): fix persistent focus ring for slide toggles

* style(ui): enhance focus ring and hover for slide toggles

* style(ui): improve accessibility and focus-visible behavior for slide toggles
2026-06-17 10:53:48 +02:00
Johannes Millan
5497212b99 fix(android): inset WebView for IME under enforced edge-to-edge (draft)
Durable follow-up to the #8295 revert. On targetSdk 36 (Android 16) edge-to-edge
is mandatory and adjustResize is a no-op for the IME, so the WebView only stays
above the soft keyboard if @capawesome/capacitor-android-edge-to-edge-support
insets it. The plugin instead zeroes the WebView bottom margin while the keyboard
is visible (assuming the system resized the window), leaving fixed content behind
the IME.

patch-package the plugin's EdgeToEdge.applyInsetsInternal so the WebView bottom
margin is always max(imeInsets.bottom, systemBarsInsets.bottom). The WebView then
shrinks above the keyboard, content resizes, and --keyboard-height stays 0 with
the add-task bar above the IME on the existing JS path. Wire patch-package via a
postinstall script; run 'npm install' to apply (also refreshes the lockfile).

DRAFT: verified the patch applies cleanly, but the gradle build and on-device
behavior are UNVERIFIED here. Needs the Android 10/14/15/16 matrix in
docs/android-edge-to-edge-keyboard.md before merge; upstream so the patch can be
dropped.
2026-06-16 17:00:55 +02:00
Johannes Millan
43444e6b53 fix(android): revert #8295 keyboard tracking that mispositioned add-task bar
The store v18.10.0 build positions the global add-task bar correctly above
the soft keyboard and resizes normally. #8295 (merged after v18.10.0) is the
only keyboard-logic change since, and it regressed both: on this branch the
bar sits behind the IME and the view no longer appears to resize.

#8295 stacked two extra keyboard-height signals (a native physical-px height
via keyboardHeightPx$ and a baseInnerHeight-tracking VisualViewport path) on
top of the single VisualViewport `obscured` signal that shipped in v18.10.0,
combined as max(obscured, nativeKeyboardHeight - layoutShrink). The sources
fire on separate async events and race; scheduleCommit() resets
baseInnerHeight to the already-shrunk innerHeight mid-IME-animation, which
zeroes layoutShrink and defeats the double-count guard, so --keyboard-height
lands on a wrong value on devices that already handle the IME.

Revert to the known-good single-signal logic. The narrow Android 10 case
#8295 targeted will be re-addressed via a single WindowInsets IME listener
that is verified on a real device matrix.

Reverts #8295. Drops the orphaned calc-keyboard-height spec (its unrelated
#8421 backup-ring/state changes are kept).
2026-06-16 17:00:55 +02:00
Johannes Millan
4fc106890a fix(i18n): add missing History menu label translation (#8430) 2026-06-16 15:54:40 +02:00
Johannes Millan
09f9ac4299
fix(focus-mode): persist fullscreen notes when a session ends mid-edit (#8432)
* style(theme): give rainbow theme a neon glow makeover

Rework the bare rainbow theme into a full neon look inspired by a
glowing dashboard reference:

- deep indigo "space" palette with a fixed aurora nebula on
  .app-container (mat-sidenav-content is not the work-area wrapper)
- frosted, glowing panels: side nav (violet bottom-glow), right panel,
  cards, menus/dialogs, add-task bar — backdrop-filter on inner panes
  only, never the side-nav host (keeps the mobile drawer) or task rows
- glowing active nav pill, primary buttons, chips, focus rings, links,
  headings and a gradient neon scrollbar
- task rows get a translucent indigo fill (were transparent) so the
  aurora tints through as glass; no per-row blur to protect the hot path
- task detail items now use the indigo --task-detail-* surfaces + glow
  instead of falling back to neutral grey
- mark the theme requiredMode 'dark' so picking it applies a dark canvas

All glows are static (no animation) to stay GPU- and battery-friendly.

* test(recurring): pin clock in #6860 helper spec to fix date-dependent flake

The helper-based test hardcoded 15/06/2026 as the recurring start date but,
unlike its sibling specs, never pinned the clock. The datepicker disables
past days, so the scheduled run on 2026-06-16 could no longer click the
now-disabled 2026-06-15 cell and timed out. Pin today to 2026-05-01 (matching
the sibling recurring-date specs) so the hardcoded date stays a selectable
future day.

* fix(focus-mode): persist fullscreen notes when a session ends mid-edit

The fullscreen markdown editor is a detached CDK overlay. When a focus
session ends while it is open, focus-mode-overlay swaps screens
(Main -> SessionDone/Break) and destroys the inline-markdown that opened
the dialog. Its `changed` output then has no listener, so on Save the
note was emitted into the void and silently lost.

On afterClosed, when the component has already been destroyed, persist the
note directly to the store using the taskId captured when the dialog
opened, instead of emitting `changed`. Skip when the content is unchanged
(avoids writing the default-text placeholder back as a real note) and warn
when there is no taskId to persist to.

Covered by inline-markdown unit tests and a focus-mode e2e for both
Countdown and Pomodoro sessions completing mid-edit.
2026-06-16 14:58:50 +02:00
Johannes Millan
f51954f805
fix(deps): bump @angular/* to 21.2.17 and tmp to 0.2.7 (security) (#8431)
Clears the open Dependabot security advisories for the Angular runtime packages and tmp.

The whole @angular/* framework family is bumped together (21.2.14 -> 21.2.17): the packages use exact-version peer deps, so bumping @angular/core alone (as #8409 did) leaves the siblings at 21.2.14 and fails 'npm ci' with an ERESOLVE peer conflict. Supersedes #8409.
2026-06-16 14:25:02 +02:00
Johannes Millan
8ae374ae60 fix(ui): render deep-link schemes in plain-text link surfaces (#8429)
render-links.pipe (task titles, planner/schedule events, plain-text
notes) had its own scheme denylist predating the GHSA-hr87 allowlist, so
obsidian://, vscode://, mailto:, tel: etc. rendered as inert text there
even after the markdown renderer was fixed.

Route the pipe through the shared ALLOWED_EXTERNAL_URL_SCHEMES so all link
surfaces enforce one policy; preserve allow-listed schemes verbatim
instead of prefixing http://. Dangerous schemes (javascript:, data:,
vbscript:, ms-msdt:) still fall through to the scheme-shaped rejection.
2026-06-16 11:52:44 +02:00
Johannes Millan
a4bde69ece fix(electron): allow obsidian:// and other app deep-link schemes (#8429)
The GHSA-hr87 scheme allowlist (#8210, v18.10.0) permitted only
http/https/mailto/file, which silently broke custom app deep-links such
as obsidian:// in task-attachment links and markdown notes.

Add low-risk app-launch + standard schemes (obsidian, vscode,
vscode-insiders, zotero, logseq, tel, sms) to the shared allowlist. This
restores the links across all sinks (OPEN_EXTERNAL, OPEN_PATH, the
navigation fallback, and the markdown link renderer) while still blocking
the OS-level handlers (ms-msdt:, search-ms:) the allowlist exists to stop.
2026-06-16 11:52:44 +02:00
Johannes Millan
bc8fea8baa test(recurring): pin clock in #6860 helper spec to fix date-dependent flake
The helper-based test hardcoded 15/06/2026 as the recurring start date but,
unlike its sibling specs, never pinned the clock. The datepicker disables
past days, so the scheduled run on 2026-06-16 could no longer click the
now-disabled 2026-06-15 cell and timed out. Pin today to 2026-05-01 (matching
the sibling recurring-date specs) so the hardcoded date stays a selectable
future day.
2026-06-16 11:52:44 +02:00
Johannes Millan
3d3923b3e9 style(theme): give rainbow theme a neon glow makeover
Rework the bare rainbow theme into a full neon look inspired by a
glowing dashboard reference:

- deep indigo "space" palette with a fixed aurora nebula on
  .app-container (mat-sidenav-content is not the work-area wrapper)
- frosted, glowing panels: side nav (violet bottom-glow), right panel,
  cards, menus/dialogs, add-task bar — backdrop-filter on inner panes
  only, never the side-nav host (keeps the mobile drawer) or task rows
- glowing active nav pill, primary buttons, chips, focus rings, links,
  headings and a gradient neon scrollbar
- task rows get a translucent indigo fill (were transparent) so the
  aurora tints through as glass; no per-row blur to protect the hot path
- task detail items now use the indigo --task-detail-* surfaces + glow
  instead of falling back to neutral grey
- mark the theme requiredMode 'dark' so picking it applies a dark canvas

All glows are static (no animation) to stay GPU- and battery-friendly.
2026-06-16 11:52:44 +02:00
Johannes Millan
e033b87808
fix(backup): guard iOS backup against overwrite on transient read failure (#8414) (#8422)
* fix(backup): guard iOS backup against overwrite on transient read failure (#8414)

The iOS write path read the existing primary slot via _readIOSFileOrNull,
which swallows all errors and returns null. On a transient read failure that
made `existing` look like "no backup yet": the near-empty guard and prev-slot
promotion were skipped and the primary was overwritten with no prev copy
preserved — defeating the two-generation ring exactly when it is needed.

Add a write-path reader (_readIOSExistingSlotOrThrow) that distinguishes a
confirmed-absent slot (legitimate first backup → null, write proceeds) from a
transient read failure (re-probed via stat → rethrow), and make _backupIOS
bail without overwriting on uncertainty, mirroring _backupAndroid's guard.
The read/restore path keeps _readIOSFileOrNull's swallow-all behavior, which
is correct there.

* test(backup): cover first-ever iOS backup when Capacitor throws on absent file (#8414)

Adds an end-to-end _backup() regression test proving the overwrite guard does
not over-correct: when the raw read throws (Capacitor's signal for a missing
file) and stat confirms absence, the primary is still written. Complements the
transient-failure bail test.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-16 11:09:32 +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
Harbinger
cabf266574
refactor(op-log): reduce duplication in persistence paths (#8406)
Extract helpers that preserve public behavior and storage formats exactly:

archive-store.service.ts:
- _loadFromStore/_saveToStore/_hasEntry helpers for archive_young
  and archive_old, eliminating 6 duplicated method bodies

operation-log-store.service.ts:
- _buildStoredEntry: shared entry construction for all append paths
- _handleAppendError: shared ConstraintError/QuotaExceeded handling
- _invalidateAppliedAndUnsyncedCaches: shared cache invalidation

state-snapshot.service.ts:
- SNAPSHOT_SELECTORS constant array, eliminates dual 17-selector
  listing in _getNgRxDataSync and getStateSnapshotAsync
- _normalizeNgRxData for shared task field normalization

archive-operation-handler.service.ts:
- _guardArchiveOverwrite: consolidates empty-archive protection
  for SyncImport/Repair/BackupImport across archiveYoung and
  archiveOld

All existing tests pass. No behavioral changes, no public API changes.

Fixes #7920
2026-06-16 11:04:59 +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
2e1330a5ef
test(theme): cover calcKeyboardHeight defensive clamps (#8421)
* test(theme): cover calcKeyboardHeight defensive clamps

Add edge-case coverage for the two Math.max guards in calcKeyboardHeight:
a negative obscured area (visual viewport taller than the layout viewport,
e.g. pinch-zoom) clamps to 0, and a native keyboard height exceeding the
base inner height passes through without flipping negative.

* fix(tasks): persist add-task-bar draft note across close/reload

The inline note (noteTxt) lived only in memory, so closing the add-task bar
(e.g. an Escape in the title input) or reloading discarded a typed note with
no warning. Persist it to sessionStorage with the same init+effect pattern as
inputTxt so a draft note survives. Closes #8415 (note-loss parts).

* refactor(backup): key sync-enabled backup guard off SyncConfig

backupStrHasSyncEnabled read globalConfig.sync.{isEnabled,syncProvider} via
string-literal keys, so a future rename of those fields would silently weaken
the guard (a synced backup would read as non-synced and auto-restore). Type
the parsed shape against Pick<SyncConfig, ...> so a rename breaks the build
here instead. Closes #8414 (Gate B hardening).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-15 23:28:07 +02:00
Johannes Millan
080f0b0be3
feat(tasks): add inline note field to the add-task bar (#8403)
* feat(tasks): add inline note field to add-task bar

Add a collapsible multi-line note/description field to the add-task bar
so a note can be entered together with the task (#6866).

- Note chip in the action-bar row toggles a textarea below the bar;
  Ctrl/Cmd+Enter from the title also expands it, and submits from within
  the note (plain Enter inserts a newline, Esc collapses).
- Collapsed by default; the chip is grayed until a note is entered.
- The trimmed note is written to task.notes on add (single create op).
- Remove a stray Log.x(taskData) debug line that would otherwise record
  the user's note into the exportable log history.

* fix(tasks): surface add-task note chip on mobile and use the notes icon

- Move the note chip to the second position (after project) so it is
  visible without horizontally scrolling the action row on narrow
  screens. The previous trailing position was always off-screen.
- Use the chat / chat_bubble_outline icon that marks notes elsewhere
  (task detail panel) instead of the generic "notes" icon.

Replaces the earlier sticky-pin attempt, which rendered an opaque fill
over the often semi-transparent add-task bar and read as a floating,
confusing control while scrolling.

* refactor(tasks): use two-way ngModel for the add-task note field

Multi-review follow-ups:
- Bind the note textarea with [(ngModel)]="stateService.noteTxt" instead
  of the split [ngModel]/(ngModelChange), matching the sibling title
  input. Add a DOM test confirming the signal two-way binding writes back
  (the sibling input pairs it with an (input) handler, so the write path
  was worth proving rather than assuming).
- Drop the now-dead isNoteExpanded mock from the actions spec; the chip
  no longer reads it.

* style(tasks): drop stray blank line in add-task-bar actions template
2026-06-15 18:07:24 +02:00
Johannes Millan
d6475999e2
fix(backup): chunk Android backup reads to survive 2MB CursorWindow (#8402)
* fix(backup): chunk Android backup reads to survive 2MB CursorWindow

KeyValStore.get() read the value via cursor.getString(), which throws SQLiteBlobTooBigException once a row exceeds Android's ~2MB CursorWindow. The on-device backup blob crosses 2MB after roughly a year of normal use, so the backup was written but never readable — surfacing as "Error invoking loadFromDb: Java exception" and silently breaking the #7901 eviction-recovery path. Combined with the data store living in evictable WebView IndexedDB (#7892), this caused total data loss on Android. Electron/iOS use file-based backups and are unaffected.

- KeyValStore.get(): read in 256K-char substr() chunks + try/catch->default; also recovers already-written oversized rows.
- KeyValStoreInstrumentedTest: emulator regression (Robolectric can't reproduce the CursorWindow limit).
- local-backup.service: _loadAndroidDbValueSafe() logs blob size (not content) to the exportable log and degrades a read failure to "no backup" instead of an opaque crash.

Closes #8401

* test(backup): cover Android KV read-degradation + write-path bail (#8402)

Adds CI-runnable Karma specs for the resilience logic from the fix commit: read degrades to null (never throws), and the write path bails without overwriting when the existing-slot read fails (preserving both ring slots). To make the window.SUPAndroid singleton (undefined off-device, no DI seam) testable, the native calls now route through thin _nativeDbLoad/_nativeDbSave methods the specs spy. The real ~2MB CursorWindow limit remains covered by KeyValStoreInstrumentedTest (emulator-only).
2026-06-15 17:51:50 +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
Maikel Hajiabadi
05e9724ba7
style(ui): globalize task-detail field style to filled form fields (#8292)
* style(ui): globalize task-detail field style to filled form fields

* style(theme): define CSS custom properties for input fields

* style(theme): refactor global form fields to use input tokens

* style(theme): override input tokens in zen theme to use hairline borders

* style(theme): override input tokens in velvet theme and remove redundant overrides

* style(theme): override Material 3 container shape radii to fit app-wide geometric rules

* Revert "style(theme): override Material 3 container shape radii to fit app-wide geometric rules"

This reverts commit 59f9620421.

* style(theme): refactor form fields error states, focus indicators, and theme refinements

* style(ui): round all corners of filled fields; drop redundant theme tokens

Material hardcodes the filled text-field's bottom corner radii to 0 and the
container-shape token only rounds the top two corners, so the added 1px border
rendered rounded-top/square-bottom instead of the intended card shape (and
regressed Velvet, which previously rounded all four corners). Force the full
border-radius via the shared --input-border-radius token.

Also drop two no-op token declarations surfaced in review:
- velvet: --input-border-color-focus duplicated the inherited base value
  (both --c-primary and --focus-ring-color resolve to --palette-primary-500)
- zen: --input-bg: transparent in the dark block was already inherited from body

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-06-15 16:20:18 +02:00
Johannes Millan
2304d10a50 test(migration): navigate to project tasks route to de-flake legacy migration e2e
STEP 8 navigated to /project/:id without the /tasks suffix. PROJECT_CHILD_ROUTES
has no default child redirect, so the child router-outlet rendered empty and the
parent-task assertion only matched the lingering Today landing view — flaking on
master CI (retries:0) whenever that stale DOM was torn down under parallel load.

Navigate to /project/TEST_PROJECT/tasks and reload for a single clean render
(migration's Genesis op makes the reload skip re-migration). Verified 4/4 on
--repeat-each plus the full spec file.
2026-06-15 15:48:34 +02:00
dependabot[bot]
3055656d27
chore(deps)(deps): bump the github-actions-minor group with 4 updates (#8399)
Bumps the github-actions-minor group with 4 updates: [gradle/actions](https://github.com/gradle/actions), [ruby/setup-ruby](https://github.com/ruby/setup-ruby), [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `gradle/actions` from 6.1.0 to 6.2.0
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](50e97c2cd7...3f131e8634)

Updates `ruby/setup-ruby` from 1.310.0 to 1.313.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](afeafc3d1a...89f90524b8)

Updates `anthropics/claude-code-action` from 1.0.133 to 1.0.147
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](787c5a0ce9...56fa348258)

Updates `github/codeql-action` from 4.36.0 to 4.36.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](7211b7c807...8aad20d150)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-minor
- dependency-name: ruby/setup-ruby
  dependency-version: 1.313.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-minor
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.147
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-minor
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 15:38:31 +02:00
Johannes Millan
36cf5e200b style(work-view): collapse isExpanded binding to satisfy prettier 2026-06-15 15:14:14 +02:00
Johannes Millan
bb329bfa43 fix(focus-mode): stack break readout to prevent label/digit overlap (#8391) 2026-06-15 15:14:14 +02:00
Johannes Millan
7d30a646d9 fix(plugins): keep plugin side panel from rendering empty (#8394)
The plugin iframe (srcdoc, v18.10.0) drove its visible UI through tokenless
PLUGIN_MESSAGE round-trips that the host accepted only when
`event.source === this.iframeRef.contentWindow`. Under zoneless change
detection the @ViewChild `iframeRef` is assigned on a later rAF/timeout CD
pass, which can land after the iframe has already posted its first messages.
Those messages were then dropped forever (postMessage is not redelivered) and
useTranslate has no timeout, so the panel stayed blank — intermittently,
depending on how the timing landed.

- Resolve the iframe window from the live DOM (host-scoped querySelector)
  instead of the cached @ViewChild. The iframe must be in the DOM to have
  posted a message, so this always finds it at message time, while staying
  scoped to this component so sibling plugin iframes are not answered.
- Drop the redundant _cleanupIframeCommunication() on fresh mount: it set an
  empty-document srcdoc that forced an extra iframe load + CD pass before the
  real document, widening the race.
- Add PluginIndexComponent spec covering the dropped-message regression and
  cross-iframe isolation.
2026-06-15 15:14:14 +02:00
Johannes Millan
df8928c911 fix(tags): use sidebar order for tag grouping/sorting/menus (#8400)
Tag grouping and sorting in the task-view customizer ignored the
sidebar order: grouping rendered via the keyvalue pipe (alphabetical
key sort) and sorting compared the alphabetically-first tag title.
Both tag-toggle menus also disagreed - the inline one followed the
sidebar while the right-click context menu was alphabetical.

Introduce a shared MenuTreeService.flattenTagViewTree as the single
source of truth for sidebar (menu-tree) tag order, and route through it:
- sort-by-tag places a task by its highest-priority tag (lowest sidebar
  index), falling back to task title for ties
- group headers are ordered by sidebar index (special buckets last);
  non-tag groups keep their previous ascending order
- both tag-toggle menus now share the same flattened order

This removes the need to force order with numeric tag prefixes.
2026-06-15 15:14:14 +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
Wadim
7d4c4b1877
fix(android): keep add-task input above keyboard on Android 10 (#8295)
* fix(android): keep add-task input above keyboard on Android 10

* fix(android): keep add-task input above keyboard on Android 10 - address comments
2026-06-15 14:11:50 +02:00
Johannes Millan
0ec8c207a8 feat(theme): add Plainspace built-in theme
A warm paper theme ported from plainspace.org: cream surfaces, a
terracotta primary and a teal accent, in light and dark. Registered in
BUILT_IN_THEMES (requiredMode: system) and listed in the theming docs.

Surfaces/ink primitives drive the semantic tokens; the primary/accent
palette ramps recolor the brand. Dark-mode Category-B tokens (subtasks,
notes, selected rows, schedule events) are re-derived from the warm
surface ladder so nothing leaks the base cold-grey ramp.

Signature paper touches:
- squared graph-paper backdrop (reuses the body::before layer)
- hand-drawn terracotta underlines on headings and collapsible headers
- a hand-drawn rule under each board column header in place of a panel box
- warm soft paper shadows (--card-shadow split per mode) and tighter
  3/6/8px radii

The ambient primary-tinted gradient wash is removed (repurposed to the
grid); remaining gradients are contextual and left untouched.
2026-06-15 14:06:21 +02:00
Johannes Millan
b12f72899f
fix(backup): auto-restore on-device backup on blank mobile launch and harden durability (#7901) (#8388)
* fix(backup): auto-restore on-device backup on blank mobile launch and harden durability (#7901)

Durability follow-ups to #7901 / #7892 (Android total data loss when the
WebView IndexedDB is evicted while the durable on-device backup survives).

- Mobile auto-restore: on a blank/evicted launch (no state cache, but a
  usable on-device backup exists) restore the newest usable backup
  automatically instead of behind a confirm dialog users routinely dismiss.
  Only triggers for a genuinely empty live store (a deliberate in-app delete
  leaves a valid empty state cache and is not auto-restored); falls back to
  the informed prompt for corrupt/data-less blobs. Reports counts via snack.

- KeyValStore.onUpgrade no longer DROPs the table. It holds the durable
  on-device backup (keys backup / backup_prev); a destructive upgrade would
  wipe it the moment DATABASE_VERSION was bumped. Upgrades are now additive
  (CREATE TABLE IF NOT EXISTS), preserving rows.

- Record the last successful local-backup time (after the meaningful-data
  guard) and surface a "Last backup: <date>" line in the mobile Automatic
  Backups settings, so no-sync users can see they're protected; the
  timestamp also rides along in exported logs for eviction diagnosis.

Docs: correct 3.06-User-Data (Android backup is native app-private SQLite,
not WebView IndexedDB) and document the auto-restore + timestamp behavior.

Tests: auto-restore (usable / import-failure / corrupt-fallback / no-backup)
and getLastBackupTime in local-backup.service.spec; config-page spy updated.

https://claude.ai/code/session_01E5YzMGtfMr33qQLjMM2SGA

* fix(backup): gate mobile auto-restore to empty op-log + non-synced backups (#7901)

Hardening from the multi-agent review of the previous commit's mobile
auto-restore. Two gates so silent auto-restore fires only in the
unambiguously safe case (eviction of a local-only user's store):

- Gate A (startup.service): only consider a backup restore when the op-log is
  empty (getLastSeq()===0), not merely when the state-cache snapshot is absent.
  A null cache alone can still have real ops the hydrator is concurrently
  replaying; auto-restoring then was unnecessary and raced the hydrator's
  replay against importCompleteBackup's destructive op-log replacement.

- Gate B (local-backup.service): only silently auto-restore a backup that had
  NO sync configured. Restoring a synced backup resets lastServerSeq and writes
  a clean-slate BACKUP_IMPORT, which can silently drop other devices' concurrent
  work, so a synced backup now goes through the informed confirm prompt instead.

Also corrects the misleading comment (cited a non-existent in-app "delete all
data" flow) and adds tests: backupStrHasSyncEnabled units; auto-restore
data-less / synced / disabled-sync cases; config-page last-backup line
shown/omitted cases.

https://claude.ai/code/session_01E5YzMGtfMr33qQLjMM2SGA

* fix(backup): only advance last-backup time on a real write (#7901)

Review follow-ups to the #7901 durability work:

- _backup() recorded LAST_LOCAL_BACKUP unconditionally after the platform
  writers, so the per-platform A3 near-empty guard (#7925) skipping a write
  still advanced the "Last backup" time — falsely claiming "just backed up"
  on exactly the post-eviction boot the guard protects. Platform writers now
  return whether they actually wrote; the time is recorded only then. Adds a
  spec for the skip case.

- Correct the LAST_LOCAL_BACKUP comment: the value is never passed to Log.*,
  so it does not "ride along in exported logs" — it is only surfaced in
  Settings.

- Document askForFileStoreBackupIfAvailable's blank-store precondition (empty
  op-log) on the method itself, not just inline.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-15 14:05:23 +02:00
Maikel Hajiabadi
ec70c067ad
fix(schedule): replace circle icon with wb_sunny for today button (closes #7537) (#8390) 2026-06-15 13:55:45 +02:00
Johannes Millan
82e7150821 test(sync): unblock #8331 e2e by stripping conflict-response piggyback
The #8331 transient-download regression test failed on every scheduled
run since it landed (abortedResolutionGets stayed 0). The CI trace shows
why: B's pre-upload download is correctly stripped and B's upload IS
rejected CONFLICT_CONCURRENT, but the ops-upload conflict comes back as
HTTP 200 whose body piggybacks the conflicting remote op in `newOps`.
The client LWW-resolves the conflict from that piggyback in-place, so
RejectedOpsHandlerService never issues the separate resolution-download
GET the #8331 fix guards — the path the test arms and aborts.

Fix: the route handler now also forwards B's upload POST to the real
server (keeping the rejection server-computed) and strips
`newOps`/`hasMorePiggyback` from the response. With no piggybacked ops,
the client falls back to the resolution-download GET, which the test
aborts — exercising the guarded catch. A new `strippedConflictPiggybacks`
guard asserts this fired. This stripped-piggyback condition is real in
production when the conflict falls beyond PIGGYBACK_LIMIT (500 ops).

No product code changed; behavior under test is unchanged.
2026-06-13 17:36:16 +02:00
Johannes Millan
96e15d6c55 docs(theme): correct safe-area env() comment for Android JS readers (#8283)
The comment claimed getComputedStyle resolves env() to a pixel value, so the
_patchCdkViewportForSafeArea readers pick up the real top inset. That is false:
reading an unregistered custom property whose value is env(...) returns the
unresolved token string (env()/var() are substituted only when a real property
uses the value, not when the custom property's own value is read), so
parseInt() yields 0. The CSS padding (var(--safe-area-top)) still self-corrects
at use-time; only the JS readers see 0 - the same as before this change, so no
regression. Comment-only.
2026-06-13 16:57:01 +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
8a06b75923 fix(sync): guard forceUpload and block WS download during encryption ops
Follow-ups to the #8309 in-tab sync-cycle guard:

- forceUpload() now claims SyncCycleGuard (released in finally), mirroring
  _forceDownload(), so its setLastServerSeq bookkeeping and session-validation
  latch can't race the immediate-upload / WS-download side channels. All
  forceUpload call sites are deferred snackbar/dialog callbacks that run after
  the originating cycle released the guard, so it cannot self-skip.
- WsTriggeredDownloadService now skips downloads while an encryption operation
  is in progress (isEncryptionOperationInProgress), mirroring
  ImmediateUploadService, so a WS notification can't trigger a download/decrypt
  while keys or a full-state import are mid-flight (e.g. password change, which
  does not hold the cycle guard). Read via lazyInject() to break the DI cycle
  (SyncWrapperService injects WsTriggeredDownloadService to start/stop the WS).
2026-06-13 16:57:01 +02:00
Johannes Millan
59e2a1791c
fix(sync): prevent lock-timeout from wedging op capture (#8306, #8318) (#8383)
A LockAcquisitionTimeoutError during op capture errored the whole
persistOperation$ stream: concatMap tore down and silently dropped every
buffered action, the positional capture FIFO leaked an entry so
flushPendingWrites() could never reach 0 (every sync then failed after
30s), and after NgRx's 10-resubscribe cap the effect died until reload.

Fix, bundled with the #8318 cleanup:
- Replace the positional FIFO queue with a pending counter. The
  meta-reducer increments it; the effect decrements it in a `finally`
  (writeOperationFromEffect), so a thrown write can never leak the flush
  signal. The decrement runs after the write commits + lock releases,
  preserving the flush commit-ordering invariant.
- The effect catches per write so one failure never tears down the shared
  stream (the resubscribe-death and silent-drop fixes).
- entityChanges is now computed in the write path via the pure
  extractEntityChanges(); the `[]` field is still emitted (Android reads
  it; isMultiEntityPayload requires it).
- writeOperation keeps its throw for the #7700 deferred retry loop
  (that path bypasses the wrapper and is not counted). This also
  structurally removes the #8307 double-dequeue.

Adds operation-log-effect-stream-survival.regression.spec.ts (stream
survives lock timeout, counter drains on always-fail, survives >10
failures) and updates the capture/flush/integration specs + sync docs.
2026-06-13 16:14:48 +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
d2367e3bac test(sync): deterministically provoke CONFLICT_CONCURRENT in #8331 e2e
The held-POST + re-entrant A-sync ordering was still racy: B's upload was
accepted (200) instead of rejected, so the resolution-download catch under
test never ran and abortedResolutionGets stayed 0 (runs 27465357634 and
27467427713).

Switch to a deterministic hybrid that keeps the real server conflict:
- pre-sync A so its concurrent op is unconditionally on the server;
- strip ops from B's pre-upload download (return the REAL response with an
  emptied ops list) so B never merges A's edit and uploads its original
  concurrent op -> the real server computes a genuine vector-clock
  CONFLICT_CONCURRENT;
- abort every post-upload resolution GET to drive the guarded catch.

Pin latestSeq back to the request's sinceSeq on the stripped download: the
client persists lastServerSeq even on an empty download, so keeping the
advanced seq would make B skip A's edit forever and break convergence.

Add a strippedPreUploadDownloads setup guard alongside abortedResolutionGets
so a setup that fails to provoke the conflict fails loudly, not vacuously.
The countRejectedOps discriminator and end-to-end convergence are unchanged.
2026-06-13 16:02:54 +02:00
Johannes Millan
72129f6b40 fix(focus-mode): strip orphaned {{cycle}} placeholder from break titles
The focus-mode overhaul (#7586) dropped the {{cycle}} variable from the
English LONG_BREAK_TITLE/SHORT_BREAK_TITLE strings, since the cycle is now
rendered separately as digits below the label. The break label is shown via
| translate with no interpolation params, so the orphaned {{cycle}} that
remained in all 27 locale files rendered as literal text.

Strip the '- <cycle> {{cycle}}' suffix from both keys in every locale to
match the English source; the cycle number is still shown by the cycle
counter.
2026-06-13 15:40:22 +02:00