diff --git a/application/src/components/public/ComponentsStatusSection.tsx b/application/src/components/public/ComponentsStatusSection.tsx new file mode 100644 index 0000000..611a946 --- /dev/null +++ b/application/src/components/public/ComponentsStatusSection.tsx @@ -0,0 +1,141 @@ + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Server } from 'lucide-react'; +import { StatusPageComponentRecord } from '@/types/statusPageComponents.types'; +import { Service, UptimeData } from '@/types/service.types'; +import { UptimeHistoryRenderer } from './UptimeHistoryRenderer'; + +interface ComponentsStatusSectionProps { + components: StatusPageComponentRecord[]; + services: Service[]; + uptimeData: Record; +} + +export const ComponentsStatusSection = ({ components, services, uptimeData }: ComponentsStatusSectionProps) => { + const getServiceForComponent = (component: StatusPageComponentRecord) => { + return services.find(service => service.id === component.service_id); + }; + + const getComponentStatus = (component: StatusPageComponentRecord) => { + const service = getServiceForComponent(component); + return service?.status || 'unknown'; + }; + + const getUptimePercentage = (serviceId: string) => { + const history = uptimeData[serviceId] || []; + if (history.length === 0) return 100; + + const upCount = history.filter(record => record.status === 'up').length; + return Math.round((upCount / history.length) * 100 * 100) / 100; + }; + + if (components.length === 0) { + return ( + + + Services + + +
+
+
+

Core Services

+

All core functionality

+
+ + Operational + +
+
+
+

API Services

+

REST and GraphQL APIs

+
+ + Operational + +
+
+
+

Database

+

Data storage and retrieval

+
+ + Operational + +
+
+
+
+ ); + } + + return ( + + + Components + + +
+ {components + .sort((a, b) => a.display_order - b.display_order) + .map((component) => { + const service = getServiceForComponent(component); + const status = getComponentStatus(component); + const uptime = service?.id ? getUptimePercentage(component.service_id) : 100; + + return ( +
+
+
+ +
+

{component.name}

+ {component.description && ( +

{component.description}

+ )} + {service && ( +
+ Uptime: {uptime}% + {service.responseTime > 0 && ( + + Response: {service.responseTime}ms + + )} +
+ )} +
+
+ + {status === 'up' ? 'Operational' : + status === 'down' ? 'Down' : + status === 'paused' ? 'Paused' : 'Warning'} + +
+ + {/* Individual component uptime history */} + {component.service_id && ( + + )} +
+ ); + })} +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/public/CurrentStatusSection.tsx b/application/src/components/public/CurrentStatusSection.tsx new file mode 100644 index 0000000..f24e197 --- /dev/null +++ b/application/src/components/public/CurrentStatusSection.tsx @@ -0,0 +1,107 @@ + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Shield, Clock } from 'lucide-react'; +import { format } from 'date-fns'; +import { OperationalPageRecord } from '@/types/operational.types'; +import { StatusPageComponentRecord } from '@/types/statusPageComponents.types'; +import { Service } from '@/types/service.types'; + +interface CurrentStatusSectionProps { + page: OperationalPageRecord; + components: StatusPageComponentRecord[]; + services: Service[]; +} + +const getActualStatus = (components: StatusPageComponentRecord[], services: Service[]) => { + if (components.length === 0) { + return 'operational'; // Default if no components + } + + let hasDown = false; + let hasDegraded = false; + let hasMaintenance = false; + + components.forEach(component => { + const service = services.find(s => s.id === component.service_id); + if (service) { + switch (service.status) { + case 'down': + hasDown = true; + break; + case 'warning': + hasDegraded = true; + break; + case 'paused': + hasMaintenance = true; + break; + } + } + }); + + // Priority: down > degraded > maintenance > operational + if (hasDown) return 'major_outage'; + if (hasDegraded) return 'degraded'; + if (hasMaintenance) return 'maintenance'; + return 'operational'; +}; + +const getStatusMessage = (status: OperationalPageRecord['status']) => { + switch (status) { + case 'operational': + return 'All systems are operational'; + case 'degraded': + return 'Some systems are experiencing degraded performance'; + case 'maintenance': + return 'Systems are currently under maintenance'; + case 'major_outage': + return 'We are experiencing a major service outage'; + default: + return 'Status unknown'; + } +}; + +const getStatusColor = (status: OperationalPageRecord['status']) => { + switch (status) { + case 'operational': + return 'text-green-600 dark:text-green-400'; + case 'degraded': + return 'text-yellow-600 dark:text-yellow-400'; + case 'maintenance': + return 'text-blue-600 dark:text-blue-400'; + case 'major_outage': + return 'text-red-600 dark:text-red-400'; + default: + return 'text-muted-foreground'; + } +}; + +export const CurrentStatusSection = ({ page, components, services }: CurrentStatusSectionProps) => { + const actualStatus = getActualStatus(components, services); + + return ( + + + + + Current Status + + + +
+
+ + {getStatusMessage(actualStatus)} + +
+
+ + Last updated {format(new Date(page.updated), 'MMM dd, yyyy HH:mm')} UTC +
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/public/OverallUptimeSection.tsx b/application/src/components/public/OverallUptimeSection.tsx new file mode 100644 index 0000000..9387f1b --- /dev/null +++ b/application/src/components/public/OverallUptimeSection.tsx @@ -0,0 +1,49 @@ + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { UptimeData } from '@/types/service.types'; +import { UptimeHistoryRenderer } from './UptimeHistoryRenderer'; + +interface OverallUptimeSectionProps { + uptimeData: Record; +} + +export const OverallUptimeSection = ({ uptimeData }: OverallUptimeSectionProps) => { + const getOverallUptime = () => { + const allHistories = Object.values(uptimeData); + if (allHistories.length === 0) return 99.9; + + let totalRecords = 0; + let upRecords = 0; + + allHistories.forEach(history => { + totalRecords += history.length; + upRecords += history.filter(record => record.status === 'up').length; + }); + + if (totalRecords === 0) return 99.9; + return Math.round((upRecords / totalRecords) * 100 * 100) / 100; + }; + + return ( + + + Overall Uptime History (Last 90 days) + + +
+ {Object.keys(uptimeData).length > 0 ? + : + } +
+
+ 90 days ago + Today +
+
+
{getOverallUptime()}%
+
Overall uptime
+
+
+
+ ); +}; \ No newline at end of file diff --git a/application/src/components/public/PublicStatusPage.tsx b/application/src/components/public/PublicStatusPage.tsx new file mode 100644 index 0000000..8cc686b --- /dev/null +++ b/application/src/components/public/PublicStatusPage.tsx @@ -0,0 +1,89 @@ + +import { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { Button } from '@/components/ui/button'; +import { usePublicStatusPageData } from './hooks/usePublicStatusPageData'; +import { StatusPageHeader } from './StatusPageHeader'; +import { CurrentStatusSection } from './CurrentStatusSection'; +import { ComponentsStatusSection } from './ComponentsStatusSection'; +import { OverallUptimeSection } from './OverallUptimeSection'; +import { PublicStatusPageFooter } from './PublicStatusPageFooter'; + +export const PublicStatusPage = () => { + const { slug } = useParams<{ slug: string }>(); + const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(slug); + + // Apply theme to document + useEffect(() => { + if (page) { + const root = document.documentElement; + if (page.theme === 'dark') { + root.classList.add('dark'); + root.classList.remove('light'); + } else { + root.classList.add('light'); + root.classList.remove('dark'); + } + } + + // Cleanup on unmount + return () => { + const root = document.documentElement; + root.classList.remove('dark', 'light'); + }; + }, [page?.theme]); + + if (loading) { + return ( +
+
+
+

Loading status page...

+
+
+ ); + } + + if (error || !page) { + return ( +
+
+

Page Not Found

+

{error || 'The requested status page could not be found.'}

+ +
+
+ ); + } + + return ( +
+ {/* Header */} + + + {/* Main Content */} +
+ {/* Current Status */} + + + {/* Components Status */} + + + {/* Overall Uptime History */} + + + {/* Footer */} + +
+ + {/* Custom CSS */} + {page.custom_css && ( +