Complete admin backend with login, where all integrations (weather, news, Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead of ENV variables. Two-layer config: ENV seeds DB on first start, then DB is source of truth. Auto-migration system on startup. Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service, admin router (protected), test_connections per integration, config.py rewrite. Frontend: react-router v6, login page, admin layout with sidebar, 8 settings pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword), shared IntegrationForm + TestButton components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
5.1 KiB
Python
150 lines
5.1 KiB
Python
"""First-run seeder: populates DB from ENV defaults when tables are empty."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import secrets
|
|
|
|
from server.auth import hash_password
|
|
from server.db import get_pool
|
|
from server.services import settings_service
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def seed_if_empty() -> None:
|
|
"""Check if admin tables are empty and seed with ENV-derived values."""
|
|
pool = await get_pool()
|
|
|
|
# ---- Admin User ----
|
|
user = await settings_service.get_admin_user()
|
|
if user is None:
|
|
admin_pw = os.getenv("ADMIN_PASSWORD", "")
|
|
if not admin_pw:
|
|
admin_pw = secrets.token_urlsafe(16)
|
|
logger.warning(
|
|
"=" * 60 + "\n"
|
|
" No ADMIN_PASSWORD set — generated: %s\n"
|
|
" Set ADMIN_PASSWORD env to use your own.\n" +
|
|
"=" * 60,
|
|
admin_pw,
|
|
)
|
|
await settings_service.create_admin_user("admin", hash_password(admin_pw))
|
|
logger.info("Admin user seeded from ENV")
|
|
|
|
# ---- Integrations ----
|
|
existing = await settings_service.get_integrations()
|
|
existing_types = {i["type"] for i in existing}
|
|
|
|
seed_integrations = [
|
|
{
|
|
"type": "weather",
|
|
"name": "Wetter (wttr.in)",
|
|
"config": {
|
|
"location": os.getenv("WEATHER_LOCATION", "Leverkusen"),
|
|
"location_secondary": os.getenv("WEATHER_LOCATION_SECONDARY", "Rab,Croatia"),
|
|
},
|
|
"enabled": True,
|
|
"display_order": 0,
|
|
},
|
|
{
|
|
"type": "news",
|
|
"name": "News (PostgreSQL)",
|
|
"config": {
|
|
"max_age_hours": int(os.getenv("NEWS_MAX_AGE_HOURS", "48")),
|
|
},
|
|
"enabled": True,
|
|
"display_order": 1,
|
|
},
|
|
{
|
|
"type": "ha",
|
|
"name": "Home Assistant",
|
|
"config": {
|
|
"url": os.getenv("HA_URL", ""),
|
|
"token": os.getenv("HA_TOKEN", ""),
|
|
},
|
|
"enabled": bool(os.getenv("HA_URL")),
|
|
"display_order": 2,
|
|
},
|
|
{
|
|
"type": "vikunja",
|
|
"name": "Vikunja Tasks",
|
|
"config": {
|
|
"url": os.getenv("VIKUNJA_URL", ""),
|
|
"token": os.getenv("VIKUNJA_TOKEN", ""),
|
|
"private_projects": [3, 4],
|
|
"sams_projects": [2, 5],
|
|
},
|
|
"enabled": bool(os.getenv("VIKUNJA_URL")),
|
|
"display_order": 3,
|
|
},
|
|
{
|
|
"type": "unraid",
|
|
"name": "Unraid Server",
|
|
"config": {
|
|
"servers": _parse_unraid_env(),
|
|
},
|
|
"enabled": bool(os.getenv("UNRAID_SERVERS")),
|
|
"display_order": 4,
|
|
},
|
|
{
|
|
"type": "mqtt",
|
|
"name": "MQTT Broker",
|
|
"config": {
|
|
"host": os.getenv("MQTT_HOST", ""),
|
|
"port": int(os.getenv("MQTT_PORT", "1883")),
|
|
"username": os.getenv("MQTT_USERNAME", ""),
|
|
"password": os.getenv("MQTT_PASSWORD", ""),
|
|
"client_id": os.getenv("MQTT_CLIENT_ID", "daily-briefing"),
|
|
"topics": _parse_mqtt_topics(),
|
|
},
|
|
"enabled": bool(os.getenv("MQTT_HOST")),
|
|
"display_order": 5,
|
|
},
|
|
]
|
|
|
|
for seed in seed_integrations:
|
|
if seed["type"] not in existing_types:
|
|
await settings_service.upsert_integration(
|
|
type_name=seed["type"],
|
|
name=seed["name"],
|
|
config=seed["config"],
|
|
enabled=seed["enabled"],
|
|
display_order=seed["display_order"],
|
|
)
|
|
logger.info("Seeded integration: %s", seed["type"])
|
|
|
|
# ---- App Settings ----
|
|
existing_settings = await settings_service.get_all_settings()
|
|
if not existing_settings:
|
|
default_settings = [
|
|
("weather_cache_ttl", "1800", "int", "cache", "Wetter Cache TTL", "Sekunden"),
|
|
("ha_cache_ttl", "30", "int", "cache", "HA Cache TTL", "Sekunden"),
|
|
("vikunja_cache_ttl", "60", "int", "cache", "Vikunja Cache TTL", "Sekunden"),
|
|
("unraid_cache_ttl", "15", "int", "cache", "Unraid Cache TTL", "Sekunden"),
|
|
("news_cache_ttl", "300", "int", "cache", "News Cache TTL", "Sekunden"),
|
|
("ws_interval", "15", "int", "general", "WebSocket Intervall", "Sekunden"),
|
|
]
|
|
for key, value, vtype, cat, label, desc in default_settings:
|
|
await settings_service.set_setting(key, value, vtype, cat, label, desc)
|
|
logger.info("Seeded %d default settings", len(default_settings))
|
|
|
|
|
|
def _parse_unraid_env() -> list:
|
|
"""Parse UNRAID_SERVERS env var."""
|
|
raw = os.getenv("UNRAID_SERVERS", "[]")
|
|
try:
|
|
return json.loads(raw)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return []
|
|
|
|
|
|
def _parse_mqtt_topics() -> list:
|
|
"""Parse MQTT_TOPICS env var."""
|
|
raw = os.getenv("MQTT_TOPICS", "#")
|
|
try:
|
|
return json.loads(raw)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return [t.strip() for t in raw.split(",") if t.strip()]
|