{"openapi":"3.1.0","info":{"title":"drbruening API","description":"\n## drbruening RAG-KI API\n\nSchnittstelle für den Zugriff auf die KI-gestützte TIA-Analyse.\n\n### Authentifizierung\nAlle Endpunkte (außer `/health` und `/api/v1/auth/login`) erfordern einen Bearer-Token.\n\n1. Token-Paar holen: `POST /api/v1/auth/login` -- liefert einen kurzlebigen\n   `access_token` und einen langlebigen `refresh_token` (Details dort).\n2. Access-Token in Header senden: `Authorization: Bearer <access_token>`.\n3. Nach Ablauf des Access-Tokens mit `POST /api/v1/auth/refresh` ein neues\n   Token-Paar holen, ohne erneute Anmeldung -- dafür ist der `refresh_token`\n   da, sicher lokal gespeichert auf dem Client-Gerät.\n\n### Mandanten-Isolation\nJeder Benutzer gehört zu einem Mandanten (Tenant), der aus dem JWT abgeleitet wird\n(implizites Tenant-Scoping). Rückgaben sind immer auf den eigenen Mandanten begrenzt,\naußer bei `/admin/*`-Endpunkten (nur `system-admin`).\n\n### Rollen\n`system-admin` (nur Matthias, tenant-übergreifend) · `developer` (externer\nProgrammierer, eigener Sandbox-Tenant mit Test-Projektdaten) · `owner` /\n`admin` (Verwaltungsrechte im eigenen Tenant) · `member` (normaler Nutzer).\n\n### Projektdaten\nDer Client spricht für **alles** ausschließlich mit dieser API -- nie direkt\nmit Slokal, nie mit einem VPN/Tunnel-Endpunkt. Rohdaten (XML/SCL von\nTIA-Portal-Projekten sowie hochgeladene Dokumente) werden trotzdem **nie**\nim Scloud-Anwendungscode gespeichert oder verarbeitet: der Datei-Upload läuft\nüber einen eigenen Scloud-API-Endpunkt, den nginx per Streaming direkt an\nSlokal durchreicht (Details siehe `POST /projects/upload`). Projekte entstehen\ndabei automatisch: der Projektname ist die Identität, jeder weitere Upload unter\ndemselben Namen ist eine neue Version (Snapshot). Scloud verwaltet dazu nur\nMetadaten (`/projects/*`, `/uploads/*`).\n","version":"1.0.0"},"paths":{"/api/v1/health/live":{"get":{"tags":["health"],"summary":"Liveness (nur Scloud + DB)","description":"Fuer den Docker-HEALTHCHECK des Containers selbst -- haengt bewusst NICHT von Slokal-Erreichbarkeit ab, sonst wuerde der Container als 'unhealthy' gelten, sobald nur Slokal down ist.","operationId":"health_live_api_v1_health_live_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/health":{"get":{"tags":["health"],"summary":"Gesundheitsstatus (Scloud, Slokal, Gesamtsystem)","description":"Ein konsolidierter Endpunkt statt vier einzelner -- liefert Scloud-, Scloud-DB-,\nSlokal- und Slokal-Software-Status in einem JSON. Wird von der Weboberfläche\n(Health-Check-Anzeige) verwendet.","operationId":"health_api_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/auth/login":{"post":{"tags":["auth"],"summary":"Anmelden","description":"Gibt ein Token-Paar zurück: einen kurzlebigen `access_token`\n(60 Minuten, siehe auch `expires_in` in\nder Antwort) und einen langlebigen `refresh_token`\n(30 Tage, sliding -- die Laufzeit\nverlängert sich bei jeder Nutzung über `POST /auth/refresh` erneut um die\nvolle Dauer).\n\n**Ablauf:**\n1. `access_token` bei jeder Anfrage senden: `Authorization: Bearer <access_token>`.\n2. Sobald er abgelaufen ist (oder proaktiv kurz davor, anhand `expires_in`):\n   `POST /auth/refresh` mit dem `refresh_token` aufrufen, um ohne erneute\n   Anmeldung ein neues Token-Paar zu erhalten.\n3. `refresh_token` sicher lokal speichern (z. B. unter Windows per DPAPI,\n   an das Windows-Benutzerkonto gebunden) -- **nicht** im Klartext neben dem\n   Programm ablegen. Damit muss sich ein Anwender auf demselben Gerät nicht\n   jede Schicht neu einloggen.","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_api_v1_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/register":{"post":{"tags":["auth"],"summary":"Benutzer registrieren","description":"Neuen Benutzer (Rolle `member`) für einen bestehenden Mandanten anlegen.\n\n**Jetzt:** nur `system-admin` darf diesen Endpunkt aufrufen -- es gibt noch\nkeine Selbstregistrierung und keinen Einladungs-Flow (kommt erst in der\nkommerziellen Version). Für Benutzer mit anderer Rolle `POST /users` nutzen.","operationId":"register_api_v1_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/auth/refresh":{"post":{"tags":["auth"],"summary":"Token-Paar erneuern","description":"Tauscht einen gültigen `refresh_token` gegen ein neues Token-Paar.\nErfordert **keinen** gültigen `access_token` mehr (Unterschied zur früheren\nSliding Session) -- genau das erlaubt es, eine bereits abgelaufene Sitzung\nohne erneute Anmeldung fortzusetzen.\n\n**Rotation:** der vorgelegte `refresh_token` wird dabei sofort ungültig, der\nzurückgegebene ist sein Nachfolger -- der Client muss seinen gespeicherten\nToken danach durch den neuen ersetzen. Wird ein bereits benutzter (rotierter)\n`refresh_token` erneut vorgelegt -- z. B. weil eine Kopie davon gestohlen\nwurde --, werden vorsorglich alle aktiven Refresh-Tokens des Nutzers\nwiderrufen; die Antwort ist dann `401` und eine erneute Anmeldung (auf allen\nGeräten) ist nötig.","operationId":"refresh_api_v1_auth_refresh_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"401":{"description":"Refresh-Token ungültig, abgelaufen, bereits verwendet, oder Nutzer/Mandant deaktiviert."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/logout":{"post":{"tags":["auth"],"summary":"Abmelden","description":"Widerruft den übergebenen `refresh_token` serverseitig -- er kann danach\nnicht mehr für `POST /auth/refresh` verwendet werden. Betrifft nur dieses\neine Gerät/diese eine Sitzung, andere angemeldete Geräte desselben Nutzers\nbleiben unberührt (kein \"überall abmelden\").\n\nDer `access_token` selbst bleibt bis zu seinem (kurzen) Ablauf technisch\ngültig, da er zustandslos ist (12-Faktor: kein Sitzungsspeicher im Prozess)\n-- der Client muss ihn zusätzlich lokal verwerfen. Erfordert deshalb bewusst\n**keinen** gültigen `access_token`: der `refresh_token` selbst ist der\nBerechtigungsnachweis, auch wenn der `access_token` schon abgelaufen ist.\nIdempotent -- ein unbekannter oder bereits widerrufener Token führt ebenfalls\nzu `{\"status\": \"ok\"}`.","operationId":"logout_api_v1_auth_logout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["auth"],"summary":"Eigene Benutzerdaten","description":"Gibt Daten des aktuell angemeldeten Benutzers zurück.","operationId":"me_api_v1_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/tenants/{tenant_id}":{"get":{"tags":["tenants"],"summary":"Mandant abrufen","description":"Nur der eigene Mandant ist sichtbar, außer für system-admin.\n\nEnthält neben Stammdaten auch den aktuellen Lizenz-/Verbrauchsstatus\n(`anfragen_pro_monat`, `anfragen_verbraucht_monat`, `periode`) für die\nLizenz-/Verbrauchsanzeige im Client -- bewusst unabhängig vom Login: ein\nMandant mit aufgebrauchtem Kontingent bleibt `is_active` und kann sich\nweiterhin anmelden, nur das Senden neuer Chat-Nachrichten wird dann\nserverseitig abgelehnt (siehe `routes/quota.py`). `anfragen_verbraucht_monat`\nist `0`, wenn im laufenden Monat noch keine einzige Chat-Anfrage gestellt wurde\n(dann existiert noch keine Zeile in der `usage`-Tabelle für diesen Zeitraum).","operationId":"get_tenant_api_v1_tenants__tenant_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["tenants"],"summary":"Mandant aktualisieren","description":"Nur `name` ist editierbar. `plan`/Stripe-Felder werden ausschließlich über den Stripe-Webhook gepflegt.","operationId":"update_tenant_api_v1_tenants__tenant_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/tenants":{"get":{"tags":["admin"],"summary":"Alle Mandanten auflisten (nur system-admin)","operationId":"admin_list_tenants_api_v1_admin_tenants_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/TenantResponse"},"type":"array","title":"Response Admin List Tenants Api V1 Admin Tenants Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"post":{"tags":["admin"],"summary":"Mandant anlegen (nur system-admin)","description":"Erstellt einen neuen Mandanten und dessen ersten Benutzer (Standardrolle\n`owner`, z.B. `developer` für die Sandbox des externen Programmierers) in\neinem Schritt. **Jetzt:** direkte Anlage mit Initialpasswort durch\nsystem-admin (nur Matthias) -- der Einladungs-Link-Flow (Nutzer setzt eigenes\nPasswort über einmaligen Token) kommt erst in der kommerziellen Version.\n\nBei `role=developer` wird zusätzlich ein Dummy-Testprojekt angelegt, damit\ndie Sandbox sofort nutzbar ist, ohne echte Kundendaten zu berühren.","operationId":"admin_create_tenant_api_v1_admin_tenants_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreateTenantRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCreateTenantResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/admin/tenants/{tenant_id}":{"delete":{"tags":["admin"],"summary":"Mandant löschen (nur system-admin)","description":"Löscht den Mandanten samt aller Benutzer, Projekte, Snapshots, Dokumente und\nChats (CASCADE) sowie alle seine Daten auf dem Analyse-Server (Projektdaten,\nSuchindex). Gibt insbesondere den `slug` wieder frei -- das Löschen\neines einzelnen Benutzers (`DELETE /users/{id}`) löscht NICHT den Mandanten\nselbst, dessen Slug bliebe sonst dauerhaft blockiert.\n\n* **409 `upload_in_progress`** -- für ein Projekt des Mandanten läuft gerade ein Upload.\n* **503 `slokal_unreachable`** -- Analyse-Server nicht erreichbar; es wurde nichts gelöscht.","operationId":"admin_delete_tenant_api_v1_admin_tenants__tenant_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/admin/slokal/status":{"get":{"tags":["admin"],"summary":"Slokal-Adresse und Erreichbarkeit (nur system-admin)","description":"`host` ist die WireGuard-Tunnel-Adresse, unter der Scloud Slokal erreicht --\nkein öffentliches Internet-IP, da Slokal nicht direkt exponiert ist.\n\n`public_ip` ist die aktuelle öffentliche IP von Slokals Heimnetz-Anschluss\n(aus dem letzten WireGuard-Handshake, kann sich bei dynamischer IP ändern) --\nzum direkten SSH-Zugriff von unterwegs. `public_ip_last_seen` zeigt, wie\naktuell dieser Wert ist (Snapshot wird minütlich aktualisiert).","operationId":"slokal_status_api_v1_admin_slokal_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlokalStatusResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/admin/slokal/shutdown":{"post":{"tags":["admin"],"summary":"Slokal sauber herunterfahren (nur system-admin)","description":"Versucht zuerst den künftigen Slokal-eigenen `POST /api/v1/system/shutdown`\n(Teil der noch ausstehenden Slokal-Portierung) -- solange der nicht\nexistiert, per SSH-Fallback über den WireGuard-Tunnel (`SLOKAL_SSH_USER`\nmuss konfiguriert und der Public Key auf Slokal hinterlegt sein).\nKlappt keins von beidem: `503 temporarily_unavailable`.","operationId":"slokal_shutdown_api_v1_admin_slokal_shutdown_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/users":{"get":{"tags":["users"],"summary":"Benutzer auflisten","description":"Tenant-Scope kommt implizit aus dem JWT -- es werden ausschließlich Benutzer\ndes eigenen Mandanten zurückgegeben.\n\n**Ausnahme system-admin:** optionaler Query-Parameter `tenant_id` zeigt\nBenutzer eines beliebigen Mandanten; ohne `tenant_id` werden Benutzer aller\nMandanten zurückgegeben (tenant-übergreifende Verwaltung).","operationId":"list_users_api_v1_users_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"tenant_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Tenant Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponse"},"title":"Response List Users Api V1 Users Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["users"],"summary":"Benutzer anlegen","description":"Wer welche Rolle anlegen darf, ist fest vorgegeben:\n\n- `system-admin`: jede Rolle, in jedem Mandanten.\n- `developer`: `owner`, `admin`, `member` -- nur im eigenen Mandanten.\n- `owner`: `admin`, `member` -- nur im eigenen Mandanten.\n- `admin`: nur `member` -- nur im eigenen Mandanten.\n- `member`: darf niemanden anlegen.\n\nEs gibt noch keinen Einladungs-Flow (kommt erst in der kommerziellen\nVersion), bei dem ein Tenant-Admin eigenständig Nutzer einladen kann.","operationId":"create_user_api_v1_users_post","security":[{"OAuth2PasswordBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/users/{user_id}":{"get":{"tags":["users"],"summary":"Benutzer abrufen","operationId":"get_user_api_v1_users__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["users"],"summary":"Benutzer aktualisieren","description":"Eigenes Profil: `full_name`, `email`, `password`. Die eigene Rolle und der\neigene Aktiv-Status sind nicht änderbar.\n\nFremde Benutzer: nur durch owner/admin des eigenen Mandanten und nur, wenn\nsie Benutzer dieser Rolle auch anlegen dürften (owner → admin/member,\nadmin → member) – das gilt für die bisherige wie für eine neue Rolle.\nsystem-admin darf alles, mandantenübergreifend; die Rolle `system-admin`\nvergibt nur ein system-admin.","operationId":"update_user_api_v1_users__user_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["users"],"summary":"Benutzer löschen (nur system-admin)","operationId":"delete_user_api_v1_users__user_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/projects":{"get":{"tags":["projects"],"summary":"Projekte des eigenen Mandanten auflisten","description":"Projekte entstehen automatisch beim ersten `POST /projects/upload` unter einem neuen Namen.","operationId":"list_projects_api_v1_projects_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ProjectResponse"},"type":"array","title":"Response List Projects Api V1 Projects Get"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/api/v1/projects/{project_id}":{"get":{"tags":["projects"],"summary":"Projekt abrufen","operationId":"get_project_api_v1_projects__project_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Project Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["projects"],"summary":"Projekt löschen","description":"Löscht das Projekt **unwiderruflich mit allen Versionen**: die Metadaten auf\nScloud sowie Rohdaten (alle Versionen), Suchindex und Chunks auf dem\nAnalyse-Server. Chats des Projekts werden mit gelöscht. Ein späterer Upload unter\ndemselben Namen legt ein neues, leeres Projekt an.\n\nLöschen dürfen owner, admin und system-admin sowie der Benutzer, dessen Upload\ndas Projekt angelegt hat.\n\n* **403 `forbidden`** -- keine Berechtigung, dieses Projekt zu löschen.\n* **409 `upload_in_progress`** -- für das Projekt läuft gerade ein Upload; erst abwarten.\n* **503 `slokal_unreachable`** -- der Analyse-Server ist nicht erreichbar. Das Projekt\n  bleibt dann vollständig bestehen (es wird nie nur halb gelöscht); später erneut versuchen.","operationId":"delete_project_api_v1_projects__project_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Project Id"}}],"responses":{"204":{"description":"Successful Response"},"403":{"description":"Keine Berechtigung (`forbidden`): weder Admin noch Ersteller des Projekts."},"404":{"description":"Projekt nicht gefunden (oder gehört zu einem anderen Mandanten)."},"409":{"description":"Für das Projekt läuft gerade ein Upload (`upload_in_progress`)."},"503":{"description":"Analyse-Server nicht erreichbar (`slokal_unreachable`) -- nichts wurde gelöscht."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/projects/{project_id}/snapshots":{"get":{"tags":["projects"],"summary":"Snapshots (Versionen) eines Projekts auflisten","description":"Jeder Upload unter dem Namen des Projekts erzeugt einen Snapshot; neueste zuerst. Der jüngste Snapshot mit Status `done` ist die aktuelle Version, gegen die Chat und Analyse laufen. Versionen werden nie gelöscht (nur mit dem ganzen Projekt).","operationId":"list_snapshots_api_v1_projects__project_id__snapshots_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Project Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SnapshotResponse"},"title":"Response List Snapshots Api V1 Projects  Project Id  Snapshots Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/projects/{project_id}/snapshots/{snapshot_id}":{"get":{"tags":["projects"],"summary":"Snapshot abrufen","operationId":"get_snapshot_api_v1_projects__project_id__snapshots__snapshot_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Project Id"}},{"name":"snapshot_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Snapshot Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SnapshotResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/projects/{project_id}/chats":{"get":{"tags":["chats"],"summary":"Eigene Chats zu diesem Projekt auflisten","description":"Liefert alle Chats des angemeldeten Users zu diesem Projekt, neueste zuerst. Ein User kann mehrere Chats zum selben Projekt haben (z.B. um neu anzufangen).","operationId":"list_project_chats_api_v1_projects__project_id__chats_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Project Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ChatResponse"},"title":"Response List Project Chats Api V1 Projects  Project Id  Chats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["chats"],"summary":"Neuen Chat zu diesem Projekt anlegen","description":"Legt immer einen neuen, leeren Chat an (nicht idempotent) -- damit lässt sich jederzeit ohne die Historie vorheriger Chats neu anfangen.","operationId":"create_project_chat_api_v1_projects__project_id__chats_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Project Id"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/chats/{chat_id}":{"get":{"tags":["chats"],"summary":"Chat abrufen","operationId":"get_chat_api_v1_chats__chat_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Chat Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["chats"],"summary":"Chat aktualisieren (Titel)","operationId":"update_chat_api_v1_chats__chat_id__patch","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Chat Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateChatRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["chats"],"summary":"Chatverlauf löschen","description":"Löscht den Chat und alle zugehörigen Nachrichten (CASCADE).","operationId":"delete_chat_api_v1_chats__chat_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Chat Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/chats/{chat_id}/messages":{"get":{"tags":["chats"],"summary":"Chatverlauf abrufen","operationId":"list_messages_api_v1_chats__chat_id__messages_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Chat Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MessageResponse"},"title":"Response List Messages Api V1 Chats  Chat Id  Messages Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["chats"],"summary":"Chat-Frage stellen (SSE-Streaming)","description":"Speichert die Frage des Users, leitet sie an die KI-Analyse auf Slokal weiter\nund **streamt** die Antwort Token für Token als Server-Sent-Events zurück\n(`data: {\"token\": \"...\"}\\n\\n`). Die vollständige Antwort wird nach Abschluss\ndes Streams im Chatverlauf gespeichert.\n\nDirekt danach, vor dem Abschluss (`data: [DONE]\\n\\n`), kommt genau einmal\n`data: {\"message_id\": \"...\"}\\n\\n\"` -- die ID der gerade gespeicherten\nAntwort. Damit lässt sich anschließend `POST .../feedback` oder\n`GET .../variables` (Beobachtungstabellen-Funktion, siehe dort) auf genau\ndiese Antwort beziehen, ohne vorher `GET .../messages` abzufragen. Bleibt die\nAntwort leer (z.B. Verbindungsabbruch mitten im Stream, siehe unten), entfällt\ndieses Event -- es gibt dann nichts, worauf sich eine `message_id` beziehen\nkönnte.\n\nKann Slokal beim Verbindungsaufbau nicht erreicht werden, antwortet der\nEndpunkt strukturiert mit `503` (bevor das Streaming beginnt). Bricht die\nVerbindung erst mitten im Stream ab, wird stattdessen ein\n`data: {\"error\": \"...\"}\\n\\n\"`-Event gesendet -- der HTTP-Status ist dann schon\ngesendet (200) und kann nicht mehr geändert werden.","operationId":"create_message_api_v1_chats__chat_id__messages_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Chat Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMessageRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/chats/{chat_id}/messages/{message_id}/feedback":{"post":{"tags":["chats"],"summary":"Feedback zu einer Antwort geben","description":"`rating`: `1` (hilfreich) oder `-1` (nicht hilfreich).","operationId":"give_feedback_api_v1_chats__chat_id__messages__message_id__feedback_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Chat Id"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Message Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/chats/{chat_id}/messages/{message_id}/variables":{"get":{"tags":["chats"],"summary":"Variablen einer Antwort abrufen (für Beobachtungstabelle)","description":"Liefert die Variablen, die deterministisch zu dieser KI-Antwort gehören --\nermittelt aus dem Signalgraphen auf Slokal zum Zeitpunkt der Antwort, nicht\naus dem Antworttext zurückgeparst. Gedacht, um daraus eine Beobachtungstabelle\nin TIA Portal zu bauen.\n\n```json\n{\n  \"message_id\": \"...\",\n  \"tags\": [{\"name\": \"E_Kralle_1_offen\", \"address\": \"%I255.1\"}],\n  \"internal\": [{\"path\": \"\\\"Roboter_1_Kralle_1_DB\\\".T_Laufzeit.Q\"}]\n}\n```\n\n- `tags`: globale Ein-/Ausgänge/Merker -- direkt per `name` oder `address` beobachtbar.\n- `internal`: bausteininterne Variablen, bereits als fertiger, gültiger\n  TIA-Operandenpfad in `path` -- auch bei mehrstufig verschachtelten\n  Instanzen aufgelöst. Ist eine Instanz nicht eindeutig auflösbar, steht\n  `path: null` mit einer `note` -- diese Einträge beim Aufbau der\n  Beobachtungstabelle überspringen.\n- Beide Listen können leer sein (z.B. bei einer allgemeinen Frage ohne\n  Signalbezug) -- dann Button deaktivieren/ausblenden.\n- Nur für Antworten der KI sinnvoll (`role: assistant`); bei einer eigenen\n  Frage (`role: user`) oder einer Antwort von vor dieser Funktion (Migration\n  007) sind beide Listen leer.","operationId":"get_message_variables_api_v1_chats__chat_id__messages__message_id__variables_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Chat Id"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Message Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VariablesResponse"}}}},"404":{"description":"Chat oder Nachricht nicht gefunden (oder gehört nicht dem angemeldeten User)."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/uploads/{upload_id}":{"get":{"tags":["upload"],"summary":"Upload-/Verarbeitungsstatus abfragen","description":"Verarbeitungsstand eines Uploads. `upload_id` steht in der 202-Antwort von\n`POST /projects/upload`.\n\n**Status:**\n\n| `status` | Bedeutung |\n|---|---|\n| `queued` | Angenommen bzw. noch in der Übertragung, Verarbeitung noch nicht gestartet (nur kurz sichtbar). |\n| `processing` | Wird entpackt, geparst und indiziert. |\n| `done` | Fertig -- die hochgeladene Version ist jetzt die **aktuelle** Version des Projekts. |\n| `failed` | Fehlgeschlagen, Details in `error`. Die bisherige aktuelle Version bleibt unverändert. |\n\n**`error`** (nur bei `failed`) hat die Form `{\"code\", \"message\", \"details\"}`.\n\n- `message` ist ein für Anwender formulierter deutscher Text **ohne technische\n  Interna** (keine Serverpfade, keine Programm-Fehlermeldungen) und kann\n  unverändert angezeigt werden. Technische Ursachen stehen nur im Server-Log.\n- Für eigene Client-Logik (Symbol, „Erneut versuchen\"-Knopf usw.) bitte `code`\n  auswerten, nie den Wortlaut von `message` -- der kann sich ändern.\n\n| `code` | Bedeutung | Durch Anwender behebbar? |\n|---|---|---|\n| `zip_corrupt` | Datei ist kein gültiges/ein beschädigtes ZIP-Archiv. | Ja: Export neu erstellen. |\n| `zip_slip_detected`, `zip_symlink_detected`, `zip_bomb_detected` | Archiv aus Sicherheitsgründen abgelehnt (unzulässige Pfade, Verknüpfungen, zu viele/zu große Dateien). | Ja: Export prüfen. |\n| `no_usable_files` | Archiv enthält keine verwertbaren Projektdateien (.xml/.scl). | Ja: richtigen Ordner exportieren. |\n| `duplicate_upload` | Archiv identisch mit der aktuellen Version -- nichts geändert. Kein echter Fehler. | -- |\n| `upload_incomplete` | Übertragung nie vollständig angekommen bzw. abgebrochen. | Ja: erneut hochladen. |\n| `interrupted` | Verarbeitung durch Neustart des Analyse-Servers abgebrochen. | Ja: erneut hochladen. |\n| `parse_error`, `internal_error` | Verarbeitung auf dem Server fehlgeschlagen. | Nein: später erneut versuchen, bei Wiederholung Support. |\n| `not_a_zip`, `too_large` | Schon beim Empfang abgewiesen (kein ZIP-Archiv bzw. größer als 500 MB); dieselbe Meldung kam bereits als direkte Antwort auf den Upload-Aufruf. | Ja: richtige Datei hochladen. |\n| `upload_in_progress`, `upload_failed` | Schon beim Empfang abgewiesen (anderer Upload lief noch bzw. Empfang auf dem Server fehlgeschlagen). | Später erneut versuchen. |\n\nBei `done` zusätzlich: `skipped_files` (wegen nicht zugelassener Dateiendung\nignoriert), `unknown_instructions` (verwendete Anweisungen ohne Beschreibung)\nund `stats` (Zähler zur Verarbeitung).\n\nZum Abfragen (Polling) reicht ein Intervall von einigen Sekunden; die\nVerarbeitung eines Projekts dauert typischerweise Sekunden bis wenige Minuten.\nIst der Analyse-Server gerade nicht erreichbar: 503 `slokal_unreachable`.","operationId":"get_upload_status_api_v1_uploads__upload_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadStatusResponse"}}}},"404":{"description":"Upload nicht gefunden (oder gehört zu einem anderen Mandanten)."},"503":{"description":"Analyse-Server nicht erreichbar (`slokal_unreachable`), später erneut versuchen."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/projects/upload":{"post":{"tags":["upload","upload"],"summary":"Projekt hochladen (neue Version eines TIA-Projekts)","description":"Überträgt den aktuellen Stand eines TIA-Projekts als **ZIP-Archiv** und legt\ndamit eine **neue Version (Snapshot)** des Projekts an. Ein einziger Request\nlegt Projekt (falls neu) und Version an -- kein vorheriger Aufruf nötig.\n\n### Projekt-Identität\nDas Projekt wird über den Query-Parameter **`project_name`** bestimmt: der Name\ndes in TIA geöffneten Projekts, **pro Mandant eindeutig** (Groß-/Kleinschreibung\nrelevant, führende/nachgestellte Leerzeichen werden entfernt).\n\n* **Unbekannter Name** -> das Projekt wird angelegt, der Upload ist seine erste Version.\n* **Bekannter Name** -> neue Version des bestehenden Projekts. Sie **ersetzt die\n  bisherige als aktuellen Stand** (Chat und Analyse laufen gegen die aktuelle Version);\n  frühere Versionen bleiben erhalten und stehen unter\n  `GET /projects/{project_id}/snapshots`. Es wird nichts gelöscht.\n\nDer Name in der `manifest.json` benennt ein bestehendes Projekt **nicht** um;\nmaßgeblich ist immer `project_name` aus der URL. Die `project_id` des Projekts\nsteht in der 202-Antwort.\n\n### Ablauf\n1. `POST /api/v1/projects/upload?project_name=<Name>` -- Body ist das rohe ZIP\n   (**kein** multipart/form-data), `Content-Type: application/zip`,\n   `Authorization: Bearer <Token>`.\n2. Die Antwort **202** kommt, sobald die Datei vollständig angekommen und grob\n   geprüft ist (ZIP-Signatur, Größe, Duplikat). Die eigentliche Verarbeitung läuft danach im\n   Hintergrund weiter.\n3. `GET /api/v1/uploads/{upload_id}` (`upload_id` aus der 202-Antwort) abfragen,\n   bis `status` `done` oder `failed` ist.\n4. Bei `done` ist die neue Version die aktuelle Version des Projekts.\n\n### Sperre: ein Upload pro Projekt\nSolange für ein Projekt ein Upload läuft (`queued`/`processing`), wird jeder weitere\nUpload für **dasselbe Projekt** mit **409 `upload_in_progress`** abgelehnt -- z. B. bei\nmehrfachem Klicken auf „Hochladen\". Die Fehlerantwort enthält in `details.upload_id`\nden laufenden Upload, dessen Status abgefragt werden kann. Andere Projekte sind nicht\nbetroffen. Ein schon beim Empfang abgewiesener Upload (z. B. 415 `not_a_zip`) sperrt\nnicht -- ein sofortiger Neuversuch mit der richtigen Datei ist möglich. Ein\nhängengebliebener Upload (z. B. Serverausfall) gibt die Sperre spätestens nach\n30 Minuten frei.\n\n### Unveränderter Stand (409 `duplicate_upload`)\nIst das Archiv **byte-identisch** zur aktuellen Version desselben Projekts, wird es\nnicht erneut verarbeitet: 409 `duplicate_upload` -- keine Änderung, kein Fehler im\neigentlichen Sinn. Es entsteht keine neue Version.\n\n### Wiederholung nach Verbindungsabbruch\nOptional den Header **`Idempotency-Key`** setzen (z. B. eine GUID pro Benutzeraktion). Wird\nderselbe Key im selben Mandanten erneut gesendet, legt das Backend nichts Neues an,\nsondern arbeitet mit dem ersten Versuch weiter.\n\n### Aufbau des ZIP-Archivs\n* **`manifest.json` im ZIP-Wurzelverzeichnis (Pflicht)** mit den Feldern\n  `tia_portal_version`, `plc_type`, `exported_at` (ISO-8601), `file_count`, `project_name`.\n* Dateipfade im ZIP **mit `/`** als Trenner (nicht `\\`). Unter Windows erzeugte ZIPs\n  (z. B. PowerShell `Compress-Archive`) verwenden `\\` und müssen vorher korrigiert werden.\n* Nutzdaten als Export-Dateien in Unterordnern (empfohlen: benannt nach den\n  TIA-Gruppen), **Dateiname = Bausteinname**, z. B.\n  `tia_xml/Programmbausteine/Roboter 1.xml`. Die Ordnertiefe ist beliebig; es wird\n  rekursiv gelesen. Das Format (SCL, FBD/KOP, Variablentabellen) wird am Inhalt der\n  Datei erkannt, nicht am Ordner oder Dateinamen.\n* Zugelassene Dateiendungen: `.xml`, `.scl`, `.pdf`, `.txt`, `.json`. Andere Dateien\n  werden ignoriert (nicht abgelehnt) und in `skipped_files` der Statusabfrage gemeldet.\n* Grenzen: max. **500 MB** hochgeladen, max. **500 MB** entpackt, max. **5000** Dateien,\n  Kompressionsverhältnis je Datei max. 100:1. Symbolische Links und Pfade, die aus dem\n  Zielordner herausführen, werden abgelehnt (asynchron, Status `failed`).\n\n### Fehlerantworten\nAlle Fehler haben die Form\n`{\"detail\": {\"error\": {\"code\": \"...\", \"message\": \"...\", \"details\": {}}}}`.\nSynchron (direkt als Antwort auf den Upload-Request): 400, 401, 409, 413, 415, 500, 503.\nFehler, die erst bei der Verarbeitung auffallen (kaputtes ZIP, Zip-Bomb,\nkeine verwertbaren Dateien, Serverfehler bei der Verarbeitung), erscheinen als\n`status: failed` mit `error` in der Statusabfrage.\n\n`message` ist für Anwender formuliert (deutsch, ohne technische Interna) und kann\ndirekt angezeigt werden. Für Programmlogik immer `code` auswerten, nie den\nWortlaut von `message`. Ausnahme: `missing_project_name`/`invalid_project_name`\ndeuten auf einen Fehler im Client hin und sind an Entwickler gerichtet.","operationId":"upload_project_doc_api_v1_projects_upload_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"project_name","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255,"description":"Name des TIA-Projekts = Identität des Projekts im Mandanten (siehe oben).","examples":["P17BR"],"title":"Project Name"},"description":"Name des TIA-Projekts = Identität des Projekts im Mandanten (siehe oben)."},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional: eindeutiger Schlüssel pro Benutzeraktion, schützt vor Doppel-Uploads bei Wiederholung.","title":"Idempotency-Key"},"description":"Optional: eindeutiger Schlüssel pro Benutzeraktion, schützt vor Doppel-Uploads bei Wiederholung."}],"responses":{"202":{"description":"Angenommen. Verarbeitung läuft im Hintergrund weiter -- Status über `GET /uploads/{upload_id}`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadAcceptedResponse"},"example":{"status":"queued","upload_id":"0d3c8f5e-6a71-4a52-9a0e-8a3f6c1b2d40","project_id":"2978c7f0-8146-415b-b8ae-8fc150293b29"}}}},"400":{"description":"`project_name` fehlt oder ist ungültig (`missing_project_name`, `invalid_project_name`); oder der Empfang der Datei ist fehlgeschlagen (`upload_failed`).","content":{"application/json":{"example":{"detail":{"error":{"code":"missing_project_name","message":"Query-Parameter `project_name` fehlt oder ist leer","details":{}}}},"schema":{"$ref":"#/components/schemas/UploadErrorResponse"}}}},"401":{"description":"Token fehlt, ungültig oder abgelaufen (`unauthorized`).","content":{"application/json":{"example":{"detail":{"error":{"code":"unauthorized","message":"Anmeldung ungültig oder abgelaufen. Bitte erneut anmelden.","details":{}}}},"schema":{"$ref":"#/components/schemas/UploadErrorResponse"}}}},"409":{"description":"`upload_in_progress`: für dieses Projekt läuft bereits ein Upload. `duplicate_upload`: Archiv identisch zur aktuellen Version, nichts geändert.","content":{"application/json":{"examples":{"upload_in_progress":{"summary":"Für das Projekt läuft bereits ein Upload","value":{"detail":{"error":{"code":"upload_in_progress","message":"Für dieses Projekt läuft bereits ein Upload. Bitte warten, bis er abgeschlossen ist, und dann erneut versuchen.","details":{"project_id":"2978c7f0-8146-415b-b8ae-8fc150293b29","upload_id":"0d3c8f5e-6a71-4a52-9a0e-8a3f6c1b2d40"}}}}},"duplicate_upload":{"summary":"Archiv identisch zur aktuellen Version","value":{"detail":{"error":{"code":"duplicate_upload","message":"Keine Änderung: Das Archiv ist identisch mit der aktuellen Version dieses Projekts.","details":{"upload_id":"0d3c8f5e-6a71-4a52-9a0e-8a3f6c1b2d40"}}}}}},"schema":{"$ref":"#/components/schemas/UploadErrorResponse"}}}},"413":{"description":"Archiv größer als 500 MB (`too_large`).","content":{"application/json":{"example":{"detail":{"error":{"code":"too_large","message":"Die Datei ist zu groß (höchstens 500 MB).","details":{}}}},"schema":{"$ref":"#/components/schemas/UploadErrorResponse"}}}},"415":{"description":"Kein ZIP-Archiv (`not_a_zip`, Signatur `PK` fehlt).","content":{"application/json":{"example":{"detail":{"error":{"code":"not_a_zip","message":"Die hochgeladene Datei ist kein ZIP-Archiv. Bitte den Projektexport als ZIP-Datei hochladen.","details":{}}}},"schema":{"$ref":"#/components/schemas/UploadErrorResponse"}}}},"500":{"description":"Unerwarteter Serverfehler beim Annehmen des Uploads (`internal_error`) -- nicht vom Anwender behebbar, später erneut versuchen.","content":{"application/json":{"example":{"detail":{"error":{"code":"internal_error","message":"Der Upload konnte auf dem Server nicht angenommen werden. Bitte später erneut versuchen; tritt der Fehler wieder auf, bitte den Support informieren.","details":{}}}},"schema":{"$ref":"#/components/schemas/UploadErrorResponse"}}}},"503":{"description":"Analyse-Server derzeit nicht erreichbar (`slokal_unreachable`) -- später erneut versuchen.","content":{"application/json":{"example":{"detail":{"error":{"code":"slokal_unreachable","message":"Der Analyse-Server ist derzeit nicht erreichbar. Bitte später erneut versuchen.","details":{}}}},"schema":{"$ref":"#/components/schemas/UploadErrorResponse"}}}},"422":{"description":"Wird bei diesem Endpunkt nicht verwendet -- ein fehlender/ungültiger `project_name` liefert 400 (siehe oben)."}},"requestBody":{"required":true,"description":"Das ZIP-Archiv als rohe Bytes (kein multipart/form-data).","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}}}},"/api/v1/stripe/webhook":{"post":{"tags":["stripe"],"summary":"Stripe Webhook","operationId":"stripe_webhook_api_v1_stripe_webhook_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"AdminCreateTenantRequest":{"properties":{"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"owner_email":{"type":"string","format":"email","title":"Owner Email"},"owner_password":{"type":"string","title":"Owner Password"},"owner_full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Full Name"},"role":{"type":"string","title":"Role","default":"owner"}},"type":"object","required":["name","slug","owner_email","owner_password"],"title":"AdminCreateTenantRequest"},"AdminCreateTenantResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"first_user_id":{"type":"string","title":"First User Id"}},"type":"object","required":["tenant_id","first_user_id"],"title":"AdminCreateTenantResponse"},"Body_login_api_v1_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"password"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_api_v1_auth_login_post"},"ChatResponse":{"properties":{"id":{"type":"string","title":"Id"},"project_id":{"type":"string","title":"Project Id"},"user_id":{"type":"string","title":"User Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","project_id","user_id","title","created_at"],"title":"ChatResponse"},"CreateMessageRequest":{"properties":{"content":{"type":"string","title":"Content"}},"type":"object","required":["content"],"title":"CreateMessageRequest"},"CreateUserRequest":{"properties":{"tenant_id":{"type":"string","format":"uuid","title":"Tenant Id"},"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"role":{"type":"string","title":"Role","default":"member"}},"type":"object","required":["tenant_id","email","password"],"title":"CreateUserRequest"},"FeedbackRequest":{"properties":{"rating":{"type":"integer","title":"Rating"},"comment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Comment"}},"type":"object","required":["rating"],"title":"FeedbackRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"MessageResponse":{"properties":{"id":{"type":"string","title":"Id"},"chat_id":{"type":"string","title":"Chat Id"},"role":{"type":"string","title":"Role"},"content":{"type":"string","title":"Content"},"rating":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rating"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","chat_id","role","content","rating","created_at"],"title":"MessageResponse"},"ProjectResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"status":{"type":"string","title":"Status"},"created_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created By"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","name","description","status","created_by","created_at"],"title":"ProjectResponse"},"RefreshTokenRequest":{"properties":{"refresh_token":{"type":"string","title":"Refresh Token"}},"type":"object","required":["refresh_token"],"title":"RefreshTokenRequest"},"RegisterRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"tenant_slug":{"type":"string","title":"Tenant Slug"}},"type":"object","required":["email","password","tenant_slug"],"title":"RegisterRequest"},"SlokalStatusResponse":{"properties":{"host":{"type":"string","title":"Host"},"port":{"type":"integer","title":"Port"},"erreichbar":{"type":"boolean","title":"Erreichbar"},"public_ip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Public Ip"},"public_ip_last_seen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Public Ip Last Seen"}},"type":"object","required":["host","port","erreichbar","public_ip","public_ip_last_seen"],"title":"SlokalStatusResponse"},"SnapshotResponse":{"properties":{"id":{"type":"string","title":"Id"},"project_id":{"type":"string","title":"Project Id"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename"},"size_bytes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Size Bytes"},"checksum_sha256":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Checksum Sha256"},"status":{"type":"string","title":"Status"},"uploaded_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Uploaded By"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","project_id","filename","size_bytes","checksum_sha256","status","uploaded_by","created_at"],"title":"SnapshotResponse"},"TenantResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"plan":{"type":"string","title":"Plan"},"is_active":{"type":"boolean","title":"Is Active"},"anfragen_pro_monat":{"type":"integer","title":"Anfragen Pro Monat"},"anfragen_verbraucht_monat":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Anfragen Verbraucht Monat"},"periode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Periode"}},"type":"object","required":["id","name","slug","plan","is_active","anfragen_pro_monat"],"title":"TenantResponse"},"TenantUpdateRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","title":"TenantUpdateRequest"},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"type":"string","title":"Refresh Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"},"expires_in":{"type":"integer","title":"Expires In"},"user_id":{"type":"string","title":"User Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"role":{"type":"string","title":"Role"}},"type":"object","required":["access_token","refresh_token","expires_in","user_id","tenant_id","role"],"title":"TokenResponse"},"UpdateChatRequest":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"}},"type":"object","title":"UpdateChatRequest"},"UpdateUserRequest":{"properties":{"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"password":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Password"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"UpdateUserRequest"},"UploadAcceptedResponse":{"properties":{"status":{"type":"string","title":"Status"},"upload_id":{"type":"string","title":"Upload Id"},"project_id":{"type":"string","title":"Project Id"}},"type":"object","required":["status","upload_id","project_id"],"title":"UploadAcceptedResponse"},"UploadErrorResponse":{"properties":{"detail":{"type":"object","title":"Detail"}},"type":"object","required":["detail"],"title":"UploadErrorResponse","description":"Einheitliche Fehlerform aller Upload-Endpunkte."},"UploadStatusResponse":{"properties":{"upload_id":{"type":"string","title":"Upload Id"},"project_id":{"type":"string","title":"Project Id"},"status":{"type":"string","title":"Status"},"error":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Error"},"skipped_files":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Skipped Files"},"unknown_instructions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Unknown Instructions"},"stats":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Stats"}},"type":"object","required":["upload_id","project_id","status"],"title":"UploadStatusResponse","examples":[{"project_id":"2978c7f0-8146-415b-b8ae-8fc150293b29","skipped_files":["Hardware/Bild.png"],"stats":{"blocks":56,"rag":{"chunks_new":3,"chunks_unchanged":79,"chunks_updated":1},"tags":250},"status":"done","unknown_instructions":[],"upload_id":"0d3c8f5e-6a71-4a52-9a0e-8a3f6c1b2d40"},{"error":{"code":"zip_corrupt","details":{},"message":"Die hochgeladene Datei ist kein gültiges ZIP-Archiv oder beschädigt. Bitte den Export erneut erstellen und hochladen."},"project_id":"2978c7f0-8146-415b-b8ae-8fc150293b29","status":"failed","upload_id":"0d3c8f5e-6a71-4a52-9a0e-8a3f6c1b2d41"}]},"UserResponse":{"properties":{"id":{"type":"string","title":"Id"},"email":{"type":"string","title":"Email"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"role":{"type":"string","title":"Role"},"tenant_id":{"type":"string","title":"Tenant Id"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","email","full_name","role","tenant_id","is_active","created_at"],"title":"UserResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VariablesResponse":{"properties":{"message_id":{"type":"string","title":"Message Id"},"tags":{"items":{"type":"object"},"type":"array","title":"Tags"},"internal":{"items":{"type":"object"},"type":"array","title":"Internal"}},"type":"object","required":["message_id","tags","internal"],"title":"VariablesResponse"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"/api/v1/auth/login"}}}}}}