diff --git a/eslint.config.js b/eslint.config.js index 6d6ccacc61..4344757658 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -224,6 +224,21 @@ module.exports = tseslint.config( 'local-rules/no-multi-entity-effect': 'warn', }, }, + // App code must route logging through Log/SyncLog/OpLog/... helpers. + // Direct console.* calls bypass the exportable log history users attach + // to bug reports. The Log implementation itself, tests, and benchmarks + // (which intentionally dump timing numbers to stdout) are exempt. + { + files: ['src/app/**/*.ts'], + ignores: [ + 'src/app/**/*.spec.ts', + 'src/app/**/*.benchmark.ts', + 'src/app/core/log.ts', + ], + rules: { + 'no-console': 'error', + }, + }, // HTML files { files: ['**/*.html'], diff --git a/src/app/core-ui/magic-side-nav/nav-list/nav-list-tree.component.ts b/src/app/core-ui/magic-side-nav/nav-list/nav-list-tree.component.ts index 767f147ee5..0f888a2bca 100644 --- a/src/app/core-ui/magic-side-nav/nav-list/nav-list-tree.component.ts +++ b/src/app/core-ui/magic-side-nav/nav-list/nav-list-tree.component.ts @@ -37,6 +37,7 @@ import { Project } from '../../../features/project/project.model'; import { isSingleEmoji } from '../../../util/extract-first-emoji'; import { expandCollapseAni } from '../../../ui/tree-dnd/tree.animations'; import { Router } from '@angular/router'; +import { Log } from '../../../core/log'; const EXPAND_ANIMATION_RESET_DELAY_MS = 250; @@ -179,10 +180,7 @@ export class NavListTreeComponent implements OnDestroy { node: TreeNode, ): void { // TODO: Implement folder context menu - console.log( - 'Folder context menu for:', - node.data?.k === MenuTreeKind.FOLDER ? node.data.name : 'unknown', - ); + Log.log('Folder context menu for:', { id: node.id, kind: node.data?.k }); } private _toggleFolder(treeNodeId: string): void { diff --git a/src/app/core-ui/work-context-menu/work-context-menu.component.ts b/src/app/core-ui/work-context-menu/work-context-menu.component.ts index acb252e281..653ddc576d 100644 --- a/src/app/core-ui/work-context-menu/work-context-menu.component.ts +++ b/src/app/core-ui/work-context-menu/work-context-menu.component.ts @@ -46,6 +46,7 @@ import { firstValueFrom, Observable, of } from 'rxjs'; import { AsyncPipe } from '@angular/common'; import { DateService } from '../../core/date/date.service'; import { openWorkContextSettingsDialog } from '../../features/work-context/dialog-work-context-settings/open-work-context-settings-dialog'; +import { Log } from '../../core/log'; @Component({ selector: 'work-context-menu', @@ -241,7 +242,7 @@ export class WorkContextMenuComponent implements OnInit { try { return await this._projectService.getCompletionInfo(this.contextId); } catch (err) { - console.error(err); + Log.err(err); this._snackService.open({ type: 'ERROR', msg: T.F.PROJECT.COMPLETE.ERROR }); return null; } @@ -301,7 +302,7 @@ export class WorkContextMenuComponent implements OnInit { msg: T.GLOBAL_SNACK.DUPLICATE_PROJECT_ERROR, type: 'ERROR', }); - console.error(err); + Log.err(err); } } @@ -447,7 +448,7 @@ export class WorkContextMenuComponent implements OnInit { msg: T.GLOBAL_SNACK.OPEN_SETTINGS_ERROR, type: 'ERROR', }); - console.error(err); + Log.err(err); } } diff --git a/src/app/core/clipboard-image/clipboard-image.service.ts b/src/app/core/clipboard-image/clipboard-image.service.ts index 73207bb250..f193a15a19 100644 --- a/src/app/core/clipboard-image/clipboard-image.service.ts +++ b/src/app/core/clipboard-image/clipboard-image.service.ts @@ -5,6 +5,7 @@ import { T } from '../../t.const'; import { GlobalConfigService } from '../../features/config/global-config.service'; import { getDefaultClipboardImagesPath } from '../../util/get-default-clipboard-images-path'; import { MIME_TYPE_EXTENSIONS } from '../../../../electron/shared-with-frontend/mime-type-mapping.const'; +import { Log } from '../log'; const DB_NAME = 'sp-clipboard-images'; const DB_VERSION = 1; @@ -117,7 +118,7 @@ export class ClipboardImageService { return { success: true, imageUrl, markdownText }; } } catch (error) { - console.error('[CLIPBOARD] Error getting file path:', error); + Log.err('[CLIPBOARD] Error getting file path:', error); } } } @@ -164,8 +165,8 @@ export class ClipboardImageService { return { success: true, imageUrl, markdownText }; } catch (error) { - console.error('[CLIPBOARD] Error saving clipboard image:', error); - console.error( + Log.err('[CLIPBOARD] Error saving clipboard image:', error); + Log.err( '[CLIPBOARD] Error stack:', error instanceof Error ? error.stack : 'no stack', ); @@ -234,7 +235,7 @@ export class ClipboardImageService { return { success: true, imageUrl, markdownText }; } catch (error) { - console.error('Error getting image from file paths:', error); + Log.err('Error getting image from file paths:', error); return null; // Fall back to regular clipboard handling } } @@ -316,7 +317,7 @@ export class ClipboardImageService { this._blobUrlCache.set(imageId, blobUrl); return blobUrl; } catch (error) { - console.error('Error resolving indexeddb URL for clipboard image:', error); + Log.err('Error resolving indexeddb URL for clipboard image:', error); return null; } } diff --git a/src/app/core/clipboard-image/resolve-clipboard-images.directive.ts b/src/app/core/clipboard-image/resolve-clipboard-images.directive.ts index ea675631c1..9770380b04 100644 --- a/src/app/core/clipboard-image/resolve-clipboard-images.directive.ts +++ b/src/app/core/clipboard-image/resolve-clipboard-images.directive.ts @@ -9,6 +9,7 @@ import { SimpleChanges, } from '@angular/core'; import { ClipboardImageService } from './clipboard-image.service'; +import { Log } from '../log'; /** * Directive that resolves indexeddb:// URLs in img elements within the host element. @@ -98,7 +99,7 @@ export class ResolveClipboardImagesDirective implements OnInit, OnDestroy, OnCha img.classList.add('indexeddb-image-error'); } } catch (error) { - console.error('Error resolving clipboard image:', error); + Log.err('Error resolving clipboard image:', error); img.alt = 'Error loading image'; img.classList.add('indexeddb-image-error'); } diff --git a/src/app/core/clipboard-image/resolve-clipboard-url.pipe.ts b/src/app/core/clipboard-image/resolve-clipboard-url.pipe.ts index 10c7c018f3..e41f1c5dff 100644 --- a/src/app/core/clipboard-image/resolve-clipboard-url.pipe.ts +++ b/src/app/core/clipboard-image/resolve-clipboard-url.pipe.ts @@ -2,6 +2,7 @@ import { Pipe, PipeTransform, inject } from '@angular/core'; import { ClipboardImageService } from './clipboard-image.service'; import { Observable, from, of, defer } from 'rxjs'; import { map, shareReplay, catchError } from 'rxjs/operators'; +import { Log } from '../log'; @Pipe({ name: 'resolveClipboardUrl', @@ -38,7 +39,7 @@ export class ResolveClipboardUrlPipe implements PipeTransform { return url; }), catchError((error) => { - console.error('Error resolving clipboard URL:', error); + Log.err('Error resolving clipboard URL:', error); return of(url); }), shareReplay({ bufferSize: 1, refCount: false }), diff --git a/src/app/core/error-handler/global-error-handler.util.ts b/src/app/core/error-handler/global-error-handler.util.ts index 557a7ac2bd..778d873f1e 100644 --- a/src/app/core/error-handler/global-error-handler.util.ts +++ b/src/app/core/error-handler/global-error-handler.util.ts @@ -88,7 +88,7 @@ export const logAdvancedStacktrace = ( // NOTE: there is an issue with this sometimes -> https://github.com/stacktracejs/stacktrace.js/issues/202 }) - .catch(console.error); + .catch((err) => Log.err(err)); const _cleanHtml = (str: string): string => { const div = document.createElement('div'); diff --git a/src/app/core/share/share-file.util.ts b/src/app/core/share/share-file.util.ts index 9e8b2d87bc..61bc6e0de0 100644 --- a/src/app/core/share/share-file.util.ts +++ b/src/app/core/share/share-file.util.ts @@ -1,6 +1,7 @@ import { Directory, Filesystem } from '@capacitor/filesystem'; import { IS_NATIVE_PLATFORM } from '../../util/is-native-platform'; import { ShareResult } from './share.model'; +import { Log } from '../log'; /** * Sanitize filename for safe file system usage. @@ -43,7 +44,7 @@ export const downloadBlob = (blob: Blob, filename: string): boolean => { anchor.click(); return true; } catch (error) { - console.warn('Browser download failed:', error); + Log.warn('Browser download failed:', error); return false; } finally { document.body.removeChild(anchor); @@ -65,7 +66,7 @@ export const cleanupCacheFile = async (relativePath: string): Promise => { directory: Directory.Cache, }); } catch (cleanupError) { - console.warn('Failed to cleanup shared file:', cleanupError); + Log.warn('Failed to cleanup shared file:', cleanupError); } }; @@ -120,7 +121,7 @@ export const openDownloadResult = async (result: ShareResult): Promise => (window as any).ea.openPath(path); return; } catch (error) { - console.warn('Failed to open path via Electron bridge:', error); + Log.warn('Failed to open path via Electron bridge:', error); } } @@ -131,7 +132,7 @@ export const openDownloadResult = async (result: ShareResult): Promise => window.open(candidate, '_blank', 'noopener'); return; } catch (error) { - console.warn('Failed to open download in new window:', error); + Log.warn('Failed to open download in new window:', error); } } }; diff --git a/src/app/core/share/share.service.ts b/src/app/core/share/share.service.ts index 77947ab05c..2113eb2f30 100644 --- a/src/app/core/share/share.service.ts +++ b/src/app/core/share/share.service.ts @@ -17,6 +17,7 @@ import * as ShareTextUtil from './share-text.util'; import * as ShareUrlBuilder from './share-url-builder.util'; import * as ShareFileUtil from './share-file.util'; import * as SharePlatformUtil from './share-platform.util'; +import { Log } from '../log'; export type ShareOutcome = 'shared' | 'cancelled' | 'unavailable' | 'failed'; export type { ShareSupport } from './share-platform.util'; @@ -252,7 +253,7 @@ export class ShareService { error: 'Share cancelled', }; } - console.warn('Capacitor share failed:', error); + Log.warn('Capacitor share failed:', error); } } @@ -273,7 +274,7 @@ export class ShareService { if (error instanceof Error && error.name === 'AbortError') { return { success: false, error: 'Share cancelled' }; } - console.warn('Web Share API failed:', error); + Log.warn('Web Share API failed:', error); } } @@ -524,7 +525,7 @@ export class ShareService { ? `file://${resolvedUri}` : `file:///${resolvedUri}`; - console.debug('[ShareService] shareCanvasViaNative', { + Log.debug('[ShareService] shareCanvasViaNative', { resolvedUri, fileUrl, relativePath, @@ -535,9 +536,9 @@ export class ShareService { path: relativePath, directory: Directory.Cache, }); - console.debug('[ShareService] shareCanvasViaNative stat', stat); + Log.debug('[ShareService] shareCanvasViaNative stat', stat); } catch (statError) { - console.warn('[ShareService] stat failed for shared image', statError); + Log.warn('[ShareService] stat failed for shared image', statError); } const canShare = (await sharePlugin.canShare?.())?.value ?? true; @@ -570,7 +571,7 @@ export class ShareService { error: 'Share cancelled', }; } - console.warn('Native image share failed:', error, { + Log.warn('Native image share failed:', error, { fileUrl: fileUrl ?? 'n/a', resolvedUri: resolvedUri ?? 'n/a', relativePath, @@ -633,7 +634,7 @@ export class ShareService { error: 'Share cancelled', }; } - console.warn('Web Share API failed:', error); + Log.warn('Web Share API failed:', error); } return { @@ -683,7 +684,7 @@ export class ShareService { uri, }; } catch (error) { - console.warn('Native download failed, falling back to browser download:', error); + Log.warn('Native download failed, falling back to browser download:', error); } } @@ -703,7 +704,7 @@ export class ShareService { target: 'download', }; } catch (error) { - console.warn('Opening image in new tab failed:', error); + Log.warn('Opening image in new tab failed:', error); } } diff --git a/src/app/core/unsplash/unsplash.service.ts b/src/app/core/unsplash/unsplash.service.ts index 28e6671f46..e18b0a650f 100644 --- a/src/app/core/unsplash/unsplash.service.ts +++ b/src/app/core/unsplash/unsplash.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { getEnvOptional } from '../../util/env'; +import { Log } from '../log'; export interface UnsplashPhoto { id: string; @@ -56,7 +57,7 @@ export class UnsplashService { } if (!this.ACCESS_KEY) { - console.warn( + Log.warn( 'No Unsplash Access Key configured. Register at https://unsplash.com/developers?utm_source=super-productivity&utm_medium=referral&utm_campaign=api-credit', ); return of({ results: [], total: 0, total_pages: 0 }); @@ -81,7 +82,7 @@ export class UnsplashService { }) .pipe( catchError((error) => { - console.error('Unsplash API error:', error); + Log.err('Unsplash API error:', error); return of({ results: [], total: 0, total_pages: 0 }); }), ); @@ -122,12 +123,12 @@ export class UnsplashService { */ trackPhotoDownload(photo: UnsplashPhoto): Observable { if (!photo.links?.download_location) { - console.warn('No download_location available for photo', photo.id); + Log.warn('No download_location available for photo', photo.id); return of(null); } if (!this.ACCESS_KEY) { - console.warn('No Unsplash Access Key configured'); + Log.warn('No Unsplash Access Key configured'); return of(null); } @@ -138,7 +139,7 @@ export class UnsplashService { // Call the download endpoint to track usage return this._http.get(photo.links.download_location, { headers }).pipe( catchError((error) => { - console.error('Failed to track photo download:', error); + Log.err('Failed to track photo download:', error); // Don't fail the selection if tracking fails return of(null); }), diff --git a/src/app/features/config/clipboard-images-cfg/clipboard-images-cfg.component.ts b/src/app/features/config/clipboard-images-cfg/clipboard-images-cfg.component.ts index 84bc4adffa..70e995e867 100644 --- a/src/app/features/config/clipboard-images-cfg/clipboard-images-cfg.component.ts +++ b/src/app/features/config/clipboard-images-cfg/clipboard-images-cfg.component.ts @@ -24,6 +24,7 @@ import { MatInput } from '@angular/material/input'; import { FormsModule } from '@angular/forms'; import { DialogClipboardImagesManagerComponent } from './dialog-clipboard-images-manager/dialog-clipboard-images-manager.component'; import { getDefaultClipboardImagesPath } from '../../../util/get-default-clipboard-images-path'; +import { Log } from '../../../core/log'; @Component({ selector: 'clipboard-images-cfg', @@ -80,7 +81,7 @@ export class ClipboardImagesCfgComponent implements OnInit { this.defaultImagePath = await getDefaultClipboardImagesPath(); this._cd.markForCheck(); } catch (error) { - console.error('Error loading default clipboard image path:', error); + Log.err('Error loading default clipboard image path:', error); } } diff --git a/src/app/features/config/clipboard-images-cfg/dialog-clipboard-images-manager/dialog-clipboard-images-manager.component.ts b/src/app/features/config/clipboard-images-cfg/dialog-clipboard-images-manager/dialog-clipboard-images-manager.component.ts index da2e1b109e..6c675774a2 100644 --- a/src/app/features/config/clipboard-images-cfg/dialog-clipboard-images-manager/dialog-clipboard-images-manager.component.ts +++ b/src/app/features/config/clipboard-images-cfg/dialog-clipboard-images-manager/dialog-clipboard-images-manager.component.ts @@ -22,6 +22,7 @@ import { MatTooltip } from '@angular/material/tooltip'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatSelect } from '@angular/material/select'; import { MatOption } from '@angular/material/core'; +import { Log } from '../../../../core/log'; type SortOrder = 'newest' | 'oldest' | 'largest' | 'smallest'; @@ -100,7 +101,7 @@ export class DialogClipboardImagesManagerComponent implements OnInit { } this.imageUrls.set(urlMap); } catch (error) { - console.error('Error loading clipboard images:', error); + Log.err('Error loading clipboard images:', error); this._snackService.open({ type: 'ERROR', msg: this._translateService.instant(T.GCF.CLIPBOARD_IMAGES.ERROR_LOADING), @@ -124,7 +125,7 @@ export class DialogClipboardImagesManagerComponent implements OnInit { throw new Error('Delete operation returned false'); } } catch (error) { - console.error('Error deleting clipboard image:', error); + Log.err('Error deleting clipboard image:', error); this._snackService.open({ type: 'ERROR', msg: this._translateService.instant(T.GCF.CLIPBOARD_IMAGES.ERROR_DELETING), @@ -159,7 +160,7 @@ export class DialogClipboardImagesManagerComponent implements OnInit { }); } } catch (error) { - console.error('Error deleting all clipboard images:', error); + Log.err('Error deleting all clipboard images:', error); await this.loadImages(); this._snackService.open({ type: 'ERROR', diff --git a/src/app/features/config/icon-input/icon-input.component.ts b/src/app/features/config/icon-input/icon-input.component.ts index 44dafcf943..0e6832abfc 100644 --- a/src/app/features/config/icon-input/icon-input.component.ts +++ b/src/app/features/config/icon-input/icon-input.component.ts @@ -20,6 +20,7 @@ import { containsEmoji, extractFirstEmoji } from '../../../util/extract-first-em import { isSingleEmoji } from '../../../util/extract-first-emoji'; import { startWith } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Log } from '../../../core/log'; @Component({ selector: 'icon-input', @@ -80,7 +81,7 @@ export class IconInputComponent extends FieldType implements this.filteredIcons.set(icons.slice(0, 50)); } } catch (error) { - console.error('Failed to load material icons:', error); + Log.err('Failed to load material icons:', error); this.filteredIcons.set([]); } } @@ -123,7 +124,7 @@ export class IconInputComponent extends FieldType implements this.isEmoji.set(false); } } catch (error) { - console.error('Failed to filter icons:', error); + Log.err('Failed to filter icons:', error); this.filteredIcons.set([]); } } diff --git a/src/app/features/config/store/global-config.effects.ts b/src/app/features/config/store/global-config.effects.ts index 5a3d2c54fe..0a226ac0ff 100644 --- a/src/app/features/config/store/global-config.effects.ts +++ b/src/app/features/config/store/global-config.effects.ts @@ -29,6 +29,7 @@ import { AppStateActions } from '../../../root-store/app-state/app-state.actions import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions'; import { selectAllTasks } from '../../tasks/store/task.selectors'; import { normalizeStartOfNextDayConfig } from '../normalize-start-of-next-day-config'; +import { Log } from '../../../core/log'; @Injectable() export class GlobalConfigEffects { @@ -243,7 +244,7 @@ export class GlobalConfigEffects { setTimeout(() => window.location.reload(), 1000); }) .catch((err) => { - console.error('Failed to migrate user profiles:', err); + Log.err('Failed to migrate user profiles:', err); this._snackService.open({ type: 'ERROR', msg: 'Failed to enable user profiles. Please try again.', diff --git a/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.ts b/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.ts index d9210065d5..0f04172c72 100644 --- a/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.ts +++ b/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.ts @@ -10,6 +10,7 @@ import { CalendarContextInfoTarget } from '../../issue/providers/calendar/calend import { selectEnabledIssueProviders } from '../../issue/store/issue-provider.selectors'; import { PluginIssueProviderRegistryService } from '../../../plugins/issue-provider/plugin-issue-provider-registry.service'; import { PluginService } from '../../../plugins/plugin.service'; +import { IssueLog } from '../../../core/log'; @Component({ selector: 'issue-provider-setup-overview', @@ -69,7 +70,7 @@ export class IssueProviderSetupOverviewComponent { this.pluginProviders = allProviders.filter((p) => !p.useAgendaView); this.pluginCalendarProviders = allProviders.filter((p) => p.useAgendaView); if (!isValidIssueProviderKey(issueProviderKey)) { - console.error(`Invalid issue provider key from plugin: "${issueProviderKey}"`); + IssueLog.err(`Invalid issue provider key from plugin: "${issueProviderKey}"`); return; } this.openSetupDialog(issueProviderKey); diff --git a/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts b/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts index 8010ed3b3d..06ca8b7bf4 100644 --- a/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts +++ b/src/app/features/issue/dialog-edit-issue-provider/dialog-edit-issue-provider.component.ts @@ -206,7 +206,7 @@ export class DialogEditIssueProviderComponent { constructor() { this._initOAuthAndOptions().catch((err) => { - console.error( + IssueLog.err( '[DialogEditIssueProvider] OAuth init failed', getSafeErrorLogMeta(err), ); @@ -453,7 +453,7 @@ export class DialogEditIssueProviderComponent { } } catch (e) { anyFailed = true; - console.error('[DialogEditIssueProvider] loadOptions failed', { + IssueLog.err('[DialogEditIssueProvider] loadOptions failed', { issueProviderKey: this.issueProviderKey, hasFieldKey: !!field.key, ...getSafeErrorLogMeta(e), diff --git a/src/app/features/issue/providers/trello/trello-api.service.ts b/src/app/features/issue/providers/trello/trello-api.service.ts index ffd8cc130d..954abe364c 100644 --- a/src/app/features/issue/providers/trello/trello-api.service.ts +++ b/src/app/features/issue/providers/trello/trello-api.service.ts @@ -18,6 +18,7 @@ import { ISSUE_PROVIDER_HUMANIZED, TRELLO_TYPE } from '../../issue.const'; import { T } from '../../../../t.const'; import { HANDLED_ERROR_PROP_STR } from '../../../../app.constants'; import { getErrorTxt } from '../../../../util/get-error-text'; +import { IssueLog } from '../../../../core/log'; const BASE_URL = 'https://api.trello.com/1'; const DEFAULT_CARD_FIELDS = [ @@ -195,7 +196,8 @@ export class TrelloApiService { }), catchError((error) => { const errorMsg = `Trello failed to resolve username "${username}": ${getErrorTxt(error)}`; - console.error(errorMsg); + // errorMsg embeds the username (user content) — keep it out of the exportable log + IssueLog.err('Trello failed to resolve username:', getErrorTxt(error)); this._snackService.open({ type: 'ERROR', msg: errorMsg, diff --git a/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts b/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts index d423ae7231..4caa1a8a0e 100644 --- a/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts +++ b/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts @@ -27,6 +27,7 @@ import { } from '../../../ui/heatmap/heatmap.component'; import { DateAdapter } from '@angular/material/core'; import { Worklog } from '../../worklog/worklog.model'; +import { Log } from '../../../core/log'; interface YearlyActivityData { dayMap: Map; @@ -368,7 +369,7 @@ export class ActivityHeatmapComponent { }); } } else if (result.error && result.error !== 'Share cancelled') { - console.error('Share failed:', result.error); + Log.err('Share failed:', result.error); this._snackService.open({ type: 'ERROR', msg: 'Failed to share heatmap', @@ -377,7 +378,7 @@ export class ActivityHeatmapComponent { } catch (error: any) { const isAbort = error?.name === 'AbortError' || error?.error === 'Share cancelled'; if (!isAbort) { - console.error('Share failed:', error); + Log.err('Share failed:', error); this._snackService.open({ type: 'ERROR', msg: 'Failed to share heatmap', diff --git a/src/app/features/metric/lazy-chart/lazy-chart.component.ts b/src/app/features/metric/lazy-chart/lazy-chart.component.ts index 7f900d552f..82c97a8507 100644 --- a/src/app/features/metric/lazy-chart/lazy-chart.component.ts +++ b/src/app/features/metric/lazy-chart/lazy-chart.component.ts @@ -31,6 +31,7 @@ import { ChartLazyLoaderService } from '../chart-lazy-loader.service'; import { ShareService } from '../../../core/share/share.service'; import { SnackService } from '../../../core/snack/snack.service'; import { T } from '../../../t.const'; +import { Log } from '../../../core/log'; interface ChartClickEvent { active: ActiveElement[]; @@ -226,10 +227,10 @@ export class LazyChartComponent implements OnDestroy { } if (!result.success && result.error && result.error !== 'Share cancelled') { - console.error('Share failed:', result.error); + Log.err('Share failed:', result.error); } } catch (error) { - console.error('Share failed:', error); + Log.err('Share failed:', error); } finally { this.isSharing.set(false); } @@ -247,14 +248,14 @@ export class LazyChartComponent implements OnDestroy { await this.chartLoaderService.loadChartModule(); this.isModuleLoaded.set(true); } catch (error) { - console.error('Failed to load chart module:', error); + Log.err('Failed to load chart module:', error); } } private initChart(canvas: ElementRef): void { const chartModule = this.chartLoaderService.getChartModule(); if (!chartModule) { - console.error('Chart module not loaded'); + Log.err('Chart module not loaded'); return; } @@ -269,7 +270,7 @@ export class LazyChartComponent implements OnDestroy { this.chartInstance?.destroy(); this.chartInstance = new Chart(canvas.nativeElement, chartConfig); } catch (error) { - console.error('Failed to initialize chart:', error); + Log.err('Failed to initialize chart:', error); } } diff --git a/src/app/features/right-panel/right-panel.component.ts b/src/app/features/right-panel/right-panel.component.ts index 976918bd83..e4dc55bf64 100644 --- a/src/app/features/right-panel/right-panel.component.ts +++ b/src/app/features/right-panel/right-panel.component.ts @@ -23,6 +23,7 @@ import { of, Subscription, timer } from 'rxjs'; import { SwipeDirective } from '../../ui/swipe-gesture/swipe.directive'; import { CssString, StyleObject, StyleObjectToString } from '../../util/styles'; import { LS } from '../../core/persistence/storage-keys.const'; +import { Log } from '../../core/log'; import { readNumberLSBounded } from '../../util/ls-util'; import { MatIconModule } from '@angular/material/icon'; import { MatRippleModule } from '@angular/material/core'; @@ -558,7 +559,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy { try { localStorage.setItem(LS.RIGHT_PANEL_WIDTH, this.currentWidth().toString()); } catch (error) { - console.warn('Failed to save right panel width to localStorage:', error); + Log.warn('Failed to save right panel width to localStorage:', error); } } @@ -577,7 +578,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy { // Additional validation if (!Number.isFinite(width) || width < RIGHT_PANEL_CONFIG.MIN_WIDTH) { - console.warn('Invalid right panel width detected, using default'); + Log.warn('Invalid right panel width detected, using default'); this.currentWidth.set(RIGHT_PANEL_CONFIG.DEFAULT_WIDTH); return; } @@ -586,7 +587,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy { const validatedWidth = clampWidth(width, RIGHT_PANEL_CONFIG.MAX_WIDTH); this.currentWidth.set(validatedWidth); } catch (error) { - console.warn('Failed to initialize right panel width:', error); + Log.warn('Failed to initialize right panel width:', error); this.currentWidth.set(RIGHT_PANEL_CONFIG.DEFAULT_WIDTH); } } diff --git a/src/app/features/simple-counter/simple-counter.service.ts b/src/app/features/simple-counter/simple-counter.service.ts index 2a4a1ca69a..4da3cd0afa 100644 --- a/src/app/features/simple-counter/simple-counter.service.ts +++ b/src/app/features/simple-counter/simple-counter.service.ts @@ -36,6 +36,7 @@ import { DateService } from 'src/app/core/date/date.service'; import { GlobalTrackingIntervalService } from '../../core/global-tracking-interval/global-tracking-interval.service'; import { ImexViewService } from '../../imex/imex-meta/imex-view.service'; import { BatchedTimeSyncAccumulator } from '../../core/util/batched-time-sync-accumulator'; +import { Log } from '../../core/log'; @Injectable({ providedIn: 'root', @@ -223,7 +224,7 @@ export class SimpleCounterService implements OnDestroy { } }) .catch((error) => { - console.error('[SimpleCounterService] Error syncing stopwatch value:', error); + Log.err('[SimpleCounterService] Error syncing stopwatch value:', error); }); } diff --git a/src/app/features/task-repeat-cfg/repeat-cfg-preview/repeat-cfg-preview.component.ts b/src/app/features/task-repeat-cfg/repeat-cfg-preview/repeat-cfg-preview.component.ts index 7f434d3239..a31e5fe428 100644 --- a/src/app/features/task-repeat-cfg/repeat-cfg-preview/repeat-cfg-preview.component.ts +++ b/src/app/features/task-repeat-cfg/repeat-cfg-preview/repeat-cfg-preview.component.ts @@ -17,6 +17,7 @@ import { MatTooltip } from '@angular/material/tooltip'; import { DateTimeFormatService } from '../../../core/date-time-format/date-time-format.service'; import { getNextRepeatOccurrence } from '../store/get-next-repeat-occurrence.util'; import { formatMonthDay } from '../../../util/format-month-day.util'; +import { Log } from '../../../core/log'; @Component({ selector: 'repeat-cfg-preview', @@ -50,7 +51,7 @@ export class RepeatCfgPreviewComponent { const nextLabel = this._translateService.instant(T.SCHEDULE.NEXT); return `${nextLabel} ${formatted}`; } catch (e) { - console.warn('Failed to compute next repeat occurrence', e); + Log.warn('Failed to compute next repeat occurrence', e); return ''; } }); diff --git a/src/app/features/tasks/store/task-electron.effects.ts b/src/app/features/tasks/store/task-electron.effects.ts index 23f77ad296..43c2d119b6 100644 --- a/src/app/features/tasks/store/task-electron.effects.ts +++ b/src/app/features/tasks/store/task-electron.effects.ts @@ -31,6 +31,7 @@ import { ipcAddTaskFromAppUri$ } from '../../../core/ipc-events'; import { TaskService } from '../task.service'; import { TaskSharedActions } from '../../../root-store/meta/task-shared.actions'; import { LOCAL_ACTIONS } from '../../../util/local-actions.token'; +import { TaskLog } from '../../../core/log'; // TODO send message to electron when current task changes here @@ -205,7 +206,10 @@ export class TaskElectronEffects { tap((data) => { // Double-check data validity as defensive programming if (!data || !data.title || typeof data.title !== 'string') { - console.error('handleAddTaskFromProtocol$ received invalid data:', data); + TaskLog.err('handleAddTaskFromProtocol$ received invalid data', { + hasData: !!data, + titleType: typeof data?.title, + }); return; } this._taskService.add(data.title); diff --git a/src/app/features/tasks/store/task-reminder.effects.ts b/src/app/features/tasks/store/task-reminder.effects.ts index 2d189d1d8e..6d404a827b 100644 --- a/src/app/features/tasks/store/task-reminder.effects.ts +++ b/src/app/features/tasks/store/task-reminder.effects.ts @@ -17,6 +17,7 @@ import { import { androidInterface } from '../../android/android-interface'; import { generateNotificationId } from '../../android/android-notification-id.util'; import { PlannerActions } from '../../planner/store/planner.actions'; +import { TaskLog } from '../../../core/log'; @Injectable() export class TaskReminderEffects { @@ -97,7 +98,7 @@ export class TaskReminderEffects { const notificationId = generateNotificationId(task.id); androidInterface.cancelNativeReminder?.(notificationId); } catch (e) { - console.error('Failed to cancel native reminder:', e); + TaskLog.err('Failed to cancel native reminder:', e); } } @@ -115,7 +116,7 @@ export class TaskReminderEffects { const notificationId = generateNotificationId(task.id + '_deadline'); androidInterface.cancelNativeReminder?.(notificationId); } catch (e) { - console.error('Failed to cancel native deadline reminder:', e); + TaskLog.err('Failed to cancel native deadline reminder:', e); } } @@ -218,7 +219,7 @@ export class TaskReminderEffects { androidInterface.cancelNativeReminder?.(deadlineNotificationId); } } catch (e) { - console.error('Failed to cancel native reminder:', e); + TaskLog.err('Failed to cancel native reminder:', e); } }), ), @@ -243,7 +244,7 @@ export class TaskReminderEffects { const notificationId = generateNotificationId(id); androidInterface.cancelNativeReminder?.(notificationId); } catch (e) { - console.error('Failed to cancel native reminder:', e); + TaskLog.err('Failed to cancel native reminder:', e); } }); }), @@ -267,7 +268,7 @@ export class TaskReminderEffects { generateNotificationId(id + '_deadline'), ); } catch (e) { - console.error('Failed to cancel native reminder:', e); + TaskLog.err('Failed to cancel native reminder:', e); } }); }), @@ -290,7 +291,7 @@ export class TaskReminderEffects { generateNotificationId(id + '_deadline'), ); } catch (e) { - console.error('Failed to cancel native reminder:', e); + TaskLog.err('Failed to cancel native reminder:', e); } }); }), @@ -313,7 +314,7 @@ export class TaskReminderEffects { generateNotificationId(task.id + '_deadline'), ); } catch (e) { - console.error('Failed to cancel native reminder:', e); + TaskLog.err('Failed to cancel native reminder:', e); } // Also cancel for subtasks task.subTaskIds?.forEach((subId) => { @@ -323,7 +324,7 @@ export class TaskReminderEffects { generateNotificationId(subId + '_deadline'), ); } catch (e) { - console.error('Failed to cancel native reminder:', e); + TaskLog.err('Failed to cancel native reminder:', e); } }); }); diff --git a/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.spec.ts b/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.spec.ts index 1bd4376979..df934dfa29 100644 --- a/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.spec.ts +++ b/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.spec.ts @@ -7,6 +7,7 @@ import { TaskAttachment } from '../task-attachment.model'; import { T } from '../../../../t.const'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ClipboardImageService } from '../../../../core/clipboard-image/clipboard-image.service'; +import { Log } from '../../../../core/log'; describe('TaskAttachmentListComponent', () => { let component: TaskAttachmentListComponent; @@ -152,12 +153,12 @@ describe('TaskAttachmentListComponent', () => { .and.returnValue(Promise.reject(new Error('Permission denied'))); setNavigatorClipboard({ writeText: writeTextSpy }); document.execCommand = jasmine.createSpy('execCommand').and.returnValue(true); - spyOn(console, 'warn'); + spyOn(Log, 'warn'); await component.copy(attachment); expect(writeTextSpy).toHaveBeenCalledWith('https://example.com/test'); - expect(console.warn).toHaveBeenCalled(); + expect(Log.warn).toHaveBeenCalled(); expect(document.execCommand).toHaveBeenCalledWith('copy'); expect(snackService.open).toHaveBeenCalledWith(T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD); }); @@ -180,12 +181,12 @@ describe('TaskAttachmentListComponent', () => { document.execCommand = jasmine .createSpy('execCommand') .and.throwError('Command not supported'); - spyOn(console, 'error'); + spyOn(Log, 'err'); await component.copy(attachment); expect(document.execCommand).toHaveBeenCalledWith('copy'); - expect(console.error).toHaveBeenCalled(); + expect(Log.err).toHaveBeenCalled(); expect(snackService.open).toHaveBeenCalledWith({ msg: 'Failed to copy to clipboard. Please copy manually.', type: 'ERROR', diff --git a/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts b/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts index 0507d0361e..cb9597986b 100644 --- a/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts +++ b/src/app/features/tasks/task-attachment/task-attachment-list/task-attachment-list.component.ts @@ -20,6 +20,7 @@ import { EnlargeImgDirective } from '../../../../ui/enlarge-img/enlarge-img.dire import { MatAnchor, MatButton } from '@angular/material/button'; import { ClipboardImageService } from '../../../../core/clipboard-image/clipboard-image.service'; import { isPathSafeToOpen } from '../../../../../../electron/shared-with-frontend/is-external-url-allowed'; +import { Log } from '../../../../core/log'; interface ResolvedAttachment extends TaskAttachment { resolvedPath?: string; @@ -141,7 +142,7 @@ export class TaskAttachmentListComponent { }); } } catch (error) { - console.error('Error resolving clipboard image:', error); + Log.err('Error resolving clipboard image:', error); } }); }); @@ -192,7 +193,7 @@ export class TaskAttachmentListComponent { this._copyWithFallback(attachment.path); } } catch (error) { - console.warn('Clipboard write failed, trying fallback method:', error); + Log.warn('Clipboard write failed, trying fallback method:', error); // Try fallback method if modern API fails this._copyWithFallback(attachment.path); } @@ -220,7 +221,7 @@ export class TaskAttachmentListComponent { }); } } catch (error) { - console.error('Fallback copy failed:', error); + Log.err('Fallback copy failed:', error); this._snackService.open({ msg: 'Failed to copy to clipboard. Please copy manually.', type: 'ERROR', diff --git a/src/app/features/tasks/util/play-done-sound.ts b/src/app/features/tasks/util/play-done-sound.ts index 3d74b04fc2..42bfeedd0d 100644 --- a/src/app/features/tasks/util/play-done-sound.ts +++ b/src/app/features/tasks/util/play-done-sound.ts @@ -30,6 +30,6 @@ export const playDoneSound = async ( source.detune.value = pitchFactor; }); } catch (e) { - console.error('Error playing done sound:', e); + TaskLog.err('Error playing done sound:', e); } }; diff --git a/src/app/features/user-profile/user-profile-button/user-profile-button.component.ts b/src/app/features/user-profile/user-profile-button/user-profile-button.component.ts index 476aa03702..a3c8fec9d2 100644 --- a/src/app/features/user-profile/user-profile-button/user-profile-button.component.ts +++ b/src/app/features/user-profile/user-profile-button/user-profile-button.component.ts @@ -10,6 +10,7 @@ import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { MatDivider } from '@angular/material/divider'; import { TranslatePipe } from '@ngx-translate/core'; import { T } from '../../../t.const'; +import { Log } from '../../../core/log'; @Component({ selector: 'user-profile-button', @@ -94,7 +95,7 @@ export class UserProfileButtonComponent { try { await this.profileService.switchProfile(profileId); } catch (error) { - console.error('Failed to switch profile:', error); + Log.err('Failed to switch profile:', error); } finally { this.isLoading.set(false); } diff --git a/src/app/features/work-context/work-context-markdown.service.ts b/src/app/features/work-context/work-context-markdown.service.ts index 85f628d34a..0616752f02 100644 --- a/src/app/features/work-context/work-context-markdown.service.ts +++ b/src/app/features/work-context/work-context-markdown.service.ts @@ -5,6 +5,7 @@ import { Store } from '@ngrx/store'; import { selectTasksWithSubTasksByIds } from '../tasks/store/task.selectors'; import { Task, TaskWithSubTasks } from '../tasks/task.model'; import { first } from 'rxjs/operators'; +import { Log } from '../../core/log'; @Injectable({ providedIn: 'root', @@ -167,7 +168,7 @@ export class WorkContextMarkdownService { await navigator.clipboard.writeText(text); return true; } catch (err) { - console.warn('Clipboard write failed, trying fallback method:', err); + Log.warn('Clipboard write failed, trying fallback method:', err); } } @@ -189,7 +190,7 @@ export class WorkContextMarkdownService { try { isSuccess = document.execCommand('copy'); } catch (err) { - console.error('Fallback copy failed:', err); + Log.err('Fallback copy failed:', err); isSuccess = false; } finally { document.body.removeChild(textarea); diff --git a/src/app/imex/sync/super-sync-restore.service.spec.ts b/src/app/imex/sync/super-sync-restore.service.spec.ts index 5f3524279a..b1917c2add 100644 --- a/src/app/imex/sync/super-sync-restore.service.spec.ts +++ b/src/app/imex/sync/super-sync-restore.service.spec.ts @@ -6,6 +6,7 @@ import { SnackService } from '../../core/snack/snack.service'; import { SyncProviderId } from '../../op-log/sync-providers/provider.const'; import { RestorePoint } from '../../op-log/sync-providers/provider.interface'; import { T } from '../../t.const'; +import { SyncLog } from '../../core/log'; describe('SuperSyncRestoreService', () => { let service: SuperSyncRestoreService; @@ -31,9 +32,9 @@ describe('SuperSyncRestoreService', () => { mockSnackService = jasmine.createSpyObj('SnackService', ['open']); - // Spy on console methods for retry logging tests - spyOn(console, 'warn'); - spyOn(console, 'error'); + // Spy on logger methods for retry logging tests + spyOn(SyncLog, 'warn'); + spyOn(SyncLog, 'err'); TestBed.configureTestingModule({ providers: [ @@ -215,7 +216,7 @@ describe('SuperSyncRestoreService', () => { await Promise.resolve(); await Promise.resolve(); expect(mockProvider.getStateAtSeq).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( + expect(SyncLog.warn).toHaveBeenCalledWith( jasmine.stringContaining('Restore failed due to network error, retrying (1/2)'), error, ); @@ -229,7 +230,7 @@ describe('SuperSyncRestoreService', () => { await Promise.resolve(); await Promise.resolve(); expect(mockProvider.getStateAtSeq).toHaveBeenCalledTimes(2); - expect(console.warn).toHaveBeenCalledWith( + expect(SyncLog.warn).toHaveBeenCalledWith( jasmine.stringContaining('Restore failed due to network error, retrying (2/2)'), error, ); diff --git a/src/app/imex/sync/super-sync-restore.service.ts b/src/app/imex/sync/super-sync-restore.service.ts index ad5b9e9f01..3bf11143b4 100644 --- a/src/app/imex/sync/super-sync-restore.service.ts +++ b/src/app/imex/sync/super-sync-restore.service.ts @@ -10,6 +10,7 @@ import { AppDataComplete } from '../../op-log/model/model-config'; import { T } from '../../t.const'; import { SyncProviderManager } from '../../op-log/sync-providers/provider-manager.service'; import { BackupService } from '../../op-log/backup/backup.service'; +import { SyncLog } from '../../core/log'; /** * Service for restoring state from Super Sync server history. @@ -81,7 +82,7 @@ export class SuperSyncRestoreService { if (isNetworkError && retryCount < MAX_RETRIES) { retryCount++; - console.warn( + SyncLog.warn( `Restore failed due to network error, retrying (${retryCount}/${MAX_RETRIES}):`, error, ); @@ -90,7 +91,7 @@ export class SuperSyncRestoreService { continue; } - console.error('Failed to restore from point:', error); + SyncLog.err('Failed to restore from point:', error); // The server can't replay end-to-end-encrypted ops to reconstruct an // earlier state, so it rejects the restore with a reason mentioning // encryption (surfaced in the thrown error message). Show an actionable diff --git a/src/app/plugins/issue-provider/plugin-issue-provider-adapter.service.ts b/src/app/plugins/issue-provider/plugin-issue-provider-adapter.service.ts index 9784229cc1..b79f8a1c25 100644 --- a/src/app/plugins/issue-provider/plugin-issue-provider-adapter.service.ts +++ b/src/app/plugins/issue-provider/plugin-issue-provider-adapter.service.ts @@ -22,6 +22,7 @@ import { TagService } from '../../features/tag/tag.service'; import { sortTagLabels } from './plugin-tag-utils'; import { getDbDateStr } from '../../util/get-db-date-str'; import { T } from '../../t.const'; +import { PluginLog } from '../../core/log'; @Injectable({ providedIn: 'root' }) export class PluginIssueProviderAdapterService implements IssueServiceInterface { @@ -55,7 +56,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface try { return await provider.definition.testConnection(pluginCfg.pluginConfig, http); } catch (e) { - console.error( + PluginLog.err( `[PluginIssueAdapter] testConnection failed for ${pluginCfg.issueProviderKey}:`, e, ); @@ -78,7 +79,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface return link; } } catch (e) { - console.error( + PluginLog.err( `[PluginIssueAdapter] getIssueLink failed for ${cfg.issueProviderKey}:`, e, ); @@ -90,7 +91,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface const issue = (await this.getById(issueId, issueProviderId)) as PluginIssue | null; return issue?.url ?? ''; } catch (e) { - console.error( + PluginLog.err( `[PluginIssueAdapter] getIssueLink url fallback failed for ${cfg.issueProviderKey}:`, e, ); @@ -114,7 +115,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface resolved.http, ); } catch (e) { - console.error( + PluginLog.err( `[PluginIssueAdapter] getById failed for ${cfg.issueProviderKey}:`, e, ); @@ -172,7 +173,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface issueData: r, })) as SearchResultItem[]; } catch (e) { - console.error( + PluginLog.err( `[PluginIssueAdapter] searchIssues failed for ${cfg.issueProviderKey}:`, e, ); @@ -258,7 +259,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface this._handleRemoteDeletion(task); return null; } - console.error( + PluginLog.err( `[PluginIssueAdapter] getFreshDataForIssueTask failed for ${cfg.issueProviderKey}:`, e, ); @@ -299,7 +300,7 @@ export class PluginIssueProviderAdapterService implements IssueServiceInterface const existingIds = new Set(allExistingIssueIds.map(String)); return results.filter((r) => !existingIds.has(r.id)); } catch (e) { - console.error( + PluginLog.err( `[PluginIssueAdapter] getNewIssuesToAddToBacklog failed for ${cfg.issueProviderKey}:`, e, ); diff --git a/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.spec.ts b/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.spec.ts index e2212f16be..797576eb00 100644 --- a/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.spec.ts +++ b/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.spec.ts @@ -7,6 +7,7 @@ import { PluginCommentsConfig, PluginFieldMapping, } from './plugin-issue-provider.model'; +import { PluginLog } from '../../core/log'; const createMockDefinition = ( overrides: Partial = {}, @@ -73,7 +74,7 @@ describe('PluginIssueProviderRegistryService', () => { it('should warn and reject duplicate registrations', () => { const definition = createMockDefinition(); - spyOn(console, 'warn'); + spyOn(PluginLog, 'warn'); registerProvider(service, 'dup', definition, 'First', 'First', 'icon1', 1000, { singular: 'A', @@ -84,7 +85,7 @@ describe('PluginIssueProviderRegistryService', () => { plural: 'Bs', }); - expect(console.warn).toHaveBeenCalledWith( + expect(PluginLog.warn).toHaveBeenCalledWith( jasmine.stringContaining('Duplicate registration'), ); // The first registration should be preserved diff --git a/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.ts b/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.ts index 7ef2c508a3..9ff8180edf 100644 --- a/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.ts +++ b/src/app/plugins/issue-provider/plugin-issue-provider-registry.service.ts @@ -7,6 +7,7 @@ import { PluginFieldMapping, } from './plugin-issue-provider.model'; import { IssueProviderKey } from '../../features/issue/issue.model'; +import { PluginLog } from '../../core/log'; @Injectable({ providedIn: 'root' }) export class PluginIssueProviderRegistryService { @@ -33,7 +34,7 @@ export class PluginIssueProviderRegistryService { }): void { const key = opts.issueProviderKey ?? `plugin:${opts.pluginId}`; if (this._providers.has(key)) { - console.warn( + PluginLog.warn( `[PluginIssueProviderRegistry] Duplicate registration for '${key}', ignoring.`, ); return; diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index 3bb3d8a0a1..6d4dd6c77f 100644 --- a/src/app/plugins/plugin-bridge.service.ts +++ b/src/app/plugins/plugin-bridge.service.ts @@ -566,7 +566,7 @@ export class PluginBridgeService implements OnDestroy { * Internal method to show plugin index.html as view */ private _showIndexHtmlAsView(pluginId: string): void { - console.log('PluginBridge: Navigating to plugin index view', { + PluginLog.log('PluginBridge: Navigating to plugin index view', { pluginId, }); // Navigate to the plugin index route @@ -888,12 +888,12 @@ export class PluginBridgeService implements OnDestroy { // Use the TaskService remove method which handles deletion properly this._taskService.remove(taskWithSubTasks); - console.log('PluginBridge: Task deleted successfully', { + PluginLog.log('PluginBridge: Task deleted successfully', { taskId, hadSubTasks: taskWithSubTasks.subTasks.length > 0, }); } catch (error) { - console.error('PluginBridge: Failed to delete task:', error); + PluginLog.err('PluginBridge: Failed to delete task:', error); throw error; } } @@ -1169,7 +1169,7 @@ export class PluginBridgeService implements OnDestroy { // below as a normal Error. const entityId = composeId(pluginId, key); this._pluginUserPersistenceService.persistPluginUserData(entityId, dataStr); - console.log('PluginBridge: Plugin data persisted successfully', { + PluginLog.log('PluginBridge: Plugin data persisted successfully', { pluginId, keyLen: key?.length ?? 0, dataSize: new Blob([dataStr]).size, @@ -1225,7 +1225,7 @@ export class PluginBridgeService implements OnDestroy { */ private async _triggerSync(pluginId: string): Promise { try { - console.log('PluginBridge: Triggering sync for plugin', pluginId); + PluginLog.log('PluginBridge: Triggering sync for plugin', pluginId); await this._syncWrapperService.sync(); PluginLog.log('PluginBridge: Sync completed successfully'); } catch (error) { @@ -1273,7 +1273,7 @@ export class PluginBridgeService implements OnDestroy { this._syncAdapterRegistry.unregister(registeredKey); } - console.log('PluginBridge: All hooks unregistered for plugin', { pluginId }); + PluginLog.log('PluginBridge: All hooks unregistered for plugin', { pluginId }); } /** @@ -1293,7 +1293,7 @@ export class PluginBridgeService implements OnDestroy { const currentButtons = this._headerButtons(); this._headerButtons.set([...currentButtons, newButton]); - console.log('PluginBridge: Header button registered', { + PluginLog.log('PluginBridge: Header button registered', { pluginId, headerBtnCfg, }); @@ -1861,7 +1861,7 @@ export class PluginBridgeService implements OnDestroy { try { handler(isFocused); } catch (error) { - console.error('Error in window focus handler:', error); + PluginLog.err('Error in window focus handler:', error); } }); } diff --git a/src/app/plugins/plugin.service.ts b/src/app/plugins/plugin.service.ts index 00d326ff1d..576d083334 100644 --- a/src/app/plugins/plugin.service.ts +++ b/src/app/plugins/plugin.service.ts @@ -503,7 +503,7 @@ export class PluginService implements OnDestroy { } }, 100); }).catch((err) => { - console.error('Plugin activation error:', err); + PluginLog.err('Plugin activation error:', err); }); const updatedState = this._getPluginState(pluginId); @@ -523,7 +523,7 @@ export class PluginService implements OnDestroy { currentState.manifest, ); if (!hasConsent) { - console.log( + PluginLog.log( 'Plugin requires Node.js execution permission but no stored consent found:', state.manifest.id, ); @@ -533,7 +533,7 @@ export class PluginService implements OnDestroy { } } else { // Plugin is not enabled, don't activate it - console.log(`Plugin ${pluginId} is not enabled, skipping activation`); + PluginLog.log(`Plugin ${pluginId} is not enabled, skipping activation`); return null; } @@ -1858,7 +1858,7 @@ export class PluginService implements OnDestroy { // Only check platform availability in Electron environment if (!this._isElectronRuntime()) { - console.warn( + PluginLog.warn( `Plugin ${manifest.id} requires nodeExecution permission which is not available in web environment`, ); return false; diff --git a/src/app/plugins/ui/plugin-index/plugin-index.component.ts b/src/app/plugins/ui/plugin-index/plugin-index.component.ts index 282bb76457..137973f311 100644 --- a/src/app/plugins/ui/plugin-index/plugin-index.component.ts +++ b/src/app/plugins/ui/plugin-index/plugin-index.component.ts @@ -298,7 +298,7 @@ export class PluginIndexComponent implements OnInit, OnDestroy { // Cleanup blob URL if it exists if (this._currentIframeUrl) { cleanupPluginIframeUrl(this._currentIframeUrl); - console.log(`Cleaned up blob URL for plugin: ${currentPluginId}`); + PluginLog.log(`Cleaned up blob URL for plugin: ${currentPluginId}`); this._currentIframeUrl = null; } diff --git a/src/app/root-store/meta/task-shared-meta-reducers/task-shared-deadline.reducer.ts b/src/app/root-store/meta/task-shared-meta-reducers/task-shared-deadline.reducer.ts index c694ffc4e6..1bd18a0be4 100644 --- a/src/app/root-store/meta/task-shared-meta-reducers/task-shared-deadline.reducer.ts +++ b/src/app/root-store/meta/task-shared-meta-reducers/task-shared-deadline.reducer.ts @@ -13,6 +13,7 @@ import { getTag, updateTags, removeTaskFromPlannerDays } from './task-shared-hel import { unique } from '../../../util/unique'; import { getDeadlineAutoPlanDecision } from '../../../features/tasks/util/get-deadline-auto-plan-fields'; import type { DeadlineAutoPlanContext } from '../../../features/tasks/util/get-deadline-auto-plan-fields'; +import { TaskLog } from '../../../core/log'; // ============================================================================= // ACTION HANDLERS @@ -107,15 +108,15 @@ const handleSetDeadline = ( // Input validation if (deadlineDay && !isDBDateStr(deadlineDay)) { - console.error('Invalid deadlineDay format:', deadlineDay); + TaskLog.err('Invalid deadlineDay format:', deadlineDay); return state; } if (deadlineWithTime !== undefined && !Number.isFinite(deadlineWithTime)) { - console.error('Invalid deadlineWithTime:', deadlineWithTime); + TaskLog.err('Invalid deadlineWithTime:', deadlineWithTime); return state; } if (deadlineRemindAt !== undefined && !Number.isFinite(deadlineRemindAt)) { - console.error('Invalid deadlineRemindAt:', deadlineRemindAt); + TaskLog.err('Invalid deadlineRemindAt:', deadlineRemindAt); return state; } diff --git a/src/app/ui/dialog-create-tag/dialog-create-tag.component.ts b/src/app/ui/dialog-create-tag/dialog-create-tag.component.ts index 1786a1dac6..d431646025 100644 --- a/src/app/ui/dialog-create-tag/dialog-create-tag.component.ts +++ b/src/app/ui/dialog-create-tag/dialog-create-tag.component.ts @@ -23,6 +23,7 @@ import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autoc import { MatOption } from '@angular/material/core'; import { MaterialIconsLoaderService } from '../material-icons-loader.service'; import { InputColorPickerComponent } from '../input-color-picker/input-color-picker.component'; +import { Log } from '../../core/log'; export interface CreateTagData { title?: string; @@ -71,7 +72,7 @@ export class DialogCreateTagComponent { const icons = await this._iconLoader.loadIcons(); this.filteredIcons.set(icons.slice(0, 50)); } catch (error) { - console.error('Failed to load material icons:', error); + Log.err('Failed to load material icons:', error); this.filteredIcons.set([]); } } @@ -86,7 +87,7 @@ export class DialogCreateTagComponent { filtered.length = Math.min(50, filtered.length); this.filteredIcons.set(filtered); } catch (error) { - console.error('Failed to filter icons:', error); + Log.err('Failed to filter icons:', error); this.filteredIcons.set([]); } } diff --git a/src/app/ui/formly-image-input/formly-image-input.component.ts b/src/app/ui/formly-image-input/formly-image-input.component.ts index c99a9cb567..690d3f928f 100644 --- a/src/app/ui/formly-image-input/formly-image-input.component.ts +++ b/src/app/ui/formly-image-input/formly-image-input.component.ts @@ -18,6 +18,7 @@ import { UnsplashService } from '../../core/unsplash/unsplash.service'; import { SnackService } from '../../core/snack/snack.service'; import { T } from '../../t.const'; import { IS_ELECTRON } from '../../app.constants'; +import { Log } from '../../core/log'; const MAX_BACKGROUND_IMAGE_FILE_SIZE_BYTES = 256 * 1024; @@ -106,7 +107,7 @@ export class FormlyImageInputComponent extends FieldType { openUnsplashPicker(): void { if (!this.isUnsplashAvailable) { - console.warn('Unsplash service is not available - no API key configured'); + Log.warn('Unsplash service is not available - no API key configured'); return; } diff --git a/src/app/ui/mentions/mention.directive.ts b/src/app/ui/mentions/mention.directive.ts index 9f2847bdf4..0683f929df 100644 --- a/src/app/ui/mentions/mention.directive.ts +++ b/src/app/ui/mentions/mention.directive.ts @@ -368,7 +368,7 @@ export class MentionDirective implements OnChanges { // Check if we have a valid active item before proceeding if (!this.searchList.activeItem) { - console.warn('MentionDirective: No active item available for selection'); + Log.warn('MentionDirective: No active item available for selection'); this.stopSearch(); return false; } diff --git a/src/app/ui/pipes/locale-date.pipe.ts b/src/app/ui/pipes/locale-date.pipe.ts index 37e82689c7..578f4a0b7b 100644 --- a/src/app/ui/pipes/locale-date.pipe.ts +++ b/src/app/ui/pipes/locale-date.pipe.ts @@ -1,6 +1,7 @@ import { inject, Pipe, PipeTransform } from '@angular/core'; import { DatePipe } from '@angular/common'; import { DateTimeFormatService } from '../../core/date-time-format/date-time-format.service'; +import { Log } from '../../core/log'; /** * Custom date pipe that respects the user's configured locale @@ -38,7 +39,7 @@ export class LocaleDatePipe implements PipeTransform { try { return this._datePipe.transform(value, format, timezone, effectiveLocale); } catch (e) { - console.warn('LocaleDatePipe: failed to format value', value, e); + Log.warn('LocaleDatePipe: failed to format value', value, e); return null; } } diff --git a/src/app/ui/tree-dnd/tree.component.ts b/src/app/ui/tree-dnd/tree.component.ts index 914f5a81cf..152afb27be 100644 --- a/src/app/ui/tree-dnd/tree.component.ts +++ b/src/app/ui/tree-dnd/tree.component.ts @@ -38,6 +38,7 @@ import { TreeIndicatorService } from './tree-indicator.service'; import { TREE_CONSTANTS } from './tree-constants'; import { assertTreeId } from './tree-guards'; import { expandCollapseAni } from './tree.animations'; +import { Log } from '../../core/log'; import { dragDelayForTouch } from '../../util/input-intent'; @Component({ @@ -169,7 +170,7 @@ export class TreeDndComponent { this._rootDropEl = this._host.nativeElement.querySelector('.root-drop'); this._indicatorService.clear(); } catch (error) { - console.error('Invalid drag start:', error); + Log.err('Invalid drag start:', error); } } @@ -411,7 +412,7 @@ export class TreeDndComponent { item.reset(); } catch (error) { // Ignore reset errors - they can happen if the item is already destroyed - console.debug('Failed to reset drag item:', error); + Log.debug('Failed to reset drag item:', error); } }; diff --git a/src/app/util/play-sound.spec.ts b/src/app/util/play-sound.spec.ts index 0ee6522eff..4d9be872d0 100644 --- a/src/app/util/play-sound.spec.ts +++ b/src/app/util/play-sound.spec.ts @@ -1,5 +1,6 @@ import { playSound } from './play-sound'; import { closeAudioContext } from './audio-context'; +import { Log } from '../core/log'; describe('playSound', () => { let mockAudioContext: any; @@ -108,12 +109,12 @@ describe('playSound', () => { }); it('should handle errors gracefully', async () => { - const consoleErrorSpy = spyOn(console, 'error'); + const logErrSpy = spyOn(Log, 'err'); fetchSpy.and.returnValue(Promise.reject(new Error('Test error'))); await playSound('nonexistent.mp3'); - expect(consoleErrorSpy).toHaveBeenCalled(); + expect(logErrSpy).toHaveBeenCalled(); }); it('should reuse the same AudioContext for multiple sounds', async () => { diff --git a/src/app/util/play-sound.ts b/src/app/util/play-sound.ts index eb3f73976b..4cb9186154 100644 --- a/src/app/util/play-sound.ts +++ b/src/app/util/play-sound.ts @@ -1,4 +1,5 @@ import { getAudioBuffer, playBuffer } from './audio-context'; +import { Log } from '../core/log'; const BASE = './assets/snd'; @@ -13,6 +14,6 @@ export const playSound = async (filePath: string, vol = 100): Promise => { const buffer = await getAudioBuffer(`${BASE}/${filePath}`); await playBuffer(buffer, vol); } catch (e) { - console.error('Error playing sound:', e); + Log.err('Error playing sound:', e); } };