# 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<ClaudeOptions>` / `IOptions<GeminiOptions>`. 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).
