# Church Profile — AI Settings Tab **Date:** 2026-06-25 **Status:** Approved (ready for implementation plan) ## Goal Move AI provider configuration (provider, model, API token) out of `appsettings.json` and into the `ChurchProfile` DB record, editable through a new **AI 設定** tab on the Church Profile page. The AI expense-assist services read their configuration from the database at request time instead of from startup `IOptions`. ## Decisions (from brainstorming) 1. **Per-provider config** — store Claude *and* Gemini settings side by side; a provider selector chooses which is active. Switching providers does not lose the other's key. 2. **Secret handling: plaintext storage + masked read** — keys stored as-is in the DB (same exposure model as today's `appsettings.Development.json`); the GET endpoint never returns the raw key to the browser. 3. **DB-only consumption** — AI services read exclusively from the DB. The `Ai:Provider` and `Claude`/`Gemini` key/model values in `appsettings.json` become dead config (left inert; `BaseUrl`/`AnthropicVersion` are the exception — see below). ## 1. Data model — new columns on `ChurchProfile` The singleton `ChurchProfile` entity (Id == 1) gains: | Column | Type | Notes | |----------------|-----------|----------------------------------------| | `AiProvider` | `string` | `"Claude"` \| `"Gemini"` — active provider | | `ClaudeModel` | `string?` | | | `ClaudeApiKey` | `string?` | Secret, stored plaintext | | `GeminiModel` | `string?` | | | `GeminiApiKey` | `string?` | Secret, stored plaintext | `BaseUrl` (Claude + Gemini) and `AnthropicVersion` (Claude) remain **constants in the service code** — they effectively never change and stay out of the UI and DB to keep both focused on the three editable values (provider, model, key). - An EF migration adds the columns. - The singleton seed sets default values so AI keeps working before anyone opens the tab: `AiProvider = "Claude"`, `ClaudeModel = "claude-haiku-4-5-20251001"`, `GeminiModel = "gemini-2.5-flash-lite"`, keys empty. - Columns participate in the existing `xmin` optimistic-concurrency token already on the entity. ## 2. Secret handling — masked read **GET `/api/church-profile`** (`ChurchProfileDto`): - Adds `aiProvider`, `claudeModel`, `geminiModel`. - Adds `claudeApiKeyMasked` / `geminiApiKeyMasked` — a masked display string (e.g. `••••••1234`, or empty string when unset). The raw key is **never** serialized. **PUT `/api/church-profile`** (`UpdateChurchProfileRequest`): - Adds `aiProvider`, `claudeModel`, `geminiModel`. - Adds nullable `claudeApiKey` / `geminiApiKey` with **leave-unchanged semantics**: - `null` or empty string → keep the existing stored key. - non-empty → overwrite the stored key. - Clearing an existing key is **out of scope for v1** (easy follow-up if needed). Masking helper: show the last 4 chars prefixed with bullets; for keys shorter than 4 chars, mask entirely. ## 3. AI services read from DB (DB-only) - `ClaudeExpenseAiService` / `GeminiExpenseAiService` stop depending on `IOptions` / `IOptions`. Instead they obtain the active config from the `ChurchProfile` record **per request** (via the existing `IChurchProfileService` / `AppDbContext`, behind a small accessor so the AI base class stays provider-agnostic). - **Runtime provider selection** replaces the boot-time DI binding in `Program.cs`: the `ExpenseAiController` (or a small factory/resolver) inspects `ChurchProfile.AiProvider` on each request and dispatches to the matching service. Both services remain registered (`AddHttpClient<...>()`), but the `IExpenseAiService` fixed-binding block based on `Ai:Provider` is removed. - `BaseUrl` / `AnthropicVersion` constants live in the respective service classes. - Empty key → service returns a null suggestion (unchanged "AI disabled" behavior). ## 4. Frontend — "AI 設定" tab Location: `APP/src/app/features/disbursement/pages/church-profile-page/`. - Add a 4th `kendo-tabstrip-tab` titled **AI 設定**, guarded by the same `PermissionModules.Settings` check used by the Site Settings / Notifications tabs. - Form (Tailwind grid `grid grid-cols-1 md:grid-cols-2`, layout via utilities not SCSS): - **Provider** — Kendo `DropdownList` (Claude / Gemini), `textField`/`valueField` against an options array, with `[valuePrimitive]="true"`. - **Claude** group: Model (text input) + API Key (password input). - **Gemini** group: Model (text input) + API Key (password input). - Key inputs display the masked value as placeholder; typing a new value replaces it, leaving it blank keeps the stored key. - `ChurchProfileDto` / `UpdateChurchProfileRequest` (TS, in `disbursement.model.ts`) and `DisbursementApiService` extend with the new fields. **No new endpoint** — reuses the existing GET/PUT. - Mobile-friendly per standing rule; the form grid already collapses to one column. ## 5. Testing - Backend service tests: - PUT with empty `claudeApiKey` preserves the existing stored key. - PUT with a non-empty `claudeApiKey` overwrites it. - GET returns masked keys, never the raw value. - Provider switch (`AiProvider`) routes an assist request to the correct service. - Frontend: extend existing church-profile component coverage where present (inline templates per the unit-test runner constraint). ## Out of scope (v1) - Encryption-at-rest for keys (chose plaintext + masked read). - Editable `BaseUrl` / `AnthropicVersion` (kept as code constants). - Clearing a stored key from the UI. - Removing the now-dead `Ai` / `Claude` / `Gemini` sections from `appsettings.json` (left inert; can be cleaned up separately).