documentation-hub 5.7.2
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/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,2131 @@
|
|
|
1
|
+
# Predictive Code Analysis - DocumentHub
|
|
2
|
+
|
|
3
|
+
**Date:** October 18, 2025
|
|
4
|
+
**Repository:** ItMeDiaTech/Documentation_Hub
|
|
5
|
+
**Focus Area:** Initial load performance and ongoing stability issues
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
Comprehensive analysis of the DocumentHub codebase identified **7 critical, high, and medium-priority issues** affecting application startup, memory management, and scalability. All issues include specific file locations, code examples, impact timelines, and proposed solutions.
|
|
12
|
+
|
|
13
|
+
### Risk Breakdown
|
|
14
|
+
|
|
15
|
+
- **3 Critical Issues:** Immediate impact on app functionality and user experience
|
|
16
|
+
- **2 High-Priority Issues:** Will cause major problems within 4-6 weeks
|
|
17
|
+
- **2 Medium-Priority Issues:** Quality-of-life improvements and UX polish
|
|
18
|
+
|
|
19
|
+
### Total Estimated Fix Effort
|
|
20
|
+
|
|
21
|
+
- Critical fixes: ~9 hours
|
|
22
|
+
- High-priority fixes: ~8 hours
|
|
23
|
+
- Medium-priority fixes: ~1.5 hours
|
|
24
|
+
- **Total: ~18.5 hours** (can be parallelized across multiple developers)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## CRITICAL ISSUE #1: Multiple app.whenReady() Race Condition
|
|
29
|
+
|
|
30
|
+
### Classification
|
|
31
|
+
|
|
32
|
+
- **Type:** Bug (Race Condition)
|
|
33
|
+
- **Priority:** 🔴 Critical
|
|
34
|
+
- **Likelihood:** 95%
|
|
35
|
+
- **Impact:** App initialization failures, null reference errors
|
|
36
|
+
- **Timeline:** **ALREADY HAPPENING** - affects every cold start
|
|
37
|
+
|
|
38
|
+
### Problem Description
|
|
39
|
+
|
|
40
|
+
Three separate `app.whenReady()` handlers run in parallel with no guaranteed execution order:
|
|
41
|
+
|
|
42
|
+
**Location 1:** `electron/main.ts:194-261`
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
app.whenReady().then(async () => {
|
|
46
|
+
log.info('Configuring session-level proxy and network monitoring...');
|
|
47
|
+
await proxyConfig.configureSessionProxy();
|
|
48
|
+
// ... proxy and network setup
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Location 2:** `electron/main.ts:572-608`
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
app.whenReady().then(async () => {
|
|
56
|
+
// Create window immediately for better perceived performance
|
|
57
|
+
await createWindow();
|
|
58
|
+
|
|
59
|
+
// Perform pre-flight certificate check in background (non-blocking)
|
|
60
|
+
setImmediate(async () => {
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 500)); // 500ms delay!
|
|
62
|
+
performPreflightCertificateCheck().then(() => {
|
|
63
|
+
/* ... */
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Location 3:** `electron/main.ts:1585-1595`
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
app.whenReady().then(() => {
|
|
73
|
+
// Initialize updater after window is created
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
updaterHandler = new AutoUpdaterHandler(); // mainWindow might be null!
|
|
76
|
+
if (!isDev) {
|
|
77
|
+
updaterHandler.checkOnStartup();
|
|
78
|
+
}
|
|
79
|
+
}, 1000);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Root Cause Analysis
|
|
84
|
+
|
|
85
|
+
1. **No Execution Order Guarantee:** Each `whenReady()` handler executes independently
|
|
86
|
+
2. **Null Reference Risk:** Line 1494 creates `CustomUpdater(mainWindow)` but `mainWindow` might still be `null` if window creation hasn't completed
|
|
87
|
+
3. **Network Race Condition:** Proxy configuration might not finish before window loads, causing API failures
|
|
88
|
+
4. **Artificial Delays:** 500ms delay (line 582) and 1000ms delay (line 1587) are fragile timing assumptions
|
|
89
|
+
|
|
90
|
+
### Impact Analysis
|
|
91
|
+
|
|
92
|
+
**User Experience:**
|
|
93
|
+
|
|
94
|
+
- Black screen on startup (window creates before proxy config completes)
|
|
95
|
+
- Auto-update fails with network errors
|
|
96
|
+
- Occasional crashes with "Cannot read property of null"
|
|
97
|
+
|
|
98
|
+
**Evidence of Existing Issues:**
|
|
99
|
+
|
|
100
|
+
- Recent addition of background certificate check (line 576 comment: "allows app to start immediately") suggests previous blocking behavior
|
|
101
|
+
- Multiple setTimeout/setImmediate workarounds indicate timing issues
|
|
102
|
+
|
|
103
|
+
### Proposed Solution
|
|
104
|
+
|
|
105
|
+
Consolidate into single, sequential initialization flow:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
app.whenReady().then(async () => {
|
|
109
|
+
log.info('========================================');
|
|
110
|
+
log.info('Starting DocumentHub initialization...');
|
|
111
|
+
log.info('========================================');
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// STEP 1: Configure network infrastructure (BLOCKING)
|
|
115
|
+
log.info('[1/4] Configuring proxy and network...');
|
|
116
|
+
await proxyConfig.configureSessionProxy();
|
|
117
|
+
|
|
118
|
+
// STEP 2: Validate certificates (BLOCKING if critical)
|
|
119
|
+
log.info('[2/4] Validating certificates...');
|
|
120
|
+
await performPreflightCertificateCheck();
|
|
121
|
+
|
|
122
|
+
// STEP 3: Create main window (BLOCKING)
|
|
123
|
+
log.info('[3/4] Creating main window...');
|
|
124
|
+
await createWindow();
|
|
125
|
+
|
|
126
|
+
// STEP 4: Initialize background services (NON-BLOCKING)
|
|
127
|
+
log.info('[4/4] Starting background services...');
|
|
128
|
+
setImmediate(() => {
|
|
129
|
+
if (!mainWindow) {
|
|
130
|
+
log.error('Main window is null during updater initialization!');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
updaterHandler = new AutoUpdaterHandler(mainWindow);
|
|
135
|
+
if (!isDev) {
|
|
136
|
+
updaterHandler.checkOnStartup();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.info('✅ DocumentHub initialization complete');
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
log.error('Failed to initialize DocumentHub:', error);
|
|
143
|
+
app.quit();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Acceptance Criteria
|
|
149
|
+
|
|
150
|
+
- [ ] Only ONE `app.whenReady()` handler exists
|
|
151
|
+
- [ ] Proxy configuration completes BEFORE window creation
|
|
152
|
+
- [ ] Certificate validation completes BEFORE network requests
|
|
153
|
+
- [ ] `mainWindow` is guaranteed non-null when AutoUpdaterHandler initializes
|
|
154
|
+
- [ ] No artificial setTimeout delays (use actual completion signals)
|
|
155
|
+
- [ ] All initialization steps logged with clear status messages
|
|
156
|
+
- [ ] App quits gracefully if critical initialization fails
|
|
157
|
+
|
|
158
|
+
### Testing Strategy
|
|
159
|
+
|
|
160
|
+
1. **Cold Start Test:** Restart app 10 times, verify no errors in logs
|
|
161
|
+
2. **Network Timing Test:** Add 2s latency to proxy config, verify app waits
|
|
162
|
+
3. **Certificate Failure Test:** Block GitHub, verify app handles gracefully
|
|
163
|
+
4. **Updater Test:** Verify updater only initializes after window exists
|
|
164
|
+
|
|
165
|
+
### Estimated Effort
|
|
166
|
+
|
|
167
|
+
**2 hours** (1 hour implementation + 1 hour testing)
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## CRITICAL ISSUE #2: Context Provider Cascade Blocks Initial Render
|
|
172
|
+
|
|
173
|
+
### Classification
|
|
174
|
+
|
|
175
|
+
- **Type:** Performance (Synchronous Blocking)
|
|
176
|
+
- **Priority:** 🔴 Critical
|
|
177
|
+
- **Likelihood:** 90%
|
|
178
|
+
- **Impact:** 3-5 second white screen on every app launch
|
|
179
|
+
- **Timeline:** **ALREADY HAPPENING** - worse as database grows
|
|
180
|
+
|
|
181
|
+
### Problem Description
|
|
182
|
+
|
|
183
|
+
Four nested context providers execute synchronous initialization in the render path:
|
|
184
|
+
|
|
185
|
+
**Location:** `src/App.tsx:114-124`
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
<ErrorBoundary>
|
|
189
|
+
<ThemeProvider> {/* 17× localStorage reads! */}
|
|
190
|
+
<UserSettingsProvider> {/* localStorage + JSON parse */}
|
|
191
|
+
<GlobalStatsProvider> {/* IndexedDB open + read */}
|
|
192
|
+
<SessionProvider> {/* IndexedDB open + migration + cleanup */}
|
|
193
|
+
<RouterProvider router={router} />
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Initialization Sequence Breakdown
|
|
197
|
+
|
|
198
|
+
#### 1. ThemeProvider (`src/contexts/ThemeContext.tsx:53-146`)
|
|
199
|
+
|
|
200
|
+
**Blocking Operations:**
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
204
|
+
const stored = localStorage.getItem('theme') as Theme; // Read #1
|
|
205
|
+
return stored || 'system';
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const [accentColor, setAccentColor] = useState<AccentColor>(() => {
|
|
209
|
+
const stored = localStorage.getItem('accentColor') as AccentColor; // Read #2
|
|
210
|
+
return stored || 'blue';
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ... 15 more localStorage.getItem() calls for:
|
|
214
|
+
// - customAccentColor, customPrimaryColor, customBackgroundColor
|
|
215
|
+
// - customHeaderColor, customSidebarColor, customBorderColor
|
|
216
|
+
// - fontSize, fontFamily, fontWeight, fontStyle
|
|
217
|
+
// - letterSpacing, lineHeight, density, animations, blur
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Blocking Time:** ~85-170ms (17 reads × 5-10ms each)
|
|
221
|
+
|
|
222
|
+
#### 2. UserSettingsProvider (`src/contexts/UserSettingsContext.tsx:123-125`)
|
|
223
|
+
|
|
224
|
+
**Blocking Operations:**
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
loadSettings(); // Reads 'userSettings' from localStorage
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
const loadSettings = () => {
|
|
232
|
+
const storedSettings = localStorage.getItem(STORAGE_KEY);
|
|
233
|
+
const parsed = safeJsonParse<Partial<UserSettings>>(
|
|
234
|
+
storedSettings,
|
|
235
|
+
{},
|
|
236
|
+
'UserSettings.loadSettings'
|
|
237
|
+
);
|
|
238
|
+
setSettings({ ...defaultUserSettings, ...parsed });
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Blocking Time:** ~10-20ms
|
|
243
|
+
|
|
244
|
+
#### 3. GlobalStatsProvider (`src/contexts/GlobalStatsContext.tsx:38-100`)
|
|
245
|
+
|
|
246
|
+
**Blocking Operations:**
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
let database: IDBPDatabase<GlobalStatsDB> | null = null;
|
|
251
|
+
|
|
252
|
+
const initDB = async () => {
|
|
253
|
+
database = await openDB<GlobalStatsDB>(DB_NAME, DB_VERSION, {
|
|
254
|
+
upgrade(db) {
|
|
255
|
+
if (!db.objectStoreNames.contains(STATS_STORE)) {
|
|
256
|
+
db.createObjectStore(STATS_STORE); // Schema creation!
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const existingStats = await database.get(STATS_STORE, STATS_KEY);
|
|
262
|
+
|
|
263
|
+
if (existingStats) {
|
|
264
|
+
const updatedStats = checkAndRollOverPeriods(existingStats); // Date calculations
|
|
265
|
+
setStats(updatedStats);
|
|
266
|
+
if (updatedStats !== existingStats) {
|
|
267
|
+
await database.put(STATS_STORE, updatedStats, STATS_KEY); // Write back!
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
initDB();
|
|
273
|
+
}, []);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Blocking Time:** ~100-300ms (IndexedDB is slow!)
|
|
277
|
+
|
|
278
|
+
#### 4. SessionProvider (`src/contexts/SessionContext.tsx:44-125`)
|
|
279
|
+
|
|
280
|
+
**Blocking Operations:**
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const loadSessionsFromStorage = useCallback(async () => {
|
|
284
|
+
// Check localStorage for old sessions
|
|
285
|
+
const hasLocalStorageSessions = localStorage.getItem('sessions');
|
|
286
|
+
|
|
287
|
+
if (hasLocalStorageSessions) {
|
|
288
|
+
log.info('Found sessions in localStorage, migrating to IndexedDB...');
|
|
289
|
+
await migrateFromLocalStorage(); // MIGRATION = VERY SLOW!
|
|
290
|
+
localStorage.removeItem('sessions');
|
|
291
|
+
localStorage.removeItem('activeSessions');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Load all sessions from IndexedDB
|
|
295
|
+
const storedSessions = await loadSessions(); // Reads ALL sessions
|
|
296
|
+
|
|
297
|
+
if (storedSessions && storedSessions.length > 0) {
|
|
298
|
+
const restored: Session[] = storedSessions.map((s) => ({
|
|
299
|
+
...s,
|
|
300
|
+
createdAt: new Date(s.createdAt), // Date parsing
|
|
301
|
+
lastModified: new Date(s.lastModified), // Date parsing
|
|
302
|
+
closedAt: s.closedAt ? new Date(s.closedAt) : undefined,
|
|
303
|
+
documents: s.documents.map((d) => ({
|
|
304
|
+
...d,
|
|
305
|
+
processedAt: d.processedAt ? new Date(d.processedAt) : undefined,
|
|
306
|
+
})),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
// Clean up sessions older than 30 days
|
|
310
|
+
const thirtyDaysAgo = new Date();
|
|
311
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
312
|
+
|
|
313
|
+
const cleanedSessions = restored.filter((s) => {
|
|
314
|
+
if (s.status === 'closed' && s.closedAt) {
|
|
315
|
+
const shouldKeep = s.closedAt > thirtyDaysAgo;
|
|
316
|
+
if (!shouldKeep) {
|
|
317
|
+
deleteSessionFromDB(s.id); // Delete old sessions!
|
|
318
|
+
}
|
|
319
|
+
return shouldKeep;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
setSessions(cleanedSessions);
|
|
325
|
+
}
|
|
326
|
+
}, [log]);
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
loadSessionsFromStorage(); // Runs on mount
|
|
330
|
+
}, [loadSessionsFromStorage]);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Blocking Time:**
|
|
334
|
+
|
|
335
|
+
- Normal: ~200-500ms
|
|
336
|
+
- **With Migration: 2-5 seconds!** (copies all localStorage data to IndexedDB)
|
|
337
|
+
|
|
338
|
+
### Total Initial Load Time
|
|
339
|
+
|
|
340
|
+
**Best Case:** 400-1000ms
|
|
341
|
+
**With Migration:** **3-5 seconds** of white/black screen
|
|
342
|
+
|
|
343
|
+
### Evidence of Existing Issues
|
|
344
|
+
|
|
345
|
+
**From Code Comments:**
|
|
346
|
+
|
|
347
|
+
- Line 198 in SessionContext: `"PERFORMANCE FIX: Increased debounce from 1s to 3s for better UI responsiveness"` - persistence was too slow!
|
|
348
|
+
- Line 199: "This reduces database writes during active editing (drag-drop, processing, etc.) and makes the UI feel much snappier" - clear performance problem
|
|
349
|
+
|
|
350
|
+
**Scaling Analysis:**
|
|
351
|
+
|
|
352
|
+
- 10 sessions: ~500ms load time ✅
|
|
353
|
+
- 50 sessions: ~1.5s load time 🟡
|
|
354
|
+
- 100 sessions: ~3s+ load time 🔴
|
|
355
|
+
- 200 sessions: ~5s+ load time 💥
|
|
356
|
+
|
|
357
|
+
### Root Cause
|
|
358
|
+
|
|
359
|
+
**Architectural Anti-Pattern:**
|
|
360
|
+
All context providers use synchronous initialization in `useState` initializers or immediate `useEffect` calls. React **cannot render anything** until all providers complete their setup, blocking the entire UI thread.
|
|
361
|
+
|
|
362
|
+
### Impact on Users
|
|
363
|
+
|
|
364
|
+
**First Launch (Clean Install):**
|
|
365
|
+
|
|
366
|
+
1. User clicks app icon
|
|
367
|
+
2. Electron window opens (black screen, `backgroundColor: '#0a0a0a'`)
|
|
368
|
+
3. **400-1000ms pass** while contexts initialize
|
|
369
|
+
4. Finally, React UI appears
|
|
370
|
+
|
|
371
|
+
**Migration Scenario (Upgrading from localStorage):**
|
|
372
|
+
|
|
373
|
+
1. User clicks app icon
|
|
374
|
+
2. Black screen appears
|
|
375
|
+
3. **3-5 SECONDS pass** while migration runs
|
|
376
|
+
4. No loading indicator, no progress bar
|
|
377
|
+
5. User thinks app is frozen
|
|
378
|
+
|
|
379
|
+
**Normal Launch (With 50+ Sessions):**
|
|
380
|
+
|
|
381
|
+
1. User clicks app icon
|
|
382
|
+
2. Black screen
|
|
383
|
+
3. **1.5-3 seconds** while all sessions load and deserialize
|
|
384
|
+
4. UI finally appears
|
|
385
|
+
|
|
386
|
+
### Proposed Solution
|
|
387
|
+
|
|
388
|
+
Implement **lazy context initialization** with loading states:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// NEW: Deferred provider pattern
|
|
392
|
+
function App() {
|
|
393
|
+
return (
|
|
394
|
+
<ErrorBoundary>
|
|
395
|
+
<ThemeProvider> {/* Only theme - needed for initial colors */}
|
|
396
|
+
<AppShell /> {/* Shows loading UI immediately */}
|
|
397
|
+
</ThemeProvider>
|
|
398
|
+
</ErrorBoundary>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function AppShell() {
|
|
403
|
+
const [isReady, setIsReady] = useState(false);
|
|
404
|
+
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
// Initialize heavy contexts in background
|
|
407
|
+
Promise.all([
|
|
408
|
+
initUserSettings(),
|
|
409
|
+
initGlobalStats(),
|
|
410
|
+
initSessions(),
|
|
411
|
+
]).then(() => {
|
|
412
|
+
setIsReady(true);
|
|
413
|
+
});
|
|
414
|
+
}, []);
|
|
415
|
+
|
|
416
|
+
if (!isReady) {
|
|
417
|
+
return <SplashScreen />; // Beautiful loading UI
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<UserSettingsProvider>
|
|
422
|
+
<GlobalStatsProvider>
|
|
423
|
+
<SessionProvider>
|
|
424
|
+
<RouterProvider router={router} />
|
|
425
|
+
</SessionProvider>
|
|
426
|
+
</GlobalStatsProvider>
|
|
427
|
+
</UserSettingsProvider>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**Alternative: Code Splitting**
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Lazy load heavy providers
|
|
436
|
+
const SessionProvider = lazy(() => import('@/contexts/SessionContext'));
|
|
437
|
+
const GlobalStatsProvider = lazy(() => import('@/contexts/GlobalStatsContext'));
|
|
438
|
+
|
|
439
|
+
<Suspense fallback={<SplashScreen />}>
|
|
440
|
+
<SessionProvider>
|
|
441
|
+
<GlobalStatsProvider>
|
|
442
|
+
<RouterProvider />
|
|
443
|
+
</GlobalStatsProvider>
|
|
444
|
+
</SessionProvider>
|
|
445
|
+
</Suspense>
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Acceptance Criterias
|
|
449
|
+
|
|
450
|
+
- [ ] App shows UI within 200ms of window creation
|
|
451
|
+
- [ ] Loading indicator displayed during context initialization
|
|
452
|
+
- [ ] Migration progress shown to user (if applicable)
|
|
453
|
+
- [ ] Session loading paginated (load 20 at a time, not all at once)
|
|
454
|
+
- [ ] ThemeProvider loads synchronously (needed for colors)
|
|
455
|
+
- [ ] Other providers load asynchronously with Suspense
|
|
456
|
+
- [ ] No white/black screen longer than 200ms
|
|
457
|
+
|
|
458
|
+
### Performance Benchmarks
|
|
459
|
+
|
|
460
|
+
- Cold start (no data): < 300ms to interactive
|
|
461
|
+
- Normal start (10 sessions): < 500ms to interactive
|
|
462
|
+
- Heavy load (100 sessions): < 1000ms to interactive
|
|
463
|
+
- Migration: Progress indicator visible within 200ms
|
|
464
|
+
|
|
465
|
+
### Testing Strategy
|
|
466
|
+
|
|
467
|
+
1. **Benchmark Test:** Measure time from window creation to first paint
|
|
468
|
+
2. **Migration Test:** Import large localStorage dataset, verify progress shown
|
|
469
|
+
3. **Scaling Test:** Create 100 dummy sessions, verify load time < 1s
|
|
470
|
+
4. **Regression Test:** Ensure all context data still loads correctly
|
|
471
|
+
|
|
472
|
+
### Estimated Effort
|
|
473
|
+
|
|
474
|
+
**4 hours** (2 hours implementation + 2 hours testing + performance tuning)
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## CRITICAL ISSUE #3: GlobalStatsProvider IndexedDB Memory Leak
|
|
479
|
+
|
|
480
|
+
### Classification
|
|
481
|
+
|
|
482
|
+
- **Type:** Bug (Memory Leak)
|
|
483
|
+
- **Priority:** 🔴 Critical
|
|
484
|
+
- **Likelihood:** 80%
|
|
485
|
+
- **Impact:** App crashes after 30-60 minutes of use
|
|
486
|
+
- **Timeline:** 2-4 weeks of normal use → noticeable slowdown; 1-2 months → crashes
|
|
487
|
+
|
|
488
|
+
### Problem Description
|
|
489
|
+
|
|
490
|
+
GlobalStatsProvider creates its own IndexedDB connection instead of using the existing ConnectionPool, potentially leaking connections.
|
|
491
|
+
|
|
492
|
+
**Location:** `src/contexts/GlobalStatsContext.tsx:38-100`
|
|
493
|
+
|
|
494
|
+
**Current Implementation:**
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
export function GlobalStatsProvider({ children }: { children: ReactNode }) {
|
|
498
|
+
const [db, setDb] = useState<IDBPDatabase<GlobalStatsDB> | null>(null);
|
|
499
|
+
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
let database: IDBPDatabase<GlobalStatsDB> | null = null;
|
|
502
|
+
let isMounted = true;
|
|
503
|
+
|
|
504
|
+
const initDB = async () => {
|
|
505
|
+
database = await openDB<GlobalStatsDB>(DB_NAME, DB_VERSION, {
|
|
506
|
+
upgrade(db: IDBPDatabase<GlobalStatsDB>) {
|
|
507
|
+
if (!db.objectStoreNames.contains(STATS_STORE)) {
|
|
508
|
+
db.createObjectStore(STATS_STORE);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (!isMounted) {
|
|
514
|
+
database.close();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
setDb(database); // ❌ Stores DB in state
|
|
519
|
+
// ... initialization code
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
initDB();
|
|
523
|
+
|
|
524
|
+
return () => {
|
|
525
|
+
isMounted = false;
|
|
526
|
+
if (database) {
|
|
527
|
+
database.close(); // ✅ Cleanup on unmount
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}, []); // ❌ Empty deps array
|
|
531
|
+
|
|
532
|
+
const updateStats = useCallback(
|
|
533
|
+
async (update: StatsUpdate) => {
|
|
534
|
+
if (!db) return; // Uses db from state
|
|
535
|
+
|
|
536
|
+
setStats((prevStats) => {
|
|
537
|
+
// ... update logic
|
|
538
|
+
|
|
539
|
+
db.put(STATS_STORE, updatedStats, STATS_KEY).catch((error: Error) =>
|
|
540
|
+
log.error('Failed to save stats:', error)
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
return updatedStats;
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
[db] // ❌ Dependency on db state
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Comparison with SessionContext (Correct Pattern):**
|
|
552
|
+
|
|
553
|
+
SessionContext uses the connection pool from `src/utils/indexedDB.ts`:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// indexedDB.ts has a singleton connection pool
|
|
557
|
+
class IndexedDBConnectionPool {
|
|
558
|
+
private db: IDBDatabase | null = null;
|
|
559
|
+
private isConnecting = false;
|
|
560
|
+
|
|
561
|
+
async getConnection(): Promise<IDBDatabase> {
|
|
562
|
+
if (this.db && this.db.objectStoreNames.length > 0) {
|
|
563
|
+
return this.db; // Reuse existing connection
|
|
564
|
+
}
|
|
565
|
+
// ... create new connection only if needed
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const connectionPool = new IndexedDBConnectionPool();
|
|
570
|
+
|
|
571
|
+
export async function saveSession(session: SerializedSession): Promise<void> {
|
|
572
|
+
const db = await connectionPool.getConnection(); // ✅ Uses pool
|
|
573
|
+
// ... save logic
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Root Cause Analysis
|
|
578
|
+
|
|
579
|
+
1. **Separate DB Instance:** GlobalStatsProvider creates its own `openDB()` call instead of using `connectionPool.getConnection()`
|
|
580
|
+
2. **State Dependency:** `db` is stored in state, triggering re-renders when it changes
|
|
581
|
+
3. **Callback Re-creation:** `updateStats` callback depends on `[db]`, so it recreates when db changes
|
|
582
|
+
4. **Potential Leak:** If `setDb()` is called with a new connection before the old one closes (e.g., during reconnection), the old connection is abandoned but not closed
|
|
583
|
+
|
|
584
|
+
### Memory Leak Scenario
|
|
585
|
+
|
|
586
|
+
```
|
|
587
|
+
Time: 0s - App starts, openDB() creates connection A
|
|
588
|
+
Time: 5s - Connection A stored in state via setDb(A)
|
|
589
|
+
Time: 30m - Network error occurs, connection A becomes invalid
|
|
590
|
+
Time: 30m - useEffect cleanup hasn't run (component still mounted)
|
|
591
|
+
Time: 30m - Auto-reconnect logic (if added) calls openDB() again
|
|
592
|
+
Time: 30m - Connection B created, setDb(B) called
|
|
593
|
+
Time: 30m - Connection A is now orphaned! (not closed, not in state)
|
|
594
|
+
Result: Connection A leaks until app restart
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Impact Analysis
|
|
598
|
+
|
|
599
|
+
**Short-term (0-2 weeks):**
|
|
600
|
+
|
|
601
|
+
- No visible issues
|
|
602
|
+
- Single DB connection per session
|
|
603
|
+
|
|
604
|
+
**Medium-term (2-4 weeks):**
|
|
605
|
+
|
|
606
|
+
- If user has network instability, reconnections create new connections
|
|
607
|
+
- Each orphaned connection holds memory and file handles
|
|
608
|
+
- Gradual slowdown as more connections leak
|
|
609
|
+
|
|
610
|
+
**Long-term (1-2 months):**
|
|
611
|
+
|
|
612
|
+
- Dozens of leaked connections
|
|
613
|
+
- Browser/Electron quota errors
|
|
614
|
+
- App crashes with "Too many open files" or "QuotaExceededError"
|
|
615
|
+
|
|
616
|
+
### Evidence
|
|
617
|
+
|
|
618
|
+
**From indexedDB.ts:**
|
|
619
|
+
|
|
620
|
+
- Lines 36-186: Sophisticated `IndexedDBConnectionPool` class exists
|
|
621
|
+
- Line 189: Singleton instance: `const connectionPool = new IndexedDBConnectionPool()`
|
|
622
|
+
- Lines 211-244: All SessionContext functions use the pool
|
|
623
|
+
- **But GlobalStatsProvider doesn't use it!**
|
|
624
|
+
|
|
625
|
+
**Risk Indicators:**
|
|
626
|
+
|
|
627
|
+
- GlobalStatsProvider uses raw `openDB()` from 'idb' library (line 44)
|
|
628
|
+
- No connection pooling, no reconnection logic
|
|
629
|
+
- State-based DB reference can change, orphaning old connections
|
|
630
|
+
|
|
631
|
+
### Proposed Solution
|
|
632
|
+
|
|
633
|
+
Refactor GlobalStatsProvider to use the existing connection pool:
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
// NEW: Use shared connection pool infrastructure
|
|
637
|
+
import { openDB, DBSchema, IDBPDatabase } from 'idb';
|
|
638
|
+
import { getConnectionPool } from '@/utils/indexedDB'; // ✅ Import pool
|
|
639
|
+
|
|
640
|
+
interface GlobalStatsDB extends DBSchema {
|
|
641
|
+
stats: {
|
|
642
|
+
key: string;
|
|
643
|
+
value: GlobalStats;
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const DB_NAME = 'DocHub_GlobalStats';
|
|
648
|
+
const DB_VERSION = 1;
|
|
649
|
+
const STATS_STORE = 'stats';
|
|
650
|
+
const STATS_KEY = 'global';
|
|
651
|
+
|
|
652
|
+
// ✅ Create connection pool for GlobalStats
|
|
653
|
+
class GlobalStatsConnectionPool {
|
|
654
|
+
private static instance: GlobalStatsConnectionPool;
|
|
655
|
+
private db: IDBPDatabase<GlobalStatsDB> | null = null;
|
|
656
|
+
|
|
657
|
+
static getInstance(): GlobalStatsConnectionPool {
|
|
658
|
+
if (!GlobalStatsConnectionPool.instance) {
|
|
659
|
+
GlobalStatsConnectionPool.instance = new GlobalStatsConnectionPool();
|
|
660
|
+
}
|
|
661
|
+
return GlobalStatsConnectionPool.instance;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async getConnection(): Promise<IDBPDatabase<GlobalStatsDB>> {
|
|
665
|
+
if (this.db) {
|
|
666
|
+
return this.db;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
this.db = await openDB<GlobalStatsDB>(DB_NAME, DB_VERSION, {
|
|
670
|
+
upgrade(db) {
|
|
671
|
+
if (!db.objectStoreNames.contains(STATS_STORE)) {
|
|
672
|
+
db.createObjectStore(STATS_STORE);
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
return this.db;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
close(): void {
|
|
681
|
+
if (this.db) {
|
|
682
|
+
this.db.close();
|
|
683
|
+
this.db = null;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const statsPool = GlobalStatsConnectionPool.getInstance();
|
|
689
|
+
|
|
690
|
+
// ✅ REFACTORED Provider
|
|
691
|
+
export function GlobalStatsProvider({ children }: { children: ReactNode }) {
|
|
692
|
+
const log = logger.namespace('GlobalStats');
|
|
693
|
+
const [stats, setStats] = useState<GlobalStats>(createDefaultGlobalStats());
|
|
694
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
695
|
+
|
|
696
|
+
// Initialize stats from database
|
|
697
|
+
useEffect(() => {
|
|
698
|
+
let isMounted = true;
|
|
699
|
+
|
|
700
|
+
const loadStats = async () => {
|
|
701
|
+
try {
|
|
702
|
+
const db = await statsPool.getConnection(); // ✅ Use pool
|
|
703
|
+
|
|
704
|
+
if (!isMounted) return;
|
|
705
|
+
|
|
706
|
+
const existingStats = await db.get(STATS_STORE, STATS_KEY);
|
|
707
|
+
|
|
708
|
+
if (!isMounted) return;
|
|
709
|
+
|
|
710
|
+
if (existingStats) {
|
|
711
|
+
const updatedStats = checkAndRollOverPeriods(existingStats);
|
|
712
|
+
setStats(updatedStats);
|
|
713
|
+
|
|
714
|
+
if (updatedStats !== existingStats) {
|
|
715
|
+
await db.put(STATS_STORE, updatedStats, STATS_KEY);
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
const defaultStats = createDefaultGlobalStats();
|
|
719
|
+
await db.put(STATS_STORE, defaultStats, STATS_KEY);
|
|
720
|
+
setStats(defaultStats);
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (isMounted) {
|
|
724
|
+
log.error('Failed to initialize GlobalStats:', error);
|
|
725
|
+
}
|
|
726
|
+
} finally {
|
|
727
|
+
if (isMounted) {
|
|
728
|
+
setIsLoading(false);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
loadStats();
|
|
734
|
+
|
|
735
|
+
return () => {
|
|
736
|
+
isMounted = false;
|
|
737
|
+
// NO database.close() here - connection pool manages lifecycle
|
|
738
|
+
};
|
|
739
|
+
}, []);
|
|
740
|
+
|
|
741
|
+
const updateStats = useCallback(
|
|
742
|
+
async (update: StatsUpdate) => {
|
|
743
|
+
try {
|
|
744
|
+
const db = await statsPool.getConnection(); // ✅ Get from pool
|
|
745
|
+
|
|
746
|
+
setStats((prevStats) => {
|
|
747
|
+
const updatedStats = { ...prevStats };
|
|
748
|
+
// ... update logic ...
|
|
749
|
+
|
|
750
|
+
// Persist asynchronously (don't block state update)
|
|
751
|
+
db.put(STATS_STORE, updatedStats, STATS_KEY).catch((error: Error) =>
|
|
752
|
+
log.error('Failed to save stats:', error)
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
return updatedStats;
|
|
756
|
+
});
|
|
757
|
+
} catch (error) {
|
|
758
|
+
log.error('Failed to update stats:', error);
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
[] // ✅ No dependencies - uses pool directly
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
// ... rest of provider
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ✅ Cleanup on app shutdown
|
|
768
|
+
if (typeof window !== 'undefined') {
|
|
769
|
+
window.addEventListener('beforeunload', () => {
|
|
770
|
+
statsPool.close();
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Acceptance Criteria
|
|
776
|
+
|
|
777
|
+
- [ ] GlobalStatsProvider uses connection pool pattern
|
|
778
|
+
- [ ] Only ONE IndexedDB connection created for GlobalStats
|
|
779
|
+
- [ ] Connection persists for entire app lifecycle
|
|
780
|
+
- [ ] No database connections leak on reconnection
|
|
781
|
+
- [ ] updateStats callback doesn't depend on db state
|
|
782
|
+
- [ ] Memory usage stable over 1+ hour session
|
|
783
|
+
- [ ] No "Too many open files" errors
|
|
784
|
+
|
|
785
|
+
### Testing Strategy
|
|
786
|
+
|
|
787
|
+
1. **Memory Leak Test:**
|
|
788
|
+
- Run app for 1 hour with periodic stats updates
|
|
789
|
+
- Monitor open file handles: `lsof -p $(pgrep Electron)` (Linux/Mac) or Process Explorer (Windows)
|
|
790
|
+
- Verify only 1 IndexedDB connection for GlobalStats exists
|
|
791
|
+
|
|
792
|
+
2. **Reconnection Test:**
|
|
793
|
+
- Simulate network interruption to trigger reconnection
|
|
794
|
+
- Verify no duplicate connections created
|
|
795
|
+
- Check memory doesn't grow after reconnects
|
|
796
|
+
|
|
797
|
+
3. **Long-Running Test:**
|
|
798
|
+
- Process 100 documents over 2 hours
|
|
799
|
+
- Monitor memory usage (should be flat)
|
|
800
|
+
- Verify no crashes or quota errors
|
|
801
|
+
|
|
802
|
+
### Estimated Effort
|
|
803
|
+
|
|
804
|
+
**3 hours** (1.5 hours refactoring + 1.5 hours testing and validation)
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## HIGH-PRIORITY ISSUE #4: O(n²) Session Persistence Performance
|
|
809
|
+
|
|
810
|
+
### Classification
|
|
811
|
+
|
|
812
|
+
- **Type:** Performance (Algorithmic Complexity)
|
|
813
|
+
- **Priority:** 🟠 High
|
|
814
|
+
- **Likelihood:** 100%
|
|
815
|
+
- **Impact:** App becomes unusable with 50+ sessions
|
|
816
|
+
- **Timeline:** 4-6 weeks of normal use → slowdown begins; 2-3 months → critical
|
|
817
|
+
|
|
818
|
+
### Problem Description
|
|
819
|
+
|
|
820
|
+
Every 3 seconds, SessionProvider saves ALL sessions to IndexedDB, which triggers expensive cleanup operations that read all sessions again.
|
|
821
|
+
|
|
822
|
+
**Location:** `src/contexts/SessionContext.tsx:139-214`
|
|
823
|
+
|
|
824
|
+
**Current Implementation:**
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
const debouncedPersistSessions = useCallback(async () => {
|
|
828
|
+
try {
|
|
829
|
+
// Critical: Ensure database size limit to prevent quota exceeded errors
|
|
830
|
+
await ensureDBSizeLimit(200); // ❌ Reads ALL sessions to calculate size!
|
|
831
|
+
|
|
832
|
+
const currentSessions = sessionsRef.current;
|
|
833
|
+
const currentActiveSessions = activeSessionsRef.current;
|
|
834
|
+
|
|
835
|
+
const serializedSessions: SerializedSession[] = currentSessions.map((s) => ({
|
|
836
|
+
...s,
|
|
837
|
+
// ... serialization
|
|
838
|
+
}));
|
|
839
|
+
|
|
840
|
+
// ❌ Save EVERY session on EVERY persist!
|
|
841
|
+
for (const session of serializedSessions) {
|
|
842
|
+
const truncatedSession = truncateSessionChanges(session, 100);
|
|
843
|
+
|
|
844
|
+
await handleQuotaExceededError(async () => saveSession(truncatedSession), session.id);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ... save active session IDs to localStorage
|
|
848
|
+
} catch (err) {
|
|
849
|
+
log.error('Failed to persist sessions:', err);
|
|
850
|
+
}
|
|
851
|
+
}, []);
|
|
852
|
+
|
|
853
|
+
// ❌ Triggers on EVERY state change!
|
|
854
|
+
useEffect(() => {
|
|
855
|
+
if (sessions.length > 0) {
|
|
856
|
+
if (persistTimerRef.current) {
|
|
857
|
+
clearTimeout(persistTimerRef.current);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// PERFORMANCE FIX: Increased debounce from 1s to 3s
|
|
861
|
+
persistTimerRef.current = setTimeout(() => {
|
|
862
|
+
debouncedPersistSessions();
|
|
863
|
+
}, 3000);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return () => {
|
|
867
|
+
if (persistTimerRef.current) {
|
|
868
|
+
clearTimeout(persistTimerRef.current);
|
|
869
|
+
persistTimerRef.current = null;
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}, [sessions, activeSessions]); // ❌ Dependency on entire arrays!
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### The Cascading O(n²) Problem
|
|
876
|
+
|
|
877
|
+
**Step 1: User processes 1 document**
|
|
878
|
+
|
|
879
|
+
```typescript
|
|
880
|
+
setSessions((prev) => prev.map(s =>
|
|
881
|
+
s.id === sessionId ? { ...s, documents: [...] } : s
|
|
882
|
+
));
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
→ Triggers `useEffect` dependency `[sessions, activeSessions]`
|
|
886
|
+
|
|
887
|
+
**Step 2: 3 seconds later, persist runs**
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
await ensureDBSizeLimit(200);
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
→ Calls `calculateDBSize()` which:
|
|
894
|
+
|
|
895
|
+
```typescript
|
|
896
|
+
const sessions = await loadSessions(); // ❌ Loads ALL sessions!
|
|
897
|
+
const jsonString = JSON.stringify(sessions); // ❌ Serializes ALL sessions!
|
|
898
|
+
const sizeInBytes = new Blob([jsonString]).size;
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
**Step 3: Save all sessions**
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
for (const session of serializedSessions) {
|
|
905
|
+
// ❌ O(n) sessions
|
|
906
|
+
await handleQuotaExceededError(async () => saveSession(truncatedSession), session.id);
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
**Step 4: If quota exceeded, cleanup triggers**
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
// Inside handleQuotaExceededError (indexedDB.ts:596-647)
|
|
914
|
+
const oldestSessions = await getOldestClosedSessions(20);
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
→ Which calls:
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
const sessions = await store.getAll(); // ❌ Reads ALL sessions AGAIN!
|
|
921
|
+
const closedSessions = sessions
|
|
922
|
+
.filter((s) => s.status === 'closed' && s.closedAt)
|
|
923
|
+
.sort((a, b) => new Date(a.closedAt!).getTime() - new Date(b.closedAt!).getTime());
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
### Complexity Analysis
|
|
927
|
+
|
|
928
|
+
**Operation:** Process 1 document
|
|
929
|
+
**Time Complexity:** O(n²) where n = number of sessions
|
|
930
|
+
|
|
931
|
+
**Breakdown:**
|
|
932
|
+
|
|
933
|
+
1. Update 1 session: O(1)
|
|
934
|
+
2. Serialize all sessions: O(n)
|
|
935
|
+
3. Calculate DB size (read all sessions): O(n)
|
|
936
|
+
4. Save all sessions: O(n)
|
|
937
|
+
5. If quota exceeded, read all sessions for cleanup: O(n)
|
|
938
|
+
|
|
939
|
+
**Total:** O(n) + O(n) + O(n) + O(n) = **O(n)** per operation
|
|
940
|
+
**But happens every 3 seconds**, and the cleanup can trigger another full read!
|
|
941
|
+
|
|
942
|
+
### Scaling Impact
|
|
943
|
+
|
|
944
|
+
| Sessions | Serialize | Size Check | Save All | Cleanup | Total Time | User Impact |
|
|
945
|
+
| -------- | --------- | ---------- | -------- | ------- | ---------- | ------------------------------- |
|
|
946
|
+
| 10 | 5ms | 10ms | 50ms | 10ms | ~75ms | ✅ Acceptable |
|
|
947
|
+
| 50 | 25ms | 50ms | 250ms | 50ms | ~375ms | 🟡 Sluggish |
|
|
948
|
+
| 100 | 50ms | 100ms | 500ms | 100ms | ~750ms | 🔴 Unusable (freezes every 3s!) |
|
|
949
|
+
| 200 | 100ms | 200ms | 1000ms | 200ms | ~1500ms | 💥 App crash (quota exceeded) |
|
|
950
|
+
|
|
951
|
+
### Evidence of Existing Issues
|
|
952
|
+
|
|
953
|
+
**From Code Comments:**
|
|
954
|
+
|
|
955
|
+
- Line 198: `"PERFORMANCE FIX: Increased debounce from 1s to 3s for better UI responsiveness"`
|
|
956
|
+
- **This is treating the symptom, not the cause!**
|
|
957
|
+
- Increased debounce just reduces frequency, doesn't fix O(n) complexity
|
|
958
|
+
|
|
959
|
+
- Line 142: `await ensureDBSizeLimit(200);` on EVERY persist
|
|
960
|
+
- This reads all sessions every time!
|
|
961
|
+
- Meant to prevent quota errors, but causes performance issues
|
|
962
|
+
|
|
963
|
+
- Line 162: `truncateSessionChanges(session, 100)`
|
|
964
|
+
- Desperate attempt to reduce data size
|
|
965
|
+
- Indicates storage is already a problem
|
|
966
|
+
s
|
|
967
|
+
|
|
968
|
+
**Architectural Issues:**
|
|
969
|
+
|
|
970
|
+
1. **Full Save on Every Change:** Saves ALL sessions when only 1 changed
|
|
971
|
+
2. **Eager Size Checking:** Checks DB size on every persist (expensive!)
|
|
972
|
+
3. **Array Dependency:** `useEffect([sessions, activeSessions])` triggers on any change to array reference
|
|
973
|
+
4. **No Dirty Tracking:** No way to know which sessions actually changed
|
|
974
|
+
|
|
975
|
+
### Impact on Users
|
|
976
|
+
|
|
977
|
+
**Scenario 1: Processing 10 documents in a row**
|
|
978
|
+
|
|
979
|
+
- Each document triggers state change
|
|
980
|
+
- 3s debounce means saves every 3s during processing
|
|
981
|
+
- With 50 sessions: UI freezes for 375ms every 3 seconds
|
|
982
|
+
- **User experience:** App feels sluggish and unresponsive
|
|
983
|
+
|
|
984
|
+
**Scenario 2: Long-running session (2+ months)**
|
|
985
|
+
|
|
986
|
+
- User accumulates 100+ closed sessions
|
|
987
|
+
- Every persist: 750ms+ blocking time
|
|
988
|
+
- App becomes unusable for basic tasks
|
|
989
|
+
|
|
990
|
+
**Scenario 3: Quota exceeded cascade**
|
|
991
|
+
|
|
992
|
+
- DB size hits 200MB limit
|
|
993
|
+
- Cleanup runs, reads all 200 sessions to find oldest
|
|
994
|
+
- Deletes 10 sessions, saves all remaining 190
|
|
995
|
+
- Triggers another size check → **infinite loop risk!**
|
|
996
|
+
|
|
997
|
+
### Proposed Solution
|
|
998
|
+
|
|
999
|
+
Implement **incremental persistence** with dirty tracking:
|
|
1000
|
+
|
|
1001
|
+
```typescript
|
|
1002
|
+
// NEW: Track which sessions changed
|
|
1003
|
+
const dirtySessionsRef = useRef<Set<string>>(new Set());
|
|
1004
|
+
|
|
1005
|
+
const markSessionDirty = (sessionId: string) => {
|
|
1006
|
+
dirtySessionsRef.current.add(sessionId);
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// REFACTORED: Only save dirty sessions
|
|
1010
|
+
const debouncedPersistSessions = useCallback(async () => {
|
|
1011
|
+
try {
|
|
1012
|
+
const currentSessions = sessionsRef.current;
|
|
1013
|
+
const dirtyIds = Array.from(dirtySessionsRef.current);
|
|
1014
|
+
|
|
1015
|
+
if (dirtyIds.length === 0) {
|
|
1016
|
+
return; // Nothing to save
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
log.debug(`[Persist] Saving ${dirtyIds.length} dirty session(s)`);
|
|
1020
|
+
|
|
1021
|
+
// ✅ Only save sessions that changed
|
|
1022
|
+
for (const sessionId of dirtyIds) {
|
|
1023
|
+
const session = currentSessions.find((s) => s.id === sessionId);
|
|
1024
|
+
|
|
1025
|
+
if (!session) continue;
|
|
1026
|
+
|
|
1027
|
+
const serialized: SerializedSession = {
|
|
1028
|
+
...session,
|
|
1029
|
+
createdAt: session.createdAt.toISOString(),
|
|
1030
|
+
lastModified: session.lastModified.toISOString(),
|
|
1031
|
+
closedAt: session.closedAt?.toISOString(),
|
|
1032
|
+
documents: session.documents.map((d) => ({
|
|
1033
|
+
...d,
|
|
1034
|
+
processedAt: d.processedAt?.toISOString(),
|
|
1035
|
+
})),
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
const truncated = truncateSessionChanges(serialized, 100);
|
|
1039
|
+
await saveSession(truncated);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Clear dirty tracking
|
|
1043
|
+
dirtySessionsRef.current.clear();
|
|
1044
|
+
|
|
1045
|
+
// ✅ Check size only once per 10 minutes (not every persist!)
|
|
1046
|
+
const lastSizeCheck = localStorage.getItem('lastDBSizeCheck');
|
|
1047
|
+
const now = Date.now();
|
|
1048
|
+
|
|
1049
|
+
if (!lastSizeCheck || now - parseInt(lastSizeCheck) > 10 * 60 * 1000) {
|
|
1050
|
+
log.debug('[Persist] Running periodic size check...');
|
|
1051
|
+
await ensureDBSizeLimit(200);
|
|
1052
|
+
localStorage.setItem('lastDBSizeCheck', now.toString());
|
|
1053
|
+
}
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
log.error('Failed to persist sessions:', err);
|
|
1056
|
+
}
|
|
1057
|
+
}, []);
|
|
1058
|
+
|
|
1059
|
+
// ✅ Track session modifications
|
|
1060
|
+
const updateSession = (sessionId: string, updates: Partial<Session>) => {
|
|
1061
|
+
setSessions((prev) =>
|
|
1062
|
+
prev.map((s) => (s.id === sessionId ? { ...s, ...updates, lastModified: new Date() } : s))
|
|
1063
|
+
);
|
|
1064
|
+
markSessionDirty(sessionId); // ✅ Mark as needing save
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
// ✅ Only trigger persist when dirty sessions exist
|
|
1068
|
+
useEffect(() => {
|
|
1069
|
+
if (sessions.length > 0 && dirtySessionsRef.current.size > 0) {
|
|
1070
|
+
if (persistTimerRef.current) {
|
|
1071
|
+
clearTimeout(persistTimerRef.current);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
persistTimerRef.current = setTimeout(() => {
|
|
1075
|
+
debouncedPersistSessions();
|
|
1076
|
+
}, 3000);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return () => {
|
|
1080
|
+
if (persistTimerRef.current) {
|
|
1081
|
+
clearTimeout(persistTimerRef.current);
|
|
1082
|
+
persistTimerRef.current = null;
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}, [sessions]); // Still depends on sessions, but only saves dirty ones
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Alternative: Batch Write with Transactions**
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
// Use IndexedDB transaction for atomic batch write
|
|
1092
|
+
const transaction = db.transaction([SESSIONS_STORE], 'readwrite');
|
|
1093
|
+
const store = transaction.objectStore(SESSIONS_STORE);
|
|
1094
|
+
|
|
1095
|
+
for (const session of dirtySessions) {
|
|
1096
|
+
store.put(session); // All writes in single transaction
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
await transaction.complete; // Commit atomically
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
### Acceptance Criteria
|
|
1103
|
+
|
|
1104
|
+
- [ ] Only modified sessions are persisted (not all sessions)
|
|
1105
|
+
- [ ] DB size check runs maximum once per 10 minutes
|
|
1106
|
+
- [ ] Dirty tracking correctly identifies changed sessions
|
|
1107
|
+
- [ ] Persist time scales linearly with # of changed sessions (not total)
|
|
1108
|
+
- [ ] 100 sessions with 1 change: < 50ms persist time
|
|
1109
|
+
- [ ] No performance degradation as total session count grows
|
|
1110
|
+
|
|
1111
|
+
### Performance Benchmarks
|
|
1112
|
+
|
|
1113
|
+
**Before Fix:**
|
|
1114
|
+
|
|
1115
|
+
- 10 sessions, 1 change: ~75ms
|
|
1116
|
+
- 50 sessions, 1 change: ~375ms ⚠️
|
|
1117
|
+
- 100 sessions, 1 change: ~750ms 🔴
|
|
1118
|
+
|
|
1119
|
+
**After Fix (Target):**
|
|
1120
|
+
|
|
1121
|
+
- 10 sessions, 1 change: ~5ms ✅
|
|
1122
|
+
- 50 sessions, 1 change: ~5ms ✅
|
|
1123
|
+
- 100 sessions, 1 change: ~5ms ✅
|
|
1124
|
+
- 100 sessions, 10 changes: ~50ms ✅
|
|
1125
|
+
|
|
1126
|
+
**Improvement:** ~15x faster for typical usage
|
|
1127
|
+
|
|
1128
|
+
### Testing Strategy
|
|
1129
|
+
|
|
1130
|
+
1. **Dirty Tracking Test:**
|
|
1131
|
+
- Create 50 sessions
|
|
1132
|
+
- Modify 1 session
|
|
1133
|
+
- Verify only 1 session written to IndexedDB
|
|
1134
|
+
|
|
1135
|
+
2. **Scaling Test:**
|
|
1136
|
+
- Create 100 sessions
|
|
1137
|
+
- Process 1 document
|
|
1138
|
+
- Measure persist time (should be < 50ms)
|
|
1139
|
+
|
|
1140
|
+
3. **Size Check Test:**
|
|
1141
|
+
- Process 10 documents in 5 minutes
|
|
1142
|
+
- Verify `ensureDBSizeLimit()` only called once
|
|
1143
|
+
|
|
1144
|
+
4. **Regression Test:**
|
|
1145
|
+
- Ensure all session data still persists correctly
|
|
1146
|
+
- No data loss on app restart
|
|
1147
|
+
|
|
1148
|
+
### Estimated Effort
|
|
1149
|
+
|
|
1150
|
+
**6 hours** (3 hours implementation + 2 hours testing + 1 hour performance tuning)
|
|
1151
|
+
|
|
1152
|
+
---
|
|
1153
|
+
|
|
1154
|
+
## HIGH-PRIORITY ISSUE #5: Theme Context Infinite Loop on Error
|
|
1155
|
+
|
|
1156
|
+
### Classification
|
|
1157
|
+
|
|
1158
|
+
- **Type:** Bug (Infinite Re-render Loop)
|
|
1159
|
+
- **Priority:** 🟠 High
|
|
1160
|
+
- **Likelihood:** 60%
|
|
1161
|
+
- **Impact:** App freeze/crash when theme color parsing fails
|
|
1162
|
+
- **Timeline:** 1-2 weeks (user enters invalid color value)
|
|
1163
|
+
|
|
1164
|
+
### Problem Description
|
|
1165
|
+
|
|
1166
|
+
ThemeContext calls `setState` inside a `useEffect` error handler, which can trigger an infinite re-render loop.
|
|
1167
|
+
|
|
1168
|
+
**Location:** `src/contexts/ThemeContext.tsx:203-268`
|
|
1169
|
+
|
|
1170
|
+
**Problematic Code:**
|
|
1171
|
+
|
|
1172
|
+
```typescript
|
|
1173
|
+
// Apply custom colors when enabled
|
|
1174
|
+
useEffect(() => {
|
|
1175
|
+
const root = window.document.documentElement;
|
|
1176
|
+
|
|
1177
|
+
if (useCustomColors) {
|
|
1178
|
+
try {
|
|
1179
|
+
root.setAttribute('data-custom-colors', 'true');
|
|
1180
|
+
|
|
1181
|
+
log.debug('[ThemeContext] Applying custom colors...');
|
|
1182
|
+
|
|
1183
|
+
// Calculate optimal text colors based on background colors
|
|
1184
|
+
const foregroundColor = getContrastTextColor(customBackgroundColor);
|
|
1185
|
+
const headerTextColor = getContrastTextColor(customHeaderColor);
|
|
1186
|
+
// ... more color calculations
|
|
1187
|
+
|
|
1188
|
+
// Convert and apply all custom colors
|
|
1189
|
+
root.style.setProperty('--custom-primary', hexToHSL(customPrimaryColor));
|
|
1190
|
+
root.style.setProperty('--custom-primary-text', hexToHSL(primaryTextColor));
|
|
1191
|
+
// ... more setProperty calls
|
|
1192
|
+
|
|
1193
|
+
log.debug('[ThemeContext] Custom colors applied successfully');
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
log.error('[ThemeContext] Error applying custom colors:', error);
|
|
1196
|
+
log.error('[ThemeContext] Color values:', {
|
|
1197
|
+
customPrimaryColor,
|
|
1198
|
+
customBackgroundColor,
|
|
1199
|
+
customHeaderColor,
|
|
1200
|
+
customSidebarColor,
|
|
1201
|
+
customBorderColor,
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
// ❌ INFINITE LOOP TRAP!
|
|
1205
|
+
setUseCustomColors(false); // Triggers useEffect again!
|
|
1206
|
+
root.removeAttribute('data-custom-colors');
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
root.removeAttribute('data-custom-colors');
|
|
1210
|
+
// ... removeProperty calls
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
localStorage.setItem('useCustomColors', String(useCustomColors));
|
|
1214
|
+
if (useCustomColors) {
|
|
1215
|
+
localStorage.setItem('customPrimaryColor', customPrimaryColor);
|
|
1216
|
+
// ... more localStorage sets
|
|
1217
|
+
}
|
|
1218
|
+
}, [useCustomColors, customPrimaryColor, customBackgroundColor /* 3 more deps */]);
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
### The Infinite Loop Scenario
|
|
1222
|
+
|
|
1223
|
+
**Step 1: User enters invalid color**
|
|
1224
|
+
|
|
1225
|
+
```
|
|
1226
|
+
User inputs "#GGGGGG" as custom primary color
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
**Step 2: useEffect runs**
|
|
1230
|
+
|
|
1231
|
+
```typescript
|
|
1232
|
+
try {
|
|
1233
|
+
root.style.setProperty('--custom-primary', hexToHSL('#GGGGGG'));
|
|
1234
|
+
// hexToHSL() throws error: "Invalid hex color"
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
setUseCustomColors(false); // ❌ State change!
|
|
1237
|
+
}
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
**Step 3: State change triggers re-render**
|
|
1241
|
+
|
|
1242
|
+
```
|
|
1243
|
+
useCustomColors changes: true → false
|
|
1244
|
+
useEffect dependency [useCustomColors, ...] changes
|
|
1245
|
+
useEffect runs again
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
**Step 4: If error persists (e.g., bad color in localStorage)**
|
|
1249
|
+
|
|
1250
|
+
```
|
|
1251
|
+
Loop continues:
|
|
1252
|
+
useEffect → error → setUseCustomColors(false) → useEffect → error → ...
|
|
1253
|
+
```
|
|
1254
|
+
|
|
1255
|
+
**Result:** React detects loop, logs error:
|
|
1256
|
+
|
|
1257
|
+
```
|
|
1258
|
+
Warning: Maximum update depth exceeded. This can happen when a component
|
|
1259
|
+
calls setState inside useEffect, and the setState causes the effect to run again.
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
### Additional Issues in Theme Application
|
|
1263
|
+
|
|
1264
|
+
**Problem 2: Multiple useEffect hooks modify same DOM element**
|
|
1265
|
+
|
|
1266
|
+
**Locations:**
|
|
1267
|
+
|
|
1268
|
+
- Line 150-178: Theme (light/dark) application
|
|
1269
|
+
- Line 180-200: Accent color application
|
|
1270
|
+
- Line 203-268: Custom colors application
|
|
1271
|
+
- Line 270-276: Density application
|
|
1272
|
+
- Line 278-288: Animations application
|
|
1273
|
+
- Line 290-300: Blur effects application
|
|
1274
|
+
- Line 303-318: Typography application
|
|
1275
|
+
|
|
1276
|
+
**All modify:** `window.document.documentElement`
|
|
1277
|
+
|
|
1278
|
+
**Race Condition Risk:**
|
|
1279
|
+
If any of these effects run simultaneously (likely during initial mount), they could conflict when setting/removing attributes.
|
|
1280
|
+
|
|
1281
|
+
**Problem 3: No validation before applying colors**
|
|
1282
|
+
|
|
1283
|
+
Colors are applied directly without validation:
|
|
1284
|
+
|
|
1285
|
+
```typescript
|
|
1286
|
+
root.style.setProperty('--custom-primary', hexToHSL(customPrimaryColor));
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
If `hexToHSL()` throws, the entire effect fails and triggers recovery (setState).
|
|
1290
|
+
|
|
1291
|
+
### Root Cause Analysis
|
|
1292
|
+
|
|
1293
|
+
1. **Error Recovery in useEffect:** Calling `setState` inside an effect's error handler creates a feedback loop
|
|
1294
|
+
2. **No Color Validation:** Invalid colors aren't caught before attempting to apply them
|
|
1295
|
+
3. **Lack of Error Boundaries:** No fallback mechanism to prevent cascading failures
|
|
1296
|
+
4. **Too Many Effects:** 7 separate useEffect hooks all modifying the same DOM element
|
|
1297
|
+
|
|
1298
|
+
### Impact on Users
|
|
1299
|
+
|
|
1300
|
+
**Scenario 1: User enters invalid color in settings**
|
|
1301
|
+
|
|
1302
|
+
```
|
|
1303
|
+
1. User types "#ZZZZZZ" in custom color picker
|
|
1304
|
+
2. App attempts to apply color
|
|
1305
|
+
3. hexToHSL() throws error
|
|
1306
|
+
4. setUseCustomColors(false) triggers
|
|
1307
|
+
5. useEffect runs again, loop continues
|
|
1308
|
+
6. Browser shows "Page Unresponsive" warning
|
|
1309
|
+
7. User forced to force-quit app
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
**Scenario 2: Corrupted localStorage**
|
|
1313
|
+
|
|
1314
|
+
```
|
|
1315
|
+
1. User has invalid color in localStorage from manual edit
|
|
1316
|
+
2. App loads, ThemeProvider initializes
|
|
1317
|
+
3. Tries to apply color, fails, disables custom colors
|
|
1318
|
+
4. Effect runs again (because dependencies changed)
|
|
1319
|
+
5. Still fails (localStorage value unchanged)
|
|
1320
|
+
6. Infinite loop on EVERY app launch
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
### Evidence of Existing Issues
|
|
1324
|
+
|
|
1325
|
+
**From Code Comments:**
|
|
1326
|
+
|
|
1327
|
+
- Line 232: `"Disable custom colors on error to prevent cascading failures"`
|
|
1328
|
+
- Confirms they've experienced failures before
|
|
1329
|
+
- The "fix" (setUseCustomColors) creates new problem
|
|
1330
|
+
|
|
1331
|
+
- Line 207: Entire try-catch block wraps color application
|
|
1332
|
+
- Defensive programming suggests this has failed in production
|
|
1333
|
+
|
|
1334
|
+
### Proposed Solution
|
|
1335
|
+
|
|
1336
|
+
**Solution 1: Validate colors before applying**
|
|
1337
|
+
|
|
1338
|
+
```typescript
|
|
1339
|
+
// NEW: Color validation utility
|
|
1340
|
+
function isValidHexColor(color: string): boolean {
|
|
1341
|
+
return /^#[0-9A-F]{6}$/i.test(color);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// REFACTORED: Validate in state setter, not in effect
|
|
1345
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
1346
|
+
const [customPrimaryColor, setCustomPrimaryColor] = useState<string>(() => {
|
|
1347
|
+
const stored = localStorage.getItem('customPrimaryColor') || '#3b82f6';
|
|
1348
|
+
return isValidHexColor(stored) ? stored : '#3b82f6'; // ✅ Validate on load
|
|
1349
|
+
});
|
|
1350
|
+
// ✅ Validate in setter
|
|
1351
|
+
const updateCustomPrimaryColor = (color: string) => {
|
|
1352
|
+
if (isValidHexColor(color)) {
|
|
1353
|
+
setCustomPrimaryColor(color);
|
|
1354
|
+
} else {
|
|
1355
|
+
log.warn(`[ThemeContext] Invalid hex color: ${color}, using default`);
|
|
1356
|
+
setCustomPrimaryColor('#3b82f6');
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// ✅ Effect no longer needs error recovery
|
|
1361
|
+
useEffect(() => {
|
|
1362
|
+
const root = window.document.documentElement;
|
|
1363
|
+
|
|
1364
|
+
if (useCustomColors) {
|
|
1365
|
+
root.setAttribute('data-custom-colors', 'true');
|
|
1366
|
+
|
|
1367
|
+
// Safe to apply - already validated
|
|
1368
|
+
const foregroundColor = getContrastTextColor(customBackgroundColor);
|
|
1369
|
+
root.style.setProperty('--custom-primary', hexToHSL(customPrimaryColor));
|
|
1370
|
+
// ... rest of color application
|
|
1371
|
+
|
|
1372
|
+
localStorage.setItem('customPrimaryColor', customPrimaryColor);
|
|
1373
|
+
} else {
|
|
1374
|
+
root.removeAttribute('data-custom-colors');
|
|
1375
|
+
// ... cleanup
|
|
1376
|
+
}
|
|
1377
|
+
}, [useCustomColors, customPrimaryColor, customBackgroundColor /* deps */]);
|
|
1378
|
+
}
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
**Solution 2: Use error boundary for catastrophic failures**
|
|
1382
|
+
|
|
1383
|
+
```typescript
|
|
1384
|
+
// Wrap entire provider in error boundary
|
|
1385
|
+
<ErrorBoundary fallback={<ThemeFallback />}>
|
|
1386
|
+
<ThemeProvider>
|
|
1387
|
+
{children}
|
|
1388
|
+
</ThemeProvider>
|
|
1389
|
+
</ErrorBoundary>
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
**Solution 3: Consolidate effects**
|
|
1393
|
+
|
|
1394
|
+
```typescript
|
|
1395
|
+
// Instead of 7 separate useEffect hooks, use one coordinated effect
|
|
1396
|
+
useEffect(() => {
|
|
1397
|
+
const root = window.document.documentElement;
|
|
1398
|
+
|
|
1399
|
+
// Apply all theme settings atomically
|
|
1400
|
+
try {
|
|
1401
|
+
applyTheme(root, theme, resolvedTheme);
|
|
1402
|
+
applyAccentColor(root, accentColor, customAccentColor);
|
|
1403
|
+
applyCustomColors(root, useCustomColors, { customPrimaryColor /* ... */ });
|
|
1404
|
+
applyDensity(root, density);
|
|
1405
|
+
applyAnimations(root, animations);
|
|
1406
|
+
applyBlur(root, blur);
|
|
1407
|
+
applyTypography(root, { fontSize, fontFamily /* ... */ });
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
log.error('[ThemeContext] Failed to apply theme:', error);
|
|
1410
|
+
// DON'T call setState here - just log and use defaults
|
|
1411
|
+
}
|
|
1412
|
+
}, [theme, accentColor /* all deps */]);
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
### Acceptance Criteria
|
|
1416
|
+
|
|
1417
|
+
- [ ] No `setState` calls inside useEffect error handlers
|
|
1418
|
+
- [ ] All colors validated before applying to DOM
|
|
1419
|
+
- [ ] Invalid colors in localStorage don't crash app
|
|
1420
|
+
- [ ] No infinite re-render loops on theme errors
|
|
1421
|
+
- [ ] Clear error messages when color validation fails
|
|
1422
|
+
- [ ] Fallback to default theme on catastrophic failure
|
|
1423
|
+
- [ ] All 7 theme aspects (theme, accent, custom, density, animations, blur, typography) apply correctly
|
|
1424
|
+
|
|
1425
|
+
### Testing Strategy
|
|
1426
|
+
|
|
1427
|
+
1. **Invalid Color Test:**
|
|
1428
|
+
- Enter "#ZZZZZZ" in custom color picker
|
|
1429
|
+
- Verify app doesn't crash
|
|
1430
|
+
- Verify fallback color applied
|
|
1431
|
+
|
|
1432
|
+
2. **Corrupted Storage Test:**
|
|
1433
|
+
- Manually set localStorage.setItem('customPrimaryColor', 'invalid')
|
|
1434
|
+
- Restart app
|
|
1435
|
+
- Verify app loads with default color
|
|
1436
|
+
|
|
1437
|
+
3. **Rapid Change Test:**
|
|
1438
|
+
- Change all theme settings rapidly (10 changes in 1 second)
|
|
1439
|
+
- Verify no render loop warnings in console
|
|
1440
|
+
|
|
1441
|
+
4. **Error Boundary Test:**
|
|
1442
|
+
- Force hexToHSL() to throw
|
|
1443
|
+
- Verify error boundary catches
|
|
1444
|
+
- Verify app remains usable
|
|
1445
|
+
|
|
1446
|
+
### Estimated Effort
|
|
1447
|
+
|
|
1448
|
+
**2 hours** (1 hour implementation + 1 hour testing)
|
|
1449
|
+
|
|
1450
|
+
---
|
|
1451
|
+
|
|
1452
|
+
## MEDIUM-PRIORITY ISSUE #6: Main Window Shows Black Screen on Startup
|
|
1453
|
+
|
|
1454
|
+
### Classification
|
|
1455
|
+
|
|
1456
|
+
- **Type:** Enhancement (UX Polish)
|
|
1457
|
+
- **Priority:** 🔵 Medium
|
|
1458
|
+
- **Likelihood:** 40%
|
|
1459
|
+
- **Impact:** Unprofessional flicker/flash on startup
|
|
1460
|
+
- **Timeline:** **ALREADY HAPPENING** - visible on every launch
|
|
1461
|
+
|
|
1462
|
+
### Problem Description
|
|
1463
|
+
|
|
1464
|
+
Main window is visible immediately on creation, showing black background before React loads.
|
|
1465
|
+
|
|
1466
|
+
**Location:** `electron/main.ts:365-395`
|
|
1467
|
+
|
|
1468
|
+
**Current Implementation:**
|
|
1469
|
+
|
|
1470
|
+
```typescript
|
|
1471
|
+
async function createWindow() {
|
|
1472
|
+
mainWindow = new BrowserWindow({
|
|
1473
|
+
width: 1400,
|
|
1474
|
+
height: 900,
|
|
1475
|
+
minWidth: 800,
|
|
1476
|
+
minHeight: 600,
|
|
1477
|
+
frame: false,
|
|
1478
|
+
titleBarStyle: 'hiddenInset',
|
|
1479
|
+
backgroundColor: '#0a0a0a', // Dark gray
|
|
1480
|
+
webPreferences: REQUIRED_SECURITY_SETTINGS,
|
|
1481
|
+
// ❌ NO show: false option!
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
Menu.setApplicationMenu(null);
|
|
1485
|
+
|
|
1486
|
+
if (isDev) {
|
|
1487
|
+
mainWindow.loadURL('http://localhost:5173'); // Shows immediately!
|
|
1488
|
+
mainWindow.webContents.openDevTools();
|
|
1489
|
+
} else {
|
|
1490
|
+
mainWindow.loadFile(join(__dirname, '../index.html'));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// ❌ NO ready-to-show event handler!
|
|
1494
|
+
|
|
1495
|
+
// Other event handlers (maximize, unmaximize, etc.)
|
|
1496
|
+
}
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
**Comparison: Comparison Window (Correct Pattern)**
|
|
1500
|
+
|
|
1501
|
+
**Location:** `electron/main.ts:657-693`
|
|
1502
|
+
|
|
1503
|
+
```typescript
|
|
1504
|
+
const comparisonWindow = new BrowserWindow({
|
|
1505
|
+
width: 1200,
|
|
1506
|
+
height: 800,
|
|
1507
|
+
// ...
|
|
1508
|
+
show: false, // ✅ Hidden initially!
|
|
1509
|
+
backgroundColor: '#ffffff',
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
comparisonWindow.loadURL(`data:text/html;...`);
|
|
1513
|
+
|
|
1514
|
+
// ✅ Show only when ready!
|
|
1515
|
+
comparisonWindow.once('ready-to-show', () => {
|
|
1516
|
+
comparisonWindow.show();
|
|
1517
|
+
});
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
### User Experience Impact
|
|
1521
|
+
|
|
1522
|
+
**Current Behavior:**
|
|
1523
|
+
|
|
1524
|
+
```
|
|
1525
|
+
Time: 0ms - User clicks app icon
|
|
1526
|
+
Time: 50ms - Electron window appears (black screen)
|
|
1527
|
+
Time: 100ms - HTML loaded, but React hasn't initialized
|
|
1528
|
+
Time: 500ms - ThemeProvider initializing (still black)
|
|
1529
|
+
Time: 800ms - GlobalStatsProvider loading from IndexedDB (still black)
|
|
1530
|
+
Time: 1200ms - SessionProvider migrating data (still black)
|
|
1531
|
+
Time: 1500ms - React finally renders UI
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
**User sees:** 1.5 seconds of black screen
|
|
1535
|
+
|
|
1536
|
+
**Expected Behavior with Fix:**
|
|
1537
|
+
|
|
1538
|
+
```
|
|
1539
|
+
Time: 0ms - User clicks app icon
|
|
1540
|
+
Time: 0ms - Window created but hidden
|
|
1541
|
+
Time: 1500ms - All providers initialized, React ready
|
|
1542
|
+
Time: 1500ms - Window shows (smooth fade-in)
|
|
1543
|
+
```
|
|
1544
|
+
|
|
1545
|
+
**User sees:** Nothing until app is fully loaded (clean startup!)
|
|
1546
|
+
|
|
1547
|
+
### Root Cause
|
|
1548
|
+
|
|
1549
|
+
**Missing Configuration:**
|
|
1550
|
+
|
|
1551
|
+
1. `show: false` not set in BrowserWindowOptions
|
|
1552
|
+
2. No `ready-to-show` event listener
|
|
1553
|
+
3. No loading splash screen
|
|
1554
|
+
|
|
1555
|
+
**Why This Matters:**
|
|
1556
|
+
|
|
1557
|
+
- Professional apps show smooth loading experience
|
|
1558
|
+
- Black screen looks unpolished and buggy
|
|
1559
|
+
- Users may think app is frozen or crashed
|
|
1560
|
+
|
|
1561
|
+
### Evidence
|
|
1562
|
+
|
|
1563
|
+
**Comparison with Industry Standards:**
|
|
1564
|
+
|
|
1565
|
+
- **VS Code:** Shows splash screen, hides window until ready
|
|
1566
|
+
- **Slack:** Displays branded loading screen
|
|
1567
|
+
- **Discord:** Custom loading animation
|
|
1568
|
+
- **Our comparison window:** Already implements this pattern correctly!
|
|
1569
|
+
|
|
1570
|
+
### Proposed Solution
|
|
1571
|
+
|
|
1572
|
+
**Option 1: Hide until ready (Recommended)**
|
|
1573
|
+
|
|
1574
|
+
```typescript
|
|
1575
|
+
async function createWindow() {
|
|
1576
|
+
mainWindow = new BrowserWindow({
|
|
1577
|
+
width: 1400,
|
|
1578
|
+
height: 900,
|
|
1579
|
+
minWidth: 800,
|
|
1580
|
+
minHeight: 600,
|
|
1581
|
+
frame: false,
|
|
1582
|
+
titleBarStyle: 'hiddenInset',
|
|
1583
|
+
backgroundColor: '#0a0a0a',
|
|
1584
|
+
show: false, // ✅ Hide initially
|
|
1585
|
+
webPreferences: REQUIRED_SECURITY_SETTINGS,
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
Menu.setApplicationMenu(null);
|
|
1589
|
+
|
|
1590
|
+
if (isDev) {
|
|
1591
|
+
mainWindow.loadURL('http://localhost:5173');
|
|
1592
|
+
mainWindow.webContents.openDevTools();
|
|
1593
|
+
} else {
|
|
1594
|
+
mainWindow.loadFile(join(__dirname, '../index.html'));
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// ✅ Show when content is ready
|
|
1598
|
+
mainWindow.once('ready-to-show', () => {
|
|
1599
|
+
mainWindow?.show();
|
|
1600
|
+
|
|
1601
|
+
// Optional: Fade in effect
|
|
1602
|
+
mainWindow?.setOpacity(0);
|
|
1603
|
+
mainWindow?.show();
|
|
1604
|
+
|
|
1605
|
+
let opacity = 0;
|
|
1606
|
+
const fadeIn = setInterval(() => {
|
|
1607
|
+
opacity += 0.1;
|
|
1608
|
+
mainWindow?.setOpacity(opacity);
|
|
1609
|
+
|
|
1610
|
+
if (opacity >= 1) {
|
|
1611
|
+
clearInterval(fadeIn);
|
|
1612
|
+
}
|
|
1613
|
+
}, 20); // 200ms total fade
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// ... rest of function
|
|
1617
|
+
}
|
|
1618
|
+
```
|
|
1619
|
+
|
|
1620
|
+
**Option 2: Splash screen (More polished)**
|
|
1621
|
+
|
|
1622
|
+
```typescript
|
|
1623
|
+
let splashWindow: BrowserWindow | null = null;
|
|
1624
|
+
|
|
1625
|
+
async function createSplashScreen() {
|
|
1626
|
+
splashWindow = new BrowserWindow({
|
|
1627
|
+
width: 400,
|
|
1628
|
+
height: 300,
|
|
1629
|
+
frame: false,
|
|
1630
|
+
transparent: true,
|
|
1631
|
+
alwaysOnTop: true,
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
splashWindow.loadFile('splash.html'); // Custom branded splash
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function createWindow() {
|
|
1638
|
+
mainWindow = new BrowserWindow({
|
|
1639
|
+
// ...
|
|
1640
|
+
show: false,
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
mainWindow.loadFile(join(__dirname, '../index.html'));
|
|
1644
|
+
|
|
1645
|
+
mainWindow.once('ready-to-show', () => {
|
|
1646
|
+
// Close splash, show main window
|
|
1647
|
+
if (splashWindow) {
|
|
1648
|
+
splashWindow.close();
|
|
1649
|
+
splashWindow = null;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
mainWindow?.show();
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
app.whenReady().then(async () => {
|
|
1657
|
+
await createSplashScreen(); // Show splash first
|
|
1658
|
+
await createWindow(); // Load main window in background
|
|
1659
|
+
});
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
### Acceptance Criteria
|
|
1663
|
+
|
|
1664
|
+
- [ ] Window hidden until `ready-to-show` event fires
|
|
1665
|
+
- [ ] No black screen visible during startup
|
|
1666
|
+
- [ ] Smooth appearance of UI (optional: fade-in effect)
|
|
1667
|
+
- [ ] Dev tools open correctly in development mode
|
|
1668
|
+
- [ ] Window shows at correct size and position
|
|
1669
|
+
- [ ] All window event handlers still work correctly
|
|
1670
|
+
|
|
1671
|
+
### Testing Strategy
|
|
1672
|
+
|
|
1673
|
+
1. **Cold Start Test:**
|
|
1674
|
+
- Restart app 10 times
|
|
1675
|
+
- Verify no black screen flicker
|
|
1676
|
+
- Measure time from click to UI visible
|
|
1677
|
+
|
|
1678
|
+
2. **Dev Mode Test:**
|
|
1679
|
+
- Run `npm run dev`
|
|
1680
|
+
- Verify dev tools open correctly
|
|
1681
|
+
- Verify HMR still works
|
|
1682
|
+
|
|
1683
|
+
3. **Production Test:**
|
|
1684
|
+
- Build production app
|
|
1685
|
+
- Test on fresh install (no cached data)
|
|
1686
|
+
- Test with existing data (migration scenario)
|
|
1687
|
+
|
|
1688
|
+
### Estimated Effort
|
|
1689
|
+
|
|
1690
|
+
**30 minutes** (15 min implementation + 15 min testing)
|
|
1691
|
+
|
|
1692
|
+
### Priority Justification
|
|
1693
|
+
|
|
1694
|
+
**Why Medium (not High):**
|
|
1695
|
+
|
|
1696
|
+
- Purely cosmetic issue (doesn't affect functionality)
|
|
1697
|
+
- Workaround exists (users can wait)
|
|
1698
|
+
- Easy fix, low risk
|
|
1699
|
+
|
|
1700
|
+
**Why Not Low:**
|
|
1701
|
+
|
|
1702
|
+
- Affects every single app launch
|
|
1703
|
+
- First impression matters (UX quality)
|
|
1704
|
+
- Simple fix with big perceived improvement
|
|
1705
|
+
|
|
1706
|
+
---
|
|
1707
|
+
|
|
1708
|
+
## MEDIUM-PRIORITY ISSUE #7: Certificate Check Delays Auto-Update
|
|
1709
|
+
|
|
1710
|
+
### Classification
|
|
1711
|
+
|
|
1712
|
+
- **Type:** Bug (Timing/Coordination)
|
|
1713
|
+
- **Priority:** 🔵 Medium
|
|
1714
|
+
- **Likelihood:** 50%
|
|
1715
|
+
- **Impact:** Auto-update delayed by 5-10 seconds in corporate environments
|
|
1716
|
+
- **Timeline:** **ALREADY HAPPENING** - affects users behind proxies
|
|
1717
|
+
|
|
1718
|
+
### Problem Description
|
|
1719
|
+
|
|
1720
|
+
Background certificate check and auto-updater initialize independently with no coordination, causing TLS failures.
|
|
1721
|
+
|
|
1722
|
+
**Location:** `electron/main.ts:576-607` and `electron/main.ts:1585-1595`
|
|
1723
|
+
|
|
1724
|
+
**Certificate Check (runs at 500ms):**
|
|
1725
|
+
|
|
1726
|
+
```typescript
|
|
1727
|
+
app.whenReady().then(async () => {
|
|
1728
|
+
await createWindow();
|
|
1729
|
+
|
|
1730
|
+
// Perform pre-flight certificate check in background (non-blocking)
|
|
1731
|
+
setImmediate(async () => {
|
|
1732
|
+
log.info('Starting background certificate check...');
|
|
1733
|
+
|
|
1734
|
+
// Small delay to ensure window is fully rendered
|
|
1735
|
+
await new Promise((resolve) => setTimeout(resolve, 500)); // ❌ Arbitrary delay
|
|
1736
|
+
|
|
1737
|
+
performPreflightCertificateCheck()
|
|
1738
|
+
.then(() => {
|
|
1739
|
+
log.info('Background certificate check completed');
|
|
1740
|
+
|
|
1741
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
1742
|
+
mainWindow.webContents.send('certificate-check-complete', {
|
|
1743
|
+
success: true,
|
|
1744
|
+
timestamp: new Date().toISOString(),
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
})
|
|
1748
|
+
.catch((error) => {
|
|
1749
|
+
log.error('Background certificate check failed:', error);
|
|
1750
|
+
// ... error handling
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
});
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
**Auto-Updater (runs at 1000ms):**
|
|
1757
|
+
|
|
1758
|
+
```typescript
|
|
1759
|
+
app.whenReady().then(() => {
|
|
1760
|
+
setTimeout(() => {
|
|
1761
|
+
updaterHandler = new AutoUpdaterHandler(); // ❌ Might run before certs validated!
|
|
1762
|
+
|
|
1763
|
+
if (!isDev) {
|
|
1764
|
+
updaterHandler.checkOnStartup(); // Immediately tries to connect to GitHub
|
|
1765
|
+
}
|
|
1766
|
+
}, 1000); // ❌ Another arbitrary delay
|
|
1767
|
+
});
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
### The Race Condition
|
|
1771
|
+
|
|
1772
|
+
**Scenario 1: Certificate check wins (500ms < 1000ms)**
|
|
1773
|
+
|
|
1774
|
+
```text
|
|
1775
|
+
Time: 0ms - App starts
|
|
1776
|
+
Time: 500ms - Certificate check starts
|
|
1777
|
+
Time: 800ms - Certificate validated successfully
|
|
1778
|
+
Time: 1000ms - Auto-updater starts
|
|
1779
|
+
Time: 1001ms - Update check succeeds (certs already validated)
|
|
1780
|
+
Result: ✅ Works correctly
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
**Scenario 2: Certificate check is slow (network latency)**
|
|
1784
|
+
|
|
1785
|
+
```text
|
|
1786
|
+
Time: 0ms - App starts
|
|
1787
|
+
Time: 500ms - Certificate check starts
|
|
1788
|
+
Time: 1000ms - Auto-updater starts
|
|
1789
|
+
Time: 1001ms - Update check fails with TLS error (certs not ready yet)
|
|
1790
|
+
Time: 2000ms - Certificate check completes (too late!)
|
|
1791
|
+
Result: ❌ Auto-update fails, user doesn't get notified
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
**Scenario 3: Corporate proxy (high latency)**
|
|
1795
|
+
|
|
1796
|
+
```text
|
|
1797
|
+
Time: 0ms - App starts
|
|
1798
|
+
Time: 500ms - Certificate check starts
|
|
1799
|
+
Time: 1000ms - Auto-updater starts
|
|
1800
|
+
Time: 1001ms - Update check hangs waiting for proxy auth
|
|
1801
|
+
Time: 3000ms - Certificate check timeout (5s limit at line 95)
|
|
1802
|
+
Time: 6000ms - Update check timeout
|
|
1803
|
+
Result: ❌ Both fail, no updates available
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
### Impact Analysis
|
|
1807
|
+
|
|
1808
|
+
**Affected Users:**
|
|
1809
|
+
|
|
1810
|
+
- Corporate environments with Zscaler/proxy
|
|
1811
|
+
- Users with slow network connections
|
|
1812
|
+
- Users with SSL-intercepting firewalls
|
|
1813
|
+
|
|
1814
|
+
**Percentage of Users:**
|
|
1815
|
+
Based on the extensive proxy configuration code (lines 51-313), this is clearly a known issue affecting a significant portion of users.
|
|
1816
|
+
|
|
1817
|
+
**User Experience:**
|
|
1818
|
+
|
|
1819
|
+
```text
|
|
1820
|
+
User opens app
|
|
1821
|
+
→ Update notification should appear
|
|
1822
|
+
→ Instead: silence (update check failed)
|
|
1823
|
+
→ User never knows update is available
|
|
1824
|
+
→ Continues using old version with bugs
|
|
1825
|
+
```
|
|
1826
|
+
|
|
1827
|
+
### Root Causes
|
|
1828
|
+
|
|
1829
|
+
1. **Independent Initialization:** Two `setTimeout` calls with arbitrary delays (500ms, 1000ms)
|
|
1830
|
+
2. **No Synchronization:** Auto-updater doesn't wait for certificate validation
|
|
1831
|
+
3. **Race Condition:** Which finishes first is unpredictable (depends on network)
|
|
1832
|
+
4. **Silent Failure:** Failed update checks don't retry or notify user
|
|
1833
|
+
|
|
1834
|
+
### Evidence of Existing Issue
|
|
1835
|
+
|
|
1836
|
+
**From Code:**
|
|
1837
|
+
|
|
1838
|
+
- Line 576 comment: "allows app to start immediately while checking network in parallel"
|
|
1839
|
+
- This was added recently to avoid blocking startup
|
|
1840
|
+
- But created a timing issue with updater
|
|
1841
|
+
|
|
1842
|
+
- Lines 51-313: Extensive proxy and certificate configuration
|
|
1843
|
+
- `proxyConfig.ts`, `zscalerConfig.ts`, custom TLS handling
|
|
1844
|
+
- Shows corporate network issues are a major concern
|
|
1845
|
+
|
|
1846
|
+
- Line 582: "Small delay to ensure window is fully rendered"
|
|
1847
|
+
- Arbitrary delay suggests timing issues
|
|
1848
|
+
|
|
1849
|
+
### Proposed Solutions
|
|
1850
|
+
|
|
1851
|
+
**Make certificate validation a prerequisite for updater:**
|
|
1852
|
+
|
|
1853
|
+
```typescript
|
|
1854
|
+
app.whenReady().then(async () => {
|
|
1855
|
+
// STEP 1: Create window first (user sees app immediately)
|
|
1856
|
+
await createWindow();
|
|
1857
|
+
|
|
1858
|
+
// STEP 2: Background initialization (non-blocking for UI)
|
|
1859
|
+
setImmediate(async () => {
|
|
1860
|
+
try {
|
|
1861
|
+
// STEP 2A: Validate certificates (BLOCKING for updater)
|
|
1862
|
+
log.info('[Init] Validating certificates...');
|
|
1863
|
+
await performPreflightCertificateCheck();
|
|
1864
|
+
log.info('[Init] ✅ Certificates validated');
|
|
1865
|
+
|
|
1866
|
+
// Notify renderer
|
|
1867
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
1868
|
+
mainWindow.webContents.send('certificate-check-complete', {
|
|
1869
|
+
success: true,
|
|
1870
|
+
timestamp: new Date().toISOString(),
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
log.error('[Init] ⚠️ Certificate validation failed:', error);
|
|
1875
|
+
|
|
1876
|
+
// Notify renderer of failure
|
|
1877
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
1878
|
+
mainWindow.webContents.send('certificate-check-complete', {
|
|
1879
|
+
success: false,
|
|
1880
|
+
error: error.message,
|
|
1881
|
+
timestamp: new Date().toISOString(),
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// STEP 2B: Initialize updater (AFTER certificates validated)
|
|
1887
|
+
// This ensures updater only runs when network is ready
|
|
1888
|
+
log.info('[Init] Initializing auto-updater...');
|
|
1889
|
+
updaterHandler = new AutoUpdaterHandler(mainWindow);
|
|
1890
|
+
|
|
1891
|
+
if (!isDev) {
|
|
1892
|
+
// Delay update check slightly to avoid impacting startup performance
|
|
1893
|
+
setTimeout(() => {
|
|
1894
|
+
log.info('[Init] Checking for updates...');
|
|
1895
|
+
updaterHandler.checkOnStartup();
|
|
1896
|
+
}, 2000); // 2 second delay AFTER certs validated
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
log.info('[Init] ✅ Background initialization complete');
|
|
1900
|
+
});
|
|
1901
|
+
});
|
|
1902
|
+
```
|
|
1903
|
+
|
|
1904
|
+
**Alternative: Retry on failure**
|
|
1905
|
+
|
|
1906
|
+
```typescript
|
|
1907
|
+
class AutoUpdaterHandler {
|
|
1908
|
+
private maxRetries = 3;
|
|
1909
|
+
private retryDelay = 5000; // 5 seconds
|
|
1910
|
+
|
|
1911
|
+
async checkForUpdatesWithRetry(attempt = 1): Promise<void> {
|
|
1912
|
+
try {
|
|
1913
|
+
await this.checkForUpdates();
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
if (attempt < this.maxRetries && this.isCertificateError(error)) {
|
|
1916
|
+
log.warn(`Update check failed (attempt ${attempt}/${this.maxRetries}), retrying...`);
|
|
1917
|
+
|
|
1918
|
+
await new Promise((resolve) => setTimeout(resolve, this.retryDelay));
|
|
1919
|
+
await this.checkForUpdatesWithRetry(attempt + 1);
|
|
1920
|
+
} else {
|
|
1921
|
+
log.error(`Update check failed after ${attempt} attempts:`, error);
|
|
1922
|
+
throw error;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
private isCertificateError(error: any): boolean {
|
|
1928
|
+
const message = error?.message?.toLowerCase() || '';
|
|
1929
|
+
return message.includes('certificate') || message.includes('tls') || message.includes('ssl');
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
```
|
|
1933
|
+
|
|
1934
|
+
### Acceptance Criteria
|
|
1935
|
+
|
|
1936
|
+
- [ ] Auto-updater only initializes AFTER certificate validation completes
|
|
1937
|
+
- [ ] No race condition between certificate check and update check
|
|
1938
|
+
- [ ] Failed certificate validation delays updater (doesn't crash it)
|
|
1939
|
+
- [ ] Update checks succeed in corporate proxy environments
|
|
1940
|
+
- [ ] User gets notified of available updates within 5 seconds of app start
|
|
1941
|
+
- [ ] Failed update checks retry with exponential backoff
|
|
1942
|
+
|
|
1943
|
+
### Testing Strategy
|
|
1944
|
+
|
|
1945
|
+
1. **Normal Network Test:**
|
|
1946
|
+
- Run on normal network
|
|
1947
|
+
- Verify update notification appears within 5 seconds
|
|
1948
|
+
|
|
1949
|
+
2. **Slow Network Test:**
|
|
1950
|
+
- Add 3s latency to all network requests
|
|
1951
|
+
- Verify updater waits for certificate validation
|
|
1952
|
+
|
|
1953
|
+
3. **Corporate Proxy Test:**
|
|
1954
|
+
- Test with Zscaler/proxy configuration
|
|
1955
|
+
- Verify update checks succeed
|
|
1956
|
+
|
|
1957
|
+
4. **Certificate Failure Test:**
|
|
1958
|
+
- Block GitHub certificate validation
|
|
1959
|
+
- Verify updater doesn't crash
|
|
1960
|
+
- Verify user gets notified (optional manual update)
|
|
1961
|
+
|
|
1962
|
+
### Estimated Effort
|
|
1963
|
+
|
|
1964
|
+
**1 hour** (30 min implementation + 30 min testing)
|
|
1965
|
+
|
|
1966
|
+
### Priority Justification
|
|
1967
|
+
|
|
1968
|
+
**Why Medium (not High):**
|
|
1969
|
+
|
|
1970
|
+
- Auto-updates still work (eventually)
|
|
1971
|
+
- Users can manually check for updates
|
|
1972
|
+
- Silent failure (doesn't break app)
|
|
1973
|
+
|
|
1974
|
+
**Why Not Low:**
|
|
1975
|
+
|
|
1976
|
+
- Affects update adoption rate
|
|
1977
|
+
- Security updates may be delayed
|
|
1978
|
+
- Common in corporate environments
|
|
1979
|
+
|
|
1980
|
+
---
|
|
1981
|
+
|
|
1982
|
+
## Summary Statistics
|
|
1983
|
+
|
|
1984
|
+
### Issue Distribution
|
|
1985
|
+
|
|
1986
|
+
- **Critical:** 3 issues (43%)
|
|
1987
|
+
- **High:** 2 issues (29%)
|
|
1988
|
+
- **Medium:** 2 issues (28%)
|
|
1989
|
+
|
|
1990
|
+
### Affected Components
|
|
1991
|
+
|
|
1992
|
+
- **Electron Main Process:** 3 issues (#1, #6, #7)
|
|
1993
|
+
- **React Contexts:** 3 issues (#2, #3, #5)
|
|
1994
|
+
- **IndexedDB/Storage:** 2 issues (#3, #4)
|
|
1995
|
+
|
|
1996
|
+
### Timeline to Impact
|
|
1997
|
+
|
|
1998
|
+
- **Immediate (Already Happening):** 4 issues (#1, #2, #6, #7)
|
|
1999
|
+
- **Short-term (2-4 weeks):** 1 issue (#3)
|
|
2000
|
+
- **Medium-term (4-6 weeks):** 2 issues (#4, #5)
|
|
2001
|
+
|
|
2002
|
+
### Total Estimated Fix Effort
|
|
2003
|
+
|
|
2004
|
+
- **Critical fixes:** 9 hours (Issues #1, #2, #3)
|
|
2005
|
+
- **High-priority fixes:** 8 hours (Issues #4, #5)
|
|
2006
|
+
- **Medium-priority fixes:** 1.5 hours (Issues #6, #7)
|
|
2007
|
+
- **TOTAL:** 18.5 hours
|
|
2008
|
+
|
|
2009
|
+
### Potential for Parallelization
|
|
2010
|
+
|
|
2011
|
+
Issues can be worked on simultaneously by different developers:
|
|
2012
|
+
|
|
2013
|
+
- **Track 1 (Electron):** Issues #1, #6, #7 → 3.5 hours
|
|
2014
|
+
- **Track 2 (Contexts):** Issues #2, #3, #5 → 9 hours
|
|
2015
|
+
- **Track 3 (Performance):** Issue #4 → 6 hours
|
|
2016
|
+
|
|
2017
|
+
**With 3 developers:** Could complete all fixes in ~9 hours (1-2 days)
|
|
2018
|
+
|
|
2019
|
+
---
|
|
2020
|
+
|
|
2021
|
+
## Recommended Action Plan
|
|
2022
|
+
|
|
2023
|
+
### Phase 1: Critical Fixes (Day 1-2)
|
|
2024
|
+
|
|
2025
|
+
**Priority:** Stop the bleeding
|
|
2026
|
+
|
|
2027
|
+
1. **Issue #1:** Consolidate `app.whenReady()` handlers → 2 hours
|
|
2028
|
+
2. **Issue #2:** Add lazy context loading → 4 hours
|
|
2029
|
+
3. **Issue #3:** Fix GlobalStatsProvider memory leak → 3 hours
|
|
2030
|
+
|
|
2031
|
+
**Total:** 9 hours | **Impact:** Eliminates crashes and major UX issues
|
|
2032
|
+
|
|
2033
|
+
### Phase 2: Performance Improvements (Week 1)
|
|
2034
|
+
|
|
2035
|
+
**Priority:** Improve scalability
|
|
2036
|
+
|
|
2037
|
+
4. **Issue #4:** Implement incremental persistence → 6 hours
|
|
2038
|
+
5. **Issue #5:** Fix theme context loop → 2 hours
|
|
2039
|
+
|
|
2040
|
+
**Total:** 8 hours | **Impact:** App remains responsive at scale
|
|
2041
|
+
|
|
2042
|
+
### Phase 3: Polish & UX (Week 2)
|
|
2043
|
+
|
|
2044
|
+
**Priority:** Professional finish
|
|
2045
|
+
|
|
2046
|
+
6. **Issue #6:** Add `ready-to-show` event → 0.5 hours
|
|
2047
|
+
7. **Issue #7:** Coordinate certificate check and updater → 1 hour
|
|
2048
|
+
|
|
2049
|
+
**Total:** 1.5 hours | **Impact:** Smooth user experience
|
|
2050
|
+
|
|
2051
|
+
---
|
|
2052
|
+
|
|
2053
|
+
## Testing & Validation Plan
|
|
2054
|
+
|
|
2055
|
+
### Automated Testing
|
|
2056
|
+
|
|
2057
|
+
- **Unit Tests:** Add tests for all fixed components
|
|
2058
|
+
- **Integration Tests:** Test context initialization flow
|
|
2059
|
+
- **Performance Tests:** Benchmark session persistence scaling
|
|
2060
|
+
- **Memory Tests:** Validate no connection leaks
|
|
2061
|
+
|
|
2062
|
+
### Manual Testing
|
|
2063
|
+
|
|
2064
|
+
- **Fresh Install:** Test clean installation with no data
|
|
2065
|
+
- **Migration:** Test upgrade from localStorage to IndexedDB
|
|
2066
|
+
- **Scale Test:** Create 100 sessions, verify performance
|
|
2067
|
+
- **Network Test:** Test with slow/proxy networks
|
|
2068
|
+
- **Error Test:** Test all error scenarios
|
|
2069
|
+
|
|
2070
|
+
### Performance Benchmarks
|
|
2071
|
+
|
|
2072
|
+
| Metric | Current | Target | Improvement |
|
|
2073
|
+
| ----------------------------- | ------- | ------ | ----------- |
|
|
2074
|
+
| Cold start (no data) | 1000ms | 300ms | 70% faster |
|
|
2075
|
+
| Normal start (10 sessions) | 1500ms | 500ms | 67% faster |
|
|
2076
|
+
| Migration start | 5000ms | 1000ms | 80% faster |
|
|
2077
|
+
| Session persist (50 sessions) | 375ms | 5ms | 98% faster |
|
|
2078
|
+
| Memory usage (1hr session) | Growing | Stable | No leaks |
|
|
2079
|
+
|
|
2080
|
+
---
|
|
2081
|
+
|
|
2082
|
+
## Long-term Recommendations
|
|
2083
|
+
|
|
2084
|
+
### Architecture Improvements
|
|
2085
|
+
|
|
2086
|
+
1. **Implement Redux or Zustand:** More predictable state management
|
|
2087
|
+
2. **Add Telemetry:** Monitor real-world performance metrics
|
|
2088
|
+
3. **Create Loading Skeleton:** Better perceived performance
|
|
2089
|
+
4. **Add Error Recovery:** Automatic retry on failures
|
|
2090
|
+
|
|
2091
|
+
### Process Improvements
|
|
2092
|
+
|
|
2093
|
+
1. **Performance Budget:** Set hard limits on load times
|
|
2094
|
+
2. **Automated Benchmarks:** CI/CD performance tests
|
|
2095
|
+
3. **Code Reviews:** Focus on state management patterns
|
|
2096
|
+
4. **Documentation:** Document initialization sequence
|
|
2097
|
+
|
|
2098
|
+
### Monitoring
|
|
2099
|
+
|
|
2100
|
+
1. **Crash Reporting:** Implement Sentry or similar
|
|
2101
|
+
2. **Performance Monitoring:** Track load times in production
|
|
2102
|
+
3. **User Metrics:** Measure actual startup times
|
|
2103
|
+
4. **Database Size Tracking:** Alert before hitting limits
|
|
2104
|
+
|
|
2105
|
+
---
|
|
2106
|
+
|
|
2107
|
+
## Conclusion
|
|
2108
|
+
|
|
2109
|
+
The DocumentHub codebase shows signs of rapid development with reactive bug-fixing. While the individual components are well-designed (connection pooling, error boundaries, logging), the integration between them has created timing issues and performance bottlenecks.
|
|
2110
|
+
|
|
2111
|
+
**Good News:**
|
|
2112
|
+
|
|
2113
|
+
- All issues are fixable
|
|
2114
|
+
- No fundamental architecture changes needed
|
|
2115
|
+
- Team is already aware of many problems (see code comments)
|
|
2116
|
+
- Infrastructure is in place (connection pools, error handlers)
|
|
2117
|
+
|
|
2118
|
+
**Key Insight:**
|
|
2119
|
+
The problems stem from **sequential dependencies not being enforced** (app.whenReady handlers), **synchronous initialization in render path** (context cascade), and **lack of dirty tracking** (O(n) persistence). Fixing these patterns will resolve multiple issues simultaneously.
|
|
2120
|
+
|
|
2121
|
+
**Recommended Next Step:**
|
|
2122
|
+
Start with Phase 1 (Critical Fixes). These 3 issues (#1, #2, #3) have the highest impact and will provide immediate relief to users. The remaining issues can be addressed incrementally without blocking releases.
|
|
2123
|
+
|
|
2124
|
+
---
|
|
2125
|
+
|
|
2126
|
+
**END OF ANALYSIS**
|
|
2127
|
+
|
|
2128
|
+
Generated: October 18, 2025
|
|
2129
|
+
Repository: ItMeDiaTech/Documentation_Hub
|
|
2130
|
+
Total Issues: 7 | Critical: 3 | High: 2 | Medium: 2
|
|
2131
|
+
Estimated Total Fix Effort: 18.5 hours
|