* 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
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>
* 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#7269https://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>
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.
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.
* 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>
* 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>
* 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>
* 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>
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.
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.
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.
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.
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.
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>
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).
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
* 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>
* 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>
- 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>
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.
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
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.
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
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.
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
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.
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.
* 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.
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.
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
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.
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
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.
- 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
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
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
- 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)
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
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>