mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-19 01:17:31 +00:00
682 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dbafa8d0a0 | test(e2e): stabilize task visibility specs | ||
|
|
79739a6f92
|
refactor(ui): replace custom SVG icons with Material font ligatures (#7575)
* refactor(ui): replace custom SVG icons with Material font ligatures Swap seven custom SVG icons for built-in Material font ligatures and delete the corresponding asset files plus one orphan backup: - repeat -> repeat (6 templates + 1 chip config) - next_week -> next_week (3 templates) - drag_handle -> drag_handle (1 commented site) - habit -> heart_check (side nav + features form) - tomorrow -> wb_twilight (3 dialogs/menus) - working_today -> timelapse (work-view header) - remove_today -> timer_off (task hover controls + note) Brand and protocol marks (jira, gitlab, caldav, calendar, trello, etc.) intentionally kept as SVG since Material has no equivalent. * test(e2e): update recurring selectors for Material font ligature swap The repeat icon switched from <mat-icon svgIcon="repeat"> to the font ligature form <mat-icon>repeat</mat-icon>, which broke 10 selectors across 8 recurring specs that filtered by the now-absent svgIcon attribute. Replace with a locator that matches the ligature text exactly (anchored regex prevents catching repeat_one or similar). |
||
|
|
a7bb11c3ed | fix(mobile): hide disabled issue panel option | ||
|
|
86497d24de | test(sync): cover incompatible supersync password change | ||
|
|
5a924eeadd | test(sync): scope keep-local validation assertion | ||
|
|
075de5a097 |
feat(e2e): add keyboard, mobile, and shorts video variants
Three new REEL_VARIANT branches for the marketing video pipeline: - shorts: 9:16 portrait 1080x1920 for TikTok / YouTube Shorts / Instagram Reels. Skips the side-panel drag beat (no horizontal room) and tightens holds for portrait capture. - keyboard: keyboard-first reel demonstrating SP shortcuts. Five beats driven by real page.keyboard.press() so cause-and-effect is honest (Shift+A capture, J/K navigate, F focus mode). - mobile: 19.5:9 phone aspect 1080x2340 with hasTouch + isMobile UA. Fixture installs a tap-ripple on touchstart/pointerdown in lieu of the cursor highlight; tap-driven choreography. Fixture gains a getVideoProfile() selector that returns the right viewport, DPR, and touch flags per variant. Overlays grow new primitives needed by the keyboard and mobile choreographies. |
||
|
|
0d9ca930e3 | fix(screenshots): skip absent electron variants | ||
|
|
09afeb93a8 | fix(video): require audio source for store trailer | ||
|
|
f273a75e6f | feat(video): add Microsoft Store trailer variant | ||
|
|
4534789571 | feat(screenshots): automate flathub output | ||
|
|
38d0898228
|
feat(flowtime): add configurable dynamic breaks (#7402)
* feat(flowtime):Add-breaks-as-per-users-choice-in-flowtime * Chore: Clean up dialog-flowtime-settings component Removed commented-out hideExpression and props related to breakRules. * fix(flowtime): correct break trigger, preserve work session, add proper break offer UI and validation * fix(flowtime): align break strategy with effect, add tests and validation fixes * fix(flowtime): address review feedback for break logic, validation, and state handling * fix(flowtime): address final review feedback * fix(flowtime): address review feedback for break offers, tracking, and rule handling * fix(flowtime): align break offer flow with focus mode rework * chore: restore package lockfile * chore: revert unrelated lockfile changes * fix(flowtime): address review feedback * test(focus-mode): fix sync behavior and update E2E expectations * test(e2e): align focus mode tests with current settings UI * test(e2e): stabilize focus mode settings setup * fix(theme): restore default css tokens * test(focus-mode): cover flowtime completion logging --------- Co-authored-by: johannesjo <johannes.millan@gmail.com> |
||
|
|
a7845acd81
|
fix(electron): retry Wayland idle helper startup (#7527)
* fix(electron): retry wayland idle helper startup * build(electron): enforce wayland helper in CI * fix(electron): avoid blocking wayland idle promotion * test(e2e): make reminder option selector exact * test(e2e): resolve reminder option selector conflict * fix(electron): gate wayland helper redetection * fix(electron): avoid stale Wayland helper packaging --------- Co-authored-by: Johannes Millan <johannes.millan@gmail.com> |
||
|
|
1fc048df61 |
chore(video): smooth marketing reel output
Note: the drag/drop reel works finally with the native app drag preview path. |
||
|
|
11c6b2f748 | Improve automated store screenshots | ||
|
|
22dbfde12b | test(sync): fix concurrent time tracking assertion | ||
|
|
65fdd2581d | test(e2e): harden supersync completion wait | ||
|
|
4b5fc3fb33 | test: stabilize flaky e2e specs | ||
|
|
34704d82b2
|
fix(ui): improve task selector project filtering (#7529)
* fix(ui): improve task selector and focus notes controls * fix(tasks): respect project short syntax setting * test(e2e): expand focus mode settings robustly |
||
|
|
40467ad1c5 | test(screenshots): stabilize electron screenshot output | ||
|
|
989563dc79 |
test(e2e): scope reminder-default lookups to avoid theme tooltip match
The new theme upload button's matTooltip ("...never run code...") leaves its
text in the DOM via cdk-describedby, so case-insensitive getByText('never')
matched both the "Never" reminder option and the tooltip. Target the
mat-option by ARIA role and exact case, and scope the post-schedule
assertion to the dialog so only the mat-select trigger text qualifies.
Also prunes a stale nested vitest/esbuild tree from the caldav plugin's
package-lock.json.
|
||
|
|
a6b8159d5b |
feat(screenshots): liquid-glass-ready electron pipeline + diagnostics
Round of robustness fixes for the store-screenshot pipeline so the Liquid Glass captures actually ship MAS-compliant PNGs: Capture & dimensions: - Capture by CGWindowID on macOS (`screencapture -l <id>` resolved via desktopCapturer.getSources matched on window title) instead of by screen rect. Survives small displays clipping the window and grabs the full hiddenInset titlebar including traffic-lights regardless of off-screen position. - `enableLargerThanScreen: true` on the BrowserWindow when launched in screenshot mode (gated on a new SP_SCREENSHOT_MODE=1 env var). Keeps the configured 1280×800 outer from being clamped down to fit the area below the menu bar / dock — without this, captures came out 20pt short and Mac App Store rejected the dimensions. - Drop `--no-sandbox` / `--disable-dev-shm-usage` from the Electron launch on macOS. They're Linux/CI helpers; on Mac `--no-sandbox` empirically suppresses the hiddenInset traffic-lights even though the same SP build launched via `npm start` shows them. Configurability: - `SCREENSHOT_CUSTOM_THEME` env var feeds the fixture's `customTheme` option, so a one-off pipeline run under e.g. liquid-glass / dracula doesn't require editing spec files. - New `scrollScheduleUp` helper called from every schedule capture (desktop slots 03 + 06, mobile 05, tablet 03) — nudges the schedule scroll-wrapper up ~80px so the captured frame shows context before work-start instead of being flush against it. - New slot-02 light variants for desktop (eisenhower) and mobile (planner-expanded) so both light and dark land at slot 02. Diagnostics: - Surface execFile stderr/stdout in the capture-failed Error so permission failures name the actual cause (`could not create image from rect`, `not authorized`, etc.) instead of just "Command failed". - Loud banner on first OS-capture failure plus an end-of-run summary (via the existing globalTeardown) re-surfacing the warning so it isn't lost in long log scrollback. Marker file in MASTER_DIR bridges fixture → globalTeardown. - Per-capture `[screenshot] <name> → <bin> <args>` log line, plus an `setBounds requested=… achieved outer=…; content=…` line right after the resize, so a "missing chrome" run is debuggable from the test output alone. - README: macOS Screen Recording permission requirement documented. |
||
|
|
44e0762fba
|
Feat/issue 7330 bc1a3f (#7522)
* fix(sync): strip same-batch-archived task IDs from TAG/PROJECT LWW payloads Issue #7330. lwwUpdateMetaReducer's orphan filter only sees taskState as it is when each op runs. A TAG LWW Update applied before its sibling archive op in the same bulk batch escapes the filter, leaving TODAY_TAG (or any tag/project) referencing a task the very next op removes — user-visible as "archived tasks reappear in today's view" on hibernate-wake. bulkOperationsMetaReducer already collects archivingOrDeletingEntityIds for the wholesale TASK LWW Update skip; this commit reuses that set to pre-clean taskIds (and PROJECT-only backlogTaskIds) on TAG/PROJECT LWW Update payloads before convertOpToAction. Cross-batch ordering is not addressed — see open issue. * fix(sync): surface post-sync validation failure as ERROR, not IN_SYNC Issue #7330. The reporter saw "State validation failed after sync. Some data may be inconsistent." while sync simultaneously reported status IN_SYNC. validateAfterSync's boolean was discarded above the snackbar layer (#6571 only added the snackbar; nothing flowed up to the status pipeline). Plumb the boolean through: - validateAfterSync: void -> boolean - _validateAndRepairAfterResolution: void -> boolean - autoResolveConflictsLWW + processRemoteOps: add validationFailed - DownloadOutcome.ops_processed + UploadOutcome.completed: add validationFailed - sync-wrapper: if either result.validationFailed, setSyncStatus('ERROR') and return HANDLED_ERROR before the IN_SYNC mark Tests: assert validateAfterSync returns the boolean, assert processRemoteOps surfaces validationFailed, assert sync-wrapper sets ERROR (not IN_SYNC) when either download or upload reports validationFailed. * fix(sync): always set top-level id on LWW Update payloads lwwUpdateMetaReducer drops LWW Updates whose payload lacks a top-level id ("Entity data has no id"). Two creation paths could produce such payloads: - createLWWUpdateOp passed entityState through unchanged, so a malformed selector result silently produced an unusable LWW op. - _convertToLWWUpdatesIfNeeded built mergedEntity by spreading baseEntity and updateChanges; when baseEntity lacked id (corrupt DELETE payload) and updateChanges had id stripped, the merge had no id either. Both sites now force the canonical entityId onto the payload. (#7330) * fix(sync): close 3 secondary gaps in #7330 fixes Multi-agent review surfaced three additional code paths mirroring the same defects we fixed at the primary path: G1: bulkOperationsMetaReducer's pre-scan only saw op.entityIds / op.entityId for archive/delete ops. moveToArchive declares only top-level task IDs, but the reducer cascades to subtasks via [t.id, ...t.subTasks.map(st => st.id)]. deleteTask cascades the same way via taskToDelete.subTaskIds. Same-batch TAG/PROJECT LWW Updates referencing those subtasks slipped through the strip. Add collectCascadedSubTaskIds to harvest subtask IDs from archive/delete payloads. G2: SyncWrapperService's LWW re-upload retry loop captured only reuploadResult.localWinOpsCreated, throwing away validationFailed. A retry-pass piggybacked download with failing post-sync validation still reported IN_SYNC. Track reuploadValidationFailed across retries and OR into the gate before IN_SYNC. G3: OperationLogSyncService's downloadCallback (used by the rejected-op handler) converted the download outcome into DownloadResultForRejection, which has no validationFailed field. A nested download triggering validation failure was lost before uploadPendingOps returned. Route the boolean via the existing piggybackValidationFailed flag in the closure scope. Also fixes one cosmetic logging field (reuploadValidationFailed included alongside download/upload in the ERROR log). * fix(sync): defense-in-depth id derivation in lwwUpdateMetaReducer Multi-agent review surfaced two follow-up improvements for #7330: 1. Consumer-side id fallback: a future LWW Update producer that forgets to backfill payload.id would silently regress the "Entity data has no id" bail. Recover from action.meta.entityId, which convertOpToAction always populates from the canonical op.entityId. Now a single consumer guards the whole class of missing-id producer regressions. 2. Tighten 7 conflict-resolution.service.spec assertions from jasmine.objectContaining({ localWinOpsCreated: N }) back to strict toEqual({...}). The loosening (added when validationFailed was introduced) hid future stray-field regressions; the validation path now returns a deterministic shape per code branch, so we can assert it. * fix(sync): close issues found in follow-up review (#7521) Multi-agent review of the prior #7330 follow-up commits surfaced four real issues. All addressed here: CRITICAL — singleton id pollution at producer + consumer Singletons use entityId='*' as a sentinel. Both the producer-side payload-id backfill (createLWWUpdateOp + _convertToLWWUpdatesIfNeeded) and the consumer-side meta.entityId fallback in lwwUpdateMetaReducer injected `id: '*'` into payloads. For singletons (GLOBAL_CONFIG, app-state, time-tracking) this leaks a synthetic `id: '*'` field into singleton feature state when the reducer spreads entityData. Gate id-injection on entityId \!== '*' at all three sites and move the consumer fallback past the singleton branch. WARNING — null-safety in collectCascadedSubTaskIds `'actionPayload' in payload` check could pass for malformed payload {actionPayload: null}, then crash on the next property access. Tighten to a typeof check. WARNING — retries-exhausted path bypasses validationFailed gate When the LWW re-upload loop exits via MAX retries with pending ops still > 0, sync-wrapper used UNKNOWN_OR_CHANGED without consulting reuploadValidationFailed. Validation failure during a retry pass is now elevated to ERROR — a more honest signal than UNKNOWN_OR_CHANGED, and consistent with the user-visible "State validation failed" snackbar they already saw. SUGGESTION — devError on consumer fallback Producer regressions should scream loudly in dev, not slip through silently. The fallback now emits devError + applies the recovery, so a future LWW producer that forgets payload.id surfaces in dev rather than only-when-the-bail-fires. * fix(sync): close validationFailed gap in USE_REMOTE and DELETE_MULTIPLE cascade Three issues found by multi-agent review of the #7330 sync fixes: 1. forceDownloadRemoteState() discarded the validationFailed result from processRemoteOps. A USE_REMOTE conflict-resolution path that applied corrupt remote state would still mark sync IN_SYNC despite the snackbar. Now returns { validationFailed } and propagates it through _handleSyncImportConflict and DownloadOutcome.no_new_ops to SyncWrapperService, plus the two direct callers (manual force download and first-sync conflict resolution). 2. sync-wrapper retry-exhaustion priority: when LWW retries exhausted AND the initial download/upload had reported validationFailed, the wrapper returned UNKNOWN_OR_CHANGED instead of ERROR. The hoisted downloadValidationFailed/uploadValidationFailed are now checked inside the retry-exhaustion branch alongside reuploadValidationFailed. 3. bulk-hydration pre-scan handled DELETE_MULTIPLE in the loop but the collectCascadedSubTaskIds helper early-returned for that action type. Since deleteTasks payload only carries flat taskIds, subtask cascade must be derived from initial batch state (mirroring what handleDeleteTasks does at apply time). Co-batched TAG/PROJECT LWW Updates referencing those subtasks could otherwise still leak. Adds regression tests for each path. * refactor(sync): centralize LWW id backfill, extract singleton sentinel constant Follow-up simplifications from the multi-agent review of #7330 fixes. - Extract SINGLETON_ENTITY_ID = '*' + isSingletonEntityId() helper in entity-registry.ts. Replaces the bare '*' literal across conflict-resolution.service.ts, lww-update.meta-reducer.ts, validate-operation-payload.ts, operation-log-recovery.service.ts, and operation-log-migration.service.ts. (S2) - Add LWW Update payload.id backfill at the apply boundary in convertOpToAction. Every applied LWW op now has its top-level id set from op.entityId (excluding singletons). The redundant defense-in-depth block in lwwUpdateMetaReducer (which derived id from meta.entityId) is removed — the converter is now the single chokepoint, with producers (createLWWUpdateOp, _convertToLWWUpdatesIfNeeded) keeping their enforcement for an explicit on-disk shape. (S1) - stripBatchArchivedTaskIdsFromLwwPayload: drop non-string entries rather than preserving them, and skip the cleaned[] allocation when no rewrite is needed (two-pass with early exit on first hit). (S5+S7) * refactor(sync): extract bulk-archive util, unify orphan-task filters, type validation propagation Continued review follow-ups for #7330. Behavior unchanged. - Move payload-archaeology helpers (collectCascadedSubTaskIds, stripBatchArchivedTaskIdsFromLwwPayload) out of bulk-hydration.meta-reducer into a co-located bulk-archive-filter.util.ts. The meta-reducer body drops back to its dispatcher role; the helpers gain an independent test surface and clearer naming. (S3) - Add a shared filterTaskIdArraysFromTagOrProjectPayload helper. Both filterOrphanedTaskIdsFromEntityData (lww-update.meta-reducer; predicate: not in live state) and stripBatchArchivedTaskIdsFromLwwPayload (bulk meta-reducer; predicate: in same-batch archive set) now wrap it. The two callers run at different layers because their predicates resolve at different times — the shared helper is the array-walking + payload-cloning + warn-logging core. (S4) - Replace the closure-captured piggybackValidationFailed boolean in uploadPendingOps with typed plumbing: validationFailed flows through DownloadResultForRejection and RejectionHandlingResult. The closure smuggle was fragile; the typed return is grep-able and survives refactors. The wider session-latch refactor proposed in review is deferred — current typed plumbing is well-tested. (S6 minimal) * refactor(sync): replace validationFailed plumbing with session latch + integration test Final review follow-up for #7330. Behavior unchanged; surface area shrinks. The validation-failed signal previously rode through 7 typed boundaries: DownloadOutcome.{ops_processed,no_new_ops}.validationFailed, UploadOutcome.completed.validationFailed, processRemoteOps return, forceDownloadRemoteState return, _handleSyncImportConflict return, DownloadResultForRejection.validationFailed, RejectionHandlingResult .validationFailed, plus a closure-captured piggybackValidationFailed in uploadPendingOps. Threading it correctly required every call site to remember to forward the boolean — a new variant or path that forgot would silently let IN_SYNC ride over corrupt state. - Add SyncSessionValidationService — a no-dep singleton with reset() / setFailed() / hasFailed(). RemoteOpsProcessingService.validateAfterSync and ConflictResolutionService.autoResolveConflictsLWW flip the latch when validation reports corruption. SyncWrapperService resets it at every entry point (sync(), _forceDownload(), resolveSyncConflict USE_REMOTE) and reads it once before deciding IN_SYNC vs ERROR. - Drop validationFailed from DownloadOutcome variants, UploadOutcome, DownloadResultForRejection, RejectionHandlingResult, processRemoteOps return, autoResolveConflictsLWW return, forceDownloadRemoteState return, and _handleSyncImportConflict return. ~120 LOC of plumbing collapsed to single latch reads. - Add a focused integration test (post-sync-validation.integration.spec.ts) that wires real RemoteOpsProcessingService + ConflictResolutionService against a stubbed ValidateStateService. Asserts the latch flips on validation failure and survives discarded-boolean callers — catching plumbing regressions a future code path could otherwise sneak past. Net change vs current branch: ~120 LOC deleted from production sources, ~270 LOC added in new service + tests (latch unit tests + integration). * fix(sync): close 3 latch-bypass gaps from codex review 1. SyncHydrationService.hydrateFromRemoteSync runs validateAndRepair() directly and previously dropped the result on failure. Snapshot hydration (file-based providers, USE_REMOTE force-download) would silently accept corrupt remote data — the wrapper would see a clean latch and report IN_SYNC. Now flips SyncSessionValidationService.setFailed() when isValid is false. 2. WsTriggeredDownloadService called downloadRemoteOps() outside the wrapper session contract, so any validation failure during a realtime apply was either dropped (next sync()'s reset cleared it) or leaked into the next session. The service now resets the latch up-front and reads it after the download, surfacing failures as setSyncStatus('ERROR'). 3. convertOpToAction only backfilled payload.id when missing. A malformed/older remote LWW op with payload.id \!= op.entityId would slip through and update the WRONG entity in lwwUpdateMetaReducer (which trusts entityData.id). Now forces id from op.entityId for non-singleton LWW ops, making "entityId is canonical" a hard invariant at the apply boundary. Adds regression tests for each: integration test for the snapshot path, two new tests on ws-triggered-download (latch flip → ERROR; latch reset between sessions), and a converter test for the entityId-mismatch case. * test(sync): unstick three failing supersync e2e tests Three independent flakes surfaced in the same run; root-caused from playwright traces: - daily-summary: time-estimate row icon was renamed timer→hourglass_empty on master ( |
||
|
|
a27fb75732 |
fix(restore): undo screenshot-pipeline reverts from 1d52843c
Second pass of collateral-revert recovery from the video commit. |
||
|
|
6708d133fc |
fix(restore): undo collateral reverts from 1d52843c
The "feat(video): Playwright-driven marketing reel pipeline" commit |
||
|
|
7dfc7a6efb |
test(e2e): match renamed time-estimate icon in daily-summary spec
The time-estimate row icon in task-detail-panel was renamed from
`timer` to `hourglass_empty` in
|
||
|
|
59a545ef77 |
feat(themes): user-installable plain-CSS themes
Lets users install themes by uploading a single .css file via Settings → Theme picker. Themes are device-local: the active selection lives in LS.CUSTOM_THEME, the CSS bytes live in a dedicated SUPThemes IDB. No sync — the previously synced globalConfig.misc.customTheme field is intentionally unused (kept on the model as orphan data for follow-up removal). Built-ins continue to use <link href> (unchanged from master). User themes inject as <style> after CSS validation. The validator catches the common exfiltration vectors — url(...), src(...), @import, and image-set() — across both string and bare-token forms, with comment stripping and CSS escape decoding before the scan to defeat keyword- hiding bypasses. Cached CSS is re-validated on read so bytes persisted by an older client can't sneak past a tightened validator. Implementation: - src/app/core/theme/theme-storage.service.ts (new): SUPThemes IDB CRUD, validates + size-caps uploads at 500 KB, exposes a reactive themes signal. - src/app/core/theme/validate-theme-css.util.ts (new): the security validator + bypass-class spec. - src/app/core/theme/custom-theme.service.ts: now the sole writer of LS.CUSTOM_THEME; orchestrates removeUserTheme so all uninstall paths fall back to default identically. - src/app/core/theme/theme-selector/theme-selector.component.ts: picker dropdown, install button (with security tooltip), per-row delete with warn-color icon. Errors surface via snackbar. - src/app/core/startup/startup.service.ts: applyActiveTheme runs at cold-start before informAboutAppReady, bounded by a 500 ms Promise.race so a stalled IDB can't hang the splash. - src/app/core/theme/global-theme.service.ts: drops the now-orphan _setupCustomThemeEffect that watched globalConfig.misc.customTheme. - e2e/store-screenshots/fixture.ts + seed/build-seed.ts: screenshot pipeline writes LS.CUSTOM_THEME via addInitScript (mirrors how DARK_MODE is already injected in helpers.ts). - src/app/t.const.ts + src/assets/i18n/en.json: 5 new keys. Behaviour change for existing users: anyone with globalConfig.misc.customTheme set to a non-default built-in will see Default on the first cold-start after merging; the picker still works and they re-pick once. Cross-device theme sync goes away — themes are now per-device, like darkMode. Tests: validate-theme-css 34/34, theme-storage 9/9, custom-theme 14/14, startup 18/18. Squash of 21 commits (the abandoned plugin-coupled approach + plain-CSS rewrite). The pre-squash tip is recoverable from reflog or origin until the branch is pruned: a8f274377. |
||
|
|
1d52843cd7 |
feat(video): Playwright-driven marketing reel pipeline
Adds an end-to-end pipeline for generating the marketing reel via Playwright, including tight/full variants, drag ghost, integrations layout, animated stats, brand-color logos, fade-to-black scene cuts, and a handover CLAUDE.md for the reel pipeline. |
||
|
|
7157ea62df |
feat(screenshots): polish app-store capture pipeline
Iterates the store-screenshot pipeline to ship-ready quality. Squashed from a series of capture/build/UX changes that all live in e2e/store-screenshots and form a coherent set. Pipeline UX - Print master capture path via Playwright globalTeardown. - Declare sharp as devDep (was imported by build-store-assets but never listed) so a fresh install runs the build cleanly. - Open dist/ folder when build finishes; opt out via SP_SCREENSHOTS_NO_OPEN=1. - Emit dist/screenshots/_preview.html contact sheet for one-click QA across every per-store layout. Capture content - New slot 00 hero across desktop / mobile / tablet specs: showMarketingOverlay paints a gradient caption strip on top of the live app. Position is orientation-aware (bottom for landscape, top for portrait). Copy lives in marketing-copy.ts as a single source of truth. - Desktop slot 05 captures the running focus timer instead of the duration picker (skip the rocket countdown via clock.runFor). - Desktop slot 07 = plain dark project view (no wallpaper) instead of catppuccin; slot 08 = desktop planner. - Mobile slots 02 and 04 use signal-based planner expansion (the component is gesture-only, so flip isExpanded via ng.getComponent). - Side nav collapsed for desktop slots 01 (schedule day-panel) and 04 (notes panel) so the right panel can breathe. - Mobile hides the per-task play column via appFeatures.isTimeTrackingEnabled. - Seed: drop Morning yoga (the only top-level done task on today), flip the remaining done subtask back to undone, trim the PR review tag chips down to the two EM tags that drive Eisenhower. Viewports - desktopMaster: 1280x800 CSS at 2x -> 2560x1600 (smaller MAS Retina size = more apparent zoom than the prior 1440x900 baseline). - androidPhone: 412x915 CSS at 3x -> 1236x2745, matching modern flagships (Pixel 7/8, Galaxy S22+). - Tablet rotations: 7" -> landscape, 10" -> portrait. android7Tablet moves from PHONE_VIEWPORTS to TABLET_VIEWPORTS. Test infra - Bump per-test timeout to 8 minutes for multi-locale single-session specs (12+ captures per locale overran the 180s default on slow viewports). - Scope LOCALES to ['en'] for now until German strings catch up. - ImportPage race tolerates slow imports without an encryption dialog: catch the 15s waitFor rejection so the 60s import-complete promise is the real ceiling. A cold dev server would otherwise abort here at 15s. |
||
|
|
2fabe1bdc6 |
feat(screenshots): single-session capture, tablet spec, polished seed
Fold the iterative work from this session into one commit: - Single-session capture across locales / themes / customTheme. Drives variant switches via NgRx dispatch through `__e2eTestHelpers.store` (re-enabled for non-prod/non-stage builds in src/main.ts), so one Chromium / Electron session covers every desktop and mobile permutation without relaunching the app or re-importing the seed. Replaces the per-(locale,theme) split specs with consolidated `desktop/all.spec.ts` and `mobile/all.spec.ts`. - Dedicated tablet spec (`scenarios/tablet/all.spec.ts`) for the iPad 13" and Android 10" tablet viewports, which render SP's desktop layout (≥ 768 mobile-breakpoint) but on a narrower / taller canvas than desktopMaster. Added PHONE_VIEWPORTS / TABLET_VIEWPORTS in matrix.ts and routed each viewport class to the right spec via testMatch in the playwright config. Tablet scenes are panel-free and hover-free so they fit a 960/1032-wide canvas under emulated touch. - Helpers for in-session theme / locale / customTheme flips (`applyTheme`, `applyLocale`, `applyCustomTheme`). screenshotMaster reads the live theme from `localStorage.DARK_MODE` and the live locale from `window.__spCurrentLocale` so captures land in the correct `<locale>/<theme>/` subdir. - Suppress Material tooltips via injected CSS, park the cursor at (0, 0) before each capture, and gracefully fall back to `page.screenshot()` when OS-level capture fails (e.g. macOS Screen Recording permission missing). Switch the Electron launch to `NODE_ENV=PROD` so DevTools doesn't auto-open on dom-ready. - Catppuccin scene now navigates to a project view (`desktop-07-project-catppuccin`) so the custom palette reads against the project primary, not a wallpaper. Seed builder no longer forces a bg image onto project themes — only tag themes get one, and the "Darken/lighten background image for better contrast" slider (`backgroundOverlayOpacity`) defaults to 80 (was 20). Override via `SP_SCREENSHOT_BG_OVERLAY_OPACITY` (0–99). - Seed polish: soften EM_URGENT/EM_IMPORTANT colors, drop the '@' prefix from tag titles, remove tag-waiting, trim noisy urgent/important tags from most tasks, give the deep-work tag a hue distinct from work and open source, flip `isConfirmBeforeExitWithoutFinishDay` to false so Electron closes cleanly between runs, and enable the bg tint on the TODAY tag (was disabled by default). - matrix.ts: rename `msstore` → `microsoft-store` output to avoid confusion with `macappstore`; wire flathub rule sourcing from the Electron pipeline; add `maxBytes` for Snap (2 MB JPEG re-encode). - Add `npm run screenshots:electron` umbrella script that chains `screenshots:capture:electron && screenshots:build` so the macappstore / flathub deliverables land in `dist/screenshots/` in one go. |
||
|
|
7e42a7c4e0 |
feat(screenshots): polish captures, wire flathub, snap JPEG re-encode
- Suppress Material tooltips during capture (initScript CSS + cursor park at 0,0) so leftover hover state doesn't bleed into the screenshot. - Focus-mode scene now sets the current task via the task hover-controls play button before opening the overlay, so the capture shows the real task title instead of "Select task to focus". - Add a flathub STORE_RULE (single global gallery, sourced from the Electron master dir for native GTK chrome). - Honour an optional maxBytes per StoreRule. Snap is capped at 2 MB; the post-processor re-encodes oversized PNGs as mozjpeg JPEG (q90 -> q60 step-down) until each file fits. Other stores keep lossless PNG. - README: refresh status + Snap / Flathub gotchas. |
||
|
|
6875076306 |
fix(screenshots): resolve lint errors in e2e fixture
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
246bb1f016 |
Merge branch 'feat/recommend-doing-our-screenshots-891ce2'
* feat/recommend-doing-our-screenshots-891ce2: feat(screenshots): automated app-store screenshot pipeline |
||
|
|
f98f80efe8 |
feat(screenshots): automated app-store screenshot pipeline
Playwright-driven pipeline that captures recognizable app states across all
target storefront viewports, locales, and themes from a single curated seed
dataset. Web Chromium pipeline feeds MS Store / Snap / Play / iOS / F-Droid
/ web; an Electron pipeline (with OS-level region capture for native window
chrome) feeds Mac App Store and is wired for Flathub.
Five grouped spec files capture multiple scenarios per (locale, theme,
customTheme) tuple in one session; transient UI state is reset between
scenes via page.reload() rather than relaunching contexts.
Outputs:
.tmp/screenshots/_master/<viewport>/<locale>/<theme>/<scenario>/...
.tmp/screenshots/_master_electron/...
dist/screenshots/{macappstore,msstore,snap,web,ios,play,fdroid}/...
Run:
npm run screenshots # web pipeline + per-store layout
npm run screenshots:capture:electron # Mac App Store / Flathub source
|
||
|
|
2271d7c9d4 |
test(e2e): tighten boards #7498 regression specs
Multi-review feedback: drop waitForTimeout (project rule), use the taskPage.markTaskAsDone helper instead of raw locator clicks, and scope the Eisenhower assertion to the specific quadrant the task belongs in. Also exact-match the Kanban "Create Tag" button to avoid matching the Eisenhower tab's plural label when both tab bodies briefly co-exist. |
||
|
|
6bec7fb500 |
fix(boards): keep tasks visible when done-toggle is clicked (#7498)
Eisenhower quadrants and the Kanban DONE column had filters that made a task vanish when its done-toggle fired: Eisenhower has no Done panel and all four quadrants filtered UnDone, while Kanban DONE excluded the in-progress tag that the toggle path doesn't strip. Relax the defaults and migrate existing user state on loadAllData, narrowly scoped to panels still matching the original default IDs. |
||
|
|
b51bd2c9ca
|
New focus mode rework (#7411)
* fix(android): avoid false WebView version lockout * fix(android): add WebView block recovery paths Builds on the prior authoritative-vs-fallback fix with three layered recovery mechanisms so users hit by a false BLOCK are never locked out of their data: - Last-known-good auto-recovery: persist the highest WebView version that has ever loaded the app on this device. A later transient mis-read that drops below MIN_CHROMIUM_VERSION is downgraded to WARN. - "Try anyway" override: third button on the block screen opens an AlertDialog with an explicit risk acknowledgment (crashes, render failures, possible data loss). Confirming persists an override and relaunches the app. Hardened against tapjacking via filterTouchesWhenObscured on both the activity and dialog window. - Override auto-clears once a healthy version is detected, so a future genuine block is not silently bypassed. Also tightens the UA regex (drops the misleading Safari Version/X fallback that always reads "4.0" and would falsely block) and adds diagnostic logging gated by Log.isLoggable for field debugging. Tests: 12 unit tests covering statusForVersion branches, all applyOverrides paths, and parseMajorVersion edge cases. Refs #7229 * fix(android): recover tracking after WebView cold start (#7390) When the WebView is killed in the background (e.g. profile switch on GrapheneOS) the JS-side state is lost on the next cold start, but the native foreground tracking service keeps accumulating elapsed time. The app previously discarded that elapsed time on cold start, leading to silent data loss for the user. Recovery flow: - syncTrackingToService$ detects "no current task + native is tracking" on the first emission after hydration and emits a recovery request. - syncOnResume$ does the same on warm resume. - processRecovery$ drains requests with exhaustMap, coalescing concurrent triggers onto a single in-flight recovery. - _doRecover syncs the native elapsed time onto the task and dispatches setCurrentId, restoring the JS-side tracking state. - The null→task re-emission in syncTrackingToService$ then calls updateTrackingService instead of startTrackingService when native is already tracking the same task, preserving the just-reconciled native counter (Kotlin's startTracking otherwise resets accumulatedMs). Supporting changes: - onResume$ is now a ReplaySubject(1) so cold-start emissions delivered before the JS subscriber attaches are still received. The 4 existing consumers are idempotent native-queue drains and verified safe. - parseNativeTrackingData extracted as a top-level pure function with shape validation; warning logs use a length-only fingerprint to avoid burning user content into the exportable log if the native contract ever changes. - Diagnostic 'source' label ('cold-start' | 'resume') in the recovery log line for field triage of any future re-reports. Tests: 12 unit tests for parseNativeTrackingData against the real production code, plus 4 helper-level tests for the null→task transition logic. The pipeline-level tests follow the file's existing pattern of re-implementing logic due to the IS_ANDROID_WEB_VIEW gate. Not addressed (out of scope, separate Kotlin work): write-side flush reliability under aggressive OS kills (flushOnPause$ may not complete before WebView termination). The recovery covers most cases by reading the native counter as the source of truth. * fix(sync): warn before destructive SYNC_IMPORT actions Previously the 'Server Already Has Data' dialog described a destructive SYNC_IMPORT as a 'merge' with a primary-colored 'Upload Local Data' button — leading users to clobber syncing devices' data. The decrypt- error 'Overwrite Remote' button had similarly understated copy and no final confirmation gate. - Rewrite D_SERVER_MIGRATION_CONFIRM body to call out 'overwrite' / 'replace' / 'other devices'; affirmative button is now 'Replace Server Data' with color=warn. - Rewrite D_DECRYPT_ERROR P3 + button label to make cross-device blast radius explicit. - Gate updatePWAndForceUpload behind a confirmDialog with a stronger warning string. - Add component spec for the migration dialog as a regression guard. * fix(infra): close db-startup race in supersync e2e stack pg_isready -U supersync without -d returned OK as soon as postgres accepted connections to the default database, but during first-run initdb the server briefly bounced while POSTGRES_DB was created. supersync's prisma db push then race-failed with P1001. - Healthcheck now runs psql -d supersync_db -c 'SELECT 1' so it only passes once the app's db is queryable. - Dockerfile.test entrypoint retries prisma db push up to 15x before giving up — defense in depth if anything else ever races. * chore(sync): instrument destructive-recovery paths for next incident Adds read-only diagnostic logs at the four sites a sync-stuck incident flows through, so the next occurrence is debuggable from a single log file without forensic recovery: - clean-slate.service: snapshot prior vector clock, count + opType breakdown of unsynced ops, syncImportReason — captured before any mutation - sync-wrapper.service: forceUpload(triggerSource) typed union stamps which error class drove the user into destructive recovery - remote-ops-processing.service: incoming full-state op shape + receiver's prior clock and unsynced-op tally about to be wiped - credential-store.service: encryptKey state on every fresh disk load, length-redacted ([length=N] / [empty]) — surfaces the isEncryptionEnabled=true + empty-key smoking-gun signature No behaviour change. Hot sync paths are untouched (full-state branch is gated; load() short-circuits on cache). Existing redaction patterns preserved — keys never logged in plaintext. * fix(sync): apply incoming SYNC_IMPORT silently with no pending ops Receiving clients with only already-synced data (no unsynced pending changes) used to see a conflict dialog when an incoming SYNC_IMPORT arrived. If the user picked USE_LOCAL — a natural reaction to "your data may be lost" — forceUploadLocalState() re-uploaded the pre-import state as a new SYNC_IMPORT, rolling back the import (e.g. encryption change) for every device. The originating device already gates the SYNC_IMPORT behind a strong warning (D_SERVER_MIGRATION_CONFIRM, |
||
|
|
8dd7cd99ef |
test(e2e): tighten boards #7498 regression specs
Multi-review feedback: drop waitForTimeout (project rule), use the taskPage.markTaskAsDone helper instead of raw locator clicks, and scope the Eisenhower assertion to the specific quadrant the task belongs in. Also exact-match the Kanban "Create Tag" button to avoid matching the Eisenhower tab's plural label when both tab bodies briefly co-exist. |
||
|
|
a66893abc6 |
fix(boards): keep tasks visible when done-toggle is clicked (#7498)
Eisenhower quadrants and the Kanban DONE column had filters that made a task vanish when its done-toggle fired: Eisenhower has no Done panel and all four quadrants filtered UnDone, while Kanban DONE excluded the in-progress tag that the toggle path doesn't strip. Relax the defaults and migrate existing user state on loadAllData, narrowly scoped to panels still matching the original default IDs. |
||
|
|
1e6fcb9a10 |
- fix(archive): also normalize malformed timeTracking field (#7487)
- test(e2e): regression test for malformed archive crash (#7487) - fix(archive): normalize malformed task field on load (#7487) |
||
|
|
414cd7f508 |
fix(tasks): recover focusedTaskId when shortcut fires on a focused task
After commit
|
||
|
|
340b75f1f0 |
fix(sections): drag-drop puts tasks at the right slot (#7452)
The display selector ordered in-section tasks by workContext.taskIds, ignoring section.taskIds — so reorders inside or across sections never moved on screen, and section→no-section drops snapped to the old slot. - Make section.taskIds authoritative for in-section ordering by walking sections first in undoneTasksBySection. - Fold the section→no-section reorder into a single atomic action: removeTaskFromSection now also repositions the task in the work-context's taskIds via sectionSharedMetaReducer (same pattern as TaskSharedActions.deleteTask et al), so one op covers both mutations and partial replay can't leave the task half-moved. |
||
|
|
eddcea7070 |
docs: trim CLAUDE.md and split E2E reference into e2e/CLAUDE.md
Drop the architecture overview (derivable from code and already covered in docs/sync-and-op-log/ and ARCHITECTURE-DECISIONS.md), collapse the long sync invariants into one-liners with pointers, and dedupe the anti-patterns table against the rule lists. Move the SuperSync docker-compose block and E2E iteration tips into e2e/CLAUDE.md where the rest of the E2E reference lives. Tighten the new docs/documentation-guide.md by dropping content that duplicates docs/wiki/0.00-Wiki-Structure-and-Organization.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
01e1776094 |
fix(task-repeat-cfg): re-anchor schedule when startDate moves earlier (#7423)
When editing a recurring task to move startDate earlier than the existing lastTaskCreationDay, the live instance was rescheduled to the day after the OLD startDate (getNextRepeatOccurrence advanced past the stale anchor) instead of landing on the NEW startDate. Re-anchor via getFirstRepeatOccurrence in that case. Skipped for repeatFromCompletionDate configs where startDate is decoupled from scheduling. |
||
|
|
d1a58124bf |
test(e2e): add regression for recurring missed-day bug #4559
Locks down the cold-start-after-miss path: configures a monthly "first of month" recurring task on Jun 1, jumps to Jul 3 — skipping Jul 1 entirely — and asserts the missed Jul 1 instance is materialised on reload. Uses setSystemTime rather than setFixedTime so the 1s debounceTime in TaskDueEffects.createRepeatableTasksAndAddDueToday$ keeps ticking on the freshly-reloaded page. |
||
|
|
19f1cabda0 |
test(e2e): use spinner cycle for SYNC_IMPORT silent-accept completion signal
Replace the syncCheckIcon-based completion race with a spinner visible→hidden cycle. The check icon may be stale from a prior sync, which forced the test to add a "wait for the new sync to start" guard; the spinner toggles per-sync and is unambiguous. The conflict dialog still races against completion, so the test fails fast if a regression brings the dialog back. |
||
|
|
9d3cf64986 |
fix(sync): apply incoming SYNC_IMPORT silently with no pending ops
Receiving clients with only already-synced data (no unsynced pending
changes) used to see a conflict dialog when an incoming SYNC_IMPORT
arrived. If the user picked USE_LOCAL — a natural reaction to "your
data may be lost" — forceUploadLocalState() re-uploaded the pre-import
state as a new SYNC_IMPORT, rolling back the import (e.g. encryption
change) for every device.
The originating device already gates the SYNC_IMPORT behind a strong
warning (D_SERVER_MIGRATION_CONFIRM,
|
||
|
|
f8f405c623
|
feat: add sections in projects (#6066)
* feat(sections): Introduce core section state, model, and persistence
* feat(sections): Implement SectionService and link sectionId to Task model
* feat(sections): Add 'Add Section' functionality to project context menu
* feat(sections): Implement cascading task deletion for sections
* feat(sections): Enable drag & drop for tasks into sections
* feat(sections): Display, manage, and reorder sections in Work View
* refactor(tasks): Add guard for invalid subtask moves in reducer
* feat(markdown): Add interfaces for markdown sections
* feat(markdown): Implement markdown section parsing utility
* feat(section): Add helper methods to SectionService
* feat(markdown): Add i18n for markdown section paste confirmation
* refactor(markdown): Prepare MarkdownPasteService for section support
* feat(markdown): Implement markdown section paste handling in MarkdownPasteService
* feat(markdown): Update paste detection to include markdown sections
* test(markdown): Add simple test for markdown section parsing utility
* fix(sections): post-merge type errors and stray file cleanup
- model-config.ts: drop unsupported `validate` field on section ModelCfg;
validators are looked up via validateAppDataProperty(key, data) instead.
- app-data-mock.ts: add `section: createEmptyEntity()` so AppDataComplete is satisfied.
- Remove test-sections.js (development artifact from the original PR).
* refactor(sections): atomize section deletion via meta-reducer
Replace the dispatch-chained section.effects.ts with a section-shared
meta-reducer that removes the section entity and cascade-deletes its
tasks (and subtasks) plus their references in projects and tags in a
single reducer pass.
Why:
- The previous effect used inject(Actions), so it would re-fire during
remote sync replay and double-delete tasks.
- It dispatched a separate deleteTasks action, producing two operations
in the sync log; a partial sync between them could leave a tree of
orphaned tasks pointing at a vanished section.
Changes:
- Add src/app/root-store/meta/task-shared-meta-reducers/section-shared.reducer.ts
- Register sectionSharedMetaReducer in Phase 5 of meta-reducer-registry.ts
- Remove src/app/features/section/store/section.effects.ts and its
EffectsModule.forFeature registration in feature-stores.module.ts
* refactor(sections): visual layer model + tag/today support
Make sections a pure visual grouping (analogous to boards) rather than a
property of tasks. This eliminates the cross-entity coupling that drove
most of the original PR's sync-correctness issues.
Section model:
- Drop Task.sectionId entirely. Sections own their membership via
Section.taskIds: string[].
- Generalize Section.projectId → contextId + contextType ('PROJECT' | 'TAG').
This unblocks sections in tag and TODAY views.
- New addTaskToSection action atomically removes the task from any other
section and inserts it into the target at the requested position via
adapter.updateMany — one reducer pass, one sync operation. Replaces the
old project.effects moveProjectTaskToSection effect, which dispatched
two persistent actions and was non-atomic.
Section deletion:
- Pure entity removal; no task cascade. Tasks in the deleted section
simply become "no section" tasks (DELETE_SECTION confirmation reflects
this; the now-obsolete DELETE_SECTION_CASCADE string is removed).
Section-shared meta-reducer (Phase 5):
- Repurposed from the previous deleteSection cascade handler. Now listens
for TaskSharedActions.deleteTask/deleteTasks and prunes deleted task IDs
from any section.taskIds, keeping section state consistent without
requiring an effect.
Selector / perf:
- selectSectionsByContextId is backed by a single memoized
selectSectionsByContextIdMap. Replaces the per-call factory pattern that
broke memoization across consumers.
- WorkViewComponent.undoneTasksBySection now indexes section→task in
O(n + m) using a Map keyed by taskId.
WorkContextMenu:
- Drop the @if (isForProject) gate on Add Section. The menu now offers it
for tags (incl. TODAY) and propagates the appropriate contextType.
Cleanup:
- Add takeUntilDestroyed to all dialog .subscribe() chains in
work-view.component.ts and work-context-menu.component.ts.
- Drop unused MatMenu/MatMenuTrigger/MatMenuItem imports.
- Yield the event loop every 10 sections during markdown imports.
- Revert the es.json edits added by the original PR (en-only policy).
* test(sections): cover reducer + meta-reducer behavior
- sectionReducer: addSection (default empty taskIds), deleteSection
(entity-only, no cascade), updateSection partial changes,
updateSectionOrder scoped to one context, addTaskToSection (anchor
positioning, uniqueness across sections, intra-section moves, ungrouping
on null sectionId, no-op when target missing).
- sectionSharedMetaReducer: deleteTask removes the id from any section,
cascades through subtasks, deleteTasks bulk variant, passes through
unrelated actions unchanged.
* fix(sections): wire new entity through suite-wide invariants
Fixes 11 unit-test failures introduced by adding the section model:
- ActionType enum gains SECTION_ADD_TASK (the new atomic move action)
and a corresponding ACTION_TYPE_TO_CODE entry; spec member-count
expectation bumps from 136 → 141 to match.
- extractEntityKeysFromState now emits SECTION:<id> keys so
OperationLogCompactionService recognizes section as a valid model.
- task-list enterPredicate distinguishes section drop-lists from
subtask drop-lists via drop.data.listId. Parent tasks may drop
into PARENT_ALLOWED_LISTS or any parent-level (section) drop-list,
but never into a subtask drop-list (listId === 'SUB').
- Add section: { ids: [], entities: {} } to the empty-state helper in
validate-state.service.spec; without it Typia validation fails
because section is now a required slice of AppDataComplete.
- Add activeWorkContextId$: of(null) to the WorkContextService mock
in work-view.component.spec; the section signal subscribes to that
observable in the constructor.
- Add 'section' → 'SECTION' to the modelToEntityType map in
operation-log-compaction.service.spec.
Five immediate-upload.service.spec.ts failures remain; they reproduce
on master with identical files, so they're unrelated to this branch.
* fix(sections): apply multi-review C1-C4 + harden taskIds reads
C1: Move sectionSharedMetaReducer before taskSharedCrudMetaReducer.
At Phase 5 it ran AFTER the task entity was already removed, so
state.task.entities[id]?.subTaskIds was always undefined and section
references to deleted subtasks leaked. The section spec passed only
because its mock reducer didn't apply the cascade.
C2: Tighten task-list enterPredicate. Subtasks may now drop only into
another subtask drop-list (listId === 'SUB') or appear as top-level
in DONE/UNDONE. Section drop-lists (listId === 'PARENT' with a section
id) reject subtask drags so section.taskIds stays parent-only.
C3: Add cdkDropListEnterPredicate to the sections-wrapper. Without it,
a task drag could land on the section-reorder list and dropSection
would treat a Task as a Section, corrupting ordering. The new
acceptSectionDragOnly predicate brand-checks the drag's data.
C4 + cleanup: drop the unused Section.isCollapsed field; remove the
redundant `// 1.` `// 2.` comments in section.reducer; tighten the
`(state as any).section` cast (AppStateSnapshot already declares the
field); fix the misleading meta-reducer-registry comment.
Plus a reported runtime crash: cleanupSectionTaskIds threw on
undefined `s.taskIds` for sections persisted before this branch added
the field. Defensive `?? []` everywhere that reads `taskIds`, and a
normalizeLoadedSections pass on `loadAllData` so old persisted state
is brought up to the current shape.
* fix(sections): Add Section button — drop takeUntilDestroyed on dialog sub
WorkContextMenuComponent lives inside a <mat-menu>. The menu (and the
component) is destroyed the moment the dialog opens, so
takeUntilDestroyed(this._destroyRef) tore down the afterClosed()
subscription before it could ever emit the typed title. Result: the
dialog appeared, the user typed, clicked Save — but no section was
ever dispatched.
MatDialog cleans up its own subscription once the dialog closes, so
the explicit teardown is unnecessary here. WorkViewComponent's similar
dialog subscriptions are unaffected (that component is not inside a
mat-menu).
* style(sections): move Add Section above Settings in context menu
* fix(sections): clean up orphan sections on project/tag deletion
Closes the cleanup gap surfaced by user audit: until now, deleting a
project or tag left behind its sections — they kept their stale
contextId and contextType but no longer matched any context, piling up
in state and the sync log. Section-level cleanup was only wired for
task deletion.
sectionSharedMetaReducer now also handles:
- TaskSharedActions.deleteProject → remove sections where
contextType='PROJECT' and contextId === projectId.
- deleteTag (single) → remove sections where contextType='TAG' and
contextId === id.
- deleteTags (bulk) → remove sections where contextType='TAG' and
contextId is in ids.
contextType is matched explicitly so a project and a tag that happen
to share the same id (theoretical, but possible) don't cross-pollute.
Added 3 specs covering each path, including the same-id collision case.
* fix(sections): clean section.taskIds on task move + tag removal
Two more cleanup paths surfaced by the user audit:
1. Task moves to another project (TaskSharedActions.moveToOtherProject):
the moved task and its subtasks were left lingering in any section
owned by the previous project. Now the meta-reducer reads the task's
old projectId from state (action runs before taskSharedCrud) and
strips the task ids from project-scoped sections in that project.
Tag-scoped sections are intentionally untouched because tag
membership doesn't change on a project move.
2. Task tagIds change (TaskSharedActions.updateTask): when a tag is
removed from a task, any section owned by that tag still kept the
task id. Now we diff oldTagIds vs new tagIds, and for each removed
tag, strip the task id from sections matching contextType='TAG' and
contextId === removedTagId. Tag additions are not auto-joined to a
section — that stays a user action.
The new cleanupTaskIdsInContexts helper does both jobs (restricting
edits to a specific contextType + a set of contextIds), keeping the
logic narrow.
Specs cover: project move strips only old-project sections (and
subtasks), tag removal strips only the dropped tags' sections, and
updateTask without a tagIds change is a no-op.
* refactor(sections): replace SectionContextType with WorkContextType
Drop the parallel `SectionContextType = 'PROJECT' | 'TAG'` union in
favour of the existing `WorkContextType` enum, which has identical
string values. Same on-disk shape (the Typia validator's literal-string
serialization is unchanged), but the model now reuses the canonical
work-context taxonomy and consumers can pass `activeWorkContextType`
straight through without translation ternaries.
Touches: section.model + section.service signature, the section-shared
meta-reducer's helpers and handlers (`WorkContextType.PROJECT/TAG`
instead of literals), the work-context-menu / work-view / markdown-paste
call sites (drop the `=== WorkContextType.PROJECT ? 'PROJECT' : 'TAG'`
ternary), and the spec fixtures.
* refactor(sections): split addTaskToSection — drop the 'NONE' sentinel
The 'NONE' string sentinel for ungrouped op-log entries had two
problems: (1) two clients ungrouping different tasks concurrently
both wrote ops keyed on entityId='NONE', so LWW conflict resolution
collapsed them and one ungroup was discarded; (2) magic string with
no constant or test pinning it.
Split into two persistent actions:
- addTaskToSection({ sectionId, taskId, afterTaskId? }) — sectionId
is now non-nullable; entityId tracks the destination section. The
reducer still atomically strips the task from any other section
(uniqueness invariant), so a single op covers a full move.
- removeTaskFromSection({ sectionId, taskId }) — entityId is the
source section. Concurrent ungroups from different sources are now
independent ops with distinct entityIds; LWW conflict resolution
treats them correctly.
The drag-drop call site already knows both source and target list ids
(srcIsSection / targetIsSection), so the SectionService just exposes
two narrow methods: `addTaskToSection(target, ...)` and
`removeTaskFromSection(source, ...)`. The work-view caller uses
addTaskToSection only (markdown paste never ungroups).
Two new specs cover removeTaskFromSection: strips only the named
section, no-op when the task isn't there, no-op when the section is
missing.
* fix(sections): apply round-2 review batch
Addresses ten findings from the second multi-review pass.
Security
- Replace plain-object grouping caches with Map<string, …> so a
malicious sync peer can't poison Object.prototype via crafted
contextIds like '__proto__'. Affects selectSectionsByContextIdMap;
the per-call factory selector is removed (see Performance below).
Correctness
- normalizeLoadedSections defends against state.ids === undefined
(very old persisted shapes wrote `section: {}`).
- Section meta-reducer now also handles TaskSharedActions.updateTasks
(bulk). Previously only updateTask fired the tag-removal cleanup;
a future bulk dispatcher mutating tagIds would silently leak orphan
task ids in tag-context sections. Spec covers it.
Architecture
- Phase 3.5 promoted to a named phase in the registry doc block.
- validateMetaReducerOrdering pins sectionSharedMetaReducer to run
before taskSharedCrudMetaReducer; any future reorder fails
dev-mode validation. The handlers' pre-CRUD state read is now
documented at the top of section-shared.reducer.ts.
Alternatives + Performance
- Drop the per-call selectSectionsByContextId factory. The service
selects selectSectionsByContextIdMap and pipes map(...) — no fresh
MemoizedSelector per call, no leak.
- addTaskToSection reducer breaks out of the uniqueness scan after
the first removal (a task is in at most one section at a time).
- Pre-filter sectionSharedMetaReducer on a static
HANDLED_ACTION_TYPES Set so the 99% of dispatches that don't match
short-circuit before allocating the handler dispatch table.
Simplicity
- Collapse SectionService.{addSection, addSectionWithId,
generateSectionId} into a single addSection that returns the new
id synchronously. Markdown-paste consumes it directly.
- Remove collectTaskAndSubtaskIds (single-task path is just
collectAffectedTaskIds with a one-element array).
- dropSection drops the object spread + extra map; reorder ids in
place.
- Tighten ExtendedState — SECTION_FEATURE_NAME is always registered,
so the optional-typing dance and the four `if (\!sectionState)`
early-returns inside helpers are removed.
* fix(sections): apply round-3 review batch
Fixes from a multi-reviewer pass (codex + claude + code-reviewer
sub-agent) on the sections feature.
Critical
- task-list: subtask-to-subtask drag was being misidentified as a
section move. _move() now takes srcListId/targetListId and only
treats a non-reserved listModelId as a section when listId is
PARENT. Subtask drop-lists ('SUB') fall through to moveSubTask.
- parse-markdown-tasks: tasks before the first markdown header were
silently dropped by parseMarkdownWithSections. They now flush into
a top-of-list "No Section" entry. Also broaden the header regex
from /^#\s+/ to /^#{1,6}\s+/ so H2-H6 are recognized.
- section.actions/reducer/service: addTaskToSection now carries an
explicit sourceSectionId. The reducer strips from that source
rather than searching state, making replay deterministic. Action
meta sets entityIds: [src, dest] when src is provided so vector-
clock conflict detection covers both. Callers in task-list and
markdown-paste pass the actual source (or null for new tasks).
Warnings / cleanups
- section-shared meta-reducer: handle removeTasksFromTodayTag and
localRemoveOverdueFromToday — TODAY is virtual so the existing
tagIds path doesn't catch them. Doc enumerates the residual gap
(scheduling/planner/short-syntax/crud/lww) and proposes a future
Phase 6.5 diff-based meta-reducer for full coverage.
- section-shared reducer: typed as MetaReducer<RootState> /
ActionReducer<RootState, Action> instead of any.
- markdown-paste: yield every 30 dispatches (was: every 10 sections)
per CLAUDE.md rule #11. Type the reduce accumulator as number.
- task-list: extract RESERVED_LIST_IDS constant; comment why it
diverges from PARENT_ALLOWED_LISTS on LATER_TODAY.
- en.json/es.json: restore trailing newlines (es.json now identical
to master).
Tests
- parse-markdown-tasks.spec: 6 cases for parseMarkdownWithSections
(H1, H2/H3, pre-header tasks, empty sections, null input).
- section.reducer.spec: 4 cases for sourceSectionId behavior
(explicit source, null, omitted, intra-section reorder).
- section-shared.reducer.spec: 3 cases for TODAY removal cleanup.
- task-list.component.spec: 5 cases for _move() routing
(subtask vs section disambiguation, source propagation).
* fix(sections): apply round-3 review nits
- task-list: type RESERVED_LIST_IDS values via `satisfies
DropListModelSource[]` so adding a new variant to the union
surfaces a typo here without forcing casts at every `.has()`.
- section.actions: tighten addTaskToSection doc — the `entityIds:
[src, dest]` claim only holds when src is a non-null string
different from dest; intra-section / null fall back to
single-entity meta.
- task-list spec: add two anchor cases for `_move` so
`getAnchorFromDragDrop` is actually exercised on section drops
(previous tests passed `[taskId]` only, which short-circuited to
null).
* fix(sections): apply round-4 review batch
High-severity sync/UX fixes:
- deleteProject now also strips cascaded task ids from tag-context
sections (one extra cleanupSectionTaskIds pass in the meta-reducer);
+ unit test
- Edit Section dialog prefill: val → txtValue
- Markdown paste: drop yieldIfNeeded mid-loop (widened the sync
interleave window); residual atomic-bulk-action gap documented
- Add Section menu item guarded against TODAY_TAG
- Paste hook switched from id-prefix scan to data-task-id attribute
- New section referential validators + auto-repair (data-repair)
- Section view bypasses customizer when filter/sort active
- Parser hardening: input cap, CRLF/BOM normalization, reduce-based
min-indent (avoids RangeError on huge spreads), JSDoc fix
- moveSubTask guard now also rejects self-moves; console.warn →
TaskLog.warn
- Work-view styling: use --s/--s3 tokens, narrow \!important rules
- Drop dead loadSections action + SectionService.sections$
- Trailing-space prettier nits in sync.model.ts + feature-stores.module
* fix(sections): apply multi-review batch — moveToArchive + cleanup
- moveToArchive: add handler in section-shared meta-reducer so archived
tasks (and their subtasks) no longer leak as stale ids in
section.taskIds until dataRepair clears them.
- Collapse the dual HANDLED_ACTION_TYPES + handlers map into a single
ACTION_HANDLERS record; the prior split is what allowed moveToArchive
to drift out of sync.
- Make addTaskToSection's sourceSectionId required (string | null) and
delete the legacy defensive sweep branch in the reducer.
- updateSectionOrder now keeps other-context sections in their slot in
state.ids instead of pushing them to the front.
- Sanitize section titles (trim + 200-char cap) on add/update.
- Remove dead addSection() in work-view.component.ts (template only
wires editSection/deleteSection; addSection lives on work-context-menu).
- Remove dead translation keys WW.ADD_SECTION and WW.ADD_SOME_TASKS.
- Drop hasHeaders field from MarkdownWithSections (null return already
encodes header-less input).
- Drop normalizeLoadedSections + ?? [] guards (Section is brand-new, no
legacy persisted shape exists) and unused entity adapter exports.
* perf(sections): apply perf review batch — drop-list coalesce + meta-reducer hot paths
- DropListService: coalesce burst register/unregister calls via a
microtask flush. Mounting 50 sections previously emitted 50 times
through the BehaviorSubject; cdkDropListConnectedTo rebuilds its
sibling graph per emission, so first paint cost was ~O(L²). Now
one downstream emission per CD pass.
- section-shared meta-reducer: replace Object.values(entities) sweeps
with `for-of state.ids` (no intermediate array alloc on every matched
task action) and collapse `.some + .filter` double-walk into a single
pass that returns null when nothing changed. Halves walk count on
affected sections under op-log replay.
- updateTasks handler: aggregate (taskId → removedTagIds) across the
batch into a `tagId → tasksToRemove` map, then sweep candidate
sections once with a single adapter.updateMany. Drops a 50-task
batch from O(N·S) to O(N + S).
- section.service: stable EMPTY_SECTIONS constant for contexts with no
sections, so OnPush downstream isn't broken by a fresh `[]` per
emission.
- work-view template: drop `|| []` on dict[section.id] — the dict is
pre-populated per section, so the fallback was allocating a fresh
array reference per CD pass for every empty section.
- acceptSectionDragOnly: bind [cdkDragData]="section" and reduce the
predicate to one property read; was running shape-validation on
every dragOver tick.
- markdown paste: cheap regex pre-screen in isMarkdownTaskList so
plain-text Ctrl+V pastes bail before invoking the parser, and
memoise the sectioned-parse result by string reference so the
immediately-following handleMarkdownPaste doesn't re-parse.
* feat(sections): project-only Add Section + empty main-list hint
- work-context-menu: tighten Add Section gating from
`isForProject || contextId \!== TODAY_TAG_ID` to `isForProject`. The
menu item is now hidden for the today list and for tag contexts in
general; sections in tag contexts can still arrive via markdown paste.
- work-view: when the sectioned view's no-section bucket is empty
(every task lives in a section), render a small italic hint
("All tasks are organized into sections") instead of leaving the
area silently empty.
* fix(sections): keep no-section drop target alive when empty
Previously the no-section area swapped task-list for a <p> hint when
empty, removing the cdkDropList. That blocked drops from sections back
into the main list. Restore the always-rendered task-list and pass the
hint via its built-in [noTasksMsg]:
- task-list keeps its drop target so cross-section moves work.
- The hint is muted via task-list's existing .no-tasks styling
(var(--text-color-muted)).
- The hint is hidden when undoneTasks() is empty so it doesn't
duplicate the horizon "no tasks planned" empty state.
* feat(sections): show Add Section in every work-context menu
Drop the gate around the Add Section menu item so it shows for
projects, regular tags, and the today list. Sections in TODAY are
stored as TAG-context sections under the TODAY tag id, matching the
existing tag-section flow.
* fix(sections): apply round-6 multi-review batch
Critical:
- enterPredicate rejects backlog → section drops; previous path left
the task in both backlog and section.taskIds.
- editSection / addSection trim whitespace before the truthy check
(sanitizeSectionTitle would otherwise produce empty titles).
- TODAY_TAG cleanup: post-reducer diff in section-shared meta-reducer
catches every flow that removes ids from TODAY (scheduling, planner,
short-syntax, undo, lww), closing the documented residual gap. Bulk
action handlers retained — handler + diff are idempotent.
Sync-replay defense:
- updateSectionOrder accepts partial / out-of-date payloads via dedup +
append-missing instead of a fragile cursor walk; new tests cover
shorter and stale payloads.
- sanitizeSectionTitle moved to section.model and applied inside the
reducer for addSection / updateSection so remote ops can't bypass
the 200-char cap.
Cleanup:
- markdown-paste memo moved from module scope to instance state, with
explicit clear after consumption (clipboard content no longer
retained for the rest of the session).
- undoneTasksBySection bound once via @let in the work-view template.
- Factory selector selectSectionsForContext(contextId) replaces the
whole-map mapping in SectionService.
- Drop log-only validateSections (data-repair already cleans).
Follow-ups documented inline (not in this PR):
- task.sectionId membership model (W4)
- removeTaskFromSection → addTaskToSection consolidation (W3)
- marked-based parseMarkdownWithSections (S1)
* fix(sections): guard TODAY-tag diff against undefined state slices
`diffRemovedTodayTaskIds`, `applyTodayTagSectionCleanup`, and
`collectAffectedTaskIds` all assumed their slices existed. During
early boot / undo / hydration paths the tag, task, or section slice
can be undefined, causing the meta-reducer to crash on
`prevTagState.entities[...]`. The crash cascaded into selector
errors (idleTime, activeType, isShowAddTaskBar) because subsequent
reducer passes never ran.
Bail out early when slices are absent — there's nothing to clean up
yet.
* test(sections): add e2e coverage for basic section flows
Covers the core user-visible section behavior:
- create section via project header context menu
- reject whitespace-only titles (verifies the C2 trim fix)
- edit section title via the per-section menu
- delete section after dialog confirmation
- drag a task into a section (Angular CDK drag-drop helper, since
Playwright's `dragTo` uses HTML5 drag events that CDK ignores)
- sections persist across a page reload (IndexedDB hydration)
The reverse drag (section → no-section bucket) is fixme'd: forward
drag passes, but the reverse target's bounding box collapses to a
hint message when empty and the CDK drop won't register reliably
in headless. Reducer-level coverage in section.reducer.spec.ts
(`removeTaskFromSection`) substitutes for now.
* fix(sections): apply round-7 multi-review batch
Hardening:
- Reducer-side `sanitizeSectionTitle` now coerces non-string input
(null / undefined) to "" instead of throwing. A malformed remote op
(`addSection({ section: { title: undefined } })`) was the threat
model the cap was added for; previously it crashed the reducer.
- `updateSection` now sanitizes on key-presence (`'title' in changes`)
rather than `typeof === 'string'` so a peer shipping `title: null`
cannot bypass the cap and corrupt the entity's typed contract.
- Single dispatcher-level boot guard in `sectionSharedMetaReducer`
catches every action handler that touches task / tag / section
slices, replacing the asymmetric per-function guards from round 6.
Cleanup:
- Revert the round-6 `selectSectionsForContext` factory selector. It
allocated a fresh `MemoizedSelector` per `getSectionsByContextId$`
call, undermining the memoization the simplification claimed. The
service's previous `.pipe(map(m => m.get(id) ?? EMPTY))` shape is
one fewer layer with identical behavior — single consumer, no win.
- Move `sanitizeSectionTitle` + `MAX_SECTION_TITLE_LENGTH` to
`section.utils.ts` (model files in this repo are pure type files).
- Inline `_clearSectionsCache()` (single caller).
- Drop the redundant 5-step settle-move at the end of the e2e
`cdkDragTo` helper.
Tests:
- Pin the actual ordering in the `updateSectionOrder` partial-payload
test (was only checking dedupe count, missed the explicit shape).
- New: empty-string title passes through (legitimate clear).
- New: null/undefined title is coerced, not stored as null.
- New: malformed remote op with undefined title doesn't crash addSection.
- Replace `waitForTimeout(500)` in the e2e whitespace-rejection test
with a polling `toHaveCount(0)` assertion (project rule violation).
- Strengthen the `updateSection` cap test with leading-whitespace input.
Deferred (tracked as follow-ups):
- W2: explicit `handleRemoveFromTodayTag` redundant with the diff —
keep both for now; soak the diff before unifying.
* test(sections): drop unused eslint-disable directives
Use `as unknown as string` casts instead of `as any` + eslint-disable
in the malformed-input reducer tests. Same intent (force the type
assertion to model what a malicious peer's payload looks like at
runtime), no lint suppression needed.
* style(sections): drop redundant message from Add Section dialog
The Add Section dialog passed both `placeholder: T.G.TITLE` and
`message: T.CONFIRM.ADD_SECTION` to DialogPromptComponent. The
message was redundant — its text ("Add Section") duplicated the
dialog's contextual purpose, and showing it forced the
mat-dialog-content out of `.isNoMsg` mode, adding outer padding
that the Add Tag dialog doesn't have.
Drop the message so the Add Section dialog matches the Add Tag
visual pattern (no outer padding, just the input field). Also
remove the now-unused `T.CONFIRM.ADD_SECTION` translation key.
* fix(sections): apply round-8 multi-review batch
Hardening:
- `sanitizeSectionTitle` swaps `String(title ?? '')` for a
`typeof === 'string'` fast-path. Closes two real defense gaps:
- `String(Symbol())` would throw and crash the reducer.
- `String({toString: () => 'x'.repeat(2**27)})` would materialize
a ~256 MB string before the slice. JSON op-log payloads can
smuggle 2 MB+ literal strings that survive the wire.
Same behavior for the documented `null` / `undefined` cases.
- Reducer + service now share a `hasTitleChange()` helper using
`Object.hasOwn` instead of `'title' in changes`. Strictly better
semantics — `'in'` walks the prototype chain, so a peer payload
with `Object.create({title: 'x'})` would have triggered
sanitization on a key the entity doesn't carry.
- Service-side `updateSection` now uses the same key-presence check
as the reducer (was `typeof === 'string'`); removes a future
reader's "why two checks?" smell.
UX / a11y:
- Add Section dialog now passes `placeholder: T.WW.ADD_SECTION_TITLE`
("Add Section") instead of generic `T.G.TITLE` ("Title"). The
dialog has no title element and no message text, so the
placeholder is the only context users (especially screen-reader
users) get for what they're naming. Mirrors the Add Tag pattern
("Add new Tag" placeholder).
Convention:
- Rename `section.utils.ts` → `section.util.ts`. Repo uses singular
`*.util.ts` (60+ files) vs plural `*.utils.ts` (was 2 outliers).
* feat(sections): Add Section in work-view background context menu
Right-click on the empty area of the work-view (project, tag, or
Today view) now opens a small context menu with one item: "Add
Section". This is a discoverability complement to the side-nav
overflow button — the work-view is where the user is already
focused when they decide they need a section, so the action should
be reachable without navigating back to the side-nav.
Implementation:
- `(contextmenu)` listener on `.task-list-wrapper` calls
`onBgContextMenu`, which skips when the click target is inside
an interactive element (task, button, input, drag handle, …) so
per-element context menus and native form behavior aren't shadowed.
- A hidden `[matMenuTriggerFor]` div is positioned at the cursor
via signals (`bgContextMenuX`, `bgContextMenuY`).
- `addSection()` reuses the same DialogPromptComponent shape as
the side-nav addSection (no `message`, descriptive placeholder),
and reads `activeWorkContextId` / `activeWorkContextType` from
`WorkContextService` so it works across project / tag / Today.
E2E test: right-click → menu item → submit dialog → assert the
section is rendered.
* fix(sections): initialize section slice in dataRepair for legacy migrations
Legacy 'pf' databases predate the sections feature and don't carry
a section field. After Typia validation rejects the missing field,
`dataRepair` was supposed to fix it — but `_repairSections` early-
returned on missing state instead of initializing it, so re-validation
failed and `OperationLogMigrationService._performMigration` aborted
with "Migration failed."
Match the existing init pattern (archiveYoung, archiveOld, reminders):
populate `dataOut.section = { ids: [], entities: {} }` if absent
before per-slice repair runs.
Verified by running e2e/tests/migration/legacy-data-migration.spec.ts.
* fix(sections): apply round-9 thorough review batch
Critical:
- Reject section → BACKLOG drag in `enterPredicate`. The handler
was removing the task from `section.taskIds` but never dispatching
`moveProjectTaskToBacklogList`, so the task vanished from the
section without appearing in the backlog. Mirrors the existing
BACKLOG → section guard.
Warnings:
- `section.reducer.ts: loadAllData` now falls back to
`initialSectionState` when the payload omits `section`. Previous
behavior preserved stale local state, violating SYNC_IMPORT /
BACKUP_IMPORT semantics ("complete fresh start").
- `markdown-paste.service.ts` checks `sectionTitle?.trim()` before
calling `addSection`, so headers like `## ` (zero-width space)
fall through to the noSection bucket instead of creating a
titleless section.
- `moveToArchive` section cleanup unions payload subtasks with the
state-derived expansion. Robust under both threat models: replay
where the parent entity is gone from state (payload carries the
tree) AND callers passing `subTasks: []` (state lookup is the
only signal).
Suggestions:
- Section overflow button gets an `aria-label="Section options"`
(new `T.WW.SECTION_OPTIONS` translation key).
- `_repairSections` now does `Array.isArray(taskIds)` instead of
`?? []` — defends against malformed remote payloads where
`taskIds` is a truthy non-array value.
- `acceptSectionDragOnly` discriminates on `'contextType' in data`
rather than `Array.isArray(data.taskIds)`. The latter would
silently accept Task drags if Task ever gained a `taskIds` field.
Documentation:
- Added LWW conflict-resolution gap to the section-shared
meta-reducer's KNOWN FOLLOW-UPs block. Project- and non-TODAY-tag-
context section.taskIds can leak phantom references after LWW
resolves a tagIds / projectId update; the diff catches TODAY but
not other contexts. Visible impact bounded by render-time
intersection in `undoneTasksBySection`. Cleaned by next
`dataRepair` pass.
False positive: `extract-entity-keys.ts` already guards via
`entityState?.ids` optional chain — no change needed.
Verified: 24 reducer tests, 17 meta-reducer tests, 51 data-repair
tests, 35 task-list tests, 7 work-view tests, 7 active section
e2e tests + migration e2e all pass.
* refactor(sections): simplification pass — drop micro-helpers + comment cleanup
A focused simplification review of the branch found small wins
that the correctness-focused rounds had accreted around.
Removed:
- `hasTitleChange()` helper. Single-line `Object.hasOwn(c, 'title')`
is clearer at the two call sites than a 3-line wrapper.
- `_parseSectionsCached` instance memo + private fields in
`MarkdownPasteService`. The `MARKDOWN_TASK_OR_HEADER_RE` pre-screen
already gates plain-text pastes; for genuine sectioned input the
parser walks once cheaply and the dialog wait dwarfs parse time.
Removes mutable instance state and the `MarkdownWithSections` type
import that only existed for the cache.
Comment trimming (74 LOC removed across 5 sites, no behavior change):
- `addTaskToSection`: 22 → 8 lines (the architectural follow-up was
duplicated elsewhere; kept the essential meta-shape note).
- `Phase 3.5 placement` block: 11 → 3 lines (runtime
`validateMetaReducerOrdering()` already enforces it).
- `moveToArchive` handler: tightened the union-rationale.
- `acceptSectionDragOnly`: 8 → 2 lines.
- `updateSection` reducer: 6 → 2 lines.
- `updateSection` service: dropped redundant explainer.
Things considered and intentionally kept (defended):
- `section.util.ts` — file is clean (one constant + one normalizer);
matches the codebase's per-feature util pattern.
- `section.service.ts` — codebase has a service per feature.
- `EMPTY_SECTIONS` frozen const — a fresh `[]` per emission would
cause downstream computed signals to recompute even for empty
contexts.
- Reducer-side `sanitizeSectionTitle(unknown)` — defends the real
schema-evolution case where a peer ships malformed payloads.
- Boot-guard 3-slice check — needed because the diff path
dereferences tag state.
Verified: 24 reducer tests, 17 meta-reducer tests pass.
* refactor(sections): restrict scope to projects + TODAY tag
Custom-tag sections added per-tag cleanup machinery (deleteTag /
deleteTags handlers, handleTaskTagsChange, handleBulkTaskTagsChange,
removeTasksFromTodayTag/localRemoveOverdueFromToday explicit handlers)
and broadened the validation surface for limited end-user value.
- Add isValidSectionContext helper enforcing PROJECT or TODAY-only.
- Guard at all entry points: SectionService.addSection (returns null),
section.reducer (rejects invalid contextId/Type), data-repair drops
invalid sections on import.
- Hide "Add Section" in custom-tag context menu and suppress the
work-view bg right-click menu there.
- Drop tag-scope handlers from section-shared meta-reducer; rely on
the existing diff-based TODAY_TAG.taskIds path for TODAY cleanup.
- Update unit tests to match.
Net: -243 LOC.
* refactor(sections): drop markdown section paste
The H1-grouped markdown paste path created sections + tasks atomically
from a hand-rolled parser, but the feature is niche, the parser is
duplicated logic next to the existing flat-list and structured paste
paths, and the documented atomicity caveat (partial state on
concurrent sync) was never resolved.
Falls back to the existing flat-list / sub-task paste paths, which
cover the dominant use case.
- Remove parseMarkdownWithSections + MarkdownWithSections /
SectionWithTasks types.
- Drop the section-paste branch and SectionService /
WorkContextService deps from MarkdownPasteService.
- Drop CONFIRM_SECTIONS i18n key.
- Drop spec coverage for the removed parser.
Net: -251 LOC.
* refactor(sections): post-review hardening + cleanups
Apply five findings from the latest cross-reviewer pass.
- loadAllData filters imported sections through isValidSectionContext.
Typia validates structure but not invariants, so a backup from an
older client could otherwise smuggle custom-tag sections back in.
- updateSection rejects payloads that touch contextId/contextType.
No legitimate flow rewrites a section's context — a malformed peer
would otherwise morph a project section into a custom-tag one.
- Add 'section' to ENTITY_STATE_KEYS so _resetEntityIdsFromObjects
reconciles ids ↔ entities like every other entity slice.
- Drop redundant service-side sanitize in updateSection — the reducer
is authoritative.
- Refresh stale addSection JSDoc (markdown-paste reference removed
with the section paste path in
|
||
|
|
fb61ed7d92 |
fix(boards): re-enable Save in Eisenhower Matrix edit dialog (#7380)
The conditionally-required match-mode and sortDir radios placed `defaultValue` inside `props`, which Formly ignores — its core extension reads `field.defaultValue`. So opening any board with >=2 included or excluded tags left those required fields undefined and the Save button disabled. Even with the default lifted, Formly skips defaults for fields hidden at init and on hidden→visible transitions unless `resetOnHide: true` is set, so picking a sortBy re-locked the form. Lift `defaultValue` to field level and add `resetOnHide: true` on `includedTagsMatch`, `excludedTagsMatch`, and `sortDir`. Add a unit spec exercising the panel fieldGroup and a Playwright e2e that drives the dialog end-to-end. |
||
|
|
3ae246d030
|
Feat/issue 7362 9d46d3 (#7363)
* test(e2e): open attachment dialog via detail panel after #7314 The attach-dialog entry point moved out of the task context menu in PR #7314 and now lives in the detail panel. Update the WebDAV sync attachment test to use openTaskDetailPanel() and click the attachment input-item instead of right-clicking and looking for an "Attach" menu item that no longer exists. * refactor(tasks): drop dead addAttachment from task context menu Leftover from #7314, which moved the attach dialog into the detail panel. The method, its TaskAttachmentService injection, and the DialogEditTaskAttachmentComponent import are unreachable from the template. * fix(api): reject inherited fields when creating subtask via REST POST /tasks with parentId silently dropped any supplied projectId/tagIds (the reducer forces tagIds=[] and projectId=parent.projectId), so callers got 201 with values different from what they sent. Reject the request with 400 UNSUPPORTED_FIELD instead, symmetric with how subTaskIds and parentId-on-PATCH are handled. * refactor(simple-counter): inline countdown wrapper, document set invariant Inline the single-call-site `_hasStartedRepeatedCountdown` private wrapper. Promote the `setCountdownRemaining` invariant note to JSDoc so it surfaces in IDE tooltips at every call site — paused-display is gated by `hasStartedCountdown`, and writing through `setCountdownRemaining` without a prior `startCountdown` call would silently leave the value invisible. * ci(plugins): run unit tests when plugin code changes New workflow runs each plugin's `npm test` in a parallel matrix when its directory or a shared package (plugin-api, vite-plugin) changes on a PR. Detects per-plugin changes via three-dot `git diff` against the PR base. * fix(sync): break iOS WebDAV conflict-dialog loop (#7339) Two compounding bugs trapped iOS WebDAV users in a per-minute conflict dialog that no button could resolve: 1. FileBasedSyncAdapter's snapshotReplacement heuristic re-fires gapDetected on every sync from a non-writing client (clientId \!= excludeClient is true forever), so the download keeps coming back with snapshotState and OperationLogSyncService keeps throwing LocalDataConflictError despite the local clock already dominating the remote snapshot. Skip hydration and conflict when compareVectorClocks(local, remote) is EQUAL or GREATER_THAN, gated on both clocks being non-empty so a fresh client still hydrates a legacy/clockless snapshot. 2. SyncWrapperService._openConflictDialog$ filtered undefined out of the afterClosed() stream, so a programmatic close (iOS WebView lifecycle, re-entry) collapsed the observable and firstValueFrom threw EmptyError — the user's Use Local/Use Remote/Cancel click never reached the resolution branches. Drop the filter so undefined flows through to the existing cancellation path. The dominate-skip deliberately does NOT append result.newOps to the op log: VectorClockService.getEntityFrontier is last-write-wins by seq, and writing historical remote ops at the current tail would regress per-entity frontiers and let future LWW resolution overwrite local data. Trade-off documented inline. Adds an adapter-level integration reproducer that asserts gapDetected re-fires forever for a non-writing client (the upstream loop trigger), a 3-client WebDAV e2e that reproduces the loop end-to-end against a real provider, plus service-level tests for the dominate-skip, the empty-clock guard, the concurrent-clock conservative path, and consecutive-sync loop prevention. * fix(sync): improve Dropbox auth dialog UX on sandboxed Linux (#7139) The "Get Authorization Code" button silently failed on Flatpak because shell.openExternal rejects without renderer feedback when the org.freedesktop.portal.Desktop talk-name isn't granted. After the user gave up and reopened the dialog, the second attempt failed again with `invalid_grant: invalid code verifier` because each call to Dropbox.getAuthHelper() generated a fresh PKCE verifier that no longer matched the originally-shown URL's challenge. Three changes: - Cache the in-flight PKCE Promise on the Dropbox provider so concurrent callers and consecutive dialog opens share one verifier+URL pair. Cleared on successful exchange, on clearAuthCredentials(), and on a rejected generation (so a one-time crypto failure doesn't poison the session). Five regression tests cover reuse, success-clear, explicit clear, concurrent calls, and rejection-recovery. - Render the auth URL as user-selectable text under a <details> disclosure. Escape hatch when both shell.openExternal and the clipboard portal are denied — the user can triple-click to select and Ctrl+C the URL into a manually-opened browser. Adds D_AUTH_CODE.MANUAL_URL_HINT translation key. - Pipe shell.openExternal rejections through errorHandlerWithFrontendInform so the existing IPC.ERROR snack channel surfaces a "Could not open the link in your browser" message instead of swallowing the failure to electron-log. Wrapped in a try/catch since errorHandlerWithFrontendInform throws synchronously if the renderer isn't ready. The Flathub manifest also needs --talk-name=org.freedesktop.portal.Desktop and --socket=wayland to fully fix the user-reported issue, but that change lives in the flathub repo. * refactor(sync): rename dialog-sync-initial-cfg to dialog-sync-cfg Pure rename — no behavior change. The component is the canonical sync config dialog, used both for first-time setup and editing. The "Initial" qualifier was misleading once it became the only sync-config surface (see #7362). * refactor(sync): consolidate sync actions into the sync dialog Move Re-authenticate, Force overwrite, and Restore from history into DialogSyncCfgComponent so the dialog is the canonical surface for sync configuration and management. - Re-authenticate (OAuth providers, currently Dropbox) shown inline when the active provider has getAuthHelper() and is already authed. Bypasses the dirty-form gate so credentials can refresh without losing in-progress edits. - Force overwrite and Restore from history live in a new "Advanced" section gated on isWasEnabled() (existing config) and disabled when the form is dirty with a "Save changes first" tooltip. Restore stays SuperSync-only. - Force overwrite reuses the existing native confirm in SyncWrapperService.forceUpload() — no extra confirmation dialog to avoid double-confirm on the other 5 internal callers. The legacy buttons on the settings page are removed in a follow-up commit. Refs #7362 * refactor(sync): move WebDAV Test Connection button into the sync dialog Test Connection was previously injected into the inline sync form on the settings page. It needs to live wherever the sync form is rendered. The next commit removes the inline form, so move the injection into DialogSyncCfgComponent first. No user-visible change: the button still appears in the WebDAV section of the sync form, just sourced from the dialog component now. * refactor(config-page): replace inline sync form with status row + buttons The Sync & Backup tab now hosts a compact status surface, not a full form clone: - When sync is disabled or unconfigured: empty-state hint + "Set up sync" primary button. - When configured: provider name, optional "Authentication required" pill (OAuth providers) and "Encrypted" pill, then "Sync now" primary button + "Configure" secondary button. The dialog is the canonical sync config surface — reachable from this tab, the header sync icon (right-click / long-press), or SyncWrapperService when first-time setup is needed. Force overwrite and Restore from history live inside the dialog now (see prior commit). Removes _buildSyncFormConfig (~250 lines of formly button injection), the empty authStatus placeholder field in SYNC_FORM, and the unused tour-syncSection class. Refs #7362 * refactor(sync): consolidate BTN_FORCE_OVERWRITE translation key The SUPER_SYNC.BTN_FORCE_OVERWRITE key was a duplicate with no consumers — its English copy ("Force Overwrite Server") even drifted from the canonical F.SYNC.S.BTN_FORCE_OVERWRITE ("Force Overwrite"). The third occurrence under D_ENTER_PASSWORD ("Use Local Data") stays — that's a different conflict-resolution action, not a duplicate. Refs #7362 * refactor(sync): simplify sync dialog — collapse advanced fields, kebab menu The previous "Advanced" zone (hr + heading + hint + button stack) plus always-visible interval/manual-only/compression toggles made the dialog visually noisy. Two changes: - Move syncInterval and isManualSyncOnly into the existing "Advanced Config" collapsible alongside compression. The collapsible is hidden for SuperSync (which uses fixed settings) — most users never need to expand it. - Move Force overwrite + Restore from history out of the dialog body into a kebab (more_vert) menu in the dialog title row. They act on saved config — the kebab placement makes that scope clear and removes the dirty-form gate / "Save changes first" ambiguity. The kebab is hidden during first-time setup (gated on isWasEnabled). The Re-authenticate button stays inline as before. Drops three translation keys added in the prior commit (ADVANCED, ADVANCED_HINT, SAVE_FIRST_HINT) — no longer referenced. Refs #7362 * fix(sync): drop primary color from Cancel button in sync dialog Cancel is a secondary action; only the affirmative button (Save) should carry the primary color. * fix(sync): clarify Dropbox info text for the new dialog flow The previous copy said "Click the button below to authenticate" — but in the consolidated dialog there is no inline Authenticate button for first-time setup; OAuth runs as part of Save. Make the copy describe the actual flow instead of pointing at a button that no longer exists. * refactor(sync): move Force overwrite + Restore into the Advanced collapsible The kebab menu introduced earlier hid Force overwrite and Restore from history behind a small icon and required users to know that "more_vert" in the dialog title contained sync actions. Fold them into the existing top-level collapsible instead — the same one that already hosts sync interval, manual-only, and compression toggles. - Rename the collapsible's label from "Advanced Config" to "Advanced" via a new sync-specific translation key (the global ADVANCED_CFG key is shared with issue providers, so we don't touch it). - The dialog component appends Force overwrite (warn) and Restore from history (SuperSync only) to the collapsible's fieldGroup in edit mode. First-time setup keeps the original SuperSync hide so the collapsible never appears empty. Re-authenticate stays as an inline button below the form because its visibility depends on an async provider.isReady() check that doesn't fit Formly's sync hideExpression. Refs #7362 * refactor(sync): single Advanced collapsible per provider, all actions inside Each provider now has exactly one "Advanced" collapsible: - non-SuperSync: top-level (interval, manual-only, compression, enable-encryption, plus injected Re-authenticate / Force overwrite in edit mode) - SuperSync: nested in the SuperSync section (server URL, plus injected Force overwrite / Restore from history in edit mode) The inner SuperSync collapsible's label switches from "Advanced Config" to "Advanced" so both surfaces read the same. Re-authenticate moves from a button below the form into the non-SuperSync Advanced collapsible as a Formly button gated on `syncProvider === Dropbox`. Drops the async provider.isReady() check — re-auth is shown for Dropbox in edit mode regardless, since `force=true` works for both stale-token and switching accounts. Honors the rule that no buttons sit below the Advanced collapsible inside the dialog. Refs #7362 * refactor(sync): use stroked style for all dialog buttons + add Nextcloud test - Every action button inside the sync dialog now uses btnStyle: 'stroked' for a consistent outlined appearance: Re-authenticate, Force overwrite, Restore from history, WebDAV/Nextcloud Test connection, file-based and SuperSync Enable encryption, SuperSync Get token, LocalFile folder pickers. - Force overwrite keeps btnType: 'warn' so it stays warn-coloured but as an outline rather than a filled warn button. - The conditional stroked-when-token-set toggle on Get token is dropped in favour of always-stroked. - Re-authenticate and Restore previously misused btnType: 'stroked' (a color slot) instead of btnStyle: 'stroked' (the actual style flag) — the formly-btn template ignored the unrecognised color, so they rendered as filled buttons. Fixed. - Adds a Test Connection button to the Nextcloud section, matching the WebDAV one — Nextcloud's serverUrl + userName are translated into the WebDAV-shaped baseUrl client-side before invoking WebdavApi.testConnection. Refs #7362 * refactor(sync): apply multi-review fixes — fragility, races, dedup, lifecycle Cross-validated findings from the multi-agent review: **Correctness / lifecycle** - `fields` becomes a `computed` signal of `_getFields(isWasEnabled())`, removing the manual `fields.set()` re-sync after the enabled flag flips. Single source of truth. - Reset `_tmpUpdatedCfg._isInitialSetup = false` when the dialog opens in edit mode so SuperSync encryption-warning hideExpressions stop treating returning users as first-timers. - Replace ad-hoc `Subscription` aggregator with `takeUntilDestroyed` on both subscriptions in the dialog component. **Race conditions** - `syncSettingsForm$` subscription on the settings page now goes through `switchMap` so a fresh emission cancels any in-flight `provider.isReady()` probe — late callbacks can no longer overwrite newer state. - Drop the redundant `_cd.markForCheck()` after `signal.set()` — signals integrate with OnPush automatically. - Use `??` instead of `||` when preserving `globalCfg` flags during provider switch, so an explicit `false` is honoured. **De-duplication** - Promote `NextcloudProvider._buildNextcloudBaseUrl` to a public static `buildBaseUrl()`. Dialog's `_testNextcloudConnection` now imports it rather than duplicating the URL-building logic. - New `OAUTH_SYNC_PROVIDERS` set in `provider.const.ts`. Re-auth button's hideExpression and the settings-page `requiresAuth` derivation can both reference it instead of hard-coding `Dropbox`. **Simplicity** - Settings page `syncStatus` collapses from 5 fields to 3 (providerId, needsAuth, isEncrypted) — empty state derives from `providerId === null`. - Drop redundant `D_INITIAL_CFG.ADVANCED` translation key in favour of the existing `T.G.ADVANCED_CFG`. **Security / logging** - Re-auth catch path now logs `{ name: e.name }` instead of the raw error object — log history is exportable, no auth detail leakage. Refs #7362 * refactor(sync): apply pass-2 review fixes — error paths, marker, factory Cross-validated findings from the second multi-agent review: **Correctness — keep the syncStatus stream alive on probe failure** - Wrap the inner async block in try/catch. Previously, a rejected `provider.isReady()` would propagate as an observable error through switchMap, killing the outer subscription and freezing the status pill. On failure, default to `needsAuth: true` so the row stays meaningful. - Replace the imperative `subscribe + .set` bridge with `toSignal()`. **Security — finish the redaction** - The first pass only redacted SyncLog.err; the snack `translateParams` still passed raw `e.message` into a `[innerHtml]` sink. Snack copy now uses the same `_redactErrorName(e)` helper so neither surface carries token/URL/stack details. Helper handles `null`/`undefined` thrown values explicitly. **Architecture — structural marker decouples routing from i18n** - Both Advanced collapsibles now carry `props.syncRole: 'advanced'`. The dialog routes on this stable identifier instead of the shared `T.G.ADVANCED_CFG` translation key (also used by Jira/Azure DevOps forms — a global rename would have silently broken sync). **Simplicity — collapse 5 button factories into one** - New `_actionBtn({ text, onClick, btnType?, hideExpression?, className? })` helper. Each per-action factory becomes a 3-4 line call site. ~60 lines net reduction in the dialog component. - Drop the orphan `props: { dropboxAuth: true }` marker on the Dropbox fieldGroup — its consumer (`_buildSyncFormConfig`) was deleted. Refs #7362 * refactor(sync): apply pass-3 review fixes — auth label asymmetry, dead snack param Pass-3 review surfaced two real findings on top of polish: **Correctness — non-OAuth providers no longer mislabelled "needs auth"** - The pass-2 try/catch returned `needsAuth: true` unconditionally on probe failure, which would surface "Authentication required" for WebDAV/Nextcloud/LocalFile — providers that don't have an auth helper. Now we capture `requiresAuth` BEFORE the throwable `isReady()` call and reuse it in the catch, so non-OAuth providers fall through to `needsAuth: false` even on transient failures. **Simplicity — drop the dead snack translateParams** - The `INCOMPLETE_CFG` translation key has no `{{error}}` placeholder, so the redacted error name was silently dropped by the translation pipe. Removing the param documents the actual contract: the snack shows static credentials-missing copy; the discriminator goes only to the (redacted) log. **Polish — cleaner types, tighter helper** - `_redactErrorName` collapses to two-arm helper: `Error.name` for Error instances, `'UnknownError'` for everything else. The null/undefined/typeof branches were exporting internal jargon to the user. - The `props.syncRole` marker now uses a typed `SyncCollapsibleProps extends FormlyFieldProps` interface (exported from sync-form.const) instead of a `Record<string, unknown>` cast. Eliminates the cast at both write and read sites. Refs #7362 * refactor(sync): polish round — shareReplay, subscription cleanup, doc tightening Three deferred items from prior reviews now applied: - shareReplay({bufferSize:1, refCount:true}) on syncSettingsForm$. The config-page subscribes long-term and the dialog re-subscribes per open; previously the no-provider branch re-fetched the sync-config-default-override.json asset on every fresh subscription. Cold-start cost is now amortised across both consumers. - Drop the manual Subscription aggregator on ConfigPageComponent. The remaining cfg$ + queryParams subscriptions now use takeUntilDestroyed(this._destroyRef); ngOnDestroy + OnDestroy go away. - Tighten dialog _isInitialSetup handling. Pass-3 review noted the pre-set + Object.assign pattern reads as if order matters when it doesn't. Move the flag into the spread payload so the intent is obvious at the call site, and use \!v.isEnabled directly so the semantics are explicit (true ⇔ first-time setup). - JSDoc on forceOverwrite() documenting why no Material confirm is added — SyncWrapperService.forceUpload already gates the action with a native confirmDialog, and that gate also serves the 5 internal snackbar callers (LockPresentError / EmptyRemoteBody / JsonParse / LegacySyncFormatDetected / retry). Wrapping here would double-confirm. Refs #7362 * refactor(sync): pass-4 polish — refCount:false, widen updateTmpCfg, trim docs Cross-validated findings from the fourth review pass: - shareReplay flips to refCount:false. Pass-4 reviewer noted that refCount:true only deduplicated the rare case where the dialog opens *while the settings page is mounted*. The far more common path — header-icon → dialog with no settings page open — saw the dialog become the sole subscriber, complete via .pipe(first()), and drop refCount to 0, so the next open re-fetched the JSON default-override anyway. With refCount:false the cached value is retained for the lifetime of SyncConfigService, matching the comment's stated intent at negligible memory cost. - updateTmpCfg signature widens to `SyncConfig & { _isInitialSetup?: boolean }`. Drops the call-site cast (a code smell that pretended the type was wider than it was) and documents that _isInitialSetup is a real, expected payload field. - Trim the misleading comment around `_isInitialSetup: \!v.isEnabled`. The "Spread last so Object.assign honours this" line wasn't true — it's an object literal property, not a spread. New comment states the actual semantics: first-time setup ⇔ sync was previously disabled. - Trim forceOverwrite() JSDoc from 6 lines to 2 — same content, matches the project's preference for terse method comments. Refs #7362 |
||
|
|
654f4d80ee |
fix(sync): break iOS WebDAV conflict-dialog loop (#7339)
Two compounding bugs trapped iOS WebDAV users in a per-minute conflict dialog that no button could resolve: 1. FileBasedSyncAdapter's snapshotReplacement heuristic re-fires gapDetected on every sync from a non-writing client (clientId \!= excludeClient is true forever), so the download keeps coming back with snapshotState and OperationLogSyncService keeps throwing LocalDataConflictError despite the local clock already dominating the remote snapshot. Skip hydration and conflict when compareVectorClocks(local, remote) is EQUAL or GREATER_THAN, gated on both clocks being non-empty so a fresh client still hydrates a legacy/clockless snapshot. 2. SyncWrapperService._openConflictDialog$ filtered undefined out of the afterClosed() stream, so a programmatic close (iOS WebView lifecycle, re-entry) collapsed the observable and firstValueFrom threw EmptyError — the user's Use Local/Use Remote/Cancel click never reached the resolution branches. Drop the filter so undefined flows through to the existing cancellation path. The dominate-skip deliberately does NOT append result.newOps to the op log: VectorClockService.getEntityFrontier is last-write-wins by seq, and writing historical remote ops at the current tail would regress per-entity frontiers and let future LWW resolution overwrite local data. Trade-off documented inline. Adds an adapter-level integration reproducer that asserts gapDetected re-fires forever for a non-writing client (the upstream loop trigger), a 3-client WebDAV e2e that reproduces the loop end-to-end against a real provider, plus service-level tests for the dominate-skip, the empty-clock guard, the concurrent-clock conservative path, and consecutive-sync loop prevention. |