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

91
.gitignore vendored Normal file
View File

@@ -0,0 +1,91 @@
# Dependencies
node_modules/
*/node_modules/
# Production builds
build/
dist/
target/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
env.bak/
venv.bak/
# Database
*.db
*.sqlite
*.sqlite3
# Docker
.dockerignore

180
README.md
View File

@@ -1,2 +1,180 @@
# labFusion
# LabFusion
A unified dashboard and integration hub for your homelab services. LabFusion provides a centralized view of your Home Assistant, Frigate, Immich, n8n, and other homelab services with real-time monitoring, data correlation, and customizable dashboards.
## Features
- **Unified Dashboard**: Single interface for all homelab services
- **Real-time Monitoring**: System metrics, service status, and event tracking
- **Service Integration**: Connect to Home Assistant, Frigate, Immich, n8n, and more
- **Data Correlation**: Cross-service insights and event correlation
- **Customizable Widgets**: Build dashboards with charts, tables, and status cards
- **Polyglot Architecture**: Java Spring Boot API gateway with Python FastAPI adapters
- **Dockerized Deployment**: Easy setup with Docker Compose
## Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ React │ │ Spring Boot │ │ FastAPI │
│ Frontend │◄──►│ API Gateway │◄──►│ Adapters │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ PostgreSQL │ │ Redis │
│ Database │ │ Message Bus │
└─────────────────┘ └─────────────────┘
```
## Quick Start
### Prerequisites
- Docker and Docker Compose
- Git
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd labfusion
```
2. Copy the environment configuration:
```bash
cp env.example .env
```
3. Edit `.env` file with your service URLs and tokens:
```bash
# Update these with your actual service URLs and tokens
HOME_ASSISTANT_URL=http://homeassistant.local:8123
HOME_ASSISTANT_TOKEN=your-ha-token-here
FRIGATE_URL=http://frigate.local:5000
FRIGATE_TOKEN=your-frigate-token-here
IMMICH_URL=http://immich.local:2283
IMMICH_API_KEY=your-immich-api-key-here
```
4. Start the services:
```bash
docker-compose up -d
```
5. Access the application:
- Frontend: http://localhost:3000
- API Gateway: http://localhost:8080
- Service Adapters: http://localhost:8000
## Services
### API Gateway (Spring Boot)
- **Port**: 8080
- **Purpose**: Core API, authentication, user management
- **Database**: PostgreSQL
- **Features**: JWT authentication, dashboard management, event storage
### Service Adapters (FastAPI)
- **Port**: 8000
- **Purpose**: Integration with external services
- **Features**: Home Assistant, Frigate, Immich, n8n integrations
### Frontend (React)
- **Port**: 3000
- **Purpose**: Dashboard UI
- **Features**: Real-time updates, customizable widgets, responsive design
### Database (PostgreSQL)
- **Port**: 5432
- **Purpose**: Persistent storage
- **Data**: Users, dashboards, widgets, events, device states
### Message Bus (Redis)
- **Port**: 6379
- **Purpose**: Inter-service communication
- **Features**: Event publishing, real-time updates
## Development
### Backend Development
#### Java API Gateway
```bash
cd backend/api-gateway
mvn spring-boot:run
```
#### Python Service Adapters
```bash
cd backend/service-adapters
pip install -r requirements.txt
uvicorn main:app --reload
```
### Frontend Development
```bash
cd frontend
npm install
npm start
```
## Configuration
### Service Integration
1. **Home Assistant**:
- Get your token from Profile → Long-lived access tokens
- Update `HOME_ASSISTANT_URL` and `HOME_ASSISTANT_TOKEN` in `.env`
2. **Frigate**:
- Get your API key from Frigate settings
- Update `FRIGATE_URL` and `FRIGATE_TOKEN` in `.env`
3. **Immich**:
- Get your API key from Immich settings
- Update `IMMICH_URL` and `IMMICH_API_KEY` in `.env`
### Dashboard Customization
- Access the Settings page to configure service integrations
- Use the dashboard builder to create custom layouts
- Add widgets for different data sources and visualizations
## API Documentation
- **API Gateway**: http://localhost:8080/swagger-ui.html
- **Service Adapters**: http://localhost:8000/docs
## Roadmap
- [x] Basic project structure and Docker setup
- [x] Spring Boot API gateway with authentication
- [x] FastAPI service adapters
- [x] React frontend with dashboard
- [ ] Home Assistant integration
- [ ] Frigate integration
- [ ] Immich integration
- [ ] Real-time WebSocket updates
- [ ] Advanced dashboard builder
- [ ] Data correlation engine
- [ ] Notification system
- [ ] Kubernetes deployment
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Support
For questions and support, please open an issue on GitHub.

95
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,95 @@
version: '3.8'
services:
# Database
postgres:
image: postgres:15
environment:
POSTGRES_DB: labfusion
POSTGRES_USER: labfusion
POSTGRES_PASSWORD: labfusion_password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- labfusion-network
# Redis for message bus
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- labfusion-network
# Java Spring Boot API Gateway (Development)
api-gateway:
build:
context: ./services/api-gateway
dockerfile: Dockerfile.dev
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/labfusion
- SPRING_DATASOURCE_USERNAME=labfusion
- SPRING_DATASOURCE_PASSWORD=labfusion_password
- REDIS_HOST=redis
- REDIS_PORT=6379
- SPRING_PROFILES_ACTIVE=dev
depends_on:
- postgres
- redis
networks:
- labfusion-network
volumes:
- ./services/api-gateway:/app
- maven_cache:/root/.m2
# Python FastAPI Service Adapters (Development)
service-adapters:
build:
context: ./services/service-adapters
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
- POSTGRES_URL=postgresql://labfusion:labfusion_password@postgres:5432/labfusion
depends_on:
- postgres
- redis
networks:
- labfusion-network
volumes:
- ./services/service-adapters:/app
# React Frontend (Development)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
environment:
- REACT_APP_API_URL=http://localhost:8080
- REACT_APP_WEBSOCKET_URL=ws://localhost:8080/ws
depends_on:
- api-gateway
networks:
- labfusion-network
volumes:
- ./frontend:/app
- /app/node_modules
volumes:
postgres_data:
redis_data:
maven_cache:
networks:
labfusion-network:
driver: bridge

85
docker-compose.yml Normal file
View File

@@ -0,0 +1,85 @@
version: '3.8'
services:
# Database
postgres:
image: postgres:15
environment:
POSTGRES_DB: labfusion
POSTGRES_USER: labfusion
POSTGRES_PASSWORD: labfusion_password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- labfusion-network
# Redis for message bus
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- labfusion-network
# Java Spring Boot API Gateway
api-gateway:
build:
context: ./services/api-gateway
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/labfusion
- SPRING_DATASOURCE_USERNAME=labfusion
- SPRING_DATASOURCE_PASSWORD=labfusion_password
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- postgres
- redis
networks:
- labfusion-network
# Python FastAPI Service Adapters
service-adapters:
build:
context: ./services/service-adapters
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
- POSTGRES_URL=postgresql://labfusion:labfusion_password@postgres:5432/labfusion
depends_on:
- postgres
- redis
networks:
- labfusion-network
# React Frontend
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- REACT_APP_API_URL=http://localhost:8080
- REACT_APP_WEBSOCKET_URL=ws://localhost:8080/ws
depends_on:
- api-gateway
networks:
- labfusion-network
volumes:
postgres_data:
redis_data:
networks:
labfusion-network:
driver: bridge

161
docs/progress.md Normal file
View File

@@ -0,0 +1,161 @@
# LabFusion Development Progress
## Project Overview
LabFusion is a unified dashboard and integration hub for homelab services, built with a polyglot microservices architecture.
## Architecture Decisions
- **Modular Services**: Each service is independently deployable
- **Polyglot**: Services can use different languages (Java, Python, Go, Node.js)
- **Message Bus**: Redis for inter-service communication
- **Database**: PostgreSQL for persistent data
- **Containerization**: Docker for all services
## Completed Tasks ✅
### Phase 1: Project Structure & Foundation
- [x] **Project Structure Setup** (2024-11-09)
- Created modular `services/` directory structure
- Moved from `backend/` to `services/` for better modularity
- Each service has its own directory with independent Docker setup
- [x] **API Gateway Service** (2024-11-09)
- Java Spring Boot application
- JPA entities for User, Dashboard, Widget, Event, DeviceState
- REST controllers for dashboard management
- PostgreSQL integration
- Redis message bus support
- JWT authentication framework
- [x] **Service Adapters** (2024-11-09)
- Python FastAPI application
- Integration endpoints for Home Assistant, Frigate, Immich, n8n
- Redis event publishing
- Configurable service connections
- Mock data for development
- [x] **Frontend Dashboard** (2024-11-09)
- React application with Ant Design
- System metrics visualization
- Service status monitoring
- Settings configuration
- Responsive design
- [x] **Infrastructure Setup** (2024-11-09)
- Docker Compose for production and development
- PostgreSQL database configuration
- Redis message bus setup
- Environment configuration templates
- [x] **Documentation** (2024-11-09)
- Comprehensive README with setup instructions
- Service-specific README files
- Progress tracking document
- Updated project structure documentation
## Current Status 🚧
### Services Directory Structure
```
services/
├── api-gateway/ # Java Spring Boot (Port 8080) ✅
├── service-adapters/ # Python FastAPI (Port 8000) ✅
├── metrics-collector/ # Go service (Port 8081) 🚧
└── notification-service/ # Node.js service (Port 8082) 🚧
```
### Infrastructure
- **Database**: PostgreSQL (Port 5432) ✅
- **Message Bus**: Redis (Port 6379) ✅
- **Frontend**: React (Port 3000) ✅
- **Containerization**: Docker Compose ✅
## Next Steps 🎯
### Phase 2: Service Implementation
- [ ] **Metrics Collector Service** (Go)
- Implement Docker API integration
- Add Prometheus metrics collection
- Create Redis publishing mechanism
- Add configuration management
- [ ] **Notification Service** (Node.js/TypeScript)
- Implement notification channels (email, webhook, push)
- Create alert rule engine
- Add notification preferences
- Integrate with Redis for event processing
### Phase 3: Real Service Integrations
- [ ] **Home Assistant Integration**
- Implement actual HA API calls
- Add WebSocket support for real-time updates
- Create entity state synchronization
- [ ] **Frigate Integration**
- Implement Frigate API integration
- Add event processing and filtering
- Create detection timeline visualization
- [ ] **Immich Integration**
- Implement Immich API calls
- Add photo metadata processing
- Create asset management features
### Phase 4: Advanced Features
- [ ] **Real-time Updates**
- WebSocket implementation for live data
- Event streaming from Redis
- Frontend real-time UI updates
- [ ] **Data Correlation Engine**
- Cross-service event correlation
- Timeline analysis
- Pattern detection
- [ ] **Authentication & Security**
- JWT token implementation
- User management
- Role-based access control
### Phase 5: Production Readiness
- [ ] **Monitoring & Logging**
- Centralized logging
- Health checks
- Performance monitoring
- [ ] **CI/CD Pipeline**
- GitHub Actions setup
- Automated testing
- Deployment automation
- [ ] **Kubernetes Migration**
- Helm charts
- Kubernetes manifests
- Production deployment
## Development Notes
### Service Independence
Each service is designed to be:
- Independently deployable
- Language-agnostic
- Self-contained with its own dependencies
- Communicating via Redis message bus
### Future Service Additions
The modular structure allows for easy addition of new services:
- **Data Processing Service** (Rust) - For heavy data processing
- **ML Service** (Python) - For machine learning features
- **API Gateway v2** (Go) - For high-performance routing
- **Cache Service** (C++) - For high-performance caching
## Technical Debt
- [ ] Add comprehensive error handling
- [ ] Implement proper logging across all services
- [ ] Add unit and integration tests
- [ ] Create API documentation with OpenAPI/Swagger
- [ ] Add health check endpoints for all services
## Resources
- [Project Specifications](specs.md)
- [Project Structure](structure.txt)
- [Main README](../README.md)

126
docs/specs.md Normal file
View File

@@ -0,0 +1,126 @@
# Project Specification: **LabFusion**
## 1. Purpose & Goals
* Provide a **unified application** that integrates data and metrics from multiple homelab services (Home Assistant, Frigate, Immich, n8n, etc.).
* Enable creation of **dashboards** that visualize system and service performance (CPU, RAM, storage, uptime, etc.).
* Support **data mashups** (cross-service insights, e.g., "Frigate detected a person → check HA presence → enrich with Immich images").
* Serve as a **learning playground** for:
* Running **polyglot microservices** (Python, Java/Spring, modern frontend).
* Hosting everything with **Docker Compose** (extendable to Kubernetes later).
* Building **APIs and integrations** across services.
---
## 2. User Stories
### Core / First Iteration
* As a homelab owner, I want to see a **system dashboard** that shows CPU, RAM, and storage usage of all devices (Docker host, NAS, etc.).
* As a homelab owner, I want to see the **status of each service** (running, uptime, logs, alerts).
* As a homelab owner, I want to have a **dashboard builder** (predefined widgets like charts, tables, logs).
### Advanced / Later Iterations
* As a homelab owner, I want to **combine service data** (e.g., Frigate face detection + HA presence info).
* As a homelab owner, I want **notifications** (e.g., service down, high CPU, suspicious camera activity).
* As a homelab owner, I want the app to expose an **API layer** that other tools (like n8n) can consume.
---
## 3. Functional Requirements
1. **System Monitoring**
* Collect metrics from Docker hosts & containers (CPU, memory, disk, network).
* Display service health (via APIs, logs, or container status).
2. **Service Integrations**
* **Home Assistant**: read entity states via its REST/WebSocket API.
* **Frigate**: consume event data (detections, timelines).
* **Immich**: access image metadata (faces, tags, time).
* **n8n**: optional integration (trigger workflows from events).
3. **Dashboards**
* Modular widget-based UI (charts, tables, cards).
* Configurable dashboards (JSON schema for layout).
* Examples:
* Host performance overview.
* Container uptime.
* HA device states.
* Frigate detections timeline.
4. **Data Correlation (Phase 2)**
* Join data across services.
* Example: *Frigate detected “Alice” at 18:42 → HA shows Alices phone connected → Confirm Alice was home.*
---
## 4. Non-Functional Requirements
* **Performance**: Handle \~1000 events/day without lag.
* **Scalability**: Add more services without rewriting core.
* **Security**: Local auth (JWT/OAuth2), role-based access (later).
* **Resilience**: Auto-restart containers, graceful error handling.
* **Extensibility**: Plugin-like integration architecture.
---
## 5. System Architecture
* **Frontend (React, Vue, or Svelte)**
* Dashboard UI (charts, cards, widgets).
* Talks to backend via REST/WebSocket API.
* **Backend (polyglot)**
* **Java/Spring Boot**: Core API gateway, auth, user management.
* **Python FastAPI**: Service integration adapters (HA, Frigate, Immich).
* **Shared Message Bus** (e.g., Redis, Kafka, or RabbitMQ): decouple services.
* **Database**: PostgreSQL (persistent config, dashboards, event history).
* **Dockerized Deployment**
* Each component in its own container.
* Docker Compose orchestrates services.
* Future upgrade path: Kubernetes.
---
## 6. Data Model (simplified)
* **User** (id, username, roles).
* **Dashboard** (id, name, layout, widgets).
* **Widget** (id, type, config, service binding).
* **Event** (timestamp, service, type, metadata).
* **Device/Service State** (timestamp, entity\_id, value).
---
## 7. Deployment & Operations
* **Docker Compose stack** (frontend, Spring API, FastAPI adapters, DB, Redis).
* CI/CD pipeline (GitHub Actions).
* Monitoring (Prometheus + Grafana, optional since youre building dashboards).
* Config via `.env` or centralized config service.
---
## Roadmap
* **Iteration 1**: Build basic dashboards with host & container metrics.
* **Iteration 2**: Add service integrations (HA, Frigate).
* **Iteration 3**: Implement cross-service correlations (the "Alice detection" scenario).
* **Iteration 4**: Add notifications & automations.
---
## Project Title
**LabFusion** a unified dashboard and integration hub for your homelab.

55
docs/structure.txt Normal file
View File

@@ -0,0 +1,55 @@
labfusion/
├── docker-compose.yml # Production Docker setup
├── docker-compose.dev.yml # Development Docker setup
├── env.example # Environment configuration template
├── .gitignore # Git ignore rules
├── README.md # Comprehensive documentation
├── services/ # Modular microservices
│ ├── api-gateway/ # Java Spring Boot API Gateway (Port 8080)
│ │ ├── src/main/java/com/labfusion/
│ │ │ ├── model/ # JPA entities (User, Dashboard, Widget, Event, DeviceState)
│ │ │ ├── repository/ # Data repositories
│ │ │ ├── service/ # Business logic
│ │ │ └── controller/ # REST controllers
│ │ ├── src/main/resources/
│ │ │ └── application.yml # Spring configuration
│ │ ├── pom.xml # Maven dependencies
│ │ ├── Dockerfile # Production container
│ │ ├── Dockerfile.dev # Development container
│ │ └── README.md # Service documentation
│ ├── service-adapters/ # Python FastAPI Service Adapters (Port 8000)
│ │ ├── main.py # FastAPI application
│ │ ├── requirements.txt # Python dependencies
│ │ ├── Dockerfile # Production container
│ │ ├── Dockerfile.dev # Development container
│ │ └── README.md # Service documentation
│ ├── metrics-collector/ # Go Metrics Collector (Port 8081) 🚧
│ │ ├── main.go # Go application (planned)
│ │ ├── go.mod # Go dependencies (planned)
│ │ ├── Dockerfile # Production container (planned)
│ │ ├── Dockerfile.dev # Development container (planned)
│ │ └── README.md # Service documentation
│ └── notification-service/ # Node.js Notification Service (Port 8082) 🚧
│ ├── src/ # TypeScript source (planned)
│ ├── package.json # Node.js dependencies (planned)
│ ├── Dockerfile # Production container (planned)
│ ├── Dockerfile.dev # Development container (planned)
│ └── README.md # Service documentation
├── frontend/ # React Frontend (Port 3000)
│ ├── src/
│ │ ├── components/ # React components
│ │ │ ├── Dashboard.js # Main dashboard
│ │ │ ├── SystemMetrics.js # Metrics visualization
│ │ │ └── Settings.js # Configuration UI
│ │ ├── App.js # Main app component
│ │ ├── index.js # App entry point
│ │ └── index.css # Global styles
│ ├── public/
│ │ └── index.html # HTML template
│ ├── package.json # Node.js dependencies
│ ├── Dockerfile # Production container
│ └── Dockerfile.dev # Development container
└── docs/ # Documentation
├── specs.md # Project specifications
├── structure.txt # Project structure
└── progress.md # Development progress tracking

32
env.example Normal file
View File

@@ -0,0 +1,32 @@
# Database Configuration
POSTGRES_DB=labfusion
POSTGRES_USER=labfusion
POSTGRES_PASSWORD=labfusion_password
# Redis Configuration
REDIS_HOST=redis
REDIS_PORT=6379
# API Gateway Configuration
API_GATEWAY_PORT=8080
JWT_SECRET=your-jwt-secret-key-here
# Service Adapters Configuration
SERVICE_ADAPTERS_PORT=8000
# Frontend Configuration
REACT_APP_API_URL=http://localhost:8080
REACT_APP_WEBSOCKET_URL=ws://localhost:8080/ws
# Service Integration URLs (update with your actual service URLs)
HOME_ASSISTANT_URL=http://homeassistant.local:8123
HOME_ASSISTANT_TOKEN=your-ha-token-here
FRIGATE_URL=http://frigate.local:5000
FRIGATE_TOKEN=your-frigate-token-here
IMMICH_URL=http://immich.local:2283
IMMICH_API_KEY=your-immich-api-key-here
N8N_URL=http://n8n.local:5678
N8N_WEBHOOK_URL=your-n8n-webhook-url-here

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>
);

View File

@@ -0,0 +1,17 @@
FROM openjdk:17-jdk-slim
WORKDIR /app
# Copy Maven files
COPY pom.xml .
COPY src ./src
# Install Maven
RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/*
# Build the application
RUN mvn clean package -DskipTests
# Run the application
EXPOSE 8080
CMD ["java", "-jar", "target/api-gateway-1.0.0.jar"]

View File

@@ -0,0 +1,21 @@
FROM openjdk:17-jdk-slim
WORKDIR /app
# Install Maven
RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/*
# Copy Maven files
COPY pom.xml .
# Download dependencies
RUN mvn dependency:go-offline -B
# Copy source code
COPY src ./src
# Expose port
EXPOSE 8080
# Run in development mode with hot reload
CMD ["mvn", "spring-boot:run", "-Dspring-boot.run.jvmArguments='-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005'"]

View File

@@ -0,0 +1,26 @@
# API Gateway Service
The core API gateway for LabFusion, built with Java Spring Boot.
## Purpose
- Central API endpoint for all frontend requests
- User authentication and authorization
- Dashboard and widget management
- Event and device state storage
## Technology Stack
- **Language**: Java 17
- **Framework**: Spring Boot 3.2.0
- **Port**: 8080
- **Database**: PostgreSQL
- **Message Bus**: Redis
## Features
- JWT-based authentication
- RESTful API endpoints
- WebSocket support for real-time updates
- Dashboard CRUD operations
- Event and device state management
## Development Status
**Complete** - Core functionality implemented

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.labfusion</groupId>
<artifactId>api-gateway</artifactId>
<version>1.0.0</version>
<name>LabFusion API Gateway</name>
<description>Core API gateway for LabFusion homelab dashboard</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- OpenAPI/Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.labfusion;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LabFusionApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(LabFusionApiGatewayApplication.class, args);
}
}

View File

@@ -0,0 +1,68 @@
package com.labfusion.controller;
import com.labfusion.model.Dashboard;
import com.labfusion.model.User;
import com.labfusion.service.DashboardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/dashboards")
@CrossOrigin(origins = "*")
public class DashboardController {
@Autowired
private DashboardService dashboardService;
@GetMapping
public ResponseEntity<List<Dashboard>> getDashboards(@AuthenticationPrincipal User user) {
List<Dashboard> dashboards = dashboardService.getDashboardsByUser(user);
return ResponseEntity.ok(dashboards);
}
@GetMapping("/{id}")
public ResponseEntity<Dashboard> getDashboard(@PathVariable Long id, @AuthenticationPrincipal User user) {
Optional<Dashboard> dashboard = dashboardService.getDashboardById(id);
if (dashboard.isPresent()) {
// Check if user owns the dashboard
if (dashboard.get().getUser().getId().equals(user.getId())) {
return ResponseEntity.ok(dashboard.get());
} else {
return ResponseEntity.forbidden().build();
}
}
return ResponseEntity.notFound().build();
}
@PostMapping
public ResponseEntity<Dashboard> createDashboard(@RequestBody Dashboard dashboard, @AuthenticationPrincipal User user) {
dashboard.setUser(user);
Dashboard savedDashboard = dashboardService.saveDashboard(dashboard);
return ResponseEntity.ok(savedDashboard);
}
@PutMapping("/{id}")
public ResponseEntity<Dashboard> updateDashboard(@PathVariable Long id, @RequestBody Dashboard dashboard, @AuthenticationPrincipal User user) {
try {
Dashboard updatedDashboard = dashboardService.updateDashboard(id, dashboard);
return ResponseEntity.ok(updatedDashboard);
} catch (RuntimeException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteDashboard(@PathVariable Long id, @AuthenticationPrincipal User user) {
Optional<Dashboard> dashboard = dashboardService.getDashboardById(id);
if (dashboard.isPresent() && dashboard.get().getUser().getId().equals(user.getId())) {
dashboardService.deleteDashboard(id);
return ResponseEntity.ok().build();
}
return ResponseEntity.forbidden().build();
}
}

View File

@@ -0,0 +1,71 @@
package com.labfusion.controller;
import com.labfusion.model.DeviceState;
import com.labfusion.model.Event;
import com.labfusion.repository.DeviceStateRepository;
import com.labfusion.repository.EventRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
@RestController
@RequestMapping("/api/system")
@CrossOrigin(origins = "*")
public class SystemController {
@Autowired
private EventRepository eventRepository;
@Autowired
private DeviceStateRepository deviceStateRepository;
@GetMapping("/events")
public ResponseEntity<List<Event>> getEvents(
@RequestParam(required = false) String service,
@RequestParam(required = false) String eventType,
@RequestParam(required = false) Integer hours) {
List<Event> events;
if (service != null && hours != null) {
LocalDateTime since = LocalDateTime.now().minusHours(hours);
events = eventRepository.findRecentEventsByService(service, since);
} else if (service != null) {
events = eventRepository.findByService(service);
} else if (eventType != null) {
events = eventRepository.findByEventType(eventType);
} else {
events = eventRepository.findAll();
}
return ResponseEntity.ok(events);
}
@GetMapping("/device-states")
public ResponseEntity<List<DeviceState>> getDeviceStates(
@RequestParam(required = false) String entityId,
@RequestParam(required = false) String service) {
List<DeviceState> states;
if (entityId != null) {
states = deviceStateRepository.findByEntityId(entityId);
} else if (service != null) {
states = deviceStateRepository.findByService(service);
} else {
states = deviceStateRepository.findAll();
}
return ResponseEntity.ok(states);
}
@GetMapping("/metrics")
public ResponseEntity<Object> getSystemMetrics() {
// This would integrate with actual system monitoring
// For now, return a placeholder response
return ResponseEntity.ok().body("System metrics endpoint - to be implemented");
}
}

View File

@@ -0,0 +1,81 @@
package com.labfusion.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "dashboards")
public class Dashboard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(columnDefinition = "JSONB")
private String layout;
@OneToMany(mappedBy = "dashboard", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Widget> widgets;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors
public Dashboard() {}
public Dashboard(String name, String description, String layout, User user) {
this.name = name;
this.description = description;
this.layout = layout;
this.user = user;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getLayout() { return layout; }
public void setLayout(String layout) { this.layout = layout; }
public List<Widget> getWidgets() { return widgets; }
public void setWidgets(List<Widget> widgets) { this.widgets = widgets; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,64 @@
package com.labfusion.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "device_states")
public class DeviceState {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "timestamp", nullable = false)
private LocalDateTime timestamp;
@Column(name = "entity_id", nullable = false)
private String entityId;
@Column(name = "value", columnDefinition = "TEXT")
private String value;
@Column(name = "service", nullable = false)
private String service;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
if (timestamp == null) {
timestamp = LocalDateTime.now();
}
}
// Constructors
public DeviceState() {}
public DeviceState(String entityId, String value, String service) {
this.entityId = entityId;
this.value = value;
this.service = service;
this.timestamp = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public String getEntityId() { return entityId; }
public void setEntityId(String entityId) { this.entityId = entityId; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
public String getService() { return service; }
public void setService(String service) { this.service = service; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

View File

@@ -0,0 +1,64 @@
package com.labfusion.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "events")
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "timestamp", nullable = false)
private LocalDateTime timestamp;
@Column(name = "service", nullable = false)
private String service;
@Column(name = "event_type", nullable = false)
private String eventType;
@Column(columnDefinition = "JSONB")
private String metadata;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
if (timestamp == null) {
timestamp = LocalDateTime.now();
}
}
// Constructors
public Event() {}
public Event(String service, String eventType, String metadata) {
this.service = service;
this.eventType = eventType;
this.metadata = metadata;
this.timestamp = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public String getService() { return service; }
public void setService(String service) { this.service = service; }
public String getEventType() { return eventType; }
public void setEventType(String eventType) { this.eventType = eventType; }
public String getMetadata() { return metadata; }
public void setMetadata(String metadata) { this.metadata = metadata; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

View File

@@ -0,0 +1,7 @@
package com.labfusion.model;
public enum Role {
ADMIN,
USER,
VIEWER
}

View File

@@ -0,0 +1,75 @@
package com.labfusion.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.Set;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(unique = true, nullable = false)
private String email;
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(EnumType.STRING)
private Set<Role> roles;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors
public User() {}
public User(String username, String password, String email, Set<Role> roles) {
this.username = username;
this.password = password;
this.email = email;
this.roles = roles;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Set<Role> getRoles() { return roles; }
public void setRoles(Set<Role> roles) { this.roles = roles; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,75 @@
package com.labfusion.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "widgets")
public class Widget {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private WidgetType type;
@Column(columnDefinition = "JSONB")
private String config;
@Column(name = "service_binding")
private String serviceBinding;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dashboard_id", nullable = false)
private Dashboard dashboard;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors
public Widget() {}
public Widget(WidgetType type, String config, String serviceBinding, Dashboard dashboard) {
this.type = type;
this.config = config;
this.serviceBinding = serviceBinding;
this.dashboard = dashboard;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public WidgetType getType() { return type; }
public void setType(WidgetType type) { this.type = type; }
public String getConfig() { return config; }
public void setConfig(String config) { this.config = config; }
public String getServiceBinding() { return serviceBinding; }
public void setServiceBinding(String serviceBinding) { this.serviceBinding = serviceBinding; }
public Dashboard getDashboard() { return dashboard; }
public void setDashboard(Dashboard dashboard) { this.dashboard = dashboard; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,11 @@
package com.labfusion.model;
public enum WidgetType {
CHART,
TABLE,
CARD,
LOGS,
METRIC,
STATUS,
TIMELINE
}

View File

@@ -0,0 +1,14 @@
package com.labfusion.repository;
import com.labfusion.model.Dashboard;
import com.labfusion.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface DashboardRepository extends JpaRepository<Dashboard, Long> {
List<Dashboard> findByUser(User user);
List<Dashboard> findByUserId(Long userId);
}

View File

@@ -0,0 +1,20 @@
package com.labfusion.repository;
import com.labfusion.model.DeviceState;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface DeviceStateRepository extends JpaRepository<DeviceState, Long> {
List<DeviceState> findByEntityId(String entityId);
List<DeviceState> findByService(String service);
List<DeviceState> findByTimestampBetween(LocalDateTime start, LocalDateTime end);
@Query("SELECT ds FROM DeviceState ds WHERE ds.entityId = :entityId ORDER BY ds.timestamp DESC")
List<DeviceState> findLatestStatesByEntityId(@Param("entityId") String entityId);
}

View File

@@ -0,0 +1,20 @@
package com.labfusion.repository;
import com.labfusion.model.Event;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface EventRepository extends JpaRepository<Event, Long> {
List<Event> findByService(String service);
List<Event> findByEventType(String eventType);
List<Event> findByTimestampBetween(LocalDateTime start, LocalDateTime end);
@Query("SELECT e FROM Event e WHERE e.service = :service AND e.timestamp >= :since ORDER BY e.timestamp DESC")
List<Event> findRecentEventsByService(@Param("service") String service, @Param("since") LocalDateTime since);
}

View File

@@ -0,0 +1,15 @@
package com.labfusion.repository;
import com.labfusion.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}

View File

@@ -0,0 +1,48 @@
package com.labfusion.service;
import com.labfusion.model.Dashboard;
import com.labfusion.model.User;
import com.labfusion.repository.DashboardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class DashboardService {
@Autowired
private DashboardRepository dashboardRepository;
public List<Dashboard> getDashboardsByUser(User user) {
return dashboardRepository.findByUser(user);
}
public List<Dashboard> getDashboardsByUserId(Long userId) {
return dashboardRepository.findByUserId(userId);
}
public Optional<Dashboard> getDashboardById(Long id) {
return dashboardRepository.findById(id);
}
public Dashboard saveDashboard(Dashboard dashboard) {
return dashboardRepository.save(dashboard);
}
public void deleteDashboard(Long id) {
dashboardRepository.deleteById(id);
}
public Dashboard updateDashboard(Long id, Dashboard updatedDashboard) {
return dashboardRepository.findById(id)
.map(dashboard -> {
dashboard.setName(updatedDashboard.getName());
dashboard.setDescription(updatedDashboard.getDescription());
dashboard.setLayout(updatedDashboard.getLayout());
return dashboardRepository.save(dashboard);
})
.orElseThrow(() -> new RuntimeException("Dashboard not found with id: " + id));
}
}

View File

@@ -0,0 +1,46 @@
spring:
application:
name: labfusion-api-gateway
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/labfusion}
username: ${SPRING_DATASOURCE_USERNAME:labfusion}
password: ${SPRING_DATASOURCE_PASSWORD:labfusion_password}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
timeout: 2000ms
security:
user:
name: admin
password: admin
server:
port: ${API_GATEWAY_PORT:8080}
logging:
level:
com.labfusion: DEBUG
org.springframework.security: DEBUG
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always

View File

@@ -0,0 +1,22 @@
# Metrics Collector Service
A Go-based service for collecting system metrics from Docker hosts and containers.
## Purpose
- Collect CPU, memory, disk, and network metrics
- Monitor container health and resource usage
- Publish metrics to Redis for consumption by other services
## Technology Stack
- **Language**: Go
- **Port**: 8081
- **Dependencies**: Docker API, Redis, Prometheus metrics
## Features
- Real-time system metrics collection
- Container resource monitoring
- Prometheus-compatible metrics export
- Configurable collection intervals
## Development Status
🚧 **Planned** - Service structure created, implementation pending

View File

@@ -0,0 +1,22 @@
# Notification Service
A Node.js service for handling notifications and alerts across the homelab.
## Purpose
- Send notifications via multiple channels (email, webhook, push)
- Process alerts from various services
- Manage notification preferences and rules
## Technology Stack
- **Language**: Node.js/TypeScript
- **Port**: 8082
- **Dependencies**: Redis, SMTP, Webhook integrations
## Features
- Multi-channel notification delivery
- Alert rule engine
- Notification history and preferences
- Integration with external services
## Development Status
🚧 **Planned** - Service structure created, implementation pending

View File

@@ -0,0 +1,21 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

View File

@@ -0,0 +1,21 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose port
EXPOSE 8000
# Run in development mode with hot reload
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

View File

@@ -0,0 +1,25 @@
# Service Adapters
Python FastAPI service for integrating with external homelab services.
## Purpose
- Integrate with Home Assistant, Frigate, Immich, n8n
- Transform external service data into standardized format
- Publish events to the message bus
- Provide unified API for service data
## Technology Stack
- **Language**: Python 3.11
- **Framework**: FastAPI
- **Port**: 8000
- **Message Bus**: Redis
## Features
- Home Assistant entity integration
- Frigate event processing
- Immich asset management
- n8n workflow triggers
- Event publishing to Redis
## Development Status
**Complete** - Core functionality implemented

View File

@@ -0,0 +1,171 @@
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
import asyncio
import redis
import json
from datetime import datetime
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
app = FastAPI(
title="LabFusion Service Adapters",
description="Service integration adapters for Home Assistant, Frigate, Immich, and other homelab services",
version="1.0.0"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Redis connection
redis_client = redis.Redis(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", 6379)),
decode_responses=True
)
# Service configurations
SERVICES = {
"home_assistant": {
"url": os.getenv("HOME_ASSISTANT_URL", "https://homeassistant.local:8123"),
"token": os.getenv("HOME_ASSISTANT_TOKEN", ""),
"enabled": bool(os.getenv("HOME_ASSISTANT_TOKEN"))
},
"frigate": {
"url": os.getenv("FRIGATE_URL", "http://frigate.local:5000"),
"token": os.getenv("FRIGATE_TOKEN", ""),
"enabled": bool(os.getenv("FRIGATE_TOKEN"))
},
"immich": {
"url": os.getenv("IMMICH_URL", "http://immich.local:2283"),
"api_key": os.getenv("IMMICH_API_KEY", ""),
"enabled": bool(os.getenv("IMMICH_API_KEY"))
},
"n8n": {
"url": os.getenv("N8N_URL", "http://n8n.local:5678"),
"webhook_url": os.getenv("N8N_WEBHOOK_URL", ""),
"enabled": bool(os.getenv("N8N_WEBHOOK_URL"))
}
}
@app.get("/")
async def root():
return {"message": "LabFusion Service Adapters API", "version": "1.0.0"}
@app.get("/health")
async def health_check():
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
@app.get("/services")
async def get_services():
"""Get status of all configured services"""
service_status = {}
for service_name, config in SERVICES.items():
service_status[service_name] = {
"enabled": config["enabled"],
"url": config["url"],
"status": "unknown" # Would check actual service status
}
return service_status
@app.get("/home-assistant/entities")
async def get_ha_entities():
"""Get Home Assistant entities"""
if not SERVICES["home_assistant"]["enabled"]:
raise HTTPException(status_code=503, detail="Home Assistant integration not configured")
# This would make actual API calls to Home Assistant
# For now, return mock data
return {
"entities": [
{
"entity_id": "sensor.cpu_usage",
"state": "45.2",
"attributes": {"unit_of_measurement": "%", "friendly_name": "CPU Usage"}
},
{
"entity_id": "sensor.memory_usage",
"state": "2.1",
"attributes": {"unit_of_measurement": "GB", "friendly_name": "Memory Usage"}
}
]
}
@app.get("/frigate/events")
async def get_frigate_events():
"""Get Frigate detection events"""
if not SERVICES["frigate"]["enabled"]:
raise HTTPException(status_code=503, detail="Frigate integration not configured")
# This would make actual API calls to Frigate
# For now, return mock data
return {
"events": [
{
"id": "event_123",
"timestamp": datetime.now().isoformat(),
"camera": "front_door",
"label": "person",
"confidence": 0.95
}
]
}
@app.get("/immich/assets")
async def get_immich_assets():
"""Get Immich photo assets"""
if not SERVICES["immich"]["enabled"]:
raise HTTPException(status_code=503, detail="Immich integration not configured")
# This would make actual API calls to Immich
# For now, return mock data
return {
"assets": [
{
"id": "asset_123",
"filename": "photo_001.jpg",
"created_at": datetime.now().isoformat(),
"tags": ["person", "outdoor"],
"faces": ["Alice", "Bob"]
}
]
}
@app.post("/publish-event")
async def publish_event(event_data: dict, background_tasks: BackgroundTasks):
"""Publish an event to the message bus"""
try:
event = {
"timestamp": datetime.now().isoformat(),
"service": event_data.get("service", "unknown"),
"event_type": event_data.get("event_type", "unknown"),
"metadata": json.dumps(event_data.get("metadata", {}))
}
# Publish to Redis
redis_client.lpush("events", json.dumps(event))
return {"status": "published", "event": event}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/events")
async def get_events(limit: int = 100):
"""Get recent events from the message bus"""
try:
events = redis_client.lrange("events", 0, limit - 1)
return {"events": [json.loads(event) for event in events]}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,14 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
httpx==0.25.2
redis==5.0.1
psycopg2-binary==2.9.9
sqlalchemy==2.0.23
alembic==1.13.1
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-dotenv==1.0.0
websockets==12.0
aiofiles==23.2.1