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

@@ -5,7 +5,7 @@
* retry logic, and optimistic locking support.
*/
import { SDKState, CheckpointStatus } from './types'
import { SDKState, CheckpointStatus, ProjectInfo } from './types'
// =============================================================================
// TYPES
@@ -73,16 +73,19 @@ const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
export class SDKApiClient {
private baseUrl: string
private tenantId: string
private projectId: string | undefined
private timeout: number
private abortControllers: Map<string, AbortController> = new Map()
constructor(options: {
baseUrl?: string
tenantId: string
projectId?: string
timeout?: number
}) {
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL
this.tenantId = options.tenantId
this.projectId = options.projectId
this.timeout = options.timeout || DEFAULT_TIMEOUT
}
@@ -188,8 +191,10 @@ export class SDKApiClient {
*/
async getState(): Promise<StateResponse | null> {
try {
const params = new URLSearchParams({ tenantId: this.tenantId })
if (this.projectId) params.set('projectId', this.projectId)
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
`${this.baseUrl}/state?${params.toString()}`,
{
method: 'GET',
headers: {
@@ -228,6 +233,7 @@ export class SDKApiClient {
},
body: JSON.stringify({
tenantId: this.tenantId,
projectId: this.projectId,
state,
version,
}),
@@ -245,8 +251,10 @@ export class SDKApiClient {
* Delete SDK state for the current tenant
*/
async deleteState(): Promise<void> {
const params = new URLSearchParams({ tenantId: this.tenantId })
if (this.projectId) params.set('projectId', this.projectId)
await this.fetchWithRetry<APIResponse<void>>(
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
`${this.baseUrl}/state?${params.toString()}`,
{
method: 'DELETE',
headers: {
@@ -571,6 +579,107 @@ export class SDKApiClient {
return this.tenantId
}
/**
* Set project ID for multi-project support
*/
setProjectId(projectId: string | undefined): void {
this.projectId = projectId
}
/**
* Get current project ID
*/
getProjectId(): string | undefined {
return this.projectId
}
// ---------------------------------------------------------------------------
// Public Methods - Project Management
// ---------------------------------------------------------------------------
/**
* List all projects for the current tenant
*/
async listProjects(): Promise<{ projects: ProjectInfo[]; total: number }> {
const response = await this.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
return response
}
/**
* Create a new project
*/
async createProject(data: {
name: string
description?: string
customer_type?: string
copy_from_project_id?: string
}): Promise<ProjectInfo> {
const response = await this.fetchWithRetry<ProjectInfo>(
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
body: JSON.stringify({
...data,
tenant_id: this.tenantId,
}),
}
)
return response
}
/**
* Update an existing project
*/
async updateProject(projectId: string, data: {
name?: string
description?: string
}): Promise<ProjectInfo> {
const response = await this.fetchWithRetry<ProjectInfo>(
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
body: JSON.stringify({
...data,
tenant_id: this.tenantId,
}),
}
)
return response
}
/**
* Archive (soft-delete) a project
*/
async archiveProject(projectId: string): Promise<void> {
await this.fetchWithRetry<{ success: boolean }>(
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
}
/**
* Health check
*/
@@ -594,19 +703,23 @@ export class SDKApiClient {
let clientInstance: SDKApiClient | null = null
export function getSDKApiClient(tenantId?: string): SDKApiClient {
export function getSDKApiClient(tenantId?: string, projectId?: string): SDKApiClient {
if (!clientInstance && !tenantId) {
throw new Error('SDKApiClient not initialized. Provide tenantId on first call.')
}
if (!clientInstance && tenantId) {
clientInstance = new SDKApiClient({ tenantId })
clientInstance = new SDKApiClient({ tenantId, projectId })
}
if (tenantId && clientInstance && clientInstance.getTenantId() !== tenantId) {
clientInstance.setTenantId(tenantId)
}
if (clientInstance) {
clientInstance.setProjectId(projectId)
}
return clientInstance!
}