@pixelated-tech/components 3.5.13 → 3.6.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.
Files changed (61) hide show
  1. package/README.md +2 -0
  2. package/dist/components/admin/site-health/google.api.integration.js +258 -0
  3. package/dist/components/admin/site-health/google.api.utils.js +47 -0
  4. package/dist/components/admin/site-health/site-health-accessibility.js +5 -103
  5. package/dist/components/admin/site-health/site-health-axe-core.js +4 -10
  6. package/dist/components/admin/site-health/site-health-cloudwatch.js +24 -26
  7. package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +4 -10
  8. package/dist/components/admin/site-health/site-health-github.js +9 -15
  9. package/dist/components/admin/site-health/site-health-google-analytics.integration.js +1 -107
  10. package/dist/components/admin/site-health/site-health-google-analytics.js +21 -29
  11. package/dist/components/admin/site-health/site-health-google-search-console.integration.js +1 -113
  12. package/dist/components/admin/site-health/site-health-google-search-console.js +22 -28
  13. package/dist/components/admin/site-health/site-health-on-site-seo.js +8 -82
  14. package/dist/components/admin/site-health/site-health-overview.js +5 -19
  15. package/dist/components/admin/site-health/site-health-performance.js +5 -167
  16. package/dist/components/admin/site-health/site-health-security.js +7 -148
  17. package/dist/components/admin/site-health/site-health-seo.js +5 -103
  18. package/dist/components/admin/site-health/site-health-template.js +68 -43
  19. package/dist/components/admin/site-health/site-health-uptime.js +4 -9
  20. package/dist/components/admin/site-health/site-health-utils.js +170 -0
  21. package/dist/components/admin/site-health/site-health.css +8 -9
  22. package/dist/index.adminclient.js +2 -5
  23. package/dist/index.adminserver.js +6 -4
  24. package/dist/index.js +39 -45
  25. package/dist/types/components/admin/site-health/google.api.integration.d.ts +82 -0
  26. package/dist/types/components/admin/site-health/google.api.integration.d.ts.map +1 -0
  27. package/dist/types/components/admin/site-health/google.api.utils.d.ts +32 -0
  28. package/dist/types/components/admin/site-health/google.api.utils.d.ts.map +1 -0
  29. package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -1
  30. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -1
  31. package/dist/types/components/admin/site-health/site-health-cloudwatch.d.ts.map +1 -1
  32. package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -1
  33. package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -1
  34. package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -1
  35. package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts +1 -21
  36. package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts.map +1 -1
  37. package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -1
  38. package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts +1 -41
  39. package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts.map +1 -1
  40. package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -1
  41. package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -1
  42. package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -1
  43. package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -1
  44. package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -1
  45. package/dist/types/components/admin/site-health/site-health-template.d.ts +8 -1
  46. package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
  47. package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -1
  48. package/dist/types/components/admin/site-health/site-health-utils.d.ts +17 -0
  49. package/dist/types/components/admin/site-health/site-health-utils.d.ts.map +1 -0
  50. package/dist/types/index.adminclient.d.ts +2 -5
  51. package/dist/types/index.adminserver.d.ts +6 -4
  52. package/dist/types/index.d.ts +38 -44
  53. package/dist/types/stories/admin/site-health.stories.d.ts.map +1 -1
  54. package/dist/types/tests/google.api.integration.test.d.ts +2 -0
  55. package/dist/types/tests/google.api.integration.test.d.ts.map +1 -0
  56. package/dist/types/tests/google.api.utils.test.d.ts +5 -0
  57. package/dist/types/tests/google.api.utils.test.d.ts.map +1 -0
  58. package/package.json +1 -1
  59. package/dist/components/admin/site-health/google-api-auth.js +0 -69
  60. package/dist/types/components/admin/site-health/google-api-auth.d.ts +0 -37
  61. package/dist/types/components/admin/site-health/google-api-auth.d.ts.map +0 -1
package/README.md CHANGED
@@ -271,6 +271,8 @@ import { Accordion, Callout } from '@pixelated-tech/components';
271
271
 
272
272
  For detailed usage examples and API documentation, see the [Component Reference Guide](docs/components.md).
273
273
 
274
+ For administrative components and site management features, see the [Admin Components Guide](docs/admin.md).
275
+
274
276
  ### Storybook Interactive Demos
275
277
 
276
278
  Explore all components with live, interactive examples:
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Google API Integration Services
3
+ * Centralized integration for Google services (Analytics, Search Console, etc.)
4
+ * Combines authentication, caching, and data processing logic
5
+ */
6
+ "use server";
7
+ import { google } from 'googleapis';
8
+ import { RouteCache } from './site-health-cache';
9
+ import { calculateDateRanges, formatChartDate, getCachedData, setCachedData } from './google.api.utils';
10
+ /**
11
+ * Create authenticated Google API client for a specific service
12
+ */
13
+ export async function createGoogleAuthClient(config, scopes) {
14
+ try {
15
+ let auth;
16
+ if (config.serviceAccountKey) {
17
+ // Use service account authentication (recommended)
18
+ const credentials = JSON.parse(config.serviceAccountKey);
19
+ auth = new google.auth.GoogleAuth({
20
+ credentials,
21
+ scopes,
22
+ });
23
+ }
24
+ else if (config.clientId && config.clientSecret && config.refreshToken) {
25
+ // Fallback to OAuth2 (deprecated for server-side apps)
26
+ const oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret);
27
+ oauth2Client.setCredentials({
28
+ refresh_token: config.refreshToken,
29
+ });
30
+ auth = oauth2Client;
31
+ }
32
+ else {
33
+ return {
34
+ success: false,
35
+ error: 'Google credentials not configured. Set GOOGLE_SERVICE_ACCOUNT_KEY or OAuth credentials.'
36
+ };
37
+ }
38
+ return { success: true, auth };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ success: false,
43
+ error: `Authentication failed: ${error.message}`
44
+ };
45
+ }
46
+ }
47
+ /**
48
+ * Create Analytics Data API client
49
+ */
50
+ export async function createAnalyticsClient(config) {
51
+ const result = await createGoogleAuthClient(config, ['https://www.googleapis.com/auth/analytics.readonly']);
52
+ if (!result.success)
53
+ return result;
54
+ return {
55
+ success: true,
56
+ client: google.analyticsdata({ version: 'v1beta', auth: result.auth }),
57
+ auth: result.auth
58
+ };
59
+ }
60
+ /**
61
+ * Create Search Console API client
62
+ */
63
+ export async function createSearchConsoleClient(config) {
64
+ const result = await createGoogleAuthClient(config, ['https://www.googleapis.com/auth/webmasters.readonly']);
65
+ if (!result.success)
66
+ return result;
67
+ return {
68
+ success: true,
69
+ client: google.searchconsole({ version: 'v1', auth: result.auth }),
70
+ auth: result.auth
71
+ };
72
+ }
73
+ // Cache for analytics data (1 hour)
74
+ const analyticsCache = new RouteCache();
75
+ /**
76
+ * Get Google Analytics data for a site with current/previous period comparison
77
+ */
78
+ export async function getGoogleAnalyticsData(config, siteName, startDate, endDate) {
79
+ try {
80
+ // Check cache first
81
+ const cacheKey = `analytics-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
82
+ const cached = getCachedData(analyticsCache, cacheKey);
83
+ if (cached) {
84
+ return { success: true, data: cached };
85
+ }
86
+ if (!config.ga4PropertyId || config.ga4PropertyId === 'GA4_PROPERTY_ID_HERE') {
87
+ return {
88
+ success: false,
89
+ error: 'GA4 Property ID not configured for this site'
90
+ };
91
+ }
92
+ // Set up authentication
93
+ const authResult = await createAnalyticsClient(config);
94
+ if (!authResult.success) {
95
+ return {
96
+ success: false,
97
+ error: authResult.error || 'Authentication failed'
98
+ };
99
+ }
100
+ const analyticsData = authResult.client;
101
+ const dateRange = calculateDateRanges(startDate, endDate);
102
+ // Fetch current period data
103
+ const currentResponse = await analyticsData.properties.runReport({
104
+ property: `properties/${config.ga4PropertyId}`,
105
+ requestBody: {
106
+ dateRanges: [{ startDate: dateRange.currentStartStr, endDate: dateRange.currentEndStr }],
107
+ dimensions: [{ name: 'date' }],
108
+ metrics: [{ name: 'screenPageViews' }],
109
+ orderBys: [{ dimension: { dimensionName: 'date' } }],
110
+ },
111
+ });
112
+ // Fetch previous period data
113
+ const previousResponse = await analyticsData.properties.runReport({
114
+ property: `properties/${config.ga4PropertyId}`,
115
+ requestBody: {
116
+ dateRanges: [{ startDate: dateRange.previousStartStr, endDate: dateRange.previousEndStr }],
117
+ dimensions: [{ name: 'date' }],
118
+ metrics: [{ name: 'screenPageViews' }],
119
+ orderBys: [{ dimension: { dimensionName: 'date' } }],
120
+ },
121
+ });
122
+ // Create a map of previous period data by date
123
+ const previousDataMap = new Map();
124
+ previousResponse.data.rows?.forEach((row) => {
125
+ const dateStr = row.dimensionValues?.[0]?.value || '';
126
+ if (dateStr) {
127
+ previousDataMap.set(dateStr, parseInt(row.metricValues?.[0]?.value || '0'));
128
+ }
129
+ });
130
+ // Combine current and previous period data
131
+ const chartData = [];
132
+ const daysInRange = Math.ceil((dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()) / (24 * 60 * 60 * 1000));
133
+ for (let i = daysInRange - 1; i >= 0; i--) {
134
+ const currentDate = new Date(dateRange.currentEnd);
135
+ currentDate.setDate(currentDate.getDate() - i);
136
+ const currentDateStr = currentDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
137
+ // Calculate corresponding previous period date
138
+ const previousDate = new Date(currentDate.getTime() - (dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()));
139
+ const previousDateStr = previousDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
140
+ // Get current period data
141
+ const currentRow = currentResponse.data.rows?.find((row) => row.dimensionValues?.[0]?.value === currentDateStr);
142
+ const currentPageViews = parseInt(currentRow?.metricValues?.[0]?.value || '0');
143
+ // Get previous period data
144
+ const previousPageViews = previousDataMap.get(previousDateStr) || 0;
145
+ chartData.push({
146
+ date: formatChartDate(currentDate),
147
+ currentPageViews: currentPageViews,
148
+ previousPageViews: previousPageViews,
149
+ });
150
+ }
151
+ // Cache the result
152
+ setCachedData(analyticsCache, cacheKey, chartData);
153
+ return { success: true, data: chartData };
154
+ }
155
+ catch (error) {
156
+ console.error('Google Analytics error:', error);
157
+ return {
158
+ success: false,
159
+ error: error.message
160
+ };
161
+ }
162
+ }
163
+ // Cache for search console data (1 hour)
164
+ const searchConsoleCache = new RouteCache();
165
+ /**
166
+ * Get Google Search Console data for a site with current/previous period comparison
167
+ */
168
+ export async function getSearchConsoleData(config, siteName, startDate, endDate) {
169
+ try {
170
+ // Check cache first
171
+ const cacheKey = `searchconsole-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
172
+ const cached = getCachedData(searchConsoleCache, cacheKey);
173
+ if (cached) {
174
+ return { success: true, data: cached };
175
+ }
176
+ if (!config.siteUrl) {
177
+ return {
178
+ success: false,
179
+ error: 'Site URL not configured for Search Console'
180
+ };
181
+ }
182
+ // Set up authentication
183
+ const authResult = await createSearchConsoleClient(config);
184
+ if (!authResult.success) {
185
+ return {
186
+ success: false,
187
+ error: authResult.error || 'Authentication failed'
188
+ };
189
+ }
190
+ const searchconsole = authResult.client;
191
+ const dateRange = calculateDateRanges(startDate, endDate);
192
+ // Fetch current period data
193
+ const currentResponse = await searchconsole.searchanalytics.query({
194
+ siteUrl: config.siteUrl,
195
+ requestBody: {
196
+ startDate: dateRange.currentStartStr,
197
+ endDate: dateRange.currentEndStr,
198
+ dimensions: ['date'],
199
+ rowLimit: 10000,
200
+ },
201
+ });
202
+ // Fetch previous period data
203
+ const previousResponse = await searchconsole.searchanalytics.query({
204
+ siteUrl: config.siteUrl,
205
+ requestBody: {
206
+ startDate: dateRange.previousStartStr,
207
+ endDate: dateRange.previousEndStr,
208
+ dimensions: ['date'],
209
+ rowLimit: 10000,
210
+ },
211
+ });
212
+ // Create a map of previous period data by date
213
+ const previousDataMap = new Map();
214
+ previousResponse.data.rows?.forEach((row) => {
215
+ const dateStr = row.keys?.[0] || '';
216
+ if (dateStr) {
217
+ previousDataMap.set(dateStr, {
218
+ clicks: parseFloat(String(row.clicks || '0')),
219
+ impressions: parseFloat(String(row.impressions || '0'))
220
+ });
221
+ }
222
+ });
223
+ // Combine current and previous period data
224
+ const chartData = [];
225
+ const daysInRange = Math.ceil((dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()) / (24 * 60 * 60 * 1000));
226
+ for (let i = daysInRange - 1; i >= 0; i--) {
227
+ const currentDate = new Date(dateRange.currentEnd);
228
+ currentDate.setDate(currentDate.getDate() - i);
229
+ const currentDateStr = currentDate.toISOString().split('T')[0];
230
+ // Calculate corresponding previous period date
231
+ const previousDate = new Date(currentDate.getTime() - (dateRange.currentEnd.getTime() - dateRange.currentStart.getTime()));
232
+ const previousDateStr = previousDate.toISOString().split('T')[0];
233
+ // Get current period data
234
+ const currentRow = currentResponse.data.rows?.find((row) => row.keys?.[0] === currentDateStr);
235
+ const currentClicks = parseFloat(String(currentRow?.clicks || '0'));
236
+ const currentImpressions = parseFloat(String(currentRow?.impressions || '0'));
237
+ // Get previous period data
238
+ const previousData = previousDataMap.get(previousDateStr) || { clicks: 0, impressions: 0 };
239
+ chartData.push({
240
+ date: formatChartDate(currentDate),
241
+ currentImpressions: Math.round(currentImpressions),
242
+ currentClicks: Math.round(currentClicks),
243
+ previousImpressions: Math.round(previousData.impressions),
244
+ previousClicks: Math.round(previousData.clicks),
245
+ });
246
+ }
247
+ // Cache the result
248
+ setCachedData(searchConsoleCache, cacheKey, chartData);
249
+ return { success: true, data: chartData };
250
+ }
251
+ catch (error) {
252
+ console.error('Google Search Console error:', error);
253
+ return {
254
+ success: false,
255
+ error: error.message
256
+ };
257
+ }
258
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Google API Integration Utilities
3
+ * Shared utility functions for Google API integrations
4
+ * These are NOT server actions - just regular utility functions
5
+ */
6
+ /**
7
+ * Calculate date ranges for current and previous periods
8
+ */
9
+ export function calculateDateRanges(startDate, endDate) {
10
+ const currentEndDate = endDate ? new Date(endDate) : new Date();
11
+ const currentStartDate = startDate ? new Date(startDate) : new Date(currentEndDate.getTime() - 30 * 24 * 60 * 60 * 1000);
12
+ // Calculate previous period (same duration before the current period)
13
+ const periodDuration = currentEndDate.getTime() - currentStartDate.getTime();
14
+ const previousEndDate = new Date(currentStartDate.getTime() - 24 * 60 * 60 * 1000); // One day before start
15
+ const previousStartDate = new Date(previousEndDate.getTime() - periodDuration);
16
+ return {
17
+ currentStart: currentStartDate,
18
+ currentEnd: currentEndDate,
19
+ previousStart: previousStartDate,
20
+ previousEnd: previousEndDate,
21
+ currentStartStr: currentStartDate.toISOString().split('T')[0],
22
+ currentEndStr: currentEndDate.toISOString().split('T')[0],
23
+ previousStartStr: previousStartDate.toISOString().split('T')[0],
24
+ previousEndStr: previousEndDate.toISOString().split('T')[0],
25
+ };
26
+ }
27
+ /**
28
+ * Format date for chart display
29
+ */
30
+ export function formatChartDate(date) {
31
+ return date.toLocaleDateString('en-US', {
32
+ month: 'short',
33
+ day: 'numeric'
34
+ });
35
+ }
36
+ /**
37
+ * Get cached data or null if not cached
38
+ */
39
+ export function getCachedData(cache, cacheKey) {
40
+ return cache.get(cacheKey);
41
+ }
42
+ /**
43
+ * Set cached data
44
+ */
45
+ export function setCachedData(cache, cacheKey, data) {
46
+ cache.set(cacheKey, data);
47
+ }
@@ -1,114 +1,16 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback } from 'react';
4
3
  import PropTypes from 'prop-types';
5
4
  import { SiteHealthTemplate } from './site-health-template';
6
- import { getScoreIndicator } from './site-health-indicators';
5
+ import { formatAuditItem, getAuditScoreIcon, getScoreColor } from './site-health-utils';
7
6
  SiteHealthAccessibility.propTypes = {
8
7
  siteName: PropTypes.string.isRequired,
9
8
  };
10
9
  export function SiteHealthAccessibility({ siteName }) {
11
- const fetchAccessibilityData = useCallback(async (site) => {
12
- const response = await fetch(`/api/site-health/core-web-vitals?siteName=${encodeURIComponent(site)}`);
13
- const result = await response.json();
14
- if (!result.success) {
15
- throw new Error(result.error || 'Failed to fetch accessibility data');
16
- }
17
- return result;
18
- }, []);
19
- return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Accessibility", fetchData: fetchAccessibilityData, children: (data) => {
20
- const getScoreColor = (score) => {
21
- return getScoreIndicator(score).color;
22
- };
23
- const getAuditScoreIcon = (score) => {
24
- return getScoreIndicator(score).icon;
25
- };
26
- // Helper function to display audit item details
27
- const formatAuditItem = (item, auditTitle) => {
28
- // Handle URLs
29
- if (item.url && typeof item.url === 'string') {
30
- return item.url;
31
- }
32
- // Handle sources (like JavaScript files)
33
- if (item.source && typeof item.source === 'string') {
34
- return item.source;
35
- }
36
- // Handle text descriptions
37
- if (item.text && typeof item.text === 'string') {
38
- return item.text;
39
- }
40
- // Handle entities (like "Google Tag Manager")
41
- if (item.entity && typeof item.entity === 'string') {
42
- return item.entity;
43
- }
44
- // Handle nodes with selectors
45
- if (item.node && typeof item.node === 'object' && 'selector' in item.node) {
46
- return `Element: ${item.node.selector}`;
47
- }
48
- // Handle nodes with snippets
49
- if (item.node && typeof item.node === 'object' && 'snippet' in item.node) {
50
- const snippet = item.node.snippet;
51
- return `Element: ${snippet.length > 50 ? snippet.substring(0, 50) + '...' : snippet}`;
52
- }
53
- // Handle origins (like domains)
54
- if (item.origin && typeof item.origin === 'string') {
55
- return item.origin;
56
- }
57
- // Handle labels
58
- if (item.label && typeof item.label === 'string') {
59
- return item.label;
60
- }
61
- // Handle numeric values with units
62
- if (item.value && typeof item.value === 'object' && 'type' in item.value && item.value.type === 'numeric') {
63
- const value = item.value;
64
- return `${value.value}${item.unit || ''}`;
65
- }
66
- // Handle statistics
67
- if (item.statistic && typeof item.statistic === 'string' && item.value) {
68
- if (typeof item.value === 'object' && 'type' in item.value && item.value.type === 'numeric') {
69
- const value = item.value;
70
- return `${item.statistic}: ${value.value}`;
71
- }
72
- return item.statistic;
73
- }
74
- // Handle timing data with audit context
75
- if (typeof item === 'number') {
76
- let context = '';
77
- if (auditTitle) {
78
- if (auditTitle.toLowerCase().includes('server') || auditTitle.toLowerCase().includes('backend')) {
79
- context = ' server response';
80
- }
81
- else if (auditTitle.toLowerCase().includes('network') || auditTitle.toLowerCase().includes('request')) {
82
- context = ' network request';
83
- }
84
- else if (auditTitle.toLowerCase().includes('render') || auditTitle.toLowerCase().includes('blocking')) {
85
- context = ' render blocking';
86
- }
87
- else if (auditTitle.toLowerCase().includes('javascript') || auditTitle.toLowerCase().includes('js')) {
88
- context = ' JavaScript';
89
- }
90
- else if (auditTitle.toLowerCase().includes('image') || auditTitle.toLowerCase().includes('media')) {
91
- context = ' media resource';
92
- }
93
- }
94
- return `${item.toFixed(2)}ms${context}`;
95
- }
96
- if (item.value && typeof item.value === 'number') {
97
- const unit = item.unit || 'ms';
98
- let context = '';
99
- if (auditTitle && unit === 'ms') {
100
- if (auditTitle.toLowerCase().includes('server')) {
101
- context = ' server time';
102
- }
103
- else if (auditTitle.toLowerCase().includes('network')) {
104
- context = ' network time';
105
- }
106
- }
107
- return `${item.value.toFixed(2)}${unit}${context}`;
108
- }
109
- // If we can't find anything meaningful, show a generic message
110
- return 'Details available';
111
- };
10
+ return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Accessibility", endpoint: {
11
+ endpoint: '/api/site-health/core-web-vitals',
12
+ responseTransformer: (result) => result, // Result is already in the correct format
13
+ }, children: (data) => {
112
14
  if (!data?.data || data.data.length === 0) {
113
15
  return (_jsx("p", { style: { color: '#6b7280' }, children: "No accessibility data available for this site." }));
114
16
  }
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback } from 'react';
4
3
  import PropTypes from 'prop-types';
5
4
  import { SiteHealthTemplate } from './site-health-template';
6
5
  import { getImpactIndicator, getIncompleteIndicator, getPassingIndicator } from './site-health-indicators';
@@ -8,14 +7,6 @@ SiteHealthAxeCore.propTypes = {
8
7
  siteName: PropTypes.string.isRequired,
9
8
  };
10
9
  export function SiteHealthAxeCore({ siteName }) {
11
- const fetchAxeCoreData = useCallback(async (site) => {
12
- const response = await fetch(`/api/site-health/axe-core?siteName=${encodeURIComponent(site)}`);
13
- const result = await response.json();
14
- if (!result.success) {
15
- throw new Error(result.error || 'Failed to fetch axe-core data');
16
- }
17
- return result;
18
- }, []);
19
10
  const getImpactColor = (impact) => {
20
11
  return getImpactIndicator(impact).color;
21
12
  };
@@ -34,7 +25,10 @@ export function SiteHealthAxeCore({ siteName }) {
34
25
  }
35
26
  return 'Unknown element';
36
27
  };
37
- return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Axe-Core Accessibility", fetchData: fetchAxeCoreData, children: (data) => {
28
+ return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Axe-Core Accessibility", endpoint: {
29
+ endpoint: '/api/site-health/axe-core',
30
+ responseTransformer: (result) => result, // Result is already in the correct format
31
+ }, children: (data) => {
38
32
  if (!data?.data || data.data.length === 0) {
39
33
  return (_jsx("p", { style: { color: '#6b7280' }, children: "No axe-core data available for this site." }));
40
34
  }
@@ -9,42 +9,40 @@ SiteHealthCloudwatch.propTypes = {
9
9
  endDate: PropTypes.string,
10
10
  };
11
11
  export function SiteHealthCloudwatch({ siteName, startDate, endDate }) {
12
- const fetchCloudwatchData = async (site) => {
13
- const params = new URLSearchParams({ siteName: site });
14
- if (startDate)
15
- params.append('startDate', startDate);
16
- if (endDate)
17
- params.append('endDate', endDate);
18
- const response = await fetch(`/api/site-health/cloudwatch?${params.toString()}`);
19
- if (!response.ok) {
20
- throw new Error(`Failed to fetch CloudWatch data: ${response.status}`);
21
- }
22
- const result = await response.json();
23
- if (!result.success) {
24
- if (result.error?.includes('Health Check ID not configured')) {
25
- throw new Error('Route53 Health Check ID not configured for this site');
26
- }
27
- else {
28
- throw new Error(result.error || 'Failed to load CloudWatch health check data');
29
- }
30
- }
31
- return result.data;
32
- };
33
- return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "CloudWatch Uptime", columnSpan: 2, fetchData: fetchCloudwatchData, children: (data) => {
34
- if (!data || data.length === 0) {
12
+ return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "CloudWatch Uptime", columnSpan: 2, endpoint: {
13
+ endpoint: '/api/site-health/cloudwatch',
14
+ params: {
15
+ ...(startDate && { startDate }),
16
+ ...(endDate && { endDate }),
17
+ },
18
+ responseTransformer: (result) => result.data, // Extract the data array from the response
19
+ }, children: (data) => {
20
+ // Ensure data is an array
21
+ if (!data || !Array.isArray(data) || data.length === 0) {
35
22
  return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "No uptime data available. Route53 health checks may not be configured to send metrics to CloudWatch." }) }));
36
23
  }
37
24
  // Check if all data points have zero checks (no actual data)
38
- const hasActualData = data.some((point) => point.totalChecks > 0);
25
+ const hasActualData = data.some((point) => point && typeof point === 'object' && point.totalChecks > 0);
39
26
  if (!hasActualData) {
40
27
  return (_jsx("div", { className: "health-visualization-placeholder", children: _jsxs("div", { className: "health-text-secondary", children: ["Health check exists but has no metric data in CloudWatch for the selected period.", _jsx("br", {}), "Route53 health checks must be configured to send metrics to CloudWatch for historical data."] }) }));
41
28
  }
42
- 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: "CloudWatch Health Check Availability Over Time" }), _jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "date", tick: { fontSize: 12 }, angle: -45, textAnchor: "end", height: 60 }), _jsx(YAxis, { tick: { fontSize: 12 }, label: { value: 'Check Count', angle: -90, position: 'insideLeft' } }), _jsx(Tooltip, { formatter: (value, name) => [
29
+ // Filter out any invalid data points
30
+ const validData = data.filter((point) => point &&
31
+ typeof point === 'object' &&
32
+ typeof point.date === 'string' &&
33
+ typeof point.successCount === 'number' &&
34
+ typeof point.failureCount === 'number' &&
35
+ typeof point.totalChecks === 'number' &&
36
+ typeof point.successRate === 'number');
37
+ if (validData.length === 0) {
38
+ return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "Invalid data format received from CloudWatch API." }) }));
39
+ }
40
+ 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: validData, 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: "CloudWatch Health Check Availability Over Time" }), _jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "date", tick: { fontSize: 12 }, angle: -45, textAnchor: "end", height: 60 }), _jsx(YAxis, { tick: { fontSize: 12 }, label: { value: 'Check Count', angle: -90, position: 'insideLeft' } }), _jsx(Tooltip, { formatter: (value, name) => [
43
41
  value?.toLocaleString() || '0',
44
42
  name || 'Unknown'
45
43
  ], labelFormatter: (label) => `Date: ${label}` }), _jsx(Legend, { wrapperStyle: {
46
44
  fontSize: '12px',
47
45
  paddingTop: '10px'
48
- } }), _jsx(Bar, { dataKey: "successCount", stackId: "checks", fill: "#10b981", name: "Successful Checks", radius: [2, 2, 0, 0] }), _jsx(Bar, { dataKey: "failureCount", stackId: "checks", fill: "#ef4444", name: "Failed Checks", radius: [2, 2, 0, 0] })] }, `cloudwatch-chart-${data.length}`) }) }) }));
46
+ } }), _jsx(Bar, { dataKey: "successCount", stackId: "checks", fill: "#10b981", name: "Successful Checks", radius: [2, 2, 0, 0] }), _jsx(Bar, { dataKey: "failureCount", stackId: "checks", fill: "#ef4444", name: "Failed Checks", radius: [2, 2, 0, 0] })] }, `cloudwatch-chart-${validData.length}`) }) }) }));
49
47
  } }));
50
48
  }
@@ -1,21 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useCallback } from 'react';
4
3
  import PropTypes from 'prop-types';
5
4
  import { SiteHealthTemplate } from './site-health-template';
6
5
  SiteHealthDependencyVulnerabilities.propTypes = {
7
6
  siteName: PropTypes.string.isRequired,
8
7
  };
9
8
  export function SiteHealthDependencyVulnerabilities({ siteName }) {
10
- const fetchDependencyData = useCallback(async (site) => {
11
- const response = await fetch(`/api/site-health/security?siteName=${encodeURIComponent(site)}`);
12
- const result = await response.json();
13
- if (!result.success) {
14
- throw new Error(result.error || 'Failed to fetch dependency data');
15
- }
16
- return result;
17
- }, []);
18
- return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Dependency Vulnerability", fetchData: fetchDependencyData, children: (data) => {
9
+ return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Dependency Vulnerability", endpoint: {
10
+ endpoint: '/api/site-health/security',
11
+ responseTransformer: (result) => result, // Result is already in the correct format
12
+ }, children: (data) => {
19
13
  if (!data) {
20
14
  return (_jsx("p", { style: { color: '#6b7280' }, children: "No dependency data available for this site." }));
21
15
  }
@@ -9,20 +9,14 @@ SiteHealthGit.propTypes = {
9
9
  endDate: PropTypes.string,
10
10
  };
11
11
  export function SiteHealthGit({ siteName, startDate, endDate }) {
12
- const fetchGitData = async (site) => {
13
- const params = new URLSearchParams({ siteName: encodeURIComponent(site) });
14
- if (startDate)
15
- params.append('startDate', startDate);
16
- if (endDate)
17
- params.append('endDate', endDate);
18
- const response = await fetch(`/api/site-health/github?${params.toString()}`);
19
- if (!response.ok) {
20
- throw new Error('Failed to fetch git data');
21
- }
22
- const data = await response.json();
23
- return data;
24
- };
25
- return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Git Push Notes", fetchData: fetchGitData, children: (data) => {
12
+ return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Git Push Notes", endpoint: {
13
+ endpoint: '/api/site-health/github',
14
+ params: {
15
+ ...(startDate && { startDate }),
16
+ ...(endDate && { endDate }),
17
+ },
18
+ responseTransformer: (result) => result, // Result is already in the correct format
19
+ }, children: (data) => {
26
20
  if (!data || !data.success) {
27
21
  return (_jsx("p", { style: { color: '#6b7280' }, children: "No git data available for this site." }));
28
22
  }
@@ -32,7 +26,7 @@ export function SiteHealthGit({ siteName, startDate, endDate }) {
32
26
  // Prepare table data
33
27
  const tableData = (data.commits || []).map((commit) => ({
34
28
  Date: new Date(commit.date).toLocaleDateString(),
35
- Message: _jsx("span", { className: "health-truncate-text", title: commit.message, children: commit.message }),
29
+ Message: _jsx("span", { title: commit.message, children: commit.message }),
36
30
  Version: commit.version ? (_jsx("span", { className: "health-version-tag", children: commit.version.split('~')[0] })) : (_jsx("span", { className: "health-text-muted", children: "-" }))
37
31
  }));
38
32
  return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: siteName.replace('-', ' ') }), _jsx("div", { className: "health-section-list", children: tableData.length === 0 ? (_jsx("p", { className: "health-empty-state", 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()] })] }));