diff --git a/docs/plainspace-integration-plan.md b/docs/plainspace-integration-plan.md index 17425c90bc..e29b089dd5 100644 --- a/docs/plainspace-integration-plan.md +++ b/docs/plainspace-integration-plan.md @@ -149,7 +149,7 @@ export interface PlainspaceCfg extends BaseIssueProviderCfg { > deliberate parity choice (a provider works on a fresh device after sync without > re-pasting), and is the accepted secret-handling posture for issue providers. > The account store (§3.3, local-only `localStorage`) holds a token **too**, but -> only to bootstrap the "Share on Plainspace" flow, which needs a token *before* +> only to bootstrap the "Share on Plainspace" flow, which needs a token _before_ > any provider exists; the provider runtime reads only `cfg.token`. An earlier > draft said the token was not stored in the cfg — that was never the case in the > shipped code. diff --git a/docs/sync-and-op-log/file-based-sync-flowchart.md b/docs/sync-and-op-log/file-based-sync-flowchart.md index 5444d4eef6..b7c1d3fdca 100644 --- a/docs/sync-and-op-log/file-based-sync-flowchart.md +++ b/docs/sync-and-op-log/file-based-sync-flowchart.md @@ -121,9 +121,6 @@ flowchart LR TYPE -->|DecryptNoPasswordError| PWD_DLG[Enter Password dialog] TYPE -->|DecryptError| DEC_DLG[Decrypt Error dialog] TYPE -->|LocalDataConflictError| CONF_DLG[SyncConflictDialog] - TYPE -->|RevMismatchForModelError
NoRemoteModelFile| INC_DLG["Incomplete sync" dialog:
Force Upload / Force Download] - TYPE -->|SyncInvalidTimeValuesError| TIME_DLG["Incoherent timestamps" dialog:
Force Upload / Force Download] - TYPE -->|LockPresentError| LOCK[Snackbar + Force Overwrite action] TYPE -->|PotentialCorsError| CORS[CORS error snackbar] TYPE -->|AuthFail / MissingCredentials| AUTH[Auth error snackbar
+ Configure action] TYPE -->|WebCryptoNotAvailable| CRYPTO[WebCrypto snackbar] @@ -134,7 +131,7 @@ flowchart LR classDef dialog fill:#48f,stroke:#26d,color:#fff classDef snack fill:#f90,stroke:#b60,color:#fff - class PWD_DLG,DEC_DLG,CONF_DLG,INC_DLG,TIME_DLG dialog + class PWD_DLG,DEC_DLG,CONF_DLG dialog class LOCK,CORS,AUTH,CRYPTO,TIMEOUT,PERM,GENERIC snack ``` @@ -148,16 +145,16 @@ flowchart LR ## Key Differences from SuperSync -| Aspect | File-Based (Dropbox, WebDAV, LocalFile) | SuperSync | -|--------|----------------------------------------|-----------| -| **Transport** | Downloads/uploads a single `sync-data.json` file | Paginated API (server-side op log) | -| **Snapshot path** | Full `snapshotState` on seq 0 download, with its own conflict-checking flow | No snapshot concept — all ops are incremental | -| **Gap detection** | Adapter detects syncVersion reset / snapshot replacement / partial trimming → re-download from seq 0 | Server handles gap detection internally | -| **Server migration** | Gap on empty server → `needsFullStateUpload` → `handleServerMigration()` | Same concept but detected via different mechanism | -| **Upload retry** | Rev matching (ETag) + exponential backoff with jitter | Server rejection codes (`CONFLICT_CONCURRENT`) | -| **Piggybacking** | Not applicable — no server to piggyback. Concurrent changes are discovered on re-download during retry. | Server returns piggybacked ops in upload response | -| **Post-sync encryption prompt** | Not applicable | Prompts user to set password or disable sync | -| **File-based error types** | `RevMismatchForModelError`, `NoRemoteModelFile`, `SyncInvalidTimeValuesError`, `LockPresentError`, `PotentialCorsError` | Not applicable | +| Aspect | File-Based (Dropbox, WebDAV, LocalFile) | SuperSync | +| ------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| **Transport** | Downloads/uploads a single `sync-data.json` file | Paginated API (server-side op log) | +| **Snapshot path** | Full `snapshotState` on seq 0 download, with its own conflict-checking flow | No snapshot concept — all ops are incremental | +| **Gap detection** | Adapter detects syncVersion reset / snapshot replacement / partial trimming → re-download from seq 0 | Server handles gap detection internally | +| **Server migration** | Gap on empty server → `needsFullStateUpload` → `handleServerMigration()` | Same concept but detected via different mechanism | +| **Upload retry** | Rev matching (ETag) + exponential backoff with jitter | Server rejection codes (`CONFLICT_CONCURRENT`) | +| **Piggybacking** | Not applicable — no server to piggyback. Concurrent changes are discovered on re-download during retry. | Server returns piggybacked ops in upload response | +| **Post-sync encryption prompt** | Not applicable | Prompts user to set password or disable sync | +| **File-based error types** | `PotentialCorsError`, `LegacySyncFormatDetectedError`, `SyncDataCorruptedError` | Not applicable | ## Notes @@ -169,9 +166,9 @@ flowchart LR ## Key Source Files -| File | Role | -|------|------| -| `src/app/imex/sync/sync-wrapper.service.ts` | Top-level orchestration + error handling | -| `src/app/op-log/sync/operation-log-sync.service.ts` | Download/upload orchestration, fresh client checks, SYNC_IMPORT handling | -| `src/app/op-log/sync/operation-log-download.service.ts` | Download + internal gap detection | -| `src/app/op-log/sync-providers/file-based/file-based-sync-adapter.service.ts` | File adapter (rev matching, gap detection, snapshot upload) | +| File | Role | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `src/app/imex/sync/sync-wrapper.service.ts` | Top-level orchestration + error handling | +| `src/app/op-log/sync/operation-log-sync.service.ts` | Download/upload orchestration, fresh client checks, SYNC_IMPORT handling | +| `src/app/op-log/sync/operation-log-download.service.ts` | Download + internal gap detection | +| `src/app/op-log/sync-providers/file-based/file-based-sync-adapter.service.ts` | File adapter (rev matching, gap detection, snapshot upload) | diff --git a/electron/common-const.test.cjs b/electron/common-const.test.cjs index 39e594f7da..a49b84e6b4 100644 --- a/electron/common-const.test.cjs +++ b/electron/common-const.test.cjs @@ -4,11 +4,9 @@ const path = require('node:path'); require('ts-node/register/transpile-only'); -const { - isGnomeDesktopEnv, - isWaylandEnv, - isGnomeWaylandEnv, -} = require(path.resolve(__dirname, 'common.const.ts')); +const { isGnomeDesktopEnv, isWaylandEnv, isGnomeWaylandEnv } = require( + path.resolve(__dirname, 'common.const.ts'), +); test('isGnomeDesktopEnv: detects GNOME from desktop env vars', () => { assert.equal(isGnomeDesktopEnv('linux', { XDG_CURRENT_DESKTOP: 'GNOME' }), true); @@ -56,7 +54,10 @@ test('isGnomeWaylandEnv: only true for GNOME AND Wayland together', () => { ); // non-GNOME Wayland (e.g. KDE) is unaffected assert.equal( - isGnomeWaylandEnv('linux', { XDG_CURRENT_DESKTOP: 'KDE', XDG_SESSION_TYPE: 'wayland' }), + isGnomeWaylandEnv('linux', { + XDG_CURRENT_DESKTOP: 'KDE', + XDG_SESSION_TYPE: 'wayland', + }), false, ); }); diff --git a/electron/indicator.test.cjs b/electron/indicator.test.cjs index 46e91ac8e1..bcc1570390 100644 --- a/electron/indicator.test.cjs +++ b/electron/indicator.test.cjs @@ -254,10 +254,7 @@ test('initIndicator uses NativeImage for Linux tray creation and updates', () => // On Linux the running icon stays static (no progress-animation frames) to // avoid StatusNotifierItem flicker (#4905). assert.equal(traySetImageCalls.at(-1).kind, 'native-image'); - assert.match( - traySetImageCalls.at(-1).iconPath, - /\/icons\/indicator\/running-d\.png$/, - ); + assert.match(traySetImageCalls.at(-1).iconPath, /\/icons\/indicator\/running-d\.png$/); beforeQuitHandler(); }); diff --git a/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.html b/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.html deleted file mode 100644 index 13f5409434..0000000000 --- a/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.html +++ /dev/null @@ -1,90 +0,0 @@ - -
- -
- - @if (data.type === 'incomplete-sync') { -

- {{ T.F.SYNC.D_INCOMPLETE_SYNC.P1 | translate }} -

- -

- {{ T.F.SYNC.D_INCOMPLETE_SYNC.P2 | translate }} - {{ data.modelId }} -

- -

{{ T.F.SYNC.D_INCOMPLETE_SYNC.T3 | translate }}

- -

{{ T.F.SYNC.D_INCOMPLETE_SYNC.T4 | translate }}

-

{{ T.F.SYNC.D_INCOMPLETE_SYNC.T5 | translate }}

- -

- {{ T.F.SYNC.D_INCOMPLETE_SYNC.T6 | translate }} -

- } @else { -

- {{ T.F.SYNC.D_INCOMPLETE_SYNC.INCOHERENT_P1 | translate }} -

-

{{ T.F.SYNC.D_INCOMPLETE_SYNC.INCOHERENT_P2 | translate }}

- } - -
- -
-
- - -
- - - @if (data.type === 'incomplete-sync' && !IS_ANDROID_WEB_VIEW) { - - } - - @if (data.type === 'incoherent-timestamps') { - - } - - -
-
diff --git a/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.scss b/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.scss deleted file mode 100644 index 531f19f41e..0000000000 --- a/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.scss +++ /dev/null @@ -1,7 +0,0 @@ -:host > * { - max-width: 500px; -} - -:host code { - font-size: 10px; -} diff --git a/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.ts b/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.ts deleted file mode 100644 index 2e46c9ceb9..0000000000 --- a/src/app/imex/sync/dialog-sync-error/dialog-sync-error.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { - MAT_DIALOG_DATA, - MatDialogActions, - MatDialogContent, - MatDialogRef, -} from '@angular/material/dialog'; -import { TranslateModule } from '@ngx-translate/core'; -import { download } from '../../../util/download'; -import { - BACKUP_FILENAME_PREFIX, - getBackupTimestamp, -} from '../../../../../electron/shared-with-frontend/get-backup-timestamp'; - -import { IS_ELECTRON } from '../../../app.constants'; -import { IS_ANDROID_WEB_VIEW } from '../../../util/is-android-web-view'; -import { MatIcon } from '@angular/material/icon'; -import { MatButton } from '@angular/material/button'; -import { BackupService } from '../../../op-log/backup/backup.service'; -import { T } from '../../../t.const'; -import { Log } from '../../../core/log'; - -export type DialogSyncErrorType = 'incomplete-sync' | 'incoherent-timestamps'; - -export interface DialogSyncErrorData { - type: DialogSyncErrorType; - modelId?: string; -} - -export type DialogSyncErrorResult = - | 'FORCE_UPDATE_REMOTE' - | 'FORCE_UPDATE_LOCAL' - | undefined; - -@Component({ - selector: 'dialog-sync-error', - imports: [MatDialogContent, TranslateModule, MatIcon, MatDialogActions, MatButton], - templateUrl: './dialog-sync-error.component.html', - styleUrl: './dialog-sync-error.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DialogSyncErrorComponent { - private _matDialogRef = inject>(MatDialogRef); - private _backupService = inject(BackupService); - - data = inject(MAT_DIALOG_DATA); - - T: typeof T = T; - IS_ANDROID_WEB_VIEW = IS_ANDROID_WEB_VIEW; - - constructor() { - this._matDialogRef.disableClose = true; - } - - async downloadBackup(): Promise { - const data = await this._backupService.loadCompleteBackup(true); - try { - const fileName = `${BACKUP_FILENAME_PREFIX}_${getBackupTimestamp()}.json`; - await download(fileName, JSON.stringify(data)); - } catch (e) { - Log.error(e); - } - } - - close(res?: DialogSyncErrorResult): void { - this._matDialogRef.close(res); - } - - closeApp(): void { - if (IS_ELECTRON) { - window.ea.shutdownNow(); - } else { - window.close(); - } - } -} diff --git a/src/app/imex/sync/sync-wrapper.service.ts b/src/app/imex/sync/sync-wrapper.service.ts index 79c1baddef..e025b9ec9d 100644 --- a/src/app/imex/sync/sync-wrapper.service.ts +++ b/src/app/imex/sync/sync-wrapper.service.ts @@ -36,13 +36,9 @@ import { ConflictReason, DecryptError, DecryptNoPasswordError, - LockPresentError, MissingCredentialsSPError, NetworkUnavailableSPError, - NoRemoteModelFile, PotentialCorsError, - RevMismatchForModelError, - SyncInvalidTimeValuesError, SyncProviderId, SyncStatus, toSyncProviderId, @@ -58,10 +54,6 @@ import { ReminderService } from '../../features/reminder/reminder.service'; import { DialogHandleDecryptErrorComponent } from './dialog-handle-decrypt-error/dialog-handle-decrypt-error.component'; import { DialogEnterEncryptionPasswordComponent } from './dialog-enter-encryption-password/dialog-enter-encryption-password.component'; -import { - DialogSyncErrorComponent, - DialogSyncErrorResult, -} from './dialog-sync-error/dialog-sync-error.component'; import { SyncLog } from '../../core/log'; import { promiseTimeout } from '../../util/promise-timeout'; import { devError } from '../../util/dev-error'; @@ -90,11 +82,9 @@ type CompletedUploadOutcome = Extract; * back to its origin without diff-archaeology. */ export type ForceUploadTriggerSource = - | 'LockPresentError' | 'EmptyRemoteBodySPError' | 'JsonParseError' | 'LegacySyncFormatDetectedError' - | 'DialogSyncError' | 'DecryptError' | 'unknown'; @@ -719,29 +709,6 @@ export class SyncWrapperService { }); } return 'HANDLED_ERROR'; - } else if (error instanceof SyncInvalidTimeValuesError) { - // Handle async dialog result properly to avoid silent error swallowing - this._handleIncoherentTimestampsDialog(); - return 'HANDLED_ERROR'; - } else if ( - error instanceof RevMismatchForModelError || - error instanceof NoRemoteModelFile - ) { - SyncLog.log(error, Object.keys(error)); - // Extract modelId safely with proper type validation - const modelId = this._extractModelIdFromError(error); - // Handle async dialog result properly to avoid silent error swallowing - this._handleIncompleteSyncDialog(modelId); - return 'HANDLED_ERROR'; - } else if (error instanceof LockPresentError) { - this._snackService.open({ - // TODO translate - msg: T.F.SYNC.S.ERROR_DATA_IS_CURRENTLY_WRITTEN, - type: 'ERROR', - actionFn: async () => this.forceUpload('LockPresentError'), - actionStr: T.F.SYNC.S.BTN_FORCE_OVERWRITE, - }); - return 'HANDLED_ERROR'; } else if (error instanceof EmptyRemoteBodySPError) { // Remote file returned an empty body (e.g. Koofr WebDAV corrupted file). // Force overwrite is safe: local data is intact, remote is empty. @@ -965,7 +932,7 @@ export class SyncWrapperService { await this.runWithSyncBlocked(async () => { // #8309: claim the sync cycle so this flow's setLastServerSeq bookkeeping // and session-validation latch are isolated from the immediate-upload / - // WS-download side channels, mirroring _forceDownload. runWithSyncBlocked + // WS-download side channels. runWithSyncBlocked // has already drained any main sync and set isEncryptionOperationInProgress // (which the side channels honour), so the only thing that could hold the // guard here is a short-lived side channel; skip-and-let-the-user-retry @@ -1002,65 +969,6 @@ export class SyncWrapperService { }); } - private async _forceDownload(): Promise { - SyncLog.log('SyncWrapperService: forceDownload called - downloading remote state'); - - await this.runWithSyncBlocked(async () => { - // #8309: claim the sync cycle so this flow's conflict-gate / session - // latch are isolated from the immediate-upload / WS-download side - // channels. runWithSyncBlocked has already drained any main sync and set - // isEncryptionOperationInProgress, so the only thing that could hold the - // guard here is a short-lived side channel; skip-and-let-the-user-retry - // rather than block. - if (!this._syncCycleGuard.tryBegin()) { - SyncLog.log('Force download skipped: another sync cycle is in progress'); - return; - } - try { - await this._forceDownloadInner(); - } finally { - this._syncCycleGuard.end(); - } - }); - } - - private async _forceDownloadInner(): Promise { - // Open a session-validation scope — read after forceDownloadRemoteState - // returns so a corrupt downloaded state is reported as ERROR. (#7330) - await this._sessionValidation.withSession(async () => { - try { - const rawProvider = this._providerManager.getActiveProvider(); - const syncCapableProvider = - await this._wrappedProvider.getOperationSyncCapable(rawProvider); - - if (!syncCapableProvider) { - SyncLog.warn( - 'SyncWrapperService: Cannot force download - provider not available', - ); - return; - } - - await this._opLogSyncService.forceDownloadRemoteState(syncCapableProvider); - if (this._sessionValidation.hasFailed()) { - SyncLog.err( - 'SyncWrapperService: Force download applied but post-sync validation failed; reporting ERROR', - ); - this._providerManager.setSyncStatus('ERROR'); - } else { - this._providerManager.setSyncStatus('IN_SYNC'); - } - SyncLog.log('SyncWrapperService: Force download complete'); - } catch (error) { - SyncLog.err('SyncWrapperService: Force download failed:', error); - const errStr = getSyncErrorStr(error); - this._snackService.open({ - msg: errStr, - type: 'ERROR', - }); - } - }); - } - async configuredAuthForSyncProviderIfNecessary( providerId: SyncProviderId, force = false, @@ -1127,70 +1035,12 @@ export class SyncWrapperService { return { wasConfigured: false }; } - /** - * Handle incoherent timestamps dialog with proper async error handling. - * Uses fire-and-forget pattern but logs errors instead of swallowing them. - */ private async _openSyncCfgDialog(): Promise { const { DialogSyncCfgComponent } = await import('./dialog-sync-cfg/dialog-sync-cfg.component'); this._matDialog.open(DialogSyncCfgComponent); } - private _handleIncoherentTimestampsDialog(): void { - this._openSyncErrorDialog({ type: 'incoherent-timestamps' }); - } - - private _handleIncompleteSyncDialog(modelId: string | undefined): void { - this._openSyncErrorDialog({ type: 'incomplete-sync', modelId }); - } - - private _openSyncErrorDialog(data: { - type: 'incomplete-sync' | 'incoherent-timestamps'; - modelId?: string; - }): void { - const dialogRef = this._matDialog.open(DialogSyncErrorComponent, { - data, - disableClose: true, - }); - - firstValueFrom(dialogRef.afterClosed()) - .then(async (res: DialogSyncErrorResult) => { - if (res === 'FORCE_UPDATE_REMOTE') { - await this.forceUpload('DialogSyncError'); - } else if (res === 'FORCE_UPDATE_LOCAL') { - await this._forceDownload(); - } - }) - .catch((err) => { - SyncLog.err('Error handling sync error dialog result:', err); - this._snackService.open({ - type: 'ERROR', - msg: T.F.SYNC.S.DIALOG_RESULT_ERROR, - }); - }); - } - - /** - * Safely extract modelId from error with proper type validation. - */ - private _extractModelIdFromError( - error: RevMismatchForModelError | NoRemoteModelFile, - ): string | undefined { - if (!error.additionalLog) { - return undefined; - } - // Handle both array and string formats - if (Array.isArray(error.additionalLog) && error.additionalLog.length > 0) { - const firstItem = error.additionalLog[0]; - return typeof firstItem === 'string' ? firstItem : undefined; - } - if (typeof error.additionalLog === 'string') { - return error.additionalLog; - } - return undefined; - } - /** * Handles missing encryption password when receiving encrypted data. * Opens a simple dialog to prompt for the password, then re-syncs. diff --git a/src/app/op-log/core/errors/sync-errors.spec.ts b/src/app/op-log/core/errors/sync-errors.spec.ts index 2e468554c7..9d273e9c1b 100644 --- a/src/app/op-log/core/errors/sync-errors.spec.ts +++ b/src/app/op-log/core/errors/sync-errors.spec.ts @@ -51,20 +51,16 @@ describe('sync errors', () => { expect((OpLog.log as jasmine.Spy).calls.count()).toBe(0); }); - it('does not log JSON parse data samples or raw original errors', () => { + it('does not log on construction for JsonParseError (privacy invariant)', () => { new JsonParseError( new SyntaxError('Unexpected token SECRET at position 6'), '{"a":"secret value"}', ); - const errText = JSON.stringify((OpLog.err as jasmine.Spy).calls.allArgs()); - expect(errText).toContain('JsonParseError'); - expect(errText).toContain('dataLength'); - expect(errText).not.toContain('SECRET'); - expect(errText).not.toContain('secret value'); + expect((OpLog.err as jasmine.Spy).calls.count()).toBe(0); }); - it('logs model validation diagnostics without validation payloads', () => { + it('does not log on construction for ModelValidationError (privacy invariant)', () => { const validationResult = { success: false, errors: [ @@ -83,15 +79,10 @@ describe('sync errors', () => { e: new Error('secret validation failure'), }); - const logText = JSON.stringify((OpLog.log as jasmine.Spy).calls.allArgs()); - expect(logText).toContain('ModelValidationError'); - expect(logText).toContain('task-id-1'); - expect(logText).toContain('validationErrorCount'); - expect(logText).not.toContain('secret title'); - expect(logText).not.toContain('secret validation failure'); + expect((OpLog.log as jasmine.Spy).calls.count()).toBe(0); }); - it('logs data validation diagnostics without validation payloads', () => { + it('does not log on construction for DataValidationFailedError (privacy invariant)', () => { const validationResult = { success: false, errors: [ @@ -105,10 +96,6 @@ describe('sync errors', () => { new DataValidationFailedError(validationResult); - const logText = JSON.stringify((OpLog.log as jasmine.Spy).calls.allArgs()); - expect(logText).toContain('DataValidationFailedError'); - expect(logText).toContain('validationErrorCount'); - expect(logText).toContain('$input.notes'); - expect(logText).not.toContain('secret note text'); + expect((OpLog.log as jasmine.Spy).calls.count()).toBe(0); }); }); diff --git a/src/app/op-log/core/errors/sync-errors.ts b/src/app/op-log/core/errors/sync-errors.ts index e62d5e0508..135e7e5ba6 100644 --- a/src/app/op-log/core/errors/sync-errors.ts +++ b/src/app/op-log/core/errors/sync-errors.ts @@ -1,12 +1,10 @@ import { IValidation } from 'typia'; import type { SyncFilePrefixInvalidPrefixDetails } from '@sp/sync-core'; -import { toSyncLogError } from '@sp/sync-core'; import { AdditionalLogErrorBase as PackageAdditionalLogErrorBase, extractErrorMessage as packageExtractErrorMessage, } from '@sp/sync-providers/errors'; import { FILE_BASED_SYNC_CONSTANTS } from '../../sync-providers/file-based/file-based-sync.types'; -import { OP_LOG_SYNC_LOGGER } from '../sync-logger.adapter'; // Re-export provider-shared error classes from @sp/sync-providers. // Single class definition per error is critical for `instanceof` checks @@ -46,20 +44,6 @@ const getValidationErrors = ( return undefined; }; -const getValidationErrorPathSummary = ( - validationResult?: IValidation, -): string | undefined => { - const errors = getValidationErrors(validationResult); - if (!errors) return undefined; - - const pathSummary = errors - .slice(0, 3) - .map((error) => error.path) - .filter(Boolean) - .join(', '); - return pathSummary || undefined; -}; - // AdditionalLogErrorBase is provided by @sp/sync-providers (without the // previous constructor-time logging side effect). The remaining app-only // errors below extend it; they MUST log at the catch site via @@ -73,16 +57,6 @@ export class ImpossibleError extends Error { override name = ' ImpossibleError'; } -// --------------APP-SIDE-ONLY API ERRORS-------------- - -export class NoEtagAPIError extends AdditionalLogErrorBase { - override name = ' NoEtagAPIError'; -} - -export class FileExistsAPIError extends Error { - override name = ' FileExistsAPIError'; -} - // --------------OTHER SYNC ERRORS-------------- export class NoSyncProviderSetError extends Error { override name = 'NoSyncProviderSetError'; @@ -127,43 +101,10 @@ export class LockAcquisitionTimeoutError extends Error { } } -export class RevMismatchForModelError extends AdditionalLogErrorBase { - override name = 'RevMismatchForModelError'; -} - export class UnknownSyncStateError extends Error { override name = 'UnknownSyncStateError'; } -export class SyncInvalidTimeValuesError extends AdditionalLogErrorBase { - override name = 'SyncInvalidTimeValuesError'; -} - -export class RevMapModelMismatchErrorOnDownload extends AdditionalLogErrorBase { - override name = 'RevMapModelMismatchErrorOnDownload'; -} - -export class RevMapModelMismatchErrorOnUpload extends AdditionalLogErrorBase { - override name = 'RevMapModelMismatchErrorOnUpload'; -} - -export class NoRemoteModelFile extends AdditionalLogErrorBase { - override name = 'NoRemoteModelFile'; -} - -export class NoRemoteMetaFile extends Error { - override name = 'NoRemoteMetaFile'; -} - -// --------------LOCKFILE ERRORS-------------- -export class LockPresentError extends Error { - override name = 'LockPresentError'; -} - -export class LockFromLocalClientPresentError extends Error { - override name = 'LockFromLocalClientPresentError'; -} - // -----ENCRYPTION & COMPRESSION---- export class DecryptNoPasswordError extends AdditionalLogErrorBase { override name = 'DecryptNoPasswordError'; @@ -238,12 +179,6 @@ export class JsonParseError extends Error { const end = Math.min(dataStr.length, position + 50); this.dataSample = `...${dataStr.substring(start, end)}...`; } - - OP_LOG_SYNC_LOGGER.err('JsonParseError', toSyncLogError(originalError), { - position: this.position, - dataLength: dataStr?.length, - hasDataSample: this.dataSample !== undefined, - }); } } @@ -260,14 +195,6 @@ export class InvalidMetaError extends AdditionalLogErrorBase { override name = 'InvalidMetaError'; } -export class MetaNotReadyError extends AdditionalLogErrorBase { - override name = 'MetaNotReadyError'; -} - -export class InvalidRevMapError extends AdditionalLogErrorBase { - override name = 'InvalidRevMapError'; -} - export class ModelIdWithoutCtrlError extends AdditionalLogErrorBase { override name = 'ModelIdWithoutCtrlError'; } @@ -303,15 +230,6 @@ export class ModelValidationError extends Error { e?: unknown; }) { super('ModelValidationError'); - OP_LOG_SYNC_LOGGER.log('ModelValidationError', { - id: params.id, - hasValidationResult: params.validationResult !== undefined, - validationErrorCount: getValidationErrors(params.validationResult)?.length, - validationPathSummary: getValidationErrorPathSummary(params.validationResult), - hasAdditionalError: params.e !== undefined, - additionalErrorName: - params.e !== undefined ? toSyncLogError(params.e).name : undefined, - }); if (params.validationResult) { try { @@ -320,12 +238,8 @@ export class ModelValidationError extends Error { const str = JSON.stringify(errors); this.additionalLog = `Model: ${params.id}, Errors: ${str.substring(0, 400)}`; } - } catch (e) { - OP_LOG_SYNC_LOGGER.err( - 'Error stringifying validation errors', - toSyncLogError(e), - { id: params.id }, - ); + } catch { + // Ignore stringification errors } } } @@ -338,10 +252,6 @@ export class DataValidationFailedError extends Error { constructor(validationResult: IValidation) { const errorSummary = DataValidationFailedError._buildErrorSummary(validationResult); super(errorSummary); - OP_LOG_SYNC_LOGGER.log('DataValidationFailedError', { - validationErrorCount: getValidationErrors(validationResult)?.length, - validationPathSummary: getValidationErrorPathSummary(validationResult), - }); try { const errors = getValidationErrors(validationResult); @@ -349,8 +259,8 @@ export class DataValidationFailedError extends Error { const str = JSON.stringify(errors); this.additionalLog = str.substring(0, 400); } - } catch (e) { - OP_LOG_SYNC_LOGGER.err('Failed to stringify validation errors', toSyncLogError(e)); + } catch { + // Ignore stringification errors } } diff --git a/src/app/op-log/sync-exports.ts b/src/app/op-log/sync-exports.ts index 95f99c6c85..0119545af6 100644 --- a/src/app/op-log/sync-exports.ts +++ b/src/app/op-log/sync-exports.ts @@ -45,11 +45,7 @@ export { SyncAlreadyInProgressError, LockAcquisitionTimeoutError, CanNotMigrateMajorDownError, - LockPresentError, - NoRemoteModelFile, PotentialCorsError, - RevMismatchForModelError, - SyncInvalidTimeValuesError, } from './core/errors/sync-errors'; // Provider interfaces diff --git a/src/app/op-log/sync/sync-cycle-guard.service.ts b/src/app/op-log/sync/sync-cycle-guard.service.ts index 0b1d52cd1e..648ab2ac7e 100644 --- a/src/app/op-log/sync/sync-cycle-guard.service.ts +++ b/src/app/op-log/sync/sync-cycle-guard.service.ts @@ -1,9 +1,8 @@ import { Injectable } from '@angular/core'; /** - * In-tab mutual-exclusion guard for the four top-level sync entry points: + * In-tab mutual-exclusion guard for the three top-level sync entry points: * - `SyncWrapperService.sync()` (periodic / user-triggered full sync) - * - `SyncWrapperService._forceDownload()` (sync-error dialog recovery) * - `ImmediateUploadService._performUpload()` (side channel) * - `WsTriggeredDownloadService._downloadOps()` (side channel) * diff --git a/src/app/op-log/sync/sync-session-validation.service.ts b/src/app/op-log/sync/sync-session-validation.service.ts index 5e02bca2d5..6c2eb6e0af 100644 --- a/src/app/op-log/sync/sync-session-validation.service.ts +++ b/src/app/op-log/sync/sync-session-validation.service.ts @@ -7,7 +7,6 @@ import { SyncLog } from '../../core/log'; * * A "sync session" is a single top-level sync operation: * - `SyncWrapperService._sync()` - * - `SyncWrapperService._forceDownload()` * - `WsTriggeredDownloadService._downloadOps()` * - `ImmediateUploadService._performUpload()` * diff --git a/src/app/t.const.ts b/src/app/t.const.ts index c026d22a75..e5a96d372b 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -1274,20 +1274,6 @@ const T = { OK: 'F.SYNC.D_FRESH_CLIENT_CONFIRM.OK', TITLE: 'F.SYNC.D_FRESH_CLIENT_CONFIRM.TITLE', }, - D_INCOMPLETE_SYNC: { - BTN_CLOSE_APP: 'F.SYNC.D_INCOMPLETE_SYNC.BTN_CLOSE_APP', - BTN_DOWNLOAD_BACKUP: 'F.SYNC.D_INCOMPLETE_SYNC.BTN_DOWNLOAD_BACKUP', - BTN_FORCE_DOWNLOAD: 'F.SYNC.D_INCOMPLETE_SYNC.BTN_FORCE_DOWNLOAD', - BTN_FORCE_UPLOAD: 'F.SYNC.D_INCOMPLETE_SYNC.BTN_FORCE_UPLOAD', - INCOHERENT_P1: 'F.SYNC.D_INCOMPLETE_SYNC.INCOHERENT_P1', - INCOHERENT_P2: 'F.SYNC.D_INCOMPLETE_SYNC.INCOHERENT_P2', - P1: 'F.SYNC.D_INCOMPLETE_SYNC.P1', - P2: 'F.SYNC.D_INCOMPLETE_SYNC.P2', - T3: 'F.SYNC.D_INCOMPLETE_SYNC.T3', - T4: 'F.SYNC.D_INCOMPLETE_SYNC.T4', - T5: 'F.SYNC.D_INCOMPLETE_SYNC.T5', - T6: 'F.SYNC.D_INCOMPLETE_SYNC.T6', - }, D_INITIAL_CFG: { SAVE_AND_ENABLE: 'F.SYNC.D_INITIAL_CFG.SAVE_AND_ENABLE', TITLE: 'F.SYNC.D_INITIAL_CFG.TITLE', @@ -1556,7 +1542,6 @@ const T = { DATA_REPAIRED: 'F.SYNC.S.DATA_REPAIRED', DECRYPTION_FAILED: 'F.SYNC.S.DECRYPTION_FAILED', DEFERRED_ACTION_FAILED: 'F.SYNC.S.DEFERRED_ACTION_FAILED', - DIALOG_RESULT_ERROR: 'F.SYNC.S.DIALOG_RESULT_ERROR', DISABLE_ENCRYPTION_FAILED: 'F.SYNC.S.DISABLE_ENCRYPTION_FAILED', ENABLE_ENCRYPTION_FAILED: 'F.SYNC.S.ENABLE_ENCRYPTION_FAILED', ENCRYPTION_DISABLED_ON_OTHER_DEVICE: @@ -1564,7 +1549,6 @@ const T = { ENCRYPTION_PASSWORD_REQUIRED: 'F.SYNC.S.ENCRYPTION_PASSWORD_REQUIRED', ENCRYPTION_REQUIRED_FOR_SUPERSYNC: 'F.SYNC.S.ENCRYPTION_REQUIRED_FOR_SUPERSYNC', ERROR_CORS: 'F.SYNC.S.ERROR_CORS', - ERROR_DATA_IS_CURRENTLY_WRITTEN: 'F.SYNC.S.ERROR_DATA_IS_CURRENTLY_WRITTEN', ERROR_PAYLOAD_TOO_LARGE: 'F.SYNC.S.ERROR_PAYLOAD_TOO_LARGE', ERROR_PERMISSION: 'F.SYNC.S.ERROR_PERMISSION', ERROR_PERMISSION_FLATPAK: 'F.SYNC.S.ERROR_PERMISSION_FLATPAK', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b1c3c164a5..c7f56c7ed7 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1250,20 +1250,6 @@ "OK": "Download Remote Data", "TITLE": "Initial Sync" }, - "D_INCOMPLETE_SYNC": { - "BTN_CLOSE_APP": "Close App", - "BTN_DOWNLOAD_BACKUP": "Download Local Backup", - "BTN_FORCE_DOWNLOAD": "Force Download Remote", - "BTN_FORCE_UPLOAD": "Force Upload Local", - "INCOHERENT_P1": "The date of your remote data is from the future", - "INCOHERENT_P2": "You should check the time configured on your systems!", - "P1": "The remote sync data is incoherent!", - "P2": "Affected model:", - "T3": "You have 2 Options:", - "T4": "1. Go to your other device and complete the sync there.", - "T5": "2. Or you can overwrite the remote data with your local one. All remote changes will\n be lost!", - "T6": "Creating a backup of the data you overwrite is recommended!!!" - }, "D_INITIAL_CFG": { "SAVE_AND_ENABLE": "Save & Enable Sync", "TITLE": "Configure Sync" @@ -1511,14 +1497,12 @@ "DATA_REPAIRED": "Sync cleanup: {{count}} reference(s) resolved", "DECRYPTION_FAILED": "Failed to decrypt synced data. Please check your encryption password.", "DEFERRED_ACTION_FAILED": "Some changes made during sync could not be saved. Please reload to recover.", - "DIALOG_RESULT_ERROR": "An error occurred while processing the sync dialog. Please try again.", "DISABLE_ENCRYPTION_FAILED": "Failed to disable encryption: {{message}}", "ENABLE_ENCRYPTION_FAILED": "Failed to enable encryption: {{message}}", "ENCRYPTION_DISABLED_ON_OTHER_DEVICE": "Encryption was disabled on another device. Your sync settings have been updated.", "ENCRYPTION_PASSWORD_REQUIRED": "Encrypted data received but no encryption password is configured. Please set your encryption password in sync settings.", "ENCRYPTION_REQUIRED_FOR_SUPERSYNC": "Encryption is required for SuperSync. Please set an encryption password to continue syncing.", "ERROR_CORS": "WebDAV Sync Error: Network request failed.\n\nThis may be a CORS issue. Please ensure:\n• Your WebDAV server allows Cross-Origin requests\n• The server URL is correct and accessible\n• You have a working internet connection", - "ERROR_DATA_IS_CURRENTLY_WRITTEN": "Remote Data is currently being written", "ERROR_PAYLOAD_TOO_LARGE": "Sync Error: Your data is too large to upload.\n\nThis happens when you have many archived tasks. Your data was NOT synced!\n\nPlease contact support for assistance.", "ERROR_PERMISSION": "File access denied. Please check your filesystem permissions.", "ERROR_PERMISSION_FLATPAK": "File access denied. Grant filesystem permission via Flatseal or use a path inside ~/.var/app/",