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>
58 lines
1.8 KiB
Python
58 lines
1.8 KiB
Python
"""Auto-migration runner. Applies pending SQL migrations on startup."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import asyncpg
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MIGRATIONS_DIR = Path(__file__).parent
|
|
|
|
|
|
async def run_migrations(pool: asyncpg.Pool) -> None:
|
|
"""Check schema_version and apply any pending .sql migration files."""
|
|
async with pool.acquire() as conn:
|
|
# Ensure the version-tracking table exists
|
|
await conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
version INTEGER PRIMARY KEY,
|
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
description TEXT NOT NULL DEFAULT ''
|
|
)
|
|
""")
|
|
|
|
row = await conn.fetchrow(
|
|
"SELECT COALESCE(MAX(version), 0) AS v FROM schema_version"
|
|
)
|
|
current_version: int = row["v"]
|
|
logger.info("Current schema version: %d", current_version)
|
|
|
|
# Discover and sort SQL files by their numeric prefix
|
|
sql_files = sorted(
|
|
MIGRATIONS_DIR.glob("[0-9]*.sql"),
|
|
key=lambda p: int(p.stem.split("_")[0]),
|
|
)
|
|
|
|
applied = 0
|
|
for sql_file in sql_files:
|
|
version = int(sql_file.stem.split("_")[0])
|
|
if version <= current_version:
|
|
continue
|
|
|
|
logger.info("Applying migration %03d: %s", version, sql_file.name)
|
|
sql = sql_file.read_text(encoding="utf-8")
|
|
|
|
# Execute the entire migration in a transaction
|
|
async with conn.transaction():
|
|
await conn.execute(sql)
|
|
|
|
logger.info("Migration %03d applied successfully", version)
|
|
applied += 1
|
|
|
|
if applied == 0:
|
|
logger.info("No pending migrations")
|
|
else:
|
|
logger.info("Applied %d migration(s)", applied)
|