From b911546e53b4352152a2ea1277f6a507c1a2d63e Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 27 Jan 2026 14:44:48 +0100 Subject: [PATCH] fix(sync): improve error visibility in SuperSync operations - Add logging to empty catch block in validation (is-related-model-data-valid.ts) - Notify users when ANY ops are rejected, not just >= 10 (rejected-ops-handler.service.ts) - Change encryption auto-disable snackbar from SUCCESS to WARNING - Add logging when sync-config-default-override.json fails to load - Add DIALOG_RESULT_ERROR translation key for sync dialog errors - Add test for single op rejection notification --- src/app/imex/sync/sync-config.service.ts | 6 ++++- .../op-log/sync/operation-log-sync.service.ts | 4 ++-- .../sync/rejected-ops-handler.service.spec.ts | 24 ++++++++++++++++--- .../sync/rejected-ops-handler.service.ts | 5 ++-- .../validation/is-related-model-data-valid.ts | 4 +++- src/app/t.const.ts | 1 + src/assets/i18n/en.json | 1 + 7 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/app/imex/sync/sync-config.service.ts b/src/app/imex/sync/sync-config.service.ts index f5a2a59781..b81dd832fb 100644 --- a/src/app/imex/sync/sync-config.service.ts +++ b/src/app/imex/sync/sync-config.service.ts @@ -132,7 +132,11 @@ export class SyncConfigService { encryptKey: '', }; }) - .catch(() => { + .catch((e) => { + SyncLog.warn( + 'Failed to load sync-config-default-override.json, using base config:', + e, + ); return { ...baseConfig, encryptKey: '', diff --git a/src/app/op-log/sync/operation-log-sync.service.ts b/src/app/op-log/sync/operation-log-sync.service.ts index 715f9f3400..fd73ff5e6f 100644 --- a/src/app/op-log/sync/operation-log-sync.service.ts +++ b/src/app/op-log/sync/operation-log-sync.service.ts @@ -831,9 +831,9 @@ export class OperationLogSyncService { 'OperationLogSyncService: Local encryption config updated to match server state.', ); - // Notify user + // Notify user - use WARNING since this is a security-relevant change this.snackService.open({ - type: 'SUCCESS', + type: 'WARNING', msg: T.F.SYNC.S.ENCRYPTION_DISABLED_ON_OTHER_DEVICE, }); } diff --git a/src/app/op-log/sync/rejected-ops-handler.service.spec.ts b/src/app/op-log/sync/rejected-ops-handler.service.spec.ts index 0cb3e12ddd..e089987733 100644 --- a/src/app/op-log/sync/rejected-ops-handler.service.spec.ts +++ b/src/app/op-log/sync/rejected-ops-handler.service.spec.ts @@ -8,7 +8,6 @@ import { OperationLogStoreService } from '../persistence/operation-log-store.ser import { SnackService } from '../../core/snack/snack.service'; import { StaleOperationResolverService } from './stale-operation-resolver.service'; import { Operation, OpType, ActionType } from '../core/operation.types'; -import { MAX_REJECTED_OPS_BEFORE_WARNING } from '../core/operation-log.const'; import { T } from '../../t.const'; describe('RejectedOpsHandlerService', () => { @@ -110,9 +109,28 @@ describe('RejectedOpsHandlerService', () => { expect(opLogStoreSpy.markRejected).toHaveBeenCalledWith(['op-1']); }); - it('should show snack warning when many ops are rejected', async () => { + it('should show snack warning when even 1 op is rejected', async () => { + const op = createOp({ id: 'op-1' }); + opLogStoreSpy.getOpById.and.returnValue(Promise.resolve(mockEntry(op))); + opLogStoreSpy.markRejected.and.resolveTo(); + + await service.handleRejectedOps([ + { opId: 'op-1', error: 'validation error', errorCode: 'VALIDATION_ERROR' }, + ]); + + expect(snackServiceSpy.open).toHaveBeenCalledWith( + jasmine.objectContaining({ + type: 'ERROR', + msg: T.F.SYNC.S.UPLOAD_OPS_REJECTED, + translateParams: { count: 1 }, + }), + ); + }); + + it('should show snack warning with correct count when multiple ops are rejected', async () => { + const opCount = 10; const rejectedOps: Array<{ opId: string; error: string }> = []; - for (let i = 0; i < MAX_REJECTED_OPS_BEFORE_WARNING; i++) { + for (let i = 0; i < opCount; i++) { const op = createOp({ id: `op-${i}` }); opLogStoreSpy.getOpById .withArgs(`op-${i}`) diff --git a/src/app/op-log/sync/rejected-ops-handler.service.ts b/src/app/op-log/sync/rejected-ops-handler.service.ts index e14a17496b..f96cc125cb 100644 --- a/src/app/op-log/sync/rejected-ops-handler.service.ts +++ b/src/app/op-log/sync/rejected-ops-handler.service.ts @@ -4,7 +4,6 @@ import { Operation, VectorClock } from '../core/operation.types'; import { OpLog } from '../../core/log'; import { SnackService } from '../../core/snack/snack.service'; import { T } from '../../t.const'; -import { MAX_REJECTED_OPS_BEFORE_WARNING } from '../core/operation-log.const'; import { StaleOperationResolverService } from './stale-operation-resolver.service'; import { DownloadCallback, RejectedOpInfo } from '../core/types/sync-results.types'; import { handleStorageQuotaError } from './sync-error-utils'; @@ -166,8 +165,8 @@ export class RejectedOpsHandlerService { `RejectedOpsHandlerService: Marked ${permanentlyRejectedOps.length} server-rejected ops as rejected`, ); - // Notify user if significant number of ops were rejected without conflict resolution - if (permanentlyRejectedOps.length >= MAX_REJECTED_OPS_BEFORE_WARNING) { + // Notify user when any ops are permanently rejected + if (permanentlyRejectedOps.length > 0) { this.snackService.open({ type: 'ERROR', msg: T.F.SYNC.S.UPLOAD_OPS_REJECTED, diff --git a/src/app/op-log/validation/is-related-model-data-valid.ts b/src/app/op-log/validation/is-related-model-data-valid.ts index d9c2631624..5a91ffda6b 100644 --- a/src/app/op-log/validation/is-related-model-data-valid.ts +++ b/src/app/op-log/validation/is-related-model-data-valid.ts @@ -104,7 +104,9 @@ const _validityError = (errTxt: string, additionalInfo?: any): void => { if (environment.production) { try { PFLog.log('Validity Error Info string: ', JSON.stringify(additionalInfo)); - } catch (e) {} + } catch (e) { + PFLog.warn('Failed to stringify validity error info:', e); + } } } if (errorCount <= 3) { diff --git a/src/app/t.const.ts b/src/app/t.const.ts index 0bbd996aad..fddc71c499 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -1145,6 +1145,7 @@ const T = { CLOCK_DRIFT_WARNING: 'F.SYNC.S.CLOCK_DRIFT_WARNING', COMPACTION_FAILED: 'F.SYNC.S.COMPACTION_FAILED', CONFLICT_RESOLUTION_FAILED: 'F.SYNC.S.CONFLICT_RESOLUTION_FAILED', + DIALOG_RESULT_ERROR: 'F.SYNC.S.DIALOG_RESULT_ERROR', CONFLICT_DIALOG_TIMEOUT: 'F.SYNC.S.CONFLICT_DIALOG_TIMEOUT', OPERATION_PERMANENTLY_FAILED: 'F.SYNC.S.OPERATION_PERMANENTLY_FAILED', DATA_REPAIRED: 'F.SYNC.S.DATA_REPAIRED', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index d09b667083..663fbf211d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1107,6 +1107,7 @@ "CLOCK_DRIFT_WARNING": "Your device clock appears to be off by {{minutes}} minutes. This may cause sync issues.", "COMPACTION_FAILED": "Database cleanup failed. App may slow down.", "CONFLICT_RESOLUTION_FAILED": "Sync conflict resolution failed. Please reload.", + "DIALOG_RESULT_ERROR": "An error occurred while processing the sync dialog. Please try again.", "CONFLICT_DIALOG_TIMEOUT": "Sync conflict dialog timed out and was cancelled. Sync will retry on next trigger.", "OPERATION_PERMANENTLY_FAILED": "Some sync operations failed and could not be applied. Data may be incomplete.", "DATA_REPAIRED": "Data automatically repaired ({{count}} issues fixed)",