""" 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 )