* feat(sync): integrate onedrive with pkce and operation log sync
* fix(sync): add oauth state validation and token refresh concurrency control
* fix(sync): refactor onedrive oauth cleanup and reduce download API calls
- Replace setInterval-based OAuth state pruning with on-demand
_pruneExpiredOAuthStates() to avoid background timer overhead
- Optimize downloadFile to read ETag from content response headers
first, falling back to a metadata request only when needed (reduces
API calls from 2 to 1 in the common case)
- Narrow OneDrive class interface from SyncProviderServiceInterface to
FileSyncProvider for stronger type guarantees
- Accept optional devPath constructor parameter for non-production
folder namespacing
- Extract isOneDriveClientIdRequired to a helper function in
sync-form.const.ts for readability
- Change default OneDrive sync folder name to 'Super Productivity'
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 review feedback for OneDrive sync
Critical fixes:
- Detect invalid_grant in token refresh responses and clear credentials
- Reset _tokenRefreshInFlightPromise in clearAuthCredentials to prevent
stale refresh results from overwriting cleared state
- Re-read config before persisting refreshed tokens to avoid race with
concurrent clearAuthCredentials calls
Important fixes:
- Extract OAuth state management to shared oauth-state.util.ts to fix
circular dependency between OneDrive provider and callback handler
- Add 401 retry pattern (_is401Retry) matching Dropbox's approach:
on 401, proactively refresh token and retry, only clear credentials
if retry fails
- Remove unused auth status translation keys (AUTH_SUCCESS,
BTN_AUTHENTICATE, BTN_REAUTHENTICATE, REAUTH_SUCCESS,
STATUS_CONFIGURED, STATUS_NOT_CONFIGURED) and form props
- Remove unused requireAuth parameter from _cfgOrError
Minor fixes:
- Replace path-exposing log messages with structured logging
- Remove unused _formatHttpErrorDetails helper
- Return empty ETag fallback in getFileRev instead of throwing
- Add folder cache invalidation comment for path changes
- Add afterEach to restore global fetch in tests
- Add PKCE defence-in-depth comment in auth code dialog
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): use eslint-disable-next-line for @microsoft.graph.conflictBehavior
Replace spread+computed-property workaround with a plain property plus
an explicit eslint-disable-next-line comment. Clearer intent: the
naming violation is a Graph API requirement, not accidental.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): pre-save OneDrive form config in reauth() to prevent MissingCredentialsSPError
Re-auth was failing silently on Linux because getAuthHelper() reads
clientId from IndexedDB (privateCfg), but the form values were never
written before calling configuredAuthForSyncProviderIfNecessary().
- Extract shared BTN_REAUTHENTICATE and REAUTH_SUCCESS translation keys
- Add OneDrive to OAUTH_SYNC_PROVIDERS
- Pre-save form config in reauth() matching the existing save() flow
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): adapt TooManyRequestsAPIError to new constructor signature after rebase
* refactor(sync): migrate OneDrive provider to packages/sync-providers
Move OneDrive core logic into the shared sync-providers package,
matching the pattern used by Dropbox, WebDAV, and other providers.
The app-side code is now a thin adapter with a createOneDriveProvider()
factory function.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): re-throw MissingRefreshTokenAPIError from _requestOAuthToken catch block
The catch block in _requestOAuthToken was swallowing the
MissingRefreshTokenAPIError thrown after clearing credentials on
invalid_grant, causing it to fall through to a generic HttpNotOkAPIError
instead. Now re-throws MissingRefreshTokenAPIError explicitly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 second review critical and important items
- Replace _is401Retry instance field with per-call parameter to prevent
concurrent 401 races (Critical #1)
- Fix _requestOAuthToken catch block to re-throw MissingRefreshTokenAPIError
instead of swallowing it (Critical #2)
- Fix _parseOAuthCallback defaulting unknown provider to 'unknown' instead
of 'dropbox' to prevent misrouting (Critical #3 + Important #6)
- Change conflictBehavior from 'replace' to 'fail' to prevent data loss
if a file exists with the same name as the folder (Critical #4)
- Await in-flight token refresh promise in clearAuthCredentials before
clearing, to close the refresh-during-clear race (Important #5)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 review important and minor items
- Extract duplicate OneDrive pre-auth save block into
_persistOneDriveFormCfgBeforeAuth() method (Important #7)
- Add GET probe in _ensureSyncFolderExists to skip per-segment
creation when folder already exists (Important #9)
- Redact sensitive params (code, code_verifier, refresh_token,
access_token) from token endpoint and Graph error bodies
(Important #11)
- Remove targetPath from _mapAndThrow error messages to prevent
logging user content (Important #12)
- Add OAuth state validation in manual paste flow for OneDrive
(Important #13)
- Fix unknown provider routing in _parseOAuthCallback (Important #14)
- Guard Electron IPC listener against post-destroy emissions
(Important #15)
- Remove dead authStatus Formly field from sync-form.const.ts (Minor)
- Simplify Headers builder in _ensureSyncFolderExists (Minor)
- Fix spec afterEach to restore original fetch instead of undefined
(Minor)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): fix clearAuthCredentials race and add unit tests for OneDrive
Fix a race condition in clearAuthCredentials where _tokenRefreshInFlightPromise
was nulled AFTER awaiting, causing the invalid_grant handler's clearAuthCredentials
to wait on itself. Now captures the promise and nulls the field before awaiting.
Add 5 new unit tests covering critical code paths:
- 401 token refresh and retry
- 401 retry failure with credential clearing
- 400 invalid_grant credential clearing
- 412 precondition failed mapping
- Concurrent token refresh deduplication
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 third review items
- Simplify clearAuthCredentials: null _tokenRefreshInFlightPromise without
awaiting to avoid deadlock when called from inside the refresh IIFE
- Add superproductivity:// scheme validation in Electron OAuth listener
- Fix invalid_grant test to assert MissingRefreshTokenAPIError
- Fix folder cache test to match GET-probe optimization
- Set setComplete default in beforeEach
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(sync): fix OAuthCallbackHandler spec to expect 'unknown' provider
The _parseOAuthCallback now returns 'unknown' instead of 'dropbox' for
URLs without an explicit provider path segment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 fourth review items
- Handle generic 401 from token endpoint (clear creds + MissingRefreshTokenAPIError)
- Remove user-supplied paths from error constructors (rule 9 compliance)
- Soften clearAuthCredentials comment to reflect actual TOCTOU guarantee
- Tighten Electron scheme gate to match native path prefix
- Add scheme info to Electron rejection log
- Fix folder-cache test to assert GET probe count
- Remove redundant per-test setComplete stub
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 fifth review items
- Scope invalid_grant/401 credential clearing to refresh_token grant only
(bad auth code exchange no longer wipes existing credentials)
- Redact OAuth code/state from Electron protocol handler logs
- Add @sp/sync-providers/onedrive TS path alias to tsconfig files
- Default undefined sync config fields instead of overwriting with undefined
- Handle OneDrive in provider-switch branch (preserve encryptKey)
- Update sync-config test expectations to match default values
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 sixth review items
- Use If-None-Match: * for null-rev uploads to prevent concurrent overwrite
- Guard in-flight refresh against stale credentials (match refreshToken/clientId/tenantId)
- Gate OneDrive provider option behind Electron/Native (web CORS unsupported)
- Don't clear credentials on transient token endpoint errors (429/5xx)
- Preserve null syncProvider instead of defaulting to WebDAV
- Hydrate full OneDrive privateCfg on provider switch
- Remove duplicate syncInterval/isManualSyncOnly Formly controls
- Revert non-English locale changes (zh.json, zh-tw.json)
- Redact URL fragments (#) in Electron protocol handler logs
- listFiles rethrows non-404 errors instead of swallowing all
- Update 401 test expectations for new rethrow behavior
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): guard credential clearing against stale concurrent refresh
Prevent a stale in-flight refresh from wiping newer credentials after
a concurrent re-auth. The refresh IIFE now throws a plain Error (not
MissingRefreshTokenAPIError) when it detects config drift, and all
clearAuthCredentials() call sites now verify the stored config still
matches before clearing. Also fix listFiles() to handle 404 from
_requestJson (which throws HttpNotOkAPIError, not RemoteFileNotFoundAPIError).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 eighth review items
- Replace If-None-Match:* with @microsoft.graph.conflictBehavior=fail query param for upload create-only semantics
- Add identity change detection in _persistOneDriveFormCfgBeforeAuth to clear tokens when useCustomApp/clientId/tenantId change
- Extract _clearIfConfigMatches helper to guard credential clearing against stale concurrent refresh
- Restrict folder probe fallthrough to 404 only, propagate other errors
- Restore locale files from upstream master
- Centralize IS_ONEDRIVE_SUPPORTED in onedrive-auth-mode.const.ts, use in factory and form config
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): address PR #7523 ninth review items
- Paginate listFiles via @odata.nextLink to avoid silent data loss
- Fix logger misuse (log -> normal with proper string message)
- Replace as any with OneDrivePrivateCfg in dialog-sync-cfg
- Throw NoRevAPIError when uploadFile response lacks eTag
- Omit undefined optional booleans from global config to prevent overwrite
- Move OneDrive dynamic import inside IS_ONEDRIVE_SUPPORTED gate
- Require state param for OneDrive full-URL paste (CSRF)
- Add id_token to sensitive keys for body redaction
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(sync): add OneDrive type alias to electron tsconfig paths
The @sp/sync-providers/onedrive path alias was missing from
electron/tsconfig.electron.json, causing the Electron build to fail
with "Cannot find module '@sp/sync-providers/onedrive'". The alias was
already present in tsconfig.base.json but electron uses its own paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(plugins/github): correct issue-number prefix on imported tasks
The title field mapping mishandled issue imports two ways:
- Search/backlog results carry no `number`, so ctx.issueNumber was
undefined and titles got a literal "#undefined " prefix. Fall back
to ctx.issueId (equals String(issue.number) for GitHub).
- mapSearchResult already display-prefixes the title with "#<n> ", so
re-applying it yielded "#<n> #<n> ...". Short syntax then treated the
non-leading "#<n>" as a tag and stripped it. toTaskValue is now
idempotent so the prefix lands exactly once.
* fix(tasks): ignore numeric #tags in issue task titles
An issue task's leading "#<number>" is the issue id, and issue titles
routinely reference other issues ("fixes #1234"). Short syntax parsed
those non-leading "#<n>" tokens as tags, prompted to create them, and
stripped them from the title. Extend the existing numeric-at-start
guard to also block numeric "#<n>" anywhere on issue tasks. Non-numeric
tags ("#mytag") still parse, so manual tagging of issue tasks via the
title keeps working.
The context menu's toggle-done button always showed "Mark as completed"
regardless of the task's done state, because both branches of the ternary
on line 151 of task-context-menu-inner.component.html referenced MARK_DONE.
The matching icon already conditionally rendered undo vs check based on
isDone, so only the label was misleading.
Add a MARK_UNDONE i18n key ("Mark as not completed") and use it on the
isDone branch, mirroring the existing icon switch.
Closes#7785
The mobile bottom sheet was opened without an explicit autoFocus, so
CDK's FocusTrap fell back to 'first-tabbable'. The intended close
button in the sheet header is display:none, so focus landed on the
tag-edit input inside the task detail panel. On touch, the bound
mat-autocomplete then opened immediately, covering the rest of the
task whenever it was opened in portrait.
Disable auto-focus on the bottom sheet (matches what task-detail-panel
already does for its own dialog opens).
Fixes#7769
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
The Capacitor 8 migration (334b14aa2) dropped `adjustMarginsForEdgeToEdge:
'auto'` from `capacitor.config.ts`. On targetSdk 36 (Android 16) edge-to-edge
is mandatory and the WebView no longer applies IME insets automatically —
position: fixed elements (the global add-task-bar in particular) end up
anchored to a layout viewport that doesn't shrink when the keyboard opens,
so they appear to scroll with content swipes and jump inconsistently during
the IME show/hide animation.
Pull in `@capawesome/capacitor-android-edge-to-edge-support` as the
structural replacement: it registers a single `ViewCompat.setOnApplyWindow
InsetsListener` that applies system-bar + display-cutout insets to the
WebView and defers IME to `windowSoftInputMode="adjustResize"` (skipping its
own bottom margin while the keyboard is visible to avoid double-counting).
Combined with `SystemBars.insetsHandling: 'disable'` (required by the plugin
to prevent Capacitor's core insets handler from fighting it) and
`Keyboard.resizeOnFullScreen: false` (the plugin's recommended setting; a
no-op on iOS where this key doesn't apply, and the Keyboard plugin is
excluded from Android via `includePlugins` anyway), this restores the
pre-migration WebView sizing behaviour.
No JS changes — the plugin self-installs once present.
The checklist toggle in the inline-markdown editor unconditionally replaced
the visible content with '- [ ] ' whenever isDefaultText was true. But
isDefaultText is wired from !task.notes (the SAVED value), while the visible
content is task.notes || defaultTaskNotes() — so when notes are empty but a
default note template is configured under Settings → Tasks, the visible
template was silently deleted on every toggle.
Narrow the early-return branch to also require currentText to be empty.
This keeps the existing empty-editor convenience (a fresh single checkbox
when there's truly nothing to preserve) while letting the regular insertion
path run when default-template content is on screen.
Add a regression test that simulates the default-template scenario.
Closes#7786
The app uses only local notifications — no @capacitor/push-notifications,
no FCM, no registerForRemoteNotifications anywhere. Declaring
remote-notification in UIBackgroundModes claims a push capability the
app never uses, which Apple may flag at App Store review.
A first-launch permission prompt can sit on screen for several seconds.
The pre-existing scheduleReminder() flow captured triggerAt before
awaiting ensurePermissions(), so for reminders due within ~3s the trigger
could age into the past by the time LocalNotifications.schedule() ran —
iOS rejects past timestamps and the very first reminder silently fails.
Re-clamp to Date.now() + 1000 after the permission await.
The iOS permission dialog is already triggered ~2s after launch by
MobileNotificationEffects.askPermissionsIfNotGiven$, which calls
ensurePermissions() on every native platform. The eager call I added to
CapacitorReminderService.initialize() was a duplicate of that existing
flow, not a contextual permission prompt — remove it to keep init
focused on registering action types (which still must happen at startup
so notifications with actionTypeId can resolve their category).
User-visible behavior is unchanged. Also adds initialize() spec coverage
that was missing entirely.
The service-worker branch in NotifyService.notify() ran before the
native branch. On iOS WKWebView under capacitor://, navigator.serviceWorker
is defined but Notification.requestPermission() never prompts, so the SW
path silently swallowed every notification and LocalNotifications.requestPermissions()
was never reached — the iOS permission dialog never appeared. Check
isNative first, and also call ensurePermissions() during reminder
service init so the prompt fires at app launch.
selectProjectById calls devError when an entity is missing, which in
non-production builds fires window.alert("devERR: …") followed by
window.confirm("Throw an error for error? …"). Both are page-blocking.
This was load-bearing for an assumption that "missing project" is always
a programmer error. It isn't: after project-task-page started subscribing
to currentProject$ via toSignal (34af7b103), a SYNC_IMPORT_REMOTE or
remote project delete on the route the user is viewing re-evaluates the
selector with an id that has just legitimately disappeared. On a dev
build that hangs the whole browser; on prod it falls back to Log.err
anyway, so the asymmetry is pure footgun.
Downgrade the missing-project branch to Log.err, matching the existing
pattern in work-context.selectors.ts:189 for the same case. The
missing-id branch above stays on devError — calling the selector with
no id genuinely is a bug.
devError() in non-production builds opens window.alert("devERR: …") then
window.confirm("Throw an error for error? ––– …"). Both are page-blocking,
so any Playwright call (goto/click/waitFor) hangs until the dialog is
handled. After the project-task-page signal subscription added in
34af7b103, selectProjectById fires devError whenever sync removes the
project the user is currently viewing — which masters' webdav delete-
cascade and supersync legacy-migration tests trigger by design.
Add installDevErrorDialogHandler that matches the exact two devError
shapes and dismisses them, then wire it into the four E2E page-creation
sites: the default test fixture, setupSyncClient, createSimulatedClient,
and createLegacyMigratedClient. Production code is untouched.
Also carve out the devError confirm in setupSyncClient's strict confirm
validator so it doesn't raise an unhandled rejection alongside our
dismiss, and drop the unused createLegacyMigratedClientNoDialogHandler
wrapper (it now lies — the underlying helper does install a handler).
The reload-then-verify pattern races with op-log persistence: after Save,
8 ops are captured (addTask, addRepeatCfg, updateRepeatCfg, 4x updateTask,
planTaskForDay), but page.reload() fires before the concatMap'd persist
queue drains. On CI only 3 of 8 ops reached IndexedDB, dropping the
updateTask that sets dueDay to the future start so the task stayed in
Today after hydration. Assert the bug behavior in-session first — that
gives the queue up to 10s to drain — then reload and re-assert.
Corrupted IndexedDB data with undefined boardCfgs or panels arrays
crashed the boards reducer during hydration. The throw surfaced via
RxJS reportUnhandledError, bypassing Angular's ErrorHandler — so the
app appeared unresponsive with no alert. Normalize the loaded state
before the existing sanitize/dedupe/fix chain so the load path can
never throw on shape.
* feat(plugins): keyed persistence API for per-context LWW (Stage A Phase 1+3)
Add an optional `key` argument to `persistDataSynced` / `loadSyncedData`,
composed at the bridge transport boundary into `pluginId:key` entity ids.
Distinct keys now produce distinct ops that LWW-resolve per-entity,
enabling document-mode-style plugins to avoid cross-context blob
overwrites without changing existing keyless callers.
Phase 3: `removePluginUserData(pluginId)` now sweeps the full prefix
(legacy entry + every keyed entry), dispatching one delete per match
with the rule-6 setTimeout(0) trailer so remote replicas don't keep
keyed entries after uninstall. The reducer-only "smart prefix match"
shortcut is wrong (one op for the prefix only, remote keyed entries
leak) — see docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md
Phase 3.
Phase 4 (document-mode plugin-side migration of the legacy single-blob
entry) is left as a separate follow-up so the host change can be
reviewed in isolation.
Issue #7749
* feat(document-mode): migrate to keyed persistence (Stage A Phase 4)
Move from one synced blob under the bare plugin id to per-entity keyed
entries:
- meta — { enabledCtxIds: string[] }, owned by background.ts
- doc:${ctxId} — one entry per context, owned by the editor iframe
- __meta__ — migration stamp
Each entry has its own LWW timestamp on the host, so a concurrent edit
in project A on Device 1 and project B on Device 2 no longer
whole-blob-collide.
The migration runs idempotently from both background.ts and editor.ts
(stamp-guarded), splits the legacy single blob into keyed entries, then
tombstones the legacy entry with an empty payload — giving LWW a
winning side against any offline device that still writes the old
shape.
flushSave / flushSaveSync no longer need to read+merge sibling state,
since each context's entry stands alone. The future-version blob guard
(isStorageUnreadable) is dropped — it referenced the wrapping blob's
version, which no longer exists; per-doc corruption still falls back
via isDocCorrupt.
Issue #7749
* fix(plugins): tighten Stage A keyspace at the boundaries
Multi-review surfaced three small gaps in the keyed-persistence rollout:
- The synchronous composeId throw covers the bridge's iframe and
direct-API entry points, but three in-process callers
(plugin-config.service, plugin.service, plugin-config-dialog) bypass
the bridge and route directly into the persistence service. A
user-installed plugin with `id: "evil:plugin"` passed manifest
validation and would have collided with the legitimate `evil`
plugin's keyed namespace — `removePluginUserData('evil')` would have
over-matched the sweep. Reject the colon at install time in
`validatePluginManifest`; keep the bridge throw as defense-in-depth.
- The new `key` arg at the bridge was typia-asserted on `data` but
unchecked itself. A compromised iframe could pass a multi-megabyte
string or a non-string value via postMessage. `data` is capped at
1 MB, but the entity id composed from `key` would be stored verbatim
in NgRx state, IndexedDB, the op-log, and on the sync wire — bypassing
the data cap. Add `assertPluginPersistenceKey` with a 256-char cap.
- `_loadPersistedData` silently returned `null` when composeId threw,
while `_persistDataSynced` rethrew. The asymmetry made a malformed
pluginId look like "no data yet" on the load side, indistinguishable
from a fresh install. Hoist composeId + key validation out of the
load try/catch so it throws symmetrically.
Issue #7749
* fix(plugins): lower per-write cap to 256 KB
The pre-Stage-A 1 MB cap was sized for the old single-blob shape, where
one entry held every context's data. With the keyed split, each entity
gets its own write budget — 1 MB per write is wildly over-provisioned
for the realistic upper bound of plugin payloads (heavy document-mode
docs ~30–100 KB, configs and automations KB-scale).
256 KB keeps 2–5× headroom over realistic payloads while bounding the
per-plugin storage growth more tightly.
Document-mode's migration loop now skips oversized legacy docs instead
of aborting the whole run: a user whose legacy blob holds one ~500 KB
doc (legal under the old cap) keeps the other contexts migrated and
the original bytes preserved in the legacy entry. The success stamp
stays at migrated:0 in that case so a future build (or pruning of the
doc) can complete the migration without data loss.
Issue #7749
* test(plugins): e2e migration of legacy single-blob to keyed entries
The migration logic in document-mode is unit-tested against a mock
PluginAPI, which can't catch real-iframe quirks (postMessage handling
of undefined second args, commit-chain timing under the host's
per-entity rate limiter, hydration ordering against the op-log). Add
two end-to-end scenarios:
- Fresh install: enable the plugin, verify the __meta__ stamp lands
at migrated:1 (the migration's final write — observing it implies
every earlier step completed).
- Legacy blob: seed a pre-Stage-A single-blob entry via the e2e helper
store, enable the plugin, verify the legacy entry is tombstoned,
meta carries the enabledCtxIds, and each doc landed under its own
doc:${ctxId} key.
Issue #7749
* chore(plugins): drop dead code and review-driven polish
Four small follow-ups from the multi-review pass:
- Don't log the plugin-supplied `key` value. Plugins may use user
content (search queries, doc titles) as keys; the log history is
exportable. Log `keyLen` instead, per CLAUDE.md rule 9.
- Delete `detectStaleLegacyWrite` and its 3 specs. Exported and
fully tested, but zero non-test callers — banner UI is forbidden
by project convention for transient-only messaging. If the need
resurfaces, the implementation is four lines.
- Drop the `attemptedAt` field from `MigrationStamp`. It was written
but never read; the success stamp is the only re-entry gate, and
the resume path is just "re-run the loop" — re-writes are content-
idempotent. Saves one rate-limited write per fresh migration.
- Update `docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md`
with an implementation-status table referencing the shipping
commits, so future readers don't have to dig through git.
Issue #7749
* fix(tasks): allow parent and sub-tasks to share tags (#7756)
Drop the parent/sub-task tag exclusivity constraint in the CRUD
meta-reducer. The constraint forcibly stripped a tag from the parent
whenever a sub-task gained the same tag (and vice versa), which surfaced
as data loss in flows that tag both -- notably the Automations plugin
adding tags on taskCreated.
Tag membership is already derived from task.tagIds (tag.taskIds is only
an ordering hint), and the tag-view selector at tag.reducer.ts:121-127
already filters sub-tasks whose parent has the same tag. The constraint
was a hygiene-driven judgment call from c19b3dbb0e with no documented
rationale and no consumer that relied on the invariant.
Also fix the markdown export in work-context-markdown.service: with
parent+sub-task coexistence now possible, drop sub-tasks whose parent
is also in the requested ids so they aren't rendered twice (once nested
under the parent, once as a top-level entry).
* fix(tasks): show sub-task own tags in worklog and search (#7762)
Follow-up to #7756. Two pre-existing code paths assumed "sub-tasks
don't have tags" and substituted the parent's tags unconditionally:
- Worklog export (worklog-export.util.ts:207) — sub-task rows reported
the parent's tags even when the sub-task carried its own.
- Search results (search-page.component.ts:108) — the context chip on
a sub-task always showed the parent's first tag.
Both now prefer the sub-task's own tags when present and fall back to
the parent's only when the sub-task has none, so the no-tags-on-sub-task
case keeps the historical UX. Updated the misleading "by design" comments
in both files. Added regression tests for the new policy in both specs.
* refactor(tasks): extract resolveDisplayTagIds; fix worklog tag filter
Multi-review of the #7756/#7762 work surfaced two issues:
1. Worklog's _filterIdsForTag in get-complete-state-for-work-context.util.ts
still used "parent wins" for sub-task tag membership, so a sub-task
tagged independently of its parent was excluded from worklog views
before reaching the display-side fix in #7762. Now mirrors the same
"own tags win, parent as fallback" policy. Regression spec added.
2. With three call sites converging on the same policy (worklog filter,
worklog export, search results), extract resolveDisplayTagIds as a
tiny named helper with its own spec. The helper carries a JSDoc
warning that it is for display only — sync/op-log paths must read
task.tagIds verbatim. The previous inline expressions diverged in
shape; collapsing them removes the risk of future drift.
Also tighten the markdown-service dedup to filter against the loaded
task set rather than the requested ids, so a stale parent id in
tag.taskIds (ordering hint) doesn't suppress a real sub-task entry.
* chore(plugins): re-bundle document-mode and document Stage A path
Reverts the unbundling from b0cae69ffe. Stage 0 (gzip + throttle, shipped
in 84625be849) handles size; document-mode remains opt-in per context, so
cross-context conflict risk is bounded. Stage A (keyed plugin-persistence
API, issue #7749) is the documented future path for closing the LWW gap
on different-context concurrent edits — picked up when conflicts are
observed in practice.
Design sketch with multi-reviewed phasing lives in
docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md. Predecessor
plan's "Future work" section now links to it.
* test(plugins): cover document-mode bundled load and PLUGIN_USER_DATA LWW
Follow-ups from the multi-review of 199e816479's re-bundling decision:
- E2E smoke test asserts document-mode appears in plugin management so
a typo in BUNDLED_PLUGIN_PATHS fails loudly.
- Spec exercises PLUGIN_USER_DATA conflict resolution end-to-end, which
previously relied on analogy to REMINDER (same array+null branch) but
was never directly asserted after the migration off 'virtual'.
- Stage A plan risks: stale-editor-view gap surfaced by the review;
PluginHooks.PERSISTED_DATA_UPDATE already exists in the API but is
never dispatched host-side — wiring it is the path to a fix.
- background.ts: comment marks the known gap at the registerHook site.
* docs(plugins): plan for wiring PERSISTED_DATA_CHANGED hook
Designs the host-side wiring for the currently-dead
PluginHooks.PERSISTED_DATA_CHANGED so plugins can react to remote-driven
changes to their persisted data. Multi-reviewed twice; v4 trims scope to
host-only (no plugin adoption in this design) and preserves the
multi-review insights as seeds for the follow-up doc-mode adoption
tracked at issue #7752.
Implementation lands in a separate PR.
* fix(ci): pin wiki-sync second checkout to actions/checkout@v5
v6 stores credentials in a $RUNNER_TEMP file referenced via
includeIf.gitdir matchers; the wiki push intermittently runs without
credentials on github-hosted runners (run 26335467138 failed with
"could not read Username for 'https://github.com'"). v5 uses the
path-independent extraheader, which is stable for this second
checkout pattern.
Leaves the primary Checkout Code on v6.0.2 so Dependabot keeps it
current.
Refs: actions/checkout#2321, #2359
* test: harden e2e failure signals
Fail otherwise-passing E2E tests on browser runtime errors, keep Playwright retries disabled, preserve Docker E2E exit codes, and make plugin/WebDAV setup failures hard failures instead of logged or skipped conditions.
* test: harden provider e2e runners
Make WebDAV and SuperSync runner scripts require provider readiness, preserve cleanup and argument forwarding, and fail manual sync clients on uncaught page errors.
* ci: require providers for scheduled e2e
Set required-provider flags in scheduled WebDAV and SuperSync jobs, and remove the duplicate provider runner scripts while keeping local npm aliases inline.
* test: catch e2e teardown pageerrors and tighten fixture
- closeClient now asserts runtime errors AFTER context.close() so
pageerrors emitted during teardown (Angular destroy hooks, late RxJS
errors) are captured instead of dropped. Matches the pattern in
guardContextCloseWithRuntimeErrorCheck.
- test.fixture.ts isolatedContext now spreads Playwright's merged
contextOptions instead of destructuring 23 fields by hand. Future
option additions propagate automatically; the page fixture uses the
shared attachPageErrorCollector and only fails on pageerror (not
console.error, which is too noisy). Guards against a configured 0
timeout being treated as undefined.
- plugin-loading.spec.ts second test now hard-asserts that the plugin
menu entry reappears after re-enable, matching the first test instead
of silently logging when not visible.
* test(sync): stabilize ImmediateUploadService spec
Two complementary fixes for flaky failures observed under full-suite
random-order runs where the upload pipeline silently never fires:
- Pin navigator.onLine = true in beforeEach (restored in afterEach).
isOnline() inside _canUpload reads navigator.onLine directly. The
keyboard-layout spec replaces the whole navigator and the is-online
spec spies on it; if order or restoration ever drifts, every "should
fire upload" test fails trivially while the "should NOT" tests pass.
- Replace tick(2100) with tick(2000); flush(). The await chain inside
withSession() (provider.isReady, withSession entry, uploadPendingOps,
optional LWW re-upload) requires more microtask drain than tick's
fixed-time window reliably provides under load. flush() drains the
pipeline regardless.
* test(e2e): guard skipOnboarding init script against data: frames
The new page-error collector started failing plugin specs because
addInitScript runs in every frame — including the empty data:text/html
iframe that plugin-index swaps in on destroy — and localStorage access
in a data: URL throws SecurityError. Wrap the four setItem calls in
try/catch so the helper noops in storage-less frames.
* chore(plugins): re-bundle document-mode and document Stage A path
Reverts the unbundling from b0cae69ffe. Stage 0 (gzip + throttle, shipped
in 84625be849) handles size; document-mode remains opt-in per context, so
cross-context conflict risk is bounded. Stage A (keyed plugin-persistence
API, issue #7749) is the documented future path for closing the LWW gap
on different-context concurrent edits — picked up when conflicts are
observed in practice.
Design sketch with multi-reviewed phasing lives in
docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md. Predecessor
plan's "Future work" section now links to it.
* test(plugins): cover document-mode bundled load and PLUGIN_USER_DATA LWW
Follow-ups from the multi-review of 199e816479's re-bundling decision:
- E2E smoke test asserts document-mode appears in plugin management so
a typo in BUNDLED_PLUGIN_PATHS fails loudly.
- Spec exercises PLUGIN_USER_DATA conflict resolution end-to-end, which
previously relied on analogy to REMINDER (same array+null branch) but
was never directly asserted after the migration off 'virtual'.
- Stage A plan risks: stale-editor-view gap surfaced by the review;
PluginHooks.PERSISTED_DATA_UPDATE already exists in the API but is
never dispatched host-side — wiring it is the path to a fix.
- background.ts: comment marks the known gap at the registerHook site.
* docs(plugins): plan for wiring PERSISTED_DATA_CHANGED hook
Designs the host-side wiring for the currently-dead
PluginHooks.PERSISTED_DATA_CHANGED so plugins can react to remote-driven
changes to their persisted data. Multi-reviewed twice; v4 trims scope to
host-only (no plugin adoption in this design) and preserves the
multi-review insights as seeds for the follow-up doc-mode adoption
tracked at issue #7752.
Implementation lands in a separate PR.
* test: strengthen unit test assertions and revive disabled plugin specs
Replace tautological assertions, setTimeout-without-expect patterns, and
placeholder `expect(true).toBe(true)` tests with real assertions across
~30 spec files. Revive 5 plugin spec files that were fully commented out
on master with live tests covering core behavior.
Production-side: extract pure helpers for testability:
- app.component: getBackgroundOverlayOpacity, getBackgroundImageBlur
- android-sync-bridge.effects: getSuperSyncCredentialBridgeCommand
- super-sync-server: export escapeHtml, SERVER_HELMET_CONFIG
Delete the always-skipped xdescribe placeholder
src/app/imex/sync/sync-fixes.spec.ts (412 LOC).
Rename operation-log-stress.spec.ts to .benchmark.ts to match its
header comment ("excluded from regular test runs").
No production behavior changes; no master commits reverted.
* feat/got-two-permanently-rejected-ops-can-4e88bc:
fix(sync): accept encrypted retries as duplicate, not invalid op id
test(tasks): reset MockStore selectors in task-ui.effects spec
feat(plugin): improve sync data size for plugins
On Android the add-task-bar appeared at the wrong position when the
keyboard opened. Only iOS set the --keyboard-height CSS variable (via
the Capacitor Keyboard plugin, which is excluded from the Android build);
Android relied solely on windowSoftInputMode=adjustResize to shrink the
WebView, which is inconsistent across Chrome WebView versions and
transient animation states.
VisualViewport self-corrects regardless of whether adjustResize fired:
when it did, window.innerHeight - visualViewport.height is ~0 and
--keyboard-height stays 0; when it didn't, the delta equals the actual
obscured height and the bar moves up accordingly. A 100px threshold
filters out URL-bar and gesture-nav-bar noise.
Gate the listener on IS_TOUCH_ONLY && !isIOS() so it covers Capacitor
Android, the legacy F-Droid build, and Android mobile-web. iOS keeps
its existing Capacitor-plugin path; adding the same listener there
would race with that path on the same CSS variable.
Also bump the gap from the keyboard from --s (8px) to --s2 (16px) so
the bottom action buttons aren't flush against the IME and are easier
to tap.
Encrypt() generates a fresh random IV per call, so a retried upload of
the same logical op produces different ciphertext for the same plaintext.
The server compared encrypted payloads byte-for-byte in
isSameDuplicateOperation, mis-classifying legitimate retries as
INVALID_OP_ID instead of DUPLICATE_OPERATION. Clients only handle
DUPLICATE_OPERATION as a silent success; INVALID_OP_ID is marked as a
permanent rejection, breaking sync after a partial-success network
failure (server committed the op, client never saw the 200).
Skip payload byte-equality when both sides declare isPayloadEncrypted.
The remaining structural fields (clientId, vectorClock, clientTimestamp,
entityType/Id, actionType, schemaVersion, syncImportReason) still guard
against a UUIDv7 id collision with genuinely different content.
Other specs override selectProjectFeatureState and selectAllTasks via
MockStore.overrideSelector, which calls setResult() and persists across
spec files even after resetSelectors() in some orderings. The new
'selectOverdueTasks excludes tasks from archived projects' test relies
on the project-archive chain (selectProjectFeatureState → selectAllProjects
→ selectArchivedProjects → selectArrayOfArchivedProjectIds →
selectArchivedProjectIds and selectAllTasks → selectAllTasksInActiveProjects),
so a stale empty mockProjectState override made the filter return 0
archived ids and pass overdueInArchived through.
Extends the existing defensive clearResult()/release() block to cover the
chain.
provideMockStore({selectors}) calls setResult() on the module-level
selector, which persists across spec files until clearResult(). Without
a reset, selectProjectById stays pinned to the last value and pollutes
later specs (e.g. project.service unarchive tests saw a stale project
and never entered the isHiddenFromMenu branch).
- Move confirm dialog from ProjectService into work-context-menu, matching
the existing deleteProject/deleteTag pattern. archive() is back to a
thin dispatch + snack and no longer returns Promise<boolean>.
- Drop UNDO from the non-hidden unarchive snack; collapse the now-unused
_archiveNow helper. The hidden-from-menu branch keeps its "Show in menu"
action.
- Read project snapshot before dispatch in unarchive() via firstValueFrom,
dropping the take(1).subscribe() pattern and its WHY-comment.
- Trim three defensive selector specs to one (selectOverdueTasks).
- Drop project-task-page.component.spec.ts (it tested its own stub template).
- Re-namespace SHOW_IN_MENU → T.F.PROJECT.S.* and ARCHIVED_NOTICE →
T.F.PROJECT.ARCHIVED_NOTICE; trim the notice template to text + button.
Net: -230 lines / +92 lines.
- Confirm dialog for archive (replaces snack+UNDO); navigation in
work-context-menu now awaits confirmation.
- Archived-projects rows link to /project/<id>/tasks; project view
shows an inline restore notice when the active project is archived.
- Unarchive snack split: when the project is still hidden from the
menu, offer a "Show in menu" action; otherwise keep UNDO.
- Terminology: snack now reads "Project restored" to match the button.
- Tests: spec for archived-projects-page and project-task-page;
defensive archive-filter coverage on selectAllTasksWithReminder /
selectAllTasksWithDeadlineReminder / selectOverdueTasks; service
spec covers confirm/cancel paths and the hidden-from-menu branch.
* feat(layout): experimental vertical action strip on right edge
Teleports main-header's action-nav-right to document.body on desktop so
it escapes any ancestor containing-block, then pins it as a vertical strip
at the viewport's right edge via fixed positioning. Reserves the column
with padding-right on .app-container so the right-panel ends to its left.
* feat(layout): right-panel spans full viewport height
Restructure app shell so right-panel is a direct flex child of
.app-container (wrapping main-content via ng-content), letting its
.side extend over the full height of the viewport — including the
header area — rather than starting below the header.
* style(layout): give vertical action strip an elevated surface bg
* style(layout): match vertical action strip styling to left side nav
* fix(layout): stack play/focus buttons vertically in action strip
* style(layout): use solid surface bg for vertical action strip
* fix(layout): keep current-task title visible beside the action strip
In the teleported vertical action strip the current-task title pill was
positioned right:100% of the play button and clipped by the strip's
overflow, and the 48px mini-fab overflowed the 48px rail and got
side-clipped.
Render the title as a position:fixed flyout to the left of the strip
(the strip has no transform/filter so it is not a containing block for
fixed descendants), aligned to the play button across the web /
mac-titlebar / obsidian-header / RTL variants, and drop the play button
wrapper's horizontal margin so it fits the rail.
* fix(layout): stop tooltip overlay from blocking action strip clicks
A tooltip shown 'below' a button in the vertical action strip lands
directly over the next button. The tooltip's cdk-overlay-pane wrapper
(unlike the inner mat-tooltip-component) had no pointer-events:none, so
it intercepted clicks for real users and for Playwright actionability —
breaking ~20 focus-mode/break e2e tests.
* test(focus-mode): use teleport-robust focus-button locator
The action nav is teleported out of <main-header> into a body-level
strip, so 'main-header focus-button button' no longer matches. Drop the
main-header ancestor; 'focus-button button' is unique either way.
* fix(layout): polish vertical action strip (flyout, spacing, bg)
- Current-task title is now a hover-reveal flyout: hidden until the play
button (or the flyout itself) is hovered. Mobile (< 1080px) unchanged
(component still display:none's it there).
- Drop the position:fixed + magic top calc; keep the component's own
absolute + translateY(-50%) so the flyout is pixel-perfectly centred
on the play button regardless of theme/Material density.
- Strip now overflow: visible so the flyout can extend past the 48px
rail (overflow-x can't be visible while overflow-y is auto).
- Re-declare --header-nav-button-gap on the strip: it is scoped to
<main-header>'s :host but the strip is teleported to <body>, so the
var was undefined => every strip button had gap: 0 (cramped).
- Extra margin between the play button and the focus button.
- Strip background now matches the left sidenav (--sidenav-bg).
* fix(layout): uniform vertical spacing between strip buttons
Drop the extra play-button and counters-group bottom margins; one
--header-nav-button-gap (var(--s)) now drives the spacing between every
button in the strip. Measured: all consecutive gaps = 8px.
* feat(layout): make vertical action bar a configurable opt-in
Adds misc.isVerticalActionBar (default off). The right-edge vertical
action strip was previously an unconditional experiment; it is now an
opt-in toggle in Settings > Misc that switches the layout live without
reload:
- app.component: @if branches the DOM between classic (horizontal
header) and vertical (right-panel hoisted to full viewport height);
.app-container gets .has-vertical-action-bar to gate CSS.
- main-header: one-shot ngAfterViewInit teleport replaced with an
effect() that teleports/restores the action nav reactively to the
config flag and the desktop/mobile breakpoint.
- app/right-panel SCSS: strip padding and full-height right-panel are
now scoped to .has-vertical-action-bar; classic layout restored as
the default.
* fix(layout): even vertical-strip spacing for panel-button wrappers
plugin-header-btns, plugin-side-panel-btns, desktop-panel-buttons and
user-profile-button are zero-height wrapper custom-elements. As direct
flex children of the vertical action strip the column gap landed on the
collapsed wrapper instead of the button(s) inside, so their icons were
unevenly spaced next to real buttons like the add button. Make each
wrapper a centered column flex item that stacks its button(s) with the
same gap, and hide truly-empty wrappers so they don't reserve a phantom
gap slot. Verified: all 7 visible strip buttons now 8px apart.
* fix(right-panel): clip transient content overflow during slide animation
.side animates width 0<->* while .side-inner keeps its min-width, so
content briefly spills past the (intentionally overflow:visible) .side
during open/close. Add an isPanelAnimating host class driven by the
@slideRightPanel start/done callbacks and clip :host for that window
only (overflow: clip — no scroll container). Mirrors the existing
resizing/windowResizing transient-state pattern. Verified: class +
clip present only during the ~200ms animation, visible when idle.
* refactor(layout): drop dual DOM for vertical action bar
Single classic layout for both modes; strip is teleported and offset by
--bar-height so the horizontal header keeps owning the title-bar zone
(native drag region + WCO + Mac traffic lights). Drops the 40px
title-bar-collision padding and the right-panel full-height override.
* feat(layout): right-panel side spans full viewport height
Move the header into right-panel's projected .content slot and make
right-panel host full-height. The panel column now starts at viewport
top with the header band only spanning the .content width, similar to
Obsidian / Linear / VS Code.
* fix(layout): only offset vertical strip below WCO band on Win/Linux
Default top inset is now 0 (web, Mac hiddenInset, native-frame Electron
all let the strip start at viewport top — none of them have a window-
control overlay clashing with the right edge). Push the strip down by
--bar-height only on Win/Linux Electron with custom title bar, where
the WCO buttons would otherwise sit on top of the strip's first row.
* style(electron): use compact 32px WCO band on Win/Linux
Shrinks the Windows Controls Overlay height from 44px to 32px so the
native min/max/close buttons sit in a slimmer band — matches VS Code /
Edge slim title bar conventions and frees more of the header zone for
app content. Width stays OS-controlled (~138px); only height is
configurable.
* style(layout): tighten WCO band to 24px and pull strip up to match
Drops WCO_HEIGHT to 24px on Win/Linux and introduces a --wco-height CSS
var mirroring it. The vertical action strip now clears the WCO band by
exactly --wco-height instead of --bar-height (48px), reclaiming the
newly-freed top zone on the right edge.
* style(layout): add breathing room above strip below WCO band
Strip top inset moves from --wco-height to --wco-height + --s so the
first action button isn't flush against the bottom of the window
controls overlay on Win/Linux.
* fix(styles): scope vertical-action-bar tooltip rule to the experiment
The pane-level `pointer-events: none` was needed only because tooltips
in the teleported vertical strip can land over the next button. Apply
it via a new `body.isVerticalActionBar` body class (toggled from
`misc.isVerticalActionBar` in GlobalThemeService, alongside the existing
`isObsidianStyleHeader` effect) so unrelated app tooltips keep their
default pane behaviour.
* refactor(styles): extract vertical-action-bar CSS to its own partial
Move the 130-line experimental strip block out of `src/styles.scss` and
into `src/styles/components/_vertical-action-bar.scss`. Wired via
`_components.scss`. No CSS rules change — pure code organisation.
* merge(master): resolve _components.scss conflict in right action bar branch
Agent-Logs-Url: https://github.com/super-productivity/super-productivity/sessions/1941dd45-72e1-4a3e-92da-912fcfcb9bed
Co-authored-by: johannesjo <1456265+johannesjo@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: johannesjo <1456265+johannesjo@users.noreply.github.com>
provideMockStore({selectors}) calls setResult() on the module-level
selector, which persists across spec files until clearResult(). Without
a reset, selectProjectById stays pinned to the last value and pollutes
later specs (e.g. project.service unarchive tests saw a stale project
and never entered the isHiddenFromMenu branch).
- Move confirm dialog from ProjectService into work-context-menu, matching
the existing deleteProject/deleteTag pattern. archive() is back to a
thin dispatch + snack and no longer returns Promise<boolean>.
- Drop UNDO from the non-hidden unarchive snack; collapse the now-unused
_archiveNow helper. The hidden-from-menu branch keeps its "Show in menu"
action.
- Read project snapshot before dispatch in unarchive() via firstValueFrom,
dropping the take(1).subscribe() pattern and its WHY-comment.
- Trim three defensive selector specs to one (selectOverdueTasks).
- Drop project-task-page.component.spec.ts (it tested its own stub template).
- Re-namespace SHOW_IN_MENU → T.F.PROJECT.S.* and ARCHIVED_NOTICE →
T.F.PROJECT.ARCHIVED_NOTICE; trim the notice template to text + button.
Net: -230 lines / +92 lines.
- Confirm dialog for archive (replaces snack+UNDO); navigation in
work-context-menu now awaits confirmation.
- Archived-projects rows link to /project/<id>/tasks; project view
shows an inline restore notice when the active project is archived.
- Unarchive snack split: when the project is still hidden from the
menu, offer a "Show in menu" action; otherwise keep UNDO.
- Terminology: snack now reads "Project restored" to match the button.
- Tests: spec for archived-projects-page and project-task-page;
defensive archive-filter coverage on selectAllTasksWithReminder /
selectAllTasksWithDeadlineReminder / selectOverdueTasks; service
spec covers confirm/cancel paths and the hidden-from-menu branch.
* test(e2e): strengthen reviewed assertions
* test(e2e): tighten types and scope pageerror capture
- markdown-link-persistence: merge compact/full op type into a single
optional-field shape so `in`-based narrowing isn't needed, and add
non-null assertions after `expect(...).not.toBeNull()` so the spec
satisfies e2e tsconfig `strict: true`.
- issue-provider-panel: attach the `pageerror` listener after the panel
is open and detach before the final assertion to avoid attributing
unrelated startup noise to the dialog loop.
- recurring-task: document the prefix contract on
`addTaskWithoutWaitingForTodayList` and why `BasePage.addTask` cannot
be reused for future-dated tasks.
* feat(project): add archive project UI
Allow users to archive completed projects via the project context menu.
Archived projects are hidden from the sidebar and visibility menu, but
retained with all their tasks. They can be unarchived at any time from
the visibility menu (eye icon), where they appear below a divider with
an unarchive icon.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(project): add unit tests for archive/unarchive project feature
- projectReducer: verify archiveProject sets isArchived=true, unarchiveProject
sets it back to false, and that other projects are not affected
- WorkContextMenuComponent: verify archiveProject() calls the service, shows
a snack, navigates away when the archived project is currently active, and
does not navigate otherwise
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(project): document archive/unarchive project feature
Add "Archiving Projects" section to the Project View wiki page explaining
how to archive a completed project and how to restore it via the visibility menu.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(project): new archived projects page
* feat(project): improve archive UX with undo snack and hidden-menu warning
- Replace plain archive notification with icon + undo action so accidental archiving is recoverable without a confirmation dialog
- Add snack on unarchive; warn when project is still hidden from menu (isHiddenFromMenu=true) so users know why it won't appear in sidebar
* feat(reminder): suppress reminders for archived project tasks
* feat(tasks): hide archived project tasks from tags, today, and boards
* feat(tasks): hide archived project tasks from overdue tasks list
* fix(repeatTasks): exclude TaskRepeatCfg of archived projects when generating the next instance
* fix(tasks): hide Task and TaskRepeatCfg of archived project from the /scheduled-list page
This effectively hides the "Overlaps with another scheduled task" message if the other task belongs to an archived project.
* feat(docs): Add documentation for archived projects
* fix: revert changes to the it.json file
* feat: hide all task-repeat-cfg from planners and introduce a new task-repeat-cfg selector selectActiveTaskRepeatCfgs
* feat(tasks): add base selector for filtering tasks from active projects
* feat(ui): add count of archived project in project visibility menu and hide the archived projects page link when none exist
* feat(ui): improve accessibility for archived projects page by adding aria-labels and aria-hidden attributes
* perf(projects): use a sorted selector in archived projects page to avoid sorting during user's search
* refactor(tasks): derive selectAllUndoneTasksWithDueDay from the common shared selector
* refactor(tasks): introduced shared selectors to centralize active projects filtering
* fix: add mock selector for selectUndoneOverdueDeadlineTasks in plan view tests
* refactor(planner): use selectAllTasksInActiveProjects to exclude archived project tasks
Replaces direct task state access with the shared selectAllTasksInActiveProjects selector, which already filters archived project tasks. Fixes archived project tasks appearing in planner view.
Test mock stores updated to include tasks+projects slices (required by new selector chain). Removed "missing entity references" test: that guard now lives upstream in selectAllTasks.
* refactor(work-context): exclude archived project tasks from TAG/TODAY and SCHEDULE
selectActiveWorkContext and selectTodayTaskIds were using raw
taskState.entities, causing archived-project tasks to appear in tag boards and Today.
Fix by filtering entities in the selectors using selectArchivedProjectIds before calling computeOrderedTaskIdsForToday / computeOrderedTaskIdsForTag.
Apply the same logic to selectTimelineTasks and remove the
combineLatest workaround added in 9d6fe5e.
* fix(work-context): exclude archived project's tasks when selecting tasks in schedule page
* fix(tasks): exclude archived project tasks from unplanned deadline banner
* fix: fix tests after rebase onto master
* fix: stabilize selectArchivedProjectIds Set instance
Derive selectArchivedProjectIds from a base selectArrayOfArchivedProjectIds selector to avoid recreating the Set whenever unrelated project properties change
* perf(selectors): avoid unnecessary recomputations from archived project selectors
selectArchivedProjectIds was creating a new Set on every project-state update, causing unnecessary recomputation across downstream selectors even when archived projects did not change.
- Add selectArrayOfArchivedProjectIds as a stable intermediate selector
- Rebuild selectArchivedProjectIds only when archived IDs change
- Memoize active-project task entities instead of filtering inline
- Remove unused selectTaskFeatureState dependency from selectActiveWorkContext
* fix(project): prevent inbox project to be archived
* fix(task): add filters for non-archived projects and comment explaining why filter was not added in selectors
* fix: always call archive service before navigateByUrl when triggering archive action
* feat(ui): add undo action on project restored from archive snack
* fix: add guards against empty projectId in tasks
* fix(tasks): keep orphaned subtasks scheduled in selectLaterTodayTasksWithSubTasks
Restored selector to its original form, but reading tasks from selectAllTasksInActiveProjects instead of selectTaskFeatureState.
While categorizing tasks, group subtasks by their parentId in a single pass to avoid calling mapSubTasksToTask that requires TaskState.
* refactor(tasks): compose selectAllTasksWithoutHiddenProjects from selectAllTasksInActiveProjects + new selector for hidden projects
* test(tasks): restore coverage gaps from selector refactors
- selectAllUndoneTasksWithDueDay: add "should include subtasks with dueDay" (dropped when selectAllTasksWithDueDay was removed in 450855b)
- selectAllTasksDueToday: add "should handle missing entity references in planner days with empty task state gracefully" (replacing the taskState.ids variant removed in 9280b85 since upstream guard now owns that invariant)
* feat(tasks): extracted a new selector selectActiveTaskMap that returns a memoized map of active tasks indexed by id
* fix(tasks): removed unused selector
* refactor(projects): move archive/unarchive snacks inside project service and drop the UNARCHIVED_BUT_HIDDEN message
* refactor(projects): use only filtered sources in selectTimelineTasks
* refactor(tasks): rename selectActiveTaskMap into selectMapOfAllTasksInActiveProjects to better align with current naming conventions
* feat(a11y): add aria label to project archive elements
* chore: add comment to selectAllTasksInActiveProjects selector's fast path
* test: add expectation on operation order in archive project action
Navigation must happen after the ProjectService.archive() call
---------
Co-authored-by: Symon <peterbaikov12@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Two design docs for slimming the document-mode plugin's sync footprint: the sync-data-model plan covers the immediate fix (bare-atom chips) plus deferred per-context entities; the delta-sync plan analyses why true deltas need finer entity granularity or a commutative CRDT (Yjs) given the partially-ordered op-log.
Refs #7740.
The document-mode plugin persisted each taskRef/subTaskRef chip with its task title as inline content plus an isDone attr. Both are re-derived from the host task cache on load (migrateStoredDoc backfills content, refreshChipContentFromCache overwrites both), so storing them only inflates the synced blob -- and the title is its byte-heavy part.
Add a pure stripChipContent() that collapses every chip to a bare identity atom { type, attrs: { taskId } }, and apply it in flushSave / flushSaveSync before serializing. The live editor document keeps its inline content, so title write-back is unaffected; only the persisted copy shrinks. A bare-atom chip loads through the existing unchanged pipeline, so no schema bump or migration is needed. Roughly halves the per-change sync payload.
OperationLogStoreService and ArchiveStoreService opened the SUP_OPS
IndexedDB database without a `versionchange` handler. A connection with
no such handler never closes itself, so a future schema upgrade opened
by another tab would be blocked indefinitely until the user manually
closed the older tabs.
Add a `versionchange` handler to each, mirroring the one ClientIdService
already has: close the connection and clear the cached handle so the
next access transparently reopens. Covered by new connection-lifecycle
tests, including a first spec file for ArchiveStoreService.
Refs #7735.
Rescopes #7735 from the original three-connection consolidation down to
its one genuine correctness fix: register versionchange handlers on the
OperationLogStoreService and ArchiveStoreService SUP_OPS connections so a
future schema bump cannot be stalled by a handler-less connection.
The connection consolidation is documented as not planned (no behavioral
benefit, net-additive LOC on a safety-critical path); the rejected
"OperationLogStoreService as connection owner" alternative is kept for
the record. Both decisions followed multi-agent review rounds.
The background context menu was bound globally to .route-wrapper with
allowedSelectors that also matched non-work-view pages (.page-wrapper,
.route-wrapper > *). Right-clicking blank space on Habits, Config, etc.
opened a menu whose actions targeted the stale active work context.
Add an isEnabled input to ContextMenuComponent and gate the background
menu on the task-list routes (tag-tasks / project-tasks) only.
Closes#7734
PR #7731 makes the app's "System default" date/time locale follow
navigator.language. The Playwright config did not pin a locale, so CI
Chromium reported en-US and date-format-dependent tests broke
(DD/MM vs MM/DD, 24h vs 12h).
Set use.locale to en-GB so navigator.language is deterministic across
machines, matching the en-GB format the recurring/date tests assume.
Reconstructs a single user's AppDataComplete by replaying their op log up to a chosen serverSeq and decrypting E2E-encrypted payloads — the recovery path the server's restore endpoint cannot offer for encrypted accounts (it throws EncryptedOpsNotSupportedError). Read-only on the DB; documented in docs/backup-and-recovery.md. Unverified against real encrypted data — see the script header.