1042 lines
34 KiB
Markdown
1042 lines
34 KiB
Markdown
# LeoCRM Architektur-Umbauplan — Komplett (V2)
|
|
|
|
## Grundprinzip
|
|
|
|
Architektur JETZT richtig stellen, bevor ERP-Module darauf aufbauen. Jede architektonische Änderung wird exponential teurer, sobald ERP-Module kommen.
|
|
|
|
## Rolle der Workspaces
|
|
|
|
Workspaces sind ausschließlich ein UI-, Navigations- und Arbeitskontext.
|
|
|
|
Ein Workspace steuert:
|
|
|
|
* welche Module und Menüpunkte angezeigt werden,
|
|
* welche Unterbereiche eines Moduls angezeigt werden,
|
|
* welche Kalender im Kalender-Modul sichtbar sind,
|
|
* welche Kontaktordner, gespeicherten Ansichten oder Filter angeboten werden,
|
|
* welche Dashboard-Widgets erscheinen,
|
|
* deren Reihenfolge und Konfiguration,
|
|
* den bevorzugten Arbeitskontext eines Benutzers.
|
|
|
|
Ein Workspace verändert niemals:
|
|
|
|
* RBAC-Berechtigungen,
|
|
* ABAC-Regeln,
|
|
* Entity Permissions,
|
|
* Owner- oder Sharing-Rechte,
|
|
* Tenant Memberships,
|
|
* RLS-Policies,
|
|
* tatsächliche Datenzugriffsrechte.
|
|
|
|
Es gilt immer:
|
|
|
|
```text
|
|
Tatsächlich sichtbare Daten
|
|
=
|
|
Workspace-Konfiguration
|
|
∩
|
|
Berechtigungen des Benutzers
|
|
∩
|
|
Objektzugriff
|
|
∩
|
|
Tenant-Isolation
|
|
```
|
|
|
|
Ein Workspace darf niemals Rechte erteilen oder bestehende Rechte erweitern.
|
|
|
|
Beispiel Kalender:
|
|
|
|
```text
|
|
Im Workspace konfigurierte Kalender
|
|
∩
|
|
Kalender, die der Benutzer lesen darf
|
|
=
|
|
im Kalender-Modul angezeigte Kalender
|
|
```
|
|
|
|
Beispiel Kontakte:
|
|
|
|
```text
|
|
Im Workspace konfigurierte Kontaktordner/Ansichten
|
|
∩
|
|
Kontakte, die der Benutzer lesen darf
|
|
=
|
|
im Kontakte-Modul dargestellte Inhalte
|
|
```
|
|
|
|
Dasselbe Kontakte-Modul darf gleichzeitig in mehreren Workspaces vorkommen, beispielsweise:
|
|
|
|
* Workspace „Verkauf"
|
|
* Workspace „Einkauf"
|
|
|
|
Beide Workspaces verwenden dasselbe Kontakte-Modul, aber mit unterschiedlichen:
|
|
|
|
* Kontaktordnern,
|
|
* gespeicherten Ansichten,
|
|
* Standardfiltern,
|
|
* Dashboard-Widgets,
|
|
* Menükonfigurationen.
|
|
|
|
Die bestehenden Rechte des Benutzers bleiben dabei unverändert.
|
|
|
|
### Workspace darf kein Backend-Berechtigungsgate werden
|
|
|
|
Ein API-Endpunkt darf nicht allein deshalb `403 Forbidden` liefern, weil ein Modul im aktuellen Workspace nicht angezeigt wird.
|
|
|
|
Der Workspace-Kontext dient nur für:
|
|
|
|
* UI-Konfiguration,
|
|
* Navigation,
|
|
* Default-Filter,
|
|
* Modulansichten,
|
|
* Kalenderauswahl,
|
|
* Ordnerauswahl,
|
|
* Widgetkonfiguration.
|
|
|
|
Die tatsächliche Autorisierung erfolgt weiterhin über:
|
|
|
|
```python
|
|
require_permission(...)
|
|
check_single_entity_access(...)
|
|
apply_visibility_filter(...)
|
|
RLS
|
|
```
|
|
|
|
Für workspacefähige Listenendpunkte kann der Workspace-Kontext als zusätzlicher Filter verwendet werden. Er ersetzt aber niemals einen Permission-Check.
|
|
|
|
Direkte Links auf ein berechtigtes Fachobjekt dürfen weiterhin funktionieren, auch wenn das zugehörige Modul im aktuellen Workspace ausgeblendet ist.
|
|
|
|
### Workspace-Kontext nicht global in Redis speichern
|
|
|
|
Das erzeugt Probleme bei mehreren geöffneten Browser-Tabs.
|
|
|
|
* Tab A arbeitet im Workspace „Verkauf".
|
|
* Tab B wechselt in „Einkauf".
|
|
* Durch eine globale Redis-Session würde Tab A ebenfalls ungewollt in „Einkauf" wechseln.
|
|
|
|
Lösung:
|
|
|
|
* Aktueller Workspace wird pro Browser-Tab im Frontend gespeichert (`sessionStorage` oder tablokaler Zustand).
|
|
* Der Client sendet bei workspacefähigen Requests:
|
|
|
|
```http
|
|
X-Workspace-ID: <uuid>
|
|
```
|
|
|
|
* Der Server validiert: Workspace gehört zum Tenant, Benutzer ist zugewiesen oder Admin, Workspace ist aktiv.
|
|
* In der Datenbank wird nur der bevorzugte Default-Workspace eines Benutzers gespeichert (`workspace_users.is_default`).
|
|
* Ein Workspacewechsel verändert keine Sessionberechtigungen.
|
|
|
|
### Rollenmodell für Workspace-Verwaltung
|
|
|
|
Workspaces haben keine eigenen Datenrechte. Trotzdem braucht ihre Konfiguration einen administrativen Verantwortungsbereich.
|
|
|
|
#### System-Administrator
|
|
|
|
Darf: alle Tenants verwalten, globale Plugins aktivieren/deaktivieren, alle Workspaces aller Tenants verwalten, globale Plattformkonfiguration ändern.
|
|
|
|
#### Tenant-Administrator
|
|
|
|
Darf innerhalb seines Tenants: Workspaces erstellen/ändern/löschen, Workspace-Manager bestimmen, Benutzer Workspaces zuweisen, Module und Widgets konfigurieren, tenantweit verfügbare Ressourcen auswählen.
|
|
|
|
#### Workspace-Manager
|
|
|
|
Keine globale RBAC-Rolle, sondern eine Zuweisung innerhalb eines konkreten Workspaces (`workspace_users.role = 'manager'`).
|
|
|
|
Darf nur für seinen Workspace: Name/Beschreibung/Icon ändern, Module ein-/ausblenden, Reihenfolge ändern, Kalenderauswahl konfigurieren, Kontaktordner und Ansichten konfigurieren, Dashboard-Widgets konfigurieren, Benutzer zuweisen/entfernen (sofern Tenant-Mitglied).
|
|
|
|
Darf nicht: Benutzerrechte ändern, Rollen/Gruppen ändern, RBAC/ABAC/Entity Permissions vergeben, Plugins aktivieren, Tenant-Einstellungen ändern, auf Daten zugreifen für die er keine normalen Rechte besitzt.
|
|
|
|
#### Workspace-Mitglied
|
|
|
|
Kann den Workspace benutzen, aber nicht konfigurieren.
|
|
|
|
Permissions für Workspace-Verwaltung:
|
|
|
|
```text
|
|
workspaces:read
|
|
workspaces:create
|
|
workspaces:update
|
|
workspaces:delete
|
|
workspaces:assign_users
|
|
workspaces:configure_modules
|
|
workspaces:configure_widgets
|
|
```
|
|
|
|
Für Workspace-Manager werden diese Rechte nicht tenantweit vergeben. Der Service prüft zusätzlich, ob der Benutzer im konkreten Workspace als `manager` eingetragen ist.
|
|
|
|
## Aktueller Stand
|
|
|
|
- P0 Fixes (6): Alle im Code, ungetestet
|
|
- P1 Fixes (9): Alle im Code, ungetestet
|
|
- Migrationen 0060-0067: In Produktion
|
|
- CI/CD Pipeline: 10 Quality Gates
|
|
- Frontend: canAccess Fallback (Workaround)
|
|
- RLS: Auf contacts + 30 Tabellen, aber überlappend mit Application Layer
|
|
|
|
## Was NICHT umgesetzt wird
|
|
|
|
- `security_resources` Tabelle — Aktuelles System (entity_permissions + owner_id + visibility.py) funktioniert. Lieber konsolidieren als neu bauen.
|
|
- Alle Services in Commands umbauen — Inkrementell, nicht Big-Bang. Neue Module nutzen Commands, alte bei Überarbeitung.
|
|
- `stored_objects` Tabelle — Stattdessen: Alles im DMS, Referenzen von Objekten.
|
|
|
|
---
|
|
|
|
## Phase 0a: Beweise liefern (4h)
|
|
|
|
**Ziel:** Beweisen dass die P0+P1 Fixes funktionieren.
|
|
|
|
### Cross-Tenant Integrationstests (2h)
|
|
- Test: User A in Tenant 1 kann keine Daten von Tenant 2 sehen
|
|
- Test: RLS blockt Cross-Tenant Zugriff auf contacts, addresses, attachments, etc.
|
|
- Test: entity_permissions funktionieren nur innerhalb des gleichen Tenants
|
|
- Test: ABAC Policies sind tenant-scoped
|
|
|
|
### RLS mit unprivilegierter Rolle testen (1h)
|
|
- Test: App läuft mit crm_runtime Rolle (nicht Superuser)
|
|
- Test: RLS blockt korrekt mit crm_runtime
|
|
- Test: set_tenant_context funktioniert mit unprivilegierter Rolle
|
|
- Test: Login funktioniert (Bootstrap-Zirkel gelöst)
|
|
|
|
### Test-Suite grün (1h)
|
|
- pytest --collect-only: 1103 Tests sammelbar
|
|
- pytest tests/test_entity_permissions.py: Alle grün
|
|
- pytest tests/test_abac.py: Alle grün
|
|
- pytest tests/test_permission_performance.py: Alle grün
|
|
- Bestehende Tests: Soweit möglich grün
|
|
|
|
**Abhängigkeit:** Keine — Sofort startbar
|
|
|
|
---
|
|
|
|
## Phase 0b: Backup und Restore (2h)
|
|
|
|
**Ziel:** Beweisen dass Backup und Restore funktionieren.
|
|
|
|
### Backup Test (1h)
|
|
- pg_dump der Produktions-DB
|
|
- DMS/Object-Storage-Backup
|
|
- Restore in Test-DB
|
|
- Datensatzanzahlen vergleichen
|
|
- RLS Policies nach Restore prüfen
|
|
- entity_permissions nach Restore prüfen
|
|
|
|
### Restore Test (1h)
|
|
|
|
Restore-Reihenfolge:
|
|
|
|
1. PostgreSQL-Backup wiederherstellen.
|
|
2. DMS/Object Storage wiederherstellen.
|
|
3. benötigte Secrets und Verschlüsselungsschlüssel bereitstellen.
|
|
4. `alembic current` prüfen.
|
|
5. `alembic upgrade head` ausführen.
|
|
6. App und Worker starten.
|
|
7. Login testen.
|
|
8. Datensatzanzahlen prüfen.
|
|
9. verwaiste Fremdschlüssel prüfen.
|
|
10. Tenant-Verteilung prüfen.
|
|
11. RLS- und Cross-Tenant-Tests ausführen.
|
|
12. DMS-Dateien stichprobenartig öffnen.
|
|
|
|
`alembic stamp` ist nur für einen separat dokumentierten Sonderfall zulässig, wenn das vorhandene Schema vorher vollständig gegen die Zielrevision validiert wurde.
|
|
|
|
**Abhängigkeit:** Phase 0a
|
|
|
|
---
|
|
|
|
## Phase 1: Security Kernel konsolidieren (10h)
|
|
|
|
**Ziel:** Ein eindeutiger Security Kernel mit klarer Verantwortungstrennung.
|
|
|
|
### 1.1 Verantwortungstabelle (1h)
|
|
|
|
| Schicht | Frage | Mechanismus |
|
|
|---------|-------|------------|
|
|
| Auth | Ist der User eingeloggt? | Session/Cookie |
|
|
| Tenant Membership | Ist User im richtigen Tenant? | UserTenant.status == 'active' |
|
|
| RBAC (Capabilities) | Darf User grundsätzlich Kontakte lesen? | `contacts:read` Permission |
|
|
| Objekt-ACL | Darf User DIESEN Kontakt sehen? | owner_id + entity_permissions |
|
|
| ABAC | Darf User Kontakte mit status=lead sehen? | entity_policies |
|
|
| RLS | Ist User im richtigen Tenant? (DB-Barriere) | `tenant_id = app.current_tenant_id` |
|
|
|
|
### 1.2 RLS auf Tenant-Isolation reduzieren (3h)
|
|
|
|
RLS soll NICHT die volle Geschäftsautorisierung übernehmen. Nur `tenant_id` Check.
|
|
|
|
- Migration: Alle RLS Policies auf contacts reduzieren auf `tenant_id` Check
|
|
- Entfernen: owner_id, sharing, permissions aus RLS Policies
|
|
- Das macht die Application Layer (visibility.py)
|
|
- RLS = Sicherheitsgurt, nicht Fahrzeugsteuerung
|
|
|
|
Neue contacts RLS Policies:
|
|
```sql
|
|
CREATE POLICY contacts_tenant_isolation ON contacts
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid)
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
```
|
|
|
|
### 1.3 visibility.py als SQL-Ausdrücke bestätigen (1h)
|
|
|
|
- `apply_visibility_filter()` liefert SQLAlchemy-Ausdrücke — korrekt
|
|
- `check_single_entity_access()` für Einzelaktionen — korrekt
|
|
- Batch-Resolution für Listen — korrekt
|
|
- Keine Python-Filter, alles in SQL
|
|
|
|
### 1.4 Überlappungen entfernen (2h)
|
|
|
|
- RLS prüft nur tenant_id (Phase 1.2)
|
|
- visibility.py prüft owner_id + sharing + permissions
|
|
- entity_permission_service prüft effective_access
|
|
- Keine Redundanz mehr
|
|
|
|
### 1.5 Frontend canAccess Fallback entfernen (3h)
|
|
|
|
- Problem: Permissions werden im Frontend nicht korrekt geladen → canAccess Fallback zeigt alles
|
|
- Fix: Permissions beim Login laden und im authStore speichern
|
|
- `usePermission()` nutzt echte Permissions aus authStore
|
|
- canAccess Fallback wird entfernt — echte Permission-Checks
|
|
- Backend: `/api/v1/auth/me` returns permissions + field_permissions
|
|
- Frontend: authStore speichert permissions, usePermission nutzt sie
|
|
|
|
**Abhängigkeit:** Phase 0a + 0b
|
|
|
|
---
|
|
|
|
## Phase 2: Datenbankrollen und RLS strikt trennen (4h)
|
|
|
|
**Ziel:** Korrekte DB-Rollen mit klaren Verantwortungen. Direkt nach Security Kernel, damit alle danach neu erstellten Tabellen sofort korrekte Owner, Grants, Default Privileges und RLS-Policies haben.
|
|
|
|
### 2.1 Rollen definieren (1h)
|
|
|
|
#### Plattformadministrator (nur einmalige Infrastruktur)
|
|
|
|
```text
|
|
crm_platform_admin
|
|
```
|
|
|
|
Darf: Datenbank und Schema initialisieren, PostgreSQL-Erweiterungen installieren, Rollen erzeugen. Zugangsdaten stehen nicht dauerhaft in API- oder Worker-Containern.
|
|
|
|
#### Migrationsrolle
|
|
|
|
```text
|
|
crm_migration
|
|
```
|
|
|
|
* Owner des Anwendungsschemas
|
|
* führt Alembic aus
|
|
* darf DDL innerhalb des Anwendungsschemas
|
|
* kein Superuser
|
|
* kein API-Login
|
|
|
|
#### Auth-Rolle
|
|
|
|
```text
|
|
crm_auth
|
|
```
|
|
|
|
* minimaler Zugriff auf Benutzer, Tenants und aktive Memberships
|
|
* keine allgemeinen Fachdatenrechte
|
|
|
|
#### API-Rolle
|
|
|
|
```text
|
|
crm_api
|
|
```
|
|
|
|
* `NOSUPERUSER`
|
|
* `NOBYPASSRLS`
|
|
* kein Tabellenowner
|
|
* fachlicher Zugriff nur unter gesetztem Tenant-Kontext
|
|
|
|
#### Worker-Rolle
|
|
|
|
```text
|
|
crm_worker
|
|
```
|
|
|
|
Der Worker darf nicht pauschal alle Mandantendaten ohne Kontext lesen.
|
|
|
|
Trennung:
|
|
* Polling/Claiming von Outbox-Jobs (ohne Tenant-Kontext)
|
|
* fachliche Verarbeitung eines konkreten Events (mit Tenant-Kontext, RLS erzwungen)
|
|
|
|
### 2.2 Default Privileges (1h)
|
|
|
|
Vollständige Default Privileges für:
|
|
* Tabellen
|
|
* Sequenzen
|
|
* Funktionen (sofern notwendig)
|
|
* Schema-Nutzung
|
|
|
|
### 2.3 docker-compose + prestart.sh anpassen (1h)
|
|
|
|
- API: `DATABASE_URL=postgresql+asyncpg://crm_api:...@postgres:5432/crm_db`
|
|
- Worker: `DATABASE_URL=postgresql+asyncpg://crm_worker:...@postgres:5432/crm_db`
|
|
- Migration: `MIGRATION_DATABASE_URL=postgresql+asyncpg://crm_migration:...@postgres:5432/crm_db`
|
|
- Auth: `AUTH_DATABASE_URL=postgresql+asyncpg://crm_auth:...@postgres:5432/crm_db`
|
|
- prestart.sh: Alembic mit crm_migration, App mit crm_api
|
|
|
|
### 2.4 Eine Variable (1h)
|
|
|
|
- `app.current_tenant_id` — einzige Variable
|
|
- `app.tenant_id` wird nicht mehr gesetzt (Legacy entfernt)
|
|
- Alle Migrationen die `app.tenant_id` nutzen werden auf `app.current_tenant_id` umgestellt
|
|
- Setze transaktionslokal: `SELECT set_config('app.current_tenant_id', :tenant_id, true)`
|
|
- Ohne Tenant-Kontext muss der fachliche Zugriff fehlschlagen
|
|
|
|
**Abhängigkeit:** Phase 1
|
|
|
|
---
|
|
|
|
## Phase 3: Plugin-System vereinfachen (4h)
|
|
|
|
**Ziel:** Router einmal registrieren, Aktivierungsstatus per Gate prüfen.
|
|
|
|
### 3.1 Statische Registrierung nur in main.py (1h)
|
|
|
|
- Alle Router beim Startup in main.py registrieren, einmalig
|
|
- PluginRegistry.activate() registriert keine Router mehr
|
|
- PluginRegistry.activate() ändert nur DB-Status + Permission Registry
|
|
|
|
### 3.2 require_active_plugin als Gate mit Cache (1h)
|
|
|
|
- Prüft: 1) Global aktiv (Registry), 2) Pro-Tenant aktiv (tenant_plugin_activation)
|
|
- Fail-closed bei Fehlern (503)
|
|
- WebSocket-Routen auch geprüft
|
|
- Cache: `Datenbank = Source of Truth, Redis = Cache`
|
|
- Cache-Key: `plugin-activation:{tenant_id}:{plugin_key}`
|
|
- Bei Aktivierung/Deaktivierung: DB aktualisieren, Cache invalidieren, Konfigurationsversion erhöhen
|
|
- Das Gate darf nicht bei jedem Request zwingend eine zusätzliche DB-Abfrage verursachen
|
|
|
|
### 3.3 Aktivierung/Deaktivierung ohne Neustart (1h)
|
|
|
|
- Gate prüft DB (über Cache), nicht in-memory Set
|
|
- Plugin aktivieren → DB Update → Cache invalidieren → Gate sieht es sofort
|
|
- Plugin deaktivieren → DB Update → Cache invalidieren → Gate blockt sofort
|
|
|
|
### 3.4 tenant_plugin_activation UI (1h)
|
|
|
|
- Settings → Plugins → pro-Tenant aktivieren/deaktivieren
|
|
- System-Admin kann global aktivieren
|
|
- Tenant-Admin kann pro-Tenant aktivieren (nur wenn global aktiv)
|
|
|
|
**Abhängigkeit:** Phase 2
|
|
|
|
---
|
|
|
|
## Phase 4: Dateisysteme vereinheitlichen (6h)
|
|
|
|
**Ziel:** Alles im DMS, Objekte referenzieren dorthin.
|
|
|
|
### 4.1 entity_attachments Tabelle (1h)
|
|
|
|
```sql
|
|
CREATE TABLE entity_attachments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
entity_type VARCHAR(50) NOT NULL,
|
|
entity_id UUID NOT NULL,
|
|
dms_file_id UUID NOT NULL REFERENCES dms_files(id) ON DELETE RESTRICT,
|
|
category VARCHAR(50),
|
|
display_name VARCHAR(200),
|
|
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
CREATE INDEX ix_entity_attachments_entity ON entity_attachments(entity_type, entity_id);
|
|
CREATE INDEX ix_entity_attachments_tenant ON entity_attachments(tenant_id);
|
|
```
|
|
|
|
`ON DELETE RESTRICT` — eine DMS-Datei darf nicht gelöscht werden, solange aktive Fachreferenzen existieren.
|
|
|
|
### 4.2 Bestehende Attachments migrieren (1h)
|
|
|
|
- Migration: Bestehende attachments → dms_files + entity_attachments
|
|
- Dateien bleiben im Storage, nur Metadaten werden migriert
|
|
- DMS File Eintrag pro bestehendem Attachment
|
|
- entity_attachments Referenz
|
|
|
|
### 4.3 attachment_service.py umbauen (2h)
|
|
|
|
- `save_attachment()`: Upload über DMS API, dann entity_attachments Eintrag
|
|
- `get_attachment()`: Lädt DMS File über Referenz
|
|
- `list_attachments()`: Lädt alle Referenzen für ein Entity
|
|
- `delete_attachment()`: Entfernt Referenz, DMS File Soft-Delete wenn keine weiteren Referenzen
|
|
- `download_attachment()`: Über DMS Storage-Backend
|
|
|
|
### 4.4 DMS erweitern (1h)
|
|
|
|
- Size-Limit: 50MB pro Datei
|
|
- MIME-Check: Erlaubte MIME-Types
|
|
- Hash: SHA-256 pro Datei
|
|
- Deduplikation: Nur tenantlokal — gleicher Hash innerhalb desselben Tenants → physische Deduplikation. Zwischen Tenants: eigener logischer Eintrag, eigener Audit-Trail, keine Offenlegung.
|
|
- Malware-Scan: Optional (ClamAV Integration später)
|
|
|
|
### 4.5 Frontend (1h)
|
|
|
|
- Upload: POST /api/v1/dms/files → dms_file_id → POST /api/v1/attachments
|
|
- Download: GET /api/v1/attachments/{id}/download → DMS File
|
|
- UI bleibt gleich, nur API-Calls ändern
|
|
|
|
**Abhängigkeit:** Phase 3
|
|
|
|
---
|
|
|
|
## Phase 5: Workspaces Backend (47-81h)
|
|
|
|
**Ziel:** Workspace Model + API + Migration + Modulkonfiguration.
|
|
|
|
### 5.1 Datenmodell (4-7h)
|
|
|
|
```sql
|
|
CREATE TABLE workspaces (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
icon VARCHAR(50) DEFAULT 'LayoutGrid',
|
|
description TEXT,
|
|
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (tenant_id, id),
|
|
UNIQUE (tenant_id, name)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX uq_workspace_default_per_tenant
|
|
ON workspaces (tenant_id)
|
|
WHERE is_default = TRUE;
|
|
```
|
|
|
|
```sql
|
|
CREATE TABLE workspace_modules (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
workspace_id UUID NOT NULL,
|
|
module_key VARCHAR(100) NOT NULL,
|
|
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
|
menu_order INTEGER NOT NULL DEFAULT 0,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
FOREIGN KEY (tenant_id, workspace_id)
|
|
REFERENCES workspaces(tenant_id, id)
|
|
ON DELETE CASCADE,
|
|
UNIQUE (tenant_id, workspace_id, module_key)
|
|
);
|
|
|
|
CREATE INDEX ix_workspace_modules_workspace
|
|
ON workspace_modules (tenant_id, workspace_id, menu_order);
|
|
```
|
|
|
|
```sql
|
|
CREATE TABLE workspace_users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
workspace_id UUID NOT NULL,
|
|
user_id UUID NOT NULL,
|
|
role VARCHAR(20) NOT NULL DEFAULT 'member',
|
|
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
|
assigned_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
FOREIGN KEY (tenant_id, workspace_id)
|
|
REFERENCES workspaces(tenant_id, id)
|
|
ON DELETE CASCADE,
|
|
FOREIGN KEY (tenant_id, user_id)
|
|
REFERENCES user_tenants(tenant_id, user_id)
|
|
ON DELETE CASCADE,
|
|
CHECK (role IN ('member', 'manager')),
|
|
UNIQUE (tenant_id, workspace_id, user_id)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX uq_workspace_default_per_user
|
|
ON workspace_users (tenant_id, user_id)
|
|
WHERE is_default = TRUE;
|
|
```
|
|
|
|
```sql
|
|
CREATE TABLE workspace_widgets (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
workspace_id UUID NOT NULL,
|
|
widget_key VARCHAR(100) NOT NULL,
|
|
title VARCHAR(200),
|
|
position_x INTEGER NOT NULL DEFAULT 0,
|
|
position_y INTEGER NOT NULL DEFAULT 0,
|
|
width INTEGER NOT NULL DEFAULT 1,
|
|
height INTEGER NOT NULL DEFAULT 1,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
FOREIGN KEY (tenant_id, workspace_id)
|
|
REFERENCES workspaces(tenant_id, id)
|
|
ON DELETE CASCADE,
|
|
CHECK (position_x >= 0),
|
|
CHECK (position_y >= 0),
|
|
CHECK (width > 0),
|
|
CHECK (height > 0)
|
|
);
|
|
|
|
CREATE INDEX ix_workspace_widgets_layout
|
|
ON workspace_widgets (tenant_id, workspace_id, position_y, position_x);
|
|
```
|
|
|
|
Kein UNIQUE Constraint auf `(workspace_id, widget_key)` — derselbe Widget-Typ muss beliebig oft vorkommen dürfen.
|
|
|
|
### 5.2 Backend-Service und APIs (6-10h)
|
|
|
|
- `app/models/workspace.py` — Workspace, WorkspaceModule, WorkspaceUser, WorkspaceWidget
|
|
- `app/services/workspace_service.py` — CRUD, Module-Zuweisung, User-Zuweisung, Workspace-Manager Validierung
|
|
- Default Workspace bei Tenant-Erstellung
|
|
|
|
API:
|
|
```
|
|
GET /api/v1/workspaces → Alle Workspaces (Admin)
|
|
POST /api/v1/workspaces → Workspace erstellen (Admin)
|
|
GET /api/v1/workspaces/{id} → Workspace Details
|
|
PUT /api/v1/workspaces/{id} → Workspace aktualisieren
|
|
DELETE /api/v1/workspaces/{id} → Workspace löschen
|
|
GET /api/v1/workspaces/my → Meine Workspaces
|
|
POST /api/v1/workspaces/{id}/modules → Module zuweisen
|
|
POST /api/v1/workspaces/{id}/users → User zuweisen
|
|
DELETE /api/v1/workspaces/{id}/users/{uid} → User entfernen
|
|
POST /api/v1/workspaces/{id}/widgets → Widget konfigurieren
|
|
```
|
|
|
|
### 5.3 Workspace-Manager und Validierung (4-7h)
|
|
|
|
- `workspace_users.role = 'manager'` für Workspace-Manager
|
|
- Service prüft: Ist User Manager dieses Workspaces? Oder Tenant-Admin? Oder System-Admin?
|
|
- Workspace-Manager kann nur seinen Workspace konfigurieren
|
|
- Workspace-Manager kann keine RBAC/ABAC/Entity Permissions verändern
|
|
|
|
### 5.4 Modulkonfiguration (5-8h)
|
|
|
|
Jedes workspacefähige Modul definiert ein eigenes validiertes Konfigurationsschema in `workspace_modules.config`.
|
|
|
|
Der Plugin-Code muss die Konfiguration validieren. Ungeprüfte beliebige JSON-Strukturen dürfen nicht direkt verwendet werden.
|
|
|
|
#### Kalender-Beispiel
|
|
|
|
```json
|
|
{
|
|
"visible_calendar_ids": ["uuid-1", "uuid-2"],
|
|
"default_calendar_id": "uuid-1",
|
|
"show_unassigned_events": false
|
|
}
|
|
```
|
|
|
|
Das Kalender-Modul zeigt im Workspace nur: `visible_calendar_ids ∩ Kalender, die der Benutzer lesen darf`.
|
|
|
|
APIs:
|
|
```
|
|
GET /api/v1/workspaces/{workspace_id}/modules/calendar/config
|
|
PUT /api/v1/workspaces/{workspace_id}/modules/calendar/config
|
|
GET /api/v1/workspaces/{workspace_id}/modules/calendar/options
|
|
```
|
|
|
|
`options` liefert nur Kalender die zum Tenant gehören und auswählbar sind. Bei der normalen Abfrage wird zusätzlich der Benutzerzugriff geprüft.
|
|
|
|
#### Kontakte-Beispiel
|
|
|
|
```json
|
|
{
|
|
"visible_folder_ids": ["uuid-vertrieb"],
|
|
"default_folder_id": "uuid-vertrieb",
|
|
"saved_filter_ids": ["uuid-offene-leads"],
|
|
"default_saved_filter_id": "uuid-offene-leads"
|
|
}
|
|
```
|
|
|
|
Im Workspace „Einkauf" kann dasselbe Kontakte-Modul mit anderer Konfiguration verwendet werden.
|
|
|
|
#### Allgemeine Regel
|
|
|
|
Jedes Plugin kann optional bereitstellen:
|
|
```python
|
|
workspace_config_schema
|
|
validate_workspace_config()
|
|
get_workspace_configuration_options()
|
|
apply_workspace_view_filter()
|
|
```
|
|
|
|
Plugins ohne Workspace-Unterstützung verwenden nur `is_visible` und `menu_order`.
|
|
|
|
### 5.5 Kalender-Integration (4-7h)
|
|
|
|
- Kalender-Modul nutzt `workspace_modules.config` für sichtbare Kalender
|
|
- `apply_workspace_view_filter()` filtert Kalender nach Workspace-Konfiguration ∩ Benutzer-Rechten
|
|
- Workspace-Manager kann Kalenderauswahl konfigurieren
|
|
|
|
### 5.6 Kontakte-/Ansichten-Integration (3-6h)
|
|
|
|
- Kontakte-Modul nutzt `workspace_modules.config` für sichtbare Ordner und Ansichten
|
|
- `apply_workspace_view_filter()` filtert Kontakte nach Workspace-Konfiguration ∩ Benutzer-Rechten
|
|
- Workspace-Manager kann Ordner und Ansichten konfigurieren
|
|
|
|
### 5.7 Tests und Fehlerkorrekturen (6-10h)
|
|
|
|
Freigabekriterien für Workspaces:
|
|
|
|
1. Derselbe Benutzer kann in zwei Browser-Tabs unterschiedliche Workspaces verwenden.
|
|
2. Ein Workspacewechsel verändert keine Benutzerrechte.
|
|
3. Ein ausgeblendetes Modul erscheint nicht in der Sidebar.
|
|
4. Ein direkt aufgerufenes berechtigtes Fachobjekt bleibt erreichbar.
|
|
5. Ein Modul ohne Benutzerpermission wird auch dann nicht angezeigt, wenn es im Workspace aktiviert ist.
|
|
6. Kontakte können in mehreren Workspaces dargestellt werden.
|
|
7. Jeder Workspace kann unterschiedliche Kontaktordner und gespeicherte Ansichten verwenden.
|
|
8. Das Kalender-Modul zeigt nur konfigurierte und gleichzeitig berechtigte Kalender.
|
|
9. Nicht berechtigte Kalender werden durch Workspace-Konfiguration niemals sichtbar.
|
|
10. Derselbe Widget-Typ kann mehrfach im selben Workspace vorkommen.
|
|
11. Widget-Instanzen besitzen unabhängige Positionen und Konfigurationen.
|
|
12. Workspace-Manager können nur ihren Workspace konfigurieren.
|
|
13. Workspace-Manager können keine RBAC-, ABAC- oder Entity Permissions verändern.
|
|
14. Benutzer anderer Tenants können keinem Workspace zugewiesen werden.
|
|
15. RLS schützt alle Workspace-Tabellen tenantübergreifend.
|
|
16. Default-Workspace ist pro Benutzer eindeutig.
|
|
17. Default-Workspace ist pro Tenant eindeutig.
|
|
18. Gelöschte oder deaktivierte Workspaces können nicht mehr ausgewählt werden.
|
|
|
|
**Abhängigkeit:** Phase 4
|
|
|
|
---
|
|
|
|
## Phase 6: Workspaces Frontend (in Phase 5 enthalten)
|
|
|
|
**Ziel:** Workspace Switcher + UI + Sidebar-Filter + Dashboard.
|
|
|
|
### 6.1 Workspace Switcher in TopBar (4-7h)
|
|
|
|
- Dropdown neben Tenant-Switcher
|
|
- Zeigt alle Workspaces des Users
|
|
- Wechseln speichert active_workspace_id in `sessionStorage` (tablokal, nicht global)
|
|
- Client sendet `X-Workspace-ID` Header bei workspacefähigen Requests
|
|
|
|
### 6.2 Sidebar-Filter nach Workspace (in 6.1 enthalten)
|
|
|
|
Sidebar-Logik:
|
|
|
|
```text
|
|
Menüpunkt sichtbar
|
|
=
|
|
Plugin global aktiv
|
|
UND
|
|
Plugin im Tenant aktiv
|
|
UND
|
|
Modul im Workspace sichtbar
|
|
UND
|
|
Benutzer besitzt grundlegende Read-Permission
|
|
```
|
|
|
|
Die Sidebar blendet aus: nicht aktive Plugins, im Workspace deaktivierte Module, Module ohne Benutzerberechtigung, leere Menügruppen.
|
|
|
|
Reihenfolge aus `workspace_modules.menu_order`.
|
|
|
|
### 6.3 Verwaltungsoberfläche (6-10h)
|
|
|
|
- Settings → Rechte → Workspaces: Liste aller Workspaces
|
|
- Workspace erstellen/bearbeiten/löschen
|
|
- Module zuweisen (Checkbox-Liste aller verfügbaren Module)
|
|
- User zuweisen (Multi-Select) mit Rolle (member/manager)
|
|
- Modulkonfiguration (Kalenderauswahl, Kontaktordner, Ansichten)
|
|
- Dashboard-Widgets konfigurieren (Drag & Drop, mehrfach verwendbar)
|
|
|
|
### 6.4 Dashboard und Mehrfach-Widgets (5-9h)
|
|
|
|
- Dashboard lädt Widgets aus workspace_widgets
|
|
- Layout pro Workspace speichern
|
|
- Default-Widgets bei Workspace-Erstellung
|
|
- Derselbe Widget-Typ kann mehrfach vorkommen (z.B. "Umsatz aktueller Monat" + "Umsatz aktuelles Jahr")
|
|
- Widget-Instanzen besitzen unabhängige Positionen und Konfigurationen
|
|
|
|
**Abhängigkeit:** Phase 5
|
|
|
|
---
|
|
|
|
## Phase 7: Outbox standardisieren (3h)
|
|
|
|
**Ziel:** Standardisierter Event-Envelope mit Delivery-Tracking.
|
|
|
|
### 7.1 Event-Envelope (1h)
|
|
|
|
```json
|
|
{
|
|
"event_id": "uuid",
|
|
"event_type": "crm.contact.created.v1",
|
|
"tenant_id": "uuid",
|
|
"aggregate_type": "contact",
|
|
"aggregate_id": "uuid",
|
|
"occurred_at": "timestamp",
|
|
"correlation_id": "uuid",
|
|
"schema_version": 1,
|
|
"payload": {}
|
|
}
|
|
```
|
|
|
|
### 7.2 outbox_deliveries Tabelle (1h)
|
|
|
|
```sql
|
|
CREATE TABLE outbox_deliveries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
event_id UUID NOT NULL REFERENCES event_outbox(id) ON DELETE CASCADE,
|
|
consumer_name VARCHAR(150) NOT NULL,
|
|
status VARCHAR(30) NOT NULL DEFAULT 'pending',
|
|
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
next_attempt_at TIMESTAMPTZ,
|
|
last_error TEXT,
|
|
processed_at TIMESTAMPTZ,
|
|
UNIQUE(event_id, consumer_name)
|
|
);
|
|
```
|
|
|
|
Beim Dispatch wird die für dieses Event erwartete Consumerliste festgeschrieben. Ein Event darf erst abgeschlossen werden, wenn alle verpflichtenden Deliveries erfolgreich sind.
|
|
|
|
`consumer_inbox` bleibt zur Idempotenz bestehen. Ein Consumer muss anhand von `consumer_name + event_id` erkennen, ob er das Event bereits verarbeitet hat.
|
|
|
|
### 7.3 Event-Namen standardisieren + Consumer-Inbox (1h)
|
|
|
|
- Format: `crm.{aggregate}.{action}.v{version}`
|
|
- `enqueue_outbox_event()` bekommt Parameter für aggregate_type, aggregate_id, correlation_id
|
|
- Jeder Consumer trägt sich in consumer_inbox ein
|
|
- Event gilt als 'published' wenn alle Deliveries 'processed' sind
|
|
- Bei Consumer-Fehler: 'failed' Status, Event bleibt pending für Retry
|
|
|
|
**Abhängigkeit:** Phase 6
|
|
|
|
---
|
|
|
|
## Phase 8: Command- und RequestContext-Grundlage (8h)
|
|
|
|
**Ziel:** Eine Transaktionsgrenze pro Geschäftsoperation. INKREMENTELL. Muss vor den ersten ERP-Modulen stehen.
|
|
|
|
### 8.1 Command Base Class + UnitOfWork (2h)
|
|
|
|
```python
|
|
class Command(ABC):
|
|
@abstractmethod
|
|
async def execute(self, context: RequestContext, uow: UnitOfWork) -> Any:
|
|
...
|
|
|
|
class UnitOfWork:
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
self.contacts = ContactRepository(db)
|
|
self.outbox = OutboxRepository(db)
|
|
self.audit = AuditRepository(db)
|
|
async def commit(self):
|
|
await self.db.commit()
|
|
async def rollback(self):
|
|
await self.db.rollback()
|
|
```
|
|
|
|
### 8.2 RequestContext (1h)
|
|
|
|
```python
|
|
class RequestContext:
|
|
user_id: uuid.UUID
|
|
tenant_id: uuid.UUID
|
|
workspace_id: uuid.UUID | None
|
|
permissions: list[str]
|
|
is_system_admin: bool
|
|
correlation_id: uuid.UUID
|
|
def require(self, permission: str):
|
|
if not self.has_permission(permission):
|
|
raise PermissionError(permission)
|
|
```
|
|
|
|
### 8.3 Kern-Module auf Commands umstellen (3h)
|
|
|
|
- CreateContact, UpdateContact, DeleteContact
|
|
- CreateAddress, UpdateAddress, DeleteAddress
|
|
- CreateAttachment, DeleteAttachment
|
|
- Ein Commit pro Operation: Route → Command → Audit + Outbox → Commit
|
|
|
|
### 8.4 Bestehende Services belassen (2h)
|
|
|
|
- Nicht alle Services gleichzeitig umbauen
|
|
- Bei Überarbeitung: Schrittweise auf Command Pattern migrieren
|
|
- Neue ERP-Module nutzen Commands von Anfang an
|
|
|
|
**Abhängigkeit:** Phase 7
|
|
|
|
---
|
|
|
|
## Phase 9: Report-System isolieren (4h)
|
|
|
|
**Ziel:** Reports in isoliertem Worker, nicht im API-Prozess.
|
|
|
|
### 9.1 Report-Job Queue (1h)
|
|
|
|
- Report-Erstellung als Background-Job (ARQ/Redis)
|
|
- API erstellt Job, gibt Job-ID zurück
|
|
- Client pollt Job-Status oder bekommt WebSocket-Notification
|
|
|
|
### 9.2 Report-Worker (2h)
|
|
|
|
- Separater Prozess für PDF-Erzeugung
|
|
- Constraints: Kein Shell-Zugriff, kein Docker-Socket, keine Secrets außer Template-Daten, Read-only Dateisystem (nur Output-Verzeichnis), CPU/RAM-Limit, nur freigegebene Templates, URL-Fetching deaktiviert
|
|
|
|
### 9.3 PDF in Object Storage (1h)
|
|
|
|
- Output in DMS, nicht im API-Container
|
|
- Job-Status: pending → processing → completed/failed
|
|
- Download-Link via DMS API
|
|
|
|
**Abhängigkeit:** Phase 8
|
|
|
|
---
|
|
|
|
## Phase 10: Migrationen als Produktbestandteil (3h)
|
|
|
|
**Ziel:** Automatische Tests für Migrationen.
|
|
|
|
### 10.1 CI Gate: Alembic auf leerer DB (1h)
|
|
|
|
- `alembic upgrade head` auf frischer DB muss funktionieren
|
|
- `alembic downgrade base` muss funktionieren
|
|
- CI bricht ab wenn Migration fehlschlägt
|
|
|
|
### 10.2 Migration-Test-Script (1h)
|
|
|
|
- Datensatzanzahlen vor/nach Migration vergleichen
|
|
- Verwaiste Fremdschlüssel prüfen
|
|
- Nullwerte prüfen
|
|
- Tenant-Verteilung prüfen
|
|
- RLS-Zugriffstest nach Migration
|
|
|
|
### 10.3 Regeln (1h)
|
|
|
|
- Veröffentlichte Migrationen nie nachträglich ändern
|
|
- Neue Revision für Fixes
|
|
- .gitignore erweitern: .env, dump.rdb, __pycache__, frontend/dist, .pytest_cache
|
|
|
|
**Abhängigkeit:** Phase 9
|
|
|
|
---
|
|
|
|
## Phase 11: CI erweitern (4h)
|
|
|
|
**Ziel:** CI muss architektonische Fehler stoppen.
|
|
|
|
### 11.1 Fehlende CI Gates (2h)
|
|
|
|
- **Ruff** (Python Linter) — Style + Import-Checks
|
|
- **Cross-Tenant Security Test** — Test der RLS Tenant-Isolation
|
|
- **Alembic Upgrade Test** — Auf leerer DB
|
|
- **Alembic Downgrade Test** — Base → head → base
|
|
- **Dependency Scan** — pip-audit für Python, npm audit für Frontend
|
|
- **Container Smoke Test** — App startet, Health-Check grün
|
|
|
|
### 11.2 Build-Hygiene (1h)
|
|
|
|
- **npm ci** statt `npm ci || npm install` — harter Abbruch bei Fehler
|
|
- **Python deps pinning** — requirements.txt mit exakten Versionen
|
|
- **.gitignore** — .env, dump.rdb, __pycache__, frontend/dist, .pytest_cache
|
|
|
|
### 11.3 Vorhandene Gates bestätigen (1h)
|
|
|
|
- Python Compile ✅, TypeScript ✅, Frontend Build ✅, Test Collection ✅, SQL Injection Check ✅, Jinja2 Sandbox ✅, RLS Variable ✅, Fail-Closed Plugin Gate ✅, Cross-Plugin Imports ✅, Alembic Heads ✅
|
|
|
|
**Abhängigkeit:** Phase 10
|
|
|
|
---
|
|
|
|
## Phase 12: Monitoring und Logging (3h)
|
|
|
|
**Ziel:** Strukturiertes Monitoring für Pilotbetrieb. Extern abgesichert.
|
|
|
|
### 12.1 Strukturiertes Logging (1h)
|
|
|
|
- JSON-Logs für alle Requests
|
|
- Log-Level pro Environment konfigurierbar
|
|
- Correlation-ID in allen Logs
|
|
- Log-Rotation konfiguriert
|
|
|
|
### 12.2 Health Endpoints + Metrics (1h)
|
|
|
|
Trennung:
|
|
|
|
```text
|
|
/health/live — Prüft ob der Prozess lebt
|
|
/health/ready — Prüft PostgreSQL, Redis, Worker, Storage
|
|
/metrics — Prometheus-kompatible Kennzahlen
|
|
```
|
|
|
|
### 12.3 Externes Alerting (1h)
|
|
|
|
Interne Webhook-Alarme reichen nicht. Wenn API, Worker oder Redis ausgefallen sind, kann das System keinen eigenen Alarm versenden.
|
|
|
|
Mindestens ein externes Monitoring: Uptime Kuma, Prometheus Alertmanager, Grafana, Sentry, oder Coolify Health Monitoring.
|
|
|
|
Alarme bei:
|
|
- API nicht erreichbar
|
|
- Worker-Heartbeat fehlt
|
|
- Readiness rot
|
|
- Fehlerrate über Schwellwert
|
|
- Response-Zeit über Schwellwert
|
|
- DB-Pool erschöpft
|
|
- Outbox-Rückstau
|
|
- fehlgeschlagene Jobs
|
|
|
|
**Abhängigkeit:** Phase 11
|
|
|
|
---
|
|
|
|
## Gesamt-Übersicht
|
|
|
|
| Phase | Inhalt | Aufwand |
|
|
|-------|--------|---------|
|
|
| 0a | Cross-Tenant Tests + RLS Tests + Test-Suite grün | 4h |
|
|
| 0b | Backup/Restore Test | 2h |
|
|
| 1 | Security Kernel + canAccess Fallback entfernen | 10h |
|
|
| 2 | DB-Rollen strikt trennen + RLS standardisieren | 4h |
|
|
| 3 | Plugin-System vereinfachen + Cache | 4h |
|
|
| 4 | Dateisysteme vereinheitlichen (DMS) | 6h |
|
|
| 5 | Workspaces Backend + Frontend (komplett) | 47-81h |
|
|
| 7 | Outbox standardisieren + Deliveries | 3h |
|
|
| 8 | Command- und RequestContext-Grundlage | 8h |
|
|
| 9 | Report-System isolieren | 4h |
|
|
| 10 | Migrationen als Produktbestandteil | 3h |
|
|
| 11 | CI erweitern + Build-Hygiene | 4h |
|
|
| 12 | Monitoring und Logging (extern) | 3h |
|
|
| | **Gesamt** | **ca. 160-240h** |
|
|
|
|
## Priorität vor ERP-Modulen
|
|
|
|
**Zwingend vor ERP:** Phase 0a-5 (Tests + Security + DB-Rollen + Plugin + DMS + Workspaces) = 77-111h
|
|
|
|
**Danach möglich:** ERP-Module können auf sauberer Architektur + Workspaces aufbauen.
|
|
|
|
**Parallel zu ERP:** Phase 7-12 (Outbox + Commands + Reports + Migrationen + CI + Monitoring) = 25h
|
|
|
|
## Abdeckung nach Plan-Abschluss
|
|
|
|
| Kategorie | Vor Plan | Nach Plan |
|
|
|-----------|:---:|:---:|
|
|
| P0 Befunde (6) | 6/6 gefixt, ungetestet | 6/6 gefixt + getestet |
|
|
| P1 Befunde (9) | 9/9 gefixt, ungetestet | 9/9 gefixt + getestet |
|
|
| Architektur 10-Punkte | 2/10 | 10/10 |
|
|
| Mindestfreigabe 10-Punkte | 2/10 | 10/10 |
|
|
| Workspaces | 0 | Vollständig |
|
|
| Monitoring | 0 | Extern abgesichert |
|
|
| CI/CD | 10 Gates | 16 Gates |
|
|
|
|
## Architekturbewertung
|
|
|
|
```text
|
|
Aktuelle Architektur: ungefähr 6/10
|
|
Nach erfolgreicher Umsetzung: ungefähr 8 bis 8,5/10
|
|
Nach Pilotbetrieb und mehreren stabilen Releases: potenziell 9/10
|
|
```
|
|
|
|
Ein Architekturwert von 10/10 ist nicht seriös messbar und vor einem realen Pilotbetrieb nicht belegbar.
|
|
|
|
Der Plan gilt erst als abgeschlossen, wenn die Änderungen nicht nur im Code vorhanden, sondern durch reproduzierbare Integrationstests nachgewiesen sind.
|
|
|
|
## Bewusst nicht umgesetzt
|
|
|
|
- `security_resources` Tabelle — Aktuelles System funktioniert, konsolidieren statt neu bauen
|
|
- Alle Services in Commands — Inkrementell, nicht Big-Bang
|
|
- `stored_objects` Tabelle — Alles im DMS, Referenzen von Objekten
|