"""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": '', "globe": '', "archive": '', "lock": '', "film": '', "star": '', "video": '', "tv": '', "music": '', "search": '', "download": '', "cog": '', "chart": '', "heartbeat": '', "key": '', "git": '', "workflow": '', "book": '', "image": '', "brain": '', "home": '', "camera": '', "message": '', "server": '', "database": '', } # ============================================================ # 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', {})