@pixelated-tech/components 3.3.6 → 3.4.1
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.COMPONENTS.md +126 -0
- package/README.md +14 -7
- package/dist/components/admin/componentusage/componentAnalysis.js +144 -0
- package/dist/components/admin/componentusage/componentDiscovery.js +85 -0
- package/dist/components/admin/deploy/deployment.integration.js +163 -0
- package/dist/components/admin/site-health/google-api-auth.js +69 -0
- package/dist/components/admin/site-health/seo-constants.js +14 -0
- package/dist/components/admin/site-health/seo-metrics.config.json +265 -0
- package/dist/components/admin/site-health/site-health-accessibility.js +158 -0
- package/dist/components/admin/site-health/site-health-axe-core.integration.js +119 -0
- package/dist/components/admin/site-health/site-health-axe-core.js +53 -0
- package/dist/components/admin/site-health/site-health-cache.js +23 -0
- package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +208 -0
- package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +38 -0
- package/dist/components/admin/site-health/site-health-github.integration.js +81 -0
- package/dist/components/admin/site-health/site-health-github.js +34 -0
- package/dist/components/admin/site-health/site-health-google-analytics.integration.js +112 -0
- package/dist/components/admin/site-health/site-health-google-analytics.js +43 -0
- package/dist/components/admin/site-health/site-health-google-search-console.integration.js +118 -0
- package/dist/components/admin/site-health/site-health-google-search-console.js +43 -0
- package/dist/components/admin/site-health/site-health-indicators.js +71 -0
- package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +669 -0
- package/dist/components/admin/site-health/site-health-on-site-seo.js +204 -0
- package/dist/components/admin/site-health/site-health-overview.js +65 -0
- package/dist/components/admin/site-health/site-health-performance.js +191 -0
- package/dist/components/admin/site-health/site-health-security.integration.js +109 -0
- package/dist/components/admin/site-health/site-health-security.js +169 -0
- package/dist/components/admin/site-health/site-health-seo.js +124 -0
- package/dist/components/admin/site-health/site-health-template.js +62 -0
- package/dist/components/admin/site-health/site-health-types.js +1 -0
- package/dist/components/admin/site-health/site-health-uptime.integration.js +29 -0
- package/dist/components/admin/site-health/site-health-uptime.js +30 -0
- package/dist/components/admin/site-health/site-health.css +427 -0
- package/dist/components/admin/sites/sites.integration.js +117 -0
- package/dist/components/cms/contentful.management.js +104 -0
- package/dist/components/cms/pixelated.linkedin1.js +0 -19
- package/dist/components/shoppingcart/shipping.from.json +101 -0
- package/dist/components/shoppingcart/shipping.parcel.json +112 -0
- package/dist/components/shoppingcart/shipping.to.json +422 -0
- package/dist/components/shoppingcart/shoppingCartDiscountCodes.json +26 -0
- package/dist/components/shoppingcart/shoppingcart.components.js +3 -2
- package/dist/components/sitebuilder/config/ConfigBuilder.js +36 -140
- package/dist/components/sitebuilder/config/siteinfo-form.json +200 -0
- package/dist/components/sitebuilder/config/visualdesignform.json +244 -0
- package/dist/components/structured/buzzwordbingo.js +3 -2
- package/dist/components/utilities/functions.js +5 -2
- package/dist/data/404-data.json +128 -102
- package/dist/data/flickr.json +25 -0
- package/dist/data/form.json +368 -368
- package/dist/data/recipes.json +3251 -3251
- package/dist/data/references.json +138 -137
- package/dist/data/requestform.json +111 -0
- package/dist/data/requests.json +136 -135
- package/dist/data/resume.json +2573 -2575
- package/dist/data/routes.json +238 -238
- package/dist/data/routes2.json +141 -140
- package/dist/index.js +17 -3
- package/dist/index.server.js +37 -15
- package/dist/types/components/admin/componentusage/componentAnalysis.d.ts +35 -0
- package/dist/types/components/admin/componentusage/componentAnalysis.d.ts.map +1 -0
- package/dist/types/components/admin/componentusage/componentDiscovery.d.ts +10 -0
- package/dist/types/components/admin/componentusage/componentDiscovery.d.ts.map +1 -0
- package/dist/types/components/admin/deploy/deployment.integration.d.ts +26 -0
- package/dist/types/components/admin/deploy/deployment.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/google-api-auth.d.ts +37 -0
- package/dist/types/components/admin/site-health/google-api-auth.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/seo-constants.d.ts +8 -0
- package/dist/types/components/admin/site-health/seo-constants.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-accessibility.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts +63 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-cache.d.ts +12 -0
- package/dist/types/components/admin/site-health/site-health-cache.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts +3 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-github.d.ts +8 -0
- package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-github.integration.d.ts +26 -0
- package/dist/types/components/admin/site-health/site-health-github.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts +8 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts +26 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts +8 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts +46 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-indicators.d.ts +73 -0
- package/dist/types/components/admin/site-health/site-health-indicators.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts +4 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts +34 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-overview.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-performance.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-security.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-security.integration.d.ts +29 -0
- package/dist/types/components/admin/site-health/site-health-security.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-seo.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-template.d.ts +12 -0
- package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-types.d.ts +186 -0
- package/dist/types/components/admin/site-health/site-health-types.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-uptime.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts +10 -0
- package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts.map +1 -0
- package/dist/types/components/admin/sites/sites.integration.d.ts +40 -0
- package/dist/types/components/admin/sites/sites.integration.d.ts.map +1 -0
- package/dist/types/components/cms/contentful.management.d.ts +41 -0
- package/dist/types/components/cms/contentful.management.d.ts.map +1 -1
- package/dist/types/components/cms/pixelated.linkedin1.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +4 -4
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
- package/dist/types/components/structured/buzzwordbingo.d.ts +1 -1
- package/dist/types/components/structured/buzzwordbingo.d.ts.map +1 -1
- package/dist/types/components/structured/buzzwordbingo.words.d.ts +2 -0
- package/dist/types/components/structured/buzzwordbingo.words.d.ts.map +1 -0
- package/dist/types/components/utilities/functions.d.ts.map +1 -1
- package/dist/types/index.d.ts +17 -3
- package/dist/types/index.server.d.ts +37 -13
- package/dist/types/stories/admin/preview.d.ts +12 -0
- package/dist/types/stories/admin/preview.d.ts.map +1 -0
- package/dist/types/stories/admin/site-health.stories.d.ts +65 -0
- package/dist/types/stories/admin/site-health.stories.d.ts.map +1 -0
- package/dist/types/stories/carousel/tiles.stories.d.ts.map +1 -1
- package/dist/types/stories/structured/buzzword-bingo.stories.d.ts +1 -1
- package/dist/types/stories/structured/buzzword-bingo.stories.d.ts.map +1 -1
- package/dist/types/tests/site-health-axe-core.test.d.ts +2 -0
- package/dist/types/tests/site-health-axe-core.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-cache.test.d.ts +2 -0
- package/dist/types/tests/site-health-cache.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-indicators.test.d.ts +2 -0
- package/dist/types/tests/site-health-indicators.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-overview.test.d.ts +2 -0
- package/dist/types/tests/site-health-overview.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-template.test.d.ts +2 -0
- package/dist/types/tests/site-health-template.test.d.ts.map +1 -0
- package/dist/types/tests/sites.integration.test.d.ts +2 -0
- package/dist/types/tests/sites.integration.test.d.ts.map +1 -0
- package/package.json +14 -8
- package/dist/data/shipping.to.json +0 -422
- package/dist/data/siteinfo-form.json +0 -200
- package/dist/data/visualdesignform.json +0 -244
- package/dist/types/data/buzzwords.d.ts +0 -2
- package/dist/types/data/buzzwords.d.ts.map +0 -1
- /package/dist/{data/buzzwords.js → components/structured/buzzwordbingo.words.js} +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { SiteHealthTemplate } from './site-health-template';
|
|
4
|
+
import { getScoreIndicator } from './site-health-indicators';
|
|
5
|
+
/**
|
|
6
|
+
* Restructure audits by type with all pages and their individual results
|
|
7
|
+
*/
|
|
8
|
+
function restructureAuditsByType(pagesAnalyzed) {
|
|
9
|
+
const auditMap = new Map();
|
|
10
|
+
// Collect all audit results by type
|
|
11
|
+
pagesAnalyzed.forEach(page => {
|
|
12
|
+
page.audits.forEach(audit => {
|
|
13
|
+
if (!auditMap.has(audit.id)) {
|
|
14
|
+
auditMap.set(audit.id, {
|
|
15
|
+
audit: { ...audit },
|
|
16
|
+
pageResults: []
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
auditMap.get(audit.id).pageResults.push({
|
|
20
|
+
pageUrl: page.url,
|
|
21
|
+
pageTitle: page.title,
|
|
22
|
+
score: audit.score,
|
|
23
|
+
displayValue: audit.displayValue,
|
|
24
|
+
details: audit.details
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
// Create restructured audits with all pages
|
|
29
|
+
const restructuredAudits = [];
|
|
30
|
+
auditMap.forEach(({ audit, pageResults }) => {
|
|
31
|
+
// Calculate overall score for this audit type
|
|
32
|
+
const validScores = pageResults.map(r => r.score).filter(score => score !== null);
|
|
33
|
+
const overallScore = validScores.length > 0 ? validScores.reduce((sum, score) => sum + score, 0) / validScores.length : null;
|
|
34
|
+
// Include ALL pages for this audit type (not just failed ones)
|
|
35
|
+
const allPageResults = [];
|
|
36
|
+
pageResults.forEach(result => {
|
|
37
|
+
allPageResults.push({
|
|
38
|
+
page: result.pageTitle || result.pageUrl,
|
|
39
|
+
url: result.pageUrl,
|
|
40
|
+
score: result.score,
|
|
41
|
+
displayValue: result.displayValue,
|
|
42
|
+
details: result.details
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
// Create restructured audit
|
|
46
|
+
const passCount = pageResults.filter(r => r.score === 1).length;
|
|
47
|
+
const totalCount = pageResults.length;
|
|
48
|
+
const restructuredAudit = {
|
|
49
|
+
id: audit.id,
|
|
50
|
+
title: audit.title,
|
|
51
|
+
score: overallScore,
|
|
52
|
+
scoreDisplayMode: audit.scoreDisplayMode,
|
|
53
|
+
displayValue: `${passCount}/${totalCount} pages pass`,
|
|
54
|
+
category: audit.category,
|
|
55
|
+
details: allPageResults.length > 0 ? {
|
|
56
|
+
items: allPageResults
|
|
57
|
+
} : undefined
|
|
58
|
+
};
|
|
59
|
+
restructuredAudits.push(restructuredAudit);
|
|
60
|
+
});
|
|
61
|
+
return restructuredAudits.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
62
|
+
}
|
|
63
|
+
// Fetch real SEO data from API
|
|
64
|
+
async function fetchOnSiteSEOData(siteName) {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(`/api/site-health/on-site-seo?siteName=${encodeURIComponent(siteName)}`);
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
|
69
|
+
}
|
|
70
|
+
const data = await response.json();
|
|
71
|
+
if (data.status === 'error') {
|
|
72
|
+
throw new Error(data.error || 'SEO analysis failed');
|
|
73
|
+
}
|
|
74
|
+
// Process data to aggregate audits by type across all pages
|
|
75
|
+
// const aggregatedOnPageAudits = restructureAuditsByType(data.pagesAnalyzed);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('Error fetching SEO data:', error);
|
|
80
|
+
// Return error structure that the template can display
|
|
81
|
+
return {
|
|
82
|
+
site: siteName,
|
|
83
|
+
url: '',
|
|
84
|
+
overallScore: null,
|
|
85
|
+
pagesAnalyzed: [],
|
|
86
|
+
onSiteAudits: [],
|
|
87
|
+
totalPages: 0,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
status: 'error',
|
|
90
|
+
error: error instanceof Error ? error.message : 'Failed to fetch SEO data'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function SiteHealthOnSiteSEO({ siteName }) {
|
|
95
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "On-Site SEO", fetchData: fetchOnSiteSEOData, children: (data) => {
|
|
96
|
+
if (!data)
|
|
97
|
+
return null;
|
|
98
|
+
if (data.status === 'error') {
|
|
99
|
+
return (_jsxs("p", { style: { color: '#ef4444', fontSize: '0.875rem' }, children: ["Error: ", data.error] }));
|
|
100
|
+
}
|
|
101
|
+
// Process data to aggregate audits by type across all pages
|
|
102
|
+
const aggregatedOnPageAudits = restructureAuditsByType(data.pagesAnalyzed);
|
|
103
|
+
const getScoreColor = (score) => {
|
|
104
|
+
return getScoreIndicator(score).color;
|
|
105
|
+
};
|
|
106
|
+
const getAuditScoreIcon = (score) => {
|
|
107
|
+
return getScoreIndicator(score).icon;
|
|
108
|
+
};
|
|
109
|
+
const formatPageIssue = (item) => {
|
|
110
|
+
// Handle page-specific results from restructured data
|
|
111
|
+
if (item.page && typeof item.page === 'string') {
|
|
112
|
+
const pageName = item.page;
|
|
113
|
+
const score = item.score;
|
|
114
|
+
const displayValue = item.displayValue;
|
|
115
|
+
const details = item.details;
|
|
116
|
+
let result = `${pageName}: ${Math.round(score * 100)}%`;
|
|
117
|
+
// Add display value if present
|
|
118
|
+
if (displayValue) {
|
|
119
|
+
result += ` (${displayValue})`;
|
|
120
|
+
}
|
|
121
|
+
// Add detailed breakdown for semantic tags
|
|
122
|
+
if (details?.items && Array.isArray(details.items)) {
|
|
123
|
+
const requiredSection = details.items.find(item => item.type === 'required');
|
|
124
|
+
const optionalSection = details.items.find(item => item.type === 'optional');
|
|
125
|
+
const summarySection = details.items.find(item => item.type === 'summary');
|
|
126
|
+
if (requiredSection?.tags && Array.isArray(requiredSection.tags)) {
|
|
127
|
+
const requiredTags = requiredSection.tags;
|
|
128
|
+
const presentRequired = requiredTags.filter(t => t.present).map(t => t.tag);
|
|
129
|
+
const missingRequired = requiredTags.filter(t => !t.present).map(t => t.tag);
|
|
130
|
+
result += `\n Required tags found: ${presentRequired.join(', ') || 'none'}`;
|
|
131
|
+
if (missingRequired.length > 0) {
|
|
132
|
+
result += `\n Required tags missing: ${missingRequired.join(', ')}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (optionalSection?.tags && Array.isArray(optionalSection.tags)) {
|
|
136
|
+
const optionalTags = optionalSection.tags;
|
|
137
|
+
const presentOptional = optionalTags.filter(t => t.present).map(t => t.tag);
|
|
138
|
+
if (presentOptional.length > 0) {
|
|
139
|
+
result += `\n Optional tags found: ${presentOptional.join(', ')}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (summarySection) {
|
|
143
|
+
const totalCount = summarySection.totalCount;
|
|
144
|
+
result += `\n Total semantic tags: ${totalCount}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
// Fallback to original formatting
|
|
150
|
+
return formatAuditItem(item);
|
|
151
|
+
};
|
|
152
|
+
const formatAuditItem = (item) => {
|
|
153
|
+
// Handle specific SEO element formatting based on the user's list
|
|
154
|
+
// Title Tags
|
|
155
|
+
if (item.title && typeof item.title === 'string') {
|
|
156
|
+
return `Title: "${item.title}" (${item.length || 'unknown'} chars)`;
|
|
157
|
+
}
|
|
158
|
+
// Meta tags
|
|
159
|
+
if (item.name && typeof item.name === 'string' && item.content) {
|
|
160
|
+
return `${item.name}: ${String(item.content)}`;
|
|
161
|
+
}
|
|
162
|
+
// Headings
|
|
163
|
+
if (item.tag && typeof item.tag === 'string' && item.tag.match(/^h[1-6]$/i)) {
|
|
164
|
+
return `${item.tag}: "${item.text || ''}"`;
|
|
165
|
+
}
|
|
166
|
+
// Links and URLs
|
|
167
|
+
if (item.url && typeof item.url === 'string') {
|
|
168
|
+
if (item.issue && typeof item.issue === 'string') {
|
|
169
|
+
return `${item.url} - ${item.issue}`;
|
|
170
|
+
}
|
|
171
|
+
return item.url;
|
|
172
|
+
}
|
|
173
|
+
// Images with alt tags
|
|
174
|
+
if (item.src && typeof item.src === 'string') {
|
|
175
|
+
return `Image: ${item.src} ${item.alt ? '(has alt)' : '(missing alt)'}`;
|
|
176
|
+
}
|
|
177
|
+
// Robots.txt directives
|
|
178
|
+
if (item.directive && typeof item.directive === 'string') {
|
|
179
|
+
return `${item.directive}: ${item.value || ''}`;
|
|
180
|
+
}
|
|
181
|
+
// Sitemap entries
|
|
182
|
+
if (item.loc && typeof item.loc === 'string') {
|
|
183
|
+
return item.loc;
|
|
184
|
+
}
|
|
185
|
+
// Default formatting
|
|
186
|
+
return Object.entries(item)
|
|
187
|
+
.map(([key, value]) => `${key}: ${String(value)}`)
|
|
188
|
+
.join(', ');
|
|
189
|
+
};
|
|
190
|
+
return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: data.site.replace('-', ' ') }), _jsxs("p", { className: "health-site-url", children: ["URL: ", data.url] }), data.overallScore !== null && (_jsx("div", { className: "health-score-container", children: _jsxs("div", { className: "health-score-item", children: [_jsx("div", { className: "health-score-label", children: "On-Site SEO Score" }), _jsxs("div", { className: "health-score-value", style: { color: getScoreColor(data.overallScore) }, children: [Math.round((data.overallScore || 0) * 100), "%"] }), _jsx("div", { className: "health-score-bar", children: _jsx("div", { className: "health-score-fill", style: {
|
|
191
|
+
width: `${(data.overallScore || 0) * 100}%`,
|
|
192
|
+
backgroundColor: getScoreColor(data.overallScore)
|
|
193
|
+
} }) })] }) })), aggregatedOnPageAudits.length > 0 && (_jsxs("div", { children: [_jsx("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: "On-Page SEO Audits" }), _jsx("div", { className: "space-y-2", children: aggregatedOnPageAudits
|
|
194
|
+
.filter(audit => audit.scoreDisplayMode !== 'notApplicable')
|
|
195
|
+
.map((audit) => (_jsxs("div", { className: "health-audit-item", children: [_jsx("span", { className: "health-audit-icon", children: getAuditScoreIcon(audit.score) }), _jsxs("div", { className: "health-audit-content", children: [_jsxs("span", { className: "health-audit-title", children: ["(", Math.round((audit.score || 0) * 100), "%) ", audit.title] }), audit.displayValue && audit.score !== 1 && (_jsx("p", { className: "health-audit-description", children: audit.displayValue })), audit.details && audit.details.items && Array.isArray(audit.details.items) && audit.details.items.length > 0 && audit.score !== 1 && (_jsx("div", { className: "health-audit-details", children: _jsx("div", { style: { fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }, children: audit.details.items
|
|
196
|
+
.filter((item) => item.score !== 1)
|
|
197
|
+
.map((item, idx) => (_jsx("div", { style: { marginBottom: '0.125rem' }, children: formatPageIssue(item) }, idx))) }) }))] })] }, audit.id))) })] })), data.onSiteAudits.length > 0 && (_jsxs("div", { style: { marginTop: data.pagesAnalyzed.length > 0 ? '2rem' : '0' }, children: [_jsx("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: "On-Site SEO Audits" }), _jsx("div", { className: "space-y-2", children: data.onSiteAudits
|
|
198
|
+
.filter(audit => audit.scoreDisplayMode !== 'notApplicable')
|
|
199
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
200
|
+
.map((audit) => (_jsxs("div", { className: "health-audit-item", children: [_jsx("span", { className: "health-audit-icon", children: getAuditScoreIcon(audit.score) }), _jsxs("div", { className: "health-audit-content", children: [_jsxs("span", { className: "health-audit-title", children: ["(", Math.round((audit.score || 0) * 100), "%) ", audit.title] }), audit.displayValue && audit.score !== 1 && (_jsx("p", { className: "health-audit-description", children: audit.displayValue })), audit.details && audit.details.items && Array.isArray(audit.details.items) && audit.details.items.length > 0 && audit.score !== 1 && (_jsx("div", { className: "health-audit-details", children: _jsx("div", { style: { fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }, children: audit.details.items
|
|
201
|
+
.filter((item) => item.score !== 1)
|
|
202
|
+
.map((item, idx) => (_jsx("div", { style: { marginBottom: '0.125rem' }, children: formatAuditItem(item) }, idx))) }) }))] })] }, audit.id))) })] })), _jsxs("p", { className: "health-timestamp", children: ["Last checked: ", new Date(data.timestamp).toLocaleString()] })] }));
|
|
203
|
+
} }));
|
|
204
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { SiteHealthTemplate } from './site-health-template';
|
|
5
|
+
import { getScoreIndicator } from './site-health-indicators';
|
|
6
|
+
export function SiteHealthOverview({ siteName }) {
|
|
7
|
+
const fetchCWVData = useCallback(async (site) => {
|
|
8
|
+
const response = await fetch(`/api/site-health/core-web-vitals?siteName=${encodeURIComponent(site)}`);
|
|
9
|
+
const result = await response.json();
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
throw new Error(result.error || 'Failed to fetch Core Web Vitals data');
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}, []);
|
|
15
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Site Overview", fetchData: fetchCWVData, children: (data) => {
|
|
16
|
+
if (!data?.data || data.data.length === 0) {
|
|
17
|
+
return (_jsx("p", { style: { color: '#6b7280' }, children: "No site health data available for this site." }));
|
|
18
|
+
}
|
|
19
|
+
const siteData = data.data[0];
|
|
20
|
+
if (siteData.status === 'error') {
|
|
21
|
+
return (_jsxs("p", { style: { color: '#ef4444', fontSize: '0.875rem' }, children: ["Error: ", siteData.error] }));
|
|
22
|
+
}
|
|
23
|
+
// Helper functions
|
|
24
|
+
const getScoreColor = (score) => {
|
|
25
|
+
return getScoreIndicator(score).color;
|
|
26
|
+
};
|
|
27
|
+
const formatScore = (score) => {
|
|
28
|
+
if (score === null)
|
|
29
|
+
return 'N/A';
|
|
30
|
+
return `${Math.round(score * 100)}%`;
|
|
31
|
+
};
|
|
32
|
+
const getStatusColor = (status) => {
|
|
33
|
+
switch (status) {
|
|
34
|
+
case 'good': return '#10b981'; // green
|
|
35
|
+
case 'needs-improvement': return '#f59e0b'; // yellow
|
|
36
|
+
case 'poor': return '#ef4444'; // red
|
|
37
|
+
default: return '#6b7280'; // gray
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const getMetricStatus = (value, thresholds) => {
|
|
41
|
+
if (value <= thresholds.good)
|
|
42
|
+
return 'good';
|
|
43
|
+
if (value <= thresholds.poor)
|
|
44
|
+
return 'needs-improvement';
|
|
45
|
+
return 'poor';
|
|
46
|
+
};
|
|
47
|
+
const formatMetric = (value, unit) => {
|
|
48
|
+
if (value === null || value === undefined)
|
|
49
|
+
return 'N/A';
|
|
50
|
+
if (unit === 'ms') {
|
|
51
|
+
return `${Math.round(value)}ms`;
|
|
52
|
+
}
|
|
53
|
+
if (unit === '') {
|
|
54
|
+
return value.toFixed(3);
|
|
55
|
+
}
|
|
56
|
+
return `${value}${unit}`;
|
|
57
|
+
};
|
|
58
|
+
return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: siteData.site.replace('-', ' ') }), _jsxs("p", { className: "health-site-url", children: ["URL: ", siteData.url] }), _jsx("div", { className: "health-score-container", children: Object.entries(siteData.scores)
|
|
59
|
+
.filter(([, score]) => score !== null)
|
|
60
|
+
.map(([category, score]) => (_jsxs("div", { className: "health-score-item", children: [_jsx("div", { className: "health-score-label", children: category.replace('-', ' ') }), _jsx("div", { className: "health-score-value", style: { color: getScoreColor(score) }, children: formatScore(score) }), _jsx("div", { className: "health-score-bar", children: _jsx("div", { className: "health-score-fill", style: {
|
|
61
|
+
width: score !== null ? `${score * 100}%` : '0%',
|
|
62
|
+
backgroundColor: score !== null ? getScoreColor(score) : '#6b7280'
|
|
63
|
+
} }) })] }, category))) }), _jsxs("div", { style: { marginBottom: '1.5rem' }, children: [_jsx("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: "Core Web Vitals" }), _jsxs("div", { className: "health-cwv-grid", children: [_jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "Cumulative Layout Shift:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.cls, { good: 0.1, poor: 0.25 })) }, children: formatMetric(siteData.metrics.cls, '') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "First Input Delay:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.fid, { good: 100, poor: 300 })) }, children: formatMetric(siteData.metrics.fid, 'ms') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "Largest Contentful Paint:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.lcp, { good: 2500, poor: 4000 })) }, children: formatMetric(siteData.metrics.lcp, 'ms') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "First Contentful Paint:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.fcp, { good: 1800, poor: 3000 })) }, children: formatMetric(siteData.metrics.fcp, 'ms') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "Time to First Byte:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.ttfb, { good: 800, poor: 1800 })) }, children: formatMetric(siteData.metrics.ttfb, 'ms') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "Speed Index:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.speedIndex, { good: 3400, poor: 5800 })) }, children: formatMetric(siteData.metrics.speedIndex, 'ms') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "Time to Interactive:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.interactive, { good: 3800, poor: 7300 })) }, children: formatMetric(siteData.metrics.interactive, 'ms') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "Total Blocking Time:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.totalBlockingTime, { good: 200, poor: 600 })) }, children: formatMetric(siteData.metrics.totalBlockingTime, 'ms') })] }), _jsxs("div", { className: "health-cwv-item", children: [_jsx("span", { className: "health-cwv-label", children: "First Meaningful Paint:" }), _jsx("span", { className: "health-cwv-value", style: { color: getStatusColor(getMetricStatus(siteData.metrics.firstMeaningfulPaint, { good: 2000, poor: 4000 })) }, children: formatMetric(siteData.metrics.firstMeaningfulPaint, 'ms') })] })] })] }), _jsxs("p", { className: "health-timestamp", children: ["Last checked: ", new Date(siteData.timestamp).toLocaleString()] })] }));
|
|
64
|
+
} }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { SiteHealthTemplate } from './site-health-template';
|
|
5
|
+
import { getScoreIndicator } from './site-health-indicators';
|
|
6
|
+
export function SiteHealthPerformance({ siteName }) {
|
|
7
|
+
const fetchCWVData = useCallback(async (site) => {
|
|
8
|
+
const response = await fetch(`/api/site-health/core-web-vitals?siteName=${encodeURIComponent(site)}`);
|
|
9
|
+
const result = await response.json();
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
throw new Error(result.error || 'Failed to fetch Core Web Vitals data');
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}, []);
|
|
15
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Performance", fetchData: fetchCWVData, children: (data) => {
|
|
16
|
+
if (!data?.data || data.data.length === 0) {
|
|
17
|
+
return (_jsx("p", { style: { color: '#6b7280' }, children: "No site health data available for this site." }));
|
|
18
|
+
}
|
|
19
|
+
const siteData = data.data[0];
|
|
20
|
+
if (siteData.status === 'error') {
|
|
21
|
+
return (_jsxs("p", { style: { color: '#ef4444', fontSize: '0.875rem' }, children: ["Error: ", siteData.error] }));
|
|
22
|
+
}
|
|
23
|
+
const getAuditScoreIcon = (score) => {
|
|
24
|
+
return getScoreIndicator(score).icon;
|
|
25
|
+
};
|
|
26
|
+
const getScoreColor = (score) => {
|
|
27
|
+
return getScoreIndicator(score).color;
|
|
28
|
+
};
|
|
29
|
+
const formatScore = (score) => {
|
|
30
|
+
if (score === null)
|
|
31
|
+
return 'N/A';
|
|
32
|
+
return `${Math.round(score * 100)}%`;
|
|
33
|
+
};
|
|
34
|
+
// Helper function to display audit item details
|
|
35
|
+
const formatAuditItem = (item, auditTitle) => {
|
|
36
|
+
// Handle URLs
|
|
37
|
+
if (item.url && typeof item.url === 'string') {
|
|
38
|
+
return item.url;
|
|
39
|
+
}
|
|
40
|
+
// Handle sources (like JavaScript files)
|
|
41
|
+
if (item.source && typeof item.source === 'string') {
|
|
42
|
+
return item.source;
|
|
43
|
+
}
|
|
44
|
+
// Handle text descriptions
|
|
45
|
+
if (item.text && typeof item.text === 'string') {
|
|
46
|
+
return item.text;
|
|
47
|
+
}
|
|
48
|
+
// Handle entities (like "Google Tag Manager")
|
|
49
|
+
if (item.entity && typeof item.entity === 'string') {
|
|
50
|
+
return item.entity;
|
|
51
|
+
}
|
|
52
|
+
// Handle nodes with selectors
|
|
53
|
+
if (item.node && typeof item.node === 'object' && 'selector' in item.node) {
|
|
54
|
+
return `Element: ${item.node.selector}`;
|
|
55
|
+
}
|
|
56
|
+
// Handle nodes with snippets
|
|
57
|
+
if (item.node && typeof item.node === 'object' && 'snippet' in item.node) {
|
|
58
|
+
const snippet = item.node.snippet;
|
|
59
|
+
return `Element: ${snippet.length > 50 ? snippet.substring(0, 50) + '...' : snippet}`;
|
|
60
|
+
}
|
|
61
|
+
// Handle origins (like domains)
|
|
62
|
+
if (item.origin && typeof item.origin === 'string') {
|
|
63
|
+
return item.origin;
|
|
64
|
+
}
|
|
65
|
+
// Handle labels
|
|
66
|
+
if (item.label && typeof item.label === 'string') {
|
|
67
|
+
return item.label;
|
|
68
|
+
}
|
|
69
|
+
// Handle numeric values with units
|
|
70
|
+
if (item.value && typeof item.value === 'object' && 'type' in item.value && item.value.type === 'numeric') {
|
|
71
|
+
const value = item.value;
|
|
72
|
+
return `${value.value}${item.unit || ''}`;
|
|
73
|
+
}
|
|
74
|
+
// Handle statistics
|
|
75
|
+
if (item.statistic && typeof item.statistic === 'string' && item.value) {
|
|
76
|
+
if (typeof item.value === 'object' && 'type' in item.value && item.value.type === 'numeric') {
|
|
77
|
+
const value = item.value;
|
|
78
|
+
return `${item.statistic}: ${value.value}`;
|
|
79
|
+
}
|
|
80
|
+
return item.statistic;
|
|
81
|
+
}
|
|
82
|
+
// Handle timing data with audit context
|
|
83
|
+
if (typeof item === 'number') {
|
|
84
|
+
let context = '';
|
|
85
|
+
if (auditTitle) {
|
|
86
|
+
if (auditTitle.toLowerCase().includes('server') || auditTitle.toLowerCase().includes('backend')) {
|
|
87
|
+
context = ' server response';
|
|
88
|
+
}
|
|
89
|
+
else if (auditTitle.toLowerCase().includes('network') || auditTitle.toLowerCase().includes('request')) {
|
|
90
|
+
context = ' network request';
|
|
91
|
+
}
|
|
92
|
+
else if (auditTitle.toLowerCase().includes('render') || auditTitle.toLowerCase().includes('blocking')) {
|
|
93
|
+
context = ' render blocking';
|
|
94
|
+
}
|
|
95
|
+
else if (auditTitle.toLowerCase().includes('javascript') || auditTitle.toLowerCase().includes('js')) {
|
|
96
|
+
context = ' JavaScript';
|
|
97
|
+
}
|
|
98
|
+
else if (auditTitle.toLowerCase().includes('image') || auditTitle.toLowerCase().includes('media')) {
|
|
99
|
+
context = ' media resource';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return `${item.toFixed(2)}ms${context}`;
|
|
103
|
+
}
|
|
104
|
+
if (item.value && typeof item.value === 'number') {
|
|
105
|
+
const unit = item.unit || 'ms';
|
|
106
|
+
let context = '';
|
|
107
|
+
if (auditTitle && unit === 'ms') {
|
|
108
|
+
if (auditTitle.toLowerCase().includes('server')) {
|
|
109
|
+
context = ' server time';
|
|
110
|
+
}
|
|
111
|
+
else if (auditTitle.toLowerCase().includes('network')) {
|
|
112
|
+
context = ' network time';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return `${item.value.toFixed(2)}${unit}${context}`;
|
|
116
|
+
}
|
|
117
|
+
// Handle timing data with more context
|
|
118
|
+
if (item.duration && typeof item.duration === 'number') {
|
|
119
|
+
const duration = item.duration;
|
|
120
|
+
let context = '';
|
|
121
|
+
if (item.url && typeof item.url === 'string') {
|
|
122
|
+
context = ` for ${item.url}`;
|
|
123
|
+
}
|
|
124
|
+
else if (item.source && typeof item.source === 'string') {
|
|
125
|
+
context = ` for ${item.source}`;
|
|
126
|
+
}
|
|
127
|
+
else if (item.name && typeof item.name === 'string') {
|
|
128
|
+
context = ` for ${item.name}`;
|
|
129
|
+
}
|
|
130
|
+
else if (item.path && typeof item.path === 'string') {
|
|
131
|
+
context = ` for ${item.path}`;
|
|
132
|
+
}
|
|
133
|
+
else if (item.request && typeof item.request === 'string') {
|
|
134
|
+
context = ` for ${item.request}`;
|
|
135
|
+
}
|
|
136
|
+
return `${duration.toFixed(2)}ms${context}`;
|
|
137
|
+
}
|
|
138
|
+
// Handle response times
|
|
139
|
+
if (item.responseTime && typeof item.responseTime === 'number') {
|
|
140
|
+
const url = (item.url && typeof item.url === 'string') ? ` (${item.url})` : '';
|
|
141
|
+
return `${item.responseTime.toFixed(2)}ms response time${url}`;
|
|
142
|
+
}
|
|
143
|
+
// Handle start/end times
|
|
144
|
+
if ((item.startTime || item.endTime) && typeof (item.startTime || item.endTime) === 'number') {
|
|
145
|
+
const start = item.startTime && typeof item.startTime === 'number' ? item.startTime.toFixed(2) : '?';
|
|
146
|
+
const end = item.endTime && typeof item.endTime === 'number' ? item.endTime.toFixed(2) : '?';
|
|
147
|
+
const url = (item.url && typeof item.url === 'string') ? ` for ${item.url}` : '';
|
|
148
|
+
return `${start}ms - ${end}ms${url}`;
|
|
149
|
+
}
|
|
150
|
+
// Handle transfer size with timing
|
|
151
|
+
if (item.transferSize && typeof item.transferSize === 'number' && item.duration && typeof item.duration === 'number') {
|
|
152
|
+
const size = (item.transferSize / 1024).toFixed(1);
|
|
153
|
+
const time = item.duration.toFixed(2);
|
|
154
|
+
const url = (item.url && typeof item.url === 'string') ? ` (${item.url})` : '';
|
|
155
|
+
return `${size} KB in ${time}ms${url}`;
|
|
156
|
+
}
|
|
157
|
+
// Handle main thread time
|
|
158
|
+
if (item.mainThreadTime && typeof item.mainThreadTime === 'number') {
|
|
159
|
+
return `${item.mainThreadTime.toFixed(1)}ms`;
|
|
160
|
+
}
|
|
161
|
+
// For other objects, try to find a meaningful display
|
|
162
|
+
if (item.group && typeof item.group === 'string') {
|
|
163
|
+
return item.group;
|
|
164
|
+
}
|
|
165
|
+
if (item.type && typeof item.type === 'string') {
|
|
166
|
+
return item.type;
|
|
167
|
+
}
|
|
168
|
+
// If we can't find anything meaningful, provide a generic description
|
|
169
|
+
// This handles raw timing data that might be from various performance metrics
|
|
170
|
+
if (typeof item === 'number') {
|
|
171
|
+
return `${item.toFixed(2)}ms`;
|
|
172
|
+
}
|
|
173
|
+
if (item.value && typeof item.value === 'number') {
|
|
174
|
+
const unit = item.unit || 'ms';
|
|
175
|
+
return `${item.value.toFixed(2)}${unit}`;
|
|
176
|
+
}
|
|
177
|
+
return 'Performance metric data available';
|
|
178
|
+
};
|
|
179
|
+
return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: siteData.site.replace('-', ' ') }), _jsxs("p", { className: "health-site-url", children: ["URL: ", siteData.url] }), _jsx("div", { style: { marginBottom: '1.5rem' }, children: _jsxs("div", { className: "health-score-item", style: { width: '100%' }, children: [_jsx("div", { className: "health-score-label", children: "Performance Score" }), _jsx("div", { className: "health-score-value", style: { color: getScoreColor(siteData.scores.performance) }, children: formatScore(siteData.scores.performance) }), _jsx("div", { className: "health-score-bar", children: _jsx("div", { className: "health-score-fill", style: {
|
|
180
|
+
width: siteData.scores.performance !== null ? `${siteData.scores.performance * 100}%` : '0%',
|
|
181
|
+
backgroundColor: siteData.scores.performance !== null ? getScoreColor(siteData.scores.performance) : '#6b7280'
|
|
182
|
+
} }) })] }) }), ((siteData.categories.performance?.audits?.length > 0) || (siteData.categories.pwa?.audits?.length > 0)) && (_jsxs("div", { children: [_jsx("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: "Performance Opportunities" }), _jsx("div", { className: "space-y-2", children: [
|
|
183
|
+
...(siteData.categories.performance?.audits || []),
|
|
184
|
+
...(siteData.categories.pwa?.audits || [])
|
|
185
|
+
]
|
|
186
|
+
.filter(audit => audit.scoreDisplayMode !== 'notApplicable' && audit.score !== null && !audit.id.includes('network-requests'))
|
|
187
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
188
|
+
.slice(0, 20)
|
|
189
|
+
.map((audit) => (_jsxs("div", { className: "health-audit-item", children: [_jsx("span", { className: "health-audit-icon", children: getAuditScoreIcon(audit.score) }), _jsxs("div", { className: "health-audit-content", children: [_jsxs("span", { className: "health-audit-title", children: ["(", Math.round((audit.score || 0) * 100), "%) ", audit.title, audit.displayValue ? `: ${audit.displayValue}` : ''] }), audit.details?.items && Array.isArray(audit.details.items) && audit.details.items.length > 0 && (audit.score || 0) < 0.9 && (_jsx("div", { className: "health-audit-details", children: _jsx("div", { style: { fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }, children: audit.details.items.map((item, idx) => (_jsx("div", { style: { marginBottom: '0.125rem' }, children: formatAuditItem(item, audit.title) }, idx))) }) }))] })] }, audit.id))) })] })), _jsxs("p", { className: "health-timestamp", children: ["Last checked: ", new Date(siteData.timestamp).toLocaleString()] })] }));
|
|
190
|
+
} }));
|
|
191
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
export async function analyzeSecurityHealth(localPath) {
|
|
7
|
+
try {
|
|
8
|
+
// Check if the local path exists and has package.json
|
|
9
|
+
if (!fs.existsSync(localPath)) {
|
|
10
|
+
return {
|
|
11
|
+
status: 'error',
|
|
12
|
+
error: 'Site directory not found'
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const packageJsonPath = path.join(localPath, 'package.json');
|
|
16
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
17
|
+
return {
|
|
18
|
+
status: 'success',
|
|
19
|
+
data: {
|
|
20
|
+
status: 'No Dependencies',
|
|
21
|
+
message: 'No package.json found',
|
|
22
|
+
vulnerabilities: [],
|
|
23
|
+
summary: { info: 0, low: 0, moderate: 0, high: 0, critical: 0, total: 0 },
|
|
24
|
+
dependencies: 0,
|
|
25
|
+
totalDependencies: 0
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Run npm audit
|
|
30
|
+
const auditResult = await runNpmAudit(localPath);
|
|
31
|
+
// Process vulnerabilities
|
|
32
|
+
const vulnerabilities = [];
|
|
33
|
+
let totalVulns = 0;
|
|
34
|
+
if (auditResult.vulnerabilities) {
|
|
35
|
+
for (const [pkgName, vulnData] of Object.entries(auditResult.vulnerabilities)) {
|
|
36
|
+
const vuln = vulnData;
|
|
37
|
+
vulnerabilities.push({
|
|
38
|
+
name: pkgName,
|
|
39
|
+
severity: vuln.severity,
|
|
40
|
+
title: Array.isArray(vuln.via) ? (typeof vuln.via[0] === 'string' ? vuln.via[0] : vuln.via[0]?.title || 'Unknown vulnerability') : vuln.title || 'Unknown vulnerability',
|
|
41
|
+
url: vuln.url,
|
|
42
|
+
range: vuln.range,
|
|
43
|
+
fixAvailable: vuln.fixAvailable || false
|
|
44
|
+
});
|
|
45
|
+
totalVulns++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Calculate overall status
|
|
49
|
+
const metadata = auditResult.metadata?.vulnerabilities || { info: 0, low: 0, moderate: 0, high: 0, critical: 0 };
|
|
50
|
+
const hasCritical = metadata.critical > 0;
|
|
51
|
+
const hasHigh = metadata.high > 0;
|
|
52
|
+
const hasModerate = metadata.moderate > 0;
|
|
53
|
+
let overallStatus = 'Secure';
|
|
54
|
+
if (hasCritical) {
|
|
55
|
+
overallStatus = 'Critical';
|
|
56
|
+
}
|
|
57
|
+
else if (hasHigh) {
|
|
58
|
+
overallStatus = 'High Risk';
|
|
59
|
+
}
|
|
60
|
+
else if (hasModerate) {
|
|
61
|
+
overallStatus = 'Moderate Risk';
|
|
62
|
+
}
|
|
63
|
+
else if (metadata.low > 0 || metadata.info > 0) {
|
|
64
|
+
overallStatus = 'Low Risk';
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
status: 'success',
|
|
68
|
+
data: {
|
|
69
|
+
status: overallStatus,
|
|
70
|
+
vulnerabilities,
|
|
71
|
+
summary: {
|
|
72
|
+
...metadata,
|
|
73
|
+
total: totalVulns
|
|
74
|
+
},
|
|
75
|
+
dependencies: auditResult.metadata?.dependencies?.total || 0,
|
|
76
|
+
totalDependencies: auditResult.metadata?.totalDependencies || 0
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error('Error running npm audit:', error);
|
|
82
|
+
return {
|
|
83
|
+
status: 'error',
|
|
84
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function runNpmAudit(localPath) {
|
|
89
|
+
try {
|
|
90
|
+
const { stdout } = await execAsync('npm audit --json', {
|
|
91
|
+
cwd: localPath,
|
|
92
|
+
timeout: 30000 // 30 second timeout
|
|
93
|
+
});
|
|
94
|
+
return JSON.parse(stdout);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// npm audit exits with code 1 when vulnerabilities are found, but still returns JSON
|
|
98
|
+
const execError = error;
|
|
99
|
+
if (execError.stdout) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(execError.stdout);
|
|
102
|
+
}
|
|
103
|
+
catch (parseError) {
|
|
104
|
+
throw new Error(`Failed to parse npm audit output: ${parseError.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`npm audit failed: ${execError.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|