Commit graph

13552 commits

Author SHA1 Message Date
John Costa
32dbc95ed9
fix(persistence): increase IndexedDB retry window for session-restart locks (#7220)
* fix(persistence): increase IndexedDB retry window for session-restart locks

On Linux desktop environments (especially Flatpak with autostart),
logging out and back in can leave stale LevelDB locks for 5-15+
seconds. The previous retry window (~3.5s) was too short to outlast
this, causing "Database Error - Cannot Load Data" on every
logout/login cycle.

Increase IDB_OPEN_RETRIES from 3 to 5 and IDB_OPEN_RETRY_BASE_DELAY_MS
from 500ms to 1000ms, giving a total retry window of ~31 seconds.

Fixes #7191

* fix: update stale retry timing comments to match new constants

* fix(persistence): gate long IDB retry window on lock-related errors

_ensureInit() is awaited by every op-log read/write, and the hydrator
auto-reloads on IndexedDB open errors. Applying the ~31s retry window
to every error class could create a reload -> wait -> reload loop that
feels like a hang. Classify the error on first failure and fall back
to a short retry budget (IDB_OPEN_RETRIES_NON_LOCK=2, ~3s window) for
errors that don't look lock-related. InvalidStateError and messages
containing "backing store" still get the full window used for stale
LevelDB locks on Linux desktop session restart. See #7191.

The spec is hardened to assert concrete constant floors rather than
recomputing the backoff formula (which only proved the formula matched
itself). Also adds unit tests for the classification helper.

* fix(persistence): address review feedback on IDB retry split

- Correct stale "auto-reload" rationale in op-log-errors.const,
  operation-log.const, operation-log-store.service, and archive-store.service:
  the hydrator shows a blocking alert dialog on IndexedDBOpenError, it does
  not auto-reload. The fail-fast argument now reflects the real reason —
  every op-log read/write awaits _ensureInit(), so a 31s wait on a non-lock
  error blocks the subsystem for 31s before the alert dialog surfaces.
- isLockRelatedIdbOpenError now checks `err instanceof DOMException || err
  instanceof Error` before reading .name/.message, mirroring the pattern in
  the sibling isConnectionClosingError. In some Electron / older runtimes
  DOMException does not satisfy instanceof Error.
- Added behavioral tests for _openDbWithRetry that spy on a new
  _openDbOnce testing seam: generic errors take exactly
  1 + IDB_OPEN_RETRIES_NON_LOCK attempts, InvalidStateError takes up to
  1 + IDB_OPEN_RETRIES, and a lock-error followed by success resolves.
- Bumped IDB_OPEN_RETRIES_NON_LOCK from 2 to 3 to match master's pre-PR
  retry count (1s + 2s + 4s = ~7s ceiling). Framing this as "keep master's
  existing retry count for non-lock errors, add long retry only for lock
  errors" is a lower-risk scope. Adjusted the const spec's upper-bound
  assertion accordingly.
- Replaced the "retries 1-5" hardcoded schedule comment in
  operation-log-store.service and archive-store.service with a formula-based
  comment so the two retry budgets can't drift out of sync with reality.
- Added two DOMException-shaped test cases to
  operation-log.const.spec.ts covering the new predicate branch.

* docs(persistence): clean up rot-prone worked examples in retry docstrings
2026-04-19 19:20:23 +02:00
Johannes Millan
bbd4b41b12
test: clear selectOverdue/LaterToday selector overrides across specs (#7274)
The new work-view.component.spec.ts uses provideMockStore with
overrideSelector for selectOverdueTasksWithSubTasks (and
selectLaterTodayTasksWithSubTasks). overrideSelector calls setResult(),
which persists across spec files and is only cleared by clearResult().

When task.selectors.spec.ts runs after work-view.component.spec.ts, the
persisted override ([] in most cases) caused four selectOverdueTasksWithSubTasks
assertions to fail with "Expected 0 to be 1".

Extend the defensive cleanup in the selectors spec's beforeEach to also
clear and release these two selectors, matching the existing pattern
documented in the same block.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 19:00:17 +02:00
Johannes Millan
c9fdf42929
fix(work-view): keep task selected when group/filter shows cross-context tasks (#7272)
* fix(work-view): keep task selected when group/filter shows cross-context tasks

When Group by project/tag or cross-context filters pulled tasks from other work
contexts into the view, tapping one of those tasks dispatched setSelectedId(id)
but an effect in WorkViewComponent immediately reset it to null because the
task was not in the current context's undone/done/later/overdue/backlog lists.
On mobile this opened the bottom sheet with no task-detail-panel content,
appearing as a blank screen.

Also consult customizedUndoneTasks().list so tasks surfaced via the customizer
keep the selection alive and the detail panel can render.

Fixes #7269

https://claude.ai/code/session_019AwFGKFEEEcBqjp7HdhU17

* test(work-view): cover selected-task retention when customizer shows cross-context tasks

Add unit tests that mirror the constructor effect in WorkViewComponent
responsible for deselecting the currently selected task. The tests pin
the fix for #7269: when the task-view customizer surfaces tasks from
other work contexts, the selection must be retained so the detail panel
can render.

Covers:
- fix: task only present in customizedUndoneTasks.list must NOT trigger
  setSelectedId(null), including the subtask-traversal case
- regression: task missing from every list (including customizer) must
  still deselect; customizer list being undefined/empty must not mask
  deselection
- existing behaviour: null selectedTaskId is a no-op; tasks in undone /
  done / laterToday / backlog / overdue (on TODAY_TAG) keep selection;
  overdue tasks on non-TODAY contexts still deselect

The component has a large dependency surface and instantiates several
signals from observables in its constructor, so the test mirrors the
effect body verbatim and drives it with plain inputs rather than
standing up the full TestBed graph.

https://claude.ai/code/session_019AwFGKFEEEcBqjp7HdhU17

* test(work-view): upgrade retention spec to drive the real component via TestBed

Replace the logic-mirror helper with a TestBed-based harness that
instantiates the real WorkViewComponent with an overridden template and
stub providers, then drives its constructor effect through the signal
inputs. Any future change to the effect body now fails the test rather
than the mirror needing to be kept in sync by hand.

https://claude.ai/code/session_019AwFGKFEEEcBqjp7HdhU17

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-19 16:46:20 +02:00
Johannes Millan
0dd97e122d feat(misc): extend default start page to projects, planner, schedule, boards
Widens MiscConfig.defaultStartPage from number to number | string so users
can pick any project as their startup screen in addition to Today (0),
legacy Inbox (1), Planner (2), Schedule (3), or Boards (4). Builds on
#6354.

- New StartPageSelectComponent (Formly type start-page-select) lists
  built-in pages plus all non-archived, non-hidden projects. A legacy
  Inbox option is rendered only while the stored value is still 1 so
  existing users see their selection without any silent config write.
- Feature-gated options (Planner/Schedule/Boards) are hidden from the
  dropdown when the corresponding appFeatures flag is off.
- DefaultStartPageGuard validates the selection against current state:
  project must exist and not be archived or hidden; feature pages only
  route when enabled; empty strings, missing projects, and disabled
  features all fall back to Today instead of stranding the user.
- Deleting a project clears misc.defaultStartPage alongside the existing
  tasks.defaultProjectId cleanup so a stale pointer can't linger.
- Unit tests for DefaultStartPageGuard cover every branch (including
  empty-string and archived/hidden regressions) and the new effect
  cleanup paths.
2026-04-18 23:24:19 +02:00
Johannes Millan
3ed287516a fix(archive): skip updateTasks dispatch for empty updates (#7268)
Editing a recurring task schedule when no archive instances exist
dispatched TaskSharedActions.updateTasks with tasks: [], tripping the
operation-log validator with "invalid entityId/entityIds (undefined)".
Guard the batch dispatch on updates.length > 0 so the empty-array path
is a no-op for all three callers that route through updateArchiveTasks.
2026-04-18 23:24:19 +02:00
Johannes Millan
404c68127a 18.2.3 2026-04-18 23:24:19 +02:00
aakhter
baa97ec4b4
fix: harden archive payload repair and validation (#7248)
* fix: harden archive payload repair and validation

* refactor: address PR review feedback on archive payload repair

- Sync subTaskIds with surviving subtasks in sanitizeTasksForArchiving so
  the archived parent cannot carry dangling references when subtasks are
  dropped during sanitization.
- Extract triple-duplicated valid-id predicate into shared
  op-log/validation/is-valid-entity-id.ts and reuse from
  validate-operation-payload, data-repair, and archive.service.
- Consolidate task sanitization into archive.service as the single source
  of truth (it also covers writeTasksToArchiveForRemoteSync). Removes the
  inline sanitizer block from task.service.moveToArchive.
- Replace console.warn / console.error in task.service.moveToArchive with
  TaskLog.warn / TaskLog.err so records flow through the app log.
- Add spec covering the subTaskIds sync behavior; update task.service
  spec to reflect archive.service-owned filtering.

All 8413 tests pass; lint clean.

* refactor(archive): avoid logging full task and tighten sanitizer spec

- task.service.moveToArchive now logs only the task id when it has to
  wrap a single task into an array. TaskLog output is exportable, so
  dumping the full task object risked leaking user-authored titles/notes.
- archive.service.spec asserts the archived parent's subTaskIds is empty
  after malformed subtasks are dropped, pinning down the sanitizer's
  subTaskIds sync behavior.

---------

Co-authored-by: Aamer Akhter <aamer_akhter@users.noreply.bitbucket.org>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-04-18 23:18:20 +02:00
Milo Ivir
7c53431945
Update Croatian translation (#7148)
* Update Croatian translation

This update corrects the current Croatian translation (see #4964).
The update is based on the English version of the 2nd April 2026.

Please update as soon as possible, because of the above metioned issue!
Thank you.

* Update Croatian

Corrected translation errors found by @johannesjo. See issue #7148.
I hope got them all!

* Update Croatian translations in hr.json

Corrected SyntaxError (missing comma) and updated translation string

* fix(i18n): apply review fixes to Croatian translation

- Restore <strong> and unsynced-changes warning in SUPER_SYNC FORCE_OVERWRITE_CONFIRM
- Add 26 keys added to en.json since PR base (BOARDS sort/tag-match, SYNC errors, FOCUS_MODE sounds, PLUGINS size limits, COPY_URL)
- Remove renamed GCF.FOCUS_MODE.L_IS_PLAY_TICK (superseded by L_FOCUS_MODE_SOUND)

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-18 23:17:22 +02:00
Alan Płócieniak
626db33e3c
feature(schedule): persist selected time view (#6473) 2026-04-18 21:04:18 +02:00
Johannes Millan
0065d291cc
fix(task-view-customizer): reset sort/group/filter on work context change (#7263)
* fix(task-view-customizer): reset sort/group/filter on work context change

The TaskViewCustomizerService persisted its sort/group/filter options in
localStorage and never reset them when the active work context changed,
causing customizations to leak across projects, tags and the Today view.
Users reported that switching to a project or tag still applied the
previous filter, forcing a manual "Reset all" click.

Subscribe to activeWorkContextTypeAndId$ (skipping the initial emission so
persisted state survives across reloads in the same context) and call
resetAll() whenever the context changes.

Fixes #7262

* refactor(task-view-customizer): persist sort/group/filter per work context

Replace the three global localStorage keys with a single map keyed by
work-context (`${type}:${id}`). Each context now has its own independent
customization state — filters set in Project A stay with Project A, and
returning to it restores them. This replaces the previous blanket
"reset on context change" fix, which neutralized persistence entirely
after the first context switch.

Orphaned LS keys (TASK_VIEW_CUSTOMIZER_SORT/GROUP/FILTER) are dropped;
users will lose any previously-persisted state once, which was already
unreliable in 18.2.0+ due to #7262.

Refs #7262

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-18 18:55:12 +02:00
Johannes Millan
47c52dfa87 18.2.2 2026-04-17 22:51:53 +02:00
Johannes Millan
3edbcdb357 style(boards): remove unused eslint-disable directives in specs 2026-04-17 22:34:37 +02:00
Johannes Millan
b9d6dc6d86
test(e2e): update Show/hide task panel button label (#7260)
* test(e2e): update Show/hide task panel button label

The TOGGLE_DETAIL_PANEL translation was renamed from "Show/Hide
additional info" to "Show/hide task panel" in dd789d2, but e2e
selectors still referenced the old string, causing every test that
opens the task detail panel to time out.

https://claude.ai/code/session_011mX4Pr24S42svmA5j7fRux

* 18.2.1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 22:19:22 +02:00
Johannes Millan
9b7d34100e 18.2.0 2026-04-17 19:48:55 +02:00
Johannes Millan
d9158d6adb fix(sync): treat unreadable legacy __meta_ as legacy detected
The legacy pfapi probe (1f5184f6e7) only mapped
RemoteFileNotFoundAPIError to LegacySyncFormatDetectedError. WebDAV
surfaces a corrupt or empty __meta_ body as InvalidDataSPError, so
those users bypassed the friendly force-overwrite snack and saw an
unhandled data error instead. Presence — even unreadable — still
proves a v16.x client touched the target, so set legacyFileFound on
InvalidDataSPError and let the existing warning path take over.

Also widen the Dropbox and LocalFile getFileRev signatures (and the
Dropbox getMetaData call under it) to accept `string | null`, matching
the interface and the `null` argument the legacy probe already passes.
No runtime behaviour change — Dropbox never serializes the revision
for metadata lookups and LocalFile only logs it.
2026-04-17 19:45:41 +02:00
Johannes Millan
fddeb581dd refactor(boards): extract rewriteTagIdsForPanel helper
Move the tag-rewrite logic out of BoardPanelComponent.drop() into a
pure rewriteTagIdsForPanel() in boards.util.ts. The component drops
from 421 to 396 lines and the tricky 'any' vs 'all' include/exclude
branches become directly unit-testable.

Cover the extracted helper with cases for each match-mode permutation
plus a frozen-input no-mutation guard, and add the first drop() specs
for board-panel.component covering sorted-mode early-return,
AND-excluded cross-panel drop, and OR-include add.
2026-04-17 19:45:28 +02:00
Johannes Millan
710ed9b5e4 fix(keyboard): navigate Shift+T to Today tag directly
goToTimeline was navigating to /timeline, a route not declared in
app.routes.ts. It fell through to the wildcard and DefaultStartPageGuard,
which lands on Today only when the user's default start page is TODAY.
For INBOX-default users the shortcut jumped to INBOX, contradicting the
"Go to Today" label shipped in #7101.

Navigate to /tag/TODAY/tasks directly so the behaviour matches the label
regardless of the configured default start page.
2026-04-17 19:45:02 +02:00
Johannes Millan
5c2645667f fix(config): restore plural for collapse/expand sub-task labels
Revert the singular COLLAPSE_SUB_TASKS / EXPAND_SUB_TASKS wording back
to plural. The shortcut handler toggles the parent task's
_hideSubTasksMode, which affects every subtask of the selection — so
the plural form accurately describes the action.
2026-04-17 19:44:56 +02:00
Johannes Millan
87f66fea9c refactor(date): centralize startOfNextDay offset behind DateService
Replaces ad-hoc `Date.now() - _dateService.startOfNextDayDiff` arithmetic
scattered across services with a small set of offset-aware helpers on
DateService, and makes the raw field private so TypeScript prevents
future regressions.

Public surface added to DateService:
  - getLogicalTodayDate(): Date — offset-aware "today"
  - getLogicalTomorrowMs(): number — logical today + 1 calendar day,
    DST-safe via setDate (naive +24h stayed on the same local date
    during fall-back transitions)
  - getStartOfNextDayDiffMs(): number — read-only accessor for the
    service-boundary case where pure utilities need the raw offset

The raw `startOfNextDayDiff` field is now `private`; TypeScript
catches both member access and destructuring from outside.

Migrated call sites (all semantic-identical except the DST fix):
  - planner.service.ts: tomorrow$ and ensureDayLoaded
  - add-tasks-for-tomorrow.service.ts: addAllDueToday/Tomorrow
    (+ offset-aware spec coverage)
  - daily-summary.component.ts: isIncludeYesterday
  - global-config.effects.ts + app-state.effects.ts: setTodayString
    payloads now read via the accessor

Preserved for sync-replay determinism:
  - isTodayWithOffset pure util in src/app/util/is-today.util.ts
  - reducers/selectors continue to take startOfNextDayDiffMs as an
    explicit argument from action payloads

Deferred with an in-file TODO: task-due.effects.ts:170 — the effect
is selector-driven (replay-sensitive per CLAUDE.md #8) and its spec is
xdescribe'd, so verification is impossible in this pass.

Documented in CLAUDE.md (Important Development Notes #14 + an
anti-patterns row).

A broader audit surfaced 10 pre-existing sites elsewhere in the
codebase that bypass the logical clock (planner daysToShow$,
argument-less getDbDateStr in pipes, simple-counter streak, plugin
counter). Those are tracked separately and not part of this refactor.
2026-04-17 18:13:02 +02:00
Johannes Millan
dd789d2520 fix(config): clarify keyboard shortcut labels (#7101)
Apply UI copy fixes for keyboard shortcut labels in en.json:
- Update wording for project backlog vs task backlog
- Swap GO_TO_SCHEDULE/GO_TO_SCHEDULED_VIEW labels so they match the
  routes they actually navigate to (Today vs Schedule)
- Replace "Toggle" with "Start/stop" for time tracking shortcuts
- Tidy task panel and task completion wording

Plus follow-ups from issue #7074:
- Make TOGGLE_DETAIL_PANEL tooltip match the new "task panel" wording
- Use singular form for COLLAPSE/EXPAND_SUB_TASKS
- Drop misleading "Press Enter to save" from KEYBOARD.HELP

Closes #7074
Co-authored-by: Symon Baikov <symonbaikov@users.noreply.github.com>
2026-04-17 18:13:02 +02:00
Johannes Millan
5e0c60e977 feat(boards): add tag AND/OR match mode and generalized sort options
Addresses #4688, #4822, #4132 (sort) and #5175, #5810 (tag match) from the
Kanban feature collection in #7245.

- Panel filter: includedTagsMatch and excludedTagsMatch (all/any) with
  current-behavior defaults (all / any) — no migration needed.
- Generalized sortBy (dueDate, created, title, timeEstimate) + sortDir;
  replaces the scheduled-only sortByDue toggle. Legacy sortByDue is
  migrated on load and scrubbed on save by sanitizePanelCfg in the
  boards reducer. Unknown sortBy values are dropped defensively.
- buildComparator uses timezone-safe dueDate logic matching task.selectors.
- Manual drag-reorder disabled (cdkDropListSortingDisabled) when auto-sort
  is active; cross-panel drag still works.
- Cross-panel drop and inline-create write paths honor match mode:
  OR-included adds only one tag when none present; AND-excluded doesn't
  mutate (add-task-bar applies tagsToRemove blindly, so stripping would
  wrongly remove user input).
2026-04-17 18:13:02 +02:00
Johannes Millan
1f5184f6e7 fix(sync): detect legacy v16.x pfapi format and warn user instead of diverging silently
When sync-data.json is missing, check for a legacy __meta_ file before
treating the provider as a fresh start. If found, throw
LegacySyncFormatDetectedError and show an error snack with a force-overwrite
action instead of silently diverging from a v16.x device still writing the
old format.

This restores the behavior added in 791fa533e7, which was unintentionally
removed in 0e9218bd68 during a client-ID refactor. The snack copy clarifies
that force-overwrite only unblocks this device and does not clean up the
legacy files on the remote — if a v16 device syncs again, divergence resumes.

Related: #5964, #6174
2026-04-17 18:13:02 +02:00
Leandro Marques
179700ccda
chore: update electron to v41.x (#7097)
* chore: update electron to v41.1.1

* feat(start-app): clear GPU cache on Electron version change for Linux

* fix(window-decorators): disable custom window title bar for GNOME

This fixes some issues when moving and resizing the window

* refactor: Migrate url.format() (DEP0116) to file:// path approach

* refactor(start-app): remove deprecated protocol.registerFileProtocol in start-app.ts

* feat(electron-builder): update gnome content snap to gnome-42-2204 for improved compatibility and update flatpak permissions

* fix(window-decorators): improve handling of custom window title bar for GNOME

* feat(start-app): implement fallback to X11 in Snap if gnome-42-2204 runtime is unavailable

* chore: update electron to v41.1.1

* chore(package-lock): remove unused dependencies from package-lock.json

* fix(electron): move snap ozone-platform switch before app ready event

app.commandLine.appendSwitch() must be called before Chromium
initializes — after the ready event fires the GPU backend is already
running and the switch is a no-op. Move the Snap X11 fallback (defense-
in-depth for missing gnome-42-2204 runtime) to run synchronously at
startup, alongside the existing gtk-version and speech-dispatcher
switches.

* fix(electron): align IS_GNOME_DESKTOP detection with preload.ts logic

The original implementation only matched Ubuntu's GNOME session
(XDG_CURRENT_DESKTOP contains both 'gnome' AND 'ubuntu'), missing plain
GNOME on Fedora, Arch, etc. The preload.ts isGnomeDesktop() already
used the correct approach: check four environment variables with OR
logic. Align common.const.ts to match, preventing a split-brain where
the main process and renderer disagree on whether the user is on GNOME
— which would produce no title bar at all on non-Ubuntu GNOME desktops.

* fix(electron): restore v41 bump lost in merge and gate title-bar toggle

- Re-apply electron 41.2.0 + minimatch 10.2.5 override (master's 0e9218bd
  reverted the dependabot bump back to 37.10.3 while this branch's
  merge-base still contained 41.2.0, so the pre-merge diff was empty).
- Regenerate root package-lock.json accordingly.
- Drop unrelated esbuild additions from plugin-dev sub-lockfiles.
- misc-settings-form: gate isUseCustomWindowTitleBar on IS_ELECTRON &&
  !IS_GNOME_DESKTOP so the toggle does not appear in the web/PWA build.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-04-17 18:12:43 +02:00
Ivan Kalashnikov
5ffaec615d
Add list selection for 'Short Syntax' entity when editing a task (#6913)
* fix(task-title): add short syntax mention support in task editing

* refactor(mentions): extract mention configuration logic into a shared utility function

* fix(task-title): add RenderLinksPipe to imports for link hint rendering

* test(task-title): fix spec DI after mention config added

Component now injects GlobalConfigService/TagService/ProjectService
for the mention autocomplete, which indirectly require NgRx Store.
Provide lightweight mocks so the spec runs without the full DI graph.

Also drops redundant [mention]="[]" — the mention directive already
activates via [mentionConfig] alone and defaults its items to [].

* refactor(mentions): replace buildMentionConfig$ helper with root service

The helper lived in src/app/util/ but imported from features/, which
reverses the intended layering. Move to features/tasks/ and expose as
a providedIn: 'root' service so:

- every editor shares one combineLatest pipeline (shareReplay'd),
  instead of each TaskTitleComponent allocating its own,
- TaskTitleComponent drops three feature-service injections in favor
  of one scoped dependency,
- the spec mocks collapse from three service stubs to one.

* test(focus-mode): mock MentionConfigService in focus-mode-main spec

FocusModeMainComponent renders <task-title>, which now injects
MentionConfigService. The existing 3 TestBed setups did not provide
it, so the real service was instantiated, pulling TagService through
a bare Store spy without .pipe() — 9 failing tests.

Stub MentionConfigService in every setup alongside the other service
mocks. Matches the pattern used in task-title.component.spec.ts.

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-04-17 17:27:37 +02:00
Johannes Millan
bac801faf3
test(sync,calendar): add regression guards for Nextcloud and plugin dueWithTime (#7252)
- operation-sync.util.spec: assert every SyncProviderId member is
  classified as file-based or non-file-based. Prevents silent no-op
  regressions like #7242 where Nextcloud was missing from
  FILE_BASED_PROVIDER_IDS.
- calendar-integration.service.spec: verify dueWithTime is propagated
  from PluginSearchResult to CalendarIntegrationEvent during the plugin
  calendar mapping. Regression guard for #7238.

https://claude.ai/code/session_018n2niBLLnWGgv3bceiiTnB

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 00:33:38 +02:00
Johannes Millan
8c3b08e016 fix(sync): add copy-URL fallback for Dropbox auth dialog (#7139)
The "Get Authorization Code" button silently fails on some Linux
packagings (Flatpak, AUR) because shell.openExternal() can reject
without visible feedback — the wasOpened boolean check in
openUrlInBrowser has been dead code since the call was migrated from
shell.openPath to shell.openExternal (which returns Promise<void>).

- Add a "Copy URL" button to the auth dialog as a reliable fallback
  users can paste into a manually-opened browser.
- Replace the dead wasOpened check with a .catch() that logs the
  rejection reason, so future portal failures surface in electron logs.
- Add regression tests documenting the PKCE verifier/challenge
  invariants that make stale auth codes fail with "invalid code
  verifier" when the dialog is reopened.
2026-04-16 19:47:52 +02:00
Johannes Millan
05cd875dd6 fix(sync): restore periodic timer for file-based providers and add visibility-hidden trigger
Two related changes that close the data-loss window for Dropbox/WebDAV/LocalFile sync:

1. Restore the periodic interval timer (`timer(syncInterval, syncInterval)`) for
   file-based providers. The timer was added in 08fdea1772 / d6880d47da to fix
   #4783 but was unintentionally dropped in 0e9218bd68, a large 87-file commit
   whose stated purpose was data-validation and client-ID work. Without the
   timer, file-based sync only fires on user activity (focus/idle/online) — so
   changes made just before closing the app may never reach the remote.

2. Add an unthrottled `visibilitychange → hidden` trigger for non-Electron
   platforms. Previously this event was either absent (desktop browser) or
   throttled at 15 minutes alongside touch activity (iOS PWA), so backgrounding
   the app within the throttle window did not trigger a sync. Electron remains
   on its existing IPC-blocking close path (execBeforeCloseService).

Refs #7144, #4783
2026-04-16 19:47:52 +02:00
Johannes Millan
f89fe1ebc3 fix(sync): prevent Dropbox setup loop when auth is cancelled (#7131)
save() previously persisted isEnabled:true and closed the dialog before
auth completed, then called sync(). When the user cancelled the auth
code dialog, sync would fail with MissingCredentialsSPError, showing a
snack whose "Configure" action reopened the dialog — the infinite loop
reported in #7131.

Run auth first, then gate save+close on provider.isReady() when the
provider has getAuthHelper. If auth was cancelled or failed, the dialog
stays open and config is left untouched.
2026-04-16 19:47:52 +02:00
Johannes Millan
3fcc1b65d9 fix(daily-summary): honor startOfNextDay when counting today's tasks
The daily summary used calendar midnight rather than the configured
day boundary when deciding (a) whether to include the previous
calendar day's tasks in the summary, and (b) whether an archived task
counts as done today. With startOfNextDay >= 1, tasks completed just
after midnight were attributed to the wrong logical day.

Extract isWithinYesterdayMargin() as a pure, testable helper keyed on
startOfNextDay, and replace getDbDateStr(t.doneOn) === day with
DateService.isToday() in getDoneTodayInArchive.

Fixes #7157
2026-04-16 19:47:52 +02:00
Johannes Millan
e0fa507086 fix(focus-mode): apply safe-area insets in overlay for Android edge-to-edge
The focus-mode-overlay uses position:fixed;inset:0 which bypasses the
body's safe-area padding applied for native mobile. This caused the banner
to render behind the Android status bar and bottom controls to overlap
the navigation bar.

Fix: add padding-top/bottom to the overlay :host so all flex children
(header+main) are shifted below the system bars. Remove the now-redundant
var(--safe-area-top) from .close-btn and .focus-mode-select-mode which
were individually compensating for the missing host offset.
2026-04-16 19:47:52 +02:00
Johannes Millan
8e8a39b832 fix(note): linkify URLs in locked notes so markdown links stay clickable
After #7004 made isLock actually disable markdown parsing, locked notes
showed `[text](url)` as raw text. Pipe the locked-path content through
RenderLinksPipe so URLs and markdown links remain clickable without
re-enabling full markdown rendering.

Fixes #7013
2026-04-16 19:47:52 +02:00
Johannes Millan
db3bb4016f fix(plugins): load community plugins via static import (#7149)
Replaces runtime HttpClient fetch of assets/community-plugins.json with a
TypeScript JSON import. The XHR to file:// inside the Electron ASAR could
fail with "Http failure during parsing", surfacing an error snack bar on
every visit to the plugin settings. Static import bundles the data at
build time and eliminates the failure path entirely.
2026-04-16 19:47:52 +02:00
Johannes Millan
09f26c3a39 fix(ui): hide top bar context menu on context-less sections (#7165)
Planner, Schedule, Boards, and Habits override the page title but have no
work context of their own. The three-dot menu was still rendered and bound
to the underlying active context (typically Today), so clicking Settings
opened Today's settings — confusing since the title said "Planner".

Hide the entire page-title-actions group on these sections via a new
isSpecialSection computed signal.
2026-04-16 19:47:52 +02:00
John Costa
e25e10bbc3
fix(tasks): escape task IDs in querySelector to handle special characters (#7222)
* fix(tasks): escape task IDs in querySelector to handle special characters

Task IDs from Dropbox sync can contain special characters like curly
braces that are invalid in CSS selectors, causing querySelector to
throw. Use CSS.escape() on the task ID before passing it to
querySelector, matching the existing pattern in planner-plan-view.

Fixes #7219

* fix(tasks): use getElementById for single-element lookups, add CSS.escape to querySelectorAll

Switch querySelector to getElementById for task ID lookups that only
need a single element — getElementById does not parse CSS selectors so
special characters in task IDs are handled natively. Add CSS.escape to
the querySelectorAll call in task.component.ts which needs to find
multiple elements with the same task ID.
2026-04-16 18:03:45 +02:00
Asish Kumar
fec7b25f23
fix(plugins): use asset-specific size errors (#7210)
* fix(plugins): use asset-specific size errors #7202

* plugins: inline asset-specific size errors

Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>

---------

Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-16 18:02:39 +02:00
Gitoffthelawn
fd2490009c
UI Improvements (#7185)
- Fixed errors
 - Improved consistency
 - Improved readability
2026-04-16 18:01:24 +02:00
Johannes Millan
47b55156f7 fix(sync): make SnackService optional in ClientIdService to restore unit tests
SnackService depends on NgRx Store, causing 282 test failures via transitive
injection: OperationLogStoreService → CLIENT_ID_PROVIDER → ClientIdService →
SnackService → Store. Making it optional preserves the warning in production
(providedIn root) while allowing tests without NgRx to run.
2026-04-16 17:41:39 +02:00
Johannes Millan
7df43358ab fix(sync): add Nextcloud to file-based provider IDs to enable sync
Nextcloud was missing from FILE_BASED_PROVIDER_IDS, causing
WrappedProviderService to log "Unknown provider type: Nextcloud" and
return null, making sync a silent no-op for all Nextcloud users.

Fixes #7242
2026-04-16 17:41:39 +02:00
Johannes Millan
663f0bf00a style(sync): fix prettier formatting in snapshot-upload spec 2026-04-16 17:41:39 +02:00
Johannes Millan
09f5ced2c9 fix(sync): prevent data loss for fresh clients from upload retry race condition
Replace the unsafe upload retry loop with a single-attempt strategy that
correctly handles concurrent uploads to file-based sync providers (WebDAV,
Dropbox, local file).

The old _uploadWithRetry called getStateSnapshot() inside a retry loop while
concurrent client ops could be in recentOps but not yet applied to NgRx,
creating a stale snapshot that caused data loss for fresh-bootstrap clients.

The new _uploadWithMismatchFallback:
- Attempts upload once with the current snapshot
- On UploadRevToMatchMismatchAPIError: re-downloads to distinguish
  - Same rev (WebDAV ETag glitch): force-uploads the already-built uploadData
  - Different rev (genuine concurrent upload): throws so the next sync cycle
    downloads the concurrent ops first, then uploads with a consistent snapshot
- SyncWrapperService handles the thrown error as transient (UNKNOWN_OR_CHANGED,
  no error snackbar)

Removes snapshotSyncVersion, extraOpIds, retry constants, and the retry-loop
infrastructure that was part of the broken approach.
2026-04-16 17:41:39 +02:00
Johannes Millan
5324512ae5 fix(reminder): prevent close-animation race and fix merge syntax in reminder module
Three bugs fixed:

1. dialog-view-task-reminders: cancel onRemindersActive$ subscription
   immediately in _close() rather than waiting for ngOnDestroy (~300ms
   later after animation). A worker tick arriving during the close animation
   could call taskIds$.next() on an OnPush component being torn down by CDK,
   corrupting Angular change detection. Guard _close() with MatDialogState to
   prevent double-close.

2. reminder.module.ts: fix merge([obs1, obs2]) → merge(obs1, obs2). RxJS 7
   treats the array as a single ObservableInput, emitting Observable objects
   as values instead of subscribing to them, so the add-task-bar wait never
   actually waited.

3. reminder.module.ts: remove dead .afterClosed() chain that was never
   subscribed to (no-op since the very first TASK reminder implementation).

Adds tests for the close-animation race condition covering the exact sequence
from issue #7189: Task A play → Task B reminder during animation window.

Fixes #7189
2026-04-16 17:41:39 +02:00
Johannes Millan
ad97930730 fix(tasks): normalize timeSpentOnDay at data boundaries to prevent crash
Fixes #7104. Legacy archived and persisted tasks can have
timeSpentOnDay: undefined when created before the field existed.
Rather than guarding every individual access site with optional
chaining (which TypeScript won't enforce since the type declares the
field as required, making guards invisible and prone to being reverted),
normalize to {} once at the data boundary so all consumers can trust
the invariant.

Pattern mirrors normalizeCountOnDay in simple-counter.reducer.ts:
- TaskArchiveService.load() and loadYoung(): normalize before returning
  raw IndexedDB data to any consumer (worklog, daily-summary, heatmap…)
- task.reducer.ts loadAllData handler: normalize active tasks on app
  startup, the same way simple-counter does for countOnDay

Also fix two HIGH-risk unguarded reducer sites identified by code
review that the previous optional-chaining commits missed:
- addSubTask: Object.keys(task.timeSpentOnDay || {}) — crashes on the
  first subtask of a parent when the new task has undefined here
- roundTimeSpentForDay: spentOnDayBefore || {} — crashes when rounding
  time for a task with undefined timeSpentOnDay

Note: normalization is in-memory only; the raw IndexedDB data is not
written back. For users with legacy corrupt data the slow path runs on
every archive load. A follow-up one-time migration would permanently
heal the stored data so the scan becomes a no-op.
2026-04-16 17:41:39 +02:00
Johannes Millan
7b58e1f521 fix(caldav): correctly map DTSTART/DUE dates and fix undated tasks appearing in Today
- All-day VTODO (VALUE=DATE) now maps to dueDay/deadlineDay instead of
  dueWithTime with midnight timestamp, fixing incorrect time display
- Tasks with no DTSTART no longer default to today; dueDay is explicitly
  set to null to suppress the issue.service default, so undated CalDAV
  tasks go to backlog instead of flooding Today
- CalDAV DUE property now maps to deadlineDay/deadlineWithTime per RFC 5545
- Counterpart fields always nulled on transitions (e.g. timed → all-day)
  to prevent stale dueWithTime persisting after server-side changes

Fixes #7125
2026-04-16 17:41:39 +02:00
Johannes Millan
fbe81b6a7c test(plugins): add unit test for PluginAPI.showIndexHtmlAsView() 2026-04-16 17:41:39 +02:00
Johannes Millan
d5292bdb90 fix(electron): cancel on close prompt no longer navigates to daily-summary
When the user clicked Cancel on the "quit without finishing your day?"
dialog, the app incorrectly navigated to the daily-summary page. Cancel
now simply keeps the app open on the current page with no side-effects.
Extracted tap logic into _handleCloseDecision() for testability and added
unit tests covering OK, Cancel, and no-done-tasks scenarios.

Fixes #7147
2026-04-16 17:41:39 +02:00
Johannes Millan
4aa75a070a fix(focus-mode): default isPauseTrackingDuringBreak to true to prevent break time counting as work
Pomodoro break time was silently accumulated into task timeSpent because
the default for isPauseTrackingDuringBreak was false, so currentTask was
never unset when a break started and every tick kept adding duration.

The pause/resume infrastructure already existed; only the default was wrong.
Existing users with the flag explicitly saved as false are unaffected.

Fixes #7181
2026-04-16 17:41:39 +02:00
Johannes Millan
0e9218bd68 fix(sync): handle data validation errors and improve client ID management
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper
- Handle corrupted remote data gracefully instead of throwing
- Add getOrGenerateClientId() as unified entry point, eliminating dual injection
  of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption,
  and sync-hydration services
- Use crypto.getRandomValues() instead of Math.random() for client ID generation
- Warn user when stored client ID is invalid and must be regenerated
- Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)
2026-04-16 17:41:39 +02:00
Serif
91658f1230
feat: add missing Turkish translations (#7243) 2026-04-16 17:31:03 +02:00
John Costa
27358ab251
fix(search): prevent overlapping search results on narrow screens (#7237)
mat-list-item applies a fixed height which doesn't grow when
flex-wrapped content exceeds it. Override with height: auto and
min-height so items expand to fit wrapped subtask breadcrumbs,
icons, and task titles on narrow viewports.

Fixes #7223
2026-04-16 15:16:55 +02:00
Johannes Millan
7cca0f4158
fix(calendar): schedule tasks created from plugin calendar events with start time (#7238)
When creating a task from a Google Calendar plugin event with a specific
start time, dueWithTime was dropped during the PluginSearchResult to
CalendarIntegrationEvent conversion, causing the task to be added with
dueDay instead of being scheduled via addAndSchedule().

https://claude.ai/code/session_01KRGjzZan7EYsgnXW4Rr1AA

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-16 03:06:18 +02:00