shinkan-jinkendo/backend/main.py
Lars 1f2c8ea0f1
Some checks failed
Deploy Development / deploy (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 1m55s
feat: enhance database migration handling and health check endpoint
- Updated migration logic in main.py to allow skipping migrations during local development with SKIP_DB_MIGRATE environment variable.
- Improved error handling for migration failures, ensuring the application does not start if migrations are incomplete.
- Added a new health check endpoint (/api/health/ready) to verify database connection and essential tables, aiding in production debugging.
- Enhanced run_migrations.py to support ordered execution of migration files and improved transaction handling.
- Updated requirements.txt to include sqlparse for SQL statement parsing when psql is unavailable.
2026-04-29 12:29:39 +02:00

182 lines
5.6 KiB
Python

"""
Shinkan Jinkendo - Main Application Entry Point
Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung
"""
from pathlib import Path
from typing import Optional
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
import os
import sys
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from version import APP_VERSION, BUILD_DATE, DB_SCHEMA_VERSION, MODULE_VERSIONS
# Run database migrations before API start — halbes Schema ist schlimmer als kein Start
# Lokal ohne DB / nur Tests: SKIP_DB_MIGRATE=1
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() in ("1", "true", "yes"):
print("⚠ SKIP_DB_MIGRATE=1 — Migrationen wurden übersprungen (nur für Entwicklung ohne DB)")
else:
try:
import run_migrations
rc = run_migrations.main()
if rc != 0:
print(f"✗ Datenbank-Migration fehlgeschlagen (Exit-Code {rc}). Start abgebrochen.")
sys.exit(1)
print("✓ Database migrations completed")
except SystemExit:
raise
except Exception as e:
print(f"✗ Migration-Laufzeitfehler: {e}")
sys.exit(1)
from routers.auth import limiter as auth_rate_limiter
# Initialize FastAPI app
app = FastAPI(
title="Shinkan Jinkendo API",
description="Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung",
version=APP_VERSION
)
# SlowAPI: Rate Limits auf /api/auth/* (Decorator in routers/auth.py)
app.state.limiter = auth_rate_limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# CORS — kommaseparierte Liste (z. B. https://dev.shinkan… und http://192.168.x.x:3098)
_cors_raw = os.getenv("ALLOWED_ORIGINS", "http://localhost:3098")
ALLOWED_ORIGINS = [o.strip() for o in _cors_raw.split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# TODO: Initialize Database with migrations
# Version Endpoint (public, no auth)
@app.get("/api/version")
def get_version():
"""Get application version and build info"""
return {
"app_version": APP_VERSION,
"build_date": BUILD_DATE,
"backend_version": APP_VERSION,
"modules": MODULE_VERSIONS,
"db_schema_version": DB_SCHEMA_VERSION,
"environment": os.getenv("ENVIRONMENT", "development")
}
# Health Check
@app.get("/health")
def health_check():
"""Health check endpoint"""
return {"status": "healthy", "version": APP_VERSION}
@app.get("/api/health/ready")
def health_ready():
"""
Verbindung + Kern-Tabellen prüfen (ohne Login).
Nutzen bei Prod-Debugging: wenn schema_complete=false, Migrationen nicht vollständig.
"""
from db import get_db, get_cursor
REQUIRED = (
"schema_migrations",
"skills",
"skill_main_categories",
"skill_categories",
"maturity_models",
"sessions",
)
tables: dict = {}
err: Optional[str] = None
try:
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT 1")
for tbl in REQUIRED:
cur.execute(
"""
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = %s
)
""",
(tbl,),
)
row = cur.fetchone()
tables[tbl] = bool(next(iter(row.values()))) if row else False
migration_count = 0
if tables.get("schema_migrations"):
cur.execute("SELECT COUNT(*)::int FROM schema_migrations")
mig_row = cur.fetchone()
migration_count = (
int(list(mig_row.values())[0]) if mig_row is not None else 0
)
except Exception as e:
err = str(e)
migration_count = 0
complete = bool(err is None and all(tables.get(t) for t in REQUIRED))
return {
"status": "ready" if complete else "degraded",
"database": err is None,
"detail": err,
"schema_complete": complete,
"tables": tables,
"schema_migrations_count": migration_count,
}
# Root Endpoint
@app.get("/")
def read_root():
"""Root endpoint - API info"""
return {
"app": "Shinkan Jinkendo API",
"version": APP_VERSION,
"docs": "/docs",
"health": "/health"
}
# Register routers
from routers import auth, profiles, exercises, clubs, skills, training_planning, catalogs, maturity_models, matrix_stack_bundle, import_wiki, import_wiki_admin
app.include_router(auth.router)
app.include_router(profiles.router)
app.include_router(exercises.router)
app.include_router(clubs.router)
app.include_router(skills.router)
app.include_router(training_planning.router)
app.include_router(catalogs.router)
app.include_router(maturity_models.router)
app.include_router(matrix_stack_bundle.router)
app.include_router(import_wiki.router)
app.include_router(import_wiki_admin.router)
# Lokale Medien (Übungen-Uploads) unter MEDIA_ROOT, ausliefern unter /media/...
_media_dir = os.getenv("MEDIA_ROOT", str(Path(__file__).resolve().parent / "media"))
Path(_media_dir).mkdir(parents=True, exist_ok=True)
app.mount("/media", StaticFiles(directory=_media_dir), name="media")
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True
)