Merge pull request #45 from operacle/develop
Fix: Implement public status page functionality
This commit is contained in:
+36
-88
@@ -1,114 +1,62 @@
|
|||||||
|
|
||||||
import React from 'react';
|
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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import { LanguageProvider } from './contexts/LanguageContext';
|
import { ThemeProvider } from '@/contexts/ThemeContext';
|
||||||
import { SidebarProvider } from './contexts/SidebarContext';
|
import { LanguageProvider } from '@/contexts/LanguageContext';
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
import { SidebarProvider } from '@/contexts/SidebarContext';
|
||||||
import { Toaster } from './components/ui/sonner';
|
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||||
import { authService } from './services/authService';
|
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
import Index from './pages/Index';
|
import Index from '@/pages/Index';
|
||||||
import Login from './pages/Login';
|
import Login from '@/pages/Login';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from '@/pages/Dashboard';
|
||||||
import ServiceDetail from './pages/ServiceDetail';
|
import ServiceDetail from '@/pages/ServiceDetail';
|
||||||
import Settings from './pages/Settings';
|
import Profile from '@/pages/Profile';
|
||||||
import Profile from './pages/Profile';
|
import Settings from '@/pages/Settings';
|
||||||
import SslDomain from './pages/SslDomain';
|
import OperationalPage from '@/pages/OperationalPage';
|
||||||
import ScheduleIncident from './pages/ScheduleIncident';
|
import ScheduleIncident from '@/pages/ScheduleIncident';
|
||||||
import OperationalPage from './pages/OperationalPage';
|
import SslDomain from '@/pages/SslDomain';
|
||||||
import PublicStatusPage from './pages/PublicStatusPage';
|
import PublicStatusPage from '@/pages/PublicStatusPage';
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from '@/pages/NotFound';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
// Protected Route Component
|
queries: {
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
retry: 1,
|
||||||
const isAuthenticated = authService.isAuthenticated();
|
refetchOnWindowFocus: false,
|
||||||
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
|
},
|
||||||
};
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
|
<ErrorBoundary>
|
||||||
<Router>
|
<Router>
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
path="/dashboard"
|
<Route path="/services/:id" element={<ServiceDetail />} />
|
||||||
element={
|
<Route path="/profile" element={<Profile />} />
|
||||||
<ProtectedRoute>
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Dashboard />
|
<Route path="/operational-page" element={<OperationalPage />} />
|
||||||
</ProtectedRoute>
|
<Route path="/schedule-incident" element={<ScheduleIncident />} />
|
||||||
}
|
<Route path="/ssl-domain" element={<SslDomain />} />
|
||||||
/>
|
<Route path="/status/:slug" element={<PublicStatusPage />} />
|
||||||
<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 />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster />
|
|
||||||
</div>
|
|
||||||
</Router>
|
</Router>
|
||||||
|
<Toaster />
|
||||||
|
</ErrorBoundary>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,8 +180,6 @@ export const CreateOperationalPageDialog = () => {
|
|||||||
<SelectItem value="default">Default</SelectItem>
|
<SelectItem value="default">Default</SelectItem>
|
||||||
<SelectItem value="dark">Dark</SelectItem>
|
<SelectItem value="dark">Dark</SelectItem>
|
||||||
<SelectItem value="light">Light</SelectItem>
|
<SelectItem value="light">Light</SelectItem>
|
||||||
<SelectItem value="blue">Blue</SelectItem>
|
|
||||||
<SelectItem value="green">Green</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -252,8 +251,6 @@ export const EditOperationalPageDialog = ({ page, open, onOpenChange }: EditOper
|
|||||||
<SelectItem value="default">Default</SelectItem>
|
<SelectItem value="default">Default</SelectItem>
|
||||||
<SelectItem value="dark">Dark</SelectItem>
|
<SelectItem value="dark">Dark</SelectItem>
|
||||||
<SelectItem value="light">Light</SelectItem>
|
<SelectItem value="light">Light</SelectItem>
|
||||||
<SelectItem value="blue">Blue</SelectItem>
|
|
||||||
<SelectItem value="green">Green</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { StatusPageComponentRecord } from '@/types/statusPageComponents.types';
|
||||||
import { Service, UptimeData } from '@/types/service.types';
|
import { Service, UptimeData } from '@/types/service.types';
|
||||||
import { UptimeHistoryRenderer } from './UptimeHistoryRenderer';
|
import { UptimeHistoryRenderer } from './UptimeHistoryRenderer';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
interface ComponentsStatusSectionProps {
|
interface ComponentsStatusSectionProps {
|
||||||
components: StatusPageComponentRecord[];
|
components: StatusPageComponentRecord[];
|
||||||
@@ -30,41 +31,106 @@ export const ComponentsStatusSection = ({ components, services, uptimeData }: Co
|
|||||||
return Math.round((upCount / history.length) * 100 * 100) / 100;
|
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) {
|
if (components.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card className="mb-8 bg-card border-border">
|
<Card className="mb-8 bg-card border-border">
|
||||||
<CardHeader>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-background/50">
|
{[
|
||||||
|
{ 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>
|
<div>
|
||||||
<h3 className="font-medium text-foreground">Core Services</h3>
|
<h3 className="font-medium text-foreground">{component.name}</h3>
|
||||||
<p className="text-sm text-muted-foreground">All core functionality</p>
|
<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>
|
||||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
|
||||||
Operational
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
{getStatusBadge(component.status)}
|
||||||
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>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -74,10 +140,16 @@ export const ComponentsStatusSection = ({ components, services, uptimeData }: Co
|
|||||||
return (
|
return (
|
||||||
<Card className="mb-8 bg-card border-border">
|
<Card className="mb-8 bg-card border-border">
|
||||||
<CardHeader>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
{components
|
{components
|
||||||
.sort((a, b) => a.display_order - b.display_order)
|
.sort((a, b) => a.display_order - b.display_order)
|
||||||
.map((component) => {
|
.map((component) => {
|
||||||
@@ -86,50 +158,47 @@ export const ComponentsStatusSection = ({ components, services, uptimeData }: Co
|
|||||||
const uptime = service?.id ? getUptimePercentage(component.service_id) : 100;
|
const uptime = service?.id ? getUptimePercentage(component.service_id) : 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={component.id} className="space-y-3">
|
<div key={component.id} className="space-y-4">
|
||||||
<div className="flex items-center justify-between p-4 rounded-lg border border-border bg-background/50">
|
<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-3">
|
<div className="flex items-center gap-4">
|
||||||
<Server className="h-5 w-5 text-muted-foreground" />
|
{getStatusIcon(status)}
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<h3 className="font-medium text-foreground">{component.name}</h3>
|
<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 && (
|
{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>
|
</div>
|
||||||
|
{service?.lastChecked && (
|
||||||
|
<span>Last checked: {format(new Date(service.lastChecked), 'HH:mm:ss')}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
</div>
|
||||||
className={`${
|
<div className="flex flex-col items-end gap-2">
|
||||||
status === 'up'
|
{getStatusBadge(status)}
|
||||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
</div>
|
||||||
: 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>
|
</div>
|
||||||
|
|
||||||
{/* Individual component uptime history */}
|
|
||||||
{component.service_id && (
|
{component.service_id && (
|
||||||
|
<div className="ml-9">
|
||||||
|
<div className="text-xs text-muted-foreground mb-2">90-day uptime history</div>
|
||||||
<UptimeHistoryRenderer
|
<UptimeHistoryRenderer
|
||||||
serviceId={component.service_id}
|
serviceId={component.service_id}
|
||||||
uptimeData={uptimeData}
|
uptimeData={uptimeData}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { format } from 'date-fns';
|
||||||
import { OperationalPageRecord } from '@/types/operational.types';
|
import { OperationalPageRecord } from '@/types/operational.types';
|
||||||
import { StatusPageComponentRecord } from '@/types/statusPageComponents.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) => {
|
export const CurrentStatusSection = ({ page, components, services }: CurrentStatusSectionProps) => {
|
||||||
const actualStatus = getActualStatus(components, services);
|
const actualStatus = getActualStatus(components, services);
|
||||||
|
const displayStatus = actualStatus; // Use actual status for real-time accuracy
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mb-8 bg-card border-border">
|
<Card className={`mb-8 border-2 ${getStatusBackground(displayStatus)}`}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-card-foreground">
|
<CardTitle className="flex items-center gap-3 text-card-foreground text-xl">
|
||||||
<Shield className="h-5 w-5" />
|
<Shield className="h-6 w-6" />
|
||||||
Current Status
|
System Status
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-6">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className={`flex items-center justify-between p-6 rounded-lg border-2 ${getStatusBackground(displayStatus)}`}>
|
||||||
<div className={`h-3 w-3 rounded-full ${
|
<div className="flex items-center gap-4">
|
||||||
actualStatus === 'operational' ? 'bg-green-500' :
|
{getStatusIcon(displayStatus)}
|
||||||
actualStatus === 'degraded' ? 'bg-yellow-500' :
|
<div>
|
||||||
actualStatus === 'maintenance' ? 'bg-blue-500' : 'bg-red-500'
|
<h3 className={`text-2xl font-bold ${getStatusColor(displayStatus)}`}>
|
||||||
}`}></div>
|
{getStatusMessage(displayStatus)}
|
||||||
<span className={`text-lg font-medium ${getStatusColor(actualStatus)}`}>
|
</h3>
|
||||||
{getStatusMessage(actualStatus)}
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
</span>
|
Status automatically updated based on component health
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
</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 justify-between text-sm text-muted-foreground border-t pt-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-4 w-4" />
|
||||||
<span>Last updated {format(new Date(page.updated), 'MMM dd, yyyy HH:mm')} UTC</span>
|
<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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { UptimeData } from '@/types/service.types';
|
||||||
import { UptimeHistoryRenderer } from './UptimeHistoryRenderer';
|
import { UptimeHistoryRenderer } from './UptimeHistoryRenderer';
|
||||||
|
|
||||||
@@ -24,24 +26,170 @@ export const OverallUptimeSection = ({ uptimeData }: OverallUptimeSectionProps)
|
|||||||
return Math.round((upRecords / totalRecords) * 100 * 100) / 100;
|
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 (
|
return (
|
||||||
<Card className="mb-8 bg-card border-border">
|
<Card className="mb-8 bg-card border-border">
|
||||||
<CardHeader>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-6">
|
||||||
<div className="flex items-center gap-1 mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{Object.keys(uptimeData).length > 0 ?
|
<div className="p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
|
||||||
<UptimeHistoryRenderer serviceId={Object.keys(uptimeData)[0]} uptimeData={uptimeData} /> :
|
<div className="flex items-center justify-between mb-2">
|
||||||
<UptimeHistoryRenderer serviceId="overall" uptimeData={uptimeData} />}
|
<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>
|
</div>
|
||||||
<div className="flex justify-between text-sm text-muted-foreground">
|
<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="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>90 days ago</span>
|
||||||
<span>Today</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>
|
||||||
<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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { RefreshCw, AlertCircle } from 'lucide-react';
|
||||||
import { usePublicStatusPageData } from './hooks/usePublicStatusPageData';
|
import { usePublicStatusPageData } from './hooks/usePublicStatusPageData';
|
||||||
import { StatusPageHeader } from './StatusPageHeader';
|
import { StatusPageHeader } from './StatusPageHeader';
|
||||||
import { CurrentStatusSection } from './CurrentStatusSection';
|
import { CurrentStatusSection } from './CurrentStatusSection';
|
||||||
@@ -11,19 +12,36 @@ import { PublicStatusPageFooter } from './PublicStatusPageFooter';
|
|||||||
|
|
||||||
export const PublicStatusPage = () => {
|
export const PublicStatusPage = () => {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
|
console.log('PublicStatusPage - slug from params:', slug);
|
||||||
|
|
||||||
const { page, components, services, uptimeData, loading, error } = usePublicStatusPageData(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
|
// Apply theme to document
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page) {
|
if (page) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
// Remove any existing theme classes
|
||||||
|
root.classList.remove('dark', 'light');
|
||||||
|
|
||||||
|
// Apply the selected theme
|
||||||
if (page.theme === 'dark') {
|
if (page.theme === 'dark') {
|
||||||
root.classList.add('dark');
|
root.classList.add('dark');
|
||||||
root.classList.remove('light');
|
} else if (page.theme === 'light') {
|
||||||
} else {
|
|
||||||
root.classList.add('light');
|
root.classList.add('light');
|
||||||
root.classList.remove('dark');
|
|
||||||
}
|
}
|
||||||
|
// For 'default' theme, don't add any class (uses system preference)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
@@ -33,12 +51,18 @@ export const PublicStatusPage = () => {
|
|||||||
};
|
};
|
||||||
}, [page?.theme]);
|
}, [page?.theme]);
|
||||||
|
|
||||||
|
console.log('PublicStatusPage state:', { loading, error, page: !!page, components: components.length, services: services.length });
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center space-y-4">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
||||||
<p className="text-muted-foreground">Loading status page...</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -47,10 +71,26 @@ export const PublicStatusPage = () => {
|
|||||||
if (error || !page) {
|
if (error || !page) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center space-y-6 max-w-md">
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-2">Page Not Found</h1>
|
<div className="mx-auto h-16 w-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center">
|
||||||
<p className="text-muted-foreground mb-4">{error || 'The requested status page could not be found.'}</p>
|
<AlertCircle className="h-8 w-8 text-red-600 dark:text-red-400" />
|
||||||
<Button onClick={() => window.history.back()}>Go Back</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -62,7 +102,7 @@ export const PublicStatusPage = () => {
|
|||||||
<StatusPageHeader page={page} />
|
<StatusPageHeader page={page} />
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* 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 */}
|
{/* Current Status */}
|
||||||
<CurrentStatusSection page={page} components={components} services={services} />
|
<CurrentStatusSection page={page} components={components} services={services} />
|
||||||
|
|
||||||
@@ -78,7 +118,7 @@ export const PublicStatusPage = () => {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<PublicStatusPageFooter page={page} />
|
<PublicStatusPageFooter page={page} />
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
{/* Custom CSS */}
|
{/* Custom CSS */}
|
||||||
{page.custom_css && (
|
{page.custom_css && (
|
||||||
|
|||||||
@@ -1,23 +1,86 @@
|
|||||||
|
|
||||||
import { Globe } from 'lucide-react';
|
|
||||||
import { OperationalPageRecord } from '@/types/operational.types';
|
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 {
|
interface PublicStatusPageFooterProps {
|
||||||
page: OperationalPageRecord;
|
page: OperationalPageRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PublicStatusPageFooter = ({ page }: PublicStatusPageFooterProps) => {
|
export const PublicStatusPageFooter = ({ page }: PublicStatusPageFooterProps) => {
|
||||||
|
const handleRefresh = () => {
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<footer className="mt-12 pt-8 border-t border-border">
|
||||||
<div className="flex items-center justify-center gap-2 mb-2">
|
<div className="space-y-6">
|
||||||
<Globe className="h-4 w-4" />
|
{/* Status Information */}
|
||||||
{page.custom_domain ? (
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6 bg-muted/30 rounded-lg border">
|
||||||
<span>Status page hosted at {page.custom_domain}</span>
|
<div className="flex items-center gap-3">
|
||||||
) : (
|
<div className="h-10 w-10 bg-green-500/10 rounded-lg flex items-center justify-center">
|
||||||
<span>Status page</span>
|
<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>
|
</div>
|
||||||
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import { StatusBadge } from '@/components/operational-page/StatusBadge';
|
|
||||||
import { OperationalPageRecord } from '@/types/operational.types';
|
import { OperationalPageRecord } from '@/types/operational.types';
|
||||||
|
import { Shield, Globe, ExternalLink } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
interface StatusPageHeaderProps {
|
interface StatusPageHeaderProps {
|
||||||
page: OperationalPageRecord;
|
page: OperationalPageRecord;
|
||||||
@@ -8,23 +9,63 @@ interface StatusPageHeaderProps {
|
|||||||
|
|
||||||
export const StatusPageHeader = ({ page }: StatusPageHeaderProps) => {
|
export const StatusPageHeader = ({ page }: StatusPageHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-card shadow-sm border-b border-border">
|
<header className="bg-background border-b border-border">
|
||||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{page.logo_url && (
|
{page.logo_url ? (
|
||||||
<img src={page.logo_url} alt="Logo" className="h-8 w-8 rounded" />
|
<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>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-card-foreground">{page.title}</h1>
|
<h1 className="text-3xl font-bold text-foreground">{page.title}</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-muted-foreground mt-1">{page.description}</p>
|
||||||
{page.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</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>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -18,38 +18,59 @@ export const usePublicStatusPageData = (slug: string | undefined) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPublicPage = async () => {
|
const fetchPublicPage = async () => {
|
||||||
if (!slug) return;
|
if (!slug) {
|
||||||
|
console.log('No slug provided');
|
||||||
|
setError('No status page slug provided');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Fetching public status page for slug:', slug);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
// Fetch operational page
|
// Fetch operational page
|
||||||
|
console.log('Fetching operational pages...');
|
||||||
const pages = await operationalPageService.getOperationalPages();
|
const pages = await operationalPageService.getOperationalPages();
|
||||||
|
console.log('All pages:', pages);
|
||||||
|
|
||||||
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
|
const foundPage = pages.find(p => p.slug === slug && p.is_public === 'true');
|
||||||
|
console.log('Found page:', foundPage);
|
||||||
|
|
||||||
if (!foundPage) {
|
if (!foundPage) {
|
||||||
|
console.log('Page not found or not public');
|
||||||
setError('Status page not found or not public');
|
setError('Status page not found or not public');
|
||||||
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPage(foundPage);
|
setPage(foundPage);
|
||||||
|
console.log('Page set successfully');
|
||||||
|
|
||||||
// Fetch components for this page
|
// Fetch components for this page
|
||||||
|
console.log('Fetching components for page:', foundPage.id);
|
||||||
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
|
const pageComponents = await statusPageComponentsService.getStatusPageComponentsByOperationalId(foundPage.id);
|
||||||
|
console.log('Components found:', pageComponents);
|
||||||
setComponents(pageComponents);
|
setComponents(pageComponents);
|
||||||
|
|
||||||
// Fetch all services
|
// Fetch all services
|
||||||
|
console.log('Fetching all services...');
|
||||||
const allServices = await serviceService.getServices();
|
const allServices = await serviceService.getServices();
|
||||||
|
console.log('Services found:', allServices);
|
||||||
setServices(allServices);
|
setServices(allServices);
|
||||||
|
|
||||||
// Fetch uptime data for each component that has a service
|
// Fetch uptime data for each component that has a service
|
||||||
|
console.log('Fetching uptime data...');
|
||||||
const uptimePromises = pageComponents
|
const uptimePromises = pageComponents
|
||||||
.filter(component => component.service_id)
|
.filter(component => component.service_id)
|
||||||
.map(async (component) => {
|
.map(async (component) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('Fetching uptime for service:', component.service_id);
|
||||||
const endDate = new Date();
|
const endDate = new Date();
|
||||||
const startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Last 90 days
|
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);
|
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 };
|
return { serviceId: component.service_id, history };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching uptime for service ${component.service_id}:`, 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;
|
uptimeMap[result.serviceId] = result.history;
|
||||||
});
|
});
|
||||||
setUptimeData(uptimeMap);
|
setUptimeData(uptimeMap);
|
||||||
|
console.log('Uptime data set successfully');
|
||||||
|
|
||||||
|
console.log('All data fetched successfully');
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching public page:', err);
|
console.error('Error fetching public page:', err);
|
||||||
setError('Failed to load status page');
|
setError(`Failed to load status page: ${err}`);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user