docs(07-05): add plan summary — Tasks 1-3 complete, Task 4 human checkpoint pending
- 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
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
---
|
||||
phase: 07-redo-and-optimize-llm-integration
|
||||
plan: "05"
|
||||
subsystem: ai-admin-frontend
|
||||
tags:
|
||||
- admin
|
||||
- ai-providers
|
||||
- frontend
|
||||
- classification-failed
|
||||
- vitest
|
||||
dependency_graph:
|
||||
requires:
|
||||
- 07-04 # Celery retry harness + re-queue endpoint
|
||||
provides:
|
||||
- GET/PUT /api/admin/ai-config
|
||||
- GET /api/admin/ai-config/test-connection
|
||||
- load_provider_config_by_id
|
||||
- AdminAiConfigTab System AI Providers section
|
||||
- DocumentCard classification_failed badge + Re-analyze button
|
||||
affects:
|
||||
- backend/api/admin.py
|
||||
- backend/services/ai_config.py
|
||||
- frontend/src/api/client.js
|
||||
- frontend/src/components/admin/AdminAiConfigTab.vue
|
||||
- frontend/src/components/documents/DocumentCard.vue
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "_ai_config_to_dict whitelist helper (mirrors _user_to_dict pattern)"
|
||||
- "atomic UPDATE SET is_active = (provider_id = target) for single-active invariant"
|
||||
- "write-only API key field (never pre-filled from server)"
|
||||
- "Vue accordion with formByProvider reactive map"
|
||||
key_files:
|
||||
created:
|
||||
- backend/tests/test_admin_ai_config.py # promoted from xfail stubs
|
||||
- frontend/tests/api.spec.js
|
||||
- frontend/tests/DocumentCard.spec.js
|
||||
modified:
|
||||
- backend/api/admin.py
|
||||
- backend/services/ai_config.py
|
||||
- frontend/src/api/client.js
|
||||
- frontend/src/components/admin/AdminAiConfigTab.vue
|
||||
- frontend/src/components/documents/DocumentCard.vue
|
||||
decisions:
|
||||
- "test-connection endpoint returns 200 ok=false for provider failures (not 5xx) so UI always gets structured status"
|
||||
- "GET /ai-config synthesises stub entries for all 10 PROVIDER_DEFAULTS providers even before any DB rows exist"
|
||||
- "api_key field in PUT: None=no change, ''=clear, non-empty=encrypt+store (three-way semantics)"
|
||||
- "AdminAiConfigTab PROVIDER_DEFAULTS mirrored in frontend JS — no /api/ai/defaults endpoint needed"
|
||||
metrics:
|
||||
duration: "~45 minutes"
|
||||
completed: "2026-06-04T21:23:15Z"
|
||||
tasks_completed: 3
|
||||
tasks_total: 3
|
||||
files_changed: 7
|
||||
---
|
||||
|
||||
# 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 excludes `api_key_enc` (T-07-01).
|
||||
- `SystemAiConfigUpdate(BaseModel)` — Pydantic model with `extra="forbid"` and a `provider_id` validator against `PROVIDER_DEFAULTS` keys (T-07-13).
|
||||
- `GET /api/admin/ai-config` — returns all 10 providers (DB rows + synthesised stubs from PROVIDER_DEFAULTS for unconfigured providers). Never exposes `api_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 single `UPDATE SET is_active = (provider_id = :target)`. Writes audit log with `fields_changed` only (T-07-14). Requires `get_current_admin` (T-07-15).
|
||||
- `GET /api/admin/ai-config/test-connection?provider_id=X` — calls `load_provider_config_by_id`, builds provider, calls `health_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` — asserts `api_key_enc` and plaintext key absent from GET response
|
||||
- `test_put_writes_active_provider` — asserts exactly 1 row with `is_active=True` after two PUTs
|
||||
- `test_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-config`
|
||||
- `saveAiConfig(body)` — PUT with JSON body
|
||||
- `testAiConnection(providerId)` — GET with `encodeURIComponent`-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`, `providers` variables 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"` when `doc.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:
|
||||
- e678930 — Task 1 (backend endpoints)
|
||||
- 0db412d — Task 2 (frontend client + AdminAiConfigTab)
|
||||
- c45d9e4 — Task 3 (DocumentCard + Vitest)
|
||||
Reference in New Issue
Block a user