documentation-hub 5.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,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
|
+
}
|