Update README and documentation; refactor frontend components for improved structure and resilience

This commit is contained in:
glenn schrooyen
2025-09-11 23:46:29 +02:00
parent 63b4bb487d
commit b9206de1a0
49 changed files with 27058 additions and 581 deletions

View File

@@ -1,136 +1,59 @@
import React from 'react';
import { Row, Col, Card, Statistic, Progress, List, Typography } from 'antd';
import {
DashboardOutlined,
ServerOutlined,
DatabaseOutlined,
WifiOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons';
import { Row, Col, Typography, Alert } from 'antd';
import SystemMetrics from './SystemMetrics';
import ServiceStatusBanner from './ServiceStatusBanner';
import SystemStatsCards from './dashboard/SystemStatsCards';
import ServiceStatusList from './dashboard/ServiceStatusList';
import RecentEventsList from './dashboard/RecentEventsList';
import LoadingSpinner from './common/LoadingSpinner';
import { useServiceStatus, useSystemData } from '../hooks/useServiceStatus';
import { ERROR_MESSAGES } from '../constants';
const { Title, Text } = Typography;
const { Title } = Typography;
const Dashboard = () => {
// Mock data - in real app, this would come from API
const systemStats = {
cpu: 45.2,
memory: 68.5,
disk: 32.1,
network: 12.3
const serviceStatus = useServiceStatus();
const { systemStats, services, events: recentEvents, loading, error } = useSystemData();
const handleRefresh = () => {
window.location.reload();
};
const services = [
{ name: 'Home Assistant', status: 'online', uptime: '7d 12h' },
{ name: 'Frigate', status: 'online', uptime: '7d 12h' },
{ name: 'Immich', status: 'online', uptime: '7d 12h' },
{ name: 'n8n', status: 'offline', uptime: '0d 0h' },
{ name: 'PostgreSQL', status: 'online', uptime: '7d 12h' },
{ name: 'Redis', status: 'online', uptime: '7d 12h' }
];
const recentEvents = [
{ time: '2 minutes ago', event: 'Person detected at front door', service: 'Frigate' },
{ time: '5 minutes ago', event: 'CPU usage above 80%', service: 'System' },
{ time: '12 minutes ago', event: 'Alice arrived home', service: 'Home Assistant' },
{ time: '1 hour ago', event: 'New photo uploaded', service: 'Immich' }
];
const getStatusIcon = (status) => {
return status === 'online' ?
<CheckCircleOutlined style={{ color: '#52c41a' }} /> :
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />;
};
if (loading) {
return (
<div className="dashboard-container">
<LoadingSpinner message="Loading dashboard..." />
</div>
);
}
return (
<div className="dashboard-container">
<ServiceStatusBanner serviceStatus={serviceStatus} onRefresh={handleRefresh} />
<Title level={2}>System Overview</Title>
{error && (
<Alert
message={ERROR_MESSAGES.DATA_LOADING_ERROR}
description={error}
type="warning"
style={{ marginBottom: 16 }}
/>
)}
{/* System Metrics */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="CPU Usage"
value={systemStats.cpu}
suffix="%"
prefix={<ServerOutlined />}
/>
<Progress percent={systemStats.cpu} showInfo={false} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Memory Usage"
value={systemStats.memory}
suffix="%"
prefix={<DatabaseOutlined />}
/>
<Progress percent={systemStats.memory} showInfo={false} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Disk Usage"
value={systemStats.disk}
suffix="%"
prefix={<DatabaseOutlined />}
/>
<Progress percent={systemStats.disk} showInfo={false} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Network"
value={systemStats.network}
suffix="Mbps"
prefix={<WifiOutlined />}
/>
</Card>
</Col>
</Row>
<SystemStatsCards systemStats={systemStats} />
<Row gutter={16}>
{/* Service Status */}
<Col span={12}>
<Card title="Service Status" style={{ height: 400 }}>
<List
dataSource={services}
renderItem={(service) => (
<List.Item>
<List.Item.Meta
avatar={getStatusIcon(service.status)}
title={service.name}
description={`Uptime: ${service.uptime}`}
/>
<Text type={service.status === 'online' ? 'success' : 'danger'}>
{service.status.toUpperCase()}
</Text>
</List.Item>
)}
/>
</Card>
<ServiceStatusList services={services} />
</Col>
{/* Recent Events */}
<Col span={12}>
<Card title="Recent Events" style={{ height: 400 }}>
<List
dataSource={recentEvents}
renderItem={(event) => (
<List.Item>
<List.Item.Meta
title={event.event}
description={`${event.time}${event.service}`}
/>
</List.Item>
)}
/>
</Card>
<RecentEventsList events={recentEvents} />
</Col>
</Row>

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { Alert, Button, Space } from 'antd';
import { WifiOutlined, ReloadOutlined } from '@ant-design/icons';
const OfflineMode = ({ onRetry }) => {
return (
<Alert
message="Offline Mode"
description={
<div>
<p>The frontend is running in offline mode because backend services are not available.</p>
<p>To enable full functionality:</p>
<ol style={{ margin: '8px 0', paddingLeft: '20px' }}>
<li>Start the backend services: <code>docker-compose up -d</code></li>
<li>Or start individual services for development</li>
<li>Refresh this page once services are running</li>
</ol>
<Space style={{ marginTop: 12 }}>
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={onRetry}
>
Retry Connection
</Button>
<Button
onClick={() => window.open('http://localhost:8083', '_blank')}
>
Check API Documentation
</Button>
</Space>
</div>
}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
);
};
export default OfflineMode;

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { Alert, Button, Space } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import StatusIcon from './common/StatusIcon';
import { UI_CONSTANTS } from '../constants';
const ServiceStatusBanner = ({ serviceStatus, onRefresh }) => {
const getStatusMessage = () => {
switch (serviceStatus.overall) {
case 'online':
return 'All services are running normally';
case 'partial':
return 'Some services are unavailable - limited functionality';
case 'offline':
return 'Backend services are offline - running in offline mode';
default:
return 'Checking service status...';
}
};
const getStatusType = () => {
switch (serviceStatus.overall) {
case 'online':
return 'success';
case 'partial':
return 'warning';
case 'offline':
return 'error';
default:
return 'info';
}
};
const getServiceDetails = () => {
const details = [];
if (!serviceStatus.apiGateway.available) {
details.push(`API Gateway: ${serviceStatus.apiGateway.error || 'Unavailable'}`);
}
if (!serviceStatus.serviceAdapters.available) {
details.push(`Service Adapters: ${serviceStatus.serviceAdapters.error || 'Unavailable'}`);
}
if (!serviceStatus.apiDocs.available) {
details.push(`API Docs: ${serviceStatus.apiDocs.error || 'Unavailable'}`);
}
return details;
};
if (serviceStatus.overall === 'online') {
return null; // Don't show banner when everything is working
}
return (
<Alert
message={
<Space>
<StatusIcon status={serviceStatus.overall} />
<span>{getStatusMessage()}</span>
{onRefresh && (
<Button
type="link"
size="small"
icon={<ReloadOutlined />}
onClick={onRefresh}
>
Refresh
</Button>
)}
</Space>
}
description={
serviceStatus.overall !== 'checking' && (
<div>
{getServiceDetails().length > 0 && (
<div style={{ marginTop: 8 }}>
<strong>Service Details:</strong>
<ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
{getServiceDetails().map((detail, index) => (
<li key={index}>{detail}</li>
))}
</ul>
</div>
)}
{serviceStatus.overall === 'offline' && (
<div style={{ marginTop: 8 }}>
<strong>Offline Mode:</strong> The frontend is running with fallback data.
Start the backend services to enable full functionality.
</div>
)}
</div>
)
}
type={getStatusType()}
showIcon={false}
style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM }}
closable={serviceStatus.overall === 'partial'}
/>
);
};
export default ServiceStatusBanner;

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { Card, Row, Col, Statistic, Progress } from 'antd';
import { Card, Row, Col, Statistic, Progress, Alert } from 'antd';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
import { useSystemData } from '../hooks/useServiceStatus';
const SystemMetrics = () => {
// Mock data for charts
const { systemStats, loading, error } = useSystemData();
// Mock data for charts (fallback when services are unavailable)
const cpuData = [
{ time: '00:00', cpu: 25 },
{ time: '04:00', cpu: 30 },
@@ -34,26 +37,45 @@ const SystemMetrics = () => {
{ time: '24:00', in: 6, out: 4 }
];
if (loading) {
return (
<Card title="System Performance Metrics">
<div style={{ textAlign: 'center', padding: '50px' }}>
Loading metrics...
</div>
</Card>
);
}
return (
<div>
{error && (
<Alert
message="Metrics Unavailable"
description="Real-time metrics are not available. Showing sample data."
type="warning"
style={{ marginBottom: 16 }}
/>
)}
<Card title="System Performance Metrics" style={{ marginBottom: 16 }}>
<Row gutter={16}>
<Col span={8}>
<Card size="small">
<Statistic title="CPU Usage (24h)" value={45.2} suffix="%" />
<Progress percent={45.2} showInfo={false} />
<Statistic title="CPU Usage (24h)" value={systemStats.cpu || 0} suffix="%" />
<Progress percent={systemStats.cpu || 0} showInfo={false} />
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic title="Memory Usage (24h)" value={68.5} suffix="%" />
<Progress percent={68.5} showInfo={false} />
<Statistic title="Memory Usage (24h)" value={systemStats.memory || 0} suffix="%" />
<Progress percent={systemStats.memory || 0} showInfo={false} />
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic title="Disk Usage" value={32.1} suffix="%" />
<Progress percent={32.1} showInfo={false} />
<Statistic title="Disk Usage" value={systemStats.disk || 0} suffix="%" />
<Progress percent={systemStats.disk || 0} showInfo={false} />
</Card>
</Col>
</Row>

View File

@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Button } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({
error,
errorInfo
});
// Log error to console in development
if (process.env.NODE_ENV === 'development') {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
}
handleReload = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '24px', textAlign: 'center' }}>
<Alert
message="Something went wrong"
description={
<div>
<p>The application encountered an unexpected error.</p>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details style={{ marginTop: '16px', textAlign: 'left' }}>
<summary>Error Details (Development)</summary>
<pre style={{ marginTop: '8px', fontSize: '12px' }}>
{this.state.error.toString()}
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={this.handleReload}
style={{ marginTop: '16px' }}
>
Reload Page
</Button>
</div>
}
type="error"
showIcon
/>
</div>
);
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired
};
export default ErrorBoundary;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Spin } from 'antd';
import { UI_CONSTANTS } from '../../constants';
const LoadingSpinner = ({
message = 'Loading...',
size = UI_CONSTANTS.SPINNER_SIZE,
centered = true
}) => {
const containerStyle = centered ? {
textAlign: 'center',
padding: `${UI_CONSTANTS.PADDING.LARGE}px`
} : {};
return (
<div style={containerStyle}>
<Spin size={size} />
{message && (
<div style={{ marginTop: 16 }}>
{message}
</div>
)}
</div>
);
};
LoadingSpinner.propTypes = {
message: PropTypes.string,
size: PropTypes.oneOf(['small', 'default', 'large']),
centered: PropTypes.bool
};
export default LoadingSpinner;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
CloseCircleOutlined
} from '@ant-design/icons';
import { COLORS } from '../../constants';
const StatusIcon = ({ status, size = 'default' }) => {
const iconProps = {
style: {
color: getStatusColor(status),
fontSize: size === 'large' ? '20px' : '16px'
}
};
switch (status) {
case 'online':
return <CheckCircleOutlined {...iconProps} />;
case 'partial':
return <ExclamationCircleOutlined {...iconProps} />;
case 'offline':
return <CloseCircleOutlined {...iconProps} />;
default:
return <ExclamationCircleOutlined {...iconProps} />;
}
};
const getStatusColor = (status) => {
switch (status) {
case 'online':
return COLORS.SUCCESS;
case 'partial':
return COLORS.WARNING;
case 'offline':
return COLORS.ERROR;
default:
return COLORS.DISABLED;
}
};
StatusIcon.propTypes = {
status: PropTypes.oneOf(['online', 'partial', 'offline', 'checking']).isRequired,
size: PropTypes.oneOf(['default', 'large'])
};
export default StatusIcon;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, List } from 'antd';
import { UI_CONSTANTS } from '../../constants';
const RecentEventsList = ({ events }) => {
const renderEventItem = (event) => (
<List.Item>
<List.Item.Meta
title={event.event}
description={`${event.time}${event.service}`}
/>
</List.Item>
);
return (
<Card title="Recent Events" style={{ height: UI_CONSTANTS.CARD_HEIGHT }}>
<List
dataSource={events}
renderItem={renderEventItem}
/>
</Card>
);
};
RecentEventsList.propTypes = {
events: PropTypes.arrayOf(PropTypes.shape({
time: PropTypes.string.isRequired,
event: PropTypes.string.isRequired,
service: PropTypes.string.isRequired
})).isRequired
};
export default RecentEventsList;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, List, Typography } from 'antd';
import StatusIcon from '../common/StatusIcon';
import { UI_CONSTANTS } from '../../constants';
const { Text } = Typography;
const ServiceStatusList = ({ services }) => {
const renderServiceItem = (service) => (
<List.Item>
<List.Item.Meta
avatar={<StatusIcon status={service.status} />}
title={service.name}
description={`Uptime: ${service.uptime}`}
/>
<Text type={service.status === 'online' ? 'success' : 'danger'}>
{service.status.toUpperCase()}
</Text>
</List.Item>
);
return (
<Card title="Service Status" style={{ height: UI_CONSTANTS.CARD_HEIGHT }}>
<List
dataSource={services}
renderItem={renderServiceItem}
/>
</Card>
);
};
ServiceStatusList.propTypes = {
services: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
status: PropTypes.oneOf(['online', 'offline']).isRequired,
uptime: PropTypes.string.isRequired
})).isRequired
};
export default ServiceStatusList;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col, Card, Statistic, Progress } from 'antd';
import {
ServerOutlined,
DatabaseOutlined,
WifiOutlined
} from '@ant-design/icons';
import { UI_CONSTANTS } from '../../constants';
const SystemStatsCards = ({ systemStats }) => {
const stats = [
{
key: 'cpu',
title: 'CPU Usage',
value: systemStats.cpu || 0,
suffix: '%',
prefix: <ServerOutlined />
},
{
key: 'memory',
title: 'Memory Usage',
value: systemStats.memory || 0,
suffix: '%',
prefix: <DatabaseOutlined />
},
{
key: 'disk',
title: 'Disk Usage',
value: systemStats.disk || 0,
suffix: '%',
prefix: <DatabaseOutlined />
},
{
key: 'network',
title: 'Network',
value: systemStats.network || 0,
suffix: 'Mbps',
prefix: <WifiOutlined />
}
];
return (
<Row gutter={16} style={{ marginBottom: UI_CONSTANTS.MARGIN_TOP }}>
{stats.map((stat) => (
<Col span={6} key={stat.key}>
<Card>
<Statistic
title={stat.title}
value={stat.value}
suffix={stat.suffix}
prefix={stat.prefix}
/>
{stat.suffix === '%' && (
<Progress
percent={stat.value}
showInfo={false}
/>
)}
</Card>
</Col>
))}
</Row>
);
};
SystemStatsCards.propTypes = {
systemStats: PropTypes.shape({
cpu: PropTypes.number,
memory: PropTypes.number,
disk: PropTypes.number,
network: PropTypes.number
}).isRequired
};
export default SystemStatsCards;