@pax8-cta/core 0.1.0
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/LICENSE +198 -0
- package/dist/auth/device-code-login.d.ts +40 -0
- package/dist/auth/device-code-login.d.ts.map +1 -0
- package/dist/auth/device-code-login.js +59 -0
- package/dist/auth/device-code-login.js.map +1 -0
- package/dist/auth/gdap-client.d.ts +81 -0
- package/dist/auth/gdap-client.d.ts.map +1 -0
- package/dist/auth/gdap-client.js +128 -0
- package/dist/auth/gdap-client.js.map +1 -0
- package/dist/auth/index.d.ts +19 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +19 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/token-manager.d.ts +54 -0
- package/dist/auth/token-manager.d.ts.map +1 -0
- package/dist/auth/token-manager.js +150 -0
- package/dist/auth/token-manager.js.map +1 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +27 -0
- package/dist/client.js.map +1 -0
- package/dist/config/client.d.ts +24 -0
- package/dist/config/client.d.ts.map +1 -0
- package/dist/config/client.js +18 -0
- package/dist/config/client.js.map +1 -0
- package/dist/config/index.d.ts +18 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +18 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +81 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +271 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +751 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +556 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/constants.d.ts +116 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +170 -0
- package/dist/constants.js.map +1 -0
- package/dist/dataverse/agent-resolver.d.ts +98 -0
- package/dist/dataverse/agent-resolver.d.ts.map +1 -0
- package/dist/dataverse/agent-resolver.js +185 -0
- package/dist/dataverse/agent-resolver.js.map +1 -0
- package/dist/dataverse/client.d.ts +104 -0
- package/dist/dataverse/client.d.ts.map +1 -0
- package/dist/dataverse/client.js +272 -0
- package/dist/dataverse/client.js.map +1 -0
- package/dist/dataverse/connection-refs.d.ts +115 -0
- package/dist/dataverse/connection-refs.d.ts.map +1 -0
- package/dist/dataverse/connection-refs.js +203 -0
- package/dist/dataverse/connection-refs.js.map +1 -0
- package/dist/dataverse/index.d.ts +20 -0
- package/dist/dataverse/index.d.ts.map +1 -0
- package/dist/dataverse/index.js +20 -0
- package/dist/dataverse/index.js.map +1 -0
- package/dist/dataverse/solution-ops.d.ts +100 -0
- package/dist/dataverse/solution-ops.d.ts.map +1 -0
- package/dist/dataverse/solution-ops.js +288 -0
- package/dist/dataverse/solution-ops.js.map +1 -0
- package/dist/errors.d.ts +171 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +178 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/mock/demo-data.d.ts +213 -0
- package/dist/mock/demo-data.d.ts.map +1 -0
- package/dist/mock/demo-data.js +1096 -0
- package/dist/mock/demo-data.js.map +1 -0
- package/dist/mock/demo-deployment-store.d.ts +77 -0
- package/dist/mock/demo-deployment-store.d.ts.map +1 -0
- package/dist/mock/demo-deployment-store.js +85 -0
- package/dist/mock/demo-deployment-store.js.map +1 -0
- package/dist/powerplatform/admin-client.d.ts +226 -0
- package/dist/powerplatform/admin-client.d.ts.map +1 -0
- package/dist/powerplatform/admin-client.js +315 -0
- package/dist/powerplatform/admin-client.js.map +1 -0
- package/dist/powerplatform/index.d.ts +18 -0
- package/dist/powerplatform/index.d.ts.map +1 -0
- package/dist/powerplatform/index.js +18 -0
- package/dist/powerplatform/index.js.map +1 -0
- package/dist/powerplatform/tenant-discovery.d.ts +100 -0
- package/dist/powerplatform/tenant-discovery.d.ts.map +1 -0
- package/dist/powerplatform/tenant-discovery.js +205 -0
- package/dist/powerplatform/tenant-discovery.js.map +1 -0
- package/dist/preconditions/check.d.ts +41 -0
- package/dist/preconditions/check.d.ts.map +1 -0
- package/dist/preconditions/check.js +173 -0
- package/dist/preconditions/check.js.map +1 -0
- package/dist/preconditions/index.d.ts +20 -0
- package/dist/preconditions/index.d.ts.map +1 -0
- package/dist/preconditions/index.js +20 -0
- package/dist/preconditions/index.js.map +1 -0
- package/dist/preconditions/loader.d.ts +33 -0
- package/dist/preconditions/loader.d.ts.map +1 -0
- package/dist/preconditions/loader.js +65 -0
- package/dist/preconditions/loader.js.map +1 -0
- package/dist/preconditions/schema.d.ts +103 -0
- package/dist/preconditions/schema.d.ts.map +1 -0
- package/dist/preconditions/schema.js +93 -0
- package/dist/preconditions/schema.js.map +1 -0
- package/dist/preconditions/types.d.ts +118 -0
- package/dist/preconditions/types.d.ts.map +1 -0
- package/dist/preconditions/types.js +17 -0
- package/dist/preconditions/types.js.map +1 -0
- package/dist/queue/index.d.ts +17 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +17 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/memory-queue.d.ts +86 -0
- package/dist/queue/memory-queue.d.ts.map +1 -0
- package/dist/queue/memory-queue.js +221 -0
- package/dist/queue/memory-queue.js.map +1 -0
- package/dist/services/audit-log.d.ts +59 -0
- package/dist/services/audit-log.d.ts.map +1 -0
- package/dist/services/audit-log.js +193 -0
- package/dist/services/audit-log.js.map +1 -0
- package/dist/services/auth-error-parser.d.ts +36 -0
- package/dist/services/auth-error-parser.d.ts.map +1 -0
- package/dist/services/auth-error-parser.js +90 -0
- package/dist/services/auth-error-parser.js.map +1 -0
- package/dist/services/deployment-doctor.d.ts +109 -0
- package/dist/services/deployment-doctor.d.ts.map +1 -0
- package/dist/services/deployment-doctor.js +476 -0
- package/dist/services/deployment-doctor.js.map +1 -0
- package/dist/services/deployment-notifications.d.ts +41 -0
- package/dist/services/deployment-notifications.d.ts.map +1 -0
- package/dist/services/deployment-notifications.js +161 -0
- package/dist/services/deployment-notifications.js.map +1 -0
- package/dist/services/deployment-progress.d.ts +89 -0
- package/dist/services/deployment-progress.d.ts.map +1 -0
- package/dist/services/deployment-progress.js +244 -0
- package/dist/services/deployment-progress.js.map +1 -0
- package/dist/services/deployment-service.d.ts +97 -0
- package/dist/services/deployment-service.d.ts.map +1 -0
- package/dist/services/deployment-service.js +375 -0
- package/dist/services/deployment-service.js.map +1 -0
- package/dist/services/drift-analyzer.d.ts +86 -0
- package/dist/services/drift-analyzer.d.ts.map +1 -0
- package/dist/services/drift-analyzer.js +273 -0
- package/dist/services/drift-analyzer.js.map +1 -0
- package/dist/services/environment-setup.d.ts +97 -0
- package/dist/services/environment-setup.d.ts.map +1 -0
- package/dist/services/environment-setup.js +250 -0
- package/dist/services/environment-setup.js.map +1 -0
- package/dist/services/health-check.d.ts +168 -0
- package/dist/services/health-check.d.ts.map +1 -0
- package/dist/services/health-check.js +705 -0
- package/dist/services/health-check.js.map +1 -0
- package/dist/services/index.d.ts +39 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +39 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/logger.d.ts +139 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +268 -0
- package/dist/services/logger.js.map +1 -0
- package/dist/services/notification-service.d.ts +55 -0
- package/dist/services/notification-service.d.ts.map +1 -0
- package/dist/services/notification-service.js +184 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/services/risk-analyzer.d.ts +252 -0
- package/dist/services/risk-analyzer.d.ts.map +1 -0
- package/dist/services/risk-analyzer.js +866 -0
- package/dist/services/risk-analyzer.js.map +1 -0
- package/dist/services/rollback.d.ts +57 -0
- package/dist/services/rollback.d.ts.map +1 -0
- package/dist/services/rollback.js +270 -0
- package/dist/services/rollback.js.map +1 -0
- package/dist/services/scheduler.d.ts +80 -0
- package/dist/services/scheduler.d.ts.map +1 -0
- package/dist/services/scheduler.js +350 -0
- package/dist/services/scheduler.js.map +1 -0
- package/dist/services/secrets.d.ts +31 -0
- package/dist/services/secrets.d.ts.map +1 -0
- package/dist/services/secrets.js +206 -0
- package/dist/services/secrets.js.map +1 -0
- package/dist/services/settings-service.d.ts +132 -0
- package/dist/services/settings-service.d.ts.map +1 -0
- package/dist/services/settings-service.js +378 -0
- package/dist/services/settings-service.js.map +1 -0
- package/dist/services/solution-diff.d.ts +127 -0
- package/dist/services/solution-diff.d.ts.map +1 -0
- package/dist/services/solution-diff.js +260 -0
- package/dist/services/solution-diff.js.map +1 -0
- package/dist/services/solution-mode-detector.d.ts +35 -0
- package/dist/services/solution-mode-detector.d.ts.map +1 -0
- package/dist/services/solution-mode-detector.js +84 -0
- package/dist/services/solution-mode-detector.js.map +1 -0
- package/dist/services/tenant-resolver.d.ts +55 -0
- package/dist/services/tenant-resolver.d.ts.map +1 -0
- package/dist/services/tenant-resolver.js +126 -0
- package/dist/services/tenant-resolver.js.map +1 -0
- package/dist/services/unmanaged-customizations.d.ts +104 -0
- package/dist/services/unmanaged-customizations.d.ts.map +1 -0
- package/dist/services/unmanaged-customizations.js +521 -0
- package/dist/services/unmanaged-customizations.js.map +1 -0
- package/dist/services/url-templater.d.ts +184 -0
- package/dist/services/url-templater.d.ts.map +1 -0
- package/dist/services/url-templater.js +327 -0
- package/dist/services/url-templater.js.map +1 -0
- package/dist/services/version-checker.d.ts +108 -0
- package/dist/services/version-checker.d.ts.map +1 -0
- package/dist/services/version-checker.js +403 -0
- package/dist/services/version-checker.js.map +1 -0
- package/dist/services/waves.d.ts +90 -0
- package/dist/services/waves.d.ts.map +1 -0
- package/dist/services/waves.js +222 -0
- package/dist/services/waves.js.map +1 -0
- package/dist/services/webhook.d.ts +95 -0
- package/dist/services/webhook.d.ts.map +1 -0
- package/dist/services/webhook.js +244 -0
- package/dist/services/webhook.js.map +1 -0
- package/dist/utils/deployment-tools.d.ts +110 -0
- package/dist/utils/deployment-tools.d.ts.map +1 -0
- package/dist/utils/deployment-tools.js +121 -0
- package/dist/utils/deployment-tools.js.map +1 -0
- package/dist/utils/index.d.ts +17 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +18 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Pax8, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { parseDuration } from "../config/schema.js";
|
|
17
|
+
import { DEFAULT_HEALTH_CHECK_RETRIES, DEFAULT_HEALTH_CHECK_EXPECTED_STATUS, HEALTH_CHECK_CACHE_DURATION_MS, HEALTH_CHECK_TIMEOUT_MS, } from "../constants.js";
|
|
18
|
+
import { getDemoTenantMetadata } from "../mock/demo-data.js";
|
|
19
|
+
/**
|
|
20
|
+
* Service for performing health checks on tenant environments
|
|
21
|
+
*/
|
|
22
|
+
export class HealthCheckService {
|
|
23
|
+
/**
|
|
24
|
+
* Perform all health checks for a tenant
|
|
25
|
+
*/
|
|
26
|
+
async checkTenantHealth(tenant, client, settings) {
|
|
27
|
+
const checks = [];
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
// Merge with defaults to ensure all properties exist
|
|
30
|
+
const defaultSettings = {
|
|
31
|
+
enabled: true,
|
|
32
|
+
expectedStatus: DEFAULT_HEALTH_CHECK_EXPECTED_STATUS,
|
|
33
|
+
timeout: "30s",
|
|
34
|
+
retries: DEFAULT_HEALTH_CHECK_RETRIES,
|
|
35
|
+
};
|
|
36
|
+
const healthSettings = {
|
|
37
|
+
...defaultSettings,
|
|
38
|
+
...tenant.healthCheck,
|
|
39
|
+
...settings,
|
|
40
|
+
};
|
|
41
|
+
if (!healthSettings.enabled) {
|
|
42
|
+
return {
|
|
43
|
+
healthy: true,
|
|
44
|
+
tenantId: tenant.tenantId,
|
|
45
|
+
tenantName: tenant.name,
|
|
46
|
+
checks: [
|
|
47
|
+
{
|
|
48
|
+
name: "health_check_disabled",
|
|
49
|
+
passed: true,
|
|
50
|
+
message: "Health checks are disabled for this tenant",
|
|
51
|
+
durationMs: 0,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
totalDurationMs: 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const timeout = healthSettings.timeout
|
|
58
|
+
? parseDuration(healthSettings.timeout)
|
|
59
|
+
: HEALTH_CHECK_TIMEOUT_MS;
|
|
60
|
+
// Check 1: Dataverse connectivity
|
|
61
|
+
const dataverseCheck = await this.checkDataverseConnectivity(client, timeout);
|
|
62
|
+
checks.push(dataverseCheck);
|
|
63
|
+
// Check 2: Custom endpoint if configured
|
|
64
|
+
if (healthSettings.endpoint) {
|
|
65
|
+
const endpointCheck = await this.checkCustomEndpoint(healthSettings.endpoint, healthSettings.expectedStatus ?? DEFAULT_HEALTH_CHECK_EXPECTED_STATUS, timeout, healthSettings.retries ?? DEFAULT_HEALTH_CHECK_RETRIES);
|
|
66
|
+
checks.push(endpointCheck);
|
|
67
|
+
}
|
|
68
|
+
// Check 3: Solution import capability
|
|
69
|
+
const importCheck = await this.checkSolutionImportCapability(client, timeout);
|
|
70
|
+
checks.push(importCheck);
|
|
71
|
+
const totalDurationMs = Date.now() - startTime;
|
|
72
|
+
const healthy = checks.every((c) => c.passed);
|
|
73
|
+
return {
|
|
74
|
+
healthy,
|
|
75
|
+
tenantId: tenant.tenantId,
|
|
76
|
+
tenantName: tenant.name,
|
|
77
|
+
checks,
|
|
78
|
+
totalDurationMs,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check basic Dataverse connectivity
|
|
83
|
+
*/
|
|
84
|
+
async checkDataverseConnectivity(client, timeout) {
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
try {
|
|
87
|
+
// Use AbortController for timeout
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
90
|
+
// Query WhoAmI to verify authentication and connectivity
|
|
91
|
+
const result = await Promise.race([
|
|
92
|
+
client.get("/WhoAmI"),
|
|
93
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
|
|
94
|
+
]);
|
|
95
|
+
clearTimeout(timeoutId);
|
|
96
|
+
return {
|
|
97
|
+
name: "dataverse_connectivity",
|
|
98
|
+
passed: true,
|
|
99
|
+
message: `Connected successfully (Org: ${result.OrganizationId})`,
|
|
100
|
+
durationMs: Date.now() - startTime,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
name: "dataverse_connectivity",
|
|
106
|
+
passed: false,
|
|
107
|
+
message: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
durationMs: Date.now() - startTime,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check a custom health endpoint
|
|
114
|
+
*/
|
|
115
|
+
async checkCustomEndpoint(endpoint, expectedStatus, timeout, retries) {
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
let lastError = "";
|
|
118
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
119
|
+
try {
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
122
|
+
const response = await fetch(endpoint, {
|
|
123
|
+
method: "GET",
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
if (response.status === expectedStatus) {
|
|
128
|
+
return {
|
|
129
|
+
name: "custom_endpoint",
|
|
130
|
+
passed: true,
|
|
131
|
+
message: `Endpoint returned expected status ${expectedStatus}`,
|
|
132
|
+
durationMs: Date.now() - startTime,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
lastError = `Unexpected status: ${response.status} (expected ${expectedStatus})`;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
139
|
+
}
|
|
140
|
+
// Wait before retry
|
|
141
|
+
if (attempt < retries - 1) {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
name: "custom_endpoint",
|
|
147
|
+
passed: false,
|
|
148
|
+
message: `Failed after ${retries} attempts: ${lastError}`,
|
|
149
|
+
durationMs: Date.now() - startTime,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Check if solution import is possible (verify permissions)
|
|
154
|
+
*/
|
|
155
|
+
async checkSolutionImportCapability(client, timeout) {
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
try {
|
|
158
|
+
// Query for existing import jobs to verify we have read access
|
|
159
|
+
// This doesn't actually import anything, just checks permissions
|
|
160
|
+
await Promise.race([
|
|
161
|
+
client.get("/importjobs", {
|
|
162
|
+
$top: "1",
|
|
163
|
+
$select: "importjobid",
|
|
164
|
+
}),
|
|
165
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
|
|
166
|
+
]);
|
|
167
|
+
return {
|
|
168
|
+
name: "solution_import_capability",
|
|
169
|
+
passed: true,
|
|
170
|
+
message: "Import job access verified",
|
|
171
|
+
durationMs: Date.now() - startTime,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
name: "solution_import_capability",
|
|
177
|
+
passed: false,
|
|
178
|
+
message: `Access check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
179
|
+
durationMs: Date.now() - startTime,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Run health checks for multiple tenants in parallel
|
|
185
|
+
*/
|
|
186
|
+
async checkMultipleTenants(tenants, clientFactory, options = {}) {
|
|
187
|
+
const { maxConcurrent = 5, onProgress } = options;
|
|
188
|
+
const results = [];
|
|
189
|
+
let completed = 0;
|
|
190
|
+
// Process in batches
|
|
191
|
+
for (let i = 0; i < tenants.length; i += maxConcurrent) {
|
|
192
|
+
const batch = tenants.slice(i, i + maxConcurrent);
|
|
193
|
+
const batchResults = await Promise.all(batch.map(async (tenant) => {
|
|
194
|
+
try {
|
|
195
|
+
const client = await clientFactory(tenant);
|
|
196
|
+
return this.checkTenantHealth(tenant, client);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return {
|
|
200
|
+
healthy: false,
|
|
201
|
+
tenantId: tenant.tenantId,
|
|
202
|
+
tenantName: tenant.name,
|
|
203
|
+
checks: [
|
|
204
|
+
{
|
|
205
|
+
name: "client_creation",
|
|
206
|
+
passed: false,
|
|
207
|
+
message: `Failed to create client: ${error instanceof Error ? error.message : String(error)}`,
|
|
208
|
+
durationMs: 0,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
totalDurationMs: 0,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
results.push(...batchResults);
|
|
216
|
+
completed += batch.length;
|
|
217
|
+
if (onProgress) {
|
|
218
|
+
onProgress(completed, tenants.length);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Generate a health report summary
|
|
225
|
+
*/
|
|
226
|
+
generateReport(results) {
|
|
227
|
+
const unhealthyDetails = results
|
|
228
|
+
.filter((r) => !r.healthy)
|
|
229
|
+
.map((r) => ({
|
|
230
|
+
tenantName: r.tenantName,
|
|
231
|
+
tenantId: r.tenantId,
|
|
232
|
+
failedChecks: r.checks.filter((c) => !c.passed).map((c) => c.message),
|
|
233
|
+
}));
|
|
234
|
+
return {
|
|
235
|
+
totalTenants: results.length,
|
|
236
|
+
healthyTenants: results.filter((r) => r.healthy).length,
|
|
237
|
+
unhealthyTenants: results.filter((r) => !r.healthy).length,
|
|
238
|
+
unhealthyDetails,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Shared singleton for basic tenant connectivity/auth health checks.
|
|
243
|
+
export const healthCheckService = new HealthCheckService();
|
|
244
|
+
export class TenantHealthMonitoringService {
|
|
245
|
+
cache = new Map();
|
|
246
|
+
CACHE_DURATION_MS = HEALTH_CHECK_CACHE_DURATION_MS;
|
|
247
|
+
/**
|
|
248
|
+
* Check health for a single tenant
|
|
249
|
+
*/
|
|
250
|
+
async checkTenantHealth(context, skipCache = false) {
|
|
251
|
+
// Check cache first
|
|
252
|
+
if (!skipCache) {
|
|
253
|
+
const cached = this.cache.get(context.tenantId);
|
|
254
|
+
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION_MS) {
|
|
255
|
+
return cached.data;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Run all health checks
|
|
259
|
+
const [gdapStatus, connectionsStatus, deploymentMetrics] = await Promise.all([
|
|
260
|
+
this.checkGDAPHealth(context),
|
|
261
|
+
this.checkConnectionsHealth(context),
|
|
262
|
+
this.analyzeDeploymentHistory(context),
|
|
263
|
+
]);
|
|
264
|
+
// Collect all issues
|
|
265
|
+
const issues = [
|
|
266
|
+
...gdapStatus.issues,
|
|
267
|
+
...connectionsStatus.issues,
|
|
268
|
+
...deploymentMetrics.issues,
|
|
269
|
+
];
|
|
270
|
+
// Calculate health score
|
|
271
|
+
const healthScore = this.calculateHealthScore(issues, deploymentMetrics.successRate);
|
|
272
|
+
// Determine overall status
|
|
273
|
+
const status = this.determineStatus(healthScore, issues);
|
|
274
|
+
const health = {
|
|
275
|
+
tenantId: context.tenantId,
|
|
276
|
+
tenantName: context.tenantName,
|
|
277
|
+
healthScore,
|
|
278
|
+
status,
|
|
279
|
+
lastChecked: new Date().toISOString(),
|
|
280
|
+
issues,
|
|
281
|
+
gdapStatus: gdapStatus.status,
|
|
282
|
+
connectionsStatus: connectionsStatus.status,
|
|
283
|
+
recentSuccessRate: deploymentMetrics.successRate,
|
|
284
|
+
recentDeployments: deploymentMetrics.counts,
|
|
285
|
+
};
|
|
286
|
+
// Cache the result
|
|
287
|
+
this.cache.set(context.tenantId, { data: health, timestamp: Date.now() });
|
|
288
|
+
return health;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Check detailed health for a single tenant
|
|
292
|
+
*/
|
|
293
|
+
async checkTenantHealthDetail(context, skipCache = false) {
|
|
294
|
+
const basicHealth = await this.checkTenantHealth(context, skipCache);
|
|
295
|
+
// Get detailed GDAP info
|
|
296
|
+
const gdapDetail = await this.getGDAPDetail(context);
|
|
297
|
+
// Get detailed connection info
|
|
298
|
+
const connectionsDetail = await this.getConnectionsDetail(context);
|
|
299
|
+
// Get recent deployment history
|
|
300
|
+
const recentHistory = this.getRecentDeploymentHistory(context);
|
|
301
|
+
// Generate recommendations
|
|
302
|
+
const recommendations = this.generateRecommendations(basicHealth, gdapDetail, connectionsDetail);
|
|
303
|
+
return {
|
|
304
|
+
...basicHealth,
|
|
305
|
+
gdap: gdapDetail,
|
|
306
|
+
connections: connectionsDetail,
|
|
307
|
+
recentDeploymentHistory: recentHistory,
|
|
308
|
+
recommendations,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Check health for multiple tenants
|
|
313
|
+
*/
|
|
314
|
+
async checkMultipleTenants(contexts, skipCache = false) {
|
|
315
|
+
return Promise.all(contexts.map((ctx) => this.checkTenantHealth(ctx, skipCache)));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Clear cache for a tenant or all tenants
|
|
319
|
+
*/
|
|
320
|
+
clearCache(tenantId) {
|
|
321
|
+
if (tenantId) {
|
|
322
|
+
this.cache.delete(tenantId);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
this.cache.clear();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// Private Health Check Methods
|
|
330
|
+
// ============================================================================
|
|
331
|
+
async checkGDAPHealth(context) {
|
|
332
|
+
const issues = [];
|
|
333
|
+
// In demo mode, use tenant metadata for deterministic GDAP scenarios
|
|
334
|
+
if (process.env.DEMO_MODE === "true") {
|
|
335
|
+
const meta = getDemoTenantMetadata(context.tenantId);
|
|
336
|
+
if (meta) {
|
|
337
|
+
switch (meta.gdapStatus) {
|
|
338
|
+
case "missing_role":
|
|
339
|
+
issues.push({
|
|
340
|
+
severity: "critical",
|
|
341
|
+
category: "permissions",
|
|
342
|
+
message: meta.gdapIssue || "Missing Power Platform Administrator role in GDAP relationship",
|
|
343
|
+
resolution: "Add the Power Platform Administrator role to this tenant's GDAP relationship in Partner Center",
|
|
344
|
+
link: "https://partner.microsoft.com/commerce/granularadminrelationships",
|
|
345
|
+
});
|
|
346
|
+
return { status: "missing_role", issues };
|
|
347
|
+
case "expired":
|
|
348
|
+
issues.push({
|
|
349
|
+
severity: "error",
|
|
350
|
+
category: "permissions",
|
|
351
|
+
message: meta.gdapIssue || "GDAP relationship expired",
|
|
352
|
+
resolution: "Renew the GDAP relationship in Partner Center",
|
|
353
|
+
link: "https://partner.microsoft.com/commerce/granularadminrelationships",
|
|
354
|
+
});
|
|
355
|
+
return { status: "expired", issues };
|
|
356
|
+
case "propagating":
|
|
357
|
+
issues.push({
|
|
358
|
+
severity: "warning",
|
|
359
|
+
category: "permissions",
|
|
360
|
+
message: meta.gdapIssue ||
|
|
361
|
+
"GDAP relationship created recently, permissions may still be propagating",
|
|
362
|
+
resolution: "Wait 24-48 hours for permissions to fully propagate",
|
|
363
|
+
});
|
|
364
|
+
return { status: "propagating", issues };
|
|
365
|
+
case "expiring_soon":
|
|
366
|
+
issues.push({
|
|
367
|
+
severity: "warning",
|
|
368
|
+
category: "permissions",
|
|
369
|
+
message: meta.gdapIssue || "GDAP relationship expiring soon",
|
|
370
|
+
resolution: "Renew GDAP relationship before it expires",
|
|
371
|
+
link: "https://partner.microsoft.com/commerce/granularadminrelationships",
|
|
372
|
+
});
|
|
373
|
+
return { status: "valid", issues };
|
|
374
|
+
case "valid":
|
|
375
|
+
default:
|
|
376
|
+
return { status: "valid", issues: [] };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { status: "valid", issues: [] };
|
|
380
|
+
}
|
|
381
|
+
// Live GDAP checks require Microsoft Graph API access, which is not available in all
|
|
382
|
+
// deployment contexts. When Graph credentials are provided, this will query:
|
|
383
|
+
// GET https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships
|
|
384
|
+
// For now, returns "unknown" — the CLI validate command performs real GDAP checks separately.
|
|
385
|
+
return { status: "unknown", issues: [] };
|
|
386
|
+
}
|
|
387
|
+
async checkConnectionsHealth(context) {
|
|
388
|
+
const issues = [];
|
|
389
|
+
// In demo mode, use tenant metadata for deterministic connection scenarios
|
|
390
|
+
if (process.env.DEMO_MODE === "true") {
|
|
391
|
+
const meta = getDemoTenantMetadata(context.tenantId);
|
|
392
|
+
if (meta) {
|
|
393
|
+
switch (meta.connectionStatus) {
|
|
394
|
+
case "expired":
|
|
395
|
+
issues.push({
|
|
396
|
+
severity: "error",
|
|
397
|
+
category: "connections",
|
|
398
|
+
message: meta.connectionIssue || "Connection reference expired: requires reauthentication",
|
|
399
|
+
resolution: "Open the solution in the maker portal and update the connection reference",
|
|
400
|
+
link: `${context.environmentUrl}/main.aspx?forceUCI=1&pagetype=apps`,
|
|
401
|
+
});
|
|
402
|
+
return { status: "expired", issues };
|
|
403
|
+
case "missing":
|
|
404
|
+
issues.push({
|
|
405
|
+
severity: "error",
|
|
406
|
+
category: "connections",
|
|
407
|
+
message: meta.connectionIssue || "Missing required connection: not configured",
|
|
408
|
+
resolution: "Configure the required connection in the maker portal before deploying",
|
|
409
|
+
link: `${context.environmentUrl}/main.aspx?forceUCI=1&pagetype=apps`,
|
|
410
|
+
});
|
|
411
|
+
return { status: "missing", issues };
|
|
412
|
+
case "expiring_certificate":
|
|
413
|
+
issues.push({
|
|
414
|
+
severity: "warning",
|
|
415
|
+
category: "connections",
|
|
416
|
+
message: meta.connectionIssue || "Connection certificate expiring soon",
|
|
417
|
+
resolution: "Rotate the OAuth certificate before it expires",
|
|
418
|
+
});
|
|
419
|
+
return { status: "valid", issues };
|
|
420
|
+
case "valid":
|
|
421
|
+
default:
|
|
422
|
+
return { status: "valid", issues: [] };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { status: "valid", issues: [] };
|
|
426
|
+
}
|
|
427
|
+
// Live connection checks require Dataverse API access per tenant. When credentials
|
|
428
|
+
// are available, this will query connectionreferences entity. For now, returns "unknown"
|
|
429
|
+
// — the CLI validate command performs real connection checks separately.
|
|
430
|
+
return { status: "unknown", issues: [] };
|
|
431
|
+
}
|
|
432
|
+
analyzeDeploymentHistory(context) {
|
|
433
|
+
const issues = [];
|
|
434
|
+
if (!context.deploymentHistory || context.deploymentHistory.length === 0) {
|
|
435
|
+
return {
|
|
436
|
+
successRate: 1.0, // Assume healthy if no history
|
|
437
|
+
counts: { total: 0, successful: 0, failed: 0 },
|
|
438
|
+
issues: [],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const recent = context.deploymentHistory.slice(0, 10);
|
|
442
|
+
const successful = recent.filter((d) => d.status === "success").length;
|
|
443
|
+
const failed = recent.filter((d) => d.status === "failure").length;
|
|
444
|
+
const successRate = successful / recent.length;
|
|
445
|
+
// Check for concerning patterns
|
|
446
|
+
if (successRate < 0.5) {
|
|
447
|
+
issues.push({
|
|
448
|
+
severity: "critical",
|
|
449
|
+
category: "history",
|
|
450
|
+
message: `Low success rate: Only ${Math.round(successRate * 100)}% of recent deployments succeeded`,
|
|
451
|
+
resolution: "Review recent deployment errors and address underlying issues before deploying again",
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
else if (successRate < 0.7) {
|
|
455
|
+
issues.push({
|
|
456
|
+
severity: "warning",
|
|
457
|
+
category: "history",
|
|
458
|
+
message: `Moderate success rate: ${Math.round(successRate * 100)}% of recent deployments succeeded`,
|
|
459
|
+
resolution: "Some recent deployments failed, consider investigating before deploying",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// Check for recent failures
|
|
463
|
+
const lastThree = recent.slice(0, 3);
|
|
464
|
+
const recentFailures = lastThree.filter((d) => d.status === "failure").length;
|
|
465
|
+
if (recentFailures >= 2) {
|
|
466
|
+
issues.push({
|
|
467
|
+
severity: "error",
|
|
468
|
+
category: "history",
|
|
469
|
+
message: `${recentFailures} of last 3 deployments failed`,
|
|
470
|
+
resolution: "Fix underlying issues before attempting another deployment",
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
successRate,
|
|
475
|
+
counts: {
|
|
476
|
+
total: recent.length,
|
|
477
|
+
successful,
|
|
478
|
+
failed,
|
|
479
|
+
},
|
|
480
|
+
issues,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
async getGDAPDetail(context) {
|
|
484
|
+
// In demo mode, return metadata-driven GDAP details
|
|
485
|
+
if (process.env.DEMO_MODE === "true") {
|
|
486
|
+
const meta = getDemoTenantMetadata(context.tenantId);
|
|
487
|
+
if (meta) {
|
|
488
|
+
const baseExpiry = meta.gdapRelationshipExpiry ||
|
|
489
|
+
new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
|
490
|
+
switch (meta.gdapStatus) {
|
|
491
|
+
case "missing_role":
|
|
492
|
+
return {
|
|
493
|
+
status: "missing_role",
|
|
494
|
+
roles: ["Dynamics 365 Administrator"],
|
|
495
|
+
missingRoles: ["Power Platform Administrator"],
|
|
496
|
+
relationshipExpiry: baseExpiry,
|
|
497
|
+
lastVerified: new Date().toISOString(),
|
|
498
|
+
issue: meta.gdapIssue || "Missing required role",
|
|
499
|
+
};
|
|
500
|
+
case "expired":
|
|
501
|
+
return {
|
|
502
|
+
status: "expired",
|
|
503
|
+
roles: [],
|
|
504
|
+
relationshipExpiry: meta.gdapRelationshipExpiry ||
|
|
505
|
+
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
506
|
+
lastVerified: new Date().toISOString(),
|
|
507
|
+
issue: meta.gdapIssue || "GDAP relationship expired",
|
|
508
|
+
};
|
|
509
|
+
case "propagating":
|
|
510
|
+
return {
|
|
511
|
+
status: "propagating",
|
|
512
|
+
roles: ["Power Platform Administrator", "Dynamics 365 Administrator"],
|
|
513
|
+
relationshipExpiry: baseExpiry,
|
|
514
|
+
lastVerified: new Date().toISOString(),
|
|
515
|
+
issue: meta.gdapIssue || "Permissions still propagating",
|
|
516
|
+
};
|
|
517
|
+
case "expiring_soon":
|
|
518
|
+
return {
|
|
519
|
+
status: "valid",
|
|
520
|
+
roles: ["Power Platform Administrator", "Dynamics 365 Administrator"],
|
|
521
|
+
relationshipExpiry: meta.gdapRelationshipExpiry ||
|
|
522
|
+
new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
523
|
+
lastVerified: new Date().toISOString(),
|
|
524
|
+
issue: meta.gdapIssue || "GDAP relationship expiring soon",
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
status: "valid",
|
|
530
|
+
roles: ["Power Platform Administrator", "Dynamics 365 Administrator"],
|
|
531
|
+
relationshipExpiry: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
|
|
532
|
+
lastVerified: new Date().toISOString(),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
// Live GDAP detail requires Microsoft Graph API access (see checkGDAPHealth above).
|
|
536
|
+
return {
|
|
537
|
+
status: "unknown",
|
|
538
|
+
lastVerified: new Date().toISOString(),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
async getConnectionsDetail(context) {
|
|
542
|
+
// In demo mode, return metadata-driven connection details
|
|
543
|
+
if (process.env.DEMO_MODE === "true") {
|
|
544
|
+
const meta = getDemoTenantMetadata(context.tenantId);
|
|
545
|
+
const connections = [];
|
|
546
|
+
if (meta) {
|
|
547
|
+
switch (meta.connectionStatus) {
|
|
548
|
+
case "expired":
|
|
549
|
+
connections.push({
|
|
550
|
+
name: "shared_commondataserviceforapps",
|
|
551
|
+
displayName: "Dataverse",
|
|
552
|
+
status: "expired",
|
|
553
|
+
expiryDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
554
|
+
issue: meta.connectionIssue || "Connection expired, needs reauthentication",
|
|
555
|
+
});
|
|
556
|
+
break;
|
|
557
|
+
case "missing":
|
|
558
|
+
connections.push({
|
|
559
|
+
name: "shared_commondataserviceforapps",
|
|
560
|
+
displayName: "Dataverse",
|
|
561
|
+
status: "valid",
|
|
562
|
+
expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
563
|
+
});
|
|
564
|
+
connections.push({
|
|
565
|
+
name: "shared_sharepointonline",
|
|
566
|
+
displayName: "SharePoint",
|
|
567
|
+
status: "missing",
|
|
568
|
+
issue: meta.connectionIssue || "Connection never configured",
|
|
569
|
+
});
|
|
570
|
+
break;
|
|
571
|
+
case "expiring_certificate":
|
|
572
|
+
connections.push({
|
|
573
|
+
name: "shared_commondataserviceforapps",
|
|
574
|
+
displayName: "Dataverse",
|
|
575
|
+
status: "valid",
|
|
576
|
+
expiryDate: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
577
|
+
issue: meta.connectionIssue || "OAuth certificate expiring in 15 days",
|
|
578
|
+
});
|
|
579
|
+
break;
|
|
580
|
+
default:
|
|
581
|
+
connections.push({
|
|
582
|
+
name: "shared_commondataserviceforapps",
|
|
583
|
+
displayName: "Dataverse",
|
|
584
|
+
status: "valid",
|
|
585
|
+
expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
586
|
+
});
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
// Add Office 365 connection for tenants that have valid/expiring connections
|
|
590
|
+
if (meta.connectionStatus !== "expired") {
|
|
591
|
+
connections.push({
|
|
592
|
+
name: "shared_office365",
|
|
593
|
+
displayName: "Office 365 Outlook",
|
|
594
|
+
status: "valid",
|
|
595
|
+
expiryDate: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString(),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
// Unknown tenant, return sensible defaults
|
|
601
|
+
connections.push({
|
|
602
|
+
name: "shared_commondataserviceforapps",
|
|
603
|
+
displayName: "Dataverse",
|
|
604
|
+
status: "valid",
|
|
605
|
+
expiryDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
606
|
+
});
|
|
607
|
+
connections.push({
|
|
608
|
+
name: "shared_office365",
|
|
609
|
+
displayName: "Office 365 Outlook",
|
|
610
|
+
status: "valid",
|
|
611
|
+
expiryDate: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString(),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return connections;
|
|
615
|
+
}
|
|
616
|
+
// Live connection detail requires Dataverse API access per tenant (see checkConnectionsHealth above).
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
getRecentDeploymentHistory(context) {
|
|
620
|
+
if (!context.deploymentHistory)
|
|
621
|
+
return [];
|
|
622
|
+
return context.deploymentHistory.slice(0, 20).map((d, idx) => ({
|
|
623
|
+
id: `deployment-${idx}`,
|
|
624
|
+
timestamp: d.completedAt,
|
|
625
|
+
status: d.status,
|
|
626
|
+
duration: d.durationMinutes,
|
|
627
|
+
error: d.error,
|
|
628
|
+
}));
|
|
629
|
+
}
|
|
630
|
+
generateRecommendations(health, gdap, connections) {
|
|
631
|
+
const recommendations = [];
|
|
632
|
+
// GDAP recommendations
|
|
633
|
+
if (gdap.status === "missing_role") {
|
|
634
|
+
recommendations.push("Add missing GDAP roles in Partner Center before deploying");
|
|
635
|
+
}
|
|
636
|
+
else if (gdap.status === "expired") {
|
|
637
|
+
recommendations.push("Renew GDAP relationship in Partner Center");
|
|
638
|
+
}
|
|
639
|
+
else if (gdap.status === "propagating") {
|
|
640
|
+
recommendations.push("Wait 24-48 hours for GDAP permissions to fully propagate");
|
|
641
|
+
}
|
|
642
|
+
// Connection recommendations
|
|
643
|
+
const expiredConnections = connections.filter((c) => c.status === "expired");
|
|
644
|
+
if (expiredConnections.length > 0) {
|
|
645
|
+
recommendations.push(`Reauthenticate ${expiredConnections.length} expired connection${expiredConnections.length > 1 ? "s" : ""} in maker portal`);
|
|
646
|
+
}
|
|
647
|
+
// History-based recommendations
|
|
648
|
+
if (health.recentSuccessRate < 0.7) {
|
|
649
|
+
recommendations.push("Review and fix recent deployment failures before deploying again");
|
|
650
|
+
}
|
|
651
|
+
// General health recommendations
|
|
652
|
+
if (health.healthScore < 50) {
|
|
653
|
+
recommendations.push("Address critical issues before attempting deployments to this tenant");
|
|
654
|
+
}
|
|
655
|
+
else if (health.healthScore < 70) {
|
|
656
|
+
recommendations.push("Consider fixing warnings to improve deployment success rate");
|
|
657
|
+
}
|
|
658
|
+
return recommendations;
|
|
659
|
+
}
|
|
660
|
+
// ============================================================================
|
|
661
|
+
// Scoring
|
|
662
|
+
// ============================================================================
|
|
663
|
+
calculateHealthScore(issues, successRate) {
|
|
664
|
+
// Start with perfect score
|
|
665
|
+
let score = 100;
|
|
666
|
+
// Deduct points for issues
|
|
667
|
+
for (const issue of issues) {
|
|
668
|
+
switch (issue.severity) {
|
|
669
|
+
case "critical":
|
|
670
|
+
score -= 40;
|
|
671
|
+
break;
|
|
672
|
+
case "error":
|
|
673
|
+
score -= 25;
|
|
674
|
+
break;
|
|
675
|
+
case "warning":
|
|
676
|
+
score -= 10;
|
|
677
|
+
break;
|
|
678
|
+
case "info":
|
|
679
|
+
score -= 2;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// Factor in deployment success rate (30% weight)
|
|
684
|
+
const historyScore = successRate * 30;
|
|
685
|
+
score = score * 0.7 + historyScore;
|
|
686
|
+
// Clamp to 0-100
|
|
687
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
688
|
+
}
|
|
689
|
+
determineStatus(score, issues) {
|
|
690
|
+
// Critical if any critical issues or score very low
|
|
691
|
+
if (issues.some((i) => i.severity === "critical") || score < 40) {
|
|
692
|
+
return "critical";
|
|
693
|
+
}
|
|
694
|
+
// Warning if any errors/warnings or medium score
|
|
695
|
+
if (issues.some((i) => i.severity === "error" || i.severity === "warning") || score < 70) {
|
|
696
|
+
return "warning";
|
|
697
|
+
}
|
|
698
|
+
return "healthy";
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Shared singleton for extended monitoring, recommendations, and scoring.
|
|
702
|
+
export const tenantHealthMonitoringService = new TenantHealthMonitoringService();
|
|
703
|
+
// Backwards-compatible alias for existing consumers.
|
|
704
|
+
export const healthChecker = tenantHealthMonitoringService;
|
|
705
|
+
//# sourceMappingURL=health-check.js.map
|