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,1085 @@
1
+ import {
2
+ DetailedHyperlinkInfo,
3
+ HyperlinkApiResponse,
4
+ HyperlinkApiResult,
5
+ HyperlinkApiSettings,
6
+ HyperlinkFixingOptions,
7
+ HyperlinkFixingResult,
8
+ HyperlinkProcessingOptions,
9
+ HyperlinkStatistics,
10
+ HyperlinkType,
11
+ HyperlinkValidationIssue,
12
+ PowerAutomateResponse,
13
+ URL_PATTERNS
14
+ } from '@/types/hyperlink';
15
+ import { Document } from '@/types/session';
16
+ import { UserSettings } from '@/types/settings';
17
+ import { logger } from '@/utils/logger';
18
+ import { sanitizeUrl, validatePowerAutomateUrl } from '@/utils/urlHelpers';
19
+ import { extractContentId, extractDocumentId } from '@/utils/urlPatterns';
20
+
21
+ /**
22
+ * Extended API response with results cache for O(1) lookups
23
+ */
24
+ interface HyperlinkApiResponseWithCache extends HyperlinkApiResponse {
25
+ resultsCache?: Map<string, HyperlinkApiResult>;
26
+ }
27
+
28
+ /**
29
+ * HyperlinkService - Singleton service for managing hyperlink operations
30
+ *
31
+ * Provides comprehensive hyperlink processing capabilities including:
32
+ * - PowerAutomate API integration for Content ID and Document ID lookups
33
+ * - URL pattern matching and validation
34
+ * - Hyperlink extraction and statistics
35
+ * - Retry logic with exponential backoff
36
+ * - Result caching for O(1) lookups
37
+ *
38
+ * @class HyperlinkService
39
+ * @singleton Use `HyperlinkService.getInstance()` to get the instance
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * // Get service instance
44
+ * const service = HyperlinkService.getInstance();
45
+ *
46
+ * // Initialize with user settings
47
+ * service.initialize(userSettings);
48
+ *
49
+ * // Process hyperlinks with API
50
+ * const response = await service.processHyperlinksWithApi(hyperlinks, apiSettings);
51
+ *
52
+ * // Validate hyperlinks
53
+ * const issues = await service.validateHyperlinks(document);
54
+ * ```
55
+ *
56
+ * @see {@link HyperlinkApiSettings} for API configuration options
57
+ * @see {@link DetailedHyperlinkInfo} for hyperlink data structure
58
+ */
59
+ export class HyperlinkService {
60
+ private static instance: HyperlinkService;
61
+ private apiSettings: HyperlinkApiSettings | null = null;
62
+ private userSettings: UserSettings | null = null;
63
+ private log = logger.namespace('HyperlinkService');
64
+
65
+ private constructor() {}
66
+
67
+ /**
68
+ * Get the singleton instance of HyperlinkService.
69
+ *
70
+ * @returns {HyperlinkService} The singleton instance
71
+ * @static
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const service = HyperlinkService.getInstance();
76
+ * ```
77
+ */
78
+ public static getInstance(): HyperlinkService {
79
+ if (!HyperlinkService.instance) {
80
+ HyperlinkService.instance = new HyperlinkService();
81
+ }
82
+ return HyperlinkService.instance;
83
+ }
84
+
85
+ /**
86
+ * Initialize the service with user settings.
87
+ *
88
+ * Configures the PowerAutomate API endpoint and validates the URL format.
89
+ * Should be called during application startup with the user's saved settings.
90
+ *
91
+ * @param {UserSettings} settings - User settings containing API configuration
92
+ * @throws {void} Does not throw - logs errors and continues with potentially invalid config
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const service = HyperlinkService.getInstance();
97
+ * service.initialize({
98
+ * apiConnections: {
99
+ * powerAutomateUrl: 'https://prod-XX.westus.logic.azure.com/...'
100
+ * }
101
+ * });
102
+ * ```
103
+ */
104
+ public initialize(settings: UserSettings): void {
105
+ // Store full settings for local dictionary access
106
+ this.userSettings = settings;
107
+
108
+ if (settings.apiConnections.powerAutomateUrl) {
109
+ // Sanitize the API URL to fix encoding issues
110
+ const sanitizedUrl = sanitizeUrl(settings.apiConnections.powerAutomateUrl);
111
+
112
+ // Validate URL format
113
+ const validation = validatePowerAutomateUrl(sanitizedUrl);
114
+ if (!validation.valid) {
115
+ this.log.error('Invalid PowerAutomate URL configuration:', validation.issues);
116
+ // Still set it but log the errors - let API call handle the failure
117
+ } else if (validation.warnings.length > 0) {
118
+ this.log.warn('PowerAutomate URL warnings:', validation.warnings);
119
+ }
120
+
121
+ this.apiSettings = {
122
+ apiUrl: sanitizedUrl,
123
+ timeout: 30000,
124
+ retryAttempts: 3,
125
+ retryDelay: 1000,
126
+ };
127
+
128
+ this.log.debug('Initialized API settings with sanitized URL:', sanitizedUrl);
129
+ }
130
+
131
+ // Log local dictionary status
132
+ if (settings.localDictionary?.enabled) {
133
+ this.log.info('Local dictionary mode enabled - using SQLite database for lookups');
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check if local dictionary mode is enabled
139
+ */
140
+ public isLocalDictionaryEnabled(): boolean {
141
+ return this.userSettings?.localDictionary?.enabled ?? false;
142
+ }
143
+
144
+ /**
145
+ * Process hyperlinks using local dictionary (SQLite database)
146
+ * This is used when local dictionary mode is enabled instead of Power Automate API
147
+ */
148
+ private async processHyperlinksWithLocalDictionary(
149
+ hyperlinks: DetailedHyperlinkInfo[]
150
+ ): Promise<HyperlinkApiResponse & { processedHyperlinks?: DetailedHyperlinkInfo[] }> {
151
+ try {
152
+ // Extract all IDs into a single lookup array
153
+ const lookupIds: string[] = [];
154
+ const uniqueIds = new Set<string>();
155
+
156
+ for (const hyperlink of hyperlinks) {
157
+ const contentId = extractContentId(hyperlink.url);
158
+ if (contentId && !uniqueIds.has(contentId)) {
159
+ lookupIds.push(contentId);
160
+ uniqueIds.add(contentId);
161
+ }
162
+
163
+ const documentId = extractDocumentId(hyperlink.url);
164
+ if (documentId && !uniqueIds.has(documentId)) {
165
+ lookupIds.push(documentId);
166
+ uniqueIds.add(documentId);
167
+ }
168
+ }
169
+
170
+ if (lookupIds.length === 0) {
171
+ // No IDs found is not a failure - it just means no hyperlinks need API processing
172
+ // Return success with empty results so other formatting operations can continue
173
+ this.log.warn('No Content_ID or Document_ID patterns found in hyperlinks - skipping API call');
174
+ return {
175
+ success: true,
176
+ timestamp: new Date(),
177
+ body: {
178
+ results: [],
179
+ errors: [],
180
+ },
181
+ };
182
+ }
183
+
184
+ // Check if electronAPI is available (handles SSR, tests, and browser contexts)
185
+ if (typeof window === 'undefined' || !window.electronAPI) {
186
+ return {
187
+ success: false,
188
+ timestamp: new Date(),
189
+ error: 'Electron API not available - local dictionary requires Electron environment',
190
+ };
191
+ }
192
+
193
+ this.log.info(`Processing ${lookupIds.length} IDs via local dictionary`);
194
+
195
+ // Call local dictionary batch lookup
196
+ const response = await window.electronAPI.dictionary.batchLookup(lookupIds);
197
+
198
+ if (!response.success || !response.results) {
199
+ return {
200
+ success: false,
201
+ timestamp: new Date(),
202
+ error: response.error || 'Local dictionary lookup failed',
203
+ };
204
+ }
205
+
206
+ this.log.info(`Local dictionary returned ${response.results.length} results`);
207
+
208
+ // Build results cache for O(1) lookups
209
+ const resultsMap = new Map<string, HyperlinkApiResult>();
210
+
211
+ const results: HyperlinkApiResult[] = response.results.map((result) => {
212
+ // Map local dictionary result to HyperlinkApiResult format
213
+ const normalizedStatus: HyperlinkApiResult['status'] =
214
+ result.Status?.toLowerCase() === 'deprecated'
215
+ ? 'deprecated'
216
+ : result.Status?.toLowerCase() === 'expired'
217
+ ? 'expired'
218
+ : result.Status?.toLowerCase() === 'not_found'
219
+ ? 'not_found'
220
+ : 'active';
221
+
222
+ const processed: HyperlinkApiResult = {
223
+ url: '',
224
+ documentId: result.Document_ID || '',
225
+ contentId: result.Content_ID || '',
226
+ title: result.Title || '',
227
+ status: normalizedStatus,
228
+ metadata: {},
229
+ };
230
+
231
+ // Cache by both IDs for quick lookup
232
+ if (processed.documentId) resultsMap.set(processed.documentId, processed);
233
+ if (processed.contentId) resultsMap.set(processed.contentId, processed);
234
+
235
+ return processed;
236
+ });
237
+
238
+ const apiResponse: HyperlinkApiResponse = {
239
+ success: true,
240
+ timestamp: new Date(),
241
+ body: {
242
+ results,
243
+ errors: [],
244
+ },
245
+ };
246
+
247
+ // Attach cache for O(1) lookups
248
+ (apiResponse as HyperlinkApiResponseWithCache).resultsCache = resultsMap;
249
+
250
+ return {
251
+ ...apiResponse,
252
+ processedHyperlinks: hyperlinks,
253
+ };
254
+ } catch (error) {
255
+ this.log.error('Local dictionary lookup error:', error);
256
+ return {
257
+ success: false,
258
+ timestamp: new Date(),
259
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
260
+ };
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Extract hyperlink IDs from documents
266
+ * This mimics the C# ExtractHyperlinkDataAsync functionality
267
+ */
268
+ public async extractHyperlinkData(document: Document): Promise<DetailedHyperlinkInfo[]> {
269
+ const hyperlinks: DetailedHyperlinkInfo[] = [];
270
+
271
+ // In a real implementation, this would parse the document
272
+ // For now, we'll simulate extraction
273
+ this.log.debug(`Extracting hyperlinks from document: ${document.name}`);
274
+
275
+ // Simulate finding hyperlinks
276
+ const mockHyperlinks: DetailedHyperlinkInfo[] = [
277
+ {
278
+ id: 'rId1',
279
+ relationshipId: 'rId1',
280
+ element: {} as any,
281
+ containingPart: 'document.xml',
282
+ url: 'https://thesource.cvshealth.com/docid=TSRC-ABC-123456',
283
+ displayText: 'Click here',
284
+ type: 'external' as HyperlinkType,
285
+ isInternal: false,
286
+ isValid: true,
287
+ context: 'See the document here: Click here for more information.',
288
+ },
289
+ {
290
+ id: 'rId2',
291
+ relationshipId: 'rId2',
292
+ element: {} as any,
293
+ containingPart: 'document.xml',
294
+ url: 'https://thesource.cvshealth.com/docid=CMS-XYZ-789012',
295
+ displayText: 'Reference',
296
+ type: 'external' as HyperlinkType,
297
+ isInternal: false,
298
+ isValid: true,
299
+ context: 'For additional details, see the Reference link.',
300
+ },
301
+ ];
302
+
303
+ return mockHyperlinks;
304
+ }
305
+
306
+ /**
307
+ * Process hyperlinks with PowerAutomate API.
308
+ *
309
+ * Sends collected Content IDs and Document IDs to the configured PowerAutomate endpoint
310
+ * for lookup. Returns updated hyperlink information including new URLs and display texts.
311
+ *
312
+ * **Processing Flow:**
313
+ * 1. Extract unique Content_ID and Document_ID from hyperlinks
314
+ * 2. Send batch request to PowerAutomate API
315
+ * 3. Build O(1) lookup cache from results
316
+ * 4. Match results back to original hyperlinks
317
+ *
318
+ * @param {DetailedHyperlinkInfo[]} hyperlinks - Array of hyperlinks to process
319
+ * @param {HyperlinkApiSettings} [settings] - Optional API settings (uses initialized settings if not provided)
320
+ * @param {Object} [userProfile] - Optional user profile for tracking
321
+ * @param {string} userProfile.firstName - User's first name
322
+ * @param {string} userProfile.lastName - User's last name
323
+ * @param {string} userProfile.email - User's email
324
+ * @returns {Promise<HyperlinkApiResponse & { processedHyperlinks?: DetailedHyperlinkInfo[] }>} API response with processed hyperlinks
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * const response = await service.processHyperlinksWithApi(hyperlinks, {
329
+ * apiUrl: 'https://prod-XX.westus.logic.azure.com/...',
330
+ * timeout: 30000,
331
+ * retryAttempts: 3
332
+ * });
333
+ *
334
+ * if (response.success) {
335
+ * console.log(`Processed ${response.results?.length} IDs`);
336
+ * }
337
+ * ```
338
+ */
339
+ public async processHyperlinksWithApi(
340
+ hyperlinks: DetailedHyperlinkInfo[],
341
+ settings?: HyperlinkApiSettings,
342
+ userProfile?: { firstName: string; lastName: string; email: string }
343
+ ): Promise<HyperlinkApiResponse & { processedHyperlinks?: DetailedHyperlinkInfo[] }> {
344
+ // Check if local dictionary mode is enabled
345
+ // When enabled, use local SQLite database instead of Power Automate API
346
+ if (this.isLocalDictionaryEnabled()) {
347
+ this.log.info('Using local dictionary for hyperlink processing (API call bypassed)');
348
+ return this.processHyperlinksWithLocalDictionary(hyperlinks);
349
+ }
350
+
351
+ const apiConfig = settings || this.apiSettings;
352
+
353
+ if (!apiConfig) {
354
+ return {
355
+ success: false,
356
+ timestamp: new Date(),
357
+ error: 'No API settings configured. Please configure PowerAutomate URL in settings.',
358
+ };
359
+ }
360
+
361
+ try {
362
+ // Extract all IDs (Content_ID and Document_ID) into a single Lookup_ID array
363
+ const lookupIds: string[] = [];
364
+ // Use Set to avoid duplicates
365
+ const uniqueIds = new Set<string>();
366
+
367
+ for (const hyperlink of hyperlinks) {
368
+ // Extract Content_ID if present
369
+ const contentId = extractContentId(hyperlink.url);
370
+ if (contentId && !uniqueIds.has(contentId)) {
371
+ lookupIds.push(contentId);
372
+ uniqueIds.add(contentId);
373
+ }
374
+
375
+ // Extract Document_ID if present
376
+ const documentId = extractDocumentId(hyperlink.url);
377
+ if (documentId && !uniqueIds.has(documentId)) {
378
+ lookupIds.push(documentId);
379
+ uniqueIds.add(documentId);
380
+ }
381
+ }
382
+
383
+ // If no IDs found, return early
384
+ if (lookupIds.length === 0) {
385
+ // No IDs found is not a failure - it just means no hyperlinks need API processing
386
+ // Return success with empty results so other formatting operations can continue
387
+ this.log.warn('No Content_ID or Document_ID patterns found in hyperlinks - skipping API call');
388
+ return {
389
+ success: true,
390
+ timestamp: new Date(),
391
+ body: {
392
+ results: [],
393
+ errors: [],
394
+ },
395
+ };
396
+ }
397
+
398
+ // Calculate hyperlink statistics
399
+ const totalHyperlinks = hyperlinks.length;
400
+ const hyperlinksChecked = hyperlinks.filter((h) =>
401
+ URL_PATTERNS.THE_SOURCE.pattern.test(h.url)
402
+ ).length;
403
+
404
+ // Create request payload matching the specification with profile data and statistics
405
+ const request = {
406
+ Lookup_ID: lookupIds,
407
+ Hyperlinks_Checked: hyperlinksChecked,
408
+ Total_Hyperlinks: totalHyperlinks,
409
+ First_Name: userProfile?.firstName || '',
410
+ Last_Name: userProfile?.lastName || '',
411
+ Email: userProfile?.email || '',
412
+ };
413
+
414
+ // Make API call
415
+ const response = await this.callPowerAutomateApi(apiConfig, request);
416
+
417
+ // Return response with processed hyperlinks for tracking
418
+ return {
419
+ ...response,
420
+ processedHyperlinks: hyperlinks,
421
+ };
422
+ } catch (error) {
423
+ return {
424
+ success: false,
425
+ timestamp: new Date(),
426
+ error: error instanceof Error ? error.message : 'Unknown error occurred',
427
+ };
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Fix source hyperlinks by appending Content IDs.
433
+ *
434
+ * Main hyperlink processing method that extracts, validates, and modifies
435
+ * hyperlinks in a document. Matches the functionality of the C# implementation.
436
+ *
437
+ * **Processing Phases:**
438
+ * 1. Extract hyperlinks from document
439
+ * 2. Call PowerAutomate API (if configured)
440
+ * 3. Append Content IDs to matching URLs
441
+ * 4. Update display texts as needed
442
+ *
443
+ * @param {Document} document - The document to process
444
+ * @param {HyperlinkFixingOptions} [options={}] - Processing options
445
+ * @param {string} [options.powerAutomateUrl] - PowerAutomate API endpoint URL
446
+ * @param {boolean} [options.appendContentId] - Whether to append content IDs
447
+ * @param {string} [options.contentIdToAppend] - Content ID to append (default: '#content')
448
+ * @param {boolean} [options.updateTitles] - Whether to update hyperlink titles
449
+ * @returns {Promise<HyperlinkFixingResult>} Processing results with statistics
450
+ *
451
+ * @example
452
+ * ```typescript
453
+ * const result = await service.fixSourceHyperlinks(document, {
454
+ * appendContentId: true,
455
+ * contentIdToAppend: '#content',
456
+ * updateTitles: true
457
+ * });
458
+ *
459
+ * console.log(`Modified ${result.modifiedHyperlinks} hyperlinks`);
460
+ * ```
461
+ */
462
+ public async fixSourceHyperlinks(
463
+ document: Document,
464
+ options: HyperlinkFixingOptions = {}
465
+ ): Promise<HyperlinkFixingResult> {
466
+ const startTime = Date.now();
467
+ const result: HyperlinkFixingResult = {
468
+ success: false,
469
+ totalHyperlinks: 0,
470
+ processedHyperlinks: 0,
471
+ modifiedHyperlinks: 0,
472
+ skippedHyperlinks: 0,
473
+ updatedUrls: 0,
474
+ updatedDisplayTexts: 0,
475
+ appendedContentIds: 0,
476
+ errorCount: 0,
477
+ errorMessages: [],
478
+ processedLinks: [],
479
+ validationIssues: [],
480
+ duration: 0,
481
+ };
482
+
483
+ try {
484
+ // Phase 1: Extract hyperlinks
485
+ const hyperlinks = await this.extractHyperlinkData(document);
486
+ result.totalHyperlinks = hyperlinks.length;
487
+
488
+ // Filter processable hyperlinks
489
+ const processableHyperlinks = hyperlinks.filter((h) =>
490
+ this.shouldProcessHyperlink(h, options)
491
+ );
492
+ result.processedHyperlinks = processableHyperlinks.length;
493
+ result.skippedHyperlinks = hyperlinks.length - processableHyperlinks.length;
494
+
495
+ if (processableHyperlinks.length === 0) {
496
+ result.success = true;
497
+ result.errorMessages.push('No processable hyperlinks found in the document');
498
+ return result;
499
+ }
500
+
501
+ // Phase 2: API Communication (if configured)
502
+ if (options.powerAutomateUrl || this.apiSettings) {
503
+ const apiResponse = await this.processHyperlinksWithApi(processableHyperlinks);
504
+ if (apiResponse.success && apiResponse.body?.results) {
505
+ // Apply API-based fixes
506
+ const apiFixResult = this.applyApiBasedFixes(processableHyperlinks, apiResponse);
507
+ result.updatedUrls += apiFixResult.updatedUrls;
508
+ result.updatedDisplayTexts += apiFixResult.updatedDisplayTexts;
509
+ }
510
+ }
511
+
512
+ // Phase 3: Apply content ID appending if configured
513
+ if (options.appendContentId) {
514
+ const contentId = options.contentIdToAppend || '#content';
515
+ const appendResult = this.appendContentIdToHyperlinks(processableHyperlinks, contentId);
516
+ result.appendedContentIds = appendResult.appendedCount;
517
+ result.updatedUrls += appendResult.appendedCount;
518
+ }
519
+
520
+ // Phase 4: Update titles if requested
521
+ if (options.updateTitles) {
522
+ const titleResult = this.updateHyperlinkTitles(processableHyperlinks);
523
+ result.updatedDisplayTexts += titleResult.updatedCount;
524
+ }
525
+
526
+ // Create summaries for processed links
527
+ result.processedLinks = processableHyperlinks.map((h) => ({
528
+ id: h.id,
529
+ url: h.url,
530
+ displayText: h.displayText,
531
+ type: h.type,
532
+ location: h.containingPart,
533
+ status: 'processed' as const,
534
+ modifications: [],
535
+ }));
536
+
537
+ result.modifiedHyperlinks = result.updatedUrls + result.updatedDisplayTexts;
538
+ result.success = true;
539
+ } catch (error) {
540
+ result.errorMessages.push(error instanceof Error ? error.message : 'Unknown error occurred');
541
+ } finally {
542
+ result.duration = Date.now() - startTime;
543
+ }
544
+
545
+ return result;
546
+ }
547
+
548
+ /**
549
+ * Validate hyperlinks in a document.
550
+ *
551
+ * Checks all hyperlinks for common issues including:
552
+ * - Invalid URL format
553
+ * - Missing Content ID in theSource URLs
554
+ * - Broken internal links
555
+ * - Empty display text
556
+ *
557
+ * @param {Document} document - The document to validate
558
+ * @returns {Promise<HyperlinkValidationIssue[]>} Array of validation issues found
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * const issues = await service.validateHyperlinks(document);
563
+ * if (issues.length > 0) {
564
+ * console.log(`Found ${issues.length} issues:`);
565
+ * issues.forEach(issue => console.log(` - ${issue.message}`));
566
+ * }
567
+ * ```
568
+ */
569
+ public async validateHyperlinks(document: Document): Promise<HyperlinkValidationIssue[]> {
570
+ const issues: HyperlinkValidationIssue[] = [];
571
+ const hyperlinks = await this.extractHyperlinkData(document);
572
+
573
+ for (const hyperlink of hyperlinks) {
574
+ // Check for invalid URL format
575
+ if (!this.isValidUrl(hyperlink.url)) {
576
+ issues.push({
577
+ hyperlinkId: hyperlink.id,
578
+ url: hyperlink.url,
579
+ issueType: 'invalid_url',
580
+ severity: 'error',
581
+ message: 'Invalid URL format',
582
+ suggestion: 'Fix the URL format or remove the hyperlink',
583
+ autoFixable: false,
584
+ });
585
+ }
586
+
587
+ // Check if theSource URLs need content ID
588
+ if (this.isTheSourceUrl(hyperlink.url) && this.needsContentId(hyperlink.url)) {
589
+ issues.push({
590
+ hyperlinkId: hyperlink.id,
591
+ url: hyperlink.url,
592
+ issueType: 'invalid_url',
593
+ severity: 'warning',
594
+ message: 'TheSource URL missing content ID',
595
+ suggestion: 'Append #content to the URL',
596
+ autoFixable: true,
597
+ });
598
+ }
599
+ }
600
+
601
+ return issues;
602
+ }
603
+
604
+ /**
605
+ * Get statistics about hyperlinks in a document.
606
+ *
607
+ * Analyzes all hyperlinks and returns detailed statistics including
608
+ * counts by type, validity, and domain distribution.
609
+ *
610
+ * @param {Document} document - The document to analyze
611
+ * @returns {Promise<HyperlinkStatistics>} Comprehensive hyperlink statistics
612
+ *
613
+ * @example
614
+ * ```typescript
615
+ * const stats = await service.getHyperlinkStatistics(document);
616
+ * console.log(`Total hyperlinks: ${stats.total}`);
617
+ * console.log(`External: ${stats.byType.external}`);
618
+ * console.log(`Internal: ${stats.byType.internal}`);
619
+ * console.log(`Valid: ${stats.validCount}, Invalid: ${stats.invalidCount}`);
620
+ * ```
621
+ */
622
+ public async getHyperlinkStatistics(document: Document): Promise<HyperlinkStatistics> {
623
+ const hyperlinks = await this.extractHyperlinkData(document);
624
+
625
+ const stats: HyperlinkStatistics = {
626
+ total: hyperlinks.length,
627
+ byType: {
628
+ external: 0,
629
+ internal: 0,
630
+ bookmark: 0,
631
+ email: 0,
632
+ file: 0,
633
+ },
634
+ byLocation: {
635
+ document: 0,
636
+ headers: 0,
637
+ footers: 0,
638
+ },
639
+ valid: 0,
640
+ invalid: 0,
641
+ withContentId: 0,
642
+ withoutContentId: 0,
643
+ duplicates: 0,
644
+ orphaned: 0,
645
+ };
646
+
647
+ const urlSet = new Set<string>();
648
+
649
+ for (const hyperlink of hyperlinks) {
650
+ // Count by type
651
+ stats.byType[hyperlink.type]++;
652
+
653
+ // Count by location
654
+ if (hyperlink.containingPart.includes('header')) {
655
+ stats.byLocation.headers++;
656
+ } else if (hyperlink.containingPart.includes('footer')) {
657
+ stats.byLocation.footers++;
658
+ } else {
659
+ stats.byLocation.document++;
660
+ }
661
+
662
+ // Count valid/invalid
663
+ if (hyperlink.isValid) {
664
+ stats.valid++;
665
+ } else {
666
+ stats.invalid++;
667
+ }
668
+
669
+ // Count content IDs
670
+ if (hyperlink.url.includes('#content')) {
671
+ stats.withContentId++;
672
+ } else {
673
+ stats.withoutContentId++;
674
+ }
675
+
676
+ // Check for duplicates
677
+ if (urlSet.has(hyperlink.url)) {
678
+ stats.duplicates++;
679
+ } else {
680
+ urlSet.add(hyperlink.url);
681
+ }
682
+ }
683
+
684
+ return stats;
685
+ }
686
+
687
+ // Private helper methods
688
+
689
+ private shouldProcessHyperlink(
690
+ hyperlink: DetailedHyperlinkInfo,
691
+ options: HyperlinkProcessingOptions
692
+ ): boolean {
693
+ if (hyperlink.isInternal && !options.processInternalLinks) return false;
694
+ if (!hyperlink.isInternal && !options.processExternalLinks) return false;
695
+
696
+ if (options.urlPattern) {
697
+ const pattern =
698
+ typeof options.urlPattern === 'string'
699
+ ? new RegExp(options.urlPattern, 'i')
700
+ : options.urlPattern;
701
+ if (!pattern.test(hyperlink.url)) return false;
702
+ }
703
+
704
+ if (options.displayTextPattern) {
705
+ const pattern =
706
+ typeof options.displayTextPattern === 'string'
707
+ ? new RegExp(options.displayTextPattern, 'i')
708
+ : options.displayTextPattern;
709
+ if (!pattern.test(hyperlink.displayText)) return false;
710
+ }
711
+
712
+ return true;
713
+ }
714
+
715
+ private applyApiBasedFixes(
716
+ hyperlinks: DetailedHyperlinkInfo[],
717
+ apiResponse: HyperlinkApiResponse
718
+ ): { updatedUrls: number; updatedDisplayTexts: number } {
719
+ let updatedUrls = 0;
720
+ let updatedDisplayTexts = 0;
721
+
722
+ if (!apiResponse.body?.results) return { updatedUrls, updatedDisplayTexts };
723
+
724
+ // Use cache for O(1) lookups if available
725
+ const apiResponseWithCache = apiResponse as HyperlinkApiResponseWithCache;
726
+ const resultsCache = apiResponseWithCache.resultsCache || new Map<string, HyperlinkApiResult>();
727
+
728
+ for (const hyperlink of hyperlinks) {
729
+ // CRITICAL PRE-FILTER: Extract IDs to determine if this hyperlink is processable
730
+ // Only hyperlinks with Content_ID or Document_ID patterns should be processed
731
+ const urlContentId = extractContentId(hyperlink.url);
732
+ const urlDocumentId = extractDocumentId(hyperlink.url);
733
+
734
+ // SKIP: This hyperlink doesn't contain Content_ID or Document_ID patterns
735
+ // Examples: external URLs, mailto links, internal bookmarks
736
+ if (!urlContentId && !urlDocumentId) {
737
+ this.log.debug(
738
+ `Skipping hyperlink (no Lookup_ID pattern): ${hyperlink.url.substring(0, 80)}`
739
+ );
740
+ continue; // Skip to next hyperlink - no API processing needed
741
+ }
742
+
743
+ // Try cache first for performance
744
+ let apiResult = null;
745
+ if (urlContentId && resultsCache.has(urlContentId)) {
746
+ apiResult = resultsCache.get(urlContentId);
747
+ } else if (urlDocumentId && resultsCache.has(urlDocumentId)) {
748
+ apiResult = resultsCache.get(urlDocumentId);
749
+ } else {
750
+ // Fallback to array search
751
+ apiResult = apiResponse.body.results.find((r) => {
752
+ if (urlContentId && r.contentId === urlContentId) return true;
753
+ if (urlDocumentId && r.documentId === urlDocumentId) return true;
754
+ return false;
755
+ });
756
+ }
757
+
758
+ if (apiResult) {
759
+ // Phase 3: URL Reconstruction
760
+ // Always use Document_ID in URL (never Content_ID)
761
+ const newUrl = `https://thesource.cvshealth.com/nuxeo/thesource/#!/view?docid=${apiResult.documentId}`;
762
+ if (newUrl !== hyperlink.url && apiResult.documentId) {
763
+ hyperlink.url = newUrl;
764
+ updatedUrls++;
765
+ }
766
+
767
+ // Phase 4: Display Text Rules
768
+ let newDisplayText = hyperlink.displayText;
769
+
770
+ // Remove existing Content_ID pattern (4-6 digits in parentheses)
771
+ newDisplayText = newDisplayText.replace(/\s*\(\d{4,6}\)\s*$/, '');
772
+
773
+ // Update title if mismatch detected
774
+ if (apiResult.title && newDisplayText.trim() !== apiResult.title.trim()) {
775
+ newDisplayText = apiResult.title;
776
+ }
777
+
778
+ // Append Content_ID (last 6 digits) if present
779
+ if (apiResult.contentId) {
780
+ // Extract last digits and pad with zeros if needed
781
+ const contentIdMatch = apiResult.contentId.match(/(\d+)$/);
782
+ if (contentIdMatch) {
783
+ const digits = contentIdMatch[1].padStart(6, '0').slice(-6);
784
+ newDisplayText = `${newDisplayText.trim()} (${digits})`;
785
+ }
786
+ }
787
+
788
+ // Add status indicators for deprecated or expired documents
789
+ if (apiResult.status === 'deprecated' || apiResult.status === 'expired') {
790
+ newDisplayText += ' - Expired';
791
+ }
792
+
793
+ if (newDisplayText !== hyperlink.displayText) {
794
+ hyperlink.displayText = newDisplayText;
795
+ updatedDisplayTexts++;
796
+ }
797
+ } else {
798
+ // API result not found - BUT only mark as "Not Found" if we extracted valid IDs
799
+ // This prevents marking external/internal links that were never meant to be processed
800
+ // (Note: We only reach here if urlContentId or urlDocumentId exists, due to the pre-filter above)
801
+ this.log.warn(`No API result for hyperlink with Lookup_ID: ${hyperlink.url}`);
802
+ if (!hyperlink.displayText.includes(' - Not Found')) {
803
+ hyperlink.displayText += ' - Not Found';
804
+ updatedDisplayTexts++;
805
+ }
806
+ }
807
+ }
808
+
809
+ return { updatedUrls, updatedDisplayTexts };
810
+ }
811
+
812
+ private appendContentIdToHyperlinks(
813
+ hyperlinks: DetailedHyperlinkInfo[],
814
+ contentId: string
815
+ ): { appendedCount: number } {
816
+ let appendedCount = 0;
817
+
818
+ for (const hyperlink of hyperlinks) {
819
+ if (
820
+ this.isTheSourceUrl(hyperlink.url) &&
821
+ this.needsContentId(hyperlink.url) &&
822
+ !hyperlink.url.includes(contentId)
823
+ ) {
824
+ hyperlink.url += contentId;
825
+ appendedCount++;
826
+ }
827
+ }
828
+
829
+ return { appendedCount };
830
+ }
831
+
832
+ private updateHyperlinkTitles(hyperlinks: DetailedHyperlinkInfo[]): { updatedCount: number } {
833
+ let updatedCount = 0;
834
+
835
+ for (const hyperlink of hyperlinks) {
836
+ const newTitle = this.extractTitleFromUrl(hyperlink.url);
837
+ if (newTitle && newTitle !== hyperlink.displayText) {
838
+ hyperlink.displayText = newTitle;
839
+ updatedCount++;
840
+ }
841
+ }
842
+
843
+ return { updatedCount };
844
+ }
845
+
846
+ private async callPowerAutomateApi(
847
+ settings: HyperlinkApiSettings,
848
+ request: {
849
+ Lookup_ID: string[];
850
+ Hyperlinks_Checked: number;
851
+ Total_Hyperlinks: number;
852
+ First_Name: string;
853
+ Last_Name: string;
854
+ Email: string;
855
+ }
856
+ ): Promise<HyperlinkApiResponse> {
857
+ const timeoutMs = settings.timeout || 30000;
858
+ const maxRetries = settings.retryAttempts || 3;
859
+
860
+ // =========================================================================
861
+ // COMPREHENSIVE LOGGING - API CALL START
862
+ // =========================================================================
863
+ this.log.info('═══════════════════════════════════════════════════════════════');
864
+ this.log.info('[HyperlinkService] Starting Power Automate API Call');
865
+ this.log.info('═══════════════════════════════════════════════════════════════');
866
+ this.log.info(`[HyperlinkService] Timestamp: ${new Date().toISOString()}`);
867
+ this.log.info(`[HyperlinkService] Lookup IDs: ${request.Lookup_ID.length} IDs`);
868
+ this.log.info(`[HyperlinkService] IDs: ${request.Lookup_ID.join(', ')}`);
869
+ this.log.info(`[HyperlinkService] Hyperlinks Checked: ${request.Hyperlinks_Checked}`);
870
+ this.log.info(`[HyperlinkService] Total Hyperlinks: ${request.Total_Hyperlinks}`);
871
+ this.log.info(`[HyperlinkService] User: ${request.First_Name} ${request.Last_Name}`);
872
+ this.log.info(`[HyperlinkService] Timeout: ${timeoutMs}ms`);
873
+ this.log.info(`[HyperlinkService] Max Retries: ${maxRetries}`);
874
+
875
+ // Sanitize the API URL to fix any encoding issues
876
+ const sanitizedUrl = sanitizeUrl(settings.apiUrl);
877
+
878
+ if (sanitizedUrl !== settings.apiUrl) {
879
+ this.log.info('[HyperlinkService] URL sanitized - Fixed encoding issues');
880
+ }
881
+
882
+ this.log.info(`[HyperlinkService] API URL: ${sanitizedUrl}`);
883
+
884
+ // Validate the URL before using it
885
+ const validation = validatePowerAutomateUrl(sanitizedUrl);
886
+ if (!validation.valid) {
887
+ const errorMsg = `Invalid PowerAutomate URL: ${validation.issues.join(', ')}`;
888
+ this.log.error('[HyperlinkService] URL Validation FAILED:', errorMsg);
889
+ throw new Error(errorMsg);
890
+ }
891
+ this.log.info('[HyperlinkService] URL Validation: PASSED');
892
+
893
+ // Use main process net.request via IPC (matches C# HttpClient behavior on corporate networks)
894
+ // This uses Chromium's networking stack which respects system proxy and certificates
895
+ if (typeof window !== 'undefined' && window.electronAPI?.callPowerAutomateApi) {
896
+ this.log.info('[HyperlinkService] Using IPC -> Main Process -> net.request');
897
+ this.log.info('───────────────────────────────────────────────────────────────');
898
+
899
+ let lastError: Error | null = null;
900
+
901
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
902
+ try {
903
+ if (attempt > 0) {
904
+ const delay = Math.pow(2, attempt) * 1000;
905
+ this.log.info(`[HyperlinkService] Retry attempt ${attempt + 1} of ${maxRetries} (waiting ${delay}ms)`);
906
+ await new Promise((resolve) => setTimeout(resolve, delay));
907
+ }
908
+
909
+ this.log.info(`[HyperlinkService] Sending IPC request (attempt ${attempt + 1})...`);
910
+ const startTime = Date.now();
911
+
912
+ const response = await window.electronAPI.callPowerAutomateApi(
913
+ sanitizedUrl,
914
+ request,
915
+ timeoutMs
916
+ );
917
+
918
+ const duration = Date.now() - startTime;
919
+
920
+ if (!response.success) {
921
+ this.log.error('[HyperlinkService] API call FAILED');
922
+ this.log.error(`[HyperlinkService] Error: ${response.error}`);
923
+ this.log.error(`[HyperlinkService] Duration: ${duration}ms`);
924
+ throw new Error(response.error || `API returned status ${response.statusCode}`);
925
+ }
926
+
927
+ const data = response.data as { Results?: Array<{ Document_ID?: string; Content_ID?: string; Title?: string; Status?: string }> };
928
+
929
+ this.log.info('═══════════════════════════════════════════════════════════════');
930
+ this.log.info('[HyperlinkService] API Call SUCCESS');
931
+ this.log.info('═══════════════════════════════════════════════════════════════');
932
+ this.log.info(`[HyperlinkService] Status Code: ${response.statusCode}`);
933
+ this.log.info(`[HyperlinkService] Duration: ${duration}ms`);
934
+ this.log.info(`[HyperlinkService] Results: ${data?.Results?.length || 0} items`);
935
+
936
+ const apiResponse: HyperlinkApiResponse = {
937
+ success: Array.isArray(data?.Results),
938
+ timestamp: new Date(),
939
+ statusCode: response.statusCode,
940
+ };
941
+
942
+ if (data?.Results) {
943
+ apiResponse.body = this.parseApiResults(data.Results);
944
+ this.log.info(`[HyperlinkService] Parsed ${data.Results.length} results into cache`);
945
+ }
946
+
947
+ this.log.info('═══════════════════════════════════════════════════════════════');
948
+
949
+ return apiResponse;
950
+ } catch (error) {
951
+ lastError = error as Error;
952
+ this.log.error(`[HyperlinkService] Attempt ${attempt + 1} failed: ${lastError.message}`);
953
+ // Check if it's a timeout error
954
+ if (error instanceof Error && error.message.includes('timeout')) {
955
+ this.log.error('[HyperlinkService] Timeout detected, not retrying');
956
+ break;
957
+ }
958
+ }
959
+ }
960
+
961
+ if (lastError && lastError.message.includes('timeout')) {
962
+ this.log.error(`[HyperlinkService] Final error: API request timeout after ${timeoutMs}ms`);
963
+ throw new Error(`API request timeout after ${timeoutMs}ms`);
964
+ }
965
+
966
+ this.log.error(`[HyperlinkService] All ${maxRetries} attempts failed`);
967
+ throw lastError || new Error('API request failed after retries');
968
+ }
969
+
970
+ // No Electron API available - provide detailed diagnostics
971
+ const windowExists = typeof window !== 'undefined';
972
+ const apiExists = windowExists && typeof window.electronAPI !== 'undefined';
973
+ const methodExists = apiExists && typeof window.electronAPI?.callPowerAutomateApi === 'function';
974
+
975
+ this.log.error('[HyperlinkService] ERROR: Electron API not available!');
976
+ this.log.error(`[HyperlinkService] Diagnostics: window=${windowExists}, electronAPI=${apiExists}, callPowerAutomateApi=${methodExists}`);
977
+
978
+ if (!windowExists) {
979
+ this.log.error('[HyperlinkService] Running in non-browser context (SSR/Node.js)');
980
+ } else if (!apiExists) {
981
+ this.log.error('[HyperlinkService] Preload script may not have loaded - check BrowserWindow preload configuration');
982
+ } else if (!methodExists) {
983
+ this.log.error('[HyperlinkService] electronAPI exists but callPowerAutomateApi method is missing');
984
+ }
985
+
986
+ throw new Error('Electron API not available - cannot make API call. Ensure the app is running in Electron.');
987
+ }
988
+
989
+ /**
990
+ * Parse API results into standardized format with caching
991
+ * Accepts results from both IPC (optional fields) and direct API (required fields)
992
+ */
993
+ private parseApiResults(results: Array<{
994
+ Document_ID?: string;
995
+ Content_ID?: string;
996
+ Title?: string;
997
+ Status?: string;
998
+ }>): HyperlinkApiResponse['body'] {
999
+ const resultsMap = new Map<string, HyperlinkApiResult>();
1000
+
1001
+ const parsedResults = results?.map((result) => {
1002
+ // Trim whitespace from all fields as specified
1003
+ const rawStatus = result.Status?.trim() || 'Active';
1004
+ // Normalize status to match HyperlinkApiResult type
1005
+ const normalizedStatus: HyperlinkApiResult['status'] =
1006
+ rawStatus.toLowerCase() === 'deprecated'
1007
+ ? 'deprecated'
1008
+ : rawStatus.toLowerCase() === 'expired'
1009
+ ? 'expired'
1010
+ : rawStatus.toLowerCase() === 'moved'
1011
+ ? 'moved'
1012
+ : rawStatus.toLowerCase() === 'not_found'
1013
+ ? 'not_found'
1014
+ : 'active';
1015
+
1016
+ const processed: HyperlinkApiResult = {
1017
+ url: '', // Will be constructed from Document_ID
1018
+ documentId: result.Document_ID?.trim() || '',
1019
+ contentId: result.Content_ID?.trim() || '',
1020
+ title: result.Title?.trim() || '',
1021
+ status: normalizedStatus,
1022
+ metadata: {},
1023
+ };
1024
+
1025
+ // Cache by both IDs for quick lookup
1026
+ if (processed.documentId) resultsMap.set(processed.documentId, processed);
1027
+ if (processed.contentId) resultsMap.set(processed.contentId, processed);
1028
+
1029
+ return processed;
1030
+ }) || [];
1031
+
1032
+ return {
1033
+ results: parsedResults,
1034
+ errors: [],
1035
+ // Note: resultsCache is added by caller if needed
1036
+ };
1037
+ }
1038
+
1039
+ // Utility methods matching the C# implementation
1040
+
1041
+ private isValidUrl(url: string): boolean {
1042
+ if (!url) return false;
1043
+ if (url.startsWith('#')) return true; // Internal links
1044
+
1045
+ try {
1046
+ new URL(url);
1047
+ return true;
1048
+ } catch {
1049
+ return false;
1050
+ }
1051
+ }
1052
+
1053
+ private isTheSourceUrl(url: string): boolean {
1054
+ return URL_PATTERNS.THE_SOURCE.pattern.test(url);
1055
+ }
1056
+
1057
+ private needsContentId(url: string): boolean {
1058
+ return (
1059
+ this.isTheSourceUrl(url) &&
1060
+ (URL_PATTERNS.CONTENT_ID.pattern.test(url) || URL_PATTERNS.DOCUMENT_ID.pattern.test(url))
1061
+ );
1062
+ }
1063
+
1064
+ // Extraction methods moved to centralized utility: src/utils/urlPatterns.ts
1065
+ // Use: extractContentId(url), extractDocumentId(url), isTheSourceUrl(url)
1066
+
1067
+ private extractTitleFromUrl(url: string): string | null {
1068
+ try {
1069
+ const urlObj = new URL(url);
1070
+ const segments = urlObj.pathname.split('/').filter(Boolean);
1071
+ if (segments.length > 0) {
1072
+ const lastSegment = segments[segments.length - 1];
1073
+ if (lastSegment && lastSegment.length > 3) {
1074
+ return decodeURIComponent(lastSegment).replace(/[-_]/g, ' ');
1075
+ }
1076
+ }
1077
+ } catch {
1078
+ // Return null if extraction fails
1079
+ }
1080
+ return null;
1081
+ }
1082
+ }
1083
+
1084
+ // Export singleton instance
1085
+ export const hyperlinkService = HyperlinkService.getInstance();