2026-04-21 14:26:12 +02:00
|
|
|
"""
|
|
|
|
|
Shinkan Jinkendo - Main Application Entry Point
|
|
|
|
|
|
|
|
|
|
Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung
|
|
|
|
|
"""
|
2026-04-27 14:27:25 +02:00
|
|
|
from pathlib import Path
|
2026-04-29 12:29:39 +02:00
|
|
|
from typing import Optional
|
2026-04-27 14:27:25 +02:00
|
|
|
|
2026-05-07 11:09:06 +02:00
|
|
|
from fastapi import FastAPI, Request
|
2026-04-21 14:26:12 +02:00
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
from fastapi.responses import JSONResponse
|
2026-04-27 14:27:25 +02:00
|
|
|
from fastapi.staticfiles import StaticFiles
|
2026-04-21 14:26:12 +02:00
|
|
|
import os
|
2026-04-29 12:29:39 +02:00
|
|
|
import sys
|
2026-04-21 14:26:12 +02:00
|
|
|
|
2026-04-29 09:57:36 +02:00
|
|
|
from slowapi import _rate_limit_exceeded_handler
|
|
|
|
|
from slowapi.errors import RateLimitExceeded
|
|
|
|
|
|
2026-04-21 14:26:12 +02:00
|
|
|
from version import APP_VERSION, BUILD_DATE, DB_SCHEMA_VERSION, MODULE_VERSIONS
|
2026-04-21 14:49:28 +02:00
|
|
|
|
2026-05-07 10:40:10 +02:00
|
|
|
|
|
|
|
|
def _is_production_environment() -> bool:
|
|
|
|
|
return os.getenv("ENVIRONMENT", "development").strip().lower() in ("production", "prod")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_openapi_enabled() -> bool:
|
|
|
|
|
return os.getenv("PUBLIC_OPENAPI", "").strip().lower() in ("1", "true", "yes")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _health_ready_public_detail_enabled() -> bool:
|
|
|
|
|
"""In Prod standardmäßig keine Tabellen-/Migrations-Details (Information Disclosure)."""
|
|
|
|
|
return os.getenv("HEALTH_READY_PUBLIC_DETAIL", "").strip().lower() in ("1", "true", "yes")
|
|
|
|
|
|
|
|
|
|
|
2026-04-29 12:29:39 +02:00
|
|
|
# Run database migrations before API start — halbes Schema ist schlimmer als kein Start
|
2026-05-07 16:28:25 +02:00
|
|
|
# (run_migrations: pending *.sql in einer Transaktion pro Datei, Buchführung schema_migrations)
|
2026-04-29 12:29:39 +02:00
|
|
|
# Lokal ohne DB / nur Tests: SKIP_DB_MIGRATE=1
|
|
|
|
|
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() in ("1", "true", "yes"):
|
2026-05-05 16:18:42 +02:00
|
|
|
print("[SKIP_DB_MIGRATE] Migrationen uebersprungen (nur fuer Entwicklung ohne DB)")
|
2026-04-29 12:29:39 +02:00
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
import run_migrations
|
|
|
|
|
|
|
|
|
|
rc = run_migrations.main()
|
|
|
|
|
if rc != 0:
|
2026-05-05 16:18:42 +02:00
|
|
|
print(f"[FAIL] Datenbank-Migration fehlgeschlagen (Exit-Code {rc}). Start abgebrochen.")
|
2026-04-29 12:29:39 +02:00
|
|
|
sys.exit(1)
|
2026-05-05 16:18:42 +02:00
|
|
|
print("[OK] Database migrations completed")
|
2026-04-29 12:29:39 +02:00
|
|
|
except SystemExit:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
2026-05-05 16:18:42 +02:00
|
|
|
print(f"[FAIL] Migration-Laufzeitfehler: {e}")
|
2026-04-29 12:29:39 +02:00
|
|
|
sys.exit(1)
|
2026-04-21 14:26:12 +02:00
|
|
|
|
2026-06-07 15:36:31 +02:00
|
|
|
# Registry-first: Module → DB (nur registrierte Rechte/Kontingente in Admin-Matrix)
|
|
|
|
|
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() not in ("1", "true", "yes"):
|
|
|
|
|
try:
|
|
|
|
|
from rights_registry import sync_rights_registry_to_db
|
|
|
|
|
|
|
|
|
|
counts = sync_rights_registry_to_db()
|
|
|
|
|
print(
|
|
|
|
|
f"[OK] Rights registry sync: {counts['capabilities']} capabilities, "
|
|
|
|
|
f"{counts['features']} features"
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"[FAIL] Rights registry sync: {e}")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
2026-06-07 15:47:49 +02:00
|
|
|
from club_features import club_feature_enforcement_enabled
|
|
|
|
|
|
|
|
|
|
_cfe = os.getenv("CLUB_FEATURE_ENFORCE", "0")
|
|
|
|
|
print(
|
|
|
|
|
f"[OK] CLUB_FEATURE_ENFORCE raw={_cfe!r} "
|
|
|
|
|
f"active={club_feature_enforcement_enabled()}"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-29 09:57:36 +02:00
|
|
|
from routers.auth import limiter as auth_rate_limiter
|
|
|
|
|
|
2026-05-07 10:40:10 +02:00
|
|
|
# OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1
|
|
|
|
|
_expose_docs = (not _is_production_environment()) or _public_openapi_enabled()
|
|
|
|
|
_openapi_url = "/openapi.json" if _expose_docs else None
|
|
|
|
|
_docs_url = "/docs" if _expose_docs else None
|
|
|
|
|
_redoc_url = "/redoc" if _expose_docs else None
|
|
|
|
|
|
2026-04-21 14:26:12 +02:00
|
|
|
# Initialize FastAPI app
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
title="Shinkan Jinkendo API",
|
|
|
|
|
description="Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung",
|
2026-05-07 10:40:10 +02:00
|
|
|
version=APP_VERSION,
|
|
|
|
|
openapi_url=_openapi_url,
|
|
|
|
|
docs_url=_docs_url,
|
|
|
|
|
redoc_url=_redoc_url,
|
2026-04-21 14:26:12 +02:00
|
|
|
)
|
|
|
|
|
|
2026-04-29 09:57:36 +02:00
|
|
|
# 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()]
|
2026-04-21 14:26:12 +02:00
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=ALLOWED_ORIGINS,
|
|
|
|
|
allow_credentials=True,
|
2026-05-09 22:11:33 +02:00
|
|
|
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
|
|
|
allow_headers=["Content-Type", "X-Auth-Token", "X-Active-Club-Id"],
|
2026-04-21 14:26:12 +02:00
|
|
|
)
|
|
|
|
|
|
2026-05-07 11:09:06 +02:00
|
|
|
|
2026-06-07 05:57:13 +02:00
|
|
|
@app.middleware("http")
|
|
|
|
|
async def account_onboarding_api_gate(request: Request, call_next):
|
|
|
|
|
"""
|
|
|
|
|
Phase A: Domänen-APIs für unverified / verified_pending_club sperren.
|
|
|
|
|
Siehe account_onboarding_gate.py und MEMBERSHIP_RBAC_DECISIONS_2026-06.md §1.1
|
|
|
|
|
"""
|
|
|
|
|
from account_onboarding_gate import evaluate_request_gate
|
|
|
|
|
|
|
|
|
|
token = request.headers.get("x-auth-token") or request.headers.get("X-Auth-Token")
|
|
|
|
|
allowed, reason, _state = evaluate_request_gate(
|
|
|
|
|
token,
|
|
|
|
|
request.url.path,
|
|
|
|
|
request.method,
|
|
|
|
|
)
|
|
|
|
|
if not allowed:
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=403,
|
|
|
|
|
content={
|
|
|
|
|
"detail": (
|
|
|
|
|
"Zugriff erst nach E-Mail-Bestätigung und Vereinsmitgliedschaft möglich. "
|
|
|
|
|
"Du kannst einen Beitrittsantrag stellen oder dein Konto in den Einstellungen verwalten."
|
|
|
|
|
),
|
|
|
|
|
"reason": reason,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return await call_next(request)
|
|
|
|
|
|
|
|
|
|
|
2026-05-07 11:09:06 +02:00
|
|
|
@app.middleware("http")
|
|
|
|
|
async def add_api_security_headers(request: Request, call_next):
|
|
|
|
|
"""Konsistente Basis-Header auch für rein JSON-Responses (MIME-Sniffing)."""
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
|
|
|
return response
|
2026-04-21 14:26:12 +02:00
|
|
|
|
|
|
|
|
# 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}
|
|
|
|
|
|
2026-04-29 12:29:39 +02:00
|
|
|
|
|
|
|
|
@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",
|
2026-05-05 16:12:17 +02:00
|
|
|
"club_members",
|
|
|
|
|
"club_member_roles",
|
2026-04-29 12:29:39 +02:00
|
|
|
)
|
|
|
|
|
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))
|
2026-05-07 10:40:10 +02:00
|
|
|
body = {
|
2026-04-29 12:29:39 +02:00
|
|
|
"status": "ready" if complete else "degraded",
|
|
|
|
|
"database": err is None,
|
|
|
|
|
"detail": err,
|
|
|
|
|
"schema_complete": complete,
|
|
|
|
|
"tables": tables,
|
|
|
|
|
"schema_migrations_count": migration_count,
|
|
|
|
|
}
|
2026-05-07 10:40:10 +02:00
|
|
|
if _is_production_environment() and not _health_ready_public_detail_enabled():
|
|
|
|
|
return {
|
|
|
|
|
"status": body["status"],
|
|
|
|
|
"database": body["database"],
|
|
|
|
|
"schema_complete": body["schema_complete"],
|
|
|
|
|
}
|
|
|
|
|
return body
|
2026-04-29 12:29:39 +02:00
|
|
|
|
|
|
|
|
|
2026-04-21 14:26:12 +02:00
|
|
|
# Root Endpoint
|
|
|
|
|
@app.get("/")
|
|
|
|
|
def read_root():
|
|
|
|
|
"""Root endpoint - API info"""
|
2026-05-07 10:40:10 +02:00
|
|
|
out = {
|
2026-04-21 14:26:12 +02:00
|
|
|
"app": "Shinkan Jinkendo API",
|
|
|
|
|
"version": APP_VERSION,
|
2026-05-07 10:40:10 +02:00
|
|
|
"health": "/health",
|
2026-04-21 14:26:12 +02:00
|
|
|
}
|
2026-05-07 10:40:10 +02:00
|
|
|
if _expose_docs:
|
|
|
|
|
out["docs"] = "/docs"
|
|
|
|
|
return out
|
2026-04-21 14:26:12 +02:00
|
|
|
|
2026-04-21 14:56:16 +02:00
|
|
|
# Register routers
|
2026-06-07 09:21:59 +02:00
|
|
|
from routers import auth, profiles, exercises, exercise_progression_graphs, clubs, club_memberships, club_join_requests, club_creation_requests, admin_users, admin_user_content, admin_rights, me_entitlements, platform_media_storage, media_assets, skills, skill_profiles, training_planning, planning_exercise_suggest, dashboard, training_modules, training_framework_programs, catalogs, maturity_models, matrix_stack_bundle, matrix_editor, import_wiki, import_wiki_admin, legal_documents, content_reports, ai_prompts_admin, ai_skill_retrieval_admin, exercise_enrichment_admin
|
2026-04-21 14:56:16 +02:00
|
|
|
|
2026-04-21 16:12:25 +02:00
|
|
|
app.include_router(auth.router)
|
|
|
|
|
app.include_router(profiles.router)
|
2026-04-22 16:44:32 +02:00
|
|
|
app.include_router(exercises.router)
|
2026-04-30 11:47:50 +02:00
|
|
|
app.include_router(exercise_progression_graphs.router)
|
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
|
|
|
app.include_router(clubs.router)
|
2026-05-05 21:15:27 +02:00
|
|
|
app.include_router(club_memberships.router)
|
|
|
|
|
app.include_router(club_join_requests.router)
|
2026-06-07 07:09:39 +02:00
|
|
|
app.include_router(club_creation_requests.router)
|
2026-05-05 21:05:52 +02:00
|
|
|
app.include_router(admin_users.router)
|
2026-06-06 17:53:25 +02:00
|
|
|
app.include_router(admin_user_content.router)
|
2026-06-07 09:21:59 +02:00
|
|
|
app.include_router(admin_rights.router)
|
2026-06-06 21:10:52 +02:00
|
|
|
app.include_router(me_entitlements.router)
|
2026-05-07 12:36:46 +02:00
|
|
|
app.include_router(platform_media_storage.router)
|
2026-05-07 12:55:50 +02:00
|
|
|
app.include_router(media_assets.router)
|
feat(compliance): P-06 Upload-Einwilligungsdialog v1-conservative
Implementiert server-seitige Rechteerklärungspflicht für alle Medien-Uploads
und Sichtbarkeits-Promotions (konservative Erstannahme: alle Uploads).
Backend:
- backend/media_rights.py (NEU): Kernmodul — validate_rights_declaration,
check_rights_coverage, assert_rights_for_promotion, assert_rights_for_exercise_link,
write_rights_declaration, update_rights_quick_fields
- backend/migrations/048_media_rights_declarations.sql (NEU): Tabelle
media_asset_rights_declarations (Append-only Audit-Log), Felder
rights_status/rights_visibility_level in media_assets
- backend/routers/media_assets.py: P-06-Pflichtprüfung in PATCH (single + bulk),
POST /api/media-assets/{id}/rights-declarations (Re-Deklaration),
GET /api/admin/media-rights/legacy-summary|legacy-assets (Admin-Endpoints)
- backend/routers/exercises.py: P-06-Felder in upload_exercise_media,
assert_rights_for_exercise_link in attach_exercise_media_from_asset
- backend/main.py: admin_rights_router registriert
Frontend:
- frontend/src/components/RightsDeclarationDialog.jsx (NEU): 9-Felder-Dialog
(konservativ: immer alle Fragen), Client-Validierung, VORLÄUFIG-Hinweis
- frontend/src/pages/MediaLibraryPage.jsx: Dialog-Intercept vor Upload,
Altbestand-Indikator (legacy_unreviewed)
- frontend/src/utils/api.js: P-06-Felder in bulkUploadMediaAssets weitergeleitet
Tests:
- backend/tests/test_media_rights_declaration.py (NEU): 28 Unit-/Integrationstests
- backend/tests/test_media_assets_archive.py: P-06 fetchone-Slots + Mock ergänzt
- backend/tests/test_media_assets_copyright_promotion.py: check_rights_coverage gemockt
- tests/dev-smoke-test.spec.js: 5 P-06 E2E-Tests ergänzt
Dokumentation:
- docs/compliance-implementation.md: P-06-Abschnitt
- docs/compliance-package-register.md: Status ⚠️ teilweise umgesetzt (KRIT-04 offen)
- docs/compliance-roadmap.md: P-06 im Freigaben-Log
Offen: KRIT-04 (rechtliche Finalisierung Einwilligungsformulierung) — technisch
vollständig, Rechtstext VORLÄUFIG.
version: 0.8.75
module: media_rights 1.0.0, media_assets 1.13.0, exercises 2.20.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 08:12:44 +02:00
|
|
|
app.include_router(media_assets.admin_rights_router)
|
2026-05-11 12:33:13 +02:00
|
|
|
app.include_router(media_assets.admin_legal_hold_router)
|
feat: Skills & Methods catalog complete
Backend:
- Created routers/skills.py with full CRUD
- Skills: list (with category filter), get, create, update, delete
- Methods: list (with category filter), get, create, update, delete
- Default: only show active items
- Read access: all authenticated users
- Write access: admin only
- Registered skills router in main.py
Frontend:
- Complete SkillsPage with 2 tabs (Fähigkeiten, Trainingsmethoden)
- Browse by category with cards layout
- Admin CRUD forms (importance rating for skills, duration/group size for methods)
- Mobile-responsive grid layout
- Updated api.js with all skill/method functions
- Added /skills route to App.jsx
Migration already exists: 003_catalogs.sql (skills, training_methods + seed data)
Next: Training Planning (core feature)
2026-04-22 16:50:31 +02:00
|
|
|
app.include_router(skills.router)
|
2026-05-20 16:42:25 +02:00
|
|
|
app.include_router(skill_profiles.router)
|
feat: Training Planning (core feature) complete
Backend:
- Created migration 006_training_planning.sql
- training_units table (planned vs actual, status, notes)
- training_unit_exercises M:N (order, duration, modifications)
- Created routers/training_planning.py with full CRUD
- List with filters (group, date range, status)
- Get detail with exercises
- Create/Update/Delete with access control
- Quick create endpoint (auto-fills from group defaults)
- Permission: trainer for own groups, admin for all
- Registered training_planning router in main.py
Frontend:
- Complete TrainingPlanningPage with full planning workflow
- Group selector + date range picker
- List view with status badges (planned/completed/cancelled)
- Create/Edit modal with exercise management
- Drag to reorder exercises (▲▼)
- Quick create button (one-click with group defaults)
- Durchführung section (actual date/time, attendance, status)
- Public notes + trainer-only notes
- Updated api.js with deleteTrainingUnit and quickCreateTrainingUnit
- Added /planning route to App.jsx
- Navigation already configured (Calendar icon)
This is the CORE feature - trainers can now plan and document training sessions.
Next: Admin routes, role system refinement, testing
2026-04-22 16:54:34 +02:00
|
|
|
app.include_router(training_planning.router)
|
2026-05-22 21:52:18 +02:00
|
|
|
app.include_router(planning_exercise_suggest.router)
|
2026-05-14 07:47:27 +02:00
|
|
|
app.include_router(dashboard.router)
|
2026-05-12 21:35:07 +02:00
|
|
|
app.include_router(training_modules.router)
|
2026-05-05 08:41:43 +02:00
|
|
|
app.include_router(training_framework_programs.router)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
app.include_router(catalogs.router)
|
2026-04-27 11:32:30 +02:00
|
|
|
app.include_router(maturity_models.router)
|
2026-04-27 13:13:36 +02:00
|
|
|
app.include_router(matrix_stack_bundle.router)
|
2026-05-22 11:12:50 +02:00
|
|
|
app.include_router(matrix_editor.router)
|
2026-04-24 14:41:52 +02:00
|
|
|
app.include_router(import_wiki.router)
|
2026-04-27 08:52:05 +02:00
|
|
|
app.include_router(import_wiki_admin.router)
|
2026-05-10 11:53:07 +02:00
|
|
|
app.include_router(legal_documents.router)
|
2026-05-11 17:54:53 +02:00
|
|
|
app.include_router(content_reports.router)
|
2026-05-22 11:02:02 +02:00
|
|
|
app.include_router(ai_prompts_admin.router)
|
2026-05-22 09:57:39 +02:00
|
|
|
app.include_router(ai_skill_retrieval_admin.router)
|
2026-05-23 07:35:45 +02:00
|
|
|
app.include_router(exercise_enrichment_admin.router)
|
2026-04-21 14:26:12 +02:00
|
|
|
|
2026-05-07 10:52:14 +02:00
|
|
|
# Lokale Übungs-Medien: standardmäßig nur über geschützten API-Pfad
|
|
|
|
|
# GET /api/exercises/{id}/media/{mid}/file (?ssetoken für <img>/<video>).
|
|
|
|
|
# Notfall/Legacy: ALLOW_PUBLIC_MEDIA_STATIC=1 → wieder öffentlich unter /media/
|
2026-04-27 14:27:25 +02:00
|
|
|
_media_dir = os.getenv("MEDIA_ROOT", str(Path(__file__).resolve().parent / "media"))
|
|
|
|
|
Path(_media_dir).mkdir(parents=True, exist_ok=True)
|
2026-05-07 10:52:14 +02:00
|
|
|
if os.getenv("ALLOW_PUBLIC_MEDIA_STATIC", "").strip().lower() in ("1", "true", "yes"):
|
|
|
|
|
app.mount("/media", StaticFiles(directory=_media_dir), name="media")
|
2026-04-27 14:27:25 +02:00
|
|
|
|
2026-04-21 14:26:12 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import uvicorn
|
|
|
|
|
uvicorn.run(
|
|
|
|
|
"main:app",
|
|
|
|
|
host="0.0.0.0",
|
|
|
|
|
port=8000,
|
|
|
|
|
reload=True
|
|
|
|
|
)
|