feat: Setup Wizard for first-run configuration

Container starts with only DB credentials. On first visit, a step-by-step
wizard guides through admin password, weather, HA, Vikunja, Unraid, MQTT,
n8n and news configuration. Backward-compat: ADMIN_PASSWORD env skips wizard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam 2026-03-02 16:06:10 +01:00
parent e25d055ba2
commit 6651bfaf60
9 changed files with 1042 additions and 34 deletions

View file

@ -22,17 +22,16 @@ async def seed_if_empty() -> None:
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,
if admin_pw:
# Explicit ENV password → seed admin user (backward compat)
await settings_service.create_admin_user("admin", hash_password(admin_pw))
logger.info("Admin user seeded from ADMIN_PASSWORD env")
else:
# No password set → setup wizard will handle admin creation
logger.info(
"No ADMIN_PASSWORD set — setup wizard will handle first-time "
"configuration. Visit the web UI to complete setup."
)
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()
@ -103,6 +102,16 @@ async def seed_if_empty() -> None:
"enabled": bool(os.getenv("MQTT_HOST")),
"display_order": 5,
},
{
"type": "n8n",
"name": "n8n Webhooks",
"config": {
"url": os.getenv("N8N_URL", ""),
"api_key": os.getenv("N8N_API_KEY", ""),
},
"enabled": bool(os.getenv("N8N_URL")),
"display_order": 6,
},
]
for seed in seed_integrations:

View file

@ -136,6 +136,27 @@ async def test_news_db(config: Dict[str, Any]) -> Dict[str, Any]:
return {"success": False, "message": str(exc)}
async def test_n8n(config: Dict[str, Any]) -> Dict[str, Any]:
"""Test n8n connection by fetching workflows."""
url = config.get("url", "")
api_key = config.get("api_key", "")
if not url or not api_key:
return {"success": False, "message": "URL und API Key sind erforderlich"}
try:
base = url.rstrip("/")
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
r = await client.get(
f"{base}/api/v1/workflows",
headers={"X-N8N-API-KEY": api_key},
)
r.raise_for_status()
data = r.json()
count = len(data.get("data", []))
return {"success": True, "message": f"Verbunden — {count} Workflows gefunden"}
except Exception as exc:
return {"success": False, "message": str(exc)}
# Map integration type → test function
TEST_FUNCTIONS = {
"weather": test_weather,
@ -144,4 +165,5 @@ TEST_FUNCTIONS = {
"unraid": test_unraid,
"mqtt": test_mqtt,
"news": test_news_db,
"n8n": test_n8n,
}