Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.8 KiB
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)
- 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.
- 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. - DB-only consumption — AI services read exclusively from the DB. The
Ai:ProviderandClaude/Geminikey/model values inappsettings.jsonbecome dead config (left inert;BaseUrl/AnthropicVersionare 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
xminoptimistic-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/geminiApiKeywith leave-unchanged semantics:nullor 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/GeminiExpenseAiServicestop depending onIOptions<ClaudeOptions>/IOptions<GeminiOptions>. Instead they obtain the active config from theChurchProfilerecord per request (via the existingIChurchProfileService/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: theExpenseAiController(or a small factory/resolver) inspectsChurchProfile.AiProvideron each request and dispatches to the matching service. Both services remain registered (AddHttpClient<...>()), but theIExpenseAiServicefixed-binding block based onAi:Provideris removed. BaseUrl/AnthropicVersionconstants 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-tabtitled AI 設定, guarded by the samePermissionModules.Settingscheck 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/valueFieldagainst 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.
- Provider — Kendo
ChurchProfileDto/UpdateChurchProfileRequest(TS, indisbursement.model.ts) andDisbursementApiServiceextend 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
claudeApiKeypreserves the existing stored key. - PUT with a non-empty
claudeApiKeyoverwrites it. - GET returns masked keys, never the raw value.
- Provider switch (
AiProvider) routes an assist request to the correct service.
- PUT with empty
- 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/Geminisections fromappsettings.json(left inert; can be cleaned up separately).