feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
This commit is contained in:
parent
89ed0c6d0a
commit
f6a42c2dd2
40 changed files with 3487 additions and 311 deletions
52
server/db.py
Normal file
52
server/db.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Shared asyncpg connection pool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import asyncpg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_pool: Optional[asyncpg.Pool] = None
|
||||
|
||||
|
||||
async def init_pool(
|
||||
host: str,
|
||||
port: int,
|
||||
dbname: str,
|
||||
user: str,
|
||||
password: str,
|
||||
min_size: int = 1,
|
||||
max_size: int = 5,
|
||||
) -> asyncpg.Pool:
|
||||
"""Create the shared connection pool. Call once during app startup."""
|
||||
global _pool
|
||||
_pool = await asyncpg.create_pool(
|
||||
host=host,
|
||||
port=port,
|
||||
database=dbname,
|
||||
user=user,
|
||||
password=password,
|
||||
min_size=min_size,
|
||||
max_size=max_size,
|
||||
)
|
||||
logger.info("Database pool initialized (%s:%d/%s)", host, port, dbname)
|
||||
return _pool
|
||||
|
||||
|
||||
async def get_pool() -> asyncpg.Pool:
|
||||
"""Return the shared pool. Raises if not yet initialized."""
|
||||
if _pool is None:
|
||||
raise RuntimeError("Database pool not initialized — call init_pool() first")
|
||||
return _pool
|
||||
|
||||
|
||||
async def close_pool() -> None:
|
||||
"""Close the shared pool. Call during app shutdown."""
|
||||
global _pool
|
||||
if _pool is not None:
|
||||
await _pool.close()
|
||||
_pool = None
|
||||
logger.info("Database pool closed")
|
||||
Loading…
Add table
Add a link
Reference in a new issue