spec-up-t-healthcheck 1.0.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/README.md +216 -0
- package/bin/cli.js +193 -0
- package/bin/demo-html.js +186 -0
- package/bin/simple-test.js +79 -0
- package/lib/checks/external-specs-urls.js +484 -0
- package/lib/checks/gitignore.js +350 -0
- package/lib/checks/package-json.js +518 -0
- package/lib/checks/spec-files.js +263 -0
- package/lib/checks/specsjson.js +361 -0
- package/lib/file-opener.js +127 -0
- package/lib/formatters.js +176 -0
- package/lib/health-check-orchestrator.js +413 -0
- package/lib/health-check-registry.js +396 -0
- package/lib/health-check-utils.js +234 -0
- package/lib/health-checker.js +145 -0
- package/lib/html-formatter.js +626 -0
- package/lib/index.js +123 -0
- package/lib/providers.js +184 -0
- package/lib/web.js +70 -0
- package/package.json +91 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTML report generator for health check results
|
|
3
|
+
*
|
|
4
|
+
* This module generates Bootstrap-styled HTML reports for health check results,
|
|
5
|
+
* providing an interactive and visually appealing presentation of the data.
|
|
6
|
+
* The generated reports include filtering capabilities, status indicators, and
|
|
7
|
+
* responsive design elements.
|
|
8
|
+
*
|
|
9
|
+
* @author spec-up-t-healthcheck
|
|
10
|
+
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates a complete HTML report from health check results.
|
|
15
|
+
*
|
|
16
|
+
* Creates a comprehensive HTML document using Bootstrap framework for styling,
|
|
17
|
+
* including interactive features like filtering passing checks, status indicators,
|
|
18
|
+
* and responsive layout. The report follows the visual design patterns from
|
|
19
|
+
* the HealthCheck.vue reference.
|
|
20
|
+
*
|
|
21
|
+
* @param {import('./health-checker.js').HealthCheckReport} healthCheckOutput - The complete health check report
|
|
22
|
+
* @param {Object} [options={}] - Configuration options for HTML generation
|
|
23
|
+
* @param {string} [options.title='Spec-Up-T Health Check Report'] - Custom title for the report
|
|
24
|
+
* @param {boolean} [options.showPassingByDefault=true] - Whether to show passing checks by default
|
|
25
|
+
* @param {string} [options.repositoryUrl] - URL to the repository being checked
|
|
26
|
+
* @returns {string} Complete HTML document as string
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```javascript
|
|
30
|
+
* const report = await runHealthChecks(provider);
|
|
31
|
+
* const htmlContent = generateHtmlReport(report, {
|
|
32
|
+
* title: 'My Custom Health Check Report',
|
|
33
|
+
* repositoryUrl: 'https://github.com/user/repo'
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function generateHtmlReport(healthCheckOutput, options = {}) {
|
|
38
|
+
const {
|
|
39
|
+
title = 'Spec-Up-T Health Check Report',
|
|
40
|
+
showPassingByDefault = true,
|
|
41
|
+
repositoryUrl
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
const { results, summary, timestamp, provider } = healthCheckOutput;
|
|
45
|
+
|
|
46
|
+
// Generate the main content sections
|
|
47
|
+
const headerSection = generateHeaderSection(title, timestamp, provider, repositoryUrl);
|
|
48
|
+
const summarySection = generateSummarySection(summary, showPassingByDefault);
|
|
49
|
+
const resultsSection = generateResultsSection(results);
|
|
50
|
+
const scriptsSection = generateScriptsSection();
|
|
51
|
+
|
|
52
|
+
return `<!DOCTYPE html>
|
|
53
|
+
<html lang="en">
|
|
54
|
+
<head>
|
|
55
|
+
<meta charset="UTF-8">
|
|
56
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
57
|
+
<title>${escapeHtml(title)}</title>
|
|
58
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
59
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
|
60
|
+
<style>
|
|
61
|
+
body {
|
|
62
|
+
padding-top: 2rem;
|
|
63
|
+
padding-bottom: 2rem;
|
|
64
|
+
background-color: #f8f9fa;
|
|
65
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
66
|
+
}
|
|
67
|
+
.report-header {
|
|
68
|
+
margin-bottom: 2rem;
|
|
69
|
+
padding-bottom: 1.5rem;
|
|
70
|
+
border-bottom: 2px solid #dee2e6;
|
|
71
|
+
}
|
|
72
|
+
.report-header h1 {
|
|
73
|
+
color: #212529;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
margin-bottom: 0.75rem;
|
|
76
|
+
}
|
|
77
|
+
.report-header h1 i {
|
|
78
|
+
margin-right: 0.5rem;
|
|
79
|
+
}
|
|
80
|
+
.timestamp {
|
|
81
|
+
color: #6c757d;
|
|
82
|
+
font-size: 0.95rem;
|
|
83
|
+
}
|
|
84
|
+
.timestamp i {
|
|
85
|
+
margin-right: 0.25rem;
|
|
86
|
+
}
|
|
87
|
+
.repo-info {
|
|
88
|
+
color: #6c757d;
|
|
89
|
+
margin-bottom: 0.5rem;
|
|
90
|
+
font-size: 0.95rem;
|
|
91
|
+
}
|
|
92
|
+
.repo-info i {
|
|
93
|
+
margin-right: 0.25rem;
|
|
94
|
+
}
|
|
95
|
+
.filter-toggle {
|
|
96
|
+
margin-bottom: 1.5rem;
|
|
97
|
+
}
|
|
98
|
+
.hidden-item {
|
|
99
|
+
display: none !important;
|
|
100
|
+
}
|
|
101
|
+
.status-icon {
|
|
102
|
+
font-size: 1.1em;
|
|
103
|
+
margin-right: 0.25rem;
|
|
104
|
+
}
|
|
105
|
+
.health-score {
|
|
106
|
+
font-size: 2.5rem;
|
|
107
|
+
font-weight: bold;
|
|
108
|
+
line-height: 1;
|
|
109
|
+
}
|
|
110
|
+
.summary-card {
|
|
111
|
+
border: none;
|
|
112
|
+
box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.1);
|
|
113
|
+
border-radius: 0.5rem;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
}
|
|
116
|
+
.results-card {
|
|
117
|
+
border: none;
|
|
118
|
+
box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.1);
|
|
119
|
+
border-radius: 0.5rem;
|
|
120
|
+
margin-bottom: 1.5rem;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
}
|
|
123
|
+
.card-header {
|
|
124
|
+
border-bottom: 1px solid #dee2e6;
|
|
125
|
+
background-color: #fff;
|
|
126
|
+
padding: 1rem 1.25rem;
|
|
127
|
+
}
|
|
128
|
+
.card-header h5 {
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
color: #212529;
|
|
131
|
+
}
|
|
132
|
+
.card-header i {
|
|
133
|
+
margin-right: 0.5rem;
|
|
134
|
+
}
|
|
135
|
+
.card-body {
|
|
136
|
+
padding: 1.5rem;
|
|
137
|
+
}
|
|
138
|
+
.table th {
|
|
139
|
+
border-top: none;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
background-color: #f8f9fa;
|
|
142
|
+
color: #495057;
|
|
143
|
+
font-size: 0.875rem;
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
letter-spacing: 0.5px;
|
|
146
|
+
padding: 0.75rem;
|
|
147
|
+
}
|
|
148
|
+
.table td {
|
|
149
|
+
padding: 0.75rem;
|
|
150
|
+
vertical-align: middle;
|
|
151
|
+
}
|
|
152
|
+
.table-striped tbody tr:nth-of-type(odd) {
|
|
153
|
+
background-color: rgba(0, 0, 0, 0.02);
|
|
154
|
+
}
|
|
155
|
+
.table-hover tbody tr:hover {
|
|
156
|
+
background-color: rgba(0, 0, 0, 0.04);
|
|
157
|
+
}
|
|
158
|
+
.status-badge {
|
|
159
|
+
white-space: nowrap;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
font-size: 0.95rem;
|
|
162
|
+
}
|
|
163
|
+
.overall-status-pass {
|
|
164
|
+
color: #198754;
|
|
165
|
+
font-size: 1.1rem;
|
|
166
|
+
}
|
|
167
|
+
.overall-status-warning {
|
|
168
|
+
color: #fd7e14;
|
|
169
|
+
font-size: 1.1rem;
|
|
170
|
+
}
|
|
171
|
+
.overall-status-fail {
|
|
172
|
+
color: #dc3545;
|
|
173
|
+
font-size: 1.1rem;
|
|
174
|
+
}
|
|
175
|
+
.text-success {
|
|
176
|
+
color: #198754 !important;
|
|
177
|
+
}
|
|
178
|
+
.text-danger {
|
|
179
|
+
color: #dc3545 !important;
|
|
180
|
+
}
|
|
181
|
+
.text-warning {
|
|
182
|
+
color: #fd7e14 !important;
|
|
183
|
+
}
|
|
184
|
+
.text-muted {
|
|
185
|
+
color: #6c757d !important;
|
|
186
|
+
}
|
|
187
|
+
.detail-errors ul,
|
|
188
|
+
.detail-warnings ul,
|
|
189
|
+
.detail-success ul {
|
|
190
|
+
padding-left: 1.25rem;
|
|
191
|
+
margin-top: 0.5rem;
|
|
192
|
+
}
|
|
193
|
+
.detail-errors li,
|
|
194
|
+
.detail-warnings li,
|
|
195
|
+
.detail-success li {
|
|
196
|
+
margin-bottom: 0.25rem;
|
|
197
|
+
}
|
|
198
|
+
/* Responsive adjustments */
|
|
199
|
+
@media (max-width: 768px) {
|
|
200
|
+
.health-score {
|
|
201
|
+
font-size: 2rem;
|
|
202
|
+
}
|
|
203
|
+
.card-body {
|
|
204
|
+
padding: 1rem;
|
|
205
|
+
}
|
|
206
|
+
.table td,
|
|
207
|
+
.table th {
|
|
208
|
+
padding: 0.5rem;
|
|
209
|
+
font-size: 0.875rem;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
213
|
+
</head>
|
|
214
|
+
<body>
|
|
215
|
+
<div class="container">
|
|
216
|
+
${headerSection}
|
|
217
|
+
${summarySection}
|
|
218
|
+
${resultsSection}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
222
|
+
${scriptsSection}
|
|
223
|
+
</body>
|
|
224
|
+
</html>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Generates the header section of the HTML report.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} title - Report title
|
|
231
|
+
* @param {string} timestamp - Report generation timestamp
|
|
232
|
+
* @param {Object} provider - Provider information
|
|
233
|
+
* @param {string} [repositoryUrl] - Optional repository URL
|
|
234
|
+
* @returns {string} HTML string for the header section
|
|
235
|
+
*/
|
|
236
|
+
function generateHeaderSection(title, timestamp, provider, repositoryUrl) {
|
|
237
|
+
const formattedTimestamp = new Date(timestamp).toLocaleString();
|
|
238
|
+
|
|
239
|
+
let repoInfo = '';
|
|
240
|
+
if (provider.repoPath) {
|
|
241
|
+
repoInfo = `<p class="repo-info">
|
|
242
|
+
<i class="bi bi-folder"></i>
|
|
243
|
+
Repository: ${escapeHtml(provider.repoPath)}
|
|
244
|
+
</p>`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (repositoryUrl) {
|
|
248
|
+
repoInfo += `<p class="repo-info">
|
|
249
|
+
<i class="bi bi-link-45deg"></i>
|
|
250
|
+
URL: <a href="${escapeHtml(repositoryUrl)}" target="_blank">${escapeHtml(repositoryUrl)}</a>
|
|
251
|
+
</p>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return `<div class="report-header">
|
|
255
|
+
<h1>
|
|
256
|
+
<i class="bi bi-heart-pulse text-primary"></i>
|
|
257
|
+
${escapeHtml(title)}
|
|
258
|
+
</h1>
|
|
259
|
+
<p class="timestamp">
|
|
260
|
+
<i class="bi bi-clock"></i>
|
|
261
|
+
Generated: ${escapeHtml(formattedTimestamp)}
|
|
262
|
+
</p>
|
|
263
|
+
${repoInfo}
|
|
264
|
+
</div>`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generates the summary section with overall statistics and status.
|
|
269
|
+
*
|
|
270
|
+
* @param {import('./health-checker.js').HealthCheckSummary} summary - Health check summary
|
|
271
|
+
* @param {boolean} showPassingByDefault - Whether to show passing checks by default
|
|
272
|
+
* @returns {string} HTML string for the summary section
|
|
273
|
+
*/
|
|
274
|
+
function generateSummarySection(summary, showPassingByDefault) {
|
|
275
|
+
const overallStatusClass = summary.hasErrors ? 'overall-status-fail' :
|
|
276
|
+
summary.hasWarnings ? 'overall-status-warning' : 'overall-status-pass';
|
|
277
|
+
|
|
278
|
+
const overallStatusIcon = summary.hasErrors ? 'bi-x-circle-fill' :
|
|
279
|
+
summary.hasWarnings ? 'bi-exclamation-triangle-fill' : 'bi-check-circle-fill';
|
|
280
|
+
|
|
281
|
+
const overallStatusText = summary.hasErrors ? 'FAILED' :
|
|
282
|
+
summary.hasWarnings ? 'PASSED WITH WARNINGS' : 'PASSED';
|
|
283
|
+
|
|
284
|
+
const scoreColor = summary.score >= 80 ? 'text-success' :
|
|
285
|
+
summary.score >= 60 ? 'text-warning' : 'text-danger';
|
|
286
|
+
|
|
287
|
+
const headerClass = summary.hasErrors ? 'bg-danger text-white' :
|
|
288
|
+
summary.hasWarnings ? 'bg-warning text-dark' : 'bg-light';
|
|
289
|
+
|
|
290
|
+
return `<div class="card summary-card mb-4">
|
|
291
|
+
<div class="card-header ${headerClass}">
|
|
292
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
293
|
+
<h5 class="mb-0">
|
|
294
|
+
<i class="bi bi-bar-chart"></i>
|
|
295
|
+
Health Check Summary
|
|
296
|
+
</h5>
|
|
297
|
+
<div class="filter-toggle form-check form-switch">
|
|
298
|
+
<input class="form-check-input" type="checkbox" id="togglePassingChecks" ${showPassingByDefault ? 'checked' : ''}>
|
|
299
|
+
<label class="form-check-label" for="togglePassingChecks">
|
|
300
|
+
Show passing checks
|
|
301
|
+
</label>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="card-body">
|
|
306
|
+
<div class="row">
|
|
307
|
+
<div class="col-md-8">
|
|
308
|
+
<div class="row">
|
|
309
|
+
<div class="col-6 col-md-3 text-center mb-3">
|
|
310
|
+
<div class="h4 mb-1">${summary.total}</div>
|
|
311
|
+
<small class="text-muted">Total</small>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="col-6 col-md-3 text-center mb-3">
|
|
314
|
+
<div class="h4 mb-1 text-success">
|
|
315
|
+
<i class="bi bi-check-circle-fill"></i> ${summary.passed}
|
|
316
|
+
</div>
|
|
317
|
+
<small class="text-muted">Passed</small>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="col-6 col-md-3 text-center mb-3">
|
|
320
|
+
<div class="h4 mb-1 text-danger">
|
|
321
|
+
<i class="bi bi-x-circle-fill"></i> ${summary.failed}
|
|
322
|
+
</div>
|
|
323
|
+
<small class="text-muted">Failed</small>
|
|
324
|
+
</div>
|
|
325
|
+
${summary.warnings > 0 ? `<div class="col-6 col-md-3 text-center mb-3">
|
|
326
|
+
<div class="h4 mb-1 text-warning">
|
|
327
|
+
<i class="bi bi-exclamation-triangle-fill"></i> ${summary.warnings}
|
|
328
|
+
</div>
|
|
329
|
+
<small class="text-muted">Warnings</small>
|
|
330
|
+
</div>` : ''}
|
|
331
|
+
</div>
|
|
332
|
+
<hr class="my-3">
|
|
333
|
+
<div class="${overallStatusClass}">
|
|
334
|
+
<i class="bi ${overallStatusIcon} status-icon"></i>
|
|
335
|
+
<strong>Overall Status: ${overallStatusText}</strong>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="col-md-4 text-center d-flex flex-column justify-content-center align-items-center">
|
|
339
|
+
<div class="health-score ${scoreColor}">${Math.round(summary.score)}%</div>
|
|
340
|
+
<small class="text-muted mt-2">Health Score</small>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generates the results section with individual check details.
|
|
349
|
+
*
|
|
350
|
+
* @param {import('./health-checker.js').HealthCheckResult[]} results - Array of check results
|
|
351
|
+
* @returns {string} HTML string for the results section
|
|
352
|
+
*/
|
|
353
|
+
function generateResultsSection(results) {
|
|
354
|
+
if (results.length === 0) {
|
|
355
|
+
return `<div class="card results-card">
|
|
356
|
+
<div class="card-body text-center py-5">
|
|
357
|
+
<i class="bi bi-heart-pulse" style="font-size: 3rem; color: #6c757d;"></i>
|
|
358
|
+
<h5 class="mt-3">No Health Check Results</h5>
|
|
359
|
+
<p class="text-muted">No health checks have been performed yet.</p>
|
|
360
|
+
</div>
|
|
361
|
+
</div>`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const resultsHtml = results.map((result, index) => {
|
|
365
|
+
const { statusClass, statusIcon, statusText } = getStatusDisplay(result);
|
|
366
|
+
const rowClass = getRowClass(result);
|
|
367
|
+
|
|
368
|
+
let detailsHtml = '';
|
|
369
|
+
if (result.details && Object.keys(result.details).length > 0) {
|
|
370
|
+
detailsHtml = formatResultDetails(result.details);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return `<tr data-status="${result.status}" class="check-row ${rowClass}">
|
|
374
|
+
<td class="${statusClass} status-badge">
|
|
375
|
+
<i class="bi ${statusIcon} status-icon"></i>
|
|
376
|
+
<span>${statusText}</span>
|
|
377
|
+
</td>
|
|
378
|
+
<td>${escapeHtml(result.check)}</td>
|
|
379
|
+
<td>
|
|
380
|
+
${escapeHtml(result.message)}
|
|
381
|
+
${detailsHtml}
|
|
382
|
+
</td>
|
|
383
|
+
</tr>`;
|
|
384
|
+
}).join('');
|
|
385
|
+
|
|
386
|
+
return `<div class="card results-card" data-section="health-check-results">
|
|
387
|
+
<div class="card-header">
|
|
388
|
+
<h5 class="mb-0">
|
|
389
|
+
<i class="bi bi-list-check"></i>
|
|
390
|
+
Detailed Results
|
|
391
|
+
</h5>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="card-body">
|
|
394
|
+
<table class="table table-striped table-hover mb-0">
|
|
395
|
+
<thead>
|
|
396
|
+
<tr>
|
|
397
|
+
<th style="width: 120px;">Status</th>
|
|
398
|
+
<th style="width: 200px;">Check</th>
|
|
399
|
+
<th>Details</th>
|
|
400
|
+
</tr>
|
|
401
|
+
</thead>
|
|
402
|
+
<tbody>
|
|
403
|
+
${resultsHtml}
|
|
404
|
+
</tbody>
|
|
405
|
+
</table>
|
|
406
|
+
</div>
|
|
407
|
+
</div>`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Generates the JavaScript section for interactive functionality.
|
|
412
|
+
*
|
|
413
|
+
* @returns {string} HTML script section
|
|
414
|
+
*/
|
|
415
|
+
function generateScriptsSection() {
|
|
416
|
+
return `<script>
|
|
417
|
+
// Toggle function for passing checks
|
|
418
|
+
document.getElementById('togglePassingChecks').addEventListener('change', function() {
|
|
419
|
+
const showPassing = this.checked;
|
|
420
|
+
// Select only fully passing checks (status="pass")
|
|
421
|
+
// Warnings (status="warn") should always remain visible as they indicate potential issues
|
|
422
|
+
const passingRows = document.querySelectorAll('tr[data-status="pass"]');
|
|
423
|
+
|
|
424
|
+
// Hide/show passing rows
|
|
425
|
+
passingRows.forEach(row => {
|
|
426
|
+
if (showPassing) {
|
|
427
|
+
row.classList.remove('hidden-item');
|
|
428
|
+
} else {
|
|
429
|
+
row.classList.add('hidden-item');
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Also hide/show success details within all checks
|
|
434
|
+
// When focusing on issues, we don't need to see successful validations
|
|
435
|
+
const successDetails = document.querySelectorAll('.detail-success');
|
|
436
|
+
successDetails.forEach(detail => {
|
|
437
|
+
if (showPassing) {
|
|
438
|
+
detail.classList.remove('hidden-item');
|
|
439
|
+
} else {
|
|
440
|
+
detail.classList.add('hidden-item');
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Check each results card to see if it should be hidden
|
|
445
|
+
document.querySelectorAll('.results-card').forEach(card => {
|
|
446
|
+
const visibleRows = card.querySelectorAll('tr.check-row:not(.hidden-item)');
|
|
447
|
+
|
|
448
|
+
if (visibleRows.length === 0) {
|
|
449
|
+
// If no visible rows, hide the entire card
|
|
450
|
+
card.classList.add('hidden-item');
|
|
451
|
+
} else {
|
|
452
|
+
// Otherwise show it
|
|
453
|
+
card.classList.remove('hidden-item');
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Initialize the filter state
|
|
459
|
+
document.getElementById('togglePassingChecks').dispatchEvent(new Event('change'));
|
|
460
|
+
</script>`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Gets the appropriate display properties for a health check result status.
|
|
465
|
+
*
|
|
466
|
+
* @param {import('./health-checker.js').HealthCheckResult} result - The health check result
|
|
467
|
+
* @returns {Object} Object containing statusClass, statusIcon, and statusText
|
|
468
|
+
*/
|
|
469
|
+
function getStatusDisplay(result) {
|
|
470
|
+
switch (result.status) {
|
|
471
|
+
case 'pass':
|
|
472
|
+
return {
|
|
473
|
+
statusClass: 'text-success',
|
|
474
|
+
statusIcon: 'bi-check-circle-fill',
|
|
475
|
+
statusText: 'Pass'
|
|
476
|
+
};
|
|
477
|
+
case 'fail':
|
|
478
|
+
return {
|
|
479
|
+
statusClass: 'text-danger',
|
|
480
|
+
statusIcon: 'bi-x-circle-fill',
|
|
481
|
+
statusText: 'Fail'
|
|
482
|
+
};
|
|
483
|
+
case 'warn':
|
|
484
|
+
return {
|
|
485
|
+
statusClass: 'text-warning',
|
|
486
|
+
statusIcon: 'bi-exclamation-triangle-fill',
|
|
487
|
+
statusText: 'Warning'
|
|
488
|
+
};
|
|
489
|
+
case 'skip':
|
|
490
|
+
return {
|
|
491
|
+
statusClass: 'text-muted',
|
|
492
|
+
statusIcon: 'bi-dash-circle',
|
|
493
|
+
statusText: 'Skipped'
|
|
494
|
+
};
|
|
495
|
+
default:
|
|
496
|
+
return {
|
|
497
|
+
statusClass: 'text-secondary',
|
|
498
|
+
statusIcon: 'bi-question-circle',
|
|
499
|
+
statusText: 'Unknown'
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Gets the appropriate CSS class for a table row based on the result status.
|
|
506
|
+
*
|
|
507
|
+
* @param {import('./health-checker.js').HealthCheckResult} result - The health check result
|
|
508
|
+
* @returns {string} CSS class name
|
|
509
|
+
*/
|
|
510
|
+
function getRowClass(result) {
|
|
511
|
+
switch (result.status) {
|
|
512
|
+
case 'fail':
|
|
513
|
+
return 'table-danger';
|
|
514
|
+
case 'warn':
|
|
515
|
+
return 'table-warning';
|
|
516
|
+
case 'pass':
|
|
517
|
+
return 'table-success';
|
|
518
|
+
default:
|
|
519
|
+
return '';
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Formats result details into HTML.
|
|
525
|
+
*
|
|
526
|
+
* @param {Object} details - The details object from a health check result
|
|
527
|
+
* @returns {string} Formatted HTML string
|
|
528
|
+
*/
|
|
529
|
+
function formatResultDetails(details) {
|
|
530
|
+
let html = '';
|
|
531
|
+
|
|
532
|
+
// Display errors array with clickable URLs
|
|
533
|
+
if (details.errors && details.errors.length > 0) {
|
|
534
|
+
html += `<div class="mt-2 detail-errors"><strong class="text-danger">Errors:</strong><ul class="mb-0 mt-1">`;
|
|
535
|
+
details.errors.forEach(error => {
|
|
536
|
+
html += `<li class="text-danger">${linkifyUrls(error)}</li>`;
|
|
537
|
+
});
|
|
538
|
+
html += `</ul></div>`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Display warnings array with clickable URLs
|
|
542
|
+
if (details.warnings && details.warnings.length > 0) {
|
|
543
|
+
html += `<div class="mt-2 detail-warnings"><strong class="text-warning">Warnings:</strong><ul class="mb-0 mt-1">`;
|
|
544
|
+
details.warnings.forEach(warning => {
|
|
545
|
+
html += `<li class="text-warning">${linkifyUrls(warning)}</li>`;
|
|
546
|
+
});
|
|
547
|
+
html += `</ul></div>`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Display success messages array with clickable URLs
|
|
551
|
+
// Add detail-success class so these can be hidden when "Show passing checks" is disabled
|
|
552
|
+
if (details.success && details.success.length > 0) {
|
|
553
|
+
html += `<div class="mt-2 detail-success"><strong class="text-success">Success:</strong><ul class="mb-0 mt-1">`;
|
|
554
|
+
details.success.forEach(success => {
|
|
555
|
+
html += `<li class="text-success">${linkifyUrls(success)}</li>`;
|
|
556
|
+
});
|
|
557
|
+
html += `</ul></div>`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Display missing fields (existing functionality)
|
|
561
|
+
if (details.missingFields && details.missingFields.length > 0) {
|
|
562
|
+
html += `<br><small class="text-muted">Missing fields: ${details.missingFields.map(escapeHtml).join(', ')}</small>`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Display count (existing functionality)
|
|
566
|
+
if (details.count !== undefined) {
|
|
567
|
+
html += `<br><small class="text-muted">Files found: ${details.count}</small>`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Display package data (existing functionality)
|
|
571
|
+
if (details.packageData) {
|
|
572
|
+
html += `<br><small class="text-muted">Package: ${escapeHtml(details.packageData.name)}@${escapeHtml(details.packageData.version)}</small>`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return html;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Escapes HTML special characters to prevent XSS attacks.
|
|
580
|
+
*
|
|
581
|
+
* @param {string} text - Text to escape
|
|
582
|
+
* @returns {string} HTML-escaped text
|
|
583
|
+
*/
|
|
584
|
+
function escapeHtml(text) {
|
|
585
|
+
if (typeof text !== 'string') {
|
|
586
|
+
return String(text);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const map = {
|
|
590
|
+
'&': '&',
|
|
591
|
+
'<': '<',
|
|
592
|
+
'>': '>',
|
|
593
|
+
'"': '"',
|
|
594
|
+
"'": '''
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Converts URLs in text to clickable links that open in a new tab.
|
|
602
|
+
* The text is first escaped for HTML safety, then URLs are converted to links.
|
|
603
|
+
*
|
|
604
|
+
* @param {string} text - Text potentially containing URLs
|
|
605
|
+
* @returns {string} HTML string with clickable links
|
|
606
|
+
*/
|
|
607
|
+
function linkifyUrls(text) {
|
|
608
|
+
if (typeof text !== 'string') {
|
|
609
|
+
return escapeHtml(String(text));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// First escape the text for HTML safety
|
|
613
|
+
const escaped = escapeHtml(text);
|
|
614
|
+
|
|
615
|
+
// URL regex pattern that matches http://, https://, and www. URLs
|
|
616
|
+
const urlPattern = /(https?:\/\/[^\s<>"]+|www\.[^\s<>"]+)/g;
|
|
617
|
+
|
|
618
|
+
// Replace URLs with clickable links
|
|
619
|
+
return escaped.replace(urlPattern, (url) => {
|
|
620
|
+
// Ensure the URL has a protocol for the href attribute
|
|
621
|
+
const href = url.startsWith('http') ? url : `https://${url}`;
|
|
622
|
+
|
|
623
|
+
// Create an anchor tag that opens in a new tab with security attributes
|
|
624
|
+
return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="text-decoration-underline">${url}</a>`;
|
|
625
|
+
});
|
|
626
|
+
}
|