feat(sdk): Multi-Projekt-Architektur — mehrere Projekte pro Tenant
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 33s
CI / test-python-backend-compliance (push) Successful in 34s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s

Jeder Tenant kann jetzt mehrere Compliance-Projekte anlegen (z.B. verschiedene
Produkte, Tochterunternehmen). CompanyProfile ist pro Projekt kopierbar und
danach unabhaengig editierbar. Multi-Tab-Support via separater BroadcastChannel
und localStorage Keys pro Projekt.

- Migration 039: compliance_projects Tabelle, sdk_states.project_id
- Backend: FastAPI CRUD-Routes fuer Projekte mit Tenant-Isolation
- Frontend: ProjectSelector UI, SDKProvider mit projectId, URL ?project=
- State API: UPSERT auf (tenant_id, project_id) mit Abwaertskompatibilitaet
- Tests: pytest fuer Model-Validierung, Row-Konvertierung, Tenant-Isolation
- Docs: MKDocs Seite, CLAUDE.md, Backend README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-09 14:53:50 +01:00
parent d3fc4cdaaa
commit 0affa4eb66
19 changed files with 1833 additions and 102 deletions

View File

@@ -59,6 +59,7 @@ const DEFAULT_MAX_RETRIES = 3
export class StateSyncManager {
private apiClient: SDKApiClient
private tenantId: string
private projectId: string | undefined
private options: Required<SyncOptions>
private callbacks: SyncCallbacks
private syncState: SyncState
@@ -71,10 +72,12 @@ export class StateSyncManager {
apiClient: SDKApiClient,
tenantId: string,
options: SyncOptions = {},
callbacks: SyncCallbacks = {}
callbacks: SyncCallbacks = {},
projectId?: string
) {
this.apiClient = apiClient
this.tenantId = tenantId
this.projectId = projectId
this.callbacks = callbacks
this.options = {
debounceMs: options.debounceMs ?? DEFAULT_DEBOUNCE_MS,
@@ -105,7 +108,10 @@ export class StateSyncManager {
}
try {
this.broadcastChannel = new BroadcastChannel(`${SYNC_CHANNEL}-${this.tenantId}`)
const channelName = this.projectId
? `${SYNC_CHANNEL}-${this.tenantId}-${this.projectId}`
: `${SYNC_CHANNEL}-${this.tenantId}`
this.broadcastChannel = new BroadcastChannel(channelName)
this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this)
} catch (error) {
console.warn('BroadcastChannel not available:', error)
@@ -209,7 +215,9 @@ export class StateSyncManager {
// ---------------------------------------------------------------------------
private getStorageKey(): string {
return `${STORAGE_KEY_PREFIX}-${this.tenantId}`
return this.projectId
? `${STORAGE_KEY_PREFIX}-${this.tenantId}-${this.projectId}`
: `${STORAGE_KEY_PREFIX}-${this.tenantId}`
}
saveToLocalStorage(state: SDKState): void {
@@ -476,7 +484,8 @@ export function createStateSyncManager(
apiClient: SDKApiClient,
tenantId: string,
options?: SyncOptions,
callbacks?: SyncCallbacks
callbacks?: SyncCallbacks,
projectId?: string
): StateSyncManager {
return new StateSyncManager(apiClient, tenantId, options, callbacks)
return new StateSyncManager(apiClient, tenantId, options, callbacks, projectId)
}