diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index 47a26602..f36cd130 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -233,32 +233,8 @@ export function loadOrCreateUnifiedConfig(): UnifiedConfig { * Generate YAML header with helpful comments. */ function generateYamlHeader(): string { - return `# ============================================================================ -# CCS Unified Configuration (config.yaml) -# ============================================================================ -# Generated by: ccs migrate -# Documentation: https://github.com/kaitranntt/ccs -# -# This file references your settings - actual env vars are in *.settings.json -# files (matching Claude's ~/.claude/settings.json pattern). -# -# To customize a profile: -# 1. Edit the *.settings.json file directly (e.g., ~/.ccs/glm.settings.json) -# 2. The file format matches Claude's settings.json: { "env": { ... } }\n# -# Structure: -# ┌─────────────────────────────────────────────────────────────────────────────┐ -# │ profiles - References to *.settings.json files for API providers │ -# │ cliproxy - References to *.settings.json files for OAuth providers │ -# │ accounts - Isolated Claude instances (managed by 'ccs auth') │ -# │ preferences - User preferences (theme, telemetry, auto-update) │ -# └─────────────────────────────────────────────────────────────────────────────┘ -# -# Usage: -# ccs Switch to profile -# ccs api add Add new API profile -# ccs cliproxy create Create CLIProxy variant -# ccs migrate --rollback Restore from backup -# + return `# CCS Unified Configuration +# Docs: https://github.com/kaitranntt/ccs `; } diff --git a/ui/src/pages/settings/sections/auth-section.tsx b/ui/src/pages/settings/sections/auth-section.tsx index 07e070a7..5d1c9772 100644 --- a/ui/src/pages/settings/sections/auth-section.tsx +++ b/ui/src/pages/settings/sections/auth-section.tsx @@ -20,6 +20,7 @@ import { Check, KeyRound, ShieldCheck, + Save, } from 'lucide-react'; import { useRawConfig } from '../hooks'; @@ -92,54 +93,35 @@ export default function AuthSection() { } }, [error]); - // Save API key - const saveApiKey = async () => { - if (editedApiKey === null) return; + // Save all changes + const saveChanges = async () => { + const hasApiKeyChange = editedApiKey !== null && editedApiKey !== tokens?.apiKey.value; + const hasSecretChange = + editedSecret !== null && editedSecret !== tokens?.managementSecret.value; + + if (!hasApiKeyChange && !hasSecretChange) return; try { setSaving(true); setError(null); + + const payload: { apiKey?: string; managementSecret?: string } = {}; + if (hasApiKeyChange) payload.apiKey = editedApiKey; + if (hasSecretChange) payload.managementSecret = editedSecret; + const response = await fetch('/api/settings/auth/tokens', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ apiKey: editedApiKey }), + body: JSON.stringify(payload), }); if (!response.ok) { const data = await response.json(); - throw new Error(data.error || 'Failed to save API key'); + throw new Error(data.error || 'Failed to save tokens'); } - setSuccess('API key updated. Restart CLIProxy to apply.'); + setSuccess('Tokens updated. Restart CLIProxy to apply.'); setEditedApiKey(null); - await fetchTokens(); - await fetchRawConfig(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setSaving(false); - } - }; - - // Save management secret - const saveSecret = async () => { - if (editedSecret === null) return; - - try { - setSaving(true); - setError(null); - const response = await fetch('/api/settings/auth/tokens', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ managementSecret: editedSecret }), - }); - - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || 'Failed to save management secret'); - } - - setSuccess('Management secret updated. Restart CLIProxy to apply.'); setEditedSecret(null); await fetchTokens(); await fetchRawConfig(); @@ -230,6 +212,11 @@ export default function AuthSection() { const displayApiKey = editedApiKey ?? tokens.apiKey.value; const displaySecret = editedSecret ?? tokens.managementSecret.value; + // Check for unsaved changes + const hasChanges = + (editedApiKey !== null && editedApiKey !== tokens.apiKey.value) || + (editedSecret !== null && editedSecret !== tokens.managementSecret.value); + return ( <> {/* Toast-style alerts */} @@ -281,13 +268,6 @@ export default function AuthSection() { type={showApiKey ? 'text' : 'password'} value={displayApiKey} onChange={(e) => setEditedApiKey(e.target.value)} - onBlur={() => { - if (editedApiKey !== null && editedApiKey !== tokens.apiKey.value) { - saveApiKey(); - } else { - setEditedApiKey(null); - } - }} placeholder="API key" disabled={saving} className="pr-20 font-mono text-sm" @@ -339,13 +319,6 @@ export default function AuthSection() { type={showSecret ? 'text' : 'password'} value={displaySecret} onChange={(e) => setEditedSecret(e.target.value)} - onBlur={() => { - if (editedSecret !== null && editedSecret !== tokens.managementSecret.value) { - saveSecret(); - } else { - setEditedSecret(null); - } - }} placeholder="Management secret" disabled={saving} className="pr-20 font-mono text-sm" @@ -406,7 +379,7 @@ export default function AuthSection() { {/* Footer */} -
+
+
);