Replace flag-based result objects with DownloadOutcome and UploadOutcome
discriminated unions at the orchestrator level. This eliminates null
returns, optional boolean checks, and ambiguous flag combinations.
- Add DownloadOutcome (5 variants) and UploadOutcome (3 variants) types
- Update OperationLogSyncService to return discriminated unions
- Update SyncWrapperService and ImmediateUploadService consumers
- Use exhaustive switch in downloadCallback adapter
- Replace piggybackedOps array pass-through with piggybackedOpsCount
- Add handleEncryptionWarningDialog() to supersync-helpers.ts to
eliminate duplicated dialog-handling blocks across import.page.ts
and two supersync spec files
- Replace isVisible() point-in-time check with waitFor({ timeout: 1000 })
in supersync.page.ts to avoid race when dialog is still animating in
- Replace silent .catch(() => {}) with a logged warning when the
encryption dialog fails to close after confirmation
* fix(build): fix tag resolution and add error handling in bump-android-version
The previous two-tag approach (`currentTag` → `prevTag`) would resolve the
wrong changelog range during `npm version` since the new tag doesn't exist
yet. Simplified to a single `lastTag...HEAD` range which correctly captures
all commits since the last release.
Added try-catch with fallback to last 20 commits when no tags exist, and
a fallback message for empty changelogs.
* fix(sync): differentiate auth error messages and clarify token revocation
- Rename server dashboard "Refresh Token" to "Revoke & Replace Token"
with explicit warning that ALL devices will be disconnected
- Return rejection reason in 401 responses (revoked, expired, etc.)
via discriminated union TokenVerificationResult type
- Show different client error messages for server-side token rejection
vs missing local credentials to aid user diagnosis
- Extract server error reason from JSON response body in AuthFailSPError
Closes#6597
* refactor(sync): use generic auth reason and improve test readability
- Replace 'User not found' and 'Account not verified' with generic
'Account unavailable' to avoid leaking account state in API responses
- Extract long reason strings to constants in middleware.spec.ts
* fix(sync-server): add per-route rate limits to silence CodeQL alert
Add explicit rate limits to routes that only had the global rate limit:
- GET /api/sync/status: 60/min
- DELETE /api/sync/data: 3/15min (destructive operation)
- GET /api/sync/restore-points: 30/min
- GET /api/sync/restore/:serverSeq: 10/5min (CPU-intensive)
- GET /reset-password: 20/15min
- GET /recover-passkey: 10/15min
These complement the global @fastify/rate-limit (100/15min) and silence
the CodeQL 'Missing rate limiting' alert on authenticated handlers.
* fix(build): fix tag resolution and add error handling in bump-android-version
The previous two-tag approach (`currentTag` → `prevTag`) would resolve the
wrong changelog range during `npm version` since the new tag doesn't exist
yet. Simplified to a single `lastTag...HEAD` range which correctly captures
all commits since the last release.
Added try-catch with fallback to last 20 commits when no tags exist, and
a fallback message for empty changelogs.
* fix(sync): differentiate auth error messages and clarify token revocation
- Rename server dashboard "Refresh Token" to "Revoke & Replace Token"
with explicit warning that ALL devices will be disconnected
- Return rejection reason in 401 responses (revoked, expired, etc.)
via discriminated union TokenVerificationResult type
- Show different client error messages for server-side token rejection
vs missing local credentials to aid user diagnosis
- Extract server error reason from JSON response body in AuthFailSPError
Closes#6597
* refactor(sync): use generic auth reason and improve test readability
- Replace 'User not found' and 'Account not verified' with generic
'Account unavailable' to avoid leaking account state in API responses
- Extract long reason strings to constants in middleware.spec.ts
* fix(sync): reset vector clock to minimal after SYNC_IMPORT
After a SYNC_IMPORT, working clocks were carrying forward ALL accumulated
client IDs (including dead ones from reinstalls/old browsers), keeping the
clock permanently at MAX_VECTOR_CLOCK_SIZE. This caused unnecessary pruning
and comparison issues on every sync.
Three complementary changes:
1. Reset working clock to minimal after SYNC_IMPORT: When a client creates
or receives a SYNC_IMPORT, its working clock is reset to only the import
client's entry + own entry. Dead client IDs are dropped. The full clock
is preserved in the stored SYNC_IMPORT operation for filtering.
2. Import-client-counter exception in SyncImportFilterService: Post-import
ops with minimal clocks appear CONCURRENT with the import (missing old
entries). A new exception recognizes them as post-import by checking if
the op has the import client's counter >= the import's own counter value.
3. Updated SimulatedClient test helper to reset clocks on SYNC_IMPORT,
matching the real behavior in OperationLogStoreService and
SyncHydrationService.
https://claude.ai/code/session_01T5K1kq8m6LPDc6dEGyXnEW
* chore: update package-lock.json
https://claude.ai/code/session_01T5K1kq8m6LPDc6dEGyXnEW
* refactor(sync): use FULL_STATE_OP_TYPES constant and improve test assertions
- Replace magic string casts `(opType as string) === 'REPAIR'` with
`FULL_STATE_OP_TYPES.has(opType)` in simulated-client helper
- Document transitive propagation assumption in import-client-counter
exception comment
- Improve test assertion to use toContain for better failure messages
---------
Co-authored-by: Claude <noreply@anthropic.com>
Suppress noisy "EXITING due to failed single instance lock" message when
a second instance is launched via xdg-open for protocol URL handling.
Register superproductivity:// as a MIME type handler in the .desktop file
so Linux users no longer need manual xdg-settings configuration.
Remove duplicate requestSingleInstanceLock() call in start-app.ts.
Closes#173
- Pre-capture provider config before destructive deleteAndReuploadWithNewEncryption
call so disableEncryption revert restores the original encryption key
- Merge guard check and config pre-capture in enableEncryption to eliminate
redundant getActiveProvider()/privateCfg.load() calls
- Fix isSubTask detection in LWW meta-reducer to use new parentId when present,
correctly handling subtask-to-main-task promotion
- Remove redundant unique() calls guarded by preceding includes() checks
The previous two-tag approach (`currentTag` → `prevTag`) would resolve the
wrong changelog range during `npm version` since the new tag doesn't exist
yet. Simplified to a single `lastTag...HEAD` range which correctly captures
all commits since the last release.
Added try-catch with fallback to last 20 commits when no tags exist, and
a fallback message for empty changelogs.
Ignore entire .claude/ directory to keep local settings and commands
out of version control. Also fix test expectation to match Bug #6726
behavior where pausedTaskId is intentionally undefined on skipBreak.
Remove unnecessary setSelectedId call from goToFocusMode() in the task
context menu. This aligns behavior with other focus mode entry points
(keyboard shortcut, header button) which only set currentTaskId.
Closes#6727
Don't override user's task choice when a break ends or is skipped.
Guard setCurrentTask dispatch with currentTaskId check in skipBreak$
and resumeTrackingOnBreakComplete$ effects, and pass undefined
pausedTaskId when syncTrackingStartToSession$ auto-skips a break.
Remove the 15k-line auto-generated CHANGELOG.md and its conventional-changelog
tooling. The bump-android-version script now derives Play Store fastlane
changelogs from git log between tags instead of parsing CHANGELOG.md.
- Use execFileSync with argv to avoid shell injection
- Filter merge commits with --no-merges
- Handle breaking-change prefix (feat!:) in regex
- Truncate at line boundaries for 500-char Play Store limit
- Remove conventional-changelog-cli and @conventional-changelog/git-client deps
- Remove release.changelog script from package.json
Two root causes for failing E2E tests in CI:
1. Encryption button not visible after provider selection: The async
provider change listener loads config from IndexedDB and updates the
Formly model, but the 1000ms waitForTimeout was insufficient in CI.
Replace with polling (200ms intervals, 10s timeout) that waits for
either enable or disable encryption button to become visible.
Also removes ~130 lines of leftover debug logging from disableEncryption().
2. Drag-handle strict mode violation on parent tasks with subtasks:
task.locator('.drag-handle') matched both the parent's drag handle
and nested subtask drag handles. Add .first() to all drag-handle
locator usages to target only the parent task's own handle.
Use JS-computed var(--safe-area-bottom) instead of env(safe-area-inset-bottom)
which returns 0 on Android WebView. Also account for bottom nav height and
position sidebar below status bar on native mobile.
Closes#6561
- Unify duplicated HTTP methods in SuperSyncProvider into shared helpers
- Remove 5 encryption DI injection tokens, use private properties for test spying
- Remove DerivedKeyCacheService wrapper, use direct imports
- Cache _getServerSeqKey() result in SuperSyncProvider
- Split SyncProviderServiceInterface into SyncProviderBase + FileSyncProvider
- Remove dead file-op stubs from SuperSync provider
- Extract shared delete-and-reupload pattern into SnapshotUploadService
- Extract helper methods from operation-log-sync.service.ts
- Auto-invalidate WrappedProviderService cache on config changes
- Fix: preserve auth credentials in encryption toggle revert paths
- Fix: add config revert on disableEncryption failure
~720 lines net reduction across 26 files.
- Add TODAY_TAG to FAKE_ROOT_STATE in TaskArchiveService so
handleUpdateTask can find it when marking tasks as done
- Update dueDay assertion to expect defined value (set to today
by updateDoneOnForTask)
- Add mock tasks to LWW integration tests so orphan filter
doesn't remove taskIds from PROJECT/TAG entities
LWW conflict resolution replaces entire TAG/PROJECT entities without
checking whether referenced taskIds still exist. Similarly, updateTag
actions from remote clients can re-introduce deleted task IDs. This
adds active filtering in both the lwwUpdateMetaReducer (for LWW Update
actions) and tagSharedMetaReducer (for updateTag actions) to remove
references to non-existent tasks before they enter the store.
When a task is completed, set dueDay to today's date instead of clearing
it, so completed tasks appear in the Today view and metrics. Also clears
dueWithTime for mutual exclusivity and adds the task to TODAY_TAG.taskIds
for ordering. Subtasks are excluded from TODAY_TAG.taskIds since their
parent manages membership. Completed tasks are also removed from future
planner days to prevent stale entries.
Closes#6716
When isManualBreakStart was enabled, the isManualSessionCompletion flag
prevented the tick reducer from stopping the timer at duration, causing
it to run past 0 indefinitely. Remove this flag entirely — break
auto-start is already correctly gated by isManualBreakStart in the
autoStartBreakOnSessionComplete$ effect.
Fixes#6715
* fix(build): ensure plugin-api is built before plugin builds
The github-issue-provider plugin build was failing in CI because
plugin-api/dist/ didn't exist when tsc --noEmit ran. The dist/ directory
is gitignored and only built during the prepare lifecycle, which may not
run reliably in all CI contexts. Adding plugin-api:build to the
plugins:build script ensures the type declarations are always available.
Also improved error logging in build-all.js to show stderr on failure.
https://claude.ai/code/session_01StB1pMk1g2k79AywyksoyZ
* fix(build): remove tsc --noEmit from github-issue-provider build
The github-issue-provider was the only plugin running tsc --noEmit as
part of its build command. In CI, TypeScript module resolution fails for
the file: dependency on plugin-api due to npm workspace hoisting
interference. All other plugins (ai-productivity-prompts,
procrastination-buster, sync-md) skip type-checking during build and
rely on esbuild to strip type-only imports.
Align with existing plugin pattern by separating build from typecheck.
Also improve error logging in build-all.js to capture stdout on failure.
https://claude.ai/code/session_01StB1pMk1g2k79AywyksoyZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
* test(e2e): fix SuperSync encryption overlay and plugin build in CI
Two root causes for failing e2e tests:
1. Plugin build failure in global-setup.ts: The plugin manifest check
only looked at the dev path (src/assets/bundled-plugins/) but not the
CI pre-built path (.tmp/angular-dist/browser/assets/bundled-plugins/).
In CI, the frontend is pre-built and downloaded as an artifact, so the
dev path doesn't exist. This caused all WebDAV tests to fail.
2. SuperSync enableEncryption overlay blocking: The sync wrapper opens
a mandatory encryption dialog with disableClose:true after a successful
sync when encryption is not yet configured. ensureOverlaysClosed()
uses Escape key which cannot dismiss disableClose dialogs.
Fixes:
- Add enableEncryptionDialog to the sync outcome race so the mandatory
dialog is detected immediately instead of waiting for a 30s timeout
- enableEncryption() now checks if the dialog is already open and
handles it directly instead of trying to right-click the sync button
- Extract _fillAndConfirmEncryptionDialog() and
_waitForEncryptionSyncComplete() helpers for reuse
- Improve enable encryption button lookup to check both top-level
(SuperSync) and Advanced collapsible positions
https://claude.ai/code/session_01CnQXCAB3WYnYdKPbNEqUS3
* chore: update package-lock.json after npm install
Regenerated lock file to resolve workspace dependencies including
vite-plugin's devDependencies (@types/node, vite, typescript).
https://claude.ai/code/session_01CnQXCAB3WYnYdKPbNEqUS3
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add mock providers for GlobalThemeService, PluginIssueProviderRegistryService,
IssueSyncAdapterRegistryService, and PluginHttpService to prevent transitive
dependency resolution reaching LayoutService which requires Store.pipe().
Move title declaration before configFormSection and convert
_mapPluginConfigField from arrow function field to method so both
are available when _getPluginFormSection() runs during field init.
With reuseExistingServer: true, Playwright silently reuses whatever
is on port 4242 without health checks. A stale ng serve that no
longer serves assets causes all tests to fail with cryptic 404s.
Setting false makes Playwright fail fast with a clear error message.
The regex /skip|start/i matched "restart_alt" (mat-icon text in
the "Reset session counter" button) before "Skip break", causing
the wrong button to be clicked and the break screen to stay visible.
- Fix type errors in dialog-edit-issue-provider component by casting
map result to LimitedFormlyFieldConfig and importing the type
- Fix type cast in issue-provider reducer migration via unknown
- Make build-packages.js only require index.html for plugins with
iFrame enabled, checking manifest.json in both root and src/
The Formly form model defaults isEncryptionEnabled to false. When
_updatePrivateConfig() merges form values over the saved config,
this overwrites the true value set by the EnableEncryption dialog.
Every subsequent sync then triggers _promptSuperSyncEncryptionIfNeeded(),
re-opening the encryption dialog and causing DecryptNoPasswordError
on other clients.
For SuperSync, isEncryptionEnabled is now preserved from the saved
config since it is managed exclusively by dedicated dialogs
(EnableEncryption, DisableEncryption, HandleDecryptError).
Also hardens the E2E page object to handle encryption dialogs that
appear mid-sync (enter_password, enable_encryption) using a stored
password from setupSuperSync().
Add a plugin-based issue provider architecture that allows plugins to
register as issue providers alongside the existing built-in providers.
- Plugin API: types for search, display, comments, two-way sync, and
field mappings in @super-productivity/plugin-api
- Registry service to manage plugin provider lifecycle
- HTTP proxy with SSRF protection for plugin network requests
- Adapter service implementing IssueServiceInterface for plugins
- Sync adapter for plugin-based two-way sync
- Plugin config UI in the issue provider edit dialog
- Plugin providers shown in setup overview and issue panel
Migrate GitHub from built-in to plugin as first proof:
- Bundled github-issue-provider plugin with full feature parity
- Reducer migration converts old GitHub data to plugin format while
preserving legacy fields for cross-version compatibility
- Auto-enable plugin when existing GitHub providers are detected
- Plugin registers under 'GITHUB' key via MigratedIssueProviderKey
- GitHub translations moved to plugin i18n bundles
Mark Two-Way Sync as experimental in the UI.
On native mobile (Capacitor), body padding for safe areas pushed the
app container below the visible viewport, clipping the sidebar bottom
and hiding items like Settings. Subtract safe area insets from the
container height and let the sidebar inherit it.
Add getPluralKey() utility that uses the browser-native Intl.PluralRules
API to select the correct CLDR plural category (one/few/many/other) for
any locale. This enables languages like Romanian, Russian, Polish, and
Arabic to provide proper plural forms beyond simple singular/plural.
Key changes:
- New getPluralKey() utility with locale-aware fallback via TranslateStore
- Rename translation keys: SINGULAR→ONE, PLURAL→OTHER (CLDR convention)
- Nest banner TXT_MULTIPLE keys into ONE/OTHER for proper English grammar
- Migrate all 27 locale files to new key structure
- Add 10 unit tests covering CLDR categories, fallback, and edge cases
Closes discussion #6698
Wire the existing but unused resetCycles NgRx action to two UI
locations so users can reset the pomodoro session counter:
- Break screen: shown only in Pomodoro mode alongside skip/planning
- Pomodoro settings dialog: left-aligned reset button in actions row
The button appeared after 3s but an 8s auto-timeout already exists
as a safety net. For SuperSync, there's no wait at all. The button
provided marginal value and created unnecessary UI noise.