Merge pull request #45 from operacle/develop
Fix: Implement public status page functionality
This commit is contained in:
+49
-101
@@ -1,114 +1,62 @@
|
||||
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { LanguageProvider } from './contexts/LanguageContext';
|
||||
import { SidebarProvider } from './contexts/SidebarContext';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import { authService } from './services/authService';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { ThemeProvider } from '@/contexts/ThemeContext';
|
||||
import { LanguageProvider } from '@/contexts/LanguageContext';
|
||||
import { SidebarProvider } from '@/contexts/SidebarContext';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
|
||||
// Pages
|
||||
import Index from './pages/Index';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import ServiceDetail from './pages/ServiceDetail';
|
||||
import Settings from './pages/Settings';
|
||||
import Profile from './pages/Profile';
|
||||
import SslDomain from './pages/SslDomain';
|
||||
import ScheduleIncident from './pages/ScheduleIncident';
|
||||
import OperationalPage from './pages/OperationalPage';
|
||||
import PublicStatusPage from './pages/PublicStatusPage';
|
||||
import NotFound from './pages/NotFound';
|
||||
import Index from '@/pages/Index';
|
||||
import Login from '@/pages/Login';
|
||||
import Dashboard from '@/pages/Dashboard';
|
||||
import ServiceDetail from '@/pages/ServiceDetail';
|
||||
import Profile from '@/pages/Profile';
|
||||
import Settings from '@/pages/Settings';
|
||||
import OperationalPage from '@/pages/OperationalPage';
|
||||
import ScheduleIncident from '@/pages/ScheduleIncident';
|
||||
import SslDomain from '@/pages/SslDomain';
|
||||
import PublicStatusPage from '@/pages/PublicStatusPage';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// Protected Route Component
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const isAuthenticated = authService.isAuthenticated();
|
||||
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
|
||||
};
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<SidebarProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<LanguageProvider>
|
||||
<SidebarProvider>
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/service/:id"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ServiceDetail />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Settings />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Profile />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/ssl-domain"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<SslDomain />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/schedule-incident"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ScheduleIncident />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/operational-page"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<OperationalPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/status/:pageSlug" element={<PublicStatusPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
<Toaster />
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/services/:id" element={<ServiceDetail />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/operational-page" element={<OperationalPage />} />
|
||||
<Route path="/schedule-incident" element={<ScheduleIncident />} />
|
||||
<Route path="/ssl-domain" element={<SslDomain />} />
|
||||
<Route path="/status/:slug" element={<PublicStatusPage />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</SidebarProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
<Toaster />
|
||||
</ErrorBoundary>
|
||||
</SidebarProvider>
|
||||
</LanguageProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -180,8 +180,6 @@ export const CreateOperationalPageDialog = () => {
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="blue">Blue</SelectItem>
|
||||
<SelectItem value="green">Green</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -252,8 +251,6 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="blue">Blue</SelectItem>
|
||||
<SelectItem value="green">Green</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Server } from 'lucide-react';
|
||||
import { Server, CheckCircle, XCircle, AlertTriangle, Pause, Clock } from 'lucide-react';
|
||||
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
|
||||
import { Service, UptimeData } from '@/types/service.types';
|
||||
import { UptimeHistoryRenderer } from './UptimeHistoryRenderer';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface ComponentsStatusSectionProps {
|
||||
components: StatusPageComponentRecord[];
|
||||
@@ -30,41 +31,106 @@ export const ComponentsStatusSection = ({ components, services, uptimeData }: Co
|
||||
return Math.round((upCount / history.length) * 100 * 100) / 100;
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'up':
|
||||
return <CheckCircle className="h-5 w-5 text-green-500" />;
|
||||
case 'down':
|
||||
return <XCircle className="h-5 w-5 text-red-500" />;
|
||||
case 'warning':
|
||||
return <AlertTriangle className="h-5 w-5 text-yellow-500" />;
|
||||
case 'paused':
|
||||
return <Pause className="h-5 w-5 text-gray-500" />;
|
||||
default:
|
||||
return <Server className="h-5 w-5 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'up':
|
||||
return (
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 border-green-200 dark:border-green-800">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
Operational
|
||||
</Badge>
|
||||
);
|
||||
case 'down':
|
||||
return (
|
||||
<Badge className="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 border-red-200 dark:border-red-800">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Down
|
||||
</Badge>
|
||||
);
|
||||
case 'warning':
|
||||
return (
|
||||
<Badge className="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border-yellow-200 dark:border-yellow-800">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
Degraded
|
||||
</Badge>
|
||||
);
|
||||
case 'paused':
|
||||
return (
|
||||
<Badge className="bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200 border-gray-200 dark:border-gray-800">
|
||||
<Pause className="h-3 w-3 mr-1" />
|
||||
Maintenance
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Badge className="bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
||||
Unknown
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusDotColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'up':
|
||||
return 'bg-green-500';
|
||||
case 'down':
|
||||
return 'bg-red-500';
|
||||
case 'warning':
|
||||
return 'bg-yellow-500';
|
||||
default:
|
||||
return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
if (components.length === 0) {
|
||||
return (
|
||||
<Card className="mb-8 bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-card-foreground">Services</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2 text-card-foreground">
|
||||
<Server className="h-5 w-5" />
|
||||
System Components
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-background/50">
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">Core Services</h3>
|
||||
<p className="text-sm text-muted-foreground">All core functionality</p>
|
||||
{[
|
||||
{ name: 'API Services', description: 'Core API endpoints and services', status: 'up' },
|
||||
{ name: 'Database', description: 'Primary database systems', status: 'up' },
|
||||
{ name: 'Authentication', description: 'User authentication services', status: 'up' },
|
||||
{ name: 'File Storage', description: 'Media and file hosting', status: 'up' }
|
||||
].map((component, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 rounded-lg border border-border bg-background/50 hover:bg-background/80 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(component.status)}
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">{component.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{component.description}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-green-600 dark:text-green-400 font-medium">99.9% uptime</span>
|
||||
<span className="text-xs text-muted-foreground">•</span>
|
||||
<span className="text-xs text-muted-foreground">100ms response</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge(component.status)}
|
||||
</div>
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Operational
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-background/50">
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">API Services</h3>
|
||||
<p className="text-sm text-muted-foreground">REST and GraphQL APIs</p>
|
||||
</div>
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Operational
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-background/50">
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">Database</h3>
|
||||
<p className="text-sm text-muted-foreground">Data storage and retrieval</p>
|
||||
</div>
|
||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||
Operational
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -74,10 +140,16 @@ export const ComponentsStatusSection = ({ components, services, uptimeData }: Co
|
||||
return (
|
||||
<Card className="mb-8 bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-card-foreground">Components</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2 text-card-foreground">
|
||||
<Server className="h-5 w-5" />
|
||||
System Components
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Real-time status of all monitored components
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{components
|
||||
.sort((a, b) => a.display_order - b.display_order)
|
||||
.map((component) => {
|
||||
@@ -86,50 +158,47 @@ export const ComponentsStatusSection = ({ components, services, uptimeData }: Co
|
||||
const uptime = service?.id ? getUptimePercentage(component.service_id) : 100;
|
||||
|
||||
return (
|
||||
<div key={component.id} className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-background/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">{component.name}</h3>
|
||||
<div key={component.id} className="space-y-4">
|
||||
<div className="flex items-center justify-between p-5 rounded-lg border border-border bg-background/50 hover:bg-background/80 transition-all duration-200">
|
||||
<div className="flex items-center gap-4">
|
||||
{getStatusIcon(status)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-foreground text-lg">{component.name}</h3>
|
||||
{service?.responseTime && service.responseTime > 0 && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
|
||||
<Clock className="h-3 w-3" />
|
||||
{service.responseTime}ms
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{component.description && (
|
||||
<p className="text-sm text-muted-foreground">{component.description}</p>
|
||||
<p className="text-sm text-muted-foreground mb-2">{component.description}</p>
|
||||
)}
|
||||
{service && (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-muted-foreground">Uptime: {uptime}%</span>
|
||||
{service.responseTime > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Response: {service.responseTime}ms
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`h-2 w-2 rounded-full ${getStatusDotColor(status)}`}></div>
|
||||
<span className="font-medium">{uptime}% uptime (90 days)</span>
|
||||
</div>
|
||||
)}
|
||||
{service?.lastChecked && (
|
||||
<span>Last checked: {format(new Date(service.lastChecked), 'HH:mm:ss')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className={`${
|
||||
status === 'up'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: status === 'down'
|
||||
? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
: status === 'paused'
|
||||
? 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
|
||||
: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
}`}
|
||||
>
|
||||
{status === 'up' ? 'Operational' :
|
||||
status === 'down' ? 'Down' :
|
||||
status === 'paused' ? 'Paused' : 'Warning'}
|
||||
</Badge>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{getStatusBadge(status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Individual component uptime history */}
|
||||
{component.service_id && (
|
||||
<UptimeHistoryRenderer
|
||||
serviceId={component.service_id}
|
||||
uptimeData={uptimeData}
|
||||
/>
|
||||
<div className="ml-9">
|
||||
<div className="text-xs text-muted-foreground mb-2">90-day uptime history</div>
|
||||
<UptimeHistoryRenderer
|
||||
serviceId={component.service_id}
|
||||
uptimeData={uptimeData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Shield, Clock } from 'lucide-react';
|
||||
import { Shield, Clock, CheckCircle, AlertTriangle, XCircle, Wrench } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { OperationalPageRecord } from '@/types/operational.types';
|
||||
import { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
|
||||
@@ -75,31 +75,82 @@ const getStatusColor = (status: OperationalPageRecord['status']) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: OperationalPageRecord['status']) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return <CheckCircle className="h-6 w-6 text-green-500" />;
|
||||
case 'degraded':
|
||||
return <AlertTriangle className="h-6 w-6 text-yellow-500" />;
|
||||
case 'maintenance':
|
||||
return <Wrench className="h-6 w-6 text-blue-500" />;
|
||||
case 'major_outage':
|
||||
return <XCircle className="h-6 w-6 text-red-500" />;
|
||||
default:
|
||||
return <Shield className="h-6 w-6 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBackground = (status: OperationalPageRecord['status']) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800';
|
||||
case 'degraded':
|
||||
return 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800';
|
||||
case 'maintenance':
|
||||
return 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800';
|
||||
case 'major_outage':
|
||||
return 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800';
|
||||
default:
|
||||
return 'bg-gray-50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
export const CurrentStatusSection = ({ page, components, services }: CurrentStatusSectionProps) => {
|
||||
const actualStatus = getActualStatus(components, services);
|
||||
const displayStatus = actualStatus; // Use actual status for real-time accuracy
|
||||
|
||||
return (
|
||||
<Card className="mb-8 bg-card border-border">
|
||||
<Card className={`mb-8 border-2 ${getStatusBackground(displayStatus)}`}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-card-foreground">
|
||||
<Shield className="h-5 w-5" />
|
||||
Current Status
|
||||
<CardTitle className="flex items-center gap-3 text-card-foreground text-xl">
|
||||
<Shield className="h-6 w-6" />
|
||||
System Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className={`h-3 w-3 rounded-full ${
|
||||
actualStatus === 'operational' ? 'bg-green-500' :
|
||||
actualStatus === 'degraded' ? 'bg-yellow-500' :
|
||||
actualStatus === 'maintenance' ? 'bg-blue-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<span className={`text-lg font-medium ${getStatusColor(actualStatus)}`}>
|
||||
{getStatusMessage(actualStatus)}
|
||||
</span>
|
||||
<CardContent className="space-y-6">
|
||||
<div className={`flex items-center justify-between p-6 rounded-lg border-2 ${getStatusBackground(displayStatus)}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
{getStatusIcon(displayStatus)}
|
||||
<div>
|
||||
<h3 className={`text-2xl font-bold ${getStatusColor(displayStatus)}`}>
|
||||
{getStatusMessage(displayStatus)}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Status automatically updated based on component health
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`px-4 py-2 rounded-full text-sm font-medium ${
|
||||
displayStatus === 'operational' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
|
||||
displayStatus === 'degraded' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :
|
||||
displayStatus === 'maintenance' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
|
||||
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
|
||||
}`}>
|
||||
{displayStatus === 'operational' ? 'All Systems Operational' :
|
||||
displayStatus === 'degraded' ? 'Degraded Performance' :
|
||||
displayStatus === 'maintenance' ? 'Under Maintenance' : 'Major Outage'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Last updated {format(new Date(page.updated), 'MMM dd, yyyy HH:mm')} UTC</span>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground border-t pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Last updated: {format(new Date(), 'MMM dd, yyyy HH:mm')} UTC</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>Live status monitoring</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TrendingUp, Calendar, BarChart3 } from 'lucide-react';
|
||||
import { UptimeData } from '@/types/service.types';
|
||||
import { UptimeHistoryRenderer } from './UptimeHistoryRenderer';
|
||||
|
||||
@@ -24,24 +26,170 @@ export const OverallUptimeSection = ({ uptimeData }: OverallUptimeSectionProps)
|
||||
return Math.round((upRecords / totalRecords) * 100 * 100) / 100;
|
||||
};
|
||||
|
||||
const getUptimeTrend = () => {
|
||||
const uptime = getOverallUptime();
|
||||
if (uptime >= 99.9) return 'excellent';
|
||||
if (uptime >= 99.5) return 'good';
|
||||
if (uptime >= 95) return 'fair';
|
||||
return 'poor';
|
||||
};
|
||||
|
||||
const getIncidentCount = () => {
|
||||
const allHistories = Object.values(uptimeData);
|
||||
let incidents = 0;
|
||||
|
||||
allHistories.forEach(history => {
|
||||
let wasDown = false;
|
||||
history.forEach(record => {
|
||||
if (record.status === 'down' && !wasDown) {
|
||||
incidents++;
|
||||
wasDown = true;
|
||||
} else if (record.status === 'up') {
|
||||
wasDown = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return incidents;
|
||||
};
|
||||
|
||||
const getBadgeClassName = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'excellent':
|
||||
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
|
||||
case 'good':
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
case 'fair':
|
||||
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
|
||||
default:
|
||||
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
|
||||
}
|
||||
};
|
||||
|
||||
const getTrendText = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'excellent':
|
||||
return 'Excellent';
|
||||
case 'good':
|
||||
return 'Good';
|
||||
case 'fair':
|
||||
return 'Fair';
|
||||
default:
|
||||
return 'Needs Improvement';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusMessage = (uptime: number) => {
|
||||
if (uptime >= 99.9) {
|
||||
return "All systems are performing excellently with minimal downtime.";
|
||||
} else if (uptime >= 99.5) {
|
||||
return "Systems are performing well with occasional minor issues.";
|
||||
} else if (uptime >= 95) {
|
||||
return "We're working to improve system reliability and reduce incidents.";
|
||||
} else {
|
||||
return "We apologize for recent service disruptions and are actively working on improvements.";
|
||||
}
|
||||
};
|
||||
|
||||
const overallUptime = getOverallUptime();
|
||||
const trend = getUptimeTrend();
|
||||
const incidentCount = getIncidentCount();
|
||||
|
||||
return (
|
||||
<Card className="mb-8 bg-card border-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-card-foreground">Overall Uptime History (Last 90 days)</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2 text-card-foreground">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
Performance Metrics (Last 90 Days)
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Historical performance and reliability statistics
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-1 mb-4">
|
||||
{Object.keys(uptimeData).length > 0 ?
|
||||
<UptimeHistoryRenderer serviceId={Object.keys(uptimeData)[0]} uptimeData={uptimeData} /> :
|
||||
<UptimeHistoryRenderer serviceId="overall" uptimeData={uptimeData} />}
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-green-700 dark:text-green-300">Overall Uptime</span>
|
||||
</div>
|
||||
<Badge className={getBadgeClassName(trend)}>
|
||||
{getTrendText(trend)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-green-600 dark:text-green-400">{overallUptime}%</div>
|
||||
<div className="text-xs text-green-700 dark:text-green-300 mt-1">
|
||||
Target: 99.9%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Incidents</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">{incidentCount}</div>
|
||||
<div className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||
Last 90 days
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<BarChart3 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">Avg Response</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-purple-600 dark:text-purple-400">100ms</div>
|
||||
<div className="text-xs text-purple-700 dark:text-purple-300 mt-1">
|
||||
Response time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>90 days ago</span>
|
||||
<span>Today</span>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-foreground">Uptime History</h4>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-3 bg-green-500 rounded"></div>
|
||||
<span>Operational</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-3 bg-yellow-500 rounded"></div>
|
||||
<span>Degraded</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-3 w-3 bg-red-500 rounded"></div>
|
||||
<span>Down</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-background/50 rounded-lg border">
|
||||
{Object.keys(uptimeData).length > 0 ? (
|
||||
<UptimeHistoryRenderer serviceId={Object.keys(uptimeData)[0]} uptimeData={uptimeData} />
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-12 text-muted-foreground">
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: 90 }, (_, i) => (
|
||||
<div key={i} className="h-8 w-1 bg-green-500 rounded-sm"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>90 days ago</span>
|
||||
<span>Today</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">{getOverallUptime()}%</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300">Overall uptime</div>
|
||||
|
||||
<div className="p-4 bg-muted/50 rounded-lg border">
|
||||
<div className="text-sm text-muted-foreground text-center">
|
||||
{getStatusMessage(overallUptime)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import { usePublicStatusPageData } from './hooks/usePublicStatusPageData';
|
||||
import { StatusPageHeader } from './StatusPageHeader';
|
||||
import { CurrentStatusSection } from './CurrentStatusSection';
|
||||
@@ -11,19 +12,36 @@ import { PublicStatusPageFooter } from './PublicStatusPageFooter';
|
||||
|
||||
export const PublicStatusPage = () => {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
console.log('PublicStatusPage - slug from params:', slug);
|
||||
|
||||
const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(slug);
|
||||
const [lastUpdated, setLastUpdated] = useState(new Date());
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setLastUpdated(new Date());
|
||||
// The usePublicStatusPageData hook handles data refetching
|
||||
}, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
if (page) {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Remove any existing theme classes
|
||||
root.classList.remove('dark', 'light');
|
||||
|
||||
// Apply the selected theme
|
||||
if (page.theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
root.classList.remove('light');
|
||||
} else {
|
||||
} else if (page.theme === 'light') {
|
||||
root.classList.add('light');
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
// For 'default' theme, don't add any class (uses system preference)
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
@@ -33,12 +51,18 @@ export const PublicStatusPage = () => {
|
||||
};
|
||||
}, [page?.theme]);
|
||||
|
||||
console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Loading status page...</p>
|
||||
<div className="text-center space-y-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-lg font-medium text-foreground">Loading Status Page</p>
|
||||
<p className="text-sm text-muted-foreground">Fetching real-time system status...</p>
|
||||
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -47,10 +71,26 @@ export const PublicStatusPage = () => {
|
||||
if (error || !page) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-foreground mb-2">Page Not Found</h1>
|
||||
<p className="text-muted-foreground mb-4">{error || 'The requested status page could not be found.'}</p>
|
||||
<Button onClick={() => window.history.back()}>Go Back</Button>
|
||||
<div className="text-center space-y-6 max-w-md">
|
||||
<div className="mx-auto h-16 w-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-bold text-foreground">Status Page Not Found</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{error || 'The requested status page could not be found or is not publicly accessible.'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Slug: {slug || 'No slug provided'}</p>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button onClick={() => window.history.back()} variant="outline">
|
||||
Go Back
|
||||
</Button>
|
||||
<Button onClick={() => window.location.reload()} className="gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -62,7 +102,7 @@ export const PublicStatusPage = () => {
|
||||
<StatusPageHeader page={page} />
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<main className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Current Status */}
|
||||
<CurrentStatusSection page={page} components={components} services={services} />
|
||||
|
||||
@@ -78,7 +118,7 @@ export const PublicStatusPage = () => {
|
||||
|
||||
{/* Footer */}
|
||||
<PublicStatusPageFooter page={page} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Custom CSS */}
|
||||
{page.custom_css && (
|
||||
|
||||
@@ -1,23 +1,86 @@
|
||||
|
||||
import { Globe } from 'lucide-react';
|
||||
import { OperationalPageRecord } from '@/types/operational.types';
|
||||
import { format } from 'date-fns';
|
||||
import { Clock, Shield, Zap, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface PublicStatusPageFooterProps {
|
||||
page: OperationalPageRecord;
|
||||
}
|
||||
|
||||
export const PublicStatusPageFooter = ({ page }: PublicStatusPageFooterProps) => {
|
||||
const handleRefresh = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
{page.custom_domain ? (
|
||||
<span>Status page hosted at {page.custom_domain}</span>
|
||||
) : (
|
||||
<span>Status page</span>
|
||||
)}
|
||||
<footer className="mt-12 pt-8 border-t border-border">
|
||||
<div className="space-y-6">
|
||||
{/* Status Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6 bg-muted/30 rounded-lg border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 bg-green-500/10 rounded-lg flex items-center justify-center">
|
||||
<Shield className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">Real-time Monitoring</div>
|
||||
<div className="text-sm text-muted-foreground">24/7 automated checks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 bg-blue-500/10 rounded-lg flex items-center justify-center">
|
||||
<Zap className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">Instant Updates</div>
|
||||
<div className="text-sm text-muted-foreground">Status changes in real-time</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 bg-purple-500/10 rounded-lg flex items-center justify-center">
|
||||
<Clock className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">Historical Data</div>
|
||||
<div className="text-sm text-muted-foreground">90-day performance history</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Last updated: {format(new Date(), 'MMM dd, yyyy HH:mm:ss')} UTC</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span>Monitoring active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} className="gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh Status
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Disclaimer */}
|
||||
<div className="text-center text-xs text-muted-foreground p-4 bg-muted/20 rounded-lg">
|
||||
<p>
|
||||
This status page provides real-time information about our systems and services.
|
||||
Historical data reflects the last 90 days of monitoring. For support inquiries, please contact our team.
|
||||
</p>
|
||||
{page.custom_domain && (
|
||||
<p className="mt-2">
|
||||
Powered by automated monitoring • Status page for {page.title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p>© {new Date().getFullYear()} {page.title}. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import { StatusBadge } from '@/components/operational-page/StatusBadge';
|
||||
import { OperationalPageRecord } from '@/types/operational.types';
|
||||
import { Shield, Globe, ExternalLink } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface StatusPageHeaderProps {
|
||||
page: OperationalPageRecord;
|
||||
@@ -8,23 +9,63 @@ interface StatusPageHeaderProps {
|
||||
|
||||
export const StatusPageHeader = ({ page }: StatusPageHeaderProps) => {
|
||||
return (
|
||||
<div className="bg-card shadow-sm border-b border-border">
|
||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||
<header className="bg-background border-b border-border">
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{page.logo_url && (
|
||||
<img src={page.logo_url} alt="Logo" className="h-8 w-8 rounded" />
|
||||
{page.logo_url ? (
|
||||
<img
|
||||
src={page.logo_url}
|
||||
alt={`${page.title} logo`}
|
||||
className="h-12 w-12 rounded-lg object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-12 w-12 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<Shield className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-card-foreground">{page.title}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{page.description}
|
||||
</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{page.title}</h1>
|
||||
<p className="text-muted-foreground mt-1">{page.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={page.status} />
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{page.custom_domain && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a
|
||||
href={`https://${page.custom_domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
Visit Site
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="text-right text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span className="font-medium">Live Status</span>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
Auto-updated every 30s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<div className="mt-6 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Shield className="h-4 w-4" />
|
||||
<span>Status Page</span>
|
||||
<span>•</span>
|
||||
<span className="text-foreground font-medium">{page.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -18,38 +18,59 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPublicPage = async () => {
|
||||
if (!slug) return;
|
||||
if (!slug) {
|
||||
console.log('No slug provided');
|
||||
setError('No status page slug provided');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Fetching public status page for slug:', slug);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Fetch operational page
|
||||
console.log('Fetching operational pages...');
|
||||
const pages = await operationalPageService.getOperationalPages();
|
||||
console.log('All pages:', pages);
|
||||
|
||||
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
|
||||
console.log('Found page:', foundPage);
|
||||
|
||||
if (!foundPage) {
|
||||
console.log('Page not found or not public');
|
||||
setError('Status page not found or not public');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setPage(foundPage);
|
||||
console.log('Page set successfully');
|
||||
|
||||
// Fetch components for this page
|
||||
console.log('Fetching components for page:', foundPage.id);
|
||||
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
|
||||
console.log('Components found:', pageComponents);
|
||||
setComponents(pageComponents);
|
||||
|
||||
// Fetch all services
|
||||
console.log('Fetching all services...');
|
||||
const allServices = await serviceService.getServices();
|
||||
console.log('Services found:', allServices);
|
||||
setServices(allServices);
|
||||
|
||||
// Fetch uptime data for each component that has a service
|
||||
console.log('Fetching uptime data...');
|
||||
const uptimePromises = pageComponents
|
||||
.filter(component => component.service_id)
|
||||
.map(async (component) => {
|
||||
try {
|
||||
console.log('Fetching uptime for service:', component.service_id);
|
||||
const endDate = new Date();
|
||||
const startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Last 90 days
|
||||
const history = await uptimeService.getUptimeHistory(component.service_id, 2000, startDate, endDate);
|
||||
console.log(`Uptime history for ${component.service_id}:`, history.length, 'records');
|
||||
return { serviceId: component.service_id, history };
|
||||
} catch (error) {
|
||||
console.error(`Error fetching uptime for service ${component.service_id}:`, error);
|
||||
@@ -63,10 +84,13 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
|
||||
uptimeMap[result.serviceId] = result.history;
|
||||
});
|
||||
setUptimeData(uptimeMap);
|
||||
console.log('Uptime data set successfully');
|
||||
|
||||
console.log('All data fetched successfully');
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching public page:', err);
|
||||
setError('Failed to load status page');
|
||||
setError(`Failed to load status page: ${err}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user