Commit graph

17487 commits

Author SHA1 Message Date
Johannes Millan
cbeecfd39a fix(sync): multi-tab vector clock staleness + shared code extraction
Bug fix:
- Fix vector clock cache staleness in multi-tab scenarios by clearing
  cache when acquiring operation write lock. Each browser tab has its
  own in-memory cache, so Tab B's cache could be stale if Tab A wrote
  while Tab B was waiting for the lock.

Shared code extraction (client/server consistency):
- Extract vector clock comparison to @sp/shared-schema
  - Client wraps shared impl with null handling
  - Server imports directly from shared
- Extract entity types to @sp/shared-schema
  - Single source of truth for ENTITY_TYPES array
  - Removes duplicated "must match" comments

Files:
- packages/shared-schema/src/vector-clock.ts (new)
- packages/shared-schema/src/entity-types.ts (new)
- src/app/op-log/store/operation-log-store.service.ts (cache clear)
- src/app/op-log/capture/operation-log.effects.ts (call cache clear)
2026-01-03 18:05:11 +01:00
Johannes Millan
9ddced5648 feat(sync-server): sync email input across all auth forms
- Email is now shared between login, register, and lost passkey forms
- Email persists to localStorage across page reloads
- Typing in any email field updates all others in real-time
2026-01-03 15:50:15 +01:00
Johannes Millan
868ed71c4a fix(sync-server): add migration script for passkey credential IDs
Existing passkeys were stored with double-encoded credential IDs due to
a bug where SimpleWebAuthn's credentialInfo.id (UTF-8 bytes of base64url
string) was stored directly instead of being decoded to raw bytes first.

This migration script converts existing passkeys from the old format
(ASCII bytes of base64url string) to the correct format (raw credential
ID bytes).

Run with: npx ts-node prisma/migrations/migrate-passkey-credentials.ts
2026-01-03 14:44:18 +01:00
Johannes Millan
a57a197d44 fix(sync-server): fix double encoding of passkey credential ID
SimpleWebAuthn's credentialInfo.id is a Uint8Array containing the
base64url-encoded credential ID as UTF-8 bytes, NOT the raw credential
ID bytes. We were storing these ASCII bytes directly, then encoding
them again as base64url during lookup.

During login, the browser sends the original base64url string, which
we decoded to raw bytes - but this didn't match the double-encoded
value stored in the database.

The fix decodes the base64url string from credentialInfo.id before
storing:
1. Convert Uint8Array to UTF-8 string (the base64url string)
2. Decode base64url to get raw credential ID bytes
3. Store raw bytes in database

Now login correctly looks up by raw credential ID bytes.
2026-01-03 13:32:37 +01:00
Johannes Millan
c07d0a4588 fix(sync-server): look up passkey by credential ID instead of email
With discoverable credentials, the user can select any passkey for
this RP. We need to look up the passkey by the credential ID that
comes back from the browser, not by the email the user entered.

This fixes the 401 error when the user's passkey credential ID
doesn't match what we expected based on email lookup.
2026-01-03 13:20:34 +01:00
Johannes Millan
0f8d2cbc42 chore: code review improvements for operation-logs branch
Phase 1 - Code Quality:
- Add clarifying comment in lww-update.meta-reducer.ts explaining
  Date.now() choice for LWW conflict resolution
- Document multi-instance deployment limitations in README.md
  (passkey challenges, snapshot locks)

Phase 2 - Testing:
- Add TODAY_TAG selector edge case tests (deleted/archived tasks)

Phase 3 - Minor Polish:
- Add createValidationErrorResponse() helper to hide Zod validation
  details in production (sync.routes.ts)
- Add database index @@index([userId, opType]) for faster restore
  point queries
2026-01-03 13:17:42 +01:00
Johannes Millan
374ba9873a fix(sync-server): use discoverable credentials for passkey login
Instead of providing allowCredentials (which requires the browser to
match specific credential IDs), let the browser discover all resident
credentials for this RP automatically.

This matches how passkeys.io works and should fix the "no passkeys
available" issue on mobile.
2026-01-03 13:16:01 +01:00
Johannes Millan
f1eab1d8ca debug(sync-server): add detailed credential ID logging for passkey
Log the full credential ID (both hex and base64url) during:
- Registration: to see exactly what's being stored
- Login: to see exactly what's being retrieved from DB

This will help debug why passkeys aren't being found during login.
2026-01-03 13:01:34 +01:00
Johannes Millan
be146bc046 chore: ignore simplewebauthn-browser.min.js from prettier 2026-01-03 12:33:42 +01:00
Johannes Millan
253e55cd2c fix(sync-server): use properly minified SimpleWebAuthn browser v13.2.2
The previous version was reformatted by prettier into 362 lines.
This version is the original minified bundle with version header.
2026-01-03 12:32:50 +01:00
Johannes Millan
d5e196b82b fix(sync-server): use v13 API format in passkey recovery page
The recovery page was using the old SimpleWebAuthn API format:
  startRegistration(options)

Updated to use the v13 format:
  startRegistration({ optionsJSON: options })

This was causing "nothing happens" when clicking Register New Passkey.
2026-01-03 12:14:47 +01:00
Johannes Millan
1fad43d198 fix(sync-server): update SimpleWebAuthn browser library to v13.2.2
The server was updated to @simplewebauthn/server v13.2.2 but the
browser library was still at pre-v13. The v13 API expects options
wrapped as { optionsJSON: options }, causing allowCredentials to be
lost during login with the old library.

This caused "no passkeys found" errors when users tried to login
after registering.
2026-01-03 12:08:04 +01:00
Johannes Millan
3a7191d5f5 fix(sync-server): require discoverable credentials for passkeys
Change residentKey from 'preferred' to 'required' for passkey registration.
Discoverable credentials (resident keys) are required for synced passkeys
like Google Password Manager to properly store and retrieve the passkey.

Also add detailed logging of registration options for debugging.
2026-01-02 22:07:03 +01:00
Johannes Millan
4cee57db14 fix(sync-server): fix SimpleWebAuthn v13 type imports
Import types from @simplewebauthn/server instead of deprecated
@simplewebauthn/types package which is no longer supported in v13.
2026-01-02 22:00:10 +01:00
Johannes Millan
f143210cd9 fix(sync-server): update SimpleWebAuthn to v13 and add debug logging
- Update @simplewebauthn/server from v11 to v13.2.2 for compatibility
- Add debug logging for WebAuthn config and authentication options
- Browser library also needs to be updated to v13.2.2 (manual step)

The version mismatch between server and browser libraries may have been
causing passkey registration/login issues.
2026-01-02 21:58:49 +01:00
Johannes Millan
4ca44fd3d3 add logs 2 2026-01-02 21:23:35 +01:00
Johannes Millan
92b8aa8178 add logs 2026-01-02 21:16:03 +01:00
Johannes Millan
f09d7c51b4 fix(sync-server): allow passkeys without user verification
The @simplewebauthn/server library defaults to requireUserVerification: true,
but our registration options set userVerification: 'preferred'. This mismatch
caused passkey registration and login to fail with:
"User verification was required, but user could not be verified"

Add requireUserVerification: false to all verification calls to match
the 'preferred' setting in registration/authentication options.
2026-01-02 21:07:37 +01:00
Johannes Millan
4843d2afb7 fix(sync-server): add body to DELETE account request
The deleteAccount fetch request sent Content-Type: application/json
but no body, causing Fastify's JSON parser to fail with 400 Bad Request.

Add empty JSON body to match the pattern used by refreshToken.
2026-01-02 21:02:34 +01:00
Johannes Millan
ab648b2b6d fix(sync-server): add WebAuthn env vars to docker-compose 2026-01-02 20:55:48 +01:00
Johannes Millan
73549b7933 fix: load .env file for supersync container 2026-01-02 20:35:42 +01:00
Johannes Millan
d27fda7ca2 Merge branch 'master' into feat/operation-logs
* master:
  fix(e2e): improve WebDAV sync test reliability

# Conflicts:
#	e2e/pages/sync.page.ts
2026-01-02 20:11:39 +01:00
Johannes Millan
84e803743d fix: remove database.service.ts (deleted in feat/operation-logs)
This file was incorrectly kept during merge resolution. It depends on
files that no longer exist (indexed-db-adapter.service, db-adapter.model,
android-db-adapter.service) which were removed as part of the operation
logs refactor.
2026-01-02 20:01:07 +01:00
Johannes Millan
e6b6468d2a fix(e2e): improve WebDAV sync test reliability
- Add waitForAppReady and tour dismissal after page reload in
  webdav-sync-tags test to prevent task-list timeout
- Use existing providerSelect locator instead of role-based combobox
  selector in sync.page.ts for more reliable dropdown interaction
- Add graceful error handling for scrollIntoViewIfNeeded with fallback
2026-01-02 19:59:59 +01:00
Johannes Millan
e6ea0d74f0 Merge branch 'master' into feat/operation-logs
* master: (37 commits)
  16.8.0
  feat(i18n): add new translations
  fix: address code review issues from today's changes
  fix: address code review issues from today's changes
  fix(data-repair): change quickSetting to CUSTOM when startDate is missing
  fix(test): fix fetch spy setup in audio tests
  fix(android): sync time tracking from notification correctly on resume
  fix(database): prevent repeated error dialogs when disk is full
  fix(reminder): prevent dismissed reminders from reappearing
  fix(task-repeat): prevent race condition when saving repeat config
  fix(android): add error handling for native service calls
  fix(reminder): cancel native Android reminders immediately on task deletion
  fix(error-handler): use getErrorTxt to prevent [object Object] in error titles
  fix(planner): use task startDate for weekly repeat weekday calculation
  fix(focus-mode): use independent 1s timer for Pomodoro countdown
  feat(focus-mode): add Skip Break button to banner during active breaks
  feat(notes): add auto-save to fullscreen markdown editor
  fix(reflection-note): prevent trailing spaces from being deleted while typing
  fix(sync): add error handling for JSON parse failures in sync data
  fix(error-handling): prevent [object Object] from appearing in error messages
  ...

# Conflicts:
#	src/app/core/persistence/database.service.ts
#	src/app/features/android/store/android-focus-mode.effects.ts
#	src/app/features/android/store/android-foreground-tracking.effects.ts
#	src/app/features/reminder/reminder.service.spec.ts
#	src/app/features/reminder/reminder.service.ts
#	src/app/features/tasks/dialog-view-task-reminders/dialog-view-task-reminders.component.ts
#	src/app/features/tasks/store/task-reminder.effects.spec.ts
#	src/app/features/tasks/store/task-reminder.effects.ts
#	src/app/features/work-context/store/work-context.effects.spec.ts
#	src/app/features/work-context/store/work-context.effects.ts
#	src/app/t.const.ts
#	src/assets/i18n/en.json
2026-01-02 19:56:30 +01:00
Johannes Millan
092d32a39e 16.8.0 2026-01-02 19:26:41 +01:00
Johannes Millan
ca22c0d8f6 feat(i18n): add new translations 2026-01-02 19:26:18 +01:00
Johannes Millan
795ec42f64 fix: address code review issues from today's changes
- fix(focus-mode): prevent double startFocusSession dispatch in Pomodoro
  when clicking Start after break time is up (check shouldAutoStartNextSession)
- fix(task-repeat): gracefully fall back to CUSTOM quickSetting when
  startDate is missing instead of throwing error
- fix(work-context): also validate TAG context after sync, not just PROJECT
- fix(android): add error handling for cancelNativeReminder calls
- fix(android): add error handling for immediate save operations
- fix(reminder): use type guard in Promise.all filter for better type safety
- docs(audio): add comment explaining fire-and-forget pattern for resume()
2026-01-02 18:34:29 +01:00
Johannes Millan
5007d6178f Merge remote-tracking branch 'origin/master'
* origin/master:
  use Log.warn instead of console.warn added more warning output if board reference is not found
  adjusted css so that right click on the whole tab label works to open the context menu removed import of no longer required ContextMenuComponent
  chore(deps): bump actions/upload-artifact from 5 to 6
  chore(deps): bump actions/cache from 4 to 5
  chore(deps-dev): bump @typescript-eslint/utils from 8.41.0 to 8.51.0
  Disable play button and show tooltip when no tasks available
  use translations for board actions
  updated boards context menu to include delete and edit actions
2026-01-02 18:21:19 +01:00
Johannes Millan
6d57aaff13 fix: address code review issues from today's changes
- fix(android): await async _syncElapsedTimeForTask in notification handlers
  to ensure time is synced before saving/pausing
- fix(notes): prevent duplicate emit in fullscreen markdown editor by
  tracking last emitted content
2026-01-02 18:16:33 +01:00
Johannes Millan
cb27b53f85 fix(data-repair): change quickSetting to CUSTOM when startDate is missing
Repeat configs with date-dependent quickSettings (WEEKLY_CURRENT_WEEKDAY,
YEARLY_CURRENT_DATE, MONTHLY_CURRENT_DATE) require a startDate to derive
the correct weekday/date. This repair changes invalid configs to CUSTOM
to prevent crashes.

Fixes #5802
2026-01-02 18:16:20 +01:00
Johannes Millan
2ec5e08df4 fix(sync): address code review findings for operation log
- Add startup warning in passkey.ts when running with in-memory
  challenge storage in production (multi-instance deployments)
- Use LOCK_NAMES.OPERATION_LOG constant in stale-operation-resolver
  instead of hardcoded 'sp_op_log' string
- Add client-side validation for empty/whitespace entityId to match
  server-side validation and provide earlier feedback
2026-01-02 18:13:22 +01:00
Johannes Millan
626ba76c1b fix(today): include tasks with dueWithTime for today in Today view (#5841)
The computeOrderedTaskIdsForToday() selector only checked task.dueDay
for TODAY membership, ignoring tasks with dueWithTime set to today.
This caused daily repeating tasks with overdue scheduled times to not
appear in the Today view after SuperSync import.

Add dueWithTime fallback to computeOrderedTaskIdsForToday():
- Check dueDay first (primary source of truth)
- If dueDay doesn't match today, check if dueWithTime is for today
- This matches the pattern already used in selectLaterTodayTasksWithSubTasks

Add 6 unit tests covering the new fallback behavior:
- Task with dueWithTime for today but no dueDay
- Task with dueWithTime for today but stale dueDay
- Task with dueWithTime for tomorrow (should NOT be included)
- Task with both dueDay and dueWithTime for today (no duplicates)
- Mix of dueDay and dueWithTime fallback tasks

Fixes #5841
2026-01-02 17:59:36 +01:00
Johannes Millan
1ba7cf8960 fix(test): fix fetch spy setup in audio tests
Use jasmine.createSpy and direct assignment instead of spyOn for
window.fetch, which may not exist or may already be spied in the
test environment.
2026-01-02 17:56:51 +01:00
Johannes Millan
55d4fd1520 fix(android): sync time tracking from notification correctly on resume
When returning to the app after tracking time in the background, the app
would show less time than the notification because the sync from the
native Android foreground service was unreliable.

Root causes:
1. The _syncElapsedTimeForTask method used .subscribe().unsubscribe()
   anti-pattern which could fail to execute the callback
2. No protection against double-counting when both native sync and
   the app's tick$ added time for the same period

This fix:
- Refactors _syncElapsedTimeForTask to use async/await with firstValueFrom()
- Adds resetTrackingStart() to prevent double-counting after native sync
- Adds immediate save to IndexedDB when notification buttons are clicked
- Adds comprehensive unit tests for sync logic (10 new tests)

Fixes #5840
Related to #5842
2026-01-02 17:56:40 +01:00
Johannes Millan
3a179df3cc fix(sync): prevent data loss when multiple clients join SuperSync
Fixes critical bug where multiple clients with existing data could each
create a SYNC_IMPORT when enabling SuperSync, causing data loss.

Root cause: When clients had local data (from legacy migration), each
would detect "server needs migration" and create competing SYNC_IMPORTs.
The last one uploaded would invalidate all prior data.

Fixes:
- Server rejects duplicate SYNC_IMPORT with SYNC_IMPORT_EXISTS (409)
- Client double-checks server is empty before creating SYNC_IMPORT
- Client merges all local op clocks into SYNC_IMPORT's vector clock
- hasSyncedOps() excludes MIGRATION/RECOVERY ops from check
- Upload service gracefully handles SYNC_IMPORT_EXISTS rejection

Tests added:
- ServerMigrationService: double-check and clock merging (5 tests)
- OperationLogStoreService: hasSyncedOps() MIGRATION exclusion (7 tests)
- OperationLogUploadService: SYNC_IMPORT_EXISTS handling (5 tests)
- E2E: Multiple clients with existing data merge correctly
2026-01-02 17:51:12 +01:00
Johannes Millan
9f6442bf6b fix(database): prevent repeated error dialogs when disk is full
Show only one error alert when IndexedDB fails (e.g., disk full) instead
of spamming the user with dozens of confirm dialogs. The app now blocks
completely and restarts after the user dismisses the alert, preventing
potential data loss from retry attempts.

Fixes #5845
2026-01-02 17:37:50 +01:00
Johannes Millan
ee98760d08 fix(sync): reduce disruptive alerts during backup import
- Show "Unable to set sync provider" alert only in development mode,
  log warning in production (pfapi.service.ts)
- Replace "Remote model version newer" alert with console warning
  (sync-wrapper.service.ts)

Fixes #5839
2026-01-02 17:24:48 +01:00
Johannes Millan
9c3834b7ee fix(reminder): prevent dismissed reminders from reappearing
Fix race condition where dismissed reminders could reappear every few
seconds. The issue occurred because the worker's 10-second interval
could fire before the async DB save completed, sending stale reminder
data back to the dialog.

Two-part fix:
1. Update worker immediately when reminders change (before DB save)
2. Track dismissed reminder IDs in dialog and filter them from incoming
   worker data

Fixes #5826
2026-01-02 17:13:28 +01:00
Johannes Millan
dc12403747 fix(task-repeat): prevent race condition when saving repeat config
Add isLoading signal to disable Save button while async data loads.
This prevents the "Initial task repeat cfg missing" error when users
click Save before the repeat config finishes loading.

Fixes #5828
2026-01-02 17:12:21 +01:00
Johannes Millan
a14c95093d fix(android): add error handling for native service calls
Wrap all androidInterface native method calls in try-catch blocks to
prevent app crashes when Java exceptions are raised during method
invocation. Users are now notified via snackbar when service start
fails, while update/stop failures are logged silently.

Changes:
- Add _safeNativeCall helper to TypeScript effects for DRY error handling
- Add safeCall inline helper to Kotlin JavaScriptInterface
- Wrap focus mode and tracking service calls with error handling
- Show user-friendly error notification on service start failures
- Add unit tests for error handling logic

Fixes #5819
2026-01-02 17:09:08 +01:00
Johannes Millan
93e957edc1 fix(reminder): cancel native Android reminders immediately on task deletion
On Android, native reminders scheduled via AlarmManager were not being
canceled reliably when tasks were deleted. This happened because the
reactive cancellation via reminders$ observable could fail when:
- App was backgrounded (WebView suspended)
- 5-second startup delay in Android effects
- Race conditions with async callbacks

Now explicitly cancel native reminders in the task deletion effects
(clearRemindersOnDelete$, clearMultipleReminders, clearRemindersForArchivedTasks$)
immediately when tasks are deleted, before the web-side cleanup.

Fixes #5831
2026-01-02 17:05:54 +01:00
Johannes Millan
b2d0319ea2 fix(error-handler): use getErrorTxt to prevent [object Object] in error titles
Replace unsafe string cast `origErr as string` with proper error text
extraction using getErrorTxt utility. This ensures GitHub issue titles
generated from crashes show meaningful error messages instead of
"[object Object]".

Fixes #5822
2026-01-02 16:56:12 +01:00
Johannes Millan
9c0a728ef4 feat(sync-server): replace password auth with passkey + magic link
Authentication changes:
- Add passkey (WebAuthn) as primary login method
- Add email magic link as fallback for devices without passkey support
- Remove password-based authentication entirely

New features:
- Passkey registration and login via @simplewebauthn/server
- Magic link login with 15-minute expiry tokens
- Passkey recovery via email link
- Self-hosted simplewebauthn-browser.min.js for reliability

Database changes:
- Add Passkey model for WebAuthn credentials
- Add PasskeyChallenge model for registration/auth challenges
- Add loginToken and loginTokenExpiresAt fields for magic links
- Add passkeyRecoveryToken fields for passkey recovery

UI changes:
- Login form: email + "Login with Passkey" + "Send Login Link"
- Register form: email + terms checkbox + "Register with Passkey"
- Consistent token display UI for both passkey and magic link login
- Remove password fields and forgot password flow

Security:
- CSP-compliant magic link redirect using external script
- Rate limiting on all auth endpoints
- Single-use magic link tokens
2026-01-02 16:52:48 +01:00
Johannes Millan
4198a6bbc9 fix(planner): use task startDate for weekly repeat weekday calculation
When creating a weekly repeating task with WEEKLY_CURRENT_WEEKDAY quick
setting, the weekday was incorrectly set based on today's date instead
of the task's scheduled date (startDate). This caused tasks scheduled
for future days to not appear in the planner.

The fix passes the startDate to getQuickSettingUpdates() so that the
correct weekday is set based on when the task is scheduled, not when
it was created.

Fixes #5806
2026-01-02 16:25:55 +01:00
Johannes Millan
ce70df4a66 fix(focus-mode): use independent 1s timer for Pomodoro countdown
The Focus Mode timer was using GlobalTrackingIntervalService.tick$
which only emits at the configured trackingInterval (up to 100s).
When users set a high tracking interval to reduce disk writes,
the Pomodoro timer would only update every N seconds.

Now uses its own interval(1000) for smooth 1-second updates,
independent of the global tracking interval setting.

Fixes #5813
2026-01-02 16:23:27 +01:00
Johannes Millan
b9bf655e00 feat(focus-mode): add Skip Break button to banner during active breaks
When using focus mode with the overlay hidden (banner-only mode), users
can now skip an active break directly from the banner. Previously, the
Skip Break button was only available in the full-screen overlay.

Fixes #5818
2026-01-02 16:06:39 +01:00
Johannes Millan
09d0131760 feat(notes): add auto-save to fullscreen markdown editor
Add debounced auto-save (500ms) to the fullscreen task note editor
to prevent data loss. Changes are now saved automatically while typing
and when pressing Escape. Remove the Cancel button since all changes
are preserved.

Fixes #5804
2026-01-02 15:59:55 +01:00
Johannes Millan
4c278812d8 fix(reflection-note): prevent trailing spaces from being deleted while typing
Decoupled textarea input state from persisted store state. The component
now uses a local inputText signal for the textarea binding instead of
directly binding to the store value. This prevents the trimmed store
value from overwriting user input after the 500ms debounce.

Fixes #5800
2026-01-02 15:51:37 +01:00
Johannes Millan
4f2dbcdaa7 feat(sync): handle auth errors for account deletion scenarios
When a SuperSync account is deleted, clients now properly detect
authentication failures and prompt for reconfiguration:

- Add _checkHttpStatus() helper to SuperSyncProvider that throws
  AuthFailSPError on 401/403 responses
- Clear cached credentials on auth failure to allow reconfiguration
- Add DELETE /api/test/user/:userId endpoint for E2E testing
- Add deleteTestUser() helper in supersync-helpers.ts
- Add E2E tests for account deletion and reconfiguration scenarios

The existing SyncWrapperService already handles AuthFailSPError by
showing a snackbar with "Configure" action, so no UI changes needed.
2026-01-02 15:48:17 +01:00