initial project setup
This commit is contained in:
91
.gitignore
vendored
Normal file
91
.gitignore
vendored
Normal 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
180
README.md
@@ -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
95
docker-compose.dev.yml
Normal 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
85
docker-compose.yml
Normal 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
161
docs/progress.md
Normal 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
126
docs/specs.md
Normal 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 Alice’s 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 you’re 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
55
docs/structure.txt
Normal 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
32
env.example
Normal 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
24
frontend/Dockerfile
Normal 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
18
frontend/Dockerfile.dev
Normal 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
50
frontend/package.json
Normal 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"
|
||||
}
|
||||
20
frontend/public/index.html
Normal file
20
frontend/public/index.html
Normal 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
111
frontend/src/App.css
Normal 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
64
frontend/src/App.js
Normal 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;
|
||||
147
frontend/src/components/Dashboard.js
Normal file
147
frontend/src/components/Dashboard.js
Normal 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;
|
||||
124
frontend/src/components/Settings.js
Normal file
124
frontend/src/components/Settings.js
Normal 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;
|
||||
111
frontend/src/components/SystemMetrics.js
Normal file
111
frontend/src/components/SystemMetrics.js
Normal 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
70
frontend/src/index.css
Normal 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
29
frontend/src/index.js
Normal 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>
|
||||
);
|
||||
17
services/api-gateway/Dockerfile
Normal file
17
services/api-gateway/Dockerfile
Normal 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"]
|
||||
21
services/api-gateway/Dockerfile.dev
Normal file
21
services/api-gateway/Dockerfile.dev
Normal 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'"]
|
||||
26
services/api-gateway/README.md
Normal file
26
services/api-gateway/README.md
Normal 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
|
||||
113
services/api-gateway/pom.xml
Normal file
113
services/api-gateway/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.labfusion.model;
|
||||
|
||||
public enum Role {
|
||||
ADMIN,
|
||||
USER,
|
||||
VIEWER
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.labfusion.model;
|
||||
|
||||
public enum WidgetType {
|
||||
CHART,
|
||||
TABLE,
|
||||
CARD,
|
||||
LOGS,
|
||||
METRIC,
|
||||
STATUS,
|
||||
TIMELINE
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
46
services/api-gateway/src/main/resources/application.yml
Normal file
46
services/api-gateway/src/main/resources/application.yml
Normal 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
|
||||
22
services/metrics-collector/README.md
Normal file
22
services/metrics-collector/README.md
Normal 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
|
||||
22
services/notification-service/README.md
Normal file
22
services/notification-service/README.md
Normal 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
|
||||
21
services/service-adapters/Dockerfile
Normal file
21
services/service-adapters/Dockerfile
Normal 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"]
|
||||
21
services/service-adapters/Dockerfile.dev
Normal file
21
services/service-adapters/Dockerfile.dev
Normal 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"]
|
||||
25
services/service-adapters/README.md
Normal file
25
services/service-adapters/README.md
Normal 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
|
||||
171
services/service-adapters/main.py
Normal file
171
services/service-adapters/main.py
Normal 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)
|
||||
14
services/service-adapters/requirements.txt
Normal file
14
services/service-adapters/requirements.txt
Normal 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
|
||||
Reference in New Issue
Block a user