wiki/implementation/trustee_feature_rbac_architecture.md
2026-01-24 09:43:35 +01:00

696 lines
24 KiB
Markdown

# Trustee Feature - RBAC Architektur & Gateway-Readiness
## Übersicht
Dieses Dokument beschreibt die **vereinfachte RBAC-Architektur** für das Trustee Feature.
**Erstellungsdatum**: 2026-01-23
**Letzte Änderung**: 2026-01-24
**Status**: Aktuell
---
## WICHTIG: Architektur-Änderung (2026-01-24)
### Feature-Instanz = Organisation
Die ursprüngliche Architektur sah vor, dass innerhalb einer Feature-Instanz mehrere "Organisationen" (Kunden) verwaltet werden. **Dies wurde geändert:**
- **Neu**: Eine Feature-Instanz repräsentiert **eine** Organisation (z.B. "SOHA Treuhand")
- **Entfernt**: `TrusteeOrganisation`, `TrusteeContract`, `TrusteeRole`, `TrusteeAccess`
- **Beibehalten**: `TrusteePosition`, `TrusteeDocument`, `TrusteePositionDocument`
### Vereinfachtes Datenmodell
```
Feature-Instanz (= Organisation, z.B. "SOHA Treuhand")
└── TrusteePosition (Buchungspositionen, Konten)
└── TrusteeDocument (Belege, Berichte)
└── TrusteePositionDocument (Zuordnung Position ↔ Dokument)
```
---
## 1. Gateway-Status
### 1.1 Ist das Gateway bereit für UI-Arbeit?
**Ja, das Gateway ist vollständig bereit.**
| Komponente | Datei | Status |
|------------|-------|--------|
| **Routes** | `routeFeatureTrustee.py` | ✅ Komplett |
| **Interface** | `interfaceFeatureTrustee.py` | ✅ Komplett |
| **Datamodel** | `datamodelFeatureTrustee.py` | ✅ Komplett |
| **RBAC** | Via System-Level AccessRules | ✅ Implementiert |
### 1.2 Verfügbare API-Endpoints
Alle Endpoints verwenden das URL-Pattern: `/api/trustee/{instanceId}/...`
| Endpoint-Gruppe | Basis-Route | CRUD | Options |
|-----------------|-------------|------|---------|
| Documents | `/api/trustee/{instanceId}/documents` | ✅ | ✅ |
| Positions | `/api/trustee/{instanceId}/positions` | ✅ | ✅ |
| Position-Documents | `/api/trustee/{instanceId}/position-documents` | ✅ | - |
| **Instance-Roles** | `/api/trustee/{instanceId}/instance-roles` | ✅ | - |
| **User Options** | `/api/users/options` | - | ✅ |
### 1.3 Frontend Views
| View | Pfad | Beschreibung |
|------|------|--------------|
| Dashboard | `/dashboard` | Übersicht |
| Positionen | `/positions` | Buchungspositionen verwalten |
| Dokumente | `/documents` | Belege/Berichte verwalten |
| Zuordnungen | `/position-documents` | Position ↔ Dokument Zuordnungen |
| Rollen & Rechte | `/instance-roles` | Instanz-Rollen verwalten (nur Admin) |
---
## 2. RBAC-Architektur (Vereinfacht)
### 2.1 Nur System-Level RBAC
Da die Feature-Instanz selbst die Organisation repräsentiert, ist **kein Feature-internes RBAC** mehr nötig.
```
┌─────────────────────────────────────────────────────────────────────────┐
│ SYSTEM-RBAC (Basis-Level) │
│ (Mandate + Feature Instance Zugang) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ User ──► UserMandate ──► Mandate │
│ │ │
│ └──► FeatureAccess ──► FeatureInstance (= Organisation) │
│ │ │
│ └── Role (trustee-admin, trustee-accountant, trustee-client)│
│ │ │
│ └── AccessRules (UI, DATA, RESOURCE) │
│ │
│ Frage: Hat der User überhaupt Zugang zu diesem Feature? │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ STUFE 2: FEATURE-SPEZIFISCHES RBAC │
│ (Isolation innerhalb des Features) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ TrusteeAccess │
│ ├── userId → Welcher User │
│ ├── organisationId → Welcher Kunde (Isolationsobjekt) │
│ ├── roleId → Welche Feature-Rolle (admin, operate, userreport)│
│ └── contractId → Optional: Einschränkung auf bestimmten Vertrag │
│ │
│ Frage: Welche Kunden darf der User sehen? Mit welchen Rechten? │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.3 Vergleich: System-RBAC vs. Feature-RBAC
| Aspekt | System-RBAC | Feature-RBAC (Trustee) |
|--------|-------------|------------------------|
| **Tabelle** | `AccessRule` | `TrusteeAccess` |
| **Scope** | Global / Mandate | Feature-Instanz |
| **Objekte** | Tables, UI-Items, API-Resources | Organisationen, Contracts |
| **Verwaltung** | SysAdmin | Feature-Admin |
| **Zweck** | "Was darf der User technisch?" | "Welche Daten darf der User sehen?" |
### 2.4 Feature-spezifische Rollen
Das Trustee-Feature definiert drei Rollen:
| Rolle | Beschreibung | Berechtigungen |
|-------|--------------|----------------|
| **admin** | Feature-Administrator | Voller CRUD auf Organisation + alle Contracts |
| **operate** | Sachbearbeiter | CRUD für Contracts, Documents, Positions |
| **userreport** | Endbenutzer | Nur eigene erstellte Records (Documents, Positions) |
### 2.5 Contract-Level Isolation (optional)
`TrusteeAccess.contractId` ermöglicht feinere Isolation:
- **Leer/NULL**: Zugriff auf alle Contracts der Organisation
- **Gesetzt**: Zugriff nur auf diesen spezifischen Contract
```
Beispiel:
- User A hat Zugriff auf Organisation "Kunde-X" mit contractId=NULL
→ Sieht alle Contracts von Kunde-X
- User B hat Zugriff auf Organisation "Kunde-X" mit contractId="contract-123"
→ Sieht nur Contract "contract-123" von Kunde-X
```
---
## 3. Implementierung im Gateway
### 3.1 Datenmodell: TrusteeAccess
```python
class TrusteeAccess(BaseModel):
id: str # UUID
organisationId: str # FK zu TrusteeOrganisation
roleId: str # FK zu TrusteeRole (admin, operate, userreport)
userId: str # FK zu User
contractId: Optional[str] # Optional: FK zu TrusteeContract
mandateId: Optional[str] # System-Kontext
featureInstanceId: Optional[str] # Feature-Instanz
```
### 3.2 Kombinierte Berechtigungsprüfung
Die Methode `checkCombinedPermission()` in `interfaceFeatureTrustee.py` kombiniert beide Stufen:
```python
def checkCombinedPermission(self, modelClass, operation, organisationId, contractId, recordCreatedBy):
"""
Kombiniert System-RBAC + Feature-Level RBAC.
1. Prüft System-RBAC (AccessLevel: ALL, GROUP, MY, NONE)
2. Bei ALL: Bypass Feature-Level (SysAdmin)
3. Sonst: Prüft TrusteeAccess für User + Organisation + Contract
"""
# Stufe 1: System-RBAC
accessLevel = self.getRbacAccessLevel(modelClass, operation)
if accessLevel == AccessLevel.NONE:
return False
if accessLevel == AccessLevel.ALL:
return True # SysAdmin bypass
# Stufe 2: Feature-RBAC via TrusteeAccess
roles = self.getUserTrusteeRoles(self.userId, organisationId, contractId)
if "admin" in roles:
return True
if "operate" in roles and modelClass in (TrusteeContract, TrusteeDocument, ...):
return True
if "userreport" in roles and recordCreatedBy == self.userId:
return True
return False
```
### 3.3 Automatische Filterung
Die Methode `filterRecordsByTrusteeAccess()` filtert Ergebnisse basierend auf User-Zugriff:
```python
def filterRecordsByTrusteeAccess(self, records, modelClass):
"""
Filtert Records basierend auf TrusteeAccess des Users.
- admin: Sieht alle Records der Organisation
- operate: Sieht alle Records der Organisation
- userreport: Sieht nur eigene Records (_createdBy = userId)
"""
```
---
## 4. Generisches Muster für andere Features
### 4.1 Feature-Isolation-Pattern
Jedes Feature kann sein eigenes Isolationsmodell definieren:
```
{Feature}Access: User → {Isolationsobjekt} → Role [→ Unter-Objekt]
```
### 4.2 Beispiele für andere Features
| Feature | Access-Tabelle | Isolationsobjekt | Unter-Objekt |
|---------|----------------|------------------|--------------|
| **Trustee** | `TrusteeAccess` | Organisation (Kunde) | Contract |
| **RealEstate** | `RealEstateAccess` | Property (Liegenschaft) | Unit (Wohnung) |
| **ProjectMgmt** | `ProjectAccess` | Project | - |
| **Accounting** | `AccountingAccess` | Company | FiscalYear |
### 4.3 Implementierungsvorlage
Für ein neues Feature mit Isolation:
1. **Datamodel**: `{Feature}Access` mit `userId`, `{isolationObject}Id`, `roleId`
2. **Interface**: `checkCombinedPermission()` und `filterRecordsByAccess()`
3. **Routes**: CRUD für `{Feature}Access`
4. **Roles**: Feature-spezifische Rollen definieren
---
## 5. API-Referenz für UI-Entwicklung
### 5.1 Access-Verwaltung (User-Zuweisung zu Kunden)
```
# Alle Access-Records abrufen
GET /api/trustee/{instanceId}/access
# Access für bestimmte Organisation
GET /api/trustee/{instanceId}/access/organisation/{orgId}
# Access für bestimmten User
GET /api/trustee/{instanceId}/access/user/{userId}
# Neuen Access erstellen (User zu Kunde zuweisen)
POST /api/trustee/{instanceId}/access
Body: {
"userId": "user-uuid",
"organisationId": "kunde-id",
"roleId": "operate",
"contractId": null // Optional
}
# Access aktualisieren
PUT /api/trustee/{instanceId}/access/{accessId}
# Access löschen
DELETE /api/trustee/{instanceId}/access/{accessId}
```
### 5.2 Options-Endpoints für Dropdowns
```
# Organisationen für Dropdown
GET /api/trustee/{instanceId}/organisations/options
→ [{ "value": "org-id", "label": "Kunde AG" }, ...]
# Rollen für Dropdown
GET /api/trustee/{instanceId}/roles/options
→ [{ "value": "admin", "label": "Administrator" }, ...]
# Contracts für Dropdown (dynamisch nach Organisation)
GET /api/trustee/{instanceId}/contracts/options
→ [{ "value": "contract-uuid", "label": "Vertrag 2026" }, ...]
# Users für Dropdown (aus aktuellem Mandate)
GET /api/users/options
→ [{ "value": "user-uuid", "label": "Max Muster" }, ...]
```
---
## 6. UI-Anforderungen
### 6.1 Access-View (User-Verwaltung)
Eine View zur Verwaltung der User-Zuweisungen:
| Spalte | Typ | Quelle |
|--------|-----|--------|
| User | Select | `/api/users/options` |
| Organisation | Select | `/api/trustee/{instanceId}/organisations/options` |
| Rolle | Select | `/api/trustee/{instanceId}/roles/options` |
| Contract | Select (optional) | `/api/trustee/{instanceId}/contracts/options` |
**Hinweis**: Das `contractId` Dropdown sollte dynamisch nach Auswahl der Organisation gefiltert werden.
### 6.2 Berechtigungen für Access-View
- **Sichtbar für**: Feature-Admins (`admin` Rolle in mindestens einer Organisation)
- **Create/Update/Delete**: Nur für Organisationen, in denen User Admin ist
---
## 7. Offene Punkte / Entscheidungen
### 7.1 Geklärt
- [x] Ist das Gateway bereit? → **Ja**
- [x] Sind Feature-spezifische Rollen gleich wie System-Rollen? → **Nein, bewusst getrennt**
- [x] Ist zweistufiges RBAC nötig? → **Ja, bereits implementiert**
- [x] Existiert der Users-Options-Endpoint? → **Ja, `/api/users/options`**
### 7.2 Offen für UI-Review
- [ ] Soll die Access-View eine eigene Seite sein oder Tab in Organisation?
- [ ] Wie wird Contract-Dropdown bei organisationId-Änderung aktualisiert?
- [ ] Braucht es eine "Bulk-Zuweisung" (User zu mehreren Orgs gleichzeitig)?
---
## 8. Zusammenfassung
### Das Gateway bietet:
1. **Vollständige CRUD-Routes** für alle Trustee-Entities
2. **Zwei-Stufen-RBAC** (System + Feature-Level)
3. **Automatische Filterung** basierend auf User-Access
4. **Options-Endpoints** für alle Dropdowns
### Das UI muss implementieren:
1. **Access-View** zur Verwaltung von User-Zuweisungen
2. **Dynamische Dropdowns** mit Abhängigkeiten (Contract nach Organisation)
3. **instanceId** in allen API-Calls verwenden
### Architektur-Prinzip:
```
System-RBAC → Zugang zum Feature
Feature-RBAC → Isolation innerhalb des Features
```
---
## 9. Implementierungsplan: Fehlende Elemente
### 9.1 Aktueller Stand (Analyse)
| Komponente | Status | Details |
|------------|--------|---------|
| **Gateway (Backend)** | ✅ Komplett | Routes, Interface, Datamodel, RBAC |
| **API Hooks** (`useTrustee.ts`) | ✅ Komplett | CRUD-Hooks für alle Entities |
| **API Client** (`trusteeApi.ts`) | ✅ Komplett | Alle Endpoints implementiert |
| **Views (Grundgerüst)** | ⚠️ Teilweise | Tabellen existieren, aber ohne Forms |
| **Create/Edit Formulare** | ❌ Fehlt | Buttons vorhanden, aber ohne Funktion |
| **Label-Auflösung** | ❌ Fehlt | Zeigt IDs statt Labels (userId, orgId) |
| **Dynamische Dropdowns** | ❌ Fehlt | Contract abhängig von Organisation |
### 9.2 Fehlende Implementierungen
#### P1: Create/Edit Formulare für alle Views
**Problem:** Die "Neu erstellen" und "Bearbeiten" Buttons existieren, aber öffnen kein Formular.
**Betroffene Views:**
- `TrusteeOrganisationsView.tsx`
- `TrusteeRolesView.tsx`
- `TrusteeAccessView.tsx`
- `TrusteeContractsView.tsx`
- `TrusteeDocumentsView.tsx`
- `TrusteePositionsView.tsx`
**Lösung:** Pro View ein Modal mit EditForm implementieren, das die automatisch generierten Felder aus `generateEditFieldsFromAttributes()` verwendet.
**Beispiel-Pattern:**
```tsx
// In TrusteeOrganisationsView.tsx
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState<TrusteeOrganisation | null>(null);
const { generateCreateFieldsFromAttributes, generateEditFieldsFromAttributes } = useTrusteeOrganisations();
const { handleCreate, handleUpdate } = useTrusteeOrganisationOperations();
// Create-Button
<button onClick={() => { setEditingItem(null); setIsModalOpen(true); }}>
+ Neue Organisation
</button>
// Edit-Button
<button onClick={() => { setEditingItem(org); setIsModalOpen(true); }}>
✏️
</button>
// Modal mit EditForm
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<EditForm
fields={editingItem ? generateEditFieldsFromAttributes() : generateCreateFieldsFromAttributes()}
initialData={editingItem || {}}
onSave={async (data) => {
if (editingItem) {
await handleUpdate(editingItem.id, data);
} else {
await handleCreate(data);
}
setIsModalOpen(false);
refetch();
}}
/>
</Modal>
```
---
#### P2: Label-Auflösung für Foreign Keys
**Problem:** `TrusteeAccessView` zeigt:
- `userId: "uuid-123"` statt `"Max Muster"`
- `organisationId: "org-1"` statt `"Kunde AG"`
- `roleId: "admin"` statt `"Administrator"`
**Lösung:** Options aus den entsprechenden Endpoints laden und als Lookup-Map verwenden.
**Implementierung:**
```tsx
// In TrusteeAccessView.tsx
const [userOptions, setUserOptions] = useState<{value: string, label: string}[]>([]);
const [orgOptions, setOrgOptions] = useState<{value: string, label: string}[]>([]);
const [roleOptions, setRoleOptions] = useState<{value: string, label: string}[]>([]);
useEffect(() => {
const loadOptions = async () => {
const [users, orgs, roles] = await Promise.all([
api.get('/api/users/options'),
api.get(`/api/trustee/${instanceId}/organisations/options`),
api.get(`/api/trustee/${instanceId}/roles/options`)
]);
setUserOptions(users.data);
setOrgOptions(orgs.data);
setRoleOptions(roles.data);
};
loadOptions();
}, [instanceId]);
// Lookup-Funktion
const getLabel = (options: {value: string, label: string}[], value: string) =>
options.find(o => o.value === value)?.label || value;
// In der Tabelle
<td>{getLabel(userOptions, access.userId)}</td>
<td>{getLabel(orgOptions, access.organisationId)}</td>
<td>{getLabel(roleOptions, access.roleId)}</td>
```
---
#### P3: Dynamische Dropdown-Filterung
**Problem:** Im AccessView/ContractView soll das Contract-Dropdown nur Contracts der ausgewählten Organisation zeigen.
**Lösung:** `dependsOn`-Attribut im Datamodel nutzen + dynamisches Nachladen.
**Implementierung im EditForm:**
```tsx
// Wenn organisationId geändert wird, Contracts neu laden
const handleFieldChange = async (fieldKey: string, value: any) => {
setFormData(prev => ({ ...prev, [fieldKey]: value }));
// Dynamische Abhängigkeiten prüfen
const dependentFields = fields.filter(f => f.dependsOn === fieldKey);
for (const depField of dependentFields) {
if (depField.optionsReference?.includes('contracts')) {
// Contracts für diese Organisation laden
const contracts = await api.get(
`/api/trustee/${instanceId}/contracts/options?organisationId=${value}`
);
// Options aktualisieren...
}
}
};
```
**Backend-Erweiterung nötig:** Der `/contracts/options` Endpoint muss einen `organisationId` Query-Parameter unterstützen.
---
#### P4: Generischer Options-Hook
**Problem:** Options-Laden ist in jeder View dupliziert.
**Lösung:** Zentraler Hook für Trustee-Options.
```tsx
// hooks/useTrusteeOptions.ts
export function useTrusteeOptions() {
const instanceId = useInstanceId();
const [options, setOptions] = useState<TrusteeOptionsMap>({});
const loadOptions = useCallback(async (entity: 'organisations' | 'roles' | 'contracts' | 'users') => {
if (entity === 'users') {
const res = await api.get('/api/users/options');
setOptions(prev => ({ ...prev, users: res.data }));
} else {
const res = await api.get(`/api/trustee/${instanceId}/${entity}/options`);
setOptions(prev => ({ ...prev, [entity]: res.data }));
}
}, [instanceId]);
const getLabel = useCallback((entity: string, value: string) => {
return options[entity]?.find(o => o.value === value)?.label || value;
}, [options]);
return { options, loadOptions, getLabel };
}
```
---
### 9.3 Implementierungsreihenfolge
| Phase | Aufgabe | Aufwand | Priorität |
|-------|---------|---------|-----------|
| **1** | `useTrusteeOptions` Hook erstellen | 2h | Hoch |
| **2** | Label-Auflösung in allen Views | 3h | Hoch |
| **3** | Create/Edit Modal in `TrusteeOrganisationsView` | 3h | Hoch |
| **4** | Create/Edit Modal für alle anderen Views kopieren | 4h | Mittel |
| **5** | Dynamische Contract-Filterung | 2h | Mittel |
| **6** | Backend: `/contracts/options?organisationId=` | 1h | Mittel |
| **7** | Position-Document Verknüpfungs-UI | 4h | Niedrig |
| **8** | Testing & Bugfixes | 4h | Hoch |
**Geschätzter Gesamtaufwand:** ~23h (3-4 Arbeitstage)
---
### 9.4 Detailplan Phase 1-3 (MVP)
#### Schritt 1: `useTrusteeOptions` Hook
**Datei:** `frontend_nyla/src/hooks/useTrusteeOptions.ts`
```typescript
/**
* Hook für Trustee-Options (Dropdowns, Label-Auflösung)
*/
import { useState, useCallback, useEffect } from 'react';
import api from '../api';
import { useInstanceId } from './useCurrentInstance';
interface Option {
value: string;
label: string;
}
interface TrusteeOptions {
users: Option[];
organisations: Option[];
roles: Option[];
contracts: Option[];
documents: Option[];
positions: Option[];
}
export function useTrusteeOptions(autoLoad: (keyof TrusteeOptions)[] = []) {
const instanceId = useInstanceId();
const [options, setOptions] = useState<Partial<TrusteeOptions>>({});
const [loading, setLoading] = useState(false);
const loadOptions = useCallback(async (
entities: (keyof TrusteeOptions)[],
filters?: { organisationId?: string }
) => {
if (!instanceId) return;
setLoading(true);
try {
const promises = entities.map(async (entity) => {
let url: string;
if (entity === 'users') {
url = '/api/users/options';
} else {
url = `/api/trustee/${instanceId}/${entity}/options`;
if (filters?.organisationId && entity === 'contracts') {
url += `?organisationId=${filters.organisationId}`;
}
}
const res = await api.get(url);
return { entity, data: res.data };
});
const results = await Promise.all(promises);
const newOptions: Partial<TrusteeOptions> = {};
results.forEach(({ entity, data }) => {
newOptions[entity] = data;
});
setOptions(prev => ({ ...prev, ...newOptions }));
} finally {
setLoading(false);
}
}, [instanceId]);
const getLabel = useCallback((entity: keyof TrusteeOptions, value: string): string => {
return options[entity]?.find(o => o.value === value)?.label || value;
}, [options]);
// Auto-Load bei Mount
useEffect(() => {
if (autoLoad.length > 0 && instanceId) {
loadOptions(autoLoad);
}
}, [instanceId, autoLoad.join(',')]);
return { options, loadOptions, getLabel, loading };
}
```
#### Schritt 2: TrusteeAccessView mit Labels
**Datei:** `frontend_nyla/src/pages/views/trustee/TrusteeAccessView.tsx`
- Import `useTrusteeOptions`
- Auto-Load: `['users', 'organisations', 'roles']`
- Tabelle: `getLabel()` für userId, organisationId, roleId
#### Schritt 3: Create/Edit Modal
**Ansatz:** Bestehende `EditPopup`-Komponente aus anderen Views (z.B. Prompts) wiederverwenden.
**Dateien:**
- `TrusteeOrganisationsView.tsx` - Modal hinzufügen
- Nutzt `generateCreateFieldsFromAttributes()` für Feld-Definitionen
- `handleCreate()` / `handleUpdate()` für API-Calls
---
### 9.5 Backend-Erweiterung (optional)
Für dynamische Contract-Filterung nach Organisation:
**Datei:** `gateway/modules/features/trustee/routeFeatureTrustee.py`
```python
@router.get("/{instanceId}/contracts/options", response_model=List[Dict[str, Any]])
async def getContractOptions(
request: Request,
instanceId: str = Path(...),
organisationId: Optional[str] = Query(None), # NEU
context: RequestContext = Depends(getRequestContext)
) -> List[Dict[str, Any]]:
"""Get contract options, optionally filtered by organisation."""
mandateId = await _validateInstanceAccess(instanceId, context)
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
if organisationId:
contracts = interface.getContractsByOrganisation(organisationId)
else:
result = interface.getAllContracts(None)
contracts = result.items if hasattr(result, 'items') else result
return [{"value": c.id, "label": c.label or c.name or c.id} for c in contracts]
```
---
### 9.6 Offene Entscheidungen für Review
| # | Frage | Optionen |
|---|-------|----------|
| 1 | **Modal vs. Inline-Edit** | A) Modal-Dialog / B) Inline in Tabelle / C) Separate Seite |
| 2 | **Bulk-Operationen** | A) Ja, Multi-Select + Batch-Aktionen / B) Nein, nur einzeln |
| 3 | **Position-Document UI** | A) Separate View / B) Inline in Position-View / C) Beides |
| 4 | **Validierung** | A) Client-only / B) Server-only / C) Beides |
---
**Dokumentversion**: 1.1
**Letzte Aktualisierung**: 2026-01-23
**Autor**: Claude (AI-Assistent)
**Status**: Zur Überprüfung