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.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. package/vite.config.ts +152 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * PowerAutomateApiService - Main Process API Client
3
+ *
4
+ * This service handles PowerAutomate API calls directly from the main process
5
+ * using Electron's net.request (Chromium networking stack).
6
+ *
7
+ * This is used by WordDocumentProcessor when running in the main process,
8
+ * where window.electronAPI is not available.
9
+ *
10
+ * Key features:
11
+ * - Uses Chromium's networking stack (respects system proxy and certificates)
12
+ * - Retry logic with exponential backoff
13
+ * - Comprehensive logging for debugging
14
+ * - Timeout handling
15
+ */
16
+
17
+ import { net, session } from 'electron';
18
+ import { logger } from '../../src/utils/logger';
19
+
20
+ const log = logger.namespace('PowerAutomateApi');
21
+
22
+ export interface PowerAutomateRequest {
23
+ Lookup_ID: string[];
24
+ Hyperlinks_Checked: number;
25
+ Total_Hyperlinks: number;
26
+ First_Name: string;
27
+ Last_Name: string;
28
+ Email: string;
29
+ }
30
+
31
+ export interface PowerAutomateResult {
32
+ Document_ID?: string;
33
+ Content_ID?: string;
34
+ Title?: string;
35
+ Status?: string;
36
+ }
37
+
38
+ export interface PowerAutomateResponse {
39
+ success: boolean;
40
+ statusCode?: number;
41
+ data?: { Results?: PowerAutomateResult[] };
42
+ error?: string;
43
+ rawResponse?: string;
44
+ duration?: number;
45
+ }
46
+
47
+ /**
48
+ * Call the PowerAutomate API from the main process
49
+ */
50
+ export async function callPowerAutomateApi(
51
+ apiUrl: string,
52
+ payload: PowerAutomateRequest,
53
+ timeoutMs: number = 30000
54
+ ): Promise<PowerAutomateResponse> {
55
+ const jsonPayload = JSON.stringify(payload);
56
+ const startTime = Date.now();
57
+
58
+ log.info('═══════════════════════════════════════════════════════════════════');
59
+ log.info('[MainProcess] Starting Power Automate HTTP Request');
60
+ log.info('═══════════════════════════════════════════════════════════════════');
61
+ log.info(`[MainProcess] Timestamp: ${new Date().toISOString()}`);
62
+ log.info(`[MainProcess] URL: ${apiUrl}`);
63
+ log.info(`[MainProcess] Lookup IDs: ${payload.Lookup_ID.length}`);
64
+ log.info(`[MainProcess] IDs: ${payload.Lookup_ID.join(', ')}`);
65
+ log.info(`[MainProcess] Timeout: ${timeoutMs}ms`);
66
+ log.info('───────────────────────────────────────────────────────────────────');
67
+
68
+ return new Promise((resolve) => {
69
+ log.info('[MainProcess] Sending request via Electron net.request...');
70
+
71
+ const timeoutHandle = setTimeout(() => {
72
+ const duration = Date.now() - startTime;
73
+ log.error('═══════════════════════════════════════════════════════════════════');
74
+ log.error('[MainProcess] REQUEST TIMEOUT');
75
+ log.error(`[MainProcess] Timeout after ${timeoutMs}ms`);
76
+ log.error('═══════════════════════════════════════════════════════════════════');
77
+ resolve({
78
+ success: false,
79
+ error: `Request timeout after ${timeoutMs}ms`,
80
+ duration,
81
+ });
82
+ }, timeoutMs);
83
+
84
+ try {
85
+ const netRequest = net.request({
86
+ method: 'POST',
87
+ url: apiUrl,
88
+ session: session.defaultSession,
89
+ });
90
+
91
+ // Set headers
92
+ netRequest.setHeader('Content-Type', 'application/json; charset=utf-8');
93
+ netRequest.setHeader('User-Agent', 'DocHub/1.0');
94
+ netRequest.setHeader('Accept', 'application/json');
95
+
96
+ let responseData = '';
97
+
98
+ netRequest.on('response', (response) => {
99
+ log.info(`[MainProcess] Response received: ${response.statusCode} ${response.statusMessage}`);
100
+
101
+ response.on('data', (chunk) => {
102
+ responseData += chunk.toString();
103
+ });
104
+
105
+ response.on('end', () => {
106
+ clearTimeout(timeoutHandle);
107
+ const duration = Date.now() - startTime;
108
+
109
+ log.info(`[MainProcess] Response complete in ${duration}ms`);
110
+
111
+ if (response.statusCode && response.statusCode >= 200 && response.statusCode < 300) {
112
+ try {
113
+ const data = JSON.parse(responseData);
114
+ log.info('═══════════════════════════════════════════════════════════════════');
115
+ log.info('[MainProcess] API Call SUCCESS');
116
+ log.info(`[MainProcess] Results: ${data?.Results?.length || 0} items`);
117
+ log.info('═══════════════════════════════════════════════════════════════════');
118
+
119
+ resolve({
120
+ success: true,
121
+ statusCode: response.statusCode,
122
+ data,
123
+ duration,
124
+ });
125
+ } catch (parseError) {
126
+ log.error('[MainProcess] Failed to parse response JSON:', parseError);
127
+ resolve({
128
+ success: false,
129
+ statusCode: response.statusCode,
130
+ error: 'Failed to parse API response',
131
+ rawResponse: responseData,
132
+ duration,
133
+ });
134
+ }
135
+ } else {
136
+ log.error('═══════════════════════════════════════════════════════════════════');
137
+ log.error('[MainProcess] API Call FAILED');
138
+ log.error(`[MainProcess] Status: ${response.statusCode}`);
139
+ log.error(`[MainProcess] Response: ${responseData.substring(0, 500)}`);
140
+ log.error('═══════════════════════════════════════════════════════════════════');
141
+
142
+ resolve({
143
+ success: false,
144
+ statusCode: response.statusCode,
145
+ error: `API returned status ${response.statusCode}`,
146
+ rawResponse: responseData,
147
+ duration,
148
+ });
149
+ }
150
+ });
151
+
152
+ response.on('error', (error) => {
153
+ clearTimeout(timeoutHandle);
154
+ const duration = Date.now() - startTime;
155
+ log.error('[MainProcess] Response error:', error);
156
+ resolve({
157
+ success: false,
158
+ error: error.message,
159
+ duration,
160
+ });
161
+ });
162
+ });
163
+
164
+ netRequest.on('error', (error) => {
165
+ clearTimeout(timeoutHandle);
166
+ const duration = Date.now() - startTime;
167
+ log.error('[MainProcess] Request error:', error);
168
+ resolve({
169
+ success: false,
170
+ error: error.message,
171
+ duration,
172
+ });
173
+ });
174
+
175
+ // Send the request body
176
+ netRequest.write(jsonPayload);
177
+ netRequest.end();
178
+ } catch (error) {
179
+ clearTimeout(timeoutHandle);
180
+ const duration = Date.now() - startTime;
181
+ log.error('[MainProcess] Exception creating request:', error);
182
+ resolve({
183
+ success: false,
184
+ error: error instanceof Error ? error.message : 'Unknown error',
185
+ duration,
186
+ });
187
+ }
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Call PowerAutomate API with retry logic
193
+ */
194
+ export async function callPowerAutomateApiWithRetry(
195
+ apiUrl: string,
196
+ payload: PowerAutomateRequest,
197
+ options: {
198
+ timeout?: number;
199
+ maxRetries?: number;
200
+ retryDelay?: number;
201
+ } = {}
202
+ ): Promise<PowerAutomateResponse> {
203
+ const { timeout = 30000, maxRetries = 3, retryDelay = 1000 } = options;
204
+
205
+ let lastError: PowerAutomateResponse | null = null;
206
+
207
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
208
+ if (attempt > 0) {
209
+ const delay = Math.pow(2, attempt) * retryDelay;
210
+ log.info(`[MainProcess] Retry attempt ${attempt + 1} of ${maxRetries} (waiting ${delay}ms)`);
211
+ await new Promise((resolve) => setTimeout(resolve, delay));
212
+ }
213
+
214
+ const response = await callPowerAutomateApi(apiUrl, payload, timeout);
215
+
216
+ if (response.success) {
217
+ return response;
218
+ }
219
+
220
+ lastError = response;
221
+
222
+ // Don't retry on timeout
223
+ if (response.error?.includes('timeout')) {
224
+ log.error('[MainProcess] Timeout detected, not retrying');
225
+ break;
226
+ }
227
+ }
228
+
229
+ log.error(`[MainProcess] All ${maxRetries} attempts failed`);
230
+ return lastError || { success: false, error: 'API request failed after retries' };
231
+ }
@@ -0,0 +1,474 @@
1
+ /**
2
+ * SharePointSyncService - Main Process
3
+ * Handles SharePoint file download via Microsoft Graph API
4
+ *
5
+ * This service runs in the Electron main process and provides
6
+ * authenticated access to SharePoint files using MSAL app-only
7
+ * (client credentials) authentication flow.
8
+ *
9
+ * @architecture Main Process Service
10
+ * @security Uses client credentials flow - requires Azure AD app registration
11
+ * @performance Supports chunked download with progress reporting
12
+ */
13
+
14
+ import { createHash } from 'crypto';
15
+ import { BrowserWindow } from 'electron';
16
+ import {
17
+ ConfidentialClientApplication,
18
+ Configuration,
19
+ ClientCredentialRequest,
20
+ } from '@azure/msal-node';
21
+ import * as XLSX from 'xlsx';
22
+ import { logger } from '../../src/utils/logger';
23
+ import { getDictionaryService } from './DictionaryService';
24
+ import type {
25
+ DictionaryEntry,
26
+ SharePointConfig,
27
+ SyncProgressUpdate,
28
+ DictionarySyncResponse,
29
+ } from '../../src/types/dictionary';
30
+
31
+ const log = logger.namespace('SharePointSyncService');
32
+
33
+ /**
34
+ * Service for syncing dictionary from SharePoint
35
+ */
36
+ export class SharePointSyncService {
37
+ private msalApp: ConfidentialClientApplication | null = null;
38
+ private clientSecret: string | null = null;
39
+ private config: SharePointConfig | null = null;
40
+ private schedulerInterval: NodeJS.Timeout | null = null;
41
+ private mainWindow: BrowserWindow | null = null;
42
+
43
+ constructor() {}
44
+
45
+ /**
46
+ * Set the main window for sending progress updates
47
+ */
48
+ setMainWindow(window: BrowserWindow): void {
49
+ this.mainWindow = window;
50
+ }
51
+
52
+ /**
53
+ * Configure the service with SharePoint details
54
+ */
55
+ configure(config: SharePointConfig): void {
56
+ this.config = config;
57
+ log.info('SharePoint sync configured', {
58
+ siteUrl: config.siteUrl,
59
+ tenantId: config.tenantId,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Set client secret securely (not stored in settings)
65
+ */
66
+ setClientSecret(secret: string): { success: boolean; error?: string } {
67
+ try {
68
+ this.clientSecret = secret;
69
+
70
+ // Reinitialize MSAL if config is available
71
+ if (this.config) {
72
+ this.initializeMsal();
73
+ }
74
+
75
+ log.info('Client secret configured');
76
+ return { success: true };
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : 'Unknown error';
79
+ log.error('Failed to set client secret', { error: message });
80
+ return { success: false, error: message };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Initialize MSAL client application
86
+ */
87
+ private initializeMsal(): void {
88
+ if (!this.config || !this.clientSecret) {
89
+ throw new Error('Configuration or client secret not set');
90
+ }
91
+
92
+ const msalConfig: Configuration = {
93
+ auth: {
94
+ clientId: this.config.clientId,
95
+ authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
96
+ clientSecret: this.clientSecret,
97
+ },
98
+ };
99
+
100
+ this.msalApp = new ConfidentialClientApplication(msalConfig);
101
+ log.info('MSAL client initialized');
102
+ }
103
+
104
+ /**
105
+ * Get access token using client credentials flow
106
+ */
107
+ private async getAccessToken(): Promise<string> {
108
+ if (!this.msalApp) {
109
+ this.initializeMsal();
110
+ }
111
+
112
+ if (!this.msalApp) {
113
+ throw new Error('MSAL client not initialized');
114
+ }
115
+
116
+ const tokenRequest: ClientCredentialRequest = {
117
+ scopes: ['https://graph.microsoft.com/.default'],
118
+ };
119
+
120
+ const response = await this.msalApp.acquireTokenByClientCredential(tokenRequest);
121
+
122
+ if (!response?.accessToken) {
123
+ throw new Error('Failed to acquire access token');
124
+ }
125
+
126
+ return response.accessToken;
127
+ }
128
+
129
+ /**
130
+ * Send progress update to renderer
131
+ */
132
+ private sendProgress(update: SyncProgressUpdate): void {
133
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
134
+ this.mainWindow.webContents.send('dictionary:sync-progress', update);
135
+ }
136
+
137
+ const dictionaryService = getDictionaryService();
138
+ dictionaryService.updateSyncStatus({
139
+ syncInProgress: update.phase !== 'complete' && update.phase !== 'error',
140
+ syncProgress: update.progress,
141
+ syncError: update.phase === 'error' ? update.message : null,
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Download the dictionary file from SharePoint
147
+ */
148
+ async sync(): Promise<DictionarySyncResponse> {
149
+ const startTime = Date.now();
150
+ const dictionaryService = getDictionaryService();
151
+
152
+ try {
153
+ if (!this.config) {
154
+ throw new Error('SharePoint configuration not set');
155
+ }
156
+
157
+ if (!this.clientSecret) {
158
+ throw new Error('Client secret not set');
159
+ }
160
+
161
+ // Initialize dictionary service if needed
162
+ await dictionaryService.initialize();
163
+
164
+ // Phase 1: Authentication
165
+ this.sendProgress({
166
+ phase: 'authenticating',
167
+ progress: 5,
168
+ message: 'Authenticating with Microsoft Graph...',
169
+ });
170
+
171
+ const accessToken = await this.getAccessToken();
172
+
173
+ // Phase 2: Download file
174
+ this.sendProgress({
175
+ phase: 'downloading',
176
+ progress: 20,
177
+ message: 'Downloading dictionary file from SharePoint...',
178
+ });
179
+
180
+ const fileBuffer = await this.downloadFile(accessToken);
181
+ const fileHash = createHash('sha256').update(fileBuffer).digest('hex');
182
+
183
+ // Check if file has changed
184
+ const currentHash = dictionaryService.getMetadata('fileHash');
185
+ if (currentHash === fileHash) {
186
+ this.sendProgress({
187
+ phase: 'complete',
188
+ progress: 100,
189
+ message: 'Dictionary is already up to date',
190
+ });
191
+
192
+ return {
193
+ success: true,
194
+ entriesImported: 0,
195
+ duration: Date.now() - startTime,
196
+ };
197
+ }
198
+
199
+ // Phase 3: Parse Excel
200
+ this.sendProgress({
201
+ phase: 'parsing',
202
+ progress: 50,
203
+ message: 'Parsing dictionary file...',
204
+ });
205
+
206
+ const entries = this.parseExcelFile(fileBuffer);
207
+
208
+ // Phase 4: Import to database
209
+ this.sendProgress({
210
+ phase: 'importing',
211
+ progress: 70,
212
+ message: `Importing ${entries.length.toLocaleString()} entries...`,
213
+ totalEntries: entries.length,
214
+ });
215
+
216
+ // Clear existing entries
217
+ dictionaryService.clearEntries();
218
+
219
+ // Import new entries
220
+ const result = dictionaryService.importEntries(entries, (processed, total) => {
221
+ const progress = 70 + Math.floor((processed / total) * 25);
222
+ this.sendProgress({
223
+ phase: 'importing',
224
+ progress,
225
+ message: `Importing entries... ${processed.toLocaleString()} / ${total.toLocaleString()}`,
226
+ entriesProcessed: processed,
227
+ totalEntries: total,
228
+ });
229
+ });
230
+
231
+ if (!result.success) {
232
+ throw new Error(result.error || 'Import failed');
233
+ }
234
+
235
+ // Save file hash
236
+ dictionaryService.setMetadata('fileHash', fileHash);
237
+
238
+ // Phase 5: Complete
239
+ this.sendProgress({
240
+ phase: 'complete',
241
+ progress: 100,
242
+ message: `Successfully imported ${result.imported.toLocaleString()} entries`,
243
+ entriesProcessed: result.imported,
244
+ totalEntries: result.imported,
245
+ });
246
+
247
+ const duration = Date.now() - startTime;
248
+ log.info('Dictionary sync completed', {
249
+ entries: result.imported,
250
+ duration: `${duration}ms`,
251
+ });
252
+
253
+ return {
254
+ success: true,
255
+ entriesImported: result.imported,
256
+ duration,
257
+ };
258
+ } catch (error) {
259
+ const message = error instanceof Error ? error.message : 'Unknown error';
260
+ log.error('Dictionary sync failed', { error: message });
261
+
262
+ this.sendProgress({
263
+ phase: 'error',
264
+ progress: 0,
265
+ message: `Sync failed: ${message}`,
266
+ });
267
+
268
+ dictionaryService.updateSyncStatus({
269
+ syncInProgress: false,
270
+ lastSyncSuccess: false,
271
+ syncError: message,
272
+ });
273
+
274
+ return {
275
+ success: false,
276
+ entriesImported: 0,
277
+ duration: Date.now() - startTime,
278
+ error: message,
279
+ };
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Download file from SharePoint using Microsoft Graph
285
+ */
286
+ private async downloadFile(accessToken: string): Promise<Buffer> {
287
+ if (!this.config) {
288
+ throw new Error('Configuration not set');
289
+ }
290
+
291
+ // Build Graph API URL for file download
292
+ // Format: /sites/{site-id}/drive/root:/{path}:/content
293
+ // Or: /sites/{hostname}:{site-path}:/drive/root:/{path}:/content
294
+ const siteUrl = new URL(this.config.siteUrl);
295
+ const sitePath = siteUrl.pathname;
296
+ const hostname = siteUrl.hostname;
297
+
298
+ // Construct the Graph API endpoint
299
+ const encodedPath = encodeURIComponent(this.config.documentLibraryPath);
300
+ const graphUrl = `https://graph.microsoft.com/v1.0/sites/${hostname}:${sitePath}:/drive/root:/${encodedPath}:/content`;
301
+
302
+ log.info('Downloading file from SharePoint', { url: graphUrl });
303
+
304
+ const response = await fetch(graphUrl, {
305
+ headers: {
306
+ Authorization: `Bearer ${accessToken}`,
307
+ },
308
+ });
309
+
310
+ if (!response.ok) {
311
+ const errorText = await response.text();
312
+ throw new Error(`Download failed: ${response.status} - ${errorText}`);
313
+ }
314
+
315
+ const arrayBuffer = await response.arrayBuffer();
316
+ return Buffer.from(arrayBuffer);
317
+ }
318
+
319
+ /**
320
+ * Parse Excel file and extract dictionary entries
321
+ *
322
+ * Looks for the "Daily_Inventory" sheet and optionally the "Dictionary_Table" table.
323
+ * Extracts Document_ID, Content_ID, Title, and Status columns.
324
+ */
325
+ parseExcelFile(buffer: Buffer): DictionaryEntry[] {
326
+ const workbook = XLSX.read(buffer, { type: 'buffer' });
327
+
328
+ // Find the Daily_Inventory sheet
329
+ const sheetName = 'Daily_Inventory';
330
+ const worksheet = workbook.Sheets[sheetName];
331
+
332
+ if (!worksheet) {
333
+ throw new Error(`Sheet "${sheetName}" not found in workbook`);
334
+ }
335
+
336
+ // Try to find the Dictionary_Table range
337
+ // Excel tables are stored in workbook metadata
338
+ let tableRange: string | undefined;
339
+ const tableName = 'Dictionary_Table';
340
+
341
+ // Check for named ranges (tables appear as defined names)
342
+ if (workbook.Workbook?.Names) {
343
+ const tableNameEntry = workbook.Workbook.Names.find(
344
+ (n: { Name: string; Ref?: string }) =>
345
+ n.Name === tableName || n.Name === `${sheetName}!${tableName}`
346
+ );
347
+ if (tableNameEntry?.Ref) {
348
+ // Extract the range from the reference (e.g., "Daily_Inventory!$A$1:$D$1000")
349
+ const refMatch = tableNameEntry.Ref.match(/\$?([A-Z]+)\$?(\d+):\$?([A-Z]+)\$?(\d+)/);
350
+ if (refMatch) {
351
+ tableRange = `${refMatch[1]}${refMatch[2]}:${refMatch[3]}${refMatch[4]}`;
352
+ log.info(`Found table "${tableName}" with range: ${tableRange}`);
353
+ }
354
+ }
355
+ }
356
+
357
+ // Convert to JSON with header row, using table range if found
358
+ const parseOptions: XLSX.Sheet2JSONOpts = {
359
+ raw: false,
360
+ defval: '',
361
+ };
362
+ if (tableRange) {
363
+ parseOptions.range = tableRange;
364
+ }
365
+
366
+ const rawData = XLSX.utils.sheet_to_json<Record<string, unknown>>(worksheet, parseOptions);
367
+
368
+ // Map to DictionaryEntry format - only extract required columns
369
+ const entries: DictionaryEntry[] = rawData.map((row) => ({
370
+ Document_ID: String(row['Document_ID'] || ''),
371
+ Content_ID: String(row['Content_ID'] || ''),
372
+ Title: String(row['Title'] || ''),
373
+ Summary: String(row['Summary'] || ''),
374
+ Type: String(row['Type'] || ''),
375
+ Release_Date: this.parseExcelDate(row['Release_Date']),
376
+ Expiration_Date: this.parseExcelDate(row['Expiration_Date']),
377
+ Status: String(row['Status'] || ''),
378
+ Owner: String(row['Owner'] || ''),
379
+ BPO: String(row['BPO'] || ''),
380
+ LOB: String(row['LOB'] || ''),
381
+ Last_Published_By: String(row['Last_Published_By'] || ''),
382
+ }));
383
+
384
+ // Filter out entries without Document_ID
385
+ const validEntries = entries.filter((e) => e.Document_ID.trim() !== '');
386
+
387
+ log.info('Parsed Excel file', {
388
+ totalRows: rawData.length,
389
+ validEntries: validEntries.length,
390
+ usedTableRange: tableRange || 'full sheet',
391
+ });
392
+
393
+ return validEntries;
394
+ }
395
+
396
+ /**
397
+ * Parse Excel date (handles Excel serial date numbers)
398
+ */
399
+ private parseExcelDate(value: unknown): string {
400
+ if (!value) return '';
401
+
402
+ // If it's already a string date, return it
403
+ if (typeof value === 'string') {
404
+ return value;
405
+ }
406
+
407
+ // If it's a number, convert from Excel serial date
408
+ if (typeof value === 'number') {
409
+ // Excel serial date: days since Dec 30, 1899
410
+ const date = new Date((value - 25569) * 86400 * 1000);
411
+ return date.toISOString().split('T')[0];
412
+ }
413
+
414
+ return String(value);
415
+ }
416
+
417
+ /**
418
+ * Start automatic sync scheduler
419
+ */
420
+ startScheduler(intervalHours: number): void {
421
+ // Clear existing scheduler
422
+ this.stopScheduler();
423
+
424
+ const intervalMs = intervalHours * 60 * 60 * 1000;
425
+
426
+ // Calculate next sync time
427
+ const dictionaryService = getDictionaryService();
428
+ const nextSync = new Date(Date.now() + intervalMs).toISOString();
429
+ dictionaryService.updateSyncStatus({ nextScheduledSync: nextSync });
430
+
431
+ this.schedulerInterval = setInterval(async () => {
432
+ log.info('Running scheduled dictionary sync');
433
+ await this.sync();
434
+
435
+ // Update next sync time
436
+ const nextSyncTime = new Date(Date.now() + intervalMs).toISOString();
437
+ dictionaryService.updateSyncStatus({ nextScheduledSync: nextSyncTime });
438
+ }, intervalMs);
439
+
440
+ log.info('Sync scheduler started', { intervalHours, nextSync });
441
+ }
442
+
443
+ /**
444
+ * Stop automatic sync scheduler
445
+ */
446
+ stopScheduler(): void {
447
+ if (this.schedulerInterval) {
448
+ clearInterval(this.schedulerInterval);
449
+ this.schedulerInterval = null;
450
+
451
+ const dictionaryService = getDictionaryService();
452
+ dictionaryService.updateSyncStatus({ nextScheduledSync: null });
453
+
454
+ log.info('Sync scheduler stopped');
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Check if scheduler is running
460
+ */
461
+ isSchedulerRunning(): boolean {
462
+ return this.schedulerInterval !== null;
463
+ }
464
+ }
465
+
466
+ // Singleton instance
467
+ let sharePointSyncServiceInstance: SharePointSyncService | null = null;
468
+
469
+ export function getSharePointSyncService(): SharePointSyncService {
470
+ if (!sharePointSyncServiceInstance) {
471
+ sharePointSyncServiceInstance = new SharePointSyncService();
472
+ }
473
+ return sharePointSyncServiceInstance;
474
+ }