mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
798 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a8f29a3f62 | test(sync): avoid conflict-gate e2e deadlock | ||
|
|
0df3e81155 |
fix(sync): remediate multi-agent review findings
- USE_REMOTE: fix guaranteed self-deadlock — flushPendingWrites() was called while already holding the non-reentrant sp_op_log lock its own Phase 2 re-acquires, so every "Use Server Data" hung 30s and aborted. New OperationWriteFlushService.flushThenRunExclusive owns the bounded flush->lock->recheck barrier; ServerMigrationService reuses it, which also bounds its previously unbounded snapshot-cutoff retry loop. - USE_REMOTE: make the rebuild crash-resumable. A raw_rebuild_incomplete META marker is set atomically with the baseline replacement and cleared after the replay commits; the next sync redoes the raw rebuild instead of the normal download (which excludes own ops server-side and would silently lose the un-replayed suffix). The resume keeps the first attempt's pre-replace backup instead of overwriting the single slot with the partial baseline. - Deferred actions: serialize overlapping drains (two concurrent drains could mint two ops for one user intent), stop the drain at the first transient failure instead of persisting successors out of order (the older edit would win LWW everywhere via clock inversion), abandon permanently invalid actions after one attempt (typed PermanentDeferredWriteError) instead of retrying + snacking on every sync forever, and latch the buffer-size warnings to once per stuck window (previously a blocking dev dialog per action past 100); drop the no-op 5000 "hard cap" tier. - writeOperation: post-append bookkeeping failures no longer propagate into the deferred retry loop, which re-appended the same action under a fresh op id (double-apply of additive payloads). - remote-apply: full-state cleanup retains every full-state op of the current batch, so an archive_pending import can no longer be deleted before its archive retry (startup replay would lose a change already visible at runtime). - Hydration: sanitize a malformed stored schemaVersion per-op instead of failing the whole boot into recovery; strict parsing stays on the receive/upload paths (0 still parses so a below-minimum remote op surfaces as VERSION_UNSUPPORTED, not a generic migration failure). - Server: request fingerprints are computed lazily — only when a dedup entry exists, and otherwise after the rate-limit/pre-quota gates but before uploadOps mutates the parsed ops. Fixes the pre-gate hashing of full payloads (authenticated DoS-cost regression) and repairs three sync-fixes retry tests to model true identical-body retries. - Delete dead code: failed-op-ids util (+spec) and MAX_VERSION_SKIP (the removed forward-compat band); fix the stale clock-merge parity comment. sync-core 210/210, shared-schema 50/50, super-sync-server 825/825, and all touched Angular specs pass. |
||
|
|
d212740254 |
test(e2e): resolve A's post-import conflict as USE_LOCAL in gate test
Client A already synced a populated state, so importing a backup diverges
from the server and A's own sync raises the sync-import conflict gate.
syncAndWait() defaulted to USE_REMOTE, which discarded A's import before it
reached the server, leaving Client B with nothing to conflict against, so
the PHASE 5 conflict dialog never appeared (run 29090279243, SuperSync 1/6).
Pass { useLocal: true } so A force-uploads the import as a new SYNC_IMPORT
the server keeps, letting B's pending simple-counter change trigger the
conflict dialog as intended.
|
||
|
|
f38342eeb9 |
fix(sync): harden replay and conflict recovery
Make remote replay and authoritative state replacement crash-resumable and atomic. Preserve pending changes until piggyback processing commits, and stop cleanly on incompatible operations. Validate sync identities and schemas while strengthening request deduplication and privacy-safe diagnostics. |
||
|
|
6e6ce34c15 |
test(e2e): incoming SYNC_IMPORT must prompt on pending non-task work
Regression test for the widened incoming-full-state conflict gate and the piggyback pre-upload pending-snapshot fix: a pending SIMPLE_COUNTER change (non-TASK/PROJECT/TAG/NOTE) must trigger the conflict dialog instead of being silently discarded by an incoming SYNC_IMPORT from another client. Not yet run (SuperSync docker stack is unavailable in this sandbox); run via npm run e2e:supersync:file or the E2E Tests (Scheduled) workflow. |
||
|
|
7f604f68f7
|
feat(task-repeat): default skip-overdue for new everyday recurring tasks (#8861)
* feat(task-repeat): default skip-overdue for new everyday recurring tasks
New recurring configs now default skipOverdue ("Don't let overdue instances
pile up") to ON only for a plain everyday schedule — the "Daily" preset or a
CUSTOM every-single-day cycle. That is the one case where the option is both
useful and provably safe: everyday tasks are the only schedule that actually
piles up (one empty overdue copy per missed day), and today is always
scheduled so a missed instance regenerates the same day and can never
silently vanish (it cannot even drop to zero).
Every other schedule stays OFF — workday/weekly/monthly/yearly and every-N-day
custom cycles keep their one missed occurrence visible, so a real obligation
(pay rent on the 1st, renew the domain) never disappears until its next
occurrence. Deriving the default purely from the effective schedule means the
"Daily" preset and a CUSTOM every-day cycle (the same schedule entered two
ways) get the same default — no "same schedule, different default" surprise.
The default is seeded from the chosen schedule in both config-creation paths:
the repeat dialog (re-derived from the final schedule on save, and skipped
when the user explicitly toggled the Advanced checkbox) and the inline
add-task-bar recurrence. Existing configs and DEFAULT_TASK_REPEAT_CFG
(skipOverdue: false) are unchanged; only newly created configs are affected.
Supersedes the broader daily+Mon-Fri variant, dropping the schedule-change
checkbox re-sync machinery: an Advanced checkbox opened on a Daily config and
then switched may briefly show a stale ON, but save always persists the safe
re-derived value.
* docs(task-repeat): clarify custom every-day default; signpost baseline
Multi-review follow-ups (no behavior change):
- Wiki 2.06 + 4.13: a CUSTOM every-single-day cycle also defaults skipOverdue
ON (it is the same schedule as the Daily preset); "off" now reads "custom
cycles longer than a day" so the docs match getDefaultSkipOverdue.
- Comment on DEFAULT_TASK_REPEAT_CFG.skipOverdue pointing to the schedule-aware
creation default, so the model's `false` baseline is not mistaken for the
effective default.
- Tighten the save() display-gap comment: the cosmetic seeded-ON/persist-OFF
gap applies to any new non-Daily config whose Advanced panel is opened
untouched, not only a Daily→switch.
* refactor(task-repeat): use type-only import in skip-overdue predicate
TaskRepeatCfgCopy is used only in type position; match the codebase's import type convention (58 other files). No behavior change.
|
||
|
|
6236dc7458
|
fix(tasks): add sub-task submit button and commit-on-blur on touch (#8860)
Android/mobile soft keyboards hide the Enter key or deliver it as a composing keydown that onKeydown filters out (to protect CJK candidate confirmation), so the inline sub-task draft could not be committed at all: neither Enter, tapping away, nor any on-screen control saved the text. - Add an always-visible submit button beside the input as an accessible, discoverable commit target (mouse-clickable, screen-reader labelled). Its mousedown is preventDefaulted so a desktop click keeps input focus, commits, and keeps the field open for rapid entry — mirroring the main add-task bar. On touch the tap commits via the blur path below. - Commit the draft on blur, but only on touch (isTouchActive): the natural mobile "done" gesture is tapping away, and the soft-keyboard Enter is unreliable there (#8791). Desktop keeps click-away-to-cancel — Enter and the button are the reliable commit paths — so blur no longer silently creates a task, and a user can move to the button without the draft being committed out from under them. Escape still discards on all devices. Both commit paths read the live input value, so IME/predictive-text buffering no longer strands the text. Closes #8856. Supersedes the standalone #8791 commit-on-blur branch. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b7839e9615 |
fix(e2e): accept overwrite confirmation in provider-switch conflict test
Since #8785, a fresh client with no last-synced baseline gets an OVERWRITE_WARNING_UNKNOWN confirmation after clicking Keep remote. The test never accepted it, so the confirm backdrop blocked the later triggerSync() click and timed out. Accept the confirmation, mirroring the sibling webdav conflict tests. |
||
|
|
994f0522ff
|
feat(tasks): redesign add-task-bar layout, toggles, note field & a11y (#8812)
* feat(tasks): move add-task-bar toggles into the actions row The add-to-top/bottom and search toggles used to overlay the input's top-right corner. They now sit at the start of the actions row (create mode) and the search info row (search mode), prepended so they scroll together with the rest of the row, separated by a dashed divider and sized to match the neighbouring action chips. A new ng-content slot on add-task-bar-actions lets the parent project the toggles into the scrollable row; they are still rendered in the search row when the action bar is not, so search stays toggleable. * feat(tasks): wrap long add-task-bar titles onto multiple lines The title field is now an auto-growing textarea (cdkTextareaAutosize) so a long title wraps into view instead of scrolling off the right edge, which is especially helpful on narrow screens. It stays single-line in meaning: Enter still submits and pasted newlines are collapsed to spaces so no line break ever reaches the task title. cdkTextareaAutosize sizes the textarea to rows*line-height and ignores its own padding, so the vertical breathing room lives on a wrapper element and the field grows a line at a time. matInput is dropped (there is no mat-form-field here and it broke the autosize height calc). E2E selectors that targeted the title by the `input` tag now use the tag-agnostic `.main-input` class. * feat(tasks): refine add-task-bar toggles, note field and input autosize - Move the note control from a labeled chip into an icon toggle in the left group (search · note · add-to-top/bottom); hidden in search mode. - Renumber the input shortcuts: Ctrl+1 add-to-bottom, Ctrl+2 search, Ctrl+3 note, Ctrl+4-9 the action chips. - Keep the submit (+) button's space reserved while empty via opacity (visibility is re-asserted by mat-icon's anti-FOUC rule, so it stayed visible). - Animate the note field expand/collapse with the shared @expandFade. - Fix a cdkTextareaAutosize height lag: the directive measures the textarea with its horizontal padding stripped but its width unchanged, so padding on the textarea made the measured wrap width wider than the rendered one and the field grew a few chars after the text had already wrapped (clipped line + scrollbar). Keep the title and note textareas padding-free and move the gutters to their wrappers so measured and rendered wrap widths match. * feat(tasks): improve add-task-bar a11y, shortcut order and toggle emphasis - Renumber the input shortcuts to match the on-screen left-to-right order: Ctrl+1 search, Ctrl+2 note, Ctrl+3 add-to-top/bottom (chips stay Ctrl+4-9); update the shortcut hints embedded in the tooltips accordingly. - Add aria-label to every icon-only button and aria-pressed to the search and note toggles so screen readers announce name and on/off state. - Take the invisible submit (+) out of the tab order (disabled while empty). - Guard the note toggle (and its Ctrl+2) to create mode; it is a no-op while searching, where the note field does not render. - Escape now dismisses an open task-suggestion list before closing the bar. - Un-dim the toggles (0.6 + :focus-visible so keyboard focus isn't faint) and give the submit + full-strength primary tint so the mobile CTA reads as the primary action. - Use the edit_note glyph for the note toggle (clearer than a chat bubble). * refactor(tasks): apply add-task-bar multi-review cleanups - De-duplicate the tooltip/aria-label expressions with @let (search, backlog, add-to-bottom) so the two attributes can never drift. - Split the Ctrl-shortcut map into local toggles (1-3) and action shortcuts (4-9) so stopPropagation derives from structure instead of a parallel key array that had to be kept in sync. - Restore the "note attached" cue: the note toggle carries .has-value (undimmed + tinted) when a collapsed note still holds text, distinct from the .active pill shown while the note field is open. - Remove dead code: the vestigial switch-add-to-btn class on the submit button, the unused .search-input rule, a stale duplicated SCSS comment, and the orphaned NOTE_BUTTON translation key. - Document that the projected .inline-action-controls styles must stay in the parent stylesheet (ng-content keeps the parent's encapsulation). * feat(tasks): use notes bubble icon and restore expanded note draft Match the notes icon used across the task feature (task list item, detail panel, archived-task dialog) instead of the one-off edit_note glyph. The glyph flips outline -> filled once the note field has text, mirroring the detail panel's empty / has-notes states. Also start the note field expanded when reopening the bar with a persisted draft note, so it is visible rather than hidden behind the collapsed toggle. * fix(tasks): repair startup-overlay selector for add-task-bar textarea The add-task-bar title field became a <textarea class="main-input">, but StartupOverlayService still queried 'add-task-bar.global input'. A CSS input type selector does not match a textarea, so on Android cold-start the partial- text handoff (cursor position + focus) never ran and the overlay cleared only via the 3s safety timeout. Target .main-input and retype to HTMLTextAreaElement. Also address multi-review nits on the same branch: - guard expandNote() in search mode so Ctrl+Enter cannot strand isNoteExpanded (mirrors toggleNote()), with a spec covering it - drop the unused fadeAnimation import and animations entry - type the inputEl viewChild as ElementRef<HTMLTextAreaElement> (matches noteEl) - repoint WorkViewPage.addBtn to .e2e-add-task-submit (.switch-add-to-btn now belongs to the add-to-backlog toggle) * test(tasks): clear add-task-bar sessionStorage between component specs * docs(tasks): update add-task-bar keyboard shortcuts for new Ctrl mapping |
||
|
|
b3e3722425 |
test(e2e): re-issue finish-day click until daily-summary nav starts
The finish-day button is a routerLink inside an @if (hasDoneTasks()) /
@else swap, so marking a task done re-creates the element. A one-shot
click can land before Angular wires the new element's routerLink and
silently no-ops, hanging archiveDoneTasks for the full 30s waitForURL.
Retry the click via expect().toPass() until navigation actually starts.
Recurrence of the race first addressed in
|
||
|
|
b60845b42c
|
test(sync): reliably dismiss setup-time E2EE dialog in webdav e2e helper (#8727)
#8709 added a mandatory disableClose "Encrypt before first upload?" modal on every fresh file-based sync setup. The setupWebdavSync helper dismissed it with locator.isVisible({ timeout }), but isVisible() returns the CURRENT state immediately and never polls — the timeout is effectively ignored. The modal opens only after save()'s awaited provider-auth check and a lazy import() of the dialog chunk, so it appears a beat after the Save click; isVisible() raced it, returned false, and the Skip click was never issued. The leftover backdrop then blocked every following interaction, failing ~11-12 @webdav specs per run with "cdk-overlay-backdrop intercepts pointer events" and conflict dialogs that never appeared. It passed only when the modal happened to already be up at the instant of the check, which is why the failures looked flaky. Use waitFor({ state: 'visible' }), which actually polls until the modal appears, then click Skip and confirm it closes. Apply the same wait to the encrypt-at- setup fill path. Verified green via a full @webdav CI run. |
||
|
|
10a47888a6
|
feat(sync): offer E2EE before first upload for file-based providers (#8709)
* feat(sync): offer E2EE at setup for file-based providers File-based providers (WebDAV, Nextcloud, Dropbox, OneDrive, local file) support optional client-side encryption but, unlike SuperSync, have no mandatory-encryption upload guard — so their first setup sync would ship plaintext before a user could enable E2EE. Offer to set an encryption password during first-time setup and persist it as part of the SAME config save: the key goes to the provider's privateCfg and isEncryptionEnabled to the global config, atomically with isEnabled. The normal first sync then encrypts from the first op via the standard download-first flow — no separate snapshot-overwrite and no plaintext-upload race. Skipping keeps the existing unencrypted behavior; works offline too. Reuses DialogEnableEncryptionComponent in a new side-effect-free collectPasswordOnly mode (returns the password, writes nothing), with a provider-aware Skip action for the disableClose setup modal. * test(sync): skip file-based setup encryption prompt in webdav e2e helper Fresh file-based setup now opens the optional "Encrypt before first upload?" dialog before the config is persisted, so setupWebdavSync would stall on the disableClose modal. Dismiss it via a new Skip selector so every WebDAV spec proceeds unencrypted, exactly as before; encryption specs still enable it afterwards via enableEncryption(). * test(sync): add e2e for file-based setup-time encryption Adds a @webdav @encryption spec that configures WebDAV with the encryption password set in the setup dialog, then asserts the first upload's remote sync-data.json does NOT contain the task title in plaintext (compression is off by default, so a plaintext upload would) — proving the first sync is encrypted — and that a second client with the same password decrypts and receives the task without overwriting the remote. setupWebdavSync gains an encryptAtSetup option (fills the new dialog instead of skipping it); adds e2e selectors on the setup dialog password/confirm/ submit controls. * ci(e2e): allow targeting the webdav job via workflow_dispatch grep The webdav e2e job hardcoded --grep "@webdav"; add a webdav_grep dispatch input (default @webdav, mirroring the supersync job) so a specific WebDAV test can be run on demand. * test(sync): fix remote sync-file path in setup-encryption e2e Non-production builds nest the file under a /DEV segment (sync-providers.factory.ts). The raw-fetch assertion omitted it and 404'd; the CI trace confirmed the app uploaded to <folder>/DEV/sync-data.json and that the first upload was encrypted. * test(sync): cover joining an unencrypted remote with setup-time encryption Adds the data-safety edge case for file-based setup-time E2EE: a client that sets a password at setup while joining a remote that already holds UNENCRYPTED data. Asserts the join reads and preserves the existing data (does not overwrite the remote with the joining client's empty state) and that a subsequent write upgrades the remote to encrypted (no plaintext titles remain). |
||
|
|
d00e2df354
|
refactor(ui): unify primary/secondary action button treatment across dialogs (#8706)
Standardize dialog actions: primary=mat-flat-button color=primary, secondary/cancel=mat-button, destructive=color=warn. Drop dead Bootstrap 'btn btn-primary' classes and decorative check/close icons. Document the convention in docs/styling-guide.md and update e2e selectors that keyed on the old stroked variant. Closes #8683 |
||
|
|
63253f8e0c
|
fix(sync): never transmit plaintext operations for E2EE-mandatory providers (#8670)
* fix(sync): never upload plaintext ops for E2EE-mandatory providers SuperSync's first-time setup ran an initial sync (dialog-sync-cfg save -> sync(true)) BEFORE the user chose an encryption password, so all local ops (incl. issue-provider credentials) were uploaded to the server in cleartext. Completing setup deleted them; aborting left them stored indefinitely, breaking the E2EE promise (GHSA-9v8x-68pf-p5x7). Add an optional `isEncryptionMandatory` capability to OperationSyncCapable (true for SuperSync) and refuse to upload in the op-log upload path while no usable key is configured. Downloads still run (merge-first) and the encryption- enable flow performs the first, encrypted upload, so the setup flow and prompt are unchanged. File-based providers, where unencrypted sync is a legitimate user choice, leave the flag unset. * fix(sync): fail closed on plaintext snapshot for E2EE-mandatory providers Multi-review hardening for GHSA-9v8x-68pf-p5x7. The op-upload guard closes the reported leak, but SnapshotUploadService.deleteAndReuploadWithNewEncryption could still push a plaintext snapshot for SuperSync on two adjacent paths: the (today UI-unreachable) disable-encryption flow, and a keyless import declaring isEncryptionEnabled:true. Reject an unencrypted snapshot for an encryption- mandatory provider before any destructive deleteAllData, so the "never transmit plaintext" invariant holds regardless of caller. Also lower the mandatory-encryption upload-skip log from warn to normal: it is an expected by-design skip during the pre-encryption setup window and would otherwise fire on every auto-sync cycle. * fix(sync): consolidate op-log into the encryption-enable snapshot Follow-up to the GHSA-9v8x-68pf-p5x7 upload guard. With the guard, first-time SuperSync setup leaves the whole local history unsynced until encryption is enabled; the enable-snapshot then represents that full state, but the ops were re-uploaded incrementally on top of it on the next sync (redundant server op-log bloat, and previously untested at whole-history volume). deleteAndReuploadWithNewEncryption now captures the pending ops the snapshot subsumes (before the destructive delete, under runWithSyncBlocked + a modal dialog so the set is stable) and marks them synced after a successful upload — mirroring planRegularOpsAfterFullStateUpload in the op-log upload path, which this direct snapshot upload bypasses. Also fixes the same latent redundancy in the enable-from-settings-with-pending-ops flow. Adds a multi-client e2e: local task history exists before first-time encrypted setup, then a second client with the same password receives exactly those tasks with no duplicates, conflicts, or errors. * ci(e2e): add optional grep input to manual SuperSync e2e dispatch * fix(sync): capture subsumed ops before state snapshot to prevent mark-synced data loss deleteAndReuploadWithNewEncryption captured getUnsynced() AFTER the full-state snapshot, so an op created in that window was marked synced yet absent from the snapshot — silently lost. Capture the unsynced set first: every marked-synced op is then guaranteed present in the snapshot, and a concurrent op arriving after the capture is left unsynced and re-uploaded next sync (idempotent by op id). * test(sync): mock WebCrypto so mandatory-encryption guard test reaches the guard The 'enabling without a usable key' case passed isEncryptionEnabled: true but did not mock WebCrypto, so the availability check threw WebCryptoNotAvailableError in non-secure CI before the /unencrypted snapshot/ guard under test. |
||
|
|
44030bb06e
|
fix(op-log): backfill SIMPLE_COUNTER fields on LWW recreate (#7330) (#8646)
Issue #7330 ("Data damage detected ... Repair attempted but failed") recurred on SIMPLE_COUNTER for users already on >= v18.6.0, where the original TASK-only fix didn't reach. Root cause is the same partial-LWW-recreate path: a concurrent delete-vs-update across devices resurrects a counter (LWW resolves local-delete + remote-update to 'remote'), and lwwUpdateMetaReducer recreated it from the {id}-only delete payload. Because SIMPLE_COUNTER had no RECREATE_FALLBACK entry, the recreated counter was missing required fields - most often `type`, an enum typia rejects and that dataRepair/autoFixTypiaErrors had no rule for - so post-sync validation dead-ended on the repair dialog every sync. Fix mirrors the TASK fix, two layers kept in lockstep by requiredKeys: - Register SIMPLE_COUNTER in RECREATE_FALLBACK (defaults from EMPTY_SIMPLE_COUNTER, type=ClickCounter) so the meta-reducer recreate path backfills required fields and the bad state is never created. - Add a simpleCounter.<id>.<field>===undefined branch to autoFixTypiaErrors to heal copies already corrupt on disk. Tests: auto-fix + meta-reducer unit specs (incl. a real-typia appDataValidators.simpleCounter proof), a full validate->repair->validate integration repro, and a deterministic SuperSync delete-vs-update e2e asserting no native repair dialog fires. |
||
|
|
a1c059855e
|
feat(sync): nudge long-time users without sync to set it up (#8637)
* feat(sync): nudge long-time users without sync to set it up Offline-first means a user who never configures sync has no backup at all; clearing browser data or losing the device wipes everything. Once the app has clearly been used for a while AND holds real data, show a calm, low-priority banner once encouraging sync setup. Trigger combines wall-clock age (>= 7 days since a lazily-seeded FIRST_USE_TIMESTAMP) with a task-count gate (>= 20 tasks), so it is robust both to users who restart many times a day and to those who leave the app running for weeks (where app-start count fails), and never nags an empty/dormant install. Shown at most once: both "Set up sync" and "Not now" persist a dismissed flag; a configured provider suppresses it entirely. Reuses the existing banner system and mirrors NoteStartupBannerService. * refactor(sync): apply review feedback to data-safety nudge - Rename LS.FIRST_USE_TIMESTAMP -> SYNC_SAFETY_FIRST_SEEN and document that it is not a true install date (seeded at upgrade time for existing installs), so no other feature reuses it as one. - Tests: add an unhydrated-config (undefined) skip case and assert the task-count selector, guarding the earlier race fix and a selector swap. - i18n: match each locale's existing register for the nudge message (pt/cs/sk/tr -> formal, id -> informal) per translation review. * test(planner): stabilize add-subtask-from-detail focus race Wait for the detail panel's deferred open-time auto-focus to settle before opening the inline subtask draft. The auto-focus lands ~200ms after the panel opens and, if it fires after the draft input is focused, steals focus and blur-closes the input — making toBeFocused() flake with 'element(s) not found'. Mirrors the guard already used in add-subtask-with-detail-panel-open.spec.ts. |
||
|
|
06a7425698
|
feat(focus-mode): make preparation opt-in, smooth start transition (#8639)
* feat(focus-mode): make preparation opt-in, smooth start transition The full-screen preparation countdown is now opt-in (off by default) via a new isShowPreparation config flag; the deprecated isSkipPreparation is kept for synced-config back-compat. By default, starting a session now plays a brief inline rocket launch from the play button, then begins. Smooth the prep->running swap: the clock/controls cross-fade sequentially (old fades out, then new fades in) via a new fadeSwap animation. Fix the focus task-selector panel that rendered transparent (undefined --c-bg-raised) -> opaque highest-elevation surface on the standard scrim. * fix(focus-mode): guard re-entrant start, reduced-motion, clock cross-fade - Ignore a re-entrant startSession() while the inline launch is playing (keyboard Enter / double-click on the still-focused FAB) and disable the play button during launch, so a second timer can't reset the new session. - Skip the inline rocket launch + its 800ms delay under prefers-reduced-motion and start immediately (no invisible dead delay for motion-sensitive users). - Cross-fade the clock digits with the duration slider (fade out before fade in) instead of a hard visibility toggle. - Fix stale e2e launch-duration comment (~600ms -> ~800ms). |
||
|
|
3ae9d968a4
|
fix(tasks): make "Add subtask" work in the Planner detail panel (#8617) (#8630)
* fix(tasks): make detail-panel add-subtask work in the Planner (#8617) The detail panel and context menu delegated "Add subtask" to AddSubtaskInputService.requestOpen(), a signal consumed only by the <task> row that renders the parent. The Planner renders tasks as <planner-task>, so the request was dropped and nothing happened (regression from #8423; Today view still worked). - task-detail-panel now hosts its own inline <add-subtask-input> (works in every view; also fixes the input opening behind the bottom panel on mobile). It controls the sub-task section's expansion via a signal and focuses the input after the expand animation completes; animates in/out with [@expandFade]. - task-detail-item gains expandedChange/afterExpand outputs so the panel can control/observe the Material expansion panel. - context menu addSubTask() reverts to direct addSubTaskTo() (a transient menu has no place to host the inline draft). Adds a Planner e2e repro and updates the affected unit specs. * fix(tasks): address multi-review findings for #8617 add-subtask - Focus the inline input via a deferred timeout in onSubTasksAfterExpand: with animations disabled Material fires afterExpand synchronously within the same CD pass, before the addSubtaskInput viewChild is committed, so the first collapsed→expand "Add subtask" click left the input unfocused. - Reset isSubTasksExpanded on task switch so the sub-task section doesn't stay sticky-expanded across tasks (the panel instance is reused). - Return focus to the "Add subtask" button when the draft is closed via Escape, instead of dropping focus to <body>. - Refresh the now-stale AddSubtaskInputService doc comment. - Assert the draft input is focused in the Planner e2e (the focus path was previously uncovered). * fix(tasks): keep context-menu add-subtask on the inline-draft bus The earlier context-menu change to addSubTaskTo() was unnecessary: the context menu's "Add sub-task" entry is gated behind isAdvancedControls, which only the <task> row enables. planner-task and schedule-event leave it false, so the entry is hidden there — meaning the menu action is only ever reachable from a rendered <task> row, where requestOpen() works. Reverting restores the v18.12 inline-draft UX for that path and shrinks the diff. (#8617 was only ever reachable via the detail panel, which the self-hosted input fix already covers.) * test(tasks): cover add-subtask focus with animations disabled Guards the deferred-focus fix: with animations disabled Material fires the expansion panel's afterExpand synchronously, before the panel's add-subtask-input viewChild is committed. Verified this test fails without the setTimeout deferral and passes with it. |
||
|
|
465ea5b034
|
feat(tasks): add quick "done" icon button to reminder dialog footer (#8595)
* feat(tasks): add quick "done" icon button to reminder dialog footer Promote marking a reminder's task as done from the overflow menu to a one-click icon button beside the snooze split-button. Uses done_all for the multi-task view and check for the single-task view, with a tooltip and aria-label sourced from existing translation keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TYst27M7X994sRwdz8Q6XV * style(tasks): match reminder done button to outlined snooze control The quick done button rendered as a borderless mat-icon-button, clashing with the outlined snooze split-button and filled primary CTA beside it. Switch it to a compact, outlined square (mat-stroked-button) so it reads as a peer of the snooze control. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TYst27M7X994sRwdz8Q6XV * refactor(tasks): equalize reminder done button spacing, drop menu duplicate - Cancel Material's adjacent dialog-action button margin in the footer row so the done button has equal flex-gap spacing on both sides (the split- button host isn't a button-base, so the margin only landed on one side). - Remove the now-redundant done/complete item from the overflow menu since it is a visible button, and drop the orphaned DONE translation key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TYst27M7X994sRwdz8Q6XV * style(tasks): widen reminder footer button gap to var(--s) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TYst27M7X994sRwdz8Q6XV * fix(tasks): show prominent reminder done button only for single task For multiple reminders, demote complete-all back into the overflow menu and rely on the existing per-row check buttons. Bulk-complete is the most destructive footer action with no undo, and 'I finished all of these' is the least likely bulk intent — so it should not occupy a prominent, single-click slot. The visible done button now only appears for a single reminder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TYst27M7X994sRwdz8Q6XV * test(reminders): dismiss deadline reminder via new done button --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d1d6949f43 |
fix(reminders): stop dismissed reminders from re-opening every 10s
An overdue scheduled reminder kept re-opening its modal every ~10s (the reminder worker's check interval) and on every app resume. Dismissing the dialog via backdrop / Escape / Android back runs none of the clear logic (ngOnDestroy only clears deadline reminders), so the worker re-emitted the still-active reminder and the module re-opened the modal indefinitely. On mobile this reads as a fully frozen app where no controls respond. Add a short in-memory UI cooldown: on a passive dismiss, scheduled reminders are suppressed from re-opening for 5 minutes. The reminder itself stays active — it re-nudges after the cooldown and on cold start — and explicit actions (snooze / done / add-to-today / reschedule) are unaffected. The cooldown is presentation-only; no synced state is touched. Related to #8551 (a dropped done-from-notification leaves a task overdue and active), the common way to reach this state on Android. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0a6a692187 |
test(sync): harden supersync e2e against flaky late decrypt dialog
The scheduled master E2E (run 27927390789, SuperSync 3/6 shard) flaked on "Bidirectional sync works after encryption change via import". The stuck overlay was the "Decryption Failed" dialog (dialog-handle-decrypt-error): it fires asynchronously after a re-sync, and the setupSuperSync dialog loop only handles it within its bounded rounds. A late re-fire was never dismissed, so the final one-shot expect(...).toHaveCount(0) watched the disableClose dialog for the full 15s and timed out. Replace the one-shot late-dialog wait + close assertion with a converging expect.toPass() poll that dismisses whichever late, non-self-closing dialog is present each round (enable-encryption or decrypt-error), then re-checks the overlay is empty — handling appearances that land early, late, or never. Extract the decrypt-error handling into a shared _handleDecryptErrorDialog helper reused by the loop and the drain. Verified via prettier/eslint/tsc; not run live (the @supersync docker e2e can't reach the server from the dev sandbox). |
||
|
|
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> |
||
|
|
b8d1dbe261 | test(e2e): harden archiveDoneTasks against finish-day navigation race | ||
|
|
a3327be73d
|
fix(focus-mode): preserve flowtime break settings across mode switch (#8489)
* fix(focus-mode): preserve flowtime break settings across mode switch Switching the Flowtime break mode (Ratio-based <-> Rule-based) hides the inactive section. Formly resets hidden fields by default, wiping the user's break rules / percentage on a Rule -> Ratio -> Rule round-trip. Set resetOnHide: false at every level of the breakRules repeat (field, fieldArray and each inner input) plus on breakPercentage, so the hidden side keeps its values. Setting it only on the top-level field preserves the rows but strips their values, which is why a heavier cache/restore approach appeared necessary. Adds regression specs that drive the real breakMode control (not the internal model) so they reproduce the dialog's actual behaviour. Part of #7581 * test(focus-mode): real-DOM e2e for flowtime mode-switch preservation Drives the actual mat-select Rule -> Ratio -> Rule round-trip and a real backspace-clear in the running app. Verified to fail without the resetOnHide fix (rule inputs are stripped after the round-trip), so it guards the #7581 regression at the layer where it actually reproduced. |
||
|
|
d0da0aea93
|
Codex/issue 8081 keep subtask input open (#8423)
* fix(tasks): keep subtask input open after add
* fix(tasks): keep subtask input open after add
* perf(tasks): avoid task row effect reactivity
* fix(tasks): address subtask input review
* feat(tasks): ease in the inline subtask input instead of popping
* fix(tasks): consume inline subtask-input request to prevent stale focus-steal
Address multi-review findings on the inline subtask input:
- The open request was held in a signal that was never cleared, so a task row re-created with the same id (e.g. navigating away from a project and back) re-ran its open effect on init and re-opened the input, stealing focus with no user action. Reset via consume() once the row acts on it.
- Drop the redundant requestId counter (a fresh request value already re-fires the effect); request payload is now just the parentId string.
- Add an aria-label to the input (placeholder is not an accessible name).
- _commit() returns void (its boolean result was unused).
* feat(tasks): animate the inline subtask input out and tighten its top gap
- Use the expandFade animation (enter + leave) so the input collapses out instead of vanishing. Caveat: when the input is the only thing in .sub-tasks (adding the first subtask, then cancelling without creating any), the enclosing @if collapses in the same tick and Angular skips the child leave animation, so it's instant in that one case.
- Reduce the input's top margin from --s-half (4px) to --s-quarter (2px).
* feat(tasks): refocus the task row when the subtask draft is cancelled with Escape
The inline subtask input's 'closed' output now reports why it closed ('escape' vs 'blur'). On Escape (a keyboard cancel) the host task calls focusSelf() so keyboard navigation continues from that row; on blur it doesn't, since focus already moved elsewhere. focusSelf() is a no-op on touch.
Covered by unit tests (reason emitted, host refocus only on escape) and an e2e assertion that the task row is focused after Escape.
* feat(tasks): return focus to the task the subtask draft was opened from
Escape previously refocused the parent row that hosts the input. Capture the originating task (which may be a subtask) when opening the draft — before focus moves into the input and the parent row claims focusedTaskId — and restore that on Escape. Falls back to the host row when no origin was captured; no-op on touch.
Focus-by-id prefers the last #t-<id> instance (the detail side-panel one) to match the existing inline-edit focus convention when a task renders twice.
Covered by unit tests and an e2e proving focus returns to the originating subtask, not its parent.
* test(tasks): update subtask e2e for the inline draft input flow
The add-subtask UX changed from 'press a -> empty subtask title focused for edit' to an inline draft input (type + Enter to commit, stays open for rapid entry). Update every e2e site that added subtasks via the old textarea selector:
- WorkViewPage.addSubTask helper: wait for .e2e-add-subtask-input, fill + Enter, then Escape to close the draft for a clean post-state (fixes simple-subtask, add-to-today, drag-task-into-subtask, finish-day-quick-history, and the sync specs that use the helper).
- add-subtask-with-detail-panel-open.spec.ts: assert the draft input opens focused from panel/main-list focus; rewrite the multi-subtask case to repeated Enter (the new rapid-entry path).
- supersync-archive-subtasks + supersync-lww-conflict: same inline-draft flow (not runnable in-sandbox; fixed by analogy).
Verified locally: detail-panel-open 3/3, simple-subtask, add-to-today 5/5, drag-into-subtask, finish-day-quick-history all green.
---------
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
|
||
|
|
52f9cb167d
|
fix(tasks): record only doneOn on completion, never auto-schedule to today (#8463) (#8472)
* fix(tasks): respect auto-add-to-today setting when completing tasks Completing an unscheduled top-level task stamped a completion-day dueDay even when "Automatically add worked-on tasks to today" was off (discussion #8463). The meta-reducer updateDoneOnForTask synthesizes that dueDay ungated by the setting, and the gated effect autoAddTodayTagOnMarkAsDone had become dead (the reducer set dueDay before its \!task.dueDay filter ran). Gating the reducer on the synced config would diverge on replay since ops apply in causal arrival order, so the decision is frozen into the op at completion time instead: getMarkDoneTaskChanges passes an explicit dueDay: null for an unscheduled top-level task when the setting is off, which trips the reducer's hasScheduleInUpdate guard and skips synthesis. null (not undefined) survives serialization for deterministic replay; the reducer itself is left untouched so legacy/replay behavior is unchanged. Applied at every completion producer: TaskService.setDone (the funnel for the context menu, reminders, bulk project-done, Android, etc.), the four components that dispatched updateTask directly (task-list, schedule-event, focus-mode-main/break now route through setDone), and the moveTaskToDone$ and auto-mark-parent effects. The dead autoAddTodayTagOnMarkAsDone effect is removed so completion-dating has a single source of truth. Only future completions are affected; already-dated tasks are unchanged. * fix(tasks): freeze offset-correct completion day into the op Follow-up to the auto-add-to-today fix. When auto-add is on, the completion day was still synthesized by the reducer, which derives it from doneOn via the offset-blind getDbDateStr during replay while the live path used the offset-adjusted todayStr. For users with a custom "start of next day" this made the stamped dueDay differ between the originating device (logical day) and a replaying device (calendar day). getMarkDoneTaskChanges now freezes the offset-adjusted logical day (DateService.todayStr) into the op for unscheduled top-level completions (dueDay: todayStr when on, dueDay: null when off). Producers pass todayStr; the reducer applies the explicit value and its synthesis stays only as a legacy fallback. Replay reproduces the frozen value, so the day is both offset-correct and identical across devices. TODAY_TAG membership is unchanged (the dueDay-change branch fires identically for an explicit vs synthesized today). * refactor(tasks): record only doneOn on completion, never stamp dueDay Completion no longer synthesizes or freezes a dueDay (reverting the Option A gating and the May-18 completion-day stamp). The Today "Done" list is driven by isDone/doneOn, not dueDay, so completed tasks still show there. Existing schedules are preserved on completion; the isAutoAddWorkedOnToToday setting now gates only the time-tracking auto-add path. Removes the dead autoAddTodayTagOnMarkAsDone effect, the reducer dueDay synthesis, and the converter's legacy dueDay back-fill (keeping the doneOn back-fill). * refactor(tasks): simplify done-today counter, document legacy-op sanitizer Multi-review follow-up. The done-today counter now counts via the flat selectAllTasks instead of selectAllTasksWithSubTasks + flattenTasks (same count, no per-emission nesting/allocation). Adds a comment clarifying that sanitizeDoneScheduleChanges now exists only as legacy-op replay defense. * test(workflow): pin finish-day archive of unscheduled completed tasks Regression guard for discussion #8463. Since completion now records only doneOn (no dueDay), this e2e proves an unscheduled completed task still (a) shows in the Today Done list and (b) is cleared to the archive on Finish Day — both driven by isDone/doneOn, not dueDay. Fails if a future change scopes the Today Done list or the finish-day archive by dueDay. * chore: drop stray plugin-dev lockfile changes from e2e build The e2e plugin build bumped vite in packages/plugin-dev lockfiles; that churn was unintentionally swept into the test commit and is unrelated to this PR. Restore both lockfiles to the base version. * test(archive): archived completion no longer stamps dueDay The TaskArchiveService.updateTask path runs the same task-shared meta-reducer as live tasks, so #8463's "completion records only doneOn, never dueDay" now applies there too. Update the assertion: completing an archived task sets doneOn but leaves dueDay undefined. |
||
|
|
19e2cbd63a |
test(notes): harden #8434 fix per multi-review
Apply verified findings from a multi-agent review of the resize-close fix: - Guard the navigation listener on `dialogRef.getState() === OPEN`. A breakpoint-crossing resize can emit more than one popstate, and MatDialogRef.close() does not check its own state — a second call would re-run the exit animation and schedule a duplicate close timeout. The guard makes the second+ fires no-ops (the saved result still resolves once, so this was churn, not data loss). - Add unit tests asserting the navigation-close actually PERSISTS the edit (the point of #8434), both via `changed` when alive and via the direct dispatch after a breakpoint switch destroys the host (#8432), plus a test that a second popstate while closing does not re-close. - e2e: hover the notes area before clicking the opacity-hidden fullscreen control, matching the real user flow. - Tighten the duplicated explanatory comment. No behavior change to the fix itself. 77 inline-markdown unit tests and the note-resize e2e pass. |
||
|
|
5966ba5f43 |
fix(notes): persist fullscreen note when a resize closes the editor (#8434)
The fullscreen markdown editor is a detached CDK overlay opened with MatDialog's default closeOnNavigation. That maps to the overlay's disposeOnNavigation, which on any Location change (popstate/hashchange) disposes the overlay with NO result. Resizing the window across the mobile layout breakpoint fires such a navigation, so the editor was torn down and the in-flight note silently dropped. Open the dialog with closeOnNavigation: false and reinstate close-on- navigation by subscribing to the same Location signal, but close through the dialog's save path so the edit is persisted instead of discarded. This also keeps the Android back-button closing the editor (it routes through history.back -> popstate) and now saves on that path too. The listener is not tied to takeUntilDestroyed so it still fires after a breakpoint switch destroys this host mid-edit; the save then lands via the existing destroyed-host dispatch (#8432). Torn down in afterClosed. Covered by inline-markdown unit tests and a new note-resize e2e. |
||
|
|
797451427e |
test(daily-summary): guard /active/daily-summary route resolution (#8449)
Add an e2e regression guard asserting the context-resolved /active/daily-summary route opens the daily summary instead of falling through to the wildcard and redirecting to the start page. The unit spec only pins the navigation string; this verifies the string is a navigable route. |
||
|
|
09f9ac4299
|
fix(focus-mode): persist fullscreen notes when a session ends mid-edit (#8432)
* style(theme): give rainbow theme a neon glow makeover Rework the bare rainbow theme into a full neon look inspired by a glowing dashboard reference: - deep indigo "space" palette with a fixed aurora nebula on .app-container (mat-sidenav-content is not the work-area wrapper) - frosted, glowing panels: side nav (violet bottom-glow), right panel, cards, menus/dialogs, add-task bar — backdrop-filter on inner panes only, never the side-nav host (keeps the mobile drawer) or task rows - glowing active nav pill, primary buttons, chips, focus rings, links, headings and a gradient neon scrollbar - task rows get a translucent indigo fill (were transparent) so the aurora tints through as glass; no per-row blur to protect the hot path - task detail items now use the indigo --task-detail-* surfaces + glow instead of falling back to neutral grey - mark the theme requiredMode 'dark' so picking it applies a dark canvas All glows are static (no animation) to stay GPU- and battery-friendly. * test(recurring): pin clock in #6860 helper spec to fix date-dependent flake The helper-based test hardcoded 15/06/2026 as the recurring start date but, unlike its sibling specs, never pinned the clock. The datepicker disables past days, so the scheduled run on 2026-06-16 could no longer click the now-disabled 2026-06-15 cell and timed out. Pin today to 2026-05-01 (matching the sibling recurring-date specs) so the hardcoded date stays a selectable future day. * fix(focus-mode): persist fullscreen notes when a session ends mid-edit The fullscreen markdown editor is a detached CDK overlay. When a focus session ends while it is open, focus-mode-overlay swaps screens (Main -> SessionDone/Break) and destroys the inline-markdown that opened the dialog. Its `changed` output then has no listener, so on Save the note was emitted into the void and silently lost. On afterClosed, when the component has already been destroyed, persist the note directly to the store using the taskId captured when the dialog opened, instead of emitting `changed`. Skip when the content is unchanged (avoids writing the default-text placeholder back as a real note) and warn when there is no taskId to persist to. Covered by inline-markdown unit tests and a focus-mode e2e for both Countdown and Pomodoro sessions completing mid-edit. |
||
|
|
bc8fea8baa |
test(recurring): pin clock in #6860 helper spec to fix date-dependent flake
The helper-based test hardcoded 15/06/2026 as the recurring start date but, unlike its sibling specs, never pinned the clock. The datepicker disables past days, so the scheduled run on 2026-06-16 could no longer click the now-disabled 2026-06-15 cell and timed out. Pin today to 2026-05-01 (matching the sibling recurring-date specs) so the hardcoded date stays a selectable future day. |
||
|
|
2304d10a50 |
test(migration): navigate to project tasks route to de-flake legacy migration e2e
STEP 8 navigated to /project/:id without the /tasks suffix. PROJECT_CHILD_ROUTES has no default child redirect, so the child router-outlet rendered empty and the parent-task assertion only matched the lingering Today landing view — flaking on master CI (retries:0) whenever that stale DOM was torn down under parallel load. Navigate to /project/TEST_PROJECT/tasks and reload for a single clean render (migration's Genesis op makes the reload skip re-migration). Verified 4/4 on --repeat-each plus the full spec file. |
||
|
|
82e7150821 |
test(sync): unblock #8331 e2e by stripping conflict-response piggyback
The #8331 transient-download regression test failed on every scheduled run since it landed (abortedResolutionGets stayed 0). The CI trace shows why: B's pre-upload download is correctly stripped and B's upload IS rejected CONFLICT_CONCURRENT, but the ops-upload conflict comes back as HTTP 200 whose body piggybacks the conflicting remote op in `newOps`. The client LWW-resolves the conflict from that piggyback in-place, so RejectedOpsHandlerService never issues the separate resolution-download GET the #8331 fix guards — the path the test arms and aborts. Fix: the route handler now also forwards B's upload POST to the real server (keeping the rejection server-computed) and strips `newOps`/`hasMorePiggyback` from the response. With no piggybacked ops, the client falls back to the resolution-download GET, which the test aborts — exercising the guarded catch. A new `strippedConflictPiggybacks` guard asserts this fired. This stripped-piggyback condition is real in production when the conflict falls beyond PIGGYBACK_LIMIT (500 ops). No product code changed; behavior under test is unchanged. |
||
|
|
d2367e3bac |
test(sync): deterministically provoke CONFLICT_CONCURRENT in #8331 e2e
The held-POST + re-entrant A-sync ordering was still racy: B's upload was accepted (200) instead of rejected, so the resolution-download catch under test never ran and abortedResolutionGets stayed 0 (runs 27465357634 and 27467427713). Switch to a deterministic hybrid that keeps the real server conflict: - pre-sync A so its concurrent op is unconditionally on the server; - strip ops from B's pre-upload download (return the REAL response with an emptied ops list) so B never merges A's edit and uploads its original concurrent op -> the real server computes a genuine vector-clock CONFLICT_CONCURRENT; - abort every post-upload resolution GET to drive the guarded catch. Pin latestSeq back to the request's sinceSeq on the stripped download: the client persists lastServerSeq even on an empty download, so keeping the advanced seq would make B skip A's edit forever and break convergence. Add a strippedPreUploadDownloads setup guard alongside abortedResolutionGets so a setup that fails to provoke the conflict fails loudly, not vacuously. The countRejectedOps discriminator and end-to-end convergence are unchanged. |
||
|
|
d865d21fa3 |
test(sync): force deterministic CONFLICT_CONCURRENT in #8331 transient-download e2e
The test relied on Client B's upload being rejected CONFLICT_CONCURRENT to exercise the resolution-download catch. But the sync engine downloads before it uploads (sync-wrapper: downloadRemoteOps -> uploadPendingOps), so when A's concurrent edit was already on the server (pre-synced at step 4) B downloaded and merged it, then uploaded a dominating op the server ACCEPTED (200) - the rejection path was never reached and abortedResolutionGets stayed 0. Run 27465357634 failed exactly this way. Force the one ordering that guarantees a rejection: let B's pre-upload download run while the server still has no concurrent op, then land A's edit in the window between B's download and B's upload by syncing A inside B's held upload POST. B never merges, so its original pending edit rides into the resolution path and the server returns a real CONFLICT_CONCURRENT. Keeps the real two-client conflict and the unchanged countRejectedOps/convergence assertions. |
||
|
|
56334897cc
|
fix(sync): persist lastServerSeq after piggybacked ops are applied (#8304) (#8372)
* fix(sync): persist lastServerSeq after piggybacked ops applied #8304 The upload path persisted lastServerSeq covering piggybacked ops before those ops were applied (processRemoteOps runs in the caller, after the upload lock releases). A crash or a cancelled SYNC_IMPORT dialog in that window advanced the seq past unstored ops, so the next download skipped them forever. The upload service now defers the seq persist to the caller once it collects piggybacked ops; the orchestrator persists only after processRemoteOps succeeds, and skips it on the cancel/USE_REMOTE/USE_LOCAL early-returns. The non-piggyback path still persists in-loop (no loss risk). Mirrors the download path's 'persist AFTER ops stored' invariant. Updates 3 upload specs that encoded the buggy persist-before-apply and adds upload-side ordering + cancel-path regression specs. * test(sync): add crash-window regression for deferred lastServerSeq #8304 * test(sync): cross-service integration test for piggyback seq persistence #8304 Wires the real OperationLogUploadService + real OperationLogSyncService over one in-memory provider seq. Verifies the seq is not advanced on cancel/crash and only advanced after processRemoteOps on success. Fails against pre-fix code (all 3). * test(sync): e2e guard for cancel-reconvergence of incoming SYNC_IMPORT Multi-client SuperSync spec covering a scenario no existing test does: an incoming SYNC_IMPORT conflicting with a LOCAL PENDING op -> conflict dialog -> CANCEL -> the next sync RE-OFFERS it -> USE_REMOTE reconverges. Documents that it routes through the (already-correct) download path, so it is a real-server guard for the shared cancel->re-offer->reconverge contract, NOT a #8304 fix regression (that path is race-only and covered by the unit + cross-service integration specs). Requires docker. |
||
|
|
217796b742
|
fix(sync): keep pending ops on transient download failure (#8331) (#8371)
* 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(sync): keep pending ops on transient download failure When an upload is rejected CONFLICT_CONCURRENT, the handler downloads to resolve. A transient failure during that download used to call markRejected() on the still-pending local edit — terminal (rejectedAt is never cleared, so the op never re-enters getUnsynced()), so the edit stayed applied locally but silently stopped syncing to other devices. - Drop the markRejected loop in the catch; log + re-throw so the ops stay pending and re-resolve on the next sync. - Roll back the per-entity resolution-attempt increment on a thrown download: a transient failure must not consume the cap (which would otherwise eventually markRejected via the exceeded branch — re-introducing the same data loss). The cap still bounds genuine non-progress loops, whose download succeeds and never reaches this catch. Adds unit regression tests (single + repeated transient throws never reject) and a SuperSync e2e that fails the resolution download past the provider retry budget, asserts the pending edit is not rejected, then converges. Fixes #8331. |
||
|
|
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. |
||
|
|
d60ff6d55f |
test: fix suite-aborting schedule spec DI crash and harden flaky tests
- schedule-locale-ng0701.spec: mock CalendarEventActionsService so the real DI chain (IssueService -> SnackService -> LOCAL_ACTIONS -> Actions) is not constructed. Its missing ScannedActionsSubject provider threw in afterAll (NG0201) and aborted the entire Karma run at ~2951/10838. - add-subtask-with-detail-panel-open.spec: wait for the panel's deferred auto-focus to land before re-focusing the main-list row, and re-apply focus on each retry via toPass() to fix a focus-race flake under load. - jira-api.service.spec: pin navigator.onLine=true for the auto-import pagination tests so they assert pagination, not the runner's network state (offline CI tripped the "Jira Offline" guard in _sendRequest$). |
||
|
|
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> |
||
|
|
c109ad1fa5
|
Update recurrent task calendar and general calendar design (#8017)
* feat(task-repeat-cfg): use schedule dialog picker for recurring task start date * refactor(calendar): extract shared DateTimePickerComponent and use in schedule/deadline dialogs * style(calendar): make primary button gray when disabled and stronger green when enabled * style(calendar): upgrade date picker calendar visuals and remove outlines * fix(task-repeat-cfg): prevent premature duration formatting during typing * fix: resolve merge conflict and fix lint errors in dialog components * test(e2e): update recurring task tests to match new schedule dialog UI * test(e2e): fix regressions and ambiguous locators in recurring task tests Resolves strict mode violations for 'Schedule' button and updates locators to handle the new separate schedule dialog robustly using the datetime-picker filter. * fix: hide unschedule button in schedule dialog when opened from recurring task cfg * fix(datetime-picker): restore calendar first-week weekday alignment by using visibility:hidden instead of display:none for body-label cell * fix(i18n): use active UI language for date locale fallback, show full month names, and translate hardcoded Time label * test: fix DateTimeFormatService unit tests by mocking TranslateService * fix(task-repeat): add default reminders to recurring tasks * test(sync): increase lock reentry timeout to prevent flakiness * style(ui): align calendar cell shapes and make hover color darker * style(ui): style inputs and action buttons in schedule and deadline dialogs * feat(task-repeat): enable quick access scheduling for recurring tasks * feat(ui): separate month and year into distinct header buttons in datetime picker * style(ui): show dropdown arrow next to the year only * fix(locale): map UI fallbacks to parse-safe locales and sync DateAdapter * fix(task-repeat): fix option caching, DST normalization, and schedule dialog time preservation * test(task-repeat): mock formatTime in repeat dialog spec * fix(ui): improve datetime-picker month/year picking, arrow direction, transitions, and highlighter sync * fix(ui): defer year selection view transition and avoid year picker pagination updating highlighter * style(ui): restore focus outline and default material look * style(ui): drop custom hover circle and pill shape, and revert month-grid names to default short format * fix(ui): gate schedule warnings on isSelectDueOnly * style(ui): remove custom button color overrides * style(ui): restore default Material hover and focus behavior * fix(ui): add calendar header aria-labels and mirror arrows in RTL * test(e2e): fix recurring task start date specs for new datetime picker * revert(date-time): restore master's browser-region-first fallback in DateTimeFormatService * fix(ui): make quick-access tooltips configurable on datetime-picker and preserve deadline labels * fix(ui): exclude disabled previous/next calendar buttons from custom hover style * fix(ui): align period header button aria-labels with actual view transitions * style(ui): refactor recurring start date button styling with logical CSS and remove any type * fix(ui): preserve selected year when navigating in year picker and toggling back * style(ui): nudge default calendar cell hover with a shared token * fix(datetime-picker): sync hover selection with keyboard navigation and focus on open * fix(deadline): grey out past days in deadline calendar * style(datetime-picker): use shared calendar hover background for header buttons * style(calendar): use primary mixed color globally for calendar hover background * style(datetime-picker): hide mouse on arrow key nav and unify hover/focus colors * style(datetime-picker): resolve selected cell highlight bug and update hover outlines * style(datetime-picker): style current date/month/year selectors with accent color * fix(task-repeat-cfg): guard repeat schedule opening with isValidSplitTime * fix(task-repeat-cfg): only persist remindAt when a valid time is present * fix(ui): add disabled guards for calendar cells and improve scheduling result * fix(ui): improve focus management and navigation in datetime-picker * fix(ui): prevent invalid 24:00 time in datetime-picker and remove debug log * refactor(ui): cleanup datetime-picker and improve calendar header logic * feat(ui): enable year navigation and consistent month selection in datetime-picker * fix(ui): align multi-year boundary logic with Angular Material anchoring * fix(repeat): restore past start-date floor for existing repeat configs * test(e2e): fix setRecurStartDate to correctly navigate month and year * test(e2e): remove duplicate helper declarations causing SyntaxError * refactor(calendar): de-risk calendar by reverting brittle hover sync and custom header * test(calendar): update unit tests for datetime-picker de-risking * fix(calendar): restore standard year-to-month drill-down navigation * test(e2e): fix recurring task and planner task compatibility - Update setRecurStartDate helper to use standard Material calendar drill-down navigation, fixing compatibility with the new UI. - Update TaskPage helper to support both 'task' and 'planner-task' elements in board and planner views. * test(e2e): fix recurring task date selection and calendar navigation - Update setRecurStartDate helper to use correct click sequence for the new date selector header. - Remove minDate restriction in recurring task edit dialog to allow moving start dates earlier (#7423). * style(ui): remove stale calendar overrides and localize header labels * test(e2e): fix failing task detail date test due to calendar i18n label change * fix(ui): remove redundant global locale mutation from DateTimePickerComponent * fix(ui): prevent calendar navigation reset when date value hasn't changed * fix(planner): wire showQuickAccess and disable auto-submit in select-due-only mode * fix(repeat): allow past dates when editing repeat configurations * fix(repeat): use DateService for logical today check * fix(test): add coverage for navigation guard and interactive flows in DateTimePickerComponent * fix(test): restore time handling coverage for select-due-only mode in DialogScheduleTaskComponent * fix(tasks): implement robust reminder quantization using mid-points and add tests * feat(planner): add stable data-test-id locators for schedule dialog buttons * fix(e2e): replace fragile translated 'Schedule' locators with data-test-id * fix(ui): improve calendar styling and i18n consistency * refactor(ui): cleanup unused code in DateTimePickerComponent * fix(planner): restore auto-submit on quick-access in DialogScheduleTaskComponent * fix(repeat): refine minDate strategy for recurring configurations * chore(i18n): restore de.json to master |
||
|
|
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. | ||
|
|
ccbd66d3d9 |
test(e2e): retry supersync time-estimate dialog until input binds
The fill('10m') could fire its `input` event before the duration input's
value-accessor (an `input` HostListener) was wired, so the typed value
never reached the model and submit() saved an empty time — the panel
stayed "-/-" and toContainText('10m') timed out (scheduled run
27197862391, SuperSync 1/6). Retry the whole open→fill→submit cycle so a
fill that didn't stick is re-applied once the control binds.
|
||
|
|
f1aaa21390 |
test(e2e): re-fill supersync encryption dialog until ngModel binds
The confirm-password fill() could land before the dialog input's [(ngModel)] value-accessor was wired, leaving the field empty and the "Set Password" button permanently disabled (scheduled run 27181596227, SuperSync 2/6). Wrap the fills + enabled-check in expect().toPass() so a fill that didn't stick is re-applied once the control binds. |
||
|
|
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.
|
||
|
|
b6f3aead1f |
test(migration): gate on work-context switch to de-flake legacy migration e2e
page.goto only changes the URL hash, so the SPA switches work-context client-side while the previous view's task-list lingers in the DOM. The task-list/count checks were satisfiable by the stale render, leaving the final task-title assertion to carry the wait for the switch on a 10s budget that occasionally outlasted under heavy parallel load. Gate on the project title rendering in <main> first, mirroring navigateToProjectByName. |
||
|
|
3ff0eebf92
|
fix(sync): make 'Use Server Data' recoverable + guard the destructive choice (#8107) (#8151)
* fix(sync): back up local state before USE_REMOTE replace #8107 forceDownloadRemoteState cleared all unsynced local ops and replaced NgRx state with the server snapshot with no recovery point, making an unintended 'Use Server Data' conflict choice irreversibly destructive. Capture a pre-wipe snapshot via the existing single-slot IMPORT_BACKUP store (BackupService.captureImportBackup) before clearUnsyncedOps, and abort the replace if the backup fails. Offer a 30s Undo snack afterwards that restores it (BackupService.restoreImportBackup). Covers both the conflict-dialog USE_REMOTE path and the manual force-download. * fix(sync): durable restore entry + one-shot recovery for USE_REMOTE backup #8107 Addresses multi-review findings on the pre-wipe backup: - Add a persistent 'Restore data from before last sync replace' button to the sync settings (all providers, gated on a remaining backup), so recovery survives a missed/replaced Undo snack or a failure that aborts after the wipe — the snack was previously the only reader of the backup. - Make restore one-shot: consume the IMPORT_BACKUP slot after a successful restore, preventing a second restore from toggling back to the replaced state and avoiding a lingering plaintext snapshot. - Undo snack: WARNING type (honest framing + dismiss control) and no auto-dismiss timer instead of SUCCESS/30s. * test(sync): e2e for USE_REMOTE pre-replace backup + undo #8107 WebDAV two-client conflict: Client B's local task is wiped by 'Keep remote' (USE_REMOTE), then the Undo snack restores it. Reproduces the #8107 data loss and verifies recovery. Modeled on the existing webdav-first-sync-conflict USE_REMOTE test. Runnable via npm run e2e:webdav:file (needs the docker WebDAV server; not runnable in the sandbox where published ports are unreachable). * fix(sync): guard USE_REMOTE in conflict dialog; drop durable restore button #8107 Swap the over-built recovery surface for a root-cause fix: - Add a confirmation guard before 'Use Server Data' (USE_REMOTE) discards pending local changes (INCOMING_IMPORT with local ops), mirroring the existing USE_LOCAL guard. The dialog frames the server as 'recommended', so this stops a misclick from silently wiping data the user can't tell is newer than the server's — attacking the cause, not just recovery. - Revert the durable settings restore button + its one-shot clear / hasImportBackup (beyond the minimal fix; only that button used them). Keeps the minimal recovery net: pre-wipe capture + sticky WARNING undo snack (and its passing WebDAV e2e). * fix(sync): explain encryption-blocked restore instead of generic error #8107 Defect 2: server-side Restore-from-History can't replay end-to-end- encrypted ops, so it rejects with a 4xx whose reason mentions encryption (the provider embeds that reason in the thrown error message). The client showed a meaningless 'Failed to restore data' (bbinet's screenshot). Detect the encryption reason in the restore catch and show a dedicated RESTORE_ENCRYPTED message explaining the limitation; falls back to the generic message for any other failure. * fix(sync): guard USE_REMOTE undo against superseded backup slot #8107 The pre-replace backup uses a single IndexedDB slot shared with the backup-import flow. The Undo snack never expires, so an intervening import or a second 'Use Server Data' could overwrite the slot before the user clicks Undo, silently restoring the wrong snapshot. Thread the backup's savedAt token from capture through the snack to restore; restoreImportBackup() now refuses when the stored backup no longer matches the token. Also clear the slot after a successful restore so a full copy of the replaced state stops lingering in IDB (uses the previously-uncalled clearImportBackup). |
||
|
|
a8bccb9e31
|
fix(tasks): add subtask via "a" shortcut when detail panel is open (#8152)
* build: drop unused elevate.exe from Windows build #8135 elevate.exe is bundled by electron-builder's NSIS target solely so electron-updater can apply privileged per-machine updates. We ship no in-app auto-updater (electron-updater is not a dependency; the autoUpdater block in electron/start-app.ts is commented out), so the binary is unused dead weight and a frequent AV false-positive. Set packElevateHelper: false to stop bundling it; safe because perMachine is not true. * fix(tasks): add subtask via "a" shortcut when detail panel is open With the detail panel open, pressing "a" created a sub-task but did not focus it for editing. Two paths were broken: - Focus inside the panel: the panel auto-focuses a detail item on open, so focus is not on a <task> row and the global task-shortcut handler drops the shortcut. The panel now handles taskAddSubTask in its keydown listener (stopPropagation avoids a double-add when focus is on an in-panel row). - Focus on the main-list task row: addSubTaskTo -> focusTaskById targeted the last #t-<id> copy, which is the in-panel copy inside the collapsed (hidden) sub-task section and cannot take focus. focusTaskById now falls back to the last focusable copy, so the new sub-task title is focused for editing (the panel follows focus to the new sub-task). |
||
|
|
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
|
||
|
|
0d1869263f |
docs: remove outdated and implemented plan docs
Delete 29 plan/design docs whose work has shipped or been superseded (SuperSync slices, sync-core extraction, encryption-at-rest drafts, document-mode/Stage-A persistence, calendar/CalDAV concepts, focus-mode time-tracking sync, etc.). Kept the still-forward-looking docs (e.g. supersync-encryption-at-rest, sync-core-simplification-roadmap, calendar-two-way-sync-technical-analysis). Source comments that cited deleted docs are rewritten into self-contained inline rationale so no "see docs/..." reference dangles. |