Initial commit: Homelab Dashboard with YAML configuration
Features: - Service health monitoring with response times - Proxmox cluster integration (nodes, VMs, containers) - PBS backup server monitoring - Camera viewer with WebRTC (go2rtc) - Docker container monitoring - Uptime Kuma integration - Mobile-friendly responsive design - YAML-based configuration for easy setup
This commit is contained in:
280
app/config.py
Normal file
280
app/config.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""Dashboard configuration loader - supports YAML config files."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List, Dict, Any
|
||||
import json
|
||||
import os
|
||||
import yaml
|
||||
|
||||
# Base paths
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CONFIG_FILE = os.path.join(BASE_DIR, "config.yaml")
|
||||
SECRETS_FILE = os.path.join(BASE_DIR, "secrets.yaml")
|
||||
SETTINGS_FILE = os.path.join(BASE_DIR, "settings.json")
|
||||
|
||||
@dataclass
|
||||
class Service:
|
||||
name: str
|
||||
url: str
|
||||
ip: str
|
||||
port: int
|
||||
category: str
|
||||
icon: str = "server"
|
||||
favorite: bool = False
|
||||
critical: bool = False
|
||||
group: Optional[str] = None
|
||||
health_check: Optional[Dict] = None
|
||||
|
||||
@dataclass
|
||||
class HealthCheckConfig:
|
||||
url: str
|
||||
timeout: float = 5.0
|
||||
|
||||
# Icon SVG paths
|
||||
SERVICE_ICONS = {
|
||||
"shield": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>',
|
||||
"globe": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9"/>',
|
||||
"archive": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/>',
|
||||
"lock": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>',
|
||||
"film": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"/>',
|
||||
"star": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>',
|
||||
"video": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>',
|
||||
"tv": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>',
|
||||
"music": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>',
|
||||
"search": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>',
|
||||
"download": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>',
|
||||
"cog": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>',
|
||||
"chart": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>',
|
||||
"heartbeat": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>',
|
||||
"key": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>',
|
||||
"git": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>',
|
||||
"workflow": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>',
|
||||
"book": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>',
|
||||
"image": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>',
|
||||
"brain": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>',
|
||||
"home": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>',
|
||||
"camera": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>',
|
||||
"message": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>',
|
||||
"server": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>',
|
||||
"database": '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>',
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Configuration Loading
|
||||
# ============================================================
|
||||
|
||||
def load_yaml(path: str) -> Dict:
|
||||
"""Load YAML file, return empty dict if not found."""
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to load {path}: {e}")
|
||||
return {}
|
||||
|
||||
def load_config() -> Dict:
|
||||
"""Load main config and merge with secrets."""
|
||||
config = load_yaml(CONFIG_FILE)
|
||||
secrets = load_yaml(SECRETS_FILE)
|
||||
|
||||
# Merge secrets into config
|
||||
if secrets:
|
||||
for key in ['proxmox', 'pbs', 'opnsense', 'sabnzbd']:
|
||||
if key in secrets and key in config:
|
||||
config[key].update(secrets[key])
|
||||
elif key in secrets:
|
||||
config[key] = secrets[key]
|
||||
|
||||
return config
|
||||
|
||||
# Load configuration
|
||||
_config = load_config()
|
||||
|
||||
# ============================================================
|
||||
# Configuration Values (with defaults for backward compatibility)
|
||||
# ============================================================
|
||||
|
||||
# Dashboard settings
|
||||
DASHBOARD_TITLE = _config.get('dashboard', {}).get('title', 'Homelab Dashboard')
|
||||
REFRESH_INTERVAL = _config.get('dashboard', {}).get('refresh_interval', 30)
|
||||
|
||||
# Proxmox configuration
|
||||
_proxmox = _config.get('proxmox', {})
|
||||
PROXMOX_ENABLED = _proxmox.get('enabled', True)
|
||||
PROXMOX_NODES = _proxmox.get('nodes', [])
|
||||
PROXMOX_API_TOKEN = _proxmox.get('api_token', '')
|
||||
PROXMOX_API_SECRET = _proxmox.get('api_secret', '')
|
||||
|
||||
# PBS configuration
|
||||
_pbs = _config.get('pbs', {})
|
||||
PBS_ENABLED = _pbs.get('enabled', True)
|
||||
PBS_URL = _pbs.get('url', '')
|
||||
PBS_API_TOKEN = _pbs.get('api_token', '')
|
||||
PBS_API_SECRET = _pbs.get('api_secret', '')
|
||||
|
||||
# OPNsense configuration
|
||||
_opnsense = _config.get('opnsense', {})
|
||||
OPNSENSE_URL = _opnsense.get('url', '')
|
||||
OPNSENSE_API_KEY = _opnsense.get('api_key', '')
|
||||
OPNSENSE_API_SECRET = _opnsense.get('api_secret', '')
|
||||
|
||||
# Prometheus configuration
|
||||
PROMETHEUS_URL = _config.get('prometheus', {}).get('url', '')
|
||||
|
||||
# Camera configuration
|
||||
_cameras = _config.get('cameras', {})
|
||||
CAMERAS_ENABLED = _cameras.get('enabled', False)
|
||||
GO2RTC_URL = _cameras.get('go2rtc_url', '')
|
||||
CAMERAS = _cameras.get('streams', [])
|
||||
|
||||
# Sabnzbd configuration
|
||||
_sabnzbd = _config.get('sabnzbd', {})
|
||||
SABNZBD_URL = _sabnzbd.get('url', '')
|
||||
SABNZBD_API_KEY = _sabnzbd.get('api_key', '')
|
||||
|
||||
# Uptime Kuma configuration
|
||||
_uptime = _config.get('uptime_kuma', {})
|
||||
UPTIME_KUMA_URL = _uptime.get('url', '')
|
||||
UPTIME_KUMA_STATUS_PAGE = _uptime.get('status_page', 'default')
|
||||
|
||||
# Docker hosts
|
||||
_docker = _config.get('docker', {})
|
||||
DOCKER_ENABLED = _docker.get('enabled', False)
|
||||
DOCKER_HOSTS = _docker.get('hosts', [])
|
||||
|
||||
# Categories
|
||||
CATEGORIES = _config.get('categories', {
|
||||
"Infrastructure": {"color": "blue", "icon": "server"},
|
||||
"Media": {"color": "purple", "icon": "film"},
|
||||
"Monitoring": {"color": "amber", "icon": "chart"},
|
||||
"Apps": {"color": "emerald", "icon": "cog"},
|
||||
"Home": {"color": "cyan", "icon": "home"},
|
||||
})
|
||||
|
||||
# Service groups
|
||||
SERVICE_GROUPS = _config.get('service_groups', {})
|
||||
|
||||
# Load services from config
|
||||
def _load_services() -> List[Service]:
|
||||
"""Load services from YAML config."""
|
||||
services = []
|
||||
for svc in _config.get('services', []):
|
||||
health_check = None
|
||||
if 'health_check' in svc:
|
||||
health_check = svc['health_check']
|
||||
|
||||
services.append(Service(
|
||||
name=svc['name'],
|
||||
url=svc['url'],
|
||||
ip=svc['ip'],
|
||||
port=svc['port'],
|
||||
category=svc['category'],
|
||||
icon=svc.get('icon', 'server'),
|
||||
favorite=svc.get('favorite', False),
|
||||
critical=svc.get('critical', False),
|
||||
group=svc.get('group'),
|
||||
health_check=health_check,
|
||||
))
|
||||
return services
|
||||
|
||||
SERVICES = _load_services()
|
||||
|
||||
# Build SERVICE_CHECK_OVERRIDES from health_check configs
|
||||
SERVICE_CHECK_OVERRIDES = {}
|
||||
for svc in SERVICES:
|
||||
if svc.health_check:
|
||||
SERVICE_CHECK_OVERRIDES[svc.name] = (
|
||||
svc.health_check.get('url', f"http://{svc.ip}:{svc.port}/"),
|
||||
svc.health_check.get('timeout', 5.0)
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# Settings (user preferences, stored in settings.json)
|
||||
# ============================================================
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
"refresh_interval": REFRESH_INTERVAL,
|
||||
"theme": _config.get('dashboard', {}).get('theme', 'dark'),
|
||||
"favorites": _config.get('favorites', []),
|
||||
"collapsed_categories": [],
|
||||
"show_response_times": _config.get('dashboard', {}).get('show_response_times', True),
|
||||
"show_icons": _config.get('dashboard', {}).get('show_icons', True),
|
||||
}
|
||||
|
||||
def load_settings():
|
||||
try:
|
||||
if os.path.exists(SETTINGS_FILE):
|
||||
with open(SETTINGS_FILE, 'r') as f:
|
||||
return {**DEFAULT_SETTINGS, **json.load(f)}
|
||||
except:
|
||||
pass
|
||||
return DEFAULT_SETTINGS.copy()
|
||||
|
||||
def save_settings(settings):
|
||||
try:
|
||||
with open(SETTINGS_FILE, 'w') as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
# ============================================================
|
||||
# Helper Functions
|
||||
# ============================================================
|
||||
|
||||
def get_services_by_category():
|
||||
categories = {}
|
||||
for service in SERVICES:
|
||||
if service.category not in categories:
|
||||
categories[service.category] = []
|
||||
categories[service.category].append(service)
|
||||
return categories
|
||||
|
||||
def get_favorites():
|
||||
settings = load_settings()
|
||||
fav_names = settings.get("favorites", [])
|
||||
return [s for s in SERVICES if s.name in fav_names or s.favorite]
|
||||
|
||||
def get_critical_services():
|
||||
return [s for s in SERVICES if s.critical]
|
||||
|
||||
def get_service_icon(icon_name):
|
||||
return SERVICE_ICONS.get(icon_name, SERVICE_ICONS["server"])
|
||||
|
||||
def reload_config():
|
||||
"""Reload configuration from files (for runtime updates)."""
|
||||
global _config, SERVICES, SERVICE_CHECK_OVERRIDES, CATEGORIES, SERVICE_GROUPS
|
||||
global PROXMOX_NODES, PROXMOX_API_TOKEN, PROXMOX_API_SECRET
|
||||
global PBS_URL, PBS_API_TOKEN, PBS_API_SECRET
|
||||
global GO2RTC_URL, CAMERAS, DOCKER_HOSTS
|
||||
|
||||
_config = load_config()
|
||||
SERVICES = _load_services()
|
||||
|
||||
# Rebuild overrides
|
||||
SERVICE_CHECK_OVERRIDES.clear()
|
||||
for svc in SERVICES:
|
||||
if svc.health_check:
|
||||
SERVICE_CHECK_OVERRIDES[svc.name] = (
|
||||
svc.health_check.get('url', f"http://{svc.ip}:{svc.port}/"),
|
||||
svc.health_check.get('timeout', 5.0)
|
||||
)
|
||||
|
||||
# Update other config values
|
||||
_proxmox = _config.get('proxmox', {})
|
||||
PROXMOX_NODES = _proxmox.get('nodes', [])
|
||||
PROXMOX_API_TOKEN = _proxmox.get('api_token', '')
|
||||
PROXMOX_API_SECRET = _proxmox.get('api_secret', '')
|
||||
|
||||
_pbs = _config.get('pbs', {})
|
||||
PBS_URL = _pbs.get('url', '')
|
||||
PBS_API_TOKEN = _pbs.get('api_token', '')
|
||||
PBS_API_SECRET = _pbs.get('api_secret', '')
|
||||
|
||||
_cameras = _config.get('cameras', {})
|
||||
GO2RTC_URL = _cameras.get('go2rtc_url', '')
|
||||
CAMERAS = _cameras.get('streams', [])
|
||||
|
||||
DOCKER_HOSTS = _config.get('docker', {}).get('hosts', [])
|
||||
CATEGORIES = _config.get('categories', CATEGORIES)
|
||||
SERVICE_GROUPS = _config.get('service_groups', {})
|
||||
Reference in New Issue
Block a user