- Backend GET/PUT /api/admin/ai-config + test-connection + load_provider_config_by_id - Frontend getAiConfig/saveAiConfig/testAiConnection helpers + AdminAiConfigTab system section - DocumentCard classification_failed badge + Re-analyze button - 131 frontend tests pass, 369 backend tests pass
8.3 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-redo-and-optimize-llm-integration | 05 | ai-admin-frontend |
|
|
|
|
|
|
Phase 7 Plan 05: Admin AI Providers Panel + Classification Failed Badge Summary
Admin AI provider configuration panel (GET/PUT/test-connection) with api_key_enc whitelist + atomic is_active flip; DocumentCard classification-failed badge driving POST /classify re-queue.
Tasks Completed
| Task | Name | Commit | Files |
|---|---|---|---|
| 1 | Admin AI-config backend + load_provider_config_by_id | e678930 |
backend/api/admin.py, backend/services/ai_config.py, backend/tests/test_admin_ai_config.py |
| 2 | Frontend client helpers + AdminAiConfigTab system section + Vitest | 0db412d |
frontend/src/api/client.js, frontend/src/components/admin/AdminAiConfigTab.vue, frontend/tests/api.spec.js |
| 3 | DocumentCard classification_failed badge + Re-analyze + Vitest | c45d9e4 |
frontend/src/components/documents/DocumentCard.vue, frontend/tests/DocumentCard.spec.js |
What Was Built
Task 1 — Backend admin AI-config endpoints
backend/services/ai_config.py gained load_provider_config_by_id(session, provider_id) — mirrors load_provider_config but filters by provider_id only (no is_active check), so the test-connection endpoint can test inactive rows.
backend/api/admin.py received:
_ai_config_to_dict(row)— whitelist helper returning{provider_id, base_url, model_name, context_chars, is_active, has_api_key, updated_at}. Explicitly excludesapi_key_enc(T-07-01).SystemAiConfigUpdate(BaseModel)— Pydantic model withextra="forbid"and aprovider_idvalidator againstPROVIDER_DEFAULTSkeys (T-07-13).GET /api/admin/ai-config— returns all 10 providers (DB rows + synthesised stubs from PROVIDER_DEFAULTS for unconfigured providers). Never exposesapi_key_enc.PUT /api/admin/ai-config— upsert with three-way api_key semantics (None=no change, ""=clear, non-empty=HKDF-encrypt). Atomic is_active flip via singleUPDATE SET is_active = (provider_id = :target). Writes audit log withfields_changedonly (T-07-14). Requiresget_current_admin(T-07-15).GET /api/admin/ai-config/test-connection?provider_id=X— callsload_provider_config_by_id, builds provider, callshealth_check(). Returns{ok: bool}as 200 — never 5xx for provider failures.
backend/tests/test_admin_ai_config.py promoted from xfail stubs to three passing tests:
test_get_never_returns_key— assertsapi_key_encand plaintext key absent from GET responsetest_put_writes_active_provider— asserts exactly 1 row withis_active=Trueafter two PUTstest_put_admin_only— asserts non-admin PUT returns 403
Task 2 — Frontend client helpers + AdminAiConfigTab system section
frontend/src/api/client.js gained:
getAiConfig()— GET/api/admin/ai-configsaveAiConfig(body)— PUT with JSON bodytestAiConnection(providerId)— GET withencodeURIComponent-encoded provider_id
frontend/src/components/admin/AdminAiConfigTab.vue extended:
- New
<section>"System AI Providers (Global)" placed above the existing per-user assignment table - Per-provider accordion (all 10 from PROVIDER_DEFAULTS)
- Write-only API key field (placeholder "(unchanged)" or "(not set)" — never pre-filled)
- Base URL, model name, context_chars inputs with PROVIDER_DEFAULTS as placeholders
- "Set Active", "Save", "Test Connection" buttons per provider
- Inline OK/Failed badges for test results (auto-clear after 3s)
- Existing
users,configs,saveConfig,providersvariables and per-user table left completely untouched (Pitfall 6 compliance)
frontend/tests/api.spec.js — 4 Vitest tests verifying fetch URL, method, headers, body, and encodeURIComponent for provider_id.
Task 3 — DocumentCard classification_failed badge
frontend/src/components/documents/DocumentCard.vue:
- Red pill badge
"Classification failed"whendoc.status === 'classification_failed' - "Re-analyze" button with
@click.stop(does not open doc on click) - On click: calls
classifyDocument(props.doc.id), emits'reclassified'upward - Spinner text "Re-analyzing…" while in flight; resets after 500ms
- Existing
v-if="doc.is_shared"block preserved unchanged
frontend/tests/DocumentCard.spec.js — 4 Vitest tests: badge renders for classification_failed, no badge for ready/processing, and reanalyze() calls classifyDocument with correct id and emits reclassified.
Test Results
- Backend: 369 passed, 6 skipped, 7 xfailed (1 pre-existing test_extractor.py ModuleNotFoundError excluded)
- Frontend: 131 passed (16 test files), build exits 0
Deviations from Plan
None — plan executed exactly as written. The existing AiConfigUpdate per-user model was renamed to UserAiConfigUpdate to avoid conflict with the new SystemAiConfigUpdate. This is a clean rename with no behavioral change (the PATCH /users/{user_id}/ai-config endpoint is unaffected).
Threat Mitigations Applied
| Threat ID | Mitigation |
|---|---|
| T-07-01 | _ai_config_to_dict whitelist; test asserts api_key_enc never in GET response |
| T-07-03 | Single UPDATE SET is_active = (provider_id = target) atomic flip; test asserts COUNT=1 |
| T-07-13 | extra="forbid" on SystemAiConfigUpdate; provider_id validated against PROVIDER_DEFAULTS |
| T-07-14 | Audit metadata contains only provider_id + fields_changed list, never api_key value |
| T-07-15 | All 3 endpoints carry Depends(get_current_admin); test asserts 403 for regular users |
Known Stubs
None — all features are fully wired.
Threat Flags
None — no new security-relevant surface beyond what is described in the plan's threat model.
Self-Check: PASSED
Files exist:
- backend/api/admin.py — contains
_ai_config_to_dict,SystemAiConfigUpdate,GET /ai-config,PUT /ai-config,test-connection - backend/services/ai_config.py — contains
load_provider_config_by_id - frontend/src/api/client.js — contains
getAiConfig,saveAiConfig,testAiConnection - frontend/src/components/admin/AdminAiConfigTab.vue — contains "System AI Providers"
- frontend/src/components/documents/DocumentCard.vue — contains
classification_failed,Re-analyze - frontend/tests/api.spec.js — exists
- frontend/tests/DocumentCard.spec.js — exists
Commits exist: