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,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CrashRecovery - Session Recovery and Error Reporting for markupr
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Auto-save session state every 5 seconds during recording (max 5s data loss)
|
|
6
|
+
* - Detection of incomplete sessions on startup
|
|
7
|
+
* - Recovery dialog coordination with renderer
|
|
8
|
+
* - Persistent crash logs for debugging
|
|
9
|
+
* - Optional anonymous crash reporting (user consent)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { app, BrowserWindow } from 'electron';
|
|
13
|
+
import Store from 'electron-store';
|
|
14
|
+
import * as fs from 'fs/promises';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { IPC_CHANNELS } from '../shared/types';
|
|
17
|
+
import { errorHandler } from './ErrorHandler';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Serializable session data for crash recovery
|
|
25
|
+
* Contains all necessary data to restore a session without Buffer objects
|
|
26
|
+
*/
|
|
27
|
+
export interface RecoverableSession {
|
|
28
|
+
id: string;
|
|
29
|
+
startTime: number;
|
|
30
|
+
lastSaveTime: number;
|
|
31
|
+
feedbackItems: RecoverableFeedbackItem[];
|
|
32
|
+
transcriptionBuffer: string;
|
|
33
|
+
sourceId: string;
|
|
34
|
+
sourceName: string;
|
|
35
|
+
screenshotCount: number;
|
|
36
|
+
metadata: {
|
|
37
|
+
appVersion: string;
|
|
38
|
+
platform: string;
|
|
39
|
+
sessionDurationMs: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RecoverableFeedbackItem {
|
|
44
|
+
id: string;
|
|
45
|
+
timestamp: number;
|
|
46
|
+
text: string;
|
|
47
|
+
confidence: number;
|
|
48
|
+
hasScreenshot: boolean;
|
|
49
|
+
screenshotId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CrashLog {
|
|
53
|
+
timestamp: string;
|
|
54
|
+
error: {
|
|
55
|
+
name: string;
|
|
56
|
+
message: string;
|
|
57
|
+
stack?: string;
|
|
58
|
+
};
|
|
59
|
+
appVersion: string;
|
|
60
|
+
platform: string;
|
|
61
|
+
arch: string;
|
|
62
|
+
sessionId?: string;
|
|
63
|
+
context?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CrashRecoverySettings {
|
|
67
|
+
enableAutoSave: boolean;
|
|
68
|
+
autoSaveIntervalMs: number;
|
|
69
|
+
enableCrashReporting: boolean; // User consent for anonymous reporting
|
|
70
|
+
maxCrashLogs: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Store Schema
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
interface CrashRecoveryStoreSchema {
|
|
78
|
+
activeSession: RecoverableSession | null;
|
|
79
|
+
crashLogs: CrashLog[];
|
|
80
|
+
settings: CrashRecoverySettings;
|
|
81
|
+
lastCleanExit: boolean;
|
|
82
|
+
lastExitTimestamp: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DEFAULT_SETTINGS: CrashRecoverySettings = {
|
|
86
|
+
enableAutoSave: true,
|
|
87
|
+
autoSaveIntervalMs: 5000, // 5 seconds (max 5 seconds potential data loss per spec)
|
|
88
|
+
enableCrashReporting: false, // Opt-in by default
|
|
89
|
+
maxCrashLogs: 50,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const store = new Store<CrashRecoveryStoreSchema>({
|
|
93
|
+
name: 'markupr-crash-recovery',
|
|
94
|
+
defaults: {
|
|
95
|
+
activeSession: null,
|
|
96
|
+
crashLogs: [],
|
|
97
|
+
settings: DEFAULT_SETTINGS,
|
|
98
|
+
lastCleanExit: true,
|
|
99
|
+
lastExitTimestamp: 0,
|
|
100
|
+
},
|
|
101
|
+
clearInvalidConfig: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// CrashRecoveryManager Class
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
class CrashRecoveryManager {
|
|
109
|
+
private saveInterval: NodeJS.Timeout | null = null;
|
|
110
|
+
private currentSession: RecoverableSession | null = null;
|
|
111
|
+
private mainWindow: BrowserWindow | null = null;
|
|
112
|
+
private isInitialized = false;
|
|
113
|
+
private crashLogPath: string;
|
|
114
|
+
|
|
115
|
+
constructor() {
|
|
116
|
+
this.crashLogPath = path.join(app.getPath('logs'), 'crash-logs.json');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ==========================================================================
|
|
120
|
+
// Initialization
|
|
121
|
+
// ==========================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Initialize the crash recovery manager
|
|
125
|
+
* Should be called early in app startup
|
|
126
|
+
*/
|
|
127
|
+
async initialize(): Promise<void> {
|
|
128
|
+
if (this.isInitialized) return;
|
|
129
|
+
|
|
130
|
+
errorHandler.log('info', 'CrashRecovery initializing', {
|
|
131
|
+
component: 'CrashRecovery',
|
|
132
|
+
operation: 'initialize',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Check if last exit was clean
|
|
136
|
+
const lastCleanExit = store.get('lastCleanExit');
|
|
137
|
+
const lastExitTimestamp = store.get('lastExitTimestamp');
|
|
138
|
+
|
|
139
|
+
if (!lastCleanExit && lastExitTimestamp > 0) {
|
|
140
|
+
errorHandler.log('warn', 'Previous session did not exit cleanly', {
|
|
141
|
+
component: 'CrashRecovery',
|
|
142
|
+
operation: 'initialize',
|
|
143
|
+
data: { lastExitTimestamp },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Mark as not clean until we properly exit
|
|
148
|
+
store.set('lastCleanExit', false);
|
|
149
|
+
|
|
150
|
+
// Check for incomplete session
|
|
151
|
+
const incomplete = store.get('activeSession');
|
|
152
|
+
if (incomplete) {
|
|
153
|
+
errorHandler.log('info', 'Found incomplete session from previous run', {
|
|
154
|
+
component: 'CrashRecovery',
|
|
155
|
+
operation: 'initialize',
|
|
156
|
+
data: {
|
|
157
|
+
sessionId: incomplete.id,
|
|
158
|
+
feedbackCount: incomplete.feedbackItems.length,
|
|
159
|
+
lastSaveTime: new Date(incomplete.lastSaveTime).toISOString(),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Set up exit handlers
|
|
165
|
+
this.setupExitHandlers();
|
|
166
|
+
|
|
167
|
+
// Migrate crash logs from file if they exist
|
|
168
|
+
await this.migrateCrashLogsFromFile();
|
|
169
|
+
|
|
170
|
+
this.isInitialized = true;
|
|
171
|
+
|
|
172
|
+
errorHandler.log('info', 'CrashRecovery initialized successfully', {
|
|
173
|
+
component: 'CrashRecovery',
|
|
174
|
+
operation: 'initialize',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Set up handlers for clean and unclean exits
|
|
180
|
+
*/
|
|
181
|
+
private setupExitHandlers(): void {
|
|
182
|
+
// Clean exit handlers
|
|
183
|
+
app.on('before-quit', () => {
|
|
184
|
+
this.handleCleanExit();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
app.on('will-quit', () => {
|
|
188
|
+
this.handleCleanExit();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Uncaught exception handler
|
|
192
|
+
process.on('uncaughtException', (error) => {
|
|
193
|
+
this.handleUncaughtException(error);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Unhandled promise rejection handler
|
|
197
|
+
process.on('unhandledRejection', (reason) => {
|
|
198
|
+
const error =
|
|
199
|
+
reason instanceof Error ? reason : new Error(String(reason));
|
|
200
|
+
this.handleUncaughtException(error, 'unhandledRejection');
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handle clean application exit
|
|
206
|
+
*/
|
|
207
|
+
private handleCleanExit(): void {
|
|
208
|
+
errorHandler.log('info', 'Clean exit initiated', {
|
|
209
|
+
component: 'CrashRecovery',
|
|
210
|
+
operation: 'handleCleanExit',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Stop auto-save
|
|
214
|
+
this.stopAutoSave();
|
|
215
|
+
|
|
216
|
+
// Clear active session if no current recording
|
|
217
|
+
if (!this.currentSession) {
|
|
218
|
+
store.delete('activeSession');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Mark clean exit
|
|
222
|
+
store.set('lastCleanExit', true);
|
|
223
|
+
store.set('lastExitTimestamp', Date.now());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle uncaught exceptions
|
|
228
|
+
*/
|
|
229
|
+
private handleUncaughtException(
|
|
230
|
+
error: Error,
|
|
231
|
+
type: string = 'uncaughtException'
|
|
232
|
+
): void {
|
|
233
|
+
errorHandler.log('error', `Uncaught exception: ${type}`, {
|
|
234
|
+
component: 'CrashRecovery',
|
|
235
|
+
operation: 'handleUncaughtException',
|
|
236
|
+
error: error.message,
|
|
237
|
+
stack: error.stack,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Save crash log
|
|
241
|
+
this.logCrash(error, { type });
|
|
242
|
+
|
|
243
|
+
// Force save current session state
|
|
244
|
+
if (this.currentSession) {
|
|
245
|
+
this.currentSession.lastSaveTime = Date.now();
|
|
246
|
+
store.set('activeSession', this.currentSession);
|
|
247
|
+
errorHandler.log('info', 'Session state saved before crash', {
|
|
248
|
+
component: 'CrashRecovery',
|
|
249
|
+
operation: 'handleUncaughtException',
|
|
250
|
+
data: { sessionId: this.currentSession.id },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ==========================================================================
|
|
256
|
+
// Session Tracking
|
|
257
|
+
// ==========================================================================
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Start tracking a new session for crash recovery
|
|
261
|
+
*/
|
|
262
|
+
startTracking(session: RecoverableSession): void {
|
|
263
|
+
errorHandler.log('info', 'Starting session tracking', {
|
|
264
|
+
component: 'CrashRecovery',
|
|
265
|
+
operation: 'startTracking',
|
|
266
|
+
data: { sessionId: session.id },
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.currentSession = {
|
|
270
|
+
...session,
|
|
271
|
+
lastSaveTime: Date.now(),
|
|
272
|
+
metadata: {
|
|
273
|
+
appVersion: app.getVersion(),
|
|
274
|
+
platform: process.platform,
|
|
275
|
+
sessionDurationMs: 0,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Save immediately
|
|
280
|
+
store.set('activeSession', this.currentSession);
|
|
281
|
+
|
|
282
|
+
// Start auto-save interval
|
|
283
|
+
const settings = this.getSettings();
|
|
284
|
+
if (settings.enableAutoSave) {
|
|
285
|
+
this.startAutoSave(settings.autoSaveIntervalMs);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Update the tracked session with new data
|
|
291
|
+
*/
|
|
292
|
+
updateSession(updates: Partial<RecoverableSession>): void {
|
|
293
|
+
if (!this.currentSession) {
|
|
294
|
+
errorHandler.log('warn', 'Attempted to update non-existent session', {
|
|
295
|
+
component: 'CrashRecovery',
|
|
296
|
+
operation: 'updateSession',
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this.currentSession = {
|
|
302
|
+
...this.currentSession,
|
|
303
|
+
...updates,
|
|
304
|
+
lastSaveTime: Date.now(),
|
|
305
|
+
metadata: {
|
|
306
|
+
...this.currentSession.metadata,
|
|
307
|
+
sessionDurationMs: Date.now() - this.currentSession.startTime,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Stop tracking the current session (normal completion)
|
|
314
|
+
*/
|
|
315
|
+
stopTracking(): void {
|
|
316
|
+
errorHandler.log('info', 'Stopping session tracking', {
|
|
317
|
+
component: 'CrashRecovery',
|
|
318
|
+
operation: 'stopTracking',
|
|
319
|
+
data: { sessionId: this.currentSession?.id },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
this.stopAutoSave();
|
|
323
|
+
this.currentSession = null;
|
|
324
|
+
store.delete('activeSession');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ==========================================================================
|
|
328
|
+
// Auto-Save
|
|
329
|
+
// ==========================================================================
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Start the auto-save interval
|
|
333
|
+
*/
|
|
334
|
+
private startAutoSave(intervalMs: number): void {
|
|
335
|
+
this.stopAutoSave();
|
|
336
|
+
|
|
337
|
+
this.saveInterval = setInterval(() => {
|
|
338
|
+
if (this.currentSession) {
|
|
339
|
+
this.currentSession.lastSaveTime = Date.now();
|
|
340
|
+
this.currentSession.metadata.sessionDurationMs =
|
|
341
|
+
Date.now() - this.currentSession.startTime;
|
|
342
|
+
store.set('activeSession', this.currentSession);
|
|
343
|
+
|
|
344
|
+
errorHandler.log('debug', 'Auto-saved session state', {
|
|
345
|
+
component: 'CrashRecovery',
|
|
346
|
+
operation: 'autoSave',
|
|
347
|
+
data: {
|
|
348
|
+
sessionId: this.currentSession.id,
|
|
349
|
+
feedbackCount: this.currentSession.feedbackItems.length,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}, intervalMs);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Stop the auto-save interval
|
|
358
|
+
*/
|
|
359
|
+
private stopAutoSave(): void {
|
|
360
|
+
if (this.saveInterval) {
|
|
361
|
+
clearInterval(this.saveInterval);
|
|
362
|
+
this.saveInterval = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ==========================================================================
|
|
367
|
+
// Recovery
|
|
368
|
+
// ==========================================================================
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Check if there's an incomplete session to recover
|
|
372
|
+
*/
|
|
373
|
+
getIncompleteSession(): RecoverableSession | null {
|
|
374
|
+
return store.get('activeSession') || null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Discard an incomplete session
|
|
379
|
+
*/
|
|
380
|
+
discardIncompleteSession(): void {
|
|
381
|
+
const session = store.get('activeSession');
|
|
382
|
+
if (session) {
|
|
383
|
+
errorHandler.log('info', 'Discarding incomplete session', {
|
|
384
|
+
component: 'CrashRecovery',
|
|
385
|
+
operation: 'discardIncompleteSession',
|
|
386
|
+
data: {
|
|
387
|
+
sessionId: session.id,
|
|
388
|
+
feedbackCount: session.feedbackItems.length,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
store.delete('activeSession');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Notify renderer about incomplete session
|
|
397
|
+
*/
|
|
398
|
+
notifyRendererOfIncompleteSession(): void {
|
|
399
|
+
const incomplete = this.getIncompleteSession();
|
|
400
|
+
if (incomplete && this.mainWindow && !this.mainWindow.isDestroyed()) {
|
|
401
|
+
this.mainWindow.webContents.send(
|
|
402
|
+
IPC_CHANNELS.SESSION_STATE_CHANGED,
|
|
403
|
+
{
|
|
404
|
+
type: 'crash-recovery',
|
|
405
|
+
session: incomplete,
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ==========================================================================
|
|
412
|
+
// Crash Logging
|
|
413
|
+
// ==========================================================================
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Log a crash for debugging
|
|
417
|
+
*/
|
|
418
|
+
private async logCrash(
|
|
419
|
+
error: Error,
|
|
420
|
+
context?: Record<string, unknown>
|
|
421
|
+
): Promise<void> {
|
|
422
|
+
const crashLog: CrashLog = {
|
|
423
|
+
timestamp: new Date().toISOString(),
|
|
424
|
+
error: {
|
|
425
|
+
name: error.name,
|
|
426
|
+
message: error.message,
|
|
427
|
+
stack: error.stack,
|
|
428
|
+
},
|
|
429
|
+
appVersion: app.getVersion(),
|
|
430
|
+
platform: process.platform,
|
|
431
|
+
arch: process.arch,
|
|
432
|
+
sessionId: this.currentSession?.id,
|
|
433
|
+
context,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Store in electron-store
|
|
437
|
+
const settings = this.getSettings();
|
|
438
|
+
const logs = store.get('crashLogs') || [];
|
|
439
|
+
logs.push(crashLog);
|
|
440
|
+
|
|
441
|
+
// Keep only the most recent logs
|
|
442
|
+
while (logs.length > settings.maxCrashLogs) {
|
|
443
|
+
logs.shift();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
store.set('crashLogs', logs);
|
|
447
|
+
|
|
448
|
+
// Also write to file for external access
|
|
449
|
+
await this.writeCrashLogToFile(crashLog);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Write crash log to JSON file
|
|
454
|
+
*/
|
|
455
|
+
private async writeCrashLogToFile(crashLog: CrashLog): Promise<void> {
|
|
456
|
+
try {
|
|
457
|
+
const logDir = path.dirname(this.crashLogPath);
|
|
458
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
459
|
+
|
|
460
|
+
let logs: CrashLog[] = [];
|
|
461
|
+
try {
|
|
462
|
+
const existing = await fs.readFile(this.crashLogPath, 'utf-8');
|
|
463
|
+
logs = JSON.parse(existing);
|
|
464
|
+
} catch {
|
|
465
|
+
// File doesn't exist or is invalid
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
logs.push(crashLog);
|
|
469
|
+
|
|
470
|
+
// Keep last 50 crash logs
|
|
471
|
+
while (logs.length > 50) {
|
|
472
|
+
logs.shift();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await fs.writeFile(this.crashLogPath, JSON.stringify(logs, null, 2));
|
|
476
|
+
} catch (err) {
|
|
477
|
+
console.error('[CrashRecovery] Failed to write crash log to file:', err);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Migrate crash logs from file to store on startup
|
|
483
|
+
*/
|
|
484
|
+
private async migrateCrashLogsFromFile(): Promise<void> {
|
|
485
|
+
try {
|
|
486
|
+
const content = await fs.readFile(this.crashLogPath, 'utf-8');
|
|
487
|
+
const fileLogs: CrashLog[] = JSON.parse(content);
|
|
488
|
+
const storeLogs = store.get('crashLogs') || [];
|
|
489
|
+
|
|
490
|
+
// Merge logs, avoiding duplicates by timestamp
|
|
491
|
+
const existingTimestamps = new Set(storeLogs.map((l) => l.timestamp));
|
|
492
|
+
const newLogs = fileLogs.filter(
|
|
493
|
+
(l) => !existingTimestamps.has(l.timestamp)
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (newLogs.length > 0) {
|
|
497
|
+
const merged = [...storeLogs, ...newLogs].sort(
|
|
498
|
+
(a, b) =>
|
|
499
|
+
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// Keep only the most recent
|
|
503
|
+
const settings = this.getSettings();
|
|
504
|
+
while (merged.length > settings.maxCrashLogs) {
|
|
505
|
+
merged.shift();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
store.set('crashLogs', merged);
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
// File doesn't exist or is invalid - that's fine
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Get recent crash logs
|
|
517
|
+
*/
|
|
518
|
+
getCrashLogs(limit: number = 10): CrashLog[] {
|
|
519
|
+
const logs = store.get('crashLogs') || [];
|
|
520
|
+
return logs.slice(-limit);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Clear crash logs
|
|
525
|
+
*/
|
|
526
|
+
clearCrashLogs(): void {
|
|
527
|
+
store.set('crashLogs', []);
|
|
528
|
+
fs.unlink(this.crashLogPath).catch(() => {
|
|
529
|
+
// Ignore if file doesn't exist
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ==========================================================================
|
|
534
|
+
// Anonymous Crash Reporting
|
|
535
|
+
// ==========================================================================
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Prepare crash report for anonymous submission
|
|
539
|
+
* Strips any potentially identifying information
|
|
540
|
+
*/
|
|
541
|
+
prepareCrashReport(crashLog: CrashLog): Record<string, unknown> {
|
|
542
|
+
return {
|
|
543
|
+
timestamp: crashLog.timestamp,
|
|
544
|
+
error: {
|
|
545
|
+
name: crashLog.error.name,
|
|
546
|
+
message: this.sanitizeErrorMessage(crashLog.error.message),
|
|
547
|
+
// Stack trace without file paths
|
|
548
|
+
stackSummary: this.sanitizeStackTrace(crashLog.error.stack),
|
|
549
|
+
},
|
|
550
|
+
appVersion: crashLog.appVersion,
|
|
551
|
+
platform: crashLog.platform,
|
|
552
|
+
arch: crashLog.arch,
|
|
553
|
+
// Don't include session ID or context
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Sanitize error message to remove potentially identifying info
|
|
559
|
+
*/
|
|
560
|
+
private sanitizeErrorMessage(message: string): string {
|
|
561
|
+
// Remove file paths
|
|
562
|
+
let sanitized = message.replace(/\/Users\/[^/\s]+/g, '/Users/[REDACTED]');
|
|
563
|
+
sanitized = sanitized.replace(/C:\\Users\\[^\\]+/g, 'C:\\Users\\[REDACTED]');
|
|
564
|
+
|
|
565
|
+
// Remove potential API keys
|
|
566
|
+
sanitized = sanitized.replace(
|
|
567
|
+
/[a-zA-Z0-9]{32,}/g,
|
|
568
|
+
'[REDACTED_KEY]'
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
return sanitized;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Sanitize stack trace to remove file paths
|
|
576
|
+
*/
|
|
577
|
+
private sanitizeStackTrace(stack?: string): string[] {
|
|
578
|
+
if (!stack) return [];
|
|
579
|
+
|
|
580
|
+
return stack
|
|
581
|
+
.split('\n')
|
|
582
|
+
.slice(0, 10) // Keep only first 10 lines
|
|
583
|
+
.map((line) => {
|
|
584
|
+
// Remove file paths, keep function names and line numbers
|
|
585
|
+
return line
|
|
586
|
+
.replace(/\/Users\/[^/\s]+/g, '')
|
|
587
|
+
.replace(/C:\\Users\\[^\\]+/g, '')
|
|
588
|
+
.trim();
|
|
589
|
+
})
|
|
590
|
+
.filter((line) => line.length > 0);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ==========================================================================
|
|
594
|
+
// Settings
|
|
595
|
+
// ==========================================================================
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Get crash recovery settings
|
|
599
|
+
*/
|
|
600
|
+
getSettings(): CrashRecoverySettings {
|
|
601
|
+
return store.get('settings') || DEFAULT_SETTINGS;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Update crash recovery settings
|
|
606
|
+
*/
|
|
607
|
+
updateSettings(updates: Partial<CrashRecoverySettings>): void {
|
|
608
|
+
const current = this.getSettings();
|
|
609
|
+
const newSettings = { ...current, ...updates };
|
|
610
|
+
store.set('settings', newSettings);
|
|
611
|
+
|
|
612
|
+
// Apply changes to active session if needed
|
|
613
|
+
if (
|
|
614
|
+
this.currentSession &&
|
|
615
|
+
updates.autoSaveIntervalMs !== undefined
|
|
616
|
+
) {
|
|
617
|
+
this.stopAutoSave();
|
|
618
|
+
if (newSettings.enableAutoSave) {
|
|
619
|
+
this.startAutoSave(newSettings.autoSaveIntervalMs);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ==========================================================================
|
|
625
|
+
// Window Management
|
|
626
|
+
// ==========================================================================
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Set the main window for IPC communication
|
|
630
|
+
*/
|
|
631
|
+
setMainWindow(window: BrowserWindow): void {
|
|
632
|
+
this.mainWindow = window;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ==========================================================================
|
|
636
|
+
// Cleanup
|
|
637
|
+
// ==========================================================================
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Clean up resources
|
|
641
|
+
*/
|
|
642
|
+
destroy(): void {
|
|
643
|
+
this.stopAutoSave();
|
|
644
|
+
this.currentSession = null;
|
|
645
|
+
this.mainWindow = null;
|
|
646
|
+
this.isInitialized = false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ============================================================================
|
|
651
|
+
// Singleton Export
|
|
652
|
+
// ============================================================================
|
|
653
|
+
|
|
654
|
+
export const crashRecovery = new CrashRecoveryManager();
|
|
655
|
+
export default CrashRecoveryManager;
|