initial project setup

This commit is contained in:
glenn schrooyen
2025-09-11 22:08:12 +02:00
parent 8cc588dc92
commit 21e4972ab1
46 changed files with 2755 additions and 1 deletions

24
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Install serve to run the app
RUN npm install -g serve
# Expose port
EXPOSE 3000
# Start the application
CMD ["serve", "-s", "build", "-l", "3000"]

18
frontend/Dockerfile.dev Normal file
View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Expose port
EXPOSE 3000
# Run in development mode with hot reload
CMD ["npm", "start"]

50
frontend/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "labfusion-frontend",
"version": "1.0.0",
"description": "LabFusion Dashboard Frontend",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"react-router-dom": "^6.8.1",
"axios": "^1.6.2",
"recharts": "^2.8.0",
"antd": "^5.12.8",
"@ant-design/icons": "^5.2.6",
"styled-components": "^6.1.6",
"react-query": "^3.39.3",
"react-hook-form": "^7.48.2",
"date-fns": "^2.30.0",
"lodash": "^4.17.21",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:8080"
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="LabFusion - Unified homelab dashboard and integration hub"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>LabFusion Dashboard</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

111
frontend/src/App.css Normal file
View File

@@ -0,0 +1,111 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
padding: 20px;
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
padding: 16px;
}
.widget {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.widget-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #262626;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.metric-card {
background: white;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.metric-value {
font-size: 2.5rem;
font-weight: bold;
color: #1890ff;
margin-bottom: 8px;
}
.metric-label {
color: #8c8c8c;
font-size: 14px;
}
.status-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: white;
border-radius: 8px;
margin-bottom: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 12px;
}
.status-online {
background-color: #52c41a;
}
.status-offline {
background-color: #ff4d4f;
}
.status-unknown {
background-color: #d9d9d9;
}

64
frontend/src/App.js Normal file
View File

@@ -0,0 +1,64 @@
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { Layout, Menu, Typography } from 'antd';
import { DashboardOutlined, SettingOutlined, BarChartOutlined } from '@ant-design/icons';
import Dashboard from './components/Dashboard';
import SystemMetrics from './components/SystemMetrics';
import Settings from './components/Settings';
import './App.css';
const { Header, Sider, Content } = Layout;
const { Title } = Typography;
function App() {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider width={250} theme="dark">
<div style={{ padding: '16px', textAlign: 'center' }}>
<Title level={3} style={{ color: 'white', margin: 0 }}>
LabFusion
</Title>
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['dashboard']}
items={[
{
key: 'dashboard',
icon: <DashboardOutlined />,
label: 'Dashboard',
},
{
key: 'metrics',
icon: <BarChartOutlined />,
label: 'System Metrics',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: 'Settings',
},
]}
/>
</Sider>
<Layout>
<Header style={{ background: '#fff', padding: '0 24px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<Title level={4} style={{ margin: 0, lineHeight: '64px' }}>
Homelab Dashboard
</Title>
</Header>
<Content style={{ margin: '24px', background: '#fff', borderRadius: '8px' }}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/metrics" element={<SystemMetrics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Content>
</Layout>
</Layout>
);
}
export default App;

View File

@@ -0,0 +1,147 @@
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 SystemMetrics from './SystemMetrics';
const { Title, Text } = 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 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' }} />;
};
return (
<div className="dashboard-container">
<Title level={2}>System Overview</Title>
{/* 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>
<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>
</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>
</Col>
</Row>
{/* System Metrics Chart */}
<Row style={{ marginTop: 24 }}>
<Col span={24}>
<SystemMetrics />
</Col>
</Row>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,124 @@
import React, { useState } from 'react';
import { Card, Form, Input, Button, Switch, Select, Divider, Typography, message } from 'antd';
const { Title, Text } = Typography;
const { Option } = Select;
const Settings = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const onFinish = (values) => {
setLoading(true);
// Simulate API call
setTimeout(() => {
setLoading(false);
message.success('Settings saved successfully!');
}, 1000);
};
return (
<div className="dashboard-container">
<Title level={2}>Settings</Title>
<Card title="Service Integrations" style={{ marginBottom: 24 }}>
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{
homeAssistant: {
enabled: true,
url: 'http://homeassistant.local:8123',
token: 'your-token-here'
},
frigate: {
enabled: true,
url: 'http://frigate.local:5000',
token: 'your-token-here'
},
immich: {
enabled: false,
url: 'http://immich.local:2283',
apiKey: 'your-api-key-here'
}
}}
>
{/* Home Assistant */}
<Card size="small" title="Home Assistant" style={{ marginBottom: 16 }}>
<Form.Item name={['homeAssistant', 'enabled']} valuePropName="checked">
<Switch checkedChildren="Enabled" unCheckedChildren="Disabled" />
</Form.Item>
<Form.Item label="URL" name={['homeAssistant', 'url']}>
<Input placeholder="http://homeassistant.local:8123" />
</Form.Item>
<Form.Item label="Token" name={['homeAssistant', 'token']}>
<Input.Password placeholder="Your Home Assistant token" />
</Form.Item>
</Card>
{/* Frigate */}
<Card size="small" title="Frigate" style={{ marginBottom: 16 }}>
<Form.Item name={['frigate', 'enabled']} valuePropName="checked">
<Switch checkedChildren="Enabled" unCheckedChildren="Disabled" />
</Form.Item>
<Form.Item label="URL" name={['frigate', 'url']}>
<Input placeholder="http://frigate.local:5000" />
</Form.Item>
<Form.Item label="Token" name={['frigate', 'token']}>
<Input.Password placeholder="Your Frigate token" />
</Form.Item>
</Card>
{/* Immich */}
<Card size="small" title="Immich" style={{ marginBottom: 16 }}>
<Form.Item name={['immich', 'enabled']} valuePropName="checked">
<Switch checkedChildren="Enabled" unCheckedChildren="Disabled" />
</Form.Item>
<Form.Item label="URL" name={['immich', 'url']}>
<Input placeholder="http://immich.local:2283" />
</Form.Item>
<Form.Item label="API Key" name={['immich', 'apiKey']}>
<Input.Password placeholder="Your Immich API key" />
</Form.Item>
</Card>
<Button type="primary" htmlType="submit" loading={loading}>
Save Settings
</Button>
</Form>
</Card>
<Card title="Dashboard Configuration">
<Form layout="vertical">
<Form.Item label="Default Dashboard Layout">
<Select defaultValue="grid" style={{ width: 200 }}>
<Option value="grid">Grid Layout</Option>
<Option value="list">List Layout</Option>
<Option value="custom">Custom Layout</Option>
</Select>
</Form.Item>
<Form.Item label="Auto-refresh Interval">
<Select defaultValue="30" style={{ width: 200 }}>
<Option value="10">10 seconds</Option>
<Option value="30">30 seconds</Option>
<Option value="60">1 minute</Option>
<Option value="300">5 minutes</Option>
</Select>
</Form.Item>
<Form.Item label="Theme">
<Select defaultValue="light" style={{ width: 200 }}>
<Option value="light">Light</Option>
<Option value="dark">Dark</Option>
<Option value="auto">Auto</Option>
</Select>
</Form.Item>
</Form>
</Card>
</div>
);
};
export default Settings;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { Card, Row, Col, Statistic, Progress } from 'antd';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
const SystemMetrics = () => {
// Mock data for charts
const cpuData = [
{ time: '00:00', cpu: 25 },
{ time: '04:00', cpu: 30 },
{ time: '08:00', cpu: 45 },
{ time: '12:00', cpu: 60 },
{ time: '16:00', cpu: 55 },
{ time: '20:00', cpu: 40 },
{ time: '24:00', cpu: 35 }
];
const memoryData = [
{ time: '00:00', memory: 2.1 },
{ time: '04:00', memory: 2.3 },
{ time: '08:00', memory: 2.8 },
{ time: '12:00', memory: 3.2 },
{ time: '16:00', memory: 3.0 },
{ time: '20:00', memory: 2.7 },
{ time: '24:00', memory: 2.4 }
];
const networkData = [
{ time: '00:00', in: 5, out: 3 },
{ time: '04:00', in: 8, out: 4 },
{ time: '08:00', in: 15, out: 8 },
{ time: '12:00', in: 20, out: 12 },
{ time: '16:00', in: 18, out: 10 },
{ time: '20:00', in: 12, out: 7 },
{ time: '24:00', in: 6, out: 4 }
];
return (
<div>
<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} />
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic title="Memory Usage (24h)" value={68.5} suffix="%" />
<Progress percent={68.5} showInfo={false} />
</Card>
</Col>
<Col span={8}>
<Card size="small">
<Statistic title="Disk Usage" value={32.1} suffix="%" />
<Progress percent={32.1} showInfo={false} />
</Card>
</Col>
</Row>
</Card>
<Row gutter={16}>
<Col span={12}>
<Card title="CPU Usage Over Time">
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={cpuData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="cpu" stroke="#1890ff" fill="#1890ff" fillOpacity={0.3} />
</AreaChart>
</ResponsiveContainer>
</Card>
</Col>
<Col span={12}>
<Card title="Memory Usage Over Time">
<ResponsiveContainer width="100%" height={300}>
<LineChart data={memoryData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="memory" stroke="#52c41a" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</Card>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: 16 }}>
<Col span={24}>
<Card title="Network Traffic">
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={networkData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="in" stackId="1" stroke="#1890ff" fill="#1890ff" fillOpacity={0.6} />
<Area type="monotone" dataKey="out" stackId="1" stroke="#52c41a" fill="#52c41a" fillOpacity={0.6} />
</AreaChart>
</ResponsiveContainer>
</Card>
</Col>
</Row>
</div>
);
};
export default SystemMetrics;

70
frontend/src/index.css Normal file
View File

@@ -0,0 +1,70 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f5f5;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
box-sizing: border-box;
}
.dashboard-container {
padding: 24px;
min-height: 100vh;
}
.widget-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.metric-card {
text-align: center;
padding: 16px;
}
.metric-value {
font-size: 2rem;
font-weight: bold;
color: #1890ff;
}
.metric-label {
color: #666;
margin-top: 8px;
}
.chart-container {
height: 300px;
padding: 16px;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-online {
background-color: #52c41a;
}
.status-offline {
background-color: #ff4d4f;
}
.status-unknown {
background-color: #d9d9d9;
}

29
frontend/src/index.js Normal file
View File

@@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ConfigProvider } from 'antd';
import App from './App';
import './index.css';
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ConfigProvider
theme={{
token: {
colorPrimary: '#1890ff',
borderRadius: 6,
},
}}
>
<App />
</ConfigProvider>
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>
);