mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
498 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a6489e17a8 |
fix(plugins): reject path-segment ids in nodeExecution consent gate
Multi-agent review of the Phase 1 gate found the uploaded plugin id — used as a path component in the bundled-manifest existsSync probe — was not rejecting path separators or dot-segments. Impact was bounded (the verified-builtin branch re-validates with the strict kebab regex before any read, so no code exec / file read / dialog spoof), but `..` / `/` left a filesystem-existence oracle and rendered misleadingly as the dialog "Plugin ID". assertSafePluginId now rejects `/`, `\`, `.`, `..`. Also from review: factor the shared Allow/Deny dialog shell, and correct the denied-cache comment (the existing token short-circuit handles the multi-call-site case; the cache only makes a denial sticky across a later non-interactive grant re-entry). Documents the uploaded-id constraints. Refs #8512 |
||
|
|
839deea1e9 |
feat(plugins): allow uploaded nodeExecution behind consent gate
Re-open the nodeExecution permission for uploaded/community plugins (previously built-in only, #8205) behind the existing main-process consent dialog. Phase 1 of #8512; unblocks the Super Productivity MCP plugin (discussion #8385). - Main process sanitizes the attacker-controlled plugin id and the self-declared name/version before they reach the consent dialog or the grant map (control/bidi/whitespace rejected, length-capped). - Bundled vs uploaded is decided by the on-disk manifest, never a renderer-supplied flag, so uploaded code can't borrow a built-in plugin's verified name. - Uploaded-plugin dialog anchors on the validated id, flags the plugin as unverified third-party with full machine access / no sandbox, and defaults to Deny. - Revoke is main-authoritative by (pluginId, webContents) so a re-upload reusing an id can't inherit a live session grant. - Consent stays session-scoped: an in-memory, never-synced denied set prevents re-prompt storms; deny keeps the plugin enabled but fails node calls closed until re-enable or restart. Refs #8512 #8385 |
||
|
|
87846ad83d
|
fix(android): keyboard + status-bar follow-ups for edge-to-edge (#8508) (#8548)
* fix(android): size fullscreen markdown/notes dialog above the keyboard (#8508) The fullscreen markdown editor (project & task notes) is position:fixed; height:100% but its keyboard rule subtracted --keyboard-overlay-offset, which is set only on iOS. On Android the rule was a no-op, so with the IME open the dialog kept full height (content behind the keyboard) or inherited the squashed sliver, leaving the toolbar + textarea + Close/Save mashed to the top. Use the resize-detecting --keyboard-height for the Android/mobile-web case (0 once the window resized, the obscured amount otherwise), mirroring the add-task bar and .app-container; keep the iOS --keyboard-overlay-offset path as a second, source-order-later rule (equal specificity, wins on iOS). On a device that resizes (--keyboard-height == 0) it is identical to the old rule, so it is never worse than before; it fixes the API >= 30 no-resize/VisualViewport-shrink case and composes with the SDK < 30 native fix (#8528). * fix(android): inset the header below the status bar on API < 30 (#8508) Under enforced edge-to-edge (targetSdk 36) the WebView extends under the status bar, but on the API < 30 WebView `env(safe-area-inset-top)` resolves to 0 (old WebViews map only display cutouts into safe-area insets, not the status bar), so `--safe-area-top` was 0 and the web header overlapped the status bar on Android 9. The web side cannot tell "edge-to-edge under the status bar" from "already natively inset" (env() is 0 in both), so a web-only fallback would double-count. Measure the overlap natively instead: in the existing keyboard layout listener (gated SDK < 30) compute max(0, rect.top - webViewTopOnScreen) — visible-frame top (status-bar height, reliable on API 28) minus the WebView's on-screen top (0 when edge-to-edge, == status-bar height once inset) — and publish it as the `--android-status-bar-overlap` CSS var (physical px -> CSS px, deduped). The web folds it in with max(env(safe-area-inset-top, 0px), var(--android-status-bar- overlap, 0px)): never a sum so it can't double-count, and on API >= 30 the var is never set so `max(env, 0) = env` keeps the verified behavior. JS readers still parse the token to 0, preserving the #8283 overlay scoping. Needs on-device validation on API < 30 and a no-op check on API >= 30. * fix(android): stop double-counting safe-area-top in note dialog height (#8508) The fullscreen note/markdown dialog subtracted --safe-area-top from its keyboard-open height while ALSO applying padding-top: --safe-area-top. Since :host is border-box (global * { box-sizing: border-box }), the padding is already inside height: 100%, so subtracting --safe-area-top again double-removed the top inset and left a --safe-area-top-sized gap between the Close/Save controls and the keyboard. Invisible while --safe-area-top was 0 on API < 30; it surfaced once the status-bar fix made it non-zero (and was latent on API >= 30 where env() already gave a non-zero value). Drop the - --safe-area-top term from the Android rule so the controls sit flush above the keyboard while the toolbar stays below the status bar. iOS keeps its own override (different keyboard runtime, unverified on device) and is flagged for separate checking. * fix(android): re-publish status-bar overlap after web reload (#8508) Review follow-ups to the #8508 keyboard/status-bar fixes: - The native --android-status-bar-overlap lives only as an inline style on the document, so a web-side window.location.reload() (language change, PWA update, sync-conflict recovery) wipes it while the dedupe field survives on the Activity -> the unchanged value is skipped and the header overlaps the status bar again on the WebView < 140 / API < 30 tail. Reset lastStatusBarOverlapCssPx in flushPendingShareIntent() (runs on every frontend (re)load) so the next layout pass re-publishes it. - Dialog keyboard rule: scope the Android rule with :not(.isIOS). iOS carries both isNativeMobile and isIOS and sets --keyboard-height non-zero, so the two equal-specificity rules both matched and iOS correctness depended on source order; they are now mutually exclusive and order-independent. - Declare --android-status-bar-overlap: 0px in :root for discoverability, alongside the other dynamic vars. |
||
|
|
431bc50da9
|
fix(sync): help Nextcloud users find their user ID + auto-detect it (#7617) (#8547)
* docs(sync): point Nextcloud user-ID lookup at the WebDAV URL (#7617) The field hint, the 404 test-connection message, and the wiki all told users to find their user ID under 'Settings -> Personal info' / 'your Files URL'. Both are unreliable: Personal info does not clearly surface the uid, and a folder's address bar shows a folder ID, not the user ID (reporter followed it and got a folder ID). Redirect all three to the authoritative source: Files -> settings gear (bottom-left) -> the WebDAV URL '.../remote.php/dav/files/<user-id>/'. * feat(sync): auto-detect Nextcloud user ID via OCS (#7617) The Nextcloud WebDAV files path needs the account's internal user ID, which differs from the email/login name people enter and is awkward to find by hand — the root cause of the recurring '404 / connection test failed' reports. Add a 'Detect user ID' button to the Nextcloud sync config that asks the server for it. - packages/sync-providers: discoverNextcloudUserId() calls the OCS endpoint /ocs/v2.php/cloud/user (OCS-APIRequest header) authenticated with login + app password and returns ocs.data.id. A 401 reports bad credentials (cleanly distinct from the 404 wrong-user-id path); a 200 that isn't an OCS payload reports 'not a Nextcloud/OCS server'. A missing/wrong URL scheme is refused up front (matches the provider's own _cfgOrError check) so credentials never hit a schemeless host. - Dialog: button fills the Username field with the detected ID. To keep auth working, if 'Login name' was empty and the user had typed their login into 'Username', that login is preserved into 'Login name' before Username is overwritten with the ID. Result handling split into _applyDetectedUserIdResult for unit-testability. - Additive and optional: generic WebDAV and the save/sync path are untouched, no synced data shapes change. - Tests: 7 package specs (success, trailing-slash, login fallback, 401, non-OCS 200, missing id, scheme guard) + 6 dialog specs (login guard, fill+confirm, 3 login-preservation cases, failure). |
||
|
|
0a0723521b
|
feat(azure-devops): add optional WIQL override for auto backlog import (#8516)
* feat(azure-devops): add optional WIQL override for auto backlog import Adds an optional autoImportWiql config field to the Azure DevOps issue provider plugin. When set, it fully replaces the generated auto-import backlog query, so the user controls scope, state filtering and ordering (e.g. to filter by iteration path, area path or work item type, or to match custom done-state names). When empty, behavior is unchanged from today's scope-based query, so it is fully backward compatible. This mirrors the Jira provider's autoAddBacklogJqlQuery (a full query the user owns) rather than appending a fragment, which can only narrow the hard-coded English done-state exclusion and cannot accept a real exported WIQL Select..From..Where statement. Adds a vitest suite covering the default scopes, quote escaping, the verbatim override and the blank-fallback, wires the plugin into the plugin-tests CI matrix, and updates the issue integration comparison wiki. Closes #7674 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DY1ESWymmU9x9ykLNaRHGH * test(azure-devops): use node test env, drop unneeded jsdom dependency The plugin tests exercise no DOM API, so vitest's default node environment is sufficient (matching the clickup and google-calendar provider plugins). Removing jsdom prunes ~530 lines of transitive devDependencies from the lockfile. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Johannes Millan <johannes.millan@gmail.com> |
||
|
|
bb3ed74480
|
feat(android): migrate edge-to-edge to built-in SystemBars (#8543)
* feat(android): migrate edge-to-edge to built-in SystemBars Replace @capawesome/capacitor-android-edge-to-edge-support with Capacitor 8's built-in SystemBars (insetsHandling: 'css'). SystemBars handles edge-to-edge insets + IME padding on WebView >= 140 / API >= 35; the WebView < 140 / API < 35 tail is covered by env() fallback plus a native keyboard shim (adjustWebViewHeightForKeyboardBelowApi30), gated to WebView < 140 so it does not fight SystemBars' own IME padding. - config: drop EdgeToEdge plugin config + includePlugins entry + dep; remove the plugin from the generated gradle registration - theme: stop JS-writing --safe-area-inset-* on Android (SystemBars owns them; the SCSS env() fallback preserves the #8283 top fix); bars are now transparent with the theme color painted behind them via setWebViewBackgroundColor, since SystemBars has no bar-color API - StartupOverlayManager: derive the overlay inset from the system-bar insets instead of the removed plugin's WebView margin Spike: TypeScript verified (tsc clean, lint clean). Kotlin and on-device behavior NOT yet validated (gradle unavailable in this environment). Requires `npm install` + `npx cap sync android` and the device matrix in docs/plans/2026-06-22-android-systembars-migration-corrected.md before merge. * build(deps): drop @capawesome edge-to-edge from lockfile Reconcile package-lock.json with the package.json change in the edge-to-edge -> SystemBars migration (removes only the @capawesome/capacitor-android-edge-to-edge- support entry). Generated by `npm install`. * docs(android): clarify SystemBars band model from implementation review Comment-only + plan-doc follow-up to the multi-agent review of the migration: - Correct the band model in the inset comments: SystemBars *injects* --safe-area-inset-* only on API >= 35; WebView >= 140 is native env() passthrough (no injection below API 35). The SCSS var(..., env()) fallback covers every band. (capacitor.config.ts, global-theme.service.ts) - Fix a stale comment in StartupOverlayManager.show() that still described the removed @capawesome plugin insetting the WebView. - Record the device-validation-only findings in the plan doc (API>=35/WebView<140 double-count corner, env vs var consumer split, API 30-34/WebView<140 IME owner, CDK overlay top shift) so they are checked on the matrix, not blind-fixed (a blind shim extension would risk re-creating #8508). No behavior change. |
||
|
|
20e85a2a19
|
refactor(tasks): restructure reminder dialog footer into primary + overflow (#8517)
* refactor(tasks): restructure reminder dialog footer into primary + overflow The footer showed up to four visually identical stroked buttons (Snooze, Done, Add to Today, Start) with no hierarchy. Reduce to a single filled primary action plus a Snooze menu and an overflow (kebab) menu: - Primary: Start (single) / Schedule for today (multiple) - Snooze button keeps the quick-defer menu (10/30/60 min, tomorrow) - New overflow menu holds the rest (Done/Complete, Add to Today, edit, unschedule, dismiss-keep-today) All existing actions remain available; only their grouping changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * refactor(tasks): add dropdown caret to reminder snooze button The Snooze button opens a menu but had no visible affordance signalling that. Add a trailing arrow_drop_down icon (native Material iconPositionEnd slot) so the menu trigger reads as such, surfaced by the review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * test(reminders): open overflow menu to reach Done in deadline specs The reminder dialog's "Done" action moved from a footer button into the new overflow ("More actions") menu, so the two deadline e2e specs must open that menu before clicking Done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * refactor(tasks): refine reminder footer hierarchy after UX review Follow-up to the footer restructure, addressing review findings: - Surface a one-tap "Done" (check) icon for single reminders, so the most common reminder response isn't two taps deep in the overflow menu. Left out for the multi-task case, where a one-tap complete-all is a footgun and per-row checks already exist. - Make the primary action deadline-aware: deadline reminders now default to the safe "Add to Today" instead of "Start" (which sets the current task and reorders Today as a side effect). Start moves into the overflow for deadlines; scheduled reminders keep Start as primary. - Move "Edit (reschedule)" into the Snooze ("when") menu so all time-deferral actions are grouped and the overflow holds pure dispositions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * test(reminders): click one-tap Done icon in deadline specs The single-task reminder footer now surfaces "Done" as a one-tap icon button (aria-label "Mark as done"), so the deadline specs click it directly instead of opening the overflow menu. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * refactor(tasks): put primary reminder action on the right, Done in menu Per UX feedback: - Reverse the footer button order so the filled primary action is always on the right (overflow on the left, Snooze in the middle). - Move "Done" back into the overflow menu instead of surfacing it as a standalone icon button. Update the deadline e2e specs to open the overflow menu before clicking Done accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * feat(tasks): allow dismissing reminder dialog + snooze split-button Three reminder-dialog improvements: - Click-away to dismiss: backdrop click / Escape now close the dialog and dismiss the reminders (clearing the reminder while keeping the task and its schedule). disableClose is kept and the events are handled in the dialog, so the worker does not immediately re-open it in a loop. - Snooze split-button: a single click snoozes 10 min (the common default, previously a hidden double-click) and a joined dropdown caret opens the full options menu. Uses a new shared .g-split-btn style. - Overflow button: replace the lone vertical "three dots" icon with a low-emphasis labeled "More" button, giving a clean emphasis ramp (More < Snooze < primary) with the primary kept on the right. Updates the deadline e2e specs and wiki for the new dismiss behavior, and adds unit tests for the backdrop/Escape dismissal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * fix(tasks): make reminder dialog close on backdrop/Escape Per product decision, clicking the backdrop or pressing Escape now simply closes the reminder dialog (drop disableClose) rather than dismissing the reminders. Deadline reminders are still cleared on close (avoiding the 10s re-fire loop); scheduled reminders stay active and the worker re-shows them until the user acts on them. Reverts the earlier intercept-and-dismiss approach (and its unit tests); updates the wiki accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * feat(tasks): add split-button UI component, restructure reminder footer Add a reusable <split-button> UI component (src/app/ui/split-button): a primary default action joined flush to a compact, centered overflow trigger that opens a passed-in menu. The joined treatment lives in the shared style layer (styles/components/split-button.scss) and now renders with no gap between the two halves and a properly centered trigger icon. Use it in the reminder dialog footer: limit the footer to two actions — "Snooze 10m" (showing the snooze duration) and the primary CTA (Start / Add to Today) — and fold the former "More actions" and snooze-options menus into a single overflow menu opened by the icon button right of snooze. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * fix(tasks): fuse split-button halves and update reminder dialog test The split-button trigger was pushed 8px away from the default action by Angular Material's `.mat-mdc-dialog-actions .mat-mdc-button-base + .mat-mdc-button-base` rule (specificity 0,3,0), which outweighed the joining `margin-left: -1px`. Match Material's selector hooks so the negative margin wins and the two halves render flush inside dialogs. Also drop the stale `disableClose: true` assertion from the reminder module spec to match the now-dismissable dialog (fixes failing CI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * fix(tasks): clarify reminder dialog "add to today" button label The multi-task primary button said "Schedule for today", which is both long and misleading: those tasks are already scheduled — the action drops their time and keeps them on Today (all day), or for deadlines adds them to Today while keeping the deadline date. Key the label on deadline vs schedule instead of single vs multiple: - all deadlines -> "Add to Today" (added to today, deadline preserved) - scheduled/mixed -> "Today" (already scheduled; kept on today, all day) Reuses the existing TODAY_TAG_TITLE string; SCHEDULE_FOR_TODAY remains for the per-row tooltips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * fix(tasks): distinguish deadline vs scheduled in reminder today-button After weighing options, label the reminder dialog's primary today-action by what it actually does in each case: - all deadlines -> "Add to Today" (task is added to Today; deadline kept) - scheduled/mixed -> "Keep in Today" (already scheduled; kept on Today, all day, with the specific time dropped) This replaces the bare, verb-less "Today" with accurate wording consistent with the dialog's existing "keep in Today" vocabulary. Adds the KEEP_IN_TODAY en string (other locales fall back to English via fallbackLang). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bqs4hesfHeJATSCr4mRoXE * chore(reminders): drop unused SNOOZE_OPTIONS string, regenerate t.const.ts The reminder-dialog footer trigger uses MORE_ACTIONS, not SNOOZE_OPTIONS, which was a dead key from an earlier iteration. Remove it and regenerate t.const.ts through prettier so the file is back to the formatted form (the prior commit had committed raw generator output, churning ~760 lines). * refactor(ui): trim unused split-button inputs, add unit spec The split-button's only consumer never overrides color or triggerIcon, so drop both inputs (and the deprecated ThemePalette type) and inline the defaults. Add a unit spec covering content projection, mainClick, menu wiring, disabled state, and the trigger aria-label. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ad149e138c
|
docs(android): document SDK 28 (API 28) behind-keyboard add-task bar root cause (#8528)
* docs(android): document SDK 28 (API 28) behind-keyboard add-task bar root cause After #8508 (patch removed in 18.12.0) a user on Android 9 / API 28 reports the global add-task bar sitting behind the soft keyboard — the device class open item #4 predicted. Capture the precise root cause (edge-to-edge forces no system resize; Type.ime() unreliable < API 30; old WebView VisualViewport doesn't shrink, so --keyboard-height stays 0) and why a web-side height fallback is the reverted #8295 trap (obscured ≈ 0 in both the resized and non-resized cases). Record the correct resize-detecting native fix recipe and the device-matrix gate it needs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gqH2i456UwdTJXwW5YHPs * fix(android): lift add-task bar above keyboard on API < 30 (#8508 follow-up) On Android 9 / API 28 under enforced edge-to-edge (targetSdk 36) the system does not resize the window for the IME and WindowInsetsCompat.Type.ime() is unreliable, so the edge-to-edge plugin never insets the WebView and neither the window nor VisualViewport shrinks. --keyboard-height stays 0 and the position:fixed add-task bar sits behind the keyboard. Add a resize-detecting native inset in CapacitorMainActivity, driven from the existing keyboard OnGlobalLayoutListener: measure the keyboard via getWindowVisibleDisplayFrame (reliable on every API level) and, only when the WebView actually extends behind the keyboard, lift it by the exact overlap via bottomMargin (matching the plugin), self-correcting as the keyboard height changes and restoring the captured margin on hide. Gated Build.VERSION.SDK_INT < 30 so it is a strict no-op on every device #8508/18.12.0 verified. Web-side height fallback was rejected: obscured = innerHeight - vvHeight is ~0 in both the resized and non-resized cases, so disambiguating requires the baseline-innerHeight + max(obscured, nativeKb - layoutShrink) math that was reverted as #8295. Native has the unambiguous geometry. Needs on-device validation across the matrix in docs/android-edge-to-edge-keyboard.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gqH2i456UwdTJXwW5YHPs * fix(android): harden SDK < 30 keyboard inset after review Address findings from a multi-agent review of adjustWebViewForKeyboardBelowApi30: - Bound the feedback loop: clamp appliedKeyboardInsetPx to [0, keypadHeight] (the lift never needs to exceed the keyboard height) and skip redundant layoutParams writes, so a WebView height that doesn't respond to the margin (e.g. the edge-to-edge plugin rewriting it) can't drive an endless requestLayout loop. - Ignore stale/pre-layout geometry while the keyboard is open (webView.height == 0) so an already-applied inset isn't collapsed to 0 for a frame; only the genuine keyboard-closed path restores the margin. - Make the dead-band density-relative (8dp) instead of a fixed 16px so it's visually constant across densities. The reviews confirmed the central risk is absent: shrinking the WebView via bottomMargin shrinks both innerHeight and visualViewport.height, so the web side's obscured stays under its 100px floor and does not add a second lift (no #8295 double-count). Still pending on-device validation per the docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017gqH2i456UwdTJXwW5YHPs * fix(android): resize WebView via height for SDK<30 keyboard inset On API < 30 under enforced edge-to-edge the system does not resize the window for the IME, so the add-task bar sat behind the keyboard (#8508 follow-up, Android 9 / API 28). The edge-to-edge plugin owns webView.bottomMargin and rewrites it to 0 on every inset dispatch while the IME is visible, so correcting the margin from a second writer flickers; WebView padding does not move the web layout viewport; and fully replacing the plugin's inset listener stops it re-painting its color overlays (white navbar gap). Instead set an explicit WebView layout height to the keyboard top while the IME is up and restore the resting height on hide. Height is a different property than the margin the plugin manages and, for an explicit-height view, the margin does not change the view size — so the two never fight and the plugin keeps doing everything else (insets + color overlays). Gated SDK_INT < 30, so API >= 30 is untouched. Also documents the root cause, the approaches that failed, and the upstream status, plus a handover/plan for migrating to Capacitor's built-in SystemBars. * fix(android): guard zero-height + reuse loc buffer in keyboard inset Multi-review follow-ups for the SDK<30 keyboard WebView-height workaround: - Skip the write when the computed keyboard-top height is <= 0 instead of coercing to 0. A 0 would collapse the WebView, and the existing height==0 guard would then latch and stop recomputing until the keyboard hides. - Reuse a single IntArray for getLocationOnScreen instead of allocating one on every layout pass while the IME is up (hot path). - Document the implicit coupling with StartupOverlayManager: both read/ mutate webView.height from layout listeners and only coexist because the overlay early-returns once its input bar is visible. Cross-referenced in both files so the guard isn't removed later. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ee47891f67 |
Merge branch 'feat/issue-8508-e897da'
* feat/issue-8508-e897da: fix(android): remove edge-to-edge WebView IME-inset patch (#8508) fix(android): correct edge-to-edge IME inset (gate API36+, latch, guard) (#8508) fix(plainspace): don't tint claim-list link icon with accent color |
||
|
|
c247bc541a |
fix(android): remove edge-to-edge WebView IME-inset patch (#8508)
The v18.11.0 patch (
|
||
|
|
2a0cc73507 |
fix(android): correct edge-to-edge IME inset (gate API36+, latch, guard) (#8508)
The v18.11.0 patch always inset the WebView for the IME, which shipped two bugs: 1. double-count on API <=35 (the system still resizes for the IME there - pre- enforcement, and we opt out on API 35 via values-v35) -> a keyboard-height blank gap above the keyboard; 2. the inset is reapplied on every OnApplyWindowInsetsListener callback via an unconditional setLayoutParams(); the IME inset fluctuates during typing (suggestion strip, layout switches), so the WebView relayouts mid-composition -> the IME composing region resets -> reversed/invisible characters. Corrected patch (regenerated via patch-package): - gate the inset to API 36+ where edge-to-edge is actually enforced (no system resize) -> no double-count, no gap on API <=35; - latch the keyboard inset while the keyboard stays visible -> no relayout on inset fluctuations during composition; - skip setLayoutParams when no margin changed. Verified: white gap gone on API 34 emulator. API 36+ typing-reversal half still needs real-device confirmation (not reproducible headless / on old emulators). Doc updated; do-not-naively-reapply guidance retained. |
||
|
|
9faf0a50d5
|
fix(sync): clarify Nextcloud connection-test 404 (wrong user ID) (#7617) (#8513)
* fix(plainspace): don't tint claim-list link icon with accent color * fix(sync): clarify Nextcloud connection-test 404 (wrong user ID) (#7617) A base-root 404 in the WebDAV connection test means auth succeeded but the DAV path /remote.php/dav/files/<userName>/ does not exist — i.e. the "Username" holds an email/display name instead of the account's user ID (Nextcloud accepts email/login for auth but the files path needs the uid). Previously this surfaced as a bare scrubbed hostname in the snack, which users misread as a stripped URL. Now: - WebdavApi.testConnection maps thrown errors to a readable, privacy-safe message plus an HTTP errorCode discriminator (404/401/status). - The Nextcloud "Test Connection" path shows a specific hint on 404 explaining that "Username" must be the user ID, not email/display name. - Wiki + field guidance clarified accordingly. |
||
|
|
8171bb05d0
|
refactor(sync): remove unused error classes, dialog, and constructor-time logging (phase 1, #8325) (#8510)
* fix(op-log): lock snapshot save to prevent lost-update window saveCurrentStateAsSnapshot() read NgRx state then lastSeq without holding OPERATION_LOG lock. An op appended between the two reads would get seq <= lastAppliedOpSeq but its effect would be absent from the snapshot. On next hydration the tail replay would start after that seq, silently skipping the op forever. Fix: wrap in lockService.request(LOCK_NAMES.OPERATION_LOG, ...) and read lastSeq BEFORE state snapshot so the worst interleaving degrades to harmless re-replay (idempotent) rather than a missed op. Fixes #8308 * fix(op-log): address review feedback on snapshot lock PR - Amend JSDoc idempotency claim: syncTimeSpent is additive on re-replay - Add inline note about compaction's opposite read order and worse failure mode - Add lock regression tests (#8308): lock acquired, read order, error handling Co-Authored-By: Claude <noreply@anthropic.com> * refactor(sync): remove unused error classes, dialog, and constructor-time logging Phase 1 of #8325: clean up orphaned sync error types and their UI. Removed error classes that are no longer thrown anywhere: - NoEtagAPIError, FileExistsAPIError (unused API errors) - RevMismatchForModelError, SyncInvalidTimeValuesError (superseded by file-based flow) - RevMapModelMismatchErrorOnDownload/Upload, NoRemoteModelFile, NoRemoteMetaFile - LockPresentError, LockFromLocalClientPresentError, MetaNotReadyError, InvalidRevMapError Removed DialogSyncErrorComponent and all references in SyncWrapperService (_forceDownload, _handleIncoherentTimestampsDialog, _handleIncompleteSyncDialog, _openSyncErrorDialog, _extractModelIdFromError). Removed constructor-time logging from JsonParseError, ModelValidationError, DataValidationFailedError — these errors are logged at the catch site; redundant construction-time logs risk leaking user data. Cleaned up dead translation keys (D_INCOMPLETE_SYNC block, DIALOG_RESULT_ERROR, ERROR_DATA_IS_CURRENTLY_WRITTEN) from en.json and t.const.ts. Updated file-based-sync-flowchart.md to reflect the removed error types. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0b4bc79354
|
refactor: retire GET /api/sync/snapshot, re-scope GET /api/sync/status as diagnostic (#8496)
Retire GET /api/sync/snapshot (attack-surface reduction): - Remove route handler from sync.routes.ts (no production client caller) - Keep internal generateSnapshot() and all downstream code - Update tests: delete GET /snapshot describe block, rework isolation test (sync.routes.spec.ts), remove GET assertion (sync-fixes.spec.ts), remove SimulatedClient.getSnapshot() helper + rework 2 integration tests to use GET /api/sync/ops instead (multi-client-sync.integration.spec.ts) Re-scope GET /api/sync/status as diagnostic: - Add doc comment to route handler marking it diagnostic - Update all documentation (README, API wiki, architecture diagrams) Documentation updates across 5 files remove GET /snapshot references and label GET /status as diagnostic. Breaking: self-hosters with external tooling relying on GET /snapshot must migrate — no shipped client version ever called this endpoint. |
||
|
|
cbe3f1527f
|
fix(caldav): support Vikunja project discovery (#8481) | ||
|
|
69cb69df51
|
docs(sync): document Proton Drive via rclone WebDAV (#8064) | ||
|
|
91d8dd80ba
|
feat(plainspace): add integration for shared projects (#8424)
* docs(plainspace): add integration plan for shared projects
* feat(plainspace): prototype 'assigned to others' work-view panel
UI-only prototype of the dual-list shared-project view: a read-only
component that lists Plainspace tasks owned by other members, grouped by
assignee, rendered as a collapsible panel in the project work view.
Uses hard-coded sample data; foreign tasks never enter the SP task store
or op-log sync. See docs/plainspace-integration-plan.md.
* refactor(plainspace): apply review fixes to prototype panel
- use defined --text-color-muted / --s-half / --s2 tokens instead of an
undefined token and redundant spacing fallbacks
- drop unused PLAINSPACE.DONE i18n key
- note avatarUrl sanitization needed before live API data
- clarify plan doc: icon registration in GlobalThemeService, issueType
union widening, and which phases are design-only vs implemented
* feat(plainspace): add mock-backed issue provider + share-on-create toggle
Phase 1: register PLAINSPACE as an issue provider (config form, API
service with mock mode, IssueServiceInterface impl, issue.model/const/
service registration, icon, issue-content config). Tasks assigned to me or
unassigned import via the normal issue->backlog pipeline; tasks assigned to
others are excluded here (shown read-only by the separate panel).
Phase 3: add a 'Share on Plainspace' toggle to the create-project dialog
that provisions a (mock) space and a bound provider via
PlainspaceShareService.
All Plainspace calls are mocked (PLAINSPACE_USE_MOCK) so the flow works
without a live backend; the real HTTP contract is isolated in
PlainspaceApiService. Build + lint:ts + lint:scss pass; added a spec for
the provider's mock/filter logic (unit run blocked: no browser in env).
* refactor(plainspace): address review feedback on provider + share flow
- add a Plainspace tile to the issue-provider setup overview so the
provider is addable manually (connect to an existing space), not only
via the share-on-create toggle
- make PlainspaceShareService self-contained: catch errors, surface a
snack, never reject (safe to fire-and-forget); log ids only
- strip the transient isShareOnPlainspace flag before it can reach
sessionStorage on the dialog cancel path
- add a getIssueProviderTooltip case for PLAINSPACE (was falling through
to the raw key)
- drop a redundant cast now that PlainspaceIssue is in IssueDataReduced
* feat(plainspace): add account login / identity (Phase 2, mock)
- PlainspaceAccountService: signals (account/isLoggedIn/currentUserId),
mock login/logout, localStorage-persisted (local-only, never synced)
- 'mine' in the import filter now derives from the signed-in identity
instead of a hard-coded constant; not-logged-in => only unassigned
- the Share-on-Plainspace flow prompts (mock) sign-in if needed and
refuses to provision a space when the user declines
- move the mock user-id constant to the plainspace feature folder to
avoid a cross-folder import cycle
- specs for the account service and updated api-service spec (sign in
before asserting the mine/unassigned split)
Identity is mocked (fixed user id) so the assigned/unassigned split lines
up with the mock space data; real OAuth/token exchange is future work.
* feat(plainspace): feed 'assigned to others' panel from live mock data
Wire the work-view panel to a new PlainspaceSharedTasksService instead of
hard-coded sample data: for a project with a bound enabled PLAINSPACE
provider, it fetches the space's tasks, keeps only those assigned to
others (assigneeId !== me), maps them to read-only rows, and threads them
through project-task-page -> work-view as an input. The panel now appears
only for shared projects (not every project), and foreign tasks still
never enter the SP task store / op-log.
Removes the prototype data const; adds a service spec.
* refactor(plainspace): replace 'assigned to others' list with a claim pool
Reframe around task ownership: SP shows only tasks assigned to me (first-
class) plus a read-only 'claim pool' of unclaimed tasks. Tasks assigned to
others are no longer represented in SP.
- import filter is now assigned-to-me only (was mine+unassigned)
- new claim flow: PlainspaceApiService.claimTask$ (assign-to-self) +
PlainspaceClaimPoolService.claim -> IssueService.addTaskFromIssue, with
pool refresh; claimed task leaves the pool
- rename AssignedToOthers component/service -> claim-pool; panel is
collapsed by default (a pool you reach for, not active work)
- drop assignee badges (redundant: list is all 'mine'; provider icon
already marks Plainspace tasks)
- mock data made resettable for test isolation; specs updated/added
- docs: capture the ownership model + rationale for dropping the list
* docs(plainspace): fix remaining stale claim-pool reference
* feat(plainspace): connect provider to the real integration API
Replace the mock-backed PlainspaceApiService with the real PAT-authed
{host}/api/integration endpoints (me, tasks, claimable-tasks, claim,
spaces, done PATCH) and map the wire SPTask -> the internal PlainspaceIssue
so the rest of the provider depends on one stable shape.
- the server scopes /tasks to the caller, so drop client-side identity and
the mock data/identity consts; "mine" is decided server-side
- store the personal API token (PAT) in PlainspaceCfg like other providers'
secrets; keep a local-only account cache to bootstrap share-on-create
- add a token field to the config form; the share flow prompts for and
validates a PAT via /me before provisioning a space
- isEnabled now requires a token; testConnection hits /me; issueLink uses
the task's real url
Adds docs/plainspace-api-extension-plan.md documenting the server contract
this client consumes (the endpoints added to Johannesjo/spaces).
* feat(plainspace): guided connect dialog with token steps + link
Replace the bare one-line API-token prompt with PlainspaceConnectDialog,
which shows where to create a personal API token and validates it before
closing:
- numbered step-by-step (Space -> People -> Advanced -> API tokens) plus an
"Open Plainspace" link button
- inline validation via /me; an invalid token shows an error and keeps the
dialog open instead of failing silently
- wire it into the share-on-create flow and sharpen the config-form token
description to point at the same place
Adds the CONNECT i18n block (regenerates t.const) and drops the now-unused
LOGIN_PROMPT string; a new component spec covers connect/cancel + template.
* feat(plainspace): auto-import + two-way done sync
- default isAutoAddToBacklog to true so tasks assigned to me auto-import
into the bound project's backlog (poll-to-backlog), matching the seamless
intent
- add PlainspaceSyncAdapterService, registered for 'PLAINSPACE' in the
two-way-sync effect, so completing/reopening a task in SP pushes the done
state back to Plainspace via PATCH /tasks/:id { done }
Only isDone is pushed (the integration API's one writable field); title and
done changes from Plainspace are pulled by the existing update polling. Adds
an adapter spec and a Plainspace adapter spy in the two-way-sync effect spec.
* fix(plainspace): match bound space by slug or id so tasks import
getMyTasks$ filtered by `task.projectId === cfg.spaceId`, but the value users
copy from the space URL is the slug while SPTask.projectId is the UUID — so a
slug-configured provider matched zero tasks and imported nothing, even though
the /tasks response was full.
Match spaceId against both projectId and projectSlug (getMyTasks$ and the claim
pool), drop the server ?projectId= param (UUID-only) in favour of the same
client-side match, and point the Space ID help text at the slug in the URL.
* chore(plainspace): log import counts to diagnose missing imports
getMyTasks$ now logs total vs space-matched task counts (ids/counts only,
no task content) so we can tell whether the space filter or the import is
dropping tasks. Temporary diagnostic.
* feat: add picker for plainspace
* feat: improve wording
* feat(plainspace): use two-circle brand mark for provider icon
* refactor(plainspace): type create-project form to drop isShareOnPlainspace any-cast
* refactor(plainspace): remove temporary getMyTasks$ diagnostic log
* feat(plainspace): push scheduled time (dueWithTime → remindAt)
* fix(plainspace): seed two-way-sync baseline so write-back actually fires
* feat(plainspace): import remindAt as dueWithTime so schedule shows in app
* refactor(plainspace): align client to scheduledAt/isRecurring API contract
* feat(plainspace): flag recurring tasks in the claim pool
* feat(plainspace): pull scheduledAt into dueWithTime on poll (schedule existing + recurring tasks)
* feat(plainspace): push title changes back to Plainspace
* fix(plainspace): address multi-review findings (perf, UX, cleanup)
Performance:
- claim pool no longer re-fetches /claimable-tasks on every task
add/complete/reorder (distinct on project id + provider identity)
- poll imported tasks via one getMyTasks$ instead of N getById$ calls
UX:
- gate the "Share on Plainspace" toggle to project create (was a dead
control in edit mode)
- success/failure snacks on claim and share (silent 409/offline before)
- distinct error state in the space-picker (vs misleading "no spaces yet")
Correctness/cleanup:
- normalize scheduledAt to canonical UTC ISO on read so the push guard
can't silently drop a reschedule on a benign reformat
- drop dead PLAINSPACE_INITIAL_POLL_DELAY, PlainspaceMember, assignee
- translate the "Advanced Config" form label
* docs(plainspace): reconcile token storage + forward-compat rollout note
- document that the PAT lives in synced provider cfg like other providers
(the earlier "not stored here" note was never true in the shipped code)
- add the typia forward-compat rollout requirement for the new built-in
PLAINSPACE issue-provider key to the Risks section
- fix stale "assigned to others" wording (now the claim pool)
* feat(plainspace): trim redundant Notes button + issue panel
Plainspace mirrors title/done/schedule onto native task fields, so the
detail panel only echoed the task and the chat button opened an
essentially empty drawer.
- Treat PLAINSPACE like ICAL: don't show the *persistent* toggle button
just because the task carries an issueId. Keep it for real notes, a
remote update (issueWasUpdated) and the open-panel close state.
- Keep the panel reachable: the hover-only "open detail panel" button
(task-hover-controls) now covers PLAINSPACE too — it's the exact
complement of the persistent button, so plain/iCal/Plainspace tasks all
get a hover affordance and there's never a double button.
- Replace the redundant title-as-"Summary" link with the two things not
already on the task: a clear "Open in Plainspace" link and a recurrence
indicator rendered with the same `repeat` mat-icon the claim pool uses
(new 'plainspace-recurring' custom field).
- Add unit coverage for the new content config.
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
0150ee23b3
|
feat(focus-mode): surface session-done + notify on countdown completion (#8475)
Countdown is the only focus mode that auto-stops with no follow-up (Pomodoro transitions into a surfaced break; Flowtime only stops on explicit user action), so completion could pass unnoticed: the SessionDone screen wasn't reliably shown and there was no cross-platform notification. On automatic Countdown completion (surfaceSessionDoneOnCompletion$): - If the focus overlay is open, the reducer's SessionDone screen is already visible — nothing forced. - If the overlay is hidden (user working elsewhere), surface a non-modal banner (BannerId.FocusModeSessionDone) with a 'What's next?' action that opens the SessionDone screen and self-dismisses once the overlay opens — instead of seizing the screen. - Raise an OS notification only when the app is unfocused (skipped while focused, since the surfaced UI is alert enough and it would fire on idle-resume) and not on Android, where the native foreground service already posts its own completion notification (documented in FocusModeForegroundService.onTimerComplete to keep the contract visible). Uses the injectable IS_ANDROID_WEB_VIEW_TOKEN (testable). Manual end and Pomodoro/Flowtime are excluded. |
||
|
|
c5651f52c7 |
docs(wiki): add Proton Drive via rclone + WebDAV guide
Document syncing to Proton Drive on desktop using the existing WebDAV provider pointed at a local 'rclone serve webdav' bridge, instead of a dedicated built-in provider. Marked experimental and no-support, in line with how generic WebDAV is treated, since it relies on rclone's reverse-engineered Proton backend. |
||
|
|
c2bf7b26a0
|
refactor(sync-core): drop shared-schema vector-clock compat re-export anda app enum (#8441)
* feat(sync-core): drop shared-schema vector-clock compat re-export and app enum - Remove the shared-schema → sync-core compatibility re-export of vector-clock types/functions; retarget app files (vector-clock.ts,operation-log.const.ts) and the server (sync.types.ts) to import from @sp/sync-core directly - Add @sp/sync-core to super-sync-server/package.json deps (it was load-bearing through the re-export) - Convert VectorClockComparison from a bare type to an as const object + derived type in sync-core,drop the app-side enum copy and the as cast - Update spec imports and pa ckage-boundaries.md * docs: remove stale shared-schema vector-clock references - Update comments in vector-clocks.md, client vector-clock.ts, and server sync.types.ts to reference @sp/sync-core directly - Remove @sp/sync-core dependency from shared-schema/package.json - Regenerate package-lock.json to reflect the removed edge * docs(sync): fix orphaned VectorClockComparison comment The comment block describing VectorClockComparison was left dangling above no declaration after the app-side enum was removed, and still claimed 'Uses enum for client-side ergonomics'. Move it above the re-export it documents and correct the wording. --------- Co-authored-by: Johannes Millan <johannes.millan@gmail.com> |
||
|
|
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 |
||
|
|
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. |
||
|
|
3db96fd8a3
|
fix(plugins): expose focused task API to iframe plugins (#8291)
Co-authored-by: Shem Freeze <whatamehs@Shems-MacBook-Air.local> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
d30fd5f434
|
fix(sync): conflict-check all entities of multi-entity ops (#8334) (#8377)
* fix(sync): conflict-check all entities of multi-entity ops #8334 Multi-entity ops (deleteTasks, moveToArchive, __updateMultipleTaskSimple, round-time-spent, batch board/issue-provider actions) carry entityIds[], but the server operations table only persisted the scalar entity_id (= entityIds[0]). Once such an op was stored, only its first entity took part in future conflict lookups, so a later stale write to a non-first entity found no prior writer and was wrongly accepted instead of rejected as CONFLICT_SUPERSEDED/CONCURRENT. - Add an entity_ids text[] column (populated for multi-entity ops only via getStoredEntityIds; single-entity ops store [] and use the scalar) + a GIN index. Migration is metadata-only with no backfill: pre-migration rows fall back to entity_id, so the fix is forward-only (entities 2..n of already-stored ops were never persisted and stay unrecoverable). - detectConflictForEntity now runs two ordered LIMIT-1 lookups (scalar btree + entity_ids GIN) and takes the higher server_seq, preserving the fast ordered hot path instead of an OR's BitmapOr+sort. - detectConflictForEntities / prefetchLatestEntityOpsForBatch match an entity as the scalar entity_id OR a member of entity_ids (unnest CASE + && / = ANY). - Harden validateOp to bound entityIds (length + per-element), mirroring entityId. Raw SQL validated against Postgres (PGlite) incl. GIN usage and two-query correctness; single path + validation + migrations covered by unit tests. The full conflict-detection.spec needs a generated Prisma client (CI), and the hot-path round-trip tradeoff should be confirmed with a real-PG EXPLAIN. * fix(sync): store entity_ids when a batch op dedups off the scalar #8334 Multi-review follow-up. getStoredEntityIds gated on `length > 1`, so a batch op whose entityIds dedup to a single value that differs from entityId (the server does not enforce entity_id === entityIds[0]) stored [] and that entity became invisible to conflict lookups — reintroducing #8334 for it. Gate on "is the set exactly [entity_id]?" instead, and cover it with unit tests. Also: correct the array-branch comment/doc — a GIN(entity_ids) lookup has no server_seq so it match-all-then-sorts (cheap only because multi-entity ops are rare), it is not an ordered walk; drop a stale "OR filter" test docstring; add a counter-note on getConflictEntityIds vs getStoredEntityIds to prevent swapping. * refactor(sync): single OR lookup for entity conflict detection #8334 Third multi-review follow-up. Revert detectConflictForEntity from the two ordered findFirst lookups back to a single Prisma OR [{entityId}, {entityIds:{has}}]. The two-query split optimized the OR's BitmapOr+sort, but that is bounded by op-log pruning (sub-ms in practice) while the split added a guaranteed extra round-trip on the common single-entity path (doubled by the FIX-1.5 re-check) — a net-negative for the median. The OR is simpler, fully typed/testable, and likely faster in aggregate; a code comment documents the split as the escalation if a real-PG EXPLAIN ever shows the OR is a problem. Review polish (no behaviour change): correct the array-branch/getStoredEntityIds comments (a GIN @> is match-all-then-sort, not an ordered walk; multi-entity-only storage's win is GIN size + keeping single-entity inserts off the GIN, not sort depth); note the batch unnest paths as the first EXPLAIN candidates under load; keep the two batch queries' CASE/prefilter SQL inline (a shared fragment would shift the positional params the conflict-detection.spec mock relies on) with a keep-in-sync note; replace a non-ASCII <= in a client-facing validation error string. * test(sync): update db mocks for OR + prefetch entity_ids lookups #8334 Running the full super-sync-server suite (with a generated Prisma client) surfaced two specs whose inline db mocks hadn't tracked the new conflict-detection SQL: - time-tracking-operations.spec: findFirst now models the single-entity lookup's OR: [{entityId}, {entityIds:{has}}] shape (it previously only matched the scalar entityId, so a concurrent single-entity update was wrongly "accepted"). - sync.service.spec: the prefetch $queryRaw mock parsed userId as the last param and flattened all params into the touched pairs; the #8334 prefilter adds idArray params after userId, so it now finds userId by type and reads the touched pairs from the VALUES join fragment only. Production code unchanged; these are test-mock fidelity fixes. Full suite: 800 passing. |
||
|
|
5e63eeb294
|
fix(plugins): stop leaked timers on plugin disable via onUnload hook (#8286)
* feat(sync): show actionable error on persistent WebDAV 409 When a WebDAV PUT keeps returning 409 Conflict even after the parent collection is created, the Base URL / Sync Folder Path is misconfigured (the classic Synology / raw-WebDAV setup mistake). Previously the raw "HTTP 409 Conflict" (or a bare "Unknown error") reached the user with no hint at the cause. Throw a dedicated WebDavSyncFolderUnusableSPError with a privacy-safe, actionable message (no path, no response body) at the spot that already detects this condition. Mirrors NetworkUnavailableSPError: a fixed user-facing message matched by instanceof. * ci(release): revive contributors section in release notes * fix(plugins): stop leaked timers on plugin disable via onUnload hook * fix(plugins): harden onUnload teardown hook after review * refactor(plugins): group lifecycle registers into options object * fix(plugins): close onReady stale-guard gap and timer interleave race |
||
|
|
67d1e32ffc
|
feat(focus-mode): focus screen UX overhaul (#7586)
* feat(focus-mode): focus screen UX overhaul Major rework of the focus mode timer screens (#7349): - Shared <focus-clock-face> drives Pomodoro / Flowtime / Countdown / Break with a single visual chrome; size tokens scale fluidly via clamp() + vmin/vh, no discrete breakpoints. - Single source of truth: --clock-time-size and --control-offset derive from --clock-face-size. - Countdown: click-to-edit duration, draggable handle on the ring, hybrid 5-min/15-min snap with 6h-per-rotation above 1h ([useFlexibleIncrement] on input-duration-slider, opt-in for other consumers). - Pomodoro prep inherits the click-to-type input; drag handle hidden so dragging the ring can't silently shift the value. - Pause keeps the selected task on screen (displayedTask falls back to the paused task while currentTask is null). - Notes panel acts as a modal: clock stays put, backdrop dims and closes on outside-click. - Session controls row below the circle (pause / complete / reset cycles); buttons fade via shared --revealed-opacity gated on host hover + document.hasFocus(). - Break screen mirrors focus-mode-main layout; cycle counter inside the circle on both focus and break; back-to-planning unified. - Flowtime settings dialog: all fields render with proper disable/enable, stable dialog width when switching modes. - arrow_backward -> arrow_back: fixes glitched glyph in repeat-type context menus (boards, simple counters, take-a-break, flowtime). * fix(focus-mode): silence naming-convention lint on formly 'props.disabled' Formly's expressionProperties path-string keys ('props.disabled') aren't camelCase and the rule has no requiresQuotes exemption; matches the existing pattern in src/app/features/issue/common-issue-form-stuff.const.ts. * test(focus-mode): mock pomodoroConfig signal on FocusModeService The component's initialization effect now reads focusModeService.pomodoroConfig() in Pomodoro+Preparation mode, but the three mocked FocusModeService instances in the spec didn't provide it, causing TypeError in 47 tests on CI (somehow not surfaced locally). * test(focus-mode): align specs with unified back-to-planning flow - focus-mode-break.spec: exitBreakToPlanning -> cancelFocusSession - focus-mode-session-done.spec: drop obsolete hideFocusOverlay assertion (cancelFocusSession now handles both clearing tracking and hiding the overlay, matching the production component) - focus-mode-main.spec: storeSpy gains selectSignal returning a signal, needed by the displayedTask paused-task fallback * fix(focus-mode): restore E2E selector hooks on refactored controls The shared <focus-clock-face> refactor moved pause/complete buttons out of the clock face into a new .circle-controls row but didn't carry the class names forward; 23 E2E specs key off them. Also re-add .task-title-placeholder on the no-task FAB so prep-state checks find it. - .pause-resume-btn on pause/resume in focus-mode-main + focus-mode-break - .complete-session-btn on the done_all button in focus-mode-main - .task-title-placeholder added to .select-task-cta FAB * fix(focus-mode): aria-label icon buttons; align break E2Es with new flow - focus-mode-break: pause/resume/skip/reset icon buttons now carry [attr.aria-label] in addition to matTooltip — icon ligature alone isn't an accessible name and breaks getByRole locators. - pomodoro-break-timing-bug-6044.spec: skipButton uses getByRole with accessible name rather than hasText (mat-icon ligature is "skip_next", not "skip break"). - focus-mode-break.spec: "exit break to planning and change timer mode" and "Back to Planning should NOT auto-start next session" now match the unified back-to-planning flow — overlay closes on click, user re-opens focus mode to change settings or verify prep state. * fix(focus-mode): address PR #7586 review feedback Maintainer review (johannesjo): - Debounce the Pomodoro work-duration write so editing it emits one synced config op instead of one per keystroke; flush on session start so a value typed inside the debounce window is not lost. (A1) - Replace the local ::ng-deep restyling of the shared input-duration-slider with opt-in [bareRing] and [hideHandle] inputs that own the chrome overrides in the slider's own styles; the four other consumers keep the default look. (A2) - Add unit tests for the flexible drag math (_setValueFromRotationFlex): the A<->B boundary anchoring at 55/60 min and both +/-180 degree wrap branches. (A3) - Delete the orphaned exitBreakToPlanning action, its stopTrackingOnExitBreakToPlanning$ effect, reducer case and specs; cancelFocusSession already unsets the current task. (B1) - Drop the unreferenced CONTINUE_TO_NEXT_SESSION and BREAK_RELAX_MSG i18n keys. (B2) - Collapse the duplicated clock-size clamp() into a single --clock-face-size-default token. (B4) Smaller focus-screen fixes (beerkumquatpome): - Hide the break task title when "pause tracking during breaks" is on. (C7) - Commit and close the duration editor on Enter. (C9) - Rename "Back to Planning" to "Exit focus session". (C11) - Show Flowtime breaks as a neutral "Break". (C13) Break-circle vertical alignment (C1) is only partially addressed here (matched the top reservation); exact alignment is a follow-up. * refactor(focus-mode): share a layout shell across timer screens and auto-start Flowtime breaks Extract a presentational focus-mode-layout component (4-row content-projection skeleton: [fmTop]/[fmTask]/[fmClock]/[fmBottom]) shared by the focus-session and break screens, so both keep a stable clock baseline across the focus<->break and prep<->in-progress transitions. focus-mode-main and focus-mode-break now consume the shell instead of each maintaining their own absolute layout. Replace the Flowtime "break offer" step with an auto-started break, mirroring Pomodoro: - Remove the BreakOffer UI state and the offerFlowtimeBreak action/reducer. - endFlowtimeSession now dispatches completeFocusSession(isManual:false) + startBreak (unsetting the task first when tracking-pause-on-break is on), so the break starts automatically and the session is logged exactly once via logFocusSession$. Also reorder the bottom controls (Back to Planning leftmost), restore the BACK_TO_PLANNING label to "Back to Planning", and drop the now-orphaned FLOWTIME_BREAK_TITLE / START_BREAK i18n keys. * test(layout): restore document.activeElement after focus-restoration specs The LayoutService "Focus restoration" tests override document.activeElement with Object.defineProperty, which shadows the native (inherited) getter with an own property on document. The afterEach only removed the mock DOM node, so the override leaked into later specs: once it ran, document.activeElement was frozen at the mock element and subsequent .focus() calls could no longer move it. Depending on Karma's spec order this broke the task.service focusTaskById tests (#7120), which then saw the stale activeElement instead of the element they focused. Delete the shadowing own property in afterEach to restore native behavior. Repro: ng test --include layout.service.spec.ts --include task.service.spec.ts * refactor(focus-mode): add interactive tracking widget and polish timer-screen layout - Replace the read-only task-tracking-info with focus-mode-task-tracking (vertical time stack + play/pause), wired through the shared layout shell - Center the task title and floor the task-row height so the clock baseline stays aligned across focus <-> break - Spacing polish: task-title-row and layout gaps to --s2, segmented-button-group padding to 0 - Drop redundant safe-area-bottom padding on the action row (the overlay already reserves it for the fixed shell) - Add "Take a moment to relax" break message and a clock-digit edit affordance * refactor(focus-mode): share timer/break layout, drop dead tracking toggle - Extract a shared <focus-mode-layout> skeleton and <focus-mode-task-row> used by both the focus session and the break. - Remove the in-view tracking play/pause toggle (start/stop stays on the global header button); strip focus-mode-task-tracking to read-only and drop RESUME_TRACKING. - Pin the mode selector out of flow and center the task·clock·bottom group; equal reserved task/bottom rows keep the clock vertically centered, with the selector kept on top. - Tighten sizing: horizontal selector segments, settings cog matched to the in-session controls, and a fluid clock clamp. |
||
|
|
d66958d6a8
|
feat(search): add completed task filter (#8168)
* feat(search): add completed-task filter #5943 * fix(search): tighten completed filter placement * test(search): include archived tasks in folder path case #5943 --------- Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
9cb9aef948
|
fix(redmine): support non-latin issue search #4149 (#8150)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
d2c989663e
|
feat(shortcuts): add optional shortcuts for scheduling #8093 (#8189)
* feat(shortcuts): add optional shortcuts for scheduling tasks to tomorrow, next week, and next month #8093 This change adds configurable, unassigned-by-default keyboard shortcuts for scheduling tasks to Tomorrow, Next Week, and Next Month. It also renames the existing 'moveToTodaysTasks' shortcut to 'taskScheduleToday' to align with the new schedule shortcuts. Closes #8093 * feat: add migration for renamed keyboard shortcut moveToTodaysTasks to taskScheduleToday * docs: fix corrupted fragment in keyboard shortcuts wiki * test: add unit tests for task scheduling shortcuts and fix migration typing * refactor: use getNextWeekDayOffset helper and preserve time/reminders for timed tasks * docs: clarify Next Month shortcut behavior * fix(config): ensure keyboard shortcut migration is one-shot by stripping legacy keys * fix(tasks): preserve reminder offset and show snack when rescheduling timed tasks * fix(tasks): fix regression in scheduleTask call and add missing snackbar confirmation * fix(tasks): align scheduling snack with planner effect formatting Use LocaleDatePipe.shortDate and include getSnackExtraStr() so the timed-task reschedule snack matches planTaskForDay's snack output. --------- Co-authored-by: johannesjo <johannes.millan@gmail.com> |
||
|
|
c9cb8dddfa
|
feat(search): include note content in global search and support folde… (#8044)
* feat(search): include note content in global search and support folder path disambiguation * fix(i18n): revert accidental changes to de.json * fix(notes): implement robust retry mechanism for focusing notes * fix(search): improve reactivity for folder path labels and archive cache invalidation * fix(notes): prevent stale focus retry loops * test(search): add regression spec for folder path reactivity * fix(notes): track and cancel stale focusItem retry loops * test(search): add regression test for archive cache invalidation on folder map changes * fix(search): improve note highlighting cleanup and remove debug log |
||
|
|
1b46d1b8f1
|
fix(header): show focus mode entry on mobile (#8179)
* fix(header): show focus mode entry on mobile #8157 * fix(header): remove unused focus session computed --------- Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
1765a4f74b
|
feat(boards): enhance project selection with multi-select and sidebar… (#8069)
* feat(boards): enhance project selection with multi-select logic - Update BoardPanelCfg to use projectIds array instead of single projectId - Implement multi-project filtering logic in BoardPanelComponent - Enhance SelectProjectComponent with connected multi-select checkbox logic - Add migration logic in sanitizePanelCfg to handle legacy board data - Update and verify all related unit tests * fix(boards): enhance panel migration and canonicalize project selection - Prefer legacy projectId during migration even if projectIds is defaulted - Canonicalize any projectIds containing "" back to [""] (All Projects) - Add regression tests for migration and canonicalization * fix(boards): prevent project assignment for All Projects panels - Ignore project IDs for additionalTaskFields when "" is present in projectIds - Add regression tests for multi-project filtering and task assignment * fix(boards): update board form spec to match projectIds changes * test(e2e): fix outdated keyboard shortcut and comment in add-to-today test * fix(boards): translate defaultLabel in SelectProjectComponent * docs(boards): document lossy canonicalization and legacy preference * refactor(boards): simplify additionalTaskFields and remove redundant test * feat(boards): add projectIds helper functions * refactor(boards): use projectIds helpers across board logic * fix(boards): keep projectIds optional for legacy data validation Making `projectIds` a required field broke the typia validator on raw-data paths that run before the boards reducer's `sanitizePanelCfg` normalizes the shape. Most critically, the one-time legacy PFAPI -> op-log migration validates, repairs, then re-validates and THROWS on failure -- and data-repair.ts has no boards handling -- so every legacy panel (carrying `projectId`, no `projectIds`) would abort that migration for existing users. Keep `projectIds` optional (absent/[''] = "All Projects") so legacy data validates; `sanitizePanelCfg` still normalizes it to a defined array before it reaches any component. Helpers and the drop() guard handle the optional type. Adds a regression spec exercising the real typia validator. --------- Co-authored-by: Johannes Millan <johannes.millan@gmail.com> |
||
|
|
7f029b1798
|
fix(plugins): harden nodeExecution grants (#8205)
* fix(plugins): harden node execution grants * fix(plugins): harden iframe bridge boundaries (#8208) * fix(plugins): harden iframe bridge boundaries * fix(plugins): tighten iframe bridge follow-up * test(plugins): make node-executor electron test hermetic The new plugin-node-executor test read the real built-in plugin manifest (src/assets/bundled-plugins/sync-md/manifest.json), which is a build artifact absent when 'npm run test:electron' runs in CI (before the frontend/plugin build). Stub the manifest read, scoped to the executor module via Module._load, so the grant/token/webContents assertions no longer depend on built plugin assets. * fix(plugins): allow iframe formatDate & getCurrentLanguage i18n methods The iframe API allow-list gate added in #8208 only listed a subset of the i18n methods that master's #8146 exposes to iframe plugins. Without this, plugin calls to formatDate/getCurrentLanguage (and translate) are rejected with 'Unknown API method'. Add all three so the merged gate matches the methods createBoundMethods/createPluginApiScript expose. * docs(plugins): document node-exec handoff bootstrap-ordering invariant The one-shot consumePluginNodeExecutionApi() handoff is defended by construction ordering, not structural isolation: PluginBridgeService must consume it before any plugin 'new Function' code runs (both share window.ea in one renderer realm). Document the invariant at the consumption site so a future lazy-service/early-plugin-load refactor can't silently regress it. Surfaced by the post-merge security review (latent finding; not currently exploitable). * fix(plugins): use static iframe sandbox attribute to avoid NG0910 Binding a security-sensitive iframe attribute (sandbox) via [attr.sandbox] makes Angular throw RuntimeError NG0910 and tear down the iframe, crashing the plugin view to the global error screen. This broke the plugin-iframe, plugin-loading and plugin-lifecycle e2e tests. Restore the static sandbox attribute (still without allow-same-origin, so the opaque-origin isolation from #8208 is preserved) and drop the now-unused iframeSandbox binding/import. |
||
|
|
3d2c811e78 | chore(task-repeat): revert RRULE Phase 1 from master (#7948) Develop the full RFC-5545 RRULE epic on the long-running feat/rrule-epic branch and merge once complete and testable in final form, rather than landing 13 phases into master one half-state at a time. Work preserved on feat/rrule-epic. Not yet shipped (post-v18.9.1), so no user impact. | ||
|
|
870d4a4882
|
docs(backup): document Android automatic restore (#8193)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
140a0367ea
|
docs(caldav): clarify bidirectional VTODO sync (#8196)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
1718b0a8b8
|
feat(task-repeat): RFC 5545 RRULE recurring schedules — EPIC · Phase 1/13 (Closes #4020) (#7948)
* feat(task-repeat): add cron / natural-language recurring schedules
Add a CRON repeat cycle so a single recurring task can express schedules
the day/week/month/year cycles cannot (e.g. every Saturday, March through
November). The cron expression drives occurrence generation; all other
schedule fields are ignored for CRON cfgs.
- Occurrence engine: getNewestPossibleCronDueDate / getNextCronOccurrence in
cron-occurrence.util.ts (cron-parser), wired into the existing
getNewestPossibleDueDate / getNextRepeatOccurrence dispatchers. Recurrence
stays day-granular: sub-daily crons create at most one task per day.
- English to cron via the crono-eng WASM module (src/assets/crono-eng.wasm,
loaded lazily), with a builtin regex parser as fallback until/if the module
is unavailable. naturalLanguageToCron() also passes raw cron through.
- Cron to english live preview under the input via cronstrue; the field shows
the interpreted cron + humanized reading as the user types and warns when an
expression is sub-daily.
- Invalid / unrecognized input shows an error snack and blocks save.
- Add-task bar: @+<cron-or-phrase> short syntax attaches a CRON repeat cfg.
- tools/build-crono-wasm.js regenerates the WASM asset from the sibling
crono-eng Zig project (committed asset is the source of truth for builds).
* fix(task-repeat): cron edge cases, clearer warnings, and corpus tests
Hardening + UX pass on the cron repeat cycle, driven by verifying the full
crono-eng corpus (415 phrases) end to end.
Fixes
- Silent never-fires: crono-eng can translate phrases (a specific year, "nearest
weekday"/W, "n-to-last day"/L-n, biweekly #-lists) into Quartz forms the
recurrence engine (cron-parser) cannot run. These passed UI validation yet a
task would never be created. naturalLanguageToCron now gates its output on
cron-parser, so such input is rejected at the field instead.
- Off-by-one for midnight crons: cron-parser's next() is exclusive of an
exact-boundary currentDate, so getNextCronOccurrence skipped the first
eligible day for 00:00 schedules (e.g. first occurrence landed a day late).
Seed the search one ms before the lower-bound day so a midnight fire stays
eligible; now matches the non-cron daily convention.
UX
- Live preview shows the interpreted cron + plain-English reading below the
field, and warns when the time of day is ignored (day-granular engine) or the
schedule is sub-daily ("runs at most once per day").
- Friendlier validation/snack message with examples instead of terse jargon.
Tests
- tools/test-crono-wasm.js (npm run test:crono): corpus-driven harness asserting
the committed WASM matches all 415 corpus crons, and that cron-parser +
cronstrue accept them — classifying the 27 known engine-unsupported forms so
it stays a regression guard.
- cron-occurrence.util.spec.ts: occurrence engine across daily/weekly/monthly/
month-range/sub-daily, varied times, start-date and last-creation gating.
- parse-natural-cron.util.spec.ts: raw passthrough, phrase recognition, null
cases, loader resilience.
- task-repeat-cfg-form.cron.spec.ts: field validator + live-preview/time-warning.
* fix(task-repeat): live cron preview + @+ title strip; add E2E
Fixes found by adding end-to-end coverage of the cron feature.
- Live preview did not render: Formly's mat-hint does not reflect a dynamic
expressionProperties.description, so the interpreted-cron/English preview never
updated as the user typed. Move it into the dialog template, driven by a
computed signal off the form's valueChanges (getCronPreview in cron-preview.util),
so it updates live and persists after applying. The time-of-day-ignored and
sub-daily warnings now render as proper translated lines.
- "@+<phrase>" short syntax left the clause in the title: shortSyntax set
taskChanges.title for the cron strip, but the later parseTimeSpentChanges
reassignment wiped it, so a bare "Foo @+every day" submitted the raw title.
Strip into the working title and emit the cleaned title at the end.
- shortSyntax returned cronExpression unconditionally (undefined when absent),
breaking 38 exact-match assertions; only include the key when set.
Tests
- e2e/tests/recurring/cron-repeat.spec.ts: @+ short-syntax attaches a CRON cfg
(title stripped); full dialog flow (phrase → live preview + time warning →
save → CRON cfg persisted).
- short-syntax.spec: @+ extraction/stripping, with and without other syntax.
- getCronPreview unit tests (interpreted cron, English, timed/sub-daily flags).
* test(task-repeat): property/fuzz/metamorphic/edge cron tests; fix first-occurrence
Large second testing pass (≈+58 unit tests, +2 E2E, new property harness),
which surfaced one gap and one upstream quirk.
Fix
- getFirstRepeatOccurrence returned null for CRON cfgs (no case → default), so a
timed CRON task's first instance and the startDate-moved-earlier path fell
back to today instead of the cron's first fire. Add getFirstCronOccurrence
(first fire on/after startDate) and route CRON through it.
Tests
- cron-occurrence.invariants.spec: property/invariant battery over many crons
(no-throw, result strictly-after / on-or-before bounds, determinism,
monotonicity), calendar edges (leap-day, year rollover, DST spring/fall),
pathological "Feb 30" (null, no hang), and getFirstCronOccurrence /
getFirstRepeatOccurrence routing.
- parse-natural-cron.metamorphic.spec: relational tests (case/whitespace/order/
irrelevant-text invariance, idempotence, determinism, raw passthrough).
- short-syntax.spec: more @+ cases (raw cron after @+, bare @ is not cron,
unrecognized phrase ignored, clause stops at the next delimiter).
- tools/test-crono-properties.js (npm run test:crono:props): WASM determinism,
marshalling fuzz (empty/oversized/unicode/boundary), English→cron→English
round-trip, and occurrence simulation across every runnable corpus cron.
- e2e: invalid phrase blocks save + shows inline error (Save disabled);
raw cron expression preview + save. Shared dialog-open helper.
Note: documented a cron-parser DST quirk — prev() skips the spring-forward
midnight, so "newest due" can land a day earlier on that date (day-granular,
once a year; lastTaskCreationDay prevents duplicates).
* test(task-repeat): selector integration, serialization, and WASM buffer-reuse
- selectors.spec: CRON cases for the selectors that drive task creation —
selectAllUnprocessedTaskRepeatCfgs (due today, overdue, already-created,
paused, future start, invalid expr, deleted instance) and
selectTaskRepeatCfgsForExactDay (exact-day match). Proves a CRON cfg is
picked up by addAllDueToday's selector path.
- cron-occurrence.invariants.spec: a CRON cfg survives a JSON round-trip with
identical occurrence behavior (sync / backup integrity).
- test-crono-properties.js: buffer-reuse checks — a short translation after a
long one is uncontaminated, and results stay deterministic across a 600-call
interleaved loop (the WASM module reuses static input/output buffers).
* test(task-repeat): cross-timezone day-resolution harness
The Karma suite only runs in the host timezone (Chrome on Windows ignores the
TZ env var — the reason the pre-existing *.tz.spec fails), so cron day-resolution
was never verified across zones. Add a Node harness (npm run test:crono:tz) that
spawns a child process per timezone — Node honors TZ — and asserts day-class
invariants in each: weekly-Monday resolves to a Monday, monthly day-15 to the
15th, daily to the next calendar day, time-of-day never shifts the day, and a
full-year daily iteration is monotonic with 366 distinct days (no DST skip in
next()). Covers fractional offsets (India +05:30, Chatham +12:45/+13:45) and
Southern-hemisphere DST (Sydney). Mirrors getNextCronOccurrence's core math.
* fix(task-repeat): DST-safe newest-due; verify real engine across timezones
Closes the time-based reliability gaps from the previous testing pass.
Fix
- getNewestPossibleCronDueDate no longer uses cron-parser's prev(), which skips
the spring-forward midnight in DST zones (a daily cron then looked like it
didn't fire that day). It now walks days backward and asks "does the cron fire
during this local day?" via a next()-based probe (next() is monotonic across
DST). Spring-forward and fall-back now resolve the exact day.
Tests
- main.ts exposes the real occurrence utils on __e2eTestHelpers.cron (dev/stage
only, stripped from production).
- e2e/cron-timezone.spec: runs the REAL engine under five forced browser
timezones (Playwright timezoneId — reliable regardless of host OS, unlike the
Karma TZ env). Each asserts the timezone was actually applied (live UTC
offset) AND that day-class results are correct, incl. fractional offsets
(Chatham +12:45) and the spring-forward day.
- test-crono-tz.js: also asserts a daily cron fires on every calendar day of the
year in all 8 zones (regression guard for the prev() skip).
- invariants.spec: spring-forward / fall-back now assert the exact day for both
next() and newest (no longer hedged).
- effects.spec: a CRON cfg's first occurrence is computed through the real
updateTaskAfterMakingItRepeatable$ effect path (live new Date()).
* test(task-repeat): dev jumpDay clock helper + live day-change e2e
Adds __e2eTestHelpers.jumpDay(n) / resetClock() (dev/stage only, stripped from
production) so recurring/day-change behavior can be fast-forwarded from the
DevTools console without touching the OS clock: jumpDay shifts Date.now() by a
cumulative day offset and forces the day-change re-sample so addAllDueToday()
runs. New e2e asserts jumpDay(1) creates the next daily instance — covering the
live day-change -> creation path end to end.
* feat(task-repeat): add "create a task for each missed occurrence" option
By default, opening the app after several scheduled occurrences were
missed creates only a single instance for the most recent occurrence.
When enabled (advanced section), a separate overdue task is created for
every missed occurrence instead, oldest first, capped at the 30 most
recent to avoid flooding the list / emitting a large sync batch.
- new getAllMissedDueDates() util reuses getNextRepeatOccurrence so the
per-cycle and CRON occurrence logic stays single-sourced and DST-safe
- service multi-spawn path is gated to the catch-up flow (target day is
today or earlier) and is mutually exclusive with skipOverdue /
waitForCompletion; the single newest-occurrence path is unchanged
- form checkbox hides when "skip overdue" is set, and vice versa
Part of #4020
* fix(task-repeat): re-anchor lastTaskCreationDay when cron expression changes
cronExpression was missing from SCHEDULE_AFFECTING_FIELDS, so editing a
CRON schedule did not re-run rescheduleTaskOnRepeatCfgUpdate$. A
previously computed (often future) lastTaskCreationDay then stuck around
and silently suppressed every occurrence until real time caught up to it
— e.g. a "Jan-Aug" cron set while the clock was past August anchored to
Jan 1 next year and produced no tasks in between.
Adding 'cronExpression' makes a cron edit re-anchor from the current
date, consistent with repeatCycle / repeatEvery / weekday edits.
Part of #4020
* feat(tasks): surface @+ recurring short syntax in autocomplete and help
The `@+<schedule>` add-task short syntax existed but was undiscoverable.
- add recurring examples to the `@` autocomplete suggestions: typing `@+`
now autocompletes phrases like `@+every monday` or
`@+every saturday from march through november`
- document `@+` in the Settings -> Short Syntax help text
Part of #4020
* fix(tasks): remove @+ recurring autocomplete entries
The `@+` examples added under the `@` mention trigger kept the suggestion
dropdown open while typing `@+<phrase>`, so pressing Enter selected a
suggestion instead of submitting the task — breaking the headline
type-and-go flow. The `@+` syntax stays documented in Settings -> Short
Syntax; only the dropdown entries are removed.
Part of #4020
* refactor(task-repeat): show cron only inside Custom recurring config
Cron appeared twice in the recurring-config dropdown: as a top-level
"Cron / natural language" quick-setting AND as a cycle inside "Custom
recurring config". Drop the duplicate top-level option; cron is now
reached via Custom -> repeatCycle = Cron.
- remove the CRON quick-setting option from the dropdown builder
- keep the repeatCycle select visible when CRON is chosen (only hide
repeatEvery) so the user can switch back
- map existing cron configs (incl. legacy quickSetting 'CRON') to CUSTOM
on load so the dropdown matches the stored cycle
- update the cron E2E to drive the dialog via Custom -> Cron
Part of #4020
* test(task-repeat): make @+ short-syntax e2e date-independent
The test asserted the `@+every monday` task appears in the Today view, but
a weekly schedule's first instance is the next Monday — so on any non-Monday
the task is correctly scheduled for a future day and isn't in Today, making
the test fail depending on the day it runs. Verify the attached CRON config
and the stripped title via the store instead, which is view- and
date-independent.
Part of #4020
* feat(task-repeat): RFC 5545 RRULE recurring schedules [WIP]
Overhaul recurring tasks from the bespoke repeatCycle format to RFC 5545
RRULE as the canonical recurrence representation (legacy fields kept
populated for old-client forward-compat). Adds a structured RRULE builder,
RRULE-backed quick presets, per-instance due-date derivation, multiple end
conditions (COUNT / UNTIL / after-N-completions), an occurrence heatmap with
completion simulation, natural-language @+ short syntax, and a REST API for
recurring tasks. Migration is lazy (on dialog open) so existing configs keep
firing untouched until edited.
WIP: some required features and tests are still outstanding, and it needs
much more real-world (user-based) testing before it is merge-ready.
* feat(task-repeat): per-occurrence overrides, due-date control move, @+ preview, NL fix [WIP]
- RECURRENCE-ID: per-instance overrides (move / re-time / re-title) surfaced to
the engine as RDATE + EXDATE so every projection stays consistent.
- Move the due-date derivation control out of the rrule builder into the dialog
so it applies to all recurring configs (presets + Custom).
- Live @+ recurrence preview (humanized rule + next occurrence date) in the
add-task-bar, so a far-off rule is obvious before submit.
- Fix @+ "every other <weekday> in <month>" mis-parsing to YEARLY;INTERVAL=2
("every 2 years"); weekly-style interval + weekdays now stays WEEKLY.
* refactor(task-repeat): trim PR to engine+builder+migration+preview; fix curator review
Trim the WIP recurring-schedules PR to its reviewable core — RFC 5545 occurrence
engine, structured RRULE builder, legacy<->RRULE migration (both directions),
live text preview, quick-setting presets, and tests — and defer the rest to
follow-up sub-MRs: natural-language @+, due-date derivation, ends-after-N-
completions, missed-occurrence backfill, heatmap preview + simulation, REST
recurring creation, and RECURRENCE-ID per-instance overrides. The full
implementation is preserved on branch feat/recurring-full.
Curator review fixes:
- quickSetting forward-compat: never persist a value outside the released
(master) union. The newer preset literals AND 'RRULE' map to 'CUSTOM' at every
persist boundary (dialog save, add-task-bar, data-repair downgrade); the rich
value drives the dialog UI in-memory only and the builder reconstructs from the
opaque rrule on open. Fixes old/mobile typia sync-validation treating such
tasks as corrupt.
- Malformed rrule now falls back to the legacy schedule fields (guarded by
isRRuleValid) instead of silently stopping the task.
- Reverse converter maps a BYDAY-less FREQ=WEEKLY onto the start weekday, so the
legacy WEEKLY engine still fires on old clients.
- Stop logging the raw rule body (the raw-override field makes it free-text user
input; log history is exportable).
- DRY: share normalizeWeekdays / toNumArray between the two converters.
* refactor(task-repeat): clamp quickSetting at the action creators
The op-log replays the action payload, not reduced state
(operation-capture.service.ts), so a reducer-level clamp would not keep an
out-of-union quickSetting off the wire. Clamp in the addTaskRepeatCfgToTask /
updateTaskRepeatCfg / updateTaskRepeatCfgs creators instead -- the single
boundary every dispatcher passes through, so old/mobile clients always replay a
value their typia union accepts. The newer preset literals and 'RRULE' stay
in-memory in the dialog form only.
Removes the now-redundant per-call-site clamps in the dialog save path and the
add-task-bar; the @+ and REST paths inherit the clamp for free when their phases
return. Adds an action-creator spec covering the clamp at each entry point.
* feat(task-repeat): nth-weekday rows support multiple weekdays per ordinal
Each ordinal row (1st/2nd/3rd/4th/last) now multi-selects weekdays via toggle
buttons, so one line can mean "the 1st Monday and the 1st Tuesday" -> BYDAY=1MO,1TU.
The per-row ordinal dropdown excludes positions already used by other rows (each
ordinal anchors at most one line), and the add button hides once all are used.
Parsing groups weekdays that share an ordinal back into one row. Applies to both
the monthly and yearly nth-weekday modes; updates the helper copy accordingly.
* test(task-repeat): complex RRULE coverage + day-by-day create-loop sim
Adds high-complexity coverage for the occurrence engine and builder:
- engine variants x settings: per-day ordinals, BYSETPOS last-weekday,
BYMONTHDAY=-1, seasonal BYMONTH, BYWEEKNO/BYYEARDAY, leap years, mixed with
COUNT / UNTIL / EXDATE / lastTaskCreationDay
- builder forward + mode-detection + lossless round-trips (incl. multi-weekday
ordinals and sub-daily raw-override)
- cfg->engine routing for complex rules with deletedInstanceDates and the
malformed-rrule legacy fallback
- a "day-march" simulator that drives getNewestPossibleDueDate one day at a time
with lastTaskCreationDay fed back like the service, asserting the created
stream has no dupes/skips, honors EXDATE/COUNT, is idempotent within a day,
and documents Phase-1 app-closed catch-up (newest missed only)
Also trims the deferred @+ short-syntax e2e (returns with its phase) and fixes
the dialog-flow e2e to target the current .rrule-result preview selector.
* feat(task-repeat): custom ordinal input for nth-weekday rows and BYSETPOS
The nth-weekday ordinal dropdown and the weekday-set 'which occurrence'
dropdown each gain a 'custom…' option that reveals a free-form input,
allowing any non-zero ordinal (e.g. -2 = 2nd-to-last, 5FR) and
comma-separated BYSETPOS lists (e.g. 2,-1). Parsed rules with such values
now render structurally instead of falling back to the raw override.
* feat(task-repeat): make weekday-set occurrence (BYSETPOS) a multi-select
Replace the single-value 'which occurrence' dropdown with toggle buttons
matching the weekday toggles, so several occurrences can combine (e.g.
first + last weekday = BYSETPOS=1,-1). 'Every' clears the narrowing; the
custom input stays for values without a toggle.
* fix(task-repeat): address review findings on rrule migration fidelity
- Yearly builder rules seed BYMONTH from the start month: a bare
FREQ=YEARLY;BYMONTHDAY=n expands across every month (RFC 5545) and
fired monthly for fresh yearly configs.
- quickSetting change handler passes the selected start date, so
date-writing presets no longer overwrite a user-picked future anchor
with today.
- Remove the createForEachMissed checkbox + copy: the engine has no
backfill support yet, so the setting silently did nothing (and wrote
an undeclared field into synced state).
- rruleToLegacyTaskRepeatCfg always resets the monthly anchor fields so
stale nth-weekday/last-day anchors can't survive the merge, sets
monthlyLastDay only for a pure BYMONTHDAY=-1 rule, and aligns
startDate to the rule's first occurrence for date-anchored
monthly/yearly rules (legacy clients read day/month from startDate);
save() re-derives the fallback so later startDate edits stay aligned.
- _normalizeMonthlyAnchor keeps monthlyLastDay on RRULE saves — it is
the derived old-client fallback for BYMONTHDAY=-1, not a stale preset
flag.
- Legacy CUSTOM migration preserves clamp semantics: day > 28 anchors
emit BYMONTHDAY=<d>,-1;BYSETPOS=1 (the day, or the last day of
shorter months) instead of a plain BYMONTHDAY that skips such months;
the builder serializer emits BYSETPOS in day-of-month modes so these
rules round-trip structurally.
- Regenerate package-lock.json to drop stale cron-parser/cronstrue
entries left over from the earlier cron approach.
* fix(task-repeat): harden rrule builder + legacy fallback after deep review
Correctness:
- Clear bySetPos (and its custom-input state) on monthly/yearly mode and
frequency switches: a BYSETPOS left over from the weekday-set mode
silently narrowed day-of-month rules (BYMONTHDAY=15;BYSETPOS=2 never
fires) with no UI to see or clear it.
- Move startDate alignment out of rruleToLegacyTaskRepeatCfg into a new
arithmetic getAlignedStartDate, applied once at save and only when the
schedule actually changed: the occurrence-search version corrupted the
clamp idiom (aligned a day-31 rule onto Feb 28/29 so old clients fired
on the wrong day forever), collapsed multi-day BYMONTHDAY lists to
their earliest day, rewrote the visible start-date field live on every
builder interaction, cost ~60ms/keystroke for sparse rules, and put
startDate into the change diff on title-only edits — retriggering the
issue-#7373 reschedule. Weekday-anchored rules are no longer aligned.
- Emit the monthly-anchor resets as null/false instead of undefined (in
the converter AND the presets' MONTHLY_ANCHOR_RESET): JSON.stringify
drops undefined keys from op-log payloads, so the reset never reached
remote clients and stale nth-weekday/last-day anchors survived there.
The anchor model fields now allow null; the nth-weekday engine guard
strips it.
- Enforce the YEARLY BYMONTH guard in the serializer: yearly date mode
without months now emits a plain FREQ=YEARLY (anniversary) instead of
a bare BYMONTHDAY that fires monthly; parsed bare yearly rules fall
back to the raw override, preserving their semantics verbatim.
- Drop the auto-seeded BYMONTH when switching away from YEARLY (it
silently constrained the new rule to one month) and seed from the
CURRENT start date rather than the ngOnInit-captured month.
- Clamp custom nth ordinals per frequency (±5 monthly, ±53 yearly) —
BYDAY=10MO is valid RFC but matches nothing, ever — and reject an
ordinal another row already anchors (duplicate rows collapse on
reload). Re-clamp poses when switching into MONTHLY.
- Drop BYSETPOS=0 on parse (parseString accepts it; re-emitting creates
a rule the occurrence engine silently treats as dead).
- Predefined set-position toggles close the explicitly-opened custom
input (both rendered active with contradictory state).
Cleanup:
- Extract parseIntList (4 cloned int-list sanitizers), pushBySetPos /
pushByDaySet (4 copy-pasted emit blocks), _clampedMonthDayPart
(monthly/yearly clamp duplication); share the monthly/yearly
weekday-set template block via ngTemplateOutlet; parse BYSETPOS via a
computed signal instead of per-CD string parsing.
- Correct the BYMONTHDAY hint: it claimed short months clamp to their
last day, but plain BYMONTHDAY skips them.
* test: make timezone demo specs host-independent
Six .tz.spec demonstration tests branched on the host's CURRENT
getTimezoneOffset() (or a literal 'America/Los_Angeles' name check)
while asserting against January test instants. In DST-observing zones
the summer offset differs from the January one (e.g. New York: EDT 240
vs EST 300), so the wrong branch was taken and the suite failed every
summer on US-east machines, blocking pre-push hooks.
Branch on the offset AT the test instant instead, with the correct
longitude threshold for each instant (07:00 UTC flips the local day at
UTC-7, 06:00 UTC at UTC-6, midnight UTC at UTC±0) — the tests now pass
in any host timezone year-round.
* test: pin browser timezone to Europe/Berlin in karma config
Re-enable the previously commented-out TZ pin so timezone-sensitive
specs run deterministically where Chrome honors the env var
(Linux/macOS, incl. CI). Verified empirically that headless Chrome on
Windows IGNORES TZ and keeps the host zone — the *.tz.spec.ts files
keep their offset-at-test-instant branching as the Windows backstop.
* test: make supersync dump/restore helpers cross-platform
createFullDump/restoreFullDump used POSIX-only shell constructs (output
redirection to /tmp, 'cat | psql', 'rm -f'). On Windows the restore
pipeline failed AFTER the 'DROP SCHEMA public CASCADE' had already
succeeded, leaving the test database without any tables — every
subsequent spec in the run then failed in createTestUser with 'table
public.users does not exist' (5 cascading failures + dozens of
never-ran tests).
Capture pg_dump via execSync stdout + fs.writeFileSync, feed the
restore through stdin (execSync input), use os.tmpdir() and
fs.unlinkSync. Verified: full dump-restore spec plus the three spec
files it previously poisoned all pass on Windows (14/14).
* test(e2e): round-trip coverage for new rrule builder widgets
Covers the four gaps hand-testing would otherwise need to catch, using
the dialog's live result band as the oracle (.rrule-result__expr =
exact assembled rule, .rrule-result__next = engine-computed upcoming
dates — no waiting for real time):
- custom nth ordinal (-2 = 2nd-to-last weekday) emits the right BYDAY
and persists verbatim
- BYSETPOS multi-select (first + last) emits BYSETPOS=1,-1 and persists
- mode switch drops a weekday-set BYSETPOS instead of leaking it into a
day-of-month rule (dead-rule regression)
- switching to YEARLY seeds BYMONTH and the upcoming dates are strictly
one per year
Persistence asserted via the NgRx store: saving a recurring cfg
re-plans the task onto its first occurrence (usually not today), so a
UI reopen from the work view is date-dependent; the parse-to-widget
rendering side is covered by the dialog/builder unit specs.
* fix(add-task-bar): open the repeat dialog for the custom recurring option
The repeat quick-setting menu emits the 'RRULE' value for 'Custom
recurring config', but the add-task-bar still branched on the legacy
'CUSTOM' value — picking the option fell through to the preset path and
silently created a weekly-fallback cfg instead of opening the dialog.
Route both 'RRULE' and 'CUSTOM' through the dialog path, preselect the
RRULE builder in the dialog via a new initialQuickSetting dialog input
(the user explicitly asked for a custom rule), and show the tune icon
for the menu entry again. Covered by a new e2e: menu pick → task
created → builder dialog opens → saved rule persists verbatim.
* fix(task-repeat): address review on rrule alignment + anchor sync safety
- getAlignedStartDate re-anchors onto the rule's actual first occurrence
(occurrence-set-neutral for INTERVAL/COUNT/UNTIL) instead of pure date
math that shifted INTERVAL cadence and skipped valid clamped occurrences
- save() always re-derives the legacy fallback fields against the final
startDate, not only when alignment moved it
- monthly anchors never persist null (released clients' typia schema only
allows absent-or-numeric): undefined resets everywhere, null stripped at
the action-creator boundary, model reverted to master signature
- round-trip guard ignores BYSETPOS=0 so the cleaned rule is emitted
instead of being preserved verbatim via raw override
* docs(wiki): fix MD024 duplicate headings on repeating-tasks page
* fix(task-repeat): harden rrule save guards + restore preset labels on reopen
Correctness (from deep review of the branch diff):
- reject COUNT combined with repeatFromCompletionDate at save: completion
re-anchors startDate/lastTaskCreationDay, restarting the COUNT window, so
the series would never terminate
- reject parseable rules that yield zero occurrences (e.g.
FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=30): they persisted as silently dead
recurrences with the legacy fallback bypassed
- map the builder's single-weekday BYSETPOS form (BYDAY=FR;BYSETPOS=-1,
'last Friday') onto the legacy nth-weekday anchor; old clients previously
fell back to startDate's day-of-month — a wrong recurrence
Polish:
- infer the preset back from the stored rrule when quickSetting was
wire-clamped to CUSTOM, so Weekends & co. reopen under their label
instead of the raw builder; new QUICK_SETTING_PRESETS single source
replaces the hand-maintained KNOWN_PRESETS set
- extract shared safeParseRRuleOptions + FREQ_TO_CYCLE (six drifted
hand-rolled parse wrappers); memoize isRRuleValid (per-cfg routing guard
on every overdue scan); share noonUtc/toLocalNoon between engine and
preview; fold toMonthArray onto toNumArray; merge the duplicate
_toPersisted action-creator helpers
- trim the wiki section documenting the create-for-each-missed feature
that was deliberately cut from this PR
* fix(task-repeat): address curator review on anchor validity + fallback phase
- never emit/persist an out-of-union monthlyWeekOfMonth: range-guard the
BYDAY ordinal in rruleToLegacyTaskRepeatCfg (the builder's custom input
allows ±5, the synced model only 1..4|-1), sanitize null AND out-of-range
anchors at the action-creator boundary, and mirror the repair in
data-repair — an out-of-union value trips released clients' typia
validation/repair flow
- reject sub-daily frequencies (raw override FREQ=HOURLY/...) at save: the
day-granular engine would accept but silently collapse them to ~daily,
and they have no legacy repeatCycle for old clients
- log hygiene: the update-all-instances flow now logs changed keys + task
ids instead of raw changes/task objects (title, notes and rrule body are
user content; the log history is exportable); engine warns log the error
name only, since rrule.js messages can embed the free-text rule body
- align single-BYDAY weekly rules onto the engine's first occurrence: the
engine groups weeks by WKST while the legacy fallback counts rolling
7-day blocks from startDate, so biweekly cfgs whose start is off the
pattern day fired on OPPOSITE alternating weeks on old vs new clients
(and WKST shifted the engine's phase, which legacy cannot express);
after re-anchoring the cadence is exactly interval*7 days in both
- diff dialog saves against the PROCESSED cfg: the lazy legacy->rrule
migration (and preset inference) no longer leak into the change set of a
title-only edit — rrule is schedule-affecting, so that leak relocated
today's live instance on unrelated edits; empty change sets skip the
update dispatch entirely instead of creating a no-op sync op
* fix(task-repeat): keep presets rrule-backed + builder mode for completion cfgs
- stop stripping the preset-generated rrule at save: getQuickSettingUpdates
OVERWRITES the rule with the preset's canonical one, so the old 'clear on
switch away from builder' branch broke the every-cfg-carries-its-rrule
contract — and an rrule:undefined clear would not even propagate (dropped
by the op-log JSON wire, leaving remote clients on the old rule); the spec
asserting the stale behavior is inverted
- completion-relative cfgs always open in builder mode: the schedule-type
toggle only exists there, so preset inference (or a stored faithful preset
label) would hide the one control that explains how the cfg fires
- monthly anchor clears: document the wire gap properly instead of flipping
values again — null trips released clients' blocking data-repair confirm
dialog on every sync (typia has no null on these fields), undefined clears
only locally; durable clears require shipping '| null' on both fields in a
release FIRST, then switching the reset value (sequenced migration note in
the model + an op-log JSON round-trip spec pinning current behavior)
- replace 6 hardcoded rrule-builder placeholders with translation keys
- revert the settings help text advertising the deferred @+ short syntax
* test(e2e): drop CUSTOM weekday-checkbox spec superseded by rrule builder
The #8025 e2e (merged in from master) drives the legacy CUSTOM recurring
form's cycle select + weekday checkboxes, which this branch removes — the
RRULE builder replaces that UI and its weekday toggles are covered by
rrule-builder-roundtrip.spec.ts. The #8025 failure mode (a saved weekly
cfg that never fires) is blocked generally at save by the first-occurrence
probe.
* test(e2e): de-flake worklog history day-row wait
The worklog is rebuilt from the archive on the history navigation, so the
day row only appears once that async load + month-expand animation settles.
The bare `waitFor` fell back to the 15s action timeout, which CI load
(prod build, 3 workers) could exceed. Use a web-first `expect().toBeVisible()`
with 30s headroom instead.
* fix(task-repeat-cfg): clear repeat-from-completion when switching to a preset
The "from completion" schedule-type toggle exists only inside the RRULE
builder. Selecting a preset hides it, but the flag stuck on the cfg, so a
preset could keep firing relative to completion with no visible control.
Clear it at the save() edit boundary, but only when actually set, so an
untouched preset save stays an empty-diff no-op (#7373 class) rather than
dispatching a spurious undefined->false op. The ADD path already starts from
a false default, so the dialog is the only path that needs it.
Also reword the monthly-anchor clear comments: the cross-client divergence on
an nth-weekday -> day-of-month switch is an inherent, unfixable limitation for
pre-rrule clients (no in-schema "no anchor" value; a `| null` migration would
help an empty client band), not a deferred TODO. Make the round-trip test pin
this explicitly and start the monthlyLastDay case from `true` so it proves the
`false` clear survives the wire instead of passing vacuously.
|
||
|
|
5314126b93
|
feat(azure-devops): configure auto import limit (#8092)
* feat(azure-devops): configure auto import limit #7017 * fix(azure-devops): translate auto import limit form #7017 --------- Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
5be516a02c
|
fix(calendar): disable iCal polling on Android (#8173)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
a0374e2769
|
fix(markdown): toggle fullscreen checklist labels #5950 (#8109)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
e1499af73a
|
docs(calendar): note per-device credentials (#8185)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
ba31037d0f
|
fix(worklog): avoid misleading export start/end times (#8090)
* fix(worklog): avoid misleading export times #5654 * chore: rerun worklog export checks * fix(worklog): show session times once per day #5654 --------- Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
3ef8413b57
|
fix(config): clear shortcut with delete keys #5604 (#8142)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com> |
||
|
|
f0a9e38c20
|
fix(backup): add Android automatic backup restore (#8066) (#8158)
* build: drop unused elevate.exe from Windows build #8135 elevate.exe is bundled by electron-builder's NSIS target solely so electron-updater can apply privileged per-machine updates. We ship no in-app auto-updater (electron-updater is not a dependency; the autoUpdater block in electron/start-app.ts is commented out), so the binary is unused dead weight and a frequent AV false-positive. Set packElevateHelper: false to stop bundling it; safe because perMachine is not true. * docs: flag task component (perf) and sync system (correctness) as high-risk areas * fix(backup): add Android automatic backup restore Adds a settings action for restoring the latest Android automatic backup without dirtying config state. Reuses the existing mobile backup import flow and documents the restore path. * fix(backup): harden manual mobile backup restore Require a usable mobile backup before restoring from Settings and use confirmation copy that warns current local data will be replaced. * refactor(backup): address review on mobile restore action Restore the config-section items-vs-customSection render invariant (the action row is a sibling block, so loosening the guard was unnecessary) and document why the isUsableBackupStr gate is not redundant with the ring loader (selectBestBackupStr can return a corrupt fallback blob). * feat(backup): confirm restore success and warn about cross-device replace - Show a success snack after a Settings restore completes (was silent). - Make the confirm copy honest that restore propagates to other synced devices on next sync; document the same in the wiki. - Add config-section.component.spec covering the onAction pending/error paths. - Note that the iOS branch is intentionally unwired (Android-only per #8066). * refactor(backup): dedupe mobile backup-load ternary Extract _loadBestMobileBackupStr() — the identical isAndroidWebView ? loadBackupAndroid() : loadBackupIOS() ternary was duplicated across the startup and settings restore paths. * test(config): destroy fixture in config-section spec teardown Defensive teardown so the spec can't leak DOM/fixture state into later specs in the shared Karma run. * test(layout): restore document.activeElement clobbered in focus specs The Focus restoration specs override document.activeElement via Object.defineProperty but never restored it, so the static value leaked into the shared Karma session and made TaskService.focusTaskById specs fail intermittently (random spec order). Delete the override in afterEach, mirroring task-shortcut.service.spec. |
||
|
|
0e04e8feaa
|
feat(tasks): checklist progress, bulk actions & notes editor UX (#8156)
* build: drop unused elevate.exe from Windows build #8135 elevate.exe is bundled by electron-builder's NSIS target solely so electron-updater can apply privileged per-machine updates. We ship no in-app auto-updater (electron-updater is not a dependency; the autoUpdater block in electron/start-app.ts is commented out), so the binary is unused dead weight and a frequent AV false-positive. Set packElevateHelper: false to stop bundling it; safe because perMachine is not true. * docs: flag task component (perf) and sync system (correctness) as high-risk areas * feat(tasks): show checklist progress indicator on task cards Derive checklist progress from a task's markdown checklist notes and surface a compact "done/total" badge in the task controls. The badge switches to a completed state when every item is checked and opens the notes panel on click, making checklist progress visible at a glance without opening the task. https://claude.ai/code/session_0131giFn7PwH5AFMUpSQXdU2 * feat(tasks): add checklist reorder and bulk actions in notes Add drag-to-reorder for checklist items in the notes editor and a checklist actions menu (Check all / Uncheck all / Clear completed), shown only when the notes are recognized as a markdown checklist. All operations are backed by pure, unit-tested helpers that manipulate the existing markdown notes string and emit through the normal change flow, preserving interleaved prose lines. https://claude.ai/code/session_0131giFn7PwH5AFMUpSQXdU2 * refactor(tasks): harden checklist reorder and add drop indicator Review follow-ups for the checklist actions: - guard moveChecklistItem against NaN/non-integer indices (a NaN drop index previously slipped past the range checks and corrupted ordering) - show a drop-target highlight while dragging a checklist item - drop redundant modelCopy.set (the model setter already syncs it) - add component tests for bulk actions and the drag-and-drop flow https://claude.ai/code/session_0131giFn7PwH5AFMUpSQXdU2 * refactor(tasks): remove checklist drag-to-reorder, refine card icon Remove native drag-to-reorder for checklist items in the notes editor (drag handlers, drop indicator, the moveChecklistItem helper, and the related styles and tests). The checklist bulk-actions menu (check all / uncheck all / clear completed) and the task-card progress badge are kept. On the task card, the checklist progress badge now stands in for the plain 'chat' notes indicator: when a task has a checklist the redundant notes icon is suppressed, and the badge itself yields to the close (X) button when the detail panel is open, mirroring how the notes icon behaves. * feat(tasks): replace unmodified default notes template on checklist toggle When the notes field shows only the app's stock default template (and it has not been edited), the checklist button now replaces that placeholder with a fresh checklist instead of appending a checkbox below it. Edited content and user notes are preserved and appended to as before. Adds a `defaultText` input on inline-markdown so the task detail panel can pass the (replaceable) stock template, guarded by isStockNotesTemplate so a user-customized template is treated as real content. * fix(tasks): indent list line on Tab regardless of cursor column handleTabKey previously only indented when the cursor sat at line start or at the end of an empty prefix, so pressing Tab mid-item moved focus out of the editor instead of indenting. It now indents whenever the collapsed cursor is on a list line, returning null only for multi-char selections or non-list lines. * feat(tasks): hint "Notes / Checklist" in empty notes header When a task has no notes yet, the notes section header now reads "Notes / Checklist" instead of "Notes", hinting that the section can hold either a free-form note or a checklist. Plain notes still show "Notes" and a recognized checklist still shows "Checklist". * feat(tasks): limit checklist item click target to checkbox and text Checklist item text was rendered as a bare text node inside the block-level row, so clicking anywhere on the row (including empty space) toggled the item. Wrap the text in a <span class="checkbox-label"> and only toggle when the click lands on the checkbox icon or that label; clicks on the rest of the row fall through to opening the editor. Cursor affordance is moved off the row onto the checkbox and label accordingly. * fix(tasks): map checklist toggle to the correct source line _handleCheckboxClick mapped the Nth rendered checkbox to the Nth source line matching `line.includes('- [')`, which also matches non-task lines such as markdown link bullets (`- [text](url)`) or prose. That shifted the index so clicking a checkbox could toggle the wrong line (often a no-op). Use the anchored `isChecklistItemLine` predicate — the same one the bulk-action helpers use — so source lines align with the rendered checkboxes. Applied to both the inline editor and the fullscreen markdown dialog (duplicated logic). * refactor(tasks): unify checklist predicate and dedupe toggle logic Addresses multi-review feedback (W1/W3/S1): - Single source of truth for "is a checklist line": isMarkdownChecklist, markdownToChecklist and getChecklistProgress now use the checklist-operations predicates instead of three divergent definitions. Tightened CHECKLIST_ITEM_RE to /^\s*- \[[ xX]\]/ so it matches exactly what marked renders (verified: marked treats - [ ]/- [x]/- [X] as tasks but NOT - []). This also fixes the badge not recognizing uppercase [X] checklists. - Extracted toggleChecklistItemAtIndex() into checklist-operations and use it in both _handleCheckboxClick copies (inline editor + fullscreen dialog), removing the duplicated index-mapping + string-toggle. The helper flips only the checkbox marker (item text containing [ ] is safe) and handles [X] uncheck — both pre-existing bugs in the old inline toggle. - getChecklistProgress counts in a single pass over the lines (no intermediate markdownToChecklist array / throwaway substring allocations) — it runs on the task-card hot path. * fix(tasks): match checklist predicate exactly to marked's task rendering Final review follow-up. The line predicate now requires "- [marker] <content>" (marker box + whitespace + a non-space char), matching what marked actually renders as a checkbox — verified against marked across 15 cases. Previously an empty "- [ ] " placeholder line (or "- [x]a" with no space) was counted as an item even though marked renders no checkbox for it, which could desync the Nth-checkbox -> Nth-line mapping (e.g. "- [ ] \n- [x] real": marked shows 1 checkbox, predicate matched 2, so clicking the real item toggled the empty line). Tightening CHECKLIST_ITEM_RE/CHECKED_ITEM_RE closes that gap and makes the "in sync with marked" comment accurate. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
eff41c041d
|
feat(project): project completion experience (#8036)
* feat(project): add project completion with celebration + trophy view
Completing a project marks it done and archives it, with a celebration
dialog (confetti + live stats) and a prompt to resolve unfinished tasks
(move to Inbox / mark done). Completed projects show a trophy badge and
a Reopen action on the archived-projects page.
isDone stays distinct from isArchived; selectArchivedProjects is left
intact so completed projects' tasks stay filtered out of Today/Overdue.
Append/merge deferred to #8032. Plan: docs/plans/2026-06-05-project-completion.md
* refactor(project): drop Archive menu item; Complete is the retire path
Archive and Complete produced near-identical end states; collapse to one
user-facing action. Removes the 'Archive project' menu item and handler so
Complete is the single way to retire a project. The archiveProject action,
reducer and ProjectService.archive() stay (needed for op-log replay of
historical archive ops and the legacy unarchive/restore path). Wiki updated;
menu specs cover the Complete flow.
* fix(project): keep completion dialog open for the active project
MatDialog closeOnNavigation (default true) dismissed the celebration the
moment completing the currently-active project navigated to '/'. Navigate
first, then open the dialog.
* test(project): e2e for completion flow (complete, celebrate, reopen)
Covers the resolve-unfinished-tasks prompt, the celebration dialog with
stats, the trophy badge on the archived page, and reopening. Adds an
openProjectContextMenu page-object helper.
* feat(project): confirm project completion
* feat(project): show completion celebration fullscreen
* fix(project): harden completion celebration flow
* style(project): use spacing tokens in completion dialog
* fix(project): align completion screen with project context
* style(project): soften completion screen coloring
* style(project): reuse completion screen surfaces
* fix: refine project completion dialog actions
* fix: complete projects with atomic task resolution
* fix: restore archive path in project completion UI
* refactor(project): remove dead completion code
The project-level completeProject action (OpType.Update) was never
dispatched — completion goes exclusively through the atomic
TaskSharedActions.completeProject (OpType.Batch) meta-reducer. Drop the
dead action, its reducer case, the PROJECT_COMPLETE enum member and the
immutable 'PCO' op-log code (which would otherwise be permanently
reserved for an op that can never be produced); enum count 147->146.
Also remove the unused selectCompletedProjects / selectPlainArchivedProjects
selectors (no consumers) and the misspelled, unused 'angel' confetti
field, keeping the regression tests that guard selectArchivedProjects.
* fix(project): make project completion non-reversible
Completion resolves a project's open tasks (move-to-inbox / mark-done),
which reopen cannot truly restore, so the "Reopen"/"Undo" affordances on
the completion path were misleading. Drop the celebration dialog's Reopen
button and the post-complete undo snack; the fullscreen celebration is
the feedback and deliberate reactivation still lives on the
archived-projects page (Project.reopen kept for it).
Also restore the project title param on the archive confirm dialog (it
was rendering a raw {{title}} placeholder), remove the now-unused
moveTasksToInbox / markTasksDone resolution helpers (the meta-reducer
resolves tasks atomically), and drop the orphaned UNDO / S.COMPLETED
i18n keys. Updates the completion e2e to the close flow and asserts the
resolution props are forwarded to the atomic action.
* fix(tasks): cancel native reminders for project-completed tasks
Completing a project marks its unfinished tasks done inside the
meta-reducer (no per-task updateTask), so unscheduleDoneTask$'s
native-reminder cancellation is bypassed and an OS-scheduled Android
notification could still fire for a now-done task. Add a local-only
effect that cancels native reminders for the force-completed task ids.
Local-only by design: it dispatches no actions (the persistent
dismissReminderOnly/clearDeadlineReminder would each be an extra synced
op), and done tasks are already filtered from reminders$ on all
platforms — only the native Android notification needs explicit removal.
* test(project): drop TaskService spies orphaned by helper removal
getByIdWithSubTaskData$/moveToProject/setDone/setUnDone were only used
by the removed moveTasksToInbox/markTasksDone helpers and their deleted
tests.
* feat(sync): affectedEntities multi-entity conflict detection for atomic completion
WIP checkpoint of the atomic completeProject approach. The Batch op declares
every touched entity (PROJECT, INBOX, TASKs, TODAY_TAG) via a new
affectedEntities field threaded through op-log capture, conflict detection,
the sync server (+Prisma migration) and shared-schema. Per-effect
completeProject handlers (issue two-way-sync, time-block, repeat-cfg)
re-derive the task changes the atomic op bypasses.
* revert(sync): remove affectedEntities multi-entity conflict detection
Reverts
|