@pixelated-tech/components 3.3.5 → 3.4.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.COMPONENTS.md +126 -0
- package/README.md +15 -9
- 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 +170 -0
- package/dist/components/admin/site-health/google-api-auth.js +69 -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 +578 -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/hubspot.components.js +3 -3
- package/dist/components/config/config.client.js +21 -10
- package/dist/components/general/table.js +3 -1
- package/dist/components/seo/googleanalytics.js +1 -2
- 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 +1 -1
- 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/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 +16 -3
- package/dist/index.server.js +36 -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/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/config/config.client.d.ts +1 -2
- package/dist/types/components/config/config.client.d.ts.map +1 -1
- package/dist/types/components/general/table.d.ts +1 -0
- package/dist/types/components/general/table.d.ts.map +1 -1
- package/dist/types/components/seo/googleanalytics.d.ts +1 -1
- package/dist/types/components/seo/googleanalytics.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/index.d.ts +16 -3
- package/dist/types/index.server.d.ts +36 -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/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 +15 -9
- 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,208 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
const psiCache = new Map();
|
|
3
|
+
const CACHE_TTL_SUCCESS = 60 * 60 * 1000; // 1 hour for successful results
|
|
4
|
+
const CACHE_TTL_ERROR = 5 * 60 * 1000; // 5 minutes for error results
|
|
5
|
+
// Clean up expired cache entries periodically
|
|
6
|
+
setInterval(() => {
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
for (const [key, entry] of psiCache.entries()) {
|
|
9
|
+
const ttl = entry.data.status === 'success' ? CACHE_TTL_SUCCESS : CACHE_TTL_ERROR;
|
|
10
|
+
if (now - entry.timestamp > ttl) {
|
|
11
|
+
psiCache.delete(key);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}, 10 * 60 * 1000); // Clean up every 10 minutes
|
|
15
|
+
export async function performCoreWebVitalsAnalysis(url, siteName, useCache = true) {
|
|
16
|
+
try {
|
|
17
|
+
// Check cache first (if caching is enabled)
|
|
18
|
+
const cacheKey = `${siteName}:${url}`;
|
|
19
|
+
if (useCache) {
|
|
20
|
+
const cached = psiCache.get(cacheKey);
|
|
21
|
+
if (cached) {
|
|
22
|
+
const ttl = cached.data.status === 'success' ? CACHE_TTL_SUCCESS : CACHE_TTL_ERROR;
|
|
23
|
+
if ((Date.now() - cached.timestamp) < ttl) {
|
|
24
|
+
return cached.data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Fetch PSI data
|
|
29
|
+
const psiData = await fetchPSIData(url);
|
|
30
|
+
// Process the PSI data
|
|
31
|
+
const resultData = processPSIData(psiData, siteName, url);
|
|
32
|
+
// Cache successful results (if caching is enabled)
|
|
33
|
+
if (useCache) {
|
|
34
|
+
psiCache.set(cacheKey, {
|
|
35
|
+
data: resultData,
|
|
36
|
+
timestamp: Date.now()
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return resultData;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.warn(`PSI API failed for ${siteName}:`, error);
|
|
43
|
+
// Return error status instead of mock data
|
|
44
|
+
const errorResult = {
|
|
45
|
+
site: siteName,
|
|
46
|
+
url: url,
|
|
47
|
+
metrics: {
|
|
48
|
+
cls: 0,
|
|
49
|
+
fid: 0,
|
|
50
|
+
lcp: 0,
|
|
51
|
+
fcp: 0,
|
|
52
|
+
ttfb: 0,
|
|
53
|
+
speedIndex: 0,
|
|
54
|
+
interactive: 0,
|
|
55
|
+
totalBlockingTime: 0,
|
|
56
|
+
firstMeaningfulPaint: 0,
|
|
57
|
+
},
|
|
58
|
+
scores: {
|
|
59
|
+
performance: 0,
|
|
60
|
+
accessibility: 0,
|
|
61
|
+
'best-practices': 0,
|
|
62
|
+
seo: 0,
|
|
63
|
+
pwa: 0,
|
|
64
|
+
},
|
|
65
|
+
categories: {
|
|
66
|
+
performance: { id: 'performance', title: 'Performance', score: null, audits: [] },
|
|
67
|
+
accessibility: { id: 'accessibility', title: 'Accessibility', score: null, audits: [] },
|
|
68
|
+
'best-practices': { id: 'best-practices', title: 'Best Practices', score: null, audits: [] },
|
|
69
|
+
seo: { id: 'seo', title: 'SEO', score: null, audits: [] },
|
|
70
|
+
pwa: { id: 'pwa', title: 'PWA', score: null, audits: [] },
|
|
71
|
+
},
|
|
72
|
+
timestamp: new Date().toISOString(),
|
|
73
|
+
status: 'error',
|
|
74
|
+
error: error instanceof Error ? error.message : 'PSI API failed',
|
|
75
|
+
};
|
|
76
|
+
// Cache error results with shorter TTL (5 minutes) (if caching is enabled)
|
|
77
|
+
if (useCache) {
|
|
78
|
+
psiCache.set(`${siteName}:${url}`, {
|
|
79
|
+
data: errorResult,
|
|
80
|
+
timestamp: Date.now()
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return errorResult;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function fetchPSIData(url) {
|
|
87
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
88
|
+
if (!apiKey) {
|
|
89
|
+
throw new Error('GOOGLE_API_KEY environment variable is not set');
|
|
90
|
+
}
|
|
91
|
+
const psiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&key=${apiKey}&strategy=mobile&category=performance&category=accessibility&category=best-practices&category=seo&category=pwa`;
|
|
92
|
+
const fetchWithRetry = async (url, maxRetries = 2) => {
|
|
93
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
94
|
+
try {
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
headers: {
|
|
100
|
+
'User-Agent': 'Mozilla/5.0 (compatible; SiteHealthMonitor/1.0)'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
clearTimeout(timeoutId);
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (attempt === maxRetries || error instanceof Error && error.name === 'AbortError') {
|
|
108
|
+
const errorMessage = error instanceof Error && error.name === 'AbortError'
|
|
109
|
+
? 'PSI API request timed out after 60 seconds'
|
|
110
|
+
: `PSI API request failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
111
|
+
throw new Error(errorMessage);
|
|
112
|
+
}
|
|
113
|
+
// Wait before retry (exponential backoff)
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
throw new Error('Max retries exceeded');
|
|
118
|
+
};
|
|
119
|
+
const psiResponse = await fetchWithRetry(psiUrl);
|
|
120
|
+
if (!psiResponse.ok) {
|
|
121
|
+
// Log the error response for debugging
|
|
122
|
+
const errorText = await psiResponse.text();
|
|
123
|
+
console.error(`PSI API error for ${url}:`, {
|
|
124
|
+
status: psiResponse.status,
|
|
125
|
+
statusText: psiResponse.statusText,
|
|
126
|
+
url: psiUrl,
|
|
127
|
+
response: errorText
|
|
128
|
+
});
|
|
129
|
+
throw new Error(`PSI API returned ${psiResponse.status}: ${psiResponse.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
const psiData = await psiResponse.json();
|
|
132
|
+
// Check if we have valid data
|
|
133
|
+
if (psiData.lighthouseResult?.audits &&
|
|
134
|
+
Object.keys(psiData.lighthouseResult.audits).length > 0) {
|
|
135
|
+
return psiData;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// PSI API returned data but no audits - likely rate limited or invalid response
|
|
139
|
+
throw new Error('Invalid PSI API response or rate limited');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function processPSIData(psiData, siteName, url) {
|
|
143
|
+
const audits = psiData.lighthouseResult.audits;
|
|
144
|
+
const categories = psiData.lighthouseResult.categories;
|
|
145
|
+
// Extract metrics with proper fallbacks
|
|
146
|
+
const realMetrics = {
|
|
147
|
+
cls: audits['cumulative-layout-shift']?.numericValue || audits['cumulative-layout-shift']?.displayValue ? parseFloat(audits['cumulative-layout-shift'].displayValue || audits['cumulative-layout-shift'].numericValue) : 0,
|
|
148
|
+
fid: audits['max-potential-fid']?.numericValue || audits['max-potential-fid']?.displayValue ? parseFloat(audits['max-potential-fid'].displayValue || audits['max-potential-fid'].numericValue) : 0,
|
|
149
|
+
lcp: audits['largest-contentful-paint']?.numericValue || audits['largest-contentful-paint']?.displayValue ? parseFloat(audits['largest-contentful-paint'].displayValue || audits['largest-contentful-paint'].numericValue) : 0,
|
|
150
|
+
fcp: audits['first-contentful-paint']?.numericValue || audits['first-contentful-paint']?.displayValue ? parseFloat(audits['first-contentful-paint'].displayValue || audits['first-contentful-paint'].numericValue) : 0,
|
|
151
|
+
ttfb: audits['server-response-time']?.numericValue ? audits['server-response-time'].numericValue * 1000 : audits['server-response-time']?.displayValue ? parseFloat(audits['server-response-time'].displayValue) * 1000 : 0,
|
|
152
|
+
speedIndex: audits['speed-index']?.numericValue || audits['speed-index']?.displayValue ? parseFloat(audits['speed-index'].displayValue || audits['speed-index'].numericValue) : 0,
|
|
153
|
+
interactive: audits['interactive']?.numericValue || audits['interactive']?.displayValue ? parseFloat(audits['interactive'].displayValue || audits['interactive'].numericValue) : 0,
|
|
154
|
+
totalBlockingTime: audits['total-blocking-time']?.numericValue || audits['total-blocking-time']?.displayValue ? parseFloat(audits['total-blocking-time'].displayValue || audits['total-blocking-time'].numericValue) : 0,
|
|
155
|
+
firstMeaningfulPaint: audits['first-meaningful-paint']?.numericValue || audits['first-meaningful-paint']?.displayValue ? parseFloat(audits['first-meaningful-paint'].displayValue || audits['first-meaningful-paint'].numericValue) : 0,
|
|
156
|
+
};
|
|
157
|
+
// Extract category scores
|
|
158
|
+
const scores = {
|
|
159
|
+
performance: categories?.performance?.score ?? null,
|
|
160
|
+
accessibility: categories?.accessibility?.score ?? null,
|
|
161
|
+
'best-practices': categories?.['best-practices']?.score ?? null,
|
|
162
|
+
seo: categories?.seo?.score ?? null,
|
|
163
|
+
pwa: categories?.pwa?.score ?? null,
|
|
164
|
+
};
|
|
165
|
+
// Extract category details with audits
|
|
166
|
+
const categoryDetails = {
|
|
167
|
+
performance: extractCategoryData(categories?.performance, audits),
|
|
168
|
+
accessibility: extractCategoryData(categories?.accessibility, audits),
|
|
169
|
+
'best-practices': extractCategoryData(categories?.['best-practices'], audits),
|
|
170
|
+
seo: extractCategoryData(categories?.seo, audits),
|
|
171
|
+
pwa: extractCategoryData(categories?.pwa, audits),
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
site: siteName,
|
|
175
|
+
url: url,
|
|
176
|
+
metrics: realMetrics,
|
|
177
|
+
scores: scores,
|
|
178
|
+
categories: categoryDetails,
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
status: 'success',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function extractCategoryData(category, audits) {
|
|
184
|
+
if (!category) {
|
|
185
|
+
return { id: 'unknown', title: 'Unknown', score: null, audits: [] };
|
|
186
|
+
}
|
|
187
|
+
const categoryAudits = category.auditRefs?.map((ref) => {
|
|
188
|
+
const audit = audits[ref.id];
|
|
189
|
+
if (!audit)
|
|
190
|
+
return null;
|
|
191
|
+
return {
|
|
192
|
+
id: audit.id,
|
|
193
|
+
title: audit.title,
|
|
194
|
+
description: audit.description,
|
|
195
|
+
score: audit.score,
|
|
196
|
+
scoreDisplayMode: audit.scoreDisplayMode,
|
|
197
|
+
displayValue: audit.displayValue,
|
|
198
|
+
numericValue: audit.numericValue,
|
|
199
|
+
details: audit.details,
|
|
200
|
+
};
|
|
201
|
+
}).filter(Boolean) || [];
|
|
202
|
+
return {
|
|
203
|
+
id: category.id,
|
|
204
|
+
title: category.title,
|
|
205
|
+
score: category.score,
|
|
206
|
+
audits: categoryAudits,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
export function SiteHealthDependencyVulnerabilities({ siteName }) {
|
|
6
|
+
const fetchDependencyData = useCallback(async (site) => {
|
|
7
|
+
const response = await fetch(`/api/site-health/security?siteName=${encodeURIComponent(site)}`);
|
|
8
|
+
const result = await response.json();
|
|
9
|
+
if (!result.success) {
|
|
10
|
+
throw new Error(result.error || 'Failed to fetch dependency data');
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}, []);
|
|
14
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Dependency Vulnerability", fetchData: fetchDependencyData, children: (data) => {
|
|
15
|
+
if (!data) {
|
|
16
|
+
return (_jsx("p", { style: { color: '#6b7280' }, children: "No dependency data available for this site." }));
|
|
17
|
+
}
|
|
18
|
+
return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: siteName.replace('-', ' ') }), _jsxs("p", { className: "health-site-url", children: ["URL: ", data.url] }), _jsx("div", { className: "health-score-container", children: _jsxs("div", { className: "health-score-item", children: [_jsx("div", { className: "health-score-label", children: "Overall Status" }), _jsx("div", { className: "health-score-value", style: {
|
|
19
|
+
color: data.status === 'Secure' ? '#10b981' :
|
|
20
|
+
data.status === 'Low Risk' ? '#f59e0b' :
|
|
21
|
+
data.status === 'Moderate Risk' ? '#f59e0b' :
|
|
22
|
+
data.status === 'High Risk' ? '#ef4444' :
|
|
23
|
+
data.status === 'Critical' ? '#ef4444' :
|
|
24
|
+
'#6b7280'
|
|
25
|
+
}, children: data.status }), _jsx("div", { className: "health-score-bar", children: _jsx("div", { className: "health-score-fill", style: {
|
|
26
|
+
width: data.status === 'Secure' ? '100%' :
|
|
27
|
+
data.status === 'Low Risk' ? '75%' :
|
|
28
|
+
data.status === 'Moderate Risk' ? '50%' :
|
|
29
|
+
data.status === 'High Risk' ? '25%' :
|
|
30
|
+
data.status === 'Critical' ? '10%' : '0%',
|
|
31
|
+
backgroundColor: data.status === 'Secure' ? '#10b981' :
|
|
32
|
+
data.status === 'Low Risk' ? '#f59e0b' :
|
|
33
|
+
data.status === 'Moderate Risk' ? '#f59e0b' :
|
|
34
|
+
data.status === 'High Risk' ? '#ef4444' :
|
|
35
|
+
data.status === 'Critical' ? '#ef4444' : '#6b7280'
|
|
36
|
+
} }) })] }) }), _jsxs("div", { className: "health-audit-item", children: [_jsx("span", { className: "health-audit-icon", style: { color: '#10b981' }, children: "\u2713" }), _jsx("div", { className: "health-audit-content", children: _jsxs("span", { className: "health-audit-title", children: ["Dependencies: ", data.totalDependencies || data.dependencies || 0] }) })] }), data.vulnerabilities && data.vulnerabilities.length > 0 && (_jsxs("div", { children: [_jsxs("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: ["Vulnerabilities (", data.summary.total, ")"] }), _jsx("div", { className: "space-y-2", children: data.vulnerabilities.map((vuln, index) => (_jsx("div", { className: `health-vulnerability-item health-vulnerability-${vuln.severity}`, children: _jsxs("div", { className: "health-vulnerability-header", children: [_jsx("span", { className: "health-vulnerability-severity", children: vuln.severity }), _jsxs("div", { children: [_jsx("span", { className: "health-vulnerability-name", children: vuln.name }), vuln.title && (_jsx("p", { className: "health-vulnerability-details", children: vuln.title })), _jsxs("div", { className: "health-vulnerability-meta", children: [_jsxs("span", { className: "health-vulnerability-range", children: ["Range: ", vuln.range] }), vuln.fixAvailable && (_jsx("span", { className: "health-vulnerability-fix", children: "\u2713 Fix available" }))] }), vuln.url && (_jsx("a", { href: vuln.url, target: "_blank", rel: "noopener noreferrer", className: "health-vulnerability-link", children: "View details \u2192" }))] })] }) }, index))) })] })), (!data.vulnerabilities || data.vulnerabilities.length === 0) && data.status === 'Secure' && (_jsxs("div", { className: "health-audit-item", children: [_jsx("span", { className: "health-audit-icon", style: { color: '#10b981' }, children: "\u2713" }), _jsx("div", { className: "health-audit-content", children: _jsx("span", { className: "health-audit-title", children: "No vulnerabilities found" }) })] })), _jsxs("p", { className: "health-timestamp", children: ["Last checked: ", new Date(data.timestamp).toLocaleString()] })] }));
|
|
37
|
+
} }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Health Integration Services
|
|
3
|
+
* Server-side utilities for analyzing git repository health
|
|
4
|
+
*/
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
/**
|
|
11
|
+
* Analyze git repository health for a site
|
|
12
|
+
*/
|
|
13
|
+
export async function analyzeGitHealth(siteConfig, startDate, endDate) {
|
|
14
|
+
try {
|
|
15
|
+
const { localPath } = siteConfig;
|
|
16
|
+
// Check if the local path exists and is a git repository
|
|
17
|
+
if (!fs.existsSync(localPath)) {
|
|
18
|
+
throw new Error('Site directory not found');
|
|
19
|
+
}
|
|
20
|
+
const gitDir = path.join(localPath, '.git');
|
|
21
|
+
if (!fs.existsSync(gitDir)) {
|
|
22
|
+
throw new Error('Not a git repository');
|
|
23
|
+
}
|
|
24
|
+
// Build git log command with date range
|
|
25
|
+
let sinceOption = '--since="30 days ago"';
|
|
26
|
+
if (startDate && endDate) {
|
|
27
|
+
sinceOption = `--since="${startDate}" --before="${endDate}"`;
|
|
28
|
+
}
|
|
29
|
+
else if (startDate) {
|
|
30
|
+
sinceOption = `--since="${startDate}"`;
|
|
31
|
+
}
|
|
32
|
+
else if (endDate) {
|
|
33
|
+
sinceOption = `--before="${endDate}"`;
|
|
34
|
+
}
|
|
35
|
+
// Get git log
|
|
36
|
+
const gitCommand = `git log --oneline ${sinceOption} --pretty=format:"%H|%ad|%s|%an" --date=iso`;
|
|
37
|
+
const { stdout: logOutput } = await execAsync(gitCommand, { cwd: localPath });
|
|
38
|
+
const commits = logOutput
|
|
39
|
+
.trim()
|
|
40
|
+
.split('\n')
|
|
41
|
+
.filter(line => line.trim())
|
|
42
|
+
.map(line => {
|
|
43
|
+
const [hash, date, ...messageParts] = line.split('|');
|
|
44
|
+
const message = messageParts.slice(0, -1).join('|');
|
|
45
|
+
const author = messageParts[messageParts.length - 1];
|
|
46
|
+
return {
|
|
47
|
+
hash,
|
|
48
|
+
date,
|
|
49
|
+
message,
|
|
50
|
+
author
|
|
51
|
+
};
|
|
52
|
+
})
|
|
53
|
+
.filter(commit => !/^\d+\.\d+\.\d+$/.test(commit.message.trim())) // Filter out version-only commits
|
|
54
|
+
.slice(0, (startDate && endDate) ? 100 : 20); // Limit to more commits when date range is specified
|
|
55
|
+
// Try to associate commits with versions
|
|
56
|
+
for (const commit of commits) {
|
|
57
|
+
try {
|
|
58
|
+
const { stdout: tagOutput } = await execAsync(`git describe --tags --contains ${commit.hash} 2>/dev/null || echo ""`, { cwd: localPath });
|
|
59
|
+
if (tagOutput.trim()) {
|
|
60
|
+
commit.version = tagOutput.trim();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Ignore errors for commits not associated with tags
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
commits,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
status: 'success'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
commits: [],
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
status: 'error',
|
|
78
|
+
error: error instanceof Error ? error.message : 'Failed to analyze git health'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Table } from '@pixelated-tech/components';
|
|
4
|
+
import { SiteHealthTemplate } from './site-health-template';
|
|
5
|
+
export function SiteHealthGit({ siteName, startDate, endDate }) {
|
|
6
|
+
const fetchGitData = async (site) => {
|
|
7
|
+
const params = new URLSearchParams({ site: encodeURIComponent(site) });
|
|
8
|
+
if (startDate)
|
|
9
|
+
params.append('startDate', startDate);
|
|
10
|
+
if (endDate)
|
|
11
|
+
params.append('endDate', endDate);
|
|
12
|
+
const response = await fetch(`/api/site-health/git?${params.toString()}`);
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error('Failed to fetch git data');
|
|
15
|
+
}
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
return data;
|
|
18
|
+
};
|
|
19
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Git Push Notes", fetchData: fetchGitData, children: (data) => {
|
|
20
|
+
if (!data || !data.success) {
|
|
21
|
+
return (_jsx("p", { style: { color: '#6b7280' }, children: "No git data available for this site." }));
|
|
22
|
+
}
|
|
23
|
+
if (data.error) {
|
|
24
|
+
return (_jsxs("p", { style: { color: '#ef4444', fontSize: '0.875rem' }, children: ["Error: ", data.error] }));
|
|
25
|
+
}
|
|
26
|
+
// Prepare table data
|
|
27
|
+
const tableData = (data.commits || []).map((commit) => ({
|
|
28
|
+
Date: new Date(commit.date).toLocaleDateString(),
|
|
29
|
+
Message: _jsx("span", { className: "max-w-xs truncate inline-block", title: commit.message, children: commit.message }),
|
|
30
|
+
Version: commit.version ? (_jsx("span", { className: "px-2 py-1 text-xs bg-green-100 text-green-800 rounded", children: commit.version.split('~')[0] })) : (_jsx("span", { className: "text-gray-400", children: "-" }))
|
|
31
|
+
}));
|
|
32
|
+
return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: siteName.replace('-', ' ') }), _jsx("div", { className: "space-y-4", children: tableData.length === 0 ? (_jsx("p", { className: "text-gray-500 text-center py-4", children: "No recent commits found" })) : (_jsx(Table, { id: "git-table", data: tableData, altRowColor: "#DDD" })) }), _jsxs("p", { className: "health-timestamp", children: ["Last checked: ", new Date(data.timestamp).toLocaleString()] })] }));
|
|
33
|
+
} }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Analytics Integration Services
|
|
3
|
+
* Server-side utilities for Google Analytics data retrieval
|
|
4
|
+
*/
|
|
5
|
+
"use server";
|
|
6
|
+
import { RouteCache } from './site-health-cache';
|
|
7
|
+
import { createAnalyticsClient } from './google-api-auth';
|
|
8
|
+
// Cache for analytics data (1 hour)
|
|
9
|
+
const analyticsCache = new RouteCache();
|
|
10
|
+
/**
|
|
11
|
+
* Get Google Analytics data for a site with current/previous period comparison
|
|
12
|
+
*/
|
|
13
|
+
export async function getGoogleAnalyticsData(config, siteName, startDate, endDate) {
|
|
14
|
+
try {
|
|
15
|
+
// Check cache first
|
|
16
|
+
const cacheKey = `analytics-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
|
|
17
|
+
const cached = analyticsCache.get(cacheKey);
|
|
18
|
+
if (cached) {
|
|
19
|
+
return { success: true, data: cached };
|
|
20
|
+
}
|
|
21
|
+
if (!config.ga4PropertyId || config.ga4PropertyId === 'GA4_PROPERTY_ID_HERE') {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: 'GA4 Property ID not configured for this site'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Set up authentication
|
|
28
|
+
const authResult = await createAnalyticsClient(config);
|
|
29
|
+
if (!authResult.success) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: authResult.error || 'Authentication failed'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const analyticsData = authResult.client;
|
|
36
|
+
// Calculate date ranges
|
|
37
|
+
const currentEndDate = endDate ? new Date(endDate) : new Date();
|
|
38
|
+
const currentStartDate = startDate ? new Date(startDate) : new Date(currentEndDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
39
|
+
// Calculate previous period (same duration before the current period)
|
|
40
|
+
const periodDuration = currentEndDate.getTime() - currentStartDate.getTime();
|
|
41
|
+
const previousEndDate = new Date(currentStartDate.getTime() - 24 * 60 * 60 * 1000); // One day before start
|
|
42
|
+
const previousStartDate = new Date(previousEndDate.getTime() - periodDuration);
|
|
43
|
+
const currentStartStr = currentStartDate.toISOString().split('T')[0];
|
|
44
|
+
const currentEndStr = currentEndDate.toISOString().split('T')[0];
|
|
45
|
+
const previousStartStr = previousStartDate.toISOString().split('T')[0];
|
|
46
|
+
const previousEndStr = previousEndDate.toISOString().split('T')[0];
|
|
47
|
+
// Fetch current period data
|
|
48
|
+
const currentResponse = await analyticsData.properties.runReport({
|
|
49
|
+
property: `properties/${config.ga4PropertyId}`,
|
|
50
|
+
requestBody: {
|
|
51
|
+
dateRanges: [{ startDate: currentStartStr, endDate: currentEndStr }],
|
|
52
|
+
dimensions: [{ name: 'date' }],
|
|
53
|
+
metrics: [{ name: 'screenPageViews' }],
|
|
54
|
+
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
// Fetch previous period data
|
|
58
|
+
const previousResponse = await analyticsData.properties.runReport({
|
|
59
|
+
property: `properties/${config.ga4PropertyId}`,
|
|
60
|
+
requestBody: {
|
|
61
|
+
dateRanges: [{ startDate: previousStartStr, endDate: previousEndStr }],
|
|
62
|
+
dimensions: [{ name: 'date' }],
|
|
63
|
+
metrics: [{ name: 'screenPageViews' }],
|
|
64
|
+
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// Create a map of previous period data by date
|
|
68
|
+
const previousDataMap = new Map();
|
|
69
|
+
previousResponse.data.rows?.forEach((row) => {
|
|
70
|
+
const dateStr = row.dimensionValues?.[0]?.value || '';
|
|
71
|
+
if (dateStr) {
|
|
72
|
+
previousDataMap.set(dateStr, parseInt(row.metricValues?.[0]?.value || '0'));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// Combine current and previous period data
|
|
76
|
+
const chartData = [];
|
|
77
|
+
const daysInRange = Math.ceil((currentEndDate.getTime() - currentStartDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
78
|
+
for (let i = daysInRange - 1; i >= 0; i--) {
|
|
79
|
+
const currentDate = new Date(currentEndDate);
|
|
80
|
+
currentDate.setDate(currentDate.getDate() - i);
|
|
81
|
+
const currentDateStr = currentDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
82
|
+
// Calculate corresponding previous period date
|
|
83
|
+
const previousDate = new Date(currentDate.getTime() - periodDuration);
|
|
84
|
+
const previousDateStr = previousDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
85
|
+
// Get current period data
|
|
86
|
+
const currentRow = currentResponse.data.rows?.find((row) => row.dimensionValues?.[0]?.value === currentDateStr);
|
|
87
|
+
const currentPageViews = parseInt(currentRow?.metricValues?.[0]?.value || '0');
|
|
88
|
+
// Get previous period data
|
|
89
|
+
const previousPageViews = previousDataMap.get(previousDateStr) || 0;
|
|
90
|
+
// Format date for display
|
|
91
|
+
const formattedDate = currentDate.toLocaleDateString('en-US', {
|
|
92
|
+
month: 'short',
|
|
93
|
+
day: 'numeric'
|
|
94
|
+
});
|
|
95
|
+
chartData.push({
|
|
96
|
+
date: formattedDate,
|
|
97
|
+
currentPageViews: currentPageViews,
|
|
98
|
+
previousPageViews: previousPageViews,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Cache the result
|
|
102
|
+
analyticsCache.set(cacheKey, chartData);
|
|
103
|
+
return { success: true, data: chartData };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error('Google Analytics error:', error);
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: error.message
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
|
4
|
+
import { SiteHealthTemplate } from './site-health-template';
|
|
5
|
+
export function SiteHealthGoogleAnalytics({ siteName, startDate, endDate }) {
|
|
6
|
+
const fetchAnalyticsData = async (site) => {
|
|
7
|
+
const params = new URLSearchParams({ siteName: site });
|
|
8
|
+
if (startDate)
|
|
9
|
+
params.append('startDate', startDate);
|
|
10
|
+
if (endDate)
|
|
11
|
+
params.append('endDate', endDate);
|
|
12
|
+
const response = await fetch(`/api/site-health/google-analytics?${params.toString()}`);
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`Failed to fetch analytics data: ${response.status}`);
|
|
15
|
+
}
|
|
16
|
+
const result = await response.json();
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
// Handle specific error types
|
|
19
|
+
if (result.error?.includes('invalid_grant') || result.error?.includes('authentication')) {
|
|
20
|
+
throw new Error('Google Analytics authentication expired. Please re-authorize the application.');
|
|
21
|
+
}
|
|
22
|
+
else if (result.error?.includes('GA4 Property ID not configured')) {
|
|
23
|
+
throw new Error('GA4 Property ID not configured for this site');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error(result.error || 'Failed to load analytics data');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result.data;
|
|
30
|
+
};
|
|
31
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Google Analytics", columnSpan: 2, fetchData: fetchAnalyticsData, children: (data) => {
|
|
32
|
+
if (!data || data.length === 0) {
|
|
33
|
+
return (_jsx("div", { className: "flex items-center justify-center h-64", children: _jsx("div", { className: "text-gray-500", children: "No data available for the selected date range" }) }));
|
|
34
|
+
}
|
|
35
|
+
return (_jsx("div", { children: _jsx("div", { style: { width: '100%', height: '400px', border: '1px solid #ddd' }, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(ComposedChart, { data: data, margin: { top: 40, right: 30, left: 20, bottom: 5 }, children: [_jsx("text", { x: "50%", y: 20, textAnchor: "middle", fontSize: "16", fontWeight: "bold", fill: "#374151", children: "Page Views (Current vs Previous Period)" }), _jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "date", tick: { fontSize: 12 }, angle: -45, textAnchor: "end", height: 60 }), _jsx(YAxis, { tick: { fontSize: 12 } }), _jsx(Tooltip, { formatter: (value, name) => [
|
|
36
|
+
value?.toLocaleString() || '0',
|
|
37
|
+
name || 'Unknown'
|
|
38
|
+
], labelFormatter: (label) => `Date: ${label}` }), _jsx(Legend, { wrapperStyle: {
|
|
39
|
+
fontSize: '12px',
|
|
40
|
+
paddingTop: '10px'
|
|
41
|
+
} }), _jsx(Bar, { dataKey: "currentPageViews", fill: "#3b82f6", name: "Current Period", radius: [2, 2, 0, 0] }), _jsx(Line, { type: "monotone", dataKey: "previousPageViews", stroke: "#ef4444", strokeWidth: 2, strokeDasharray: "5 5", dot: { fill: '#ef4444', strokeWidth: 2, r: 3 }, activeDot: { r: 5, stroke: '#ef4444', strokeWidth: 2 }, name: "Previous Period" })] }, `chart-${data.length}`) }) }) }));
|
|
42
|
+
} }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Search Console Integration Services
|
|
3
|
+
* Server-side utilities for Google Search Console data retrieval
|
|
4
|
+
*/
|
|
5
|
+
"use server";
|
|
6
|
+
import { RouteCache } from './site-health-cache';
|
|
7
|
+
import { createSearchConsoleClient } from './google-api-auth';
|
|
8
|
+
// Cache for search console data (1 hour)
|
|
9
|
+
const searchConsoleCache = new RouteCache();
|
|
10
|
+
/**
|
|
11
|
+
* Get Google Search Console data for a site with current/previous period comparison
|
|
12
|
+
*/
|
|
13
|
+
export async function getSearchConsoleData(config, siteName, startDate, endDate) {
|
|
14
|
+
try {
|
|
15
|
+
// Check cache first
|
|
16
|
+
const cacheKey = `searchconsole-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
|
|
17
|
+
const cached = searchConsoleCache.get(cacheKey);
|
|
18
|
+
if (cached) {
|
|
19
|
+
return { success: true, data: cached };
|
|
20
|
+
}
|
|
21
|
+
if (!config.siteUrl) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: 'Site URL not configured for Search Console'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Set up authentication
|
|
28
|
+
const authResult = await createSearchConsoleClient(config);
|
|
29
|
+
if (!authResult.success) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: authResult.error || 'Authentication failed'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const searchconsole = authResult.client;
|
|
36
|
+
// Calculate date ranges
|
|
37
|
+
const currentEndDate = endDate ? new Date(endDate) : new Date();
|
|
38
|
+
const currentStartDate = startDate ? new Date(startDate) : new Date(currentEndDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
39
|
+
// Calculate previous period (same duration before the current period)
|
|
40
|
+
const periodDuration = currentEndDate.getTime() - currentStartDate.getTime();
|
|
41
|
+
const previousEndDate = new Date(currentStartDate.getTime() - 24 * 60 * 60 * 1000); // One day before start
|
|
42
|
+
const previousStartDate = new Date(previousEndDate.getTime() - periodDuration);
|
|
43
|
+
const currentStartStr = currentStartDate.toISOString().split('T')[0];
|
|
44
|
+
const currentEndStr = currentEndDate.toISOString().split('T')[0];
|
|
45
|
+
const previousStartStr = previousStartDate.toISOString().split('T')[0];
|
|
46
|
+
const previousEndStr = previousEndDate.toISOString().split('T')[0];
|
|
47
|
+
// Fetch current period data
|
|
48
|
+
const currentResponse = await searchconsole.searchanalytics.query({
|
|
49
|
+
siteUrl: config.siteUrl,
|
|
50
|
+
requestBody: {
|
|
51
|
+
startDate: currentStartStr,
|
|
52
|
+
endDate: currentEndStr,
|
|
53
|
+
dimensions: ['date'],
|
|
54
|
+
rowLimit: 10000,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
// Fetch previous period data
|
|
58
|
+
const previousResponse = await searchconsole.searchanalytics.query({
|
|
59
|
+
siteUrl: config.siteUrl,
|
|
60
|
+
requestBody: {
|
|
61
|
+
startDate: previousStartStr,
|
|
62
|
+
endDate: previousEndStr,
|
|
63
|
+
dimensions: ['date'],
|
|
64
|
+
rowLimit: 10000,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// Create a map of previous period data by date
|
|
68
|
+
const previousDataMap = new Map();
|
|
69
|
+
previousResponse.data.rows?.forEach((row) => {
|
|
70
|
+
const dateStr = row.keys?.[0] || '';
|
|
71
|
+
if (dateStr) {
|
|
72
|
+
previousDataMap.set(dateStr, {
|
|
73
|
+
clicks: parseFloat(String(row.clicks || '0')),
|
|
74
|
+
impressions: parseFloat(String(row.impressions || '0'))
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Combine current and previous period data
|
|
79
|
+
const chartData = [];
|
|
80
|
+
const daysInRange = Math.ceil((currentEndDate.getTime() - currentStartDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
81
|
+
for (let i = daysInRange - 1; i >= 0; i--) {
|
|
82
|
+
const currentDate = new Date(currentEndDate);
|
|
83
|
+
currentDate.setDate(currentDate.getDate() - i);
|
|
84
|
+
const currentDateStr = currentDate.toISOString().split('T')[0];
|
|
85
|
+
// Calculate corresponding previous period date
|
|
86
|
+
const previousDate = new Date(currentDate.getTime() - periodDuration);
|
|
87
|
+
const previousDateStr = previousDate.toISOString().split('T')[0];
|
|
88
|
+
// Get current period data
|
|
89
|
+
const currentRow = currentResponse.data.rows?.find((row) => row.keys?.[0] === currentDateStr);
|
|
90
|
+
const currentClicks = parseFloat(String(currentRow?.clicks || '0'));
|
|
91
|
+
const currentImpressions = parseFloat(String(currentRow?.impressions || '0'));
|
|
92
|
+
// Get previous period data
|
|
93
|
+
const previousData = previousDataMap.get(previousDateStr) || { clicks: 0, impressions: 0 };
|
|
94
|
+
// Format date for display
|
|
95
|
+
const formattedDate = currentDate.toLocaleDateString('en-US', {
|
|
96
|
+
month: 'short',
|
|
97
|
+
day: 'numeric'
|
|
98
|
+
});
|
|
99
|
+
chartData.push({
|
|
100
|
+
date: formattedDate,
|
|
101
|
+
currentImpressions: Math.round(currentImpressions),
|
|
102
|
+
currentClicks: Math.round(currentClicks),
|
|
103
|
+
previousImpressions: Math.round(previousData.impressions),
|
|
104
|
+
previousClicks: Math.round(previousData.clicks),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Cache the result
|
|
108
|
+
searchConsoleCache.set(cacheKey, chartData);
|
|
109
|
+
return { success: true, data: chartData };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error('Google Search Console error:', error);
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: error.message
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|