feat(azure-ai+gpu-ui): per-tenant Azure AI config + GPU health panel

- Per-tenant Azure AI config stored in tenants.tenant_config JSONB
- GET/PUT /api/tenants/{id}/ai-config + POST .../test connection
- api_key never returned to frontend (has_api_key: bool pattern)
- azure_ai.py resolves creds from tenant config when ai_enabled=True
- ai_tasks.py loads tenant config and passes it to validate_thumbnail
- Admin GPU Status section: probe button + status badge + last-checked time
- Notifications: _BELL_CHANNELS filter (notification+alert only in bell)
- Tenants.tsx: per-row Azure AI Config modal with URL auto-parse helper
- Remove duplicate in-memory /gpu-probe endpoints (kept DB-backed /probe/gpu)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:04:09 +01:00
parent 34f89cc225
commit 22c29d5655
11 changed files with 792 additions and 24 deletions
+44
View File
@@ -21,6 +21,28 @@ export interface TenantUpdate {
is_active?: boolean
}
export interface TenantAIConfig {
ai_enabled: boolean
ai_endpoint: string | null
ai_deployment: string
ai_api_version: string
has_api_key: boolean
ai_max_tokens: number
ai_temperature: number
ai_validation_prompt: string | null
}
export interface TenantAIConfigUpdate {
ai_enabled: boolean
ai_endpoint?: string | null
ai_deployment?: string
ai_api_version?: string
ai_api_key?: string | null
ai_max_tokens?: number
ai_temperature?: number
ai_validation_prompt?: string | null
}
export async function getTenants(): Promise<Tenant[]> {
const res = await api.get<Tenant[]>('/tenants/')
return res.data
@@ -44,3 +66,25 @@ export async function updateTenant(id: string, data: TenantUpdate): Promise<Tena
export async function deleteTenant(id: string): Promise<void> {
await api.delete(`/tenants/${id}`)
}
export async function getTenantAIConfig(tenantId: string): Promise<TenantAIConfig> {
const res = await api.get<TenantAIConfig>(`/tenants/${tenantId}/ai-config`)
return res.data
}
export async function updateTenantAIConfig(
tenantId: string,
config: TenantAIConfigUpdate,
): Promise<TenantAIConfig> {
const res = await api.put<TenantAIConfig>(`/tenants/${tenantId}/ai-config`, config)
return res.data
}
export async function testTenantAIConfig(
tenantId: string,
): Promise<{ ok: boolean; error?: string }> {
const res = await api.post<{ ok: boolean; error?: string }>(
`/tenants/${tenantId}/ai-config/test`,
)
return res.data
}
+21
View File
@@ -197,3 +197,24 @@ export async function updateWorkerConfig(
const res = await api.put<WorkerConfig>(`/worker/configs/${queueName}`, update)
return res.data
}
// ---------------------------------------------------------------------------
// GPU probe
// ---------------------------------------------------------------------------
export interface GPUProbeResult {
status: 'ok' | 'failed' | 'error' | 'unknown'
device_type?: string | null
error?: string | null
probed_at?: string | null
}
export async function getGpuProbeResult(): Promise<GPUProbeResult> {
const res = await api.get<GPUProbeResult>('/worker/gpu-probe')
return res.data
}
export async function triggerGpuProbe(): Promise<{ task_id: string; queued: boolean }> {
const res = await api.post<{ task_id: string; queued: boolean }>('/worker/gpu-probe')
return res.data
}