markupr 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorHandler - Centralized Error Management for markupr
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Categorized error handling (permission, API key, network, capture, critical)
|
|
6
|
+
* - User-friendly error dialogs and notifications
|
|
7
|
+
* - Persistent logging to disk for debugging
|
|
8
|
+
* - Recovery suggestions and system preferences access
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { app, dialog, shell, BrowserWindow, Notification } from 'electron';
|
|
12
|
+
import * as fs from 'fs/promises';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { IPC_CHANNELS } from '../shared/types';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface ErrorContext {
|
|
21
|
+
component: string;
|
|
22
|
+
operation: string;
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
27
|
+
|
|
28
|
+
export interface LogEntry {
|
|
29
|
+
timestamp: string;
|
|
30
|
+
level: LogLevel;
|
|
31
|
+
message: string;
|
|
32
|
+
component?: string;
|
|
33
|
+
operation?: string;
|
|
34
|
+
data?: Record<string, unknown>;
|
|
35
|
+
error?: string;
|
|
36
|
+
stack?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ErrorCategory =
|
|
40
|
+
| 'permission'
|
|
41
|
+
| 'api_key'
|
|
42
|
+
| 'network'
|
|
43
|
+
| 'capture'
|
|
44
|
+
| 'transcription'
|
|
45
|
+
| 'audio'
|
|
46
|
+
| 'file'
|
|
47
|
+
| 'unknown';
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Constants
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
const MAX_LOG_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
|
|
54
|
+
const MAX_LOG_LINES = 10000;
|
|
55
|
+
const LOG_ROTATION_CHECK_INTERVAL_MS = 60000; // Check every minute
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// ErrorHandler Class
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
class ErrorHandler {
|
|
62
|
+
private logPath: string;
|
|
63
|
+
private mainWindow: BrowserWindow | null = null;
|
|
64
|
+
private logBuffer: string[] = [];
|
|
65
|
+
private flushTimer: NodeJS.Timeout | null = null;
|
|
66
|
+
private rotationTimer: NodeJS.Timeout | null = null;
|
|
67
|
+
private isInitialized: boolean = false;
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
this.logPath = path.join(app.getPath('logs'), 'markupr.log');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initialize the error handler
|
|
75
|
+
*/
|
|
76
|
+
async initialize(): Promise<void> {
|
|
77
|
+
if (this.isInitialized) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Ensure log directory exists
|
|
81
|
+
const logDir = path.dirname(this.logPath);
|
|
82
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
83
|
+
|
|
84
|
+
// Start log rotation check
|
|
85
|
+
this.rotationTimer = setInterval(
|
|
86
|
+
() => this.checkLogRotation(),
|
|
87
|
+
LOG_ROTATION_CHECK_INTERVAL_MS
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Start flush timer for buffered logs
|
|
91
|
+
this.flushTimer = setInterval(() => this.flushLogs(), 5000);
|
|
92
|
+
|
|
93
|
+
this.isInitialized = true;
|
|
94
|
+
this.log('info', 'ErrorHandler initialized', { component: 'ErrorHandler' });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('[ErrorHandler] Failed to initialize:', error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set the main window for IPC communication
|
|
102
|
+
*/
|
|
103
|
+
setMainWindow(window: BrowserWindow): void {
|
|
104
|
+
this.mainWindow = window;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ==========================================================================
|
|
108
|
+
// Permission Errors
|
|
109
|
+
// ==========================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handle permission errors and guide user to system settings
|
|
113
|
+
*/
|
|
114
|
+
async handlePermissionError(type: 'microphone' | 'screen'): Promise<boolean> {
|
|
115
|
+
const messages = {
|
|
116
|
+
microphone: {
|
|
117
|
+
title: 'Microphone Access Required',
|
|
118
|
+
message: 'markupr needs microphone access to capture your voice feedback.',
|
|
119
|
+
detail:
|
|
120
|
+
'Click "Open Settings" to grant microphone permission in System Settings.' +
|
|
121
|
+
'\n\nAfter enabling, you may need to restart the app.',
|
|
122
|
+
pane: 'Privacy_Microphone',
|
|
123
|
+
},
|
|
124
|
+
screen: {
|
|
125
|
+
title: 'Screen Recording Required',
|
|
126
|
+
message: 'markupr needs screen recording permission to capture screenshots.',
|
|
127
|
+
detail:
|
|
128
|
+
'Click "Open Settings" to grant screen recording permission in System Settings.' +
|
|
129
|
+
'\n\nYou will need to restart the app after enabling.',
|
|
130
|
+
pane: 'Privacy_ScreenCapture',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const config = messages[type];
|
|
135
|
+
|
|
136
|
+
this.log('warn', `Permission denied: ${type}`, {
|
|
137
|
+
component: 'ErrorHandler',
|
|
138
|
+
operation: 'handlePermissionError',
|
|
139
|
+
data: { permissionType: type },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const { response } = await dialog.showMessageBox({
|
|
143
|
+
type: 'warning',
|
|
144
|
+
buttons: ['Open Settings', 'Cancel'],
|
|
145
|
+
defaultId: 0,
|
|
146
|
+
cancelId: 1,
|
|
147
|
+
title: config.title,
|
|
148
|
+
message: config.message,
|
|
149
|
+
detail: config.detail,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (response === 0) {
|
|
153
|
+
// Open system preferences
|
|
154
|
+
if (process.platform === 'darwin') {
|
|
155
|
+
await shell.openExternal(
|
|
156
|
+
`x-apple.systempreferences:com.apple.preference.security?${config.pane}`
|
|
157
|
+
);
|
|
158
|
+
this.log('info', `Opened system preferences for ${type}`);
|
|
159
|
+
} else if (process.platform === 'win32') {
|
|
160
|
+
// Windows: Open Settings app to privacy section
|
|
161
|
+
await shell.openExternal('ms-settings:privacy-microphone');
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ==========================================================================
|
|
170
|
+
// API Key Errors
|
|
171
|
+
// ==========================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handle API key errors - invalid, missing, or expired
|
|
175
|
+
*/
|
|
176
|
+
handleApiKeyError(error?: Error): void {
|
|
177
|
+
const errorMessage = error?.message || 'API key validation failed';
|
|
178
|
+
|
|
179
|
+
this.log('error', 'API key error', {
|
|
180
|
+
component: 'ErrorHandler',
|
|
181
|
+
operation: 'handleApiKeyError',
|
|
182
|
+
error: errorMessage,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Notify renderer to show settings
|
|
186
|
+
this.emitToRenderer(IPC_CHANNELS.SHOW_SETTINGS, { tab: 'api-key' });
|
|
187
|
+
|
|
188
|
+
// Show notification
|
|
189
|
+
this.notifyUser('API Key Invalid', 'Please check your OpenAI API key in settings.');
|
|
190
|
+
|
|
191
|
+
// Also show dialog for critical operations
|
|
192
|
+
dialog.showMessageBox({
|
|
193
|
+
type: 'warning',
|
|
194
|
+
title: 'API Key Required',
|
|
195
|
+
message: 'Your OpenAI API key is missing or invalid.',
|
|
196
|
+
detail:
|
|
197
|
+
'markupr uses OpenAI for post-session narration transcription. ' +
|
|
198
|
+
'Please enter a valid API key in Settings.\n\n' +
|
|
199
|
+
'Create a key at platform.openai.com/api-keys',
|
|
200
|
+
buttons: ['Open Settings', 'OK'],
|
|
201
|
+
defaultId: 0,
|
|
202
|
+
}).then(({ response }) => {
|
|
203
|
+
if (response === 0) {
|
|
204
|
+
this.emitToRenderer(IPC_CHANNELS.SHOW_SETTINGS, { tab: 'api-key' });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ==========================================================================
|
|
210
|
+
// Network Errors
|
|
211
|
+
// ==========================================================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Handle network errors with user-friendly messaging
|
|
215
|
+
*/
|
|
216
|
+
handleNetworkError(error: Error, context: ErrorContext): void {
|
|
217
|
+
this.log('error', 'Network error', {
|
|
218
|
+
component: context.component,
|
|
219
|
+
operation: context.operation,
|
|
220
|
+
error: error.message,
|
|
221
|
+
data: context.data,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Emit to renderer for UI updates
|
|
225
|
+
this.emitToRenderer(IPC_CHANNELS.NETWORK_ERROR, {
|
|
226
|
+
message: 'Connection issue detected',
|
|
227
|
+
isBuffering: true,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Show non-intrusive notification
|
|
231
|
+
this.notifyUser(
|
|
232
|
+
'Connection Issue',
|
|
233
|
+
'Check your internet connection. Audio is being buffered locally.'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Handle network recovery
|
|
239
|
+
*/
|
|
240
|
+
handleNetworkRecovery(): void {
|
|
241
|
+
this.log('info', 'Network connection restored', {
|
|
242
|
+
component: 'ErrorHandler',
|
|
243
|
+
operation: 'handleNetworkRecovery',
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this.emitToRenderer(IPC_CHANNELS.NETWORK_RESTORED, {});
|
|
247
|
+
this.notifyUser('Connection Restored', 'Transcription service reconnected.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ==========================================================================
|
|
251
|
+
// Capture Errors
|
|
252
|
+
// ==========================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Handle screen capture errors
|
|
256
|
+
* These are usually non-critical - we can continue the session
|
|
257
|
+
*/
|
|
258
|
+
handleCaptureError(error: Error, context: ErrorContext): void {
|
|
259
|
+
this.log('warn', 'Capture failed', {
|
|
260
|
+
component: context.component,
|
|
261
|
+
operation: context.operation,
|
|
262
|
+
error: error.message,
|
|
263
|
+
data: context.data,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Don't show notification for capture errors - they're expected sometimes
|
|
267
|
+
// (e.g., window closed, minimized, etc.)
|
|
268
|
+
|
|
269
|
+
// Just emit to renderer for potential UI feedback
|
|
270
|
+
this.emitToRenderer(IPC_CHANNELS.CAPTURE_WARNING, {
|
|
271
|
+
message: 'Screenshot capture skipped',
|
|
272
|
+
reason: error.message,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ==========================================================================
|
|
277
|
+
// Audio Errors
|
|
278
|
+
// ==========================================================================
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Handle audio capture errors
|
|
282
|
+
*/
|
|
283
|
+
handleAudioError(error: Error, context: ErrorContext): void {
|
|
284
|
+
this.log('error', 'Audio error', {
|
|
285
|
+
component: context.component,
|
|
286
|
+
operation: context.operation,
|
|
287
|
+
error: error.message,
|
|
288
|
+
data: context.data,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Check if it's a permission error
|
|
292
|
+
if (
|
|
293
|
+
error.message.includes('permission') ||
|
|
294
|
+
error.message.includes('denied') ||
|
|
295
|
+
error.message.includes('NotAllowedError')
|
|
296
|
+
) {
|
|
297
|
+
this.handlePermissionError('microphone');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check if it's a device error
|
|
302
|
+
if (
|
|
303
|
+
error.message.includes('device') ||
|
|
304
|
+
error.message.includes('NotFoundError') ||
|
|
305
|
+
error.message.includes('NotReadableError')
|
|
306
|
+
) {
|
|
307
|
+
this.notifyUser(
|
|
308
|
+
'Microphone Error',
|
|
309
|
+
'Could not access microphone. Please check your audio device.'
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Generic audio error
|
|
315
|
+
this.emitToRenderer(IPC_CHANNELS.AUDIO_ERROR, {
|
|
316
|
+
message: error.message,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ==========================================================================
|
|
321
|
+
// Transcription Errors
|
|
322
|
+
// ==========================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Handle transcription service errors
|
|
326
|
+
*/
|
|
327
|
+
handleTranscriptionError(error: Error, context: ErrorContext): void {
|
|
328
|
+
this.log('error', 'Transcription error', {
|
|
329
|
+
component: context.component,
|
|
330
|
+
operation: context.operation,
|
|
331
|
+
error: error.message,
|
|
332
|
+
data: context.data,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Check for auth errors
|
|
336
|
+
if (this.isAuthError(error)) {
|
|
337
|
+
this.handleApiKeyError(error);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check for rate limit errors
|
|
342
|
+
if (this.isRateLimitError(error)) {
|
|
343
|
+
this.notifyUser('Rate Limited', 'Too many requests. Please wait a moment before retrying.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Network errors
|
|
348
|
+
if (this.isNetworkError(error)) {
|
|
349
|
+
this.handleNetworkError(error, context);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Generic transcription error
|
|
354
|
+
this.emitToRenderer(IPC_CHANNELS.TRANSCRIPTION_ERROR, {
|
|
355
|
+
message: 'Transcription service error',
|
|
356
|
+
detail: error.message,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ==========================================================================
|
|
361
|
+
// File System Errors
|
|
362
|
+
// ==========================================================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Handle file system errors
|
|
366
|
+
*/
|
|
367
|
+
handleFileError(error: Error, context: ErrorContext): void {
|
|
368
|
+
this.log('error', 'File system error', {
|
|
369
|
+
component: context.component,
|
|
370
|
+
operation: context.operation,
|
|
371
|
+
error: error.message,
|
|
372
|
+
data: context.data,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
376
|
+
|
|
377
|
+
switch (nodeError.code) {
|
|
378
|
+
case 'ENOENT':
|
|
379
|
+
this.notifyUser('File Not Found', 'The requested file or directory does not exist.');
|
|
380
|
+
break;
|
|
381
|
+
case 'EACCES':
|
|
382
|
+
case 'EPERM':
|
|
383
|
+
this.notifyUser(
|
|
384
|
+
'Permission Denied',
|
|
385
|
+
'Cannot access this file. Check folder permissions.'
|
|
386
|
+
);
|
|
387
|
+
break;
|
|
388
|
+
case 'ENOSPC':
|
|
389
|
+
this.notifyUser(
|
|
390
|
+
'Disk Full',
|
|
391
|
+
'Not enough disk space. Please free up some space and try again.'
|
|
392
|
+
);
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
this.notifyUser('File Error', `Could not complete file operation: ${error.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ==========================================================================
|
|
400
|
+
// Critical Errors
|
|
401
|
+
// ==========================================================================
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Handle critical errors that require app restart
|
|
405
|
+
*/
|
|
406
|
+
handleCriticalError(error: Error, context: ErrorContext): void {
|
|
407
|
+
this.log('error', 'CRITICAL ERROR', {
|
|
408
|
+
component: context.component,
|
|
409
|
+
operation: context.operation,
|
|
410
|
+
error: error.message,
|
|
411
|
+
stack: error.stack,
|
|
412
|
+
data: context.data,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Flush logs immediately
|
|
416
|
+
this.flushLogs();
|
|
417
|
+
|
|
418
|
+
// Show blocking dialog
|
|
419
|
+
dialog
|
|
420
|
+
.showMessageBox({
|
|
421
|
+
type: 'error',
|
|
422
|
+
title: 'Something Went Wrong',
|
|
423
|
+
message: `An error occurred in ${context.component}`,
|
|
424
|
+
detail:
|
|
425
|
+
`${error.message}\n\n` +
|
|
426
|
+
'Your session data has been saved.\n' +
|
|
427
|
+
'Please restart the app to continue.',
|
|
428
|
+
buttons: ['Restart', 'Quit'],
|
|
429
|
+
defaultId: 0,
|
|
430
|
+
cancelId: 1,
|
|
431
|
+
})
|
|
432
|
+
.then(({ response }) => {
|
|
433
|
+
if (response === 0) {
|
|
434
|
+
app.relaunch();
|
|
435
|
+
}
|
|
436
|
+
app.quit();
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ==========================================================================
|
|
441
|
+
// Notifications
|
|
442
|
+
// ==========================================================================
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Show a non-blocking notification to the user
|
|
446
|
+
*/
|
|
447
|
+
notifyUser(title: string, message: string): void {
|
|
448
|
+
// First try to use renderer notification
|
|
449
|
+
this.emitToRenderer(IPC_CHANNELS.NOTIFICATION, { title, message });
|
|
450
|
+
|
|
451
|
+
// Also show system notification if supported
|
|
452
|
+
if (Notification.isSupported()) {
|
|
453
|
+
new Notification({
|
|
454
|
+
title: `markupr: ${title}`,
|
|
455
|
+
body: message,
|
|
456
|
+
silent: true,
|
|
457
|
+
}).show();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ==========================================================================
|
|
462
|
+
// Logging
|
|
463
|
+
// ==========================================================================
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Log a message with context
|
|
467
|
+
*/
|
|
468
|
+
log(
|
|
469
|
+
level: LogLevel,
|
|
470
|
+
message: string,
|
|
471
|
+
context?: Partial<ErrorContext> & { error?: string; stack?: string }
|
|
472
|
+
): void {
|
|
473
|
+
const entry: LogEntry = {
|
|
474
|
+
timestamp: new Date().toISOString(),
|
|
475
|
+
level,
|
|
476
|
+
message,
|
|
477
|
+
component: context?.component,
|
|
478
|
+
operation: context?.operation,
|
|
479
|
+
data: context?.data,
|
|
480
|
+
error: context?.error,
|
|
481
|
+
stack: context?.stack,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// Console output with color
|
|
485
|
+
const colors = {
|
|
486
|
+
debug: '\x1b[90m', // gray
|
|
487
|
+
info: '\x1b[36m', // cyan
|
|
488
|
+
warn: '\x1b[33m', // yellow
|
|
489
|
+
error: '\x1b[31m', // red
|
|
490
|
+
};
|
|
491
|
+
const reset = '\x1b[0m';
|
|
492
|
+
const prefix = `${colors[level]}[${level.toUpperCase()}]${reset}`;
|
|
493
|
+
const componentStr = context?.component ? ` [${context.component}]` : '';
|
|
494
|
+
|
|
495
|
+
console.log(`${prefix}${componentStr} ${message}`, context?.data || '');
|
|
496
|
+
|
|
497
|
+
// Buffer for file output
|
|
498
|
+
this.logBuffer.push(JSON.stringify(entry));
|
|
499
|
+
|
|
500
|
+
// Flush immediately on error
|
|
501
|
+
if (level === 'error') {
|
|
502
|
+
this.flushLogs();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Flush buffered logs to disk
|
|
508
|
+
*/
|
|
509
|
+
private async flushLogs(): Promise<void> {
|
|
510
|
+
if (this.logBuffer.length === 0) return;
|
|
511
|
+
|
|
512
|
+
const logs = this.logBuffer.splice(0);
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
await fs.appendFile(this.logPath, logs.join('\n') + '\n', 'utf-8');
|
|
516
|
+
} catch (error) {
|
|
517
|
+
// Log write failure - output to console only
|
|
518
|
+
console.error('[ErrorHandler] Failed to write logs:', error);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Check if log rotation is needed
|
|
524
|
+
*/
|
|
525
|
+
private async checkLogRotation(): Promise<void> {
|
|
526
|
+
try {
|
|
527
|
+
const stats = await fs.stat(this.logPath);
|
|
528
|
+
|
|
529
|
+
if (stats.size > MAX_LOG_SIZE_BYTES) {
|
|
530
|
+
await this.rotateLogs();
|
|
531
|
+
}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
// File doesn't exist yet or other error - ignore
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Rotate logs - keep last N lines
|
|
539
|
+
*/
|
|
540
|
+
private async rotateLogs(): Promise<void> {
|
|
541
|
+
try {
|
|
542
|
+
const content = await fs.readFile(this.logPath, 'utf-8');
|
|
543
|
+
const lines = content.split('\n');
|
|
544
|
+
|
|
545
|
+
if (lines.length > MAX_LOG_LINES) {
|
|
546
|
+
// Keep last MAX_LOG_LINES/2 lines
|
|
547
|
+
const keepLines = lines.slice(-(MAX_LOG_LINES / 2));
|
|
548
|
+
await fs.writeFile(this.logPath, keepLines.join('\n'), 'utf-8');
|
|
549
|
+
this.log('info', 'Log file rotated', { component: 'ErrorHandler' });
|
|
550
|
+
}
|
|
551
|
+
} catch (error) {
|
|
552
|
+
console.error('[ErrorHandler] Log rotation failed:', error);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get recent logs for error reporting
|
|
558
|
+
*/
|
|
559
|
+
async getRecentLogs(lines: number = 100): Promise<LogEntry[]> {
|
|
560
|
+
try {
|
|
561
|
+
const content = await fs.readFile(this.logPath, 'utf-8');
|
|
562
|
+
const logLines = content.trim().split('\n').slice(-lines);
|
|
563
|
+
|
|
564
|
+
return logLines
|
|
565
|
+
.filter((line) => line.trim())
|
|
566
|
+
.map((line) => {
|
|
567
|
+
try {
|
|
568
|
+
return JSON.parse(line) as LogEntry;
|
|
569
|
+
} catch {
|
|
570
|
+
return { timestamp: '', level: 'info' as LogLevel, message: line };
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
} catch {
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get log file path for support
|
|
580
|
+
*/
|
|
581
|
+
getLogPath(): string {
|
|
582
|
+
return this.logPath;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ==========================================================================
|
|
586
|
+
// Error Classification Helpers
|
|
587
|
+
// ==========================================================================
|
|
588
|
+
|
|
589
|
+
private isAuthError(error: Error): boolean {
|
|
590
|
+
const message = error.message.toLowerCase();
|
|
591
|
+
return (
|
|
592
|
+
message.includes('401') ||
|
|
593
|
+
message.includes('unauthorized') ||
|
|
594
|
+
message.includes('invalid api key') ||
|
|
595
|
+
message.includes('authentication') ||
|
|
596
|
+
message.includes('forbidden')
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private isRateLimitError(error: Error): boolean {
|
|
601
|
+
const message = error.message.toLowerCase();
|
|
602
|
+
return (
|
|
603
|
+
message.includes('429') || message.includes('rate limit') || message.includes('too many')
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private isNetworkError(error: Error): boolean {
|
|
608
|
+
const message = error.message.toLowerCase();
|
|
609
|
+
return (
|
|
610
|
+
message.includes('network') ||
|
|
611
|
+
message.includes('connection') ||
|
|
612
|
+
message.includes('timeout') ||
|
|
613
|
+
message.includes('econnrefused') ||
|
|
614
|
+
message.includes('enotfound') ||
|
|
615
|
+
message.includes('socket')
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Categorize an error for logging/reporting
|
|
621
|
+
*/
|
|
622
|
+
categorizeError(error: Error): ErrorCategory {
|
|
623
|
+
const message = error.message.toLowerCase();
|
|
624
|
+
|
|
625
|
+
if (message.includes('permission') || message.includes('denied')) {
|
|
626
|
+
return 'permission';
|
|
627
|
+
}
|
|
628
|
+
if (this.isAuthError(error)) {
|
|
629
|
+
return 'api_key';
|
|
630
|
+
}
|
|
631
|
+
if (this.isNetworkError(error)) {
|
|
632
|
+
return 'network';
|
|
633
|
+
}
|
|
634
|
+
if (message.includes('capture') || message.includes('screenshot')) {
|
|
635
|
+
return 'capture';
|
|
636
|
+
}
|
|
637
|
+
if (message.includes('transcri')) {
|
|
638
|
+
return 'transcription';
|
|
639
|
+
}
|
|
640
|
+
if (
|
|
641
|
+
message.includes('audio') ||
|
|
642
|
+
message.includes('microphone') ||
|
|
643
|
+
message.includes('media')
|
|
644
|
+
) {
|
|
645
|
+
return 'audio';
|
|
646
|
+
}
|
|
647
|
+
if (
|
|
648
|
+
message.includes('file') ||
|
|
649
|
+
message.includes('directory') ||
|
|
650
|
+
message.includes('enoent') ||
|
|
651
|
+
message.includes('eacces')
|
|
652
|
+
) {
|
|
653
|
+
return 'file';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return 'unknown';
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ==========================================================================
|
|
660
|
+
// IPC Helper
|
|
661
|
+
// ==========================================================================
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Send event to renderer
|
|
665
|
+
*/
|
|
666
|
+
private emitToRenderer(channel: string, data: unknown): void {
|
|
667
|
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
668
|
+
this.mainWindow.webContents.send(channel, data);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ==========================================================================
|
|
673
|
+
// Cleanup
|
|
674
|
+
// ==========================================================================
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Clean up resources
|
|
678
|
+
*/
|
|
679
|
+
async destroy(): Promise<void> {
|
|
680
|
+
if (this.flushTimer) {
|
|
681
|
+
clearInterval(this.flushTimer);
|
|
682
|
+
this.flushTimer = null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (this.rotationTimer) {
|
|
686
|
+
clearInterval(this.rotationTimer);
|
|
687
|
+
this.rotationTimer = null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Final log flush
|
|
691
|
+
await this.flushLogs();
|
|
692
|
+
|
|
693
|
+
this.mainWindow = null;
|
|
694
|
+
this.isInitialized = false;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ============================================================================
|
|
699
|
+
// Singleton Export
|
|
700
|
+
// ============================================================================
|
|
701
|
+
|
|
702
|
+
export const errorHandler = new ErrorHandler();
|
|
703
|
+
export default ErrorHandler;
|