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

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