Files

568 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../../../scripts/app.js";
import { DoctorAPI } from "../doctor_api.js";
import { getRuntimeApiKey, setRuntimeApiKey } from "../llm_key_store.js";
import { SUPPORTED_LANGUAGES, getDoctorRuntimeSettings, getDoctorSetting, setDoctorSetting } from "../comfyui_frontend_compat.js";
import { LLM_PROVIDER_OPTIONS } from "../llm_provider_quick_switch.js";
export function render(container) {
const doctorUI = app.Doctor;
// One-time CSS injection for small UI helpers (tooltips etc.)
if (!document.getElementById('doctor-settings-tip-styles')) {
const style = document.createElement('style');
style.id = 'doctor-settings-tip-styles';
style.textContent = `
.doctor-tip {
position: relative;
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.doctor-tip-icon {
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid #555;
background: #222;
color: #ddd;
font-size: 12px;
line-height: 16px;
padding: 0;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
}
.doctor-tip-icon:hover {
border-color: #666;
background: #2a2a2a;
color: #fff;
}
.doctor-tip-popover {
display: none;
position: absolute;
right: 0;
top: 22px;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 6px;
padding: 10px;
width: 280px;
max-width: 280px;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.doctor-tip:hover .doctor-tip-popover,
.doctor-tip:focus-within .doctor-tip-popover {
display: block;
}
.doctor-tip-link {
display: block;
color: #7db7ff;
text-decoration: underline;
overflow-wrap: anywhere;
word-break: normal;
font-size: 12px;
}
`;
document.head.appendChild(style);
}
// Get current settings values
const {
language: currentLanguage,
provider: currentProvider,
baseUrl: currentBaseUrl,
model: currentModel,
privacyMode: currentPrivacyMode,
autoOpenOnError: currentAutoOpenOnError,
} = getDoctorRuntimeSettings(app);
// S8: API key is session-only; never read from persisted ComfyUI settings.
const currentApiKey = getRuntimeApiKey();
const settingsPanel = document.createElement('div');
settingsPanel.id = 'doctor-settings-panel';
// Remove 'display: none' and 'background' styling that was for the collapsible panel
settingsPanel.style.cssText = `
padding: 15px;
height: 100%;
overflow-y: auto;
`;
// S8: Migration notice — show once per session when legacy key was imported
const migrationNotice = (app.Doctor && app.Doctor.legacyApiKeyMigrated)
? `<div style="padding: 10px; margin-bottom: 15px; background: rgba(33,150,243,0.12); border: 1px solid #2196f3; border-radius: 6px; font-size: 12px; color: #90caf9; line-height: 1.5;">
<strong>${doctorUI.getUIText('api_key_migrated_title') || 'ℹ️ API Key Migrated'}</strong><br>
${doctorUI.getUIText('api_key_migrated_notice') || 'Your previously saved API key has been moved to session memory and cleared from stored settings.<br>For permanent storage, use <code>DOCTOR_LLM_API_KEY</code> environment variable or the <strong>Advanced Key Store</strong> below.'}
</div>`
: '';
settingsPanel.innerHTML = `
${migrationNotice}
<h4 style="margin: 0 0 20px 0; font-size: 16px; color: #ddd; border-bottom: 1px solid #444; padding-bottom: 10px;">${doctorUI.getUIText('settings_title') || 'Settings'}</h4>
<div style="display: flex; flex-direction: column; gap: 15px;">
<div>
<label style="display: block; font-size: 13px; color: #aaa; margin-bottom: 5px;">${doctorUI.getUIText('language_label')}</label>
<select id="doctor-language-select" style="width: 100%; padding: 8px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 13px;">
${SUPPORTED_LANGUAGES.map(lang =>
`<option value="${lang.value}" ${lang.value === currentLanguage ? 'selected' : ''}>${lang.text}</option>`
).join('')}
</select>
</div>
<div>
<label style="display: block; font-size: 13px; color: #aaa; margin-bottom: 5px;">${doctorUI.getUIText('ai_provider_label')}</label>
<select id="doctor-provider-select" style="width: 100%; padding: 8px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 13px;">
${LLM_PROVIDER_OPTIONS.map(provider =>
`<option value="${provider.value}" ${currentProvider === provider.value ? 'selected' : ''}>${provider.label}</option>`
).join('')}
</select>
</div>
<div>
<label style="display: block; font-size: 13px; color: #aaa; margin-bottom: 5px;">${doctorUI.getUIText('base_url_label')}</label>
<input type="text" id="doctor-baseurl-input" value="${currentBaseUrl}" style="width: 100%; padding: 8px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 13px; box-sizing: border-box;" />
</div>
<div>
<label style="display: flex; align-items: center; justify-content: space-between; font-size: 13px; color: #aaa; margin-bottom: 5px;">
<span>${doctorUI.getUIText('api_key_label')}</span>
<span class="doctor-tip">
<button type="button" class="doctor-tip-icon" aria-label="API key tip">?</button>
<div class="doctor-tip-popover" role="tooltip">
<div style="font-size: 12px; color: #ddd; line-height: 1.4; margin-bottom: 6px;">${doctorUI.getUIText('api_key_tip')}</div>
<a class="doctor-tip-link" href="https://github.com/rookiestar28/ComfyUI-Doctor" target="_blank" rel="noopener noreferrer">https://github.com/rookiestar28/ComfyUI-Doctor</a>
</div>
</span>
</label>
<input type="password" id="doctor-apikey-input" value="${currentApiKey}" placeholder="${doctorUI.getUIText('api_key_placeholder')}" style="width: 100%; padding: 8px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 13px; box-sizing: border-box;" />
<div style="font-size: 11px; color: #888; margin-top: 4px;">${doctorUI.getUIText('api_key_session_only_hint') || '⚡ Session-only — cleared on reload. Use Advanced Key Store below to persist.'}</div>
</div>
<details id="doctor-key-store-section" style="border-top: 1px solid #444; padding-top: 12px; margin-top: -3px;">
<summary style="cursor: pointer; font-size: 13px; color: #aaa; user-select: none;">${doctorUI.getUIText('keystore_title') || '🔐 Advanced Key Store (Server-side)'}</summary>
<div style="margin-top: 10px; padding: 10px; background: #1a1a1a; border-radius: 6px; border: 1px solid #333;">
<div style="font-size: 11px; color: #f0ad4e; margin-bottom: 10px; line-height: 1.4; padding: 8px; background: rgba(240,173,78,0.1); border-radius: 4px;">
${doctorUI.getUIText('keystore_warning') || '⚠️ Server store saves keys as plaintext JSON on disk (protected by OS file permissions).<br>ENV vars always take higher priority. Use only in trusted localhost/single-user environments.'}
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div>
<label style="display: block; font-size: 12px; color: #888; margin-bottom: 4px;">${doctorUI.getUIText('keystore_provider_label') || 'Provider'}</label>
<select id="doctor-keystore-provider" style="width: 100%; padding: 6px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 12px;">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="deepseek">DeepSeek</option>
<option value="groq">Groq</option>
<option value="gemini">Google Gemini</option>
<option value="xai">xAI</option>
<option value="openrouter">OpenRouter</option>
<option value="generic">${doctorUI.getUIText('keystore_provider_generic_option') || 'Generic (fallback)'}</option>
</select>
</div>
<div>
<label style="display: block; font-size: 12px; color: #888; margin-bottom: 4px;">${doctorUI.getUIText('keystore_api_key_label') || 'API Key'}</label>
<input type="password" id="doctor-keystore-key" placeholder="sk-..." style="width: 100%; padding: 6px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 12px; box-sizing: border-box;" />
</div>
<div>
<label style="display: block; font-size: 12px; color: #888; margin-bottom: 4px;">${doctorUI.getUIText('keystore_admin_token_label') || 'Admin Token (if configured)'}</label>
<input type="password" id="doctor-keystore-admin" placeholder="${doctorUI.getUIText('keystore_admin_token_placeholder') || 'optional'}" style="width: 100%; padding: 6px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 12px; box-sizing: border-box;" />
</div>
<div style="display: flex; gap: 8px;">
<button id="doctor-keystore-save-btn" style="flex: 1; padding: 8px; background: #2196f3; border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">${doctorUI.getUIText('keystore_save_btn') || '💾 Save to Server'}</button>
<button id="doctor-keystore-delete-btn" style="flex: 1; padding: 8px; background: #e53935; border: none; border-radius: 4px; color: white; cursor: pointer; font-size: 12px;">${doctorUI.getUIText('keystore_delete_btn') || '🗑️ Delete'}</button>
</div>
<div id="doctor-keystore-status" style="font-size: 11px; color: #888; margin-top: 4px;"></div>
<div id="doctor-keystore-providers-grid" style="margin-top: 8px;"></div>
</div>
</div>
</details>
<div>
<label style="display: block; font-size: 13px; color: #aaa; margin-bottom: 5px;">${doctorUI.getUIText('model_name_label')}</label>
<div style="display: flex; gap: 5px; align-items: center;">
<select id="doctor-model-select" style="flex: 1; padding: 8px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 13px; box-sizing: border-box;">
<option value="">${doctorUI.getUIText('loading_models')}</option>
</select>
<button id="doctor-refresh-models-btn" style="padding: 8px 12px; background: #333; border: 1px solid #444; border-radius: 4px; color: #eee; cursor: pointer; font-size: 13px;" title="Refresh model list">🔄</button>
</div>
<div style="margin-top: 5px;">
<label style="font-size: 12px; color: #888; cursor: pointer;">
<input type="checkbox" id="doctor-manual-model-toggle" style="margin-right: 3px;">
${doctorUI.getUIText('enter_model_manually')}
</label>
</div>
<input type="text" id="doctor-model-input-manual" value="${currentModel}" placeholder="${doctorUI.getUIText('model_manual_placeholder')}" style="width: 100%; padding: 8px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 13px; box-sizing: border-box; margin-top: 5px; display: none;" />
</div>
<div style="border-top: 1px solid #444; padding-top: 15px; margin-top: 5px;">
<label style="display: flex; align-items: center; gap: 5px; font-size: 13px; color: #aaa; margin-bottom: 5px;">
🔒 <span id="doctor-privacy-label">${doctorUI.getUIText('privacy_mode')}</span>
</label>
<select id="doctor-privacy-select" style="width: 100%; padding: 8px; background: #111; border: 1px solid #444; border-radius: 4px; color: #eee; font-size: 13px;">
<option value="none" ${currentPrivacyMode === 'none' ? 'selected' : ''} id="privacy-none-option">${doctorUI.getUIText('privacy_mode_none')}</option>
<option value="basic" ${currentPrivacyMode === 'basic' ? 'selected' : ''} id="privacy-basic-option">${doctorUI.getUIText('privacy_mode_basic')}</option>
<option value="strict" ${currentPrivacyMode === 'strict' ? 'selected' : ''} id="privacy-strict-option">${doctorUI.getUIText('privacy_mode_strict')}</option>
</select>
<div id="doctor-privacy-hint" style="font-size: 12px; color: #888; margin-top: 5px;">${doctorUI.getUIText('privacy_mode_hint')}</div>
</div>
<div style="border-top: 1px solid #444; padding-top: 15px; margin-top: 5px;">
<label style="display: flex; align-items: center; gap: 8px; font-size: 13px; color: #aaa; cursor: pointer;">
<input type="checkbox" id="doctor-auto-open-toggle" ${currentAutoOpenOnError ? 'checked' : ''} style="width: 16px; height: 16px; cursor: pointer;">
<span>${doctorUI.getUIText('auto_open_on_error_label') || 'Auto-open error report panel on new errors'}</span>
</label>
<div style="font-size: 12px; color: #888; margin-top: 5px; margin-left: 24px;">${doctorUI.getUIText('auto_open_on_error_hint') || 'When enabled, the right-side error report panel will automatically open when a new error is detected'}</div>
</div>
<button id="doctor-save-settings-btn" style="width: 100%; padding: 10px; background: #4caf50; border: none; border-radius: 4px; color: white; font-weight: bold; cursor: pointer; font-size: 14px; margin-top: 10px;">💾 ${doctorUI.getUIText('save_settings_btn')}</button>
</div>
`;
container.appendChild(settingsPanel);
// --- Interaction Logic ---
const providerSelect = settingsPanel.querySelector('#doctor-provider-select');
const baseUrlInput = settingsPanel.querySelector('#doctor-baseurl-input');
const modelSelect = settingsPanel.querySelector('#doctor-model-select');
const refreshModelsBtn = settingsPanel.querySelector('#doctor-refresh-models-btn');
const manualToggle = settingsPanel.querySelector('#doctor-manual-model-toggle');
const modelInputManual = settingsPanel.querySelector('#doctor-model-input-manual');
const apiKeyInput = settingsPanel.querySelector('#doctor-apikey-input');
// Load models logic
const loadModels = async () => {
const baseUrl = baseUrlInput.value;
const apiKey = apiKeyInput.value;
if (!baseUrl) {
modelSelect.innerHTML = '<option value="">Please set Base URL first</option>';
return;
}
refreshModelsBtn.textContent = '⏳';
refreshModelsBtn.disabled = true;
modelSelect.disabled = true;
try {
const result = await DoctorAPI.listModels(baseUrl, apiKey);
if (result.success && result.models.length > 0) {
modelSelect.innerHTML = '';
result.models.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name;
modelSelect.appendChild(option);
});
// Restore selection
// Logic: if manual is checked, we don't care about dropdown value.
// But if we toggle back, we might want current value.
// Also handle "currentModel not in list"
// Get fresh currentModel from settings in case it changed via manual input previously
const storedModel = getDoctorSetting("Doctor.LLM.Model", "", app);
if (storedModel) {
const modelExists = result.models.find(m => m.id === storedModel);
if (modelExists) {
modelSelect.value = storedModel;
} else {
const option = document.createElement('option');
option.value = storedModel;
option.textContent = `${storedModel} (current)`;
option.selected = true;
modelSelect.insertBefore(option, modelSelect.firstChild);
}
}
} else {
modelSelect.innerHTML = '<option value="">No models found - use manual input</option>';
}
} catch (error) {
console.error('[ComfyUI-Doctor] Failed to load models:', error);
modelSelect.innerHTML = '<option value="">Error loading models - use manual input</option>';
} finally {
refreshModelsBtn.textContent = '🔄';
refreshModelsBtn.disabled = false;
modelSelect.disabled = false;
}
};
refreshModelsBtn.onclick = () => loadModels();
manualToggle.onchange = () => {
const storedModel = getDoctorSetting("Doctor.LLM.Model", "", app);
if (manualToggle.checked) {
modelSelect.style.display = 'none';
refreshModelsBtn.style.display = 'none';
modelInputManual.style.display = 'block';
modelInputManual.value = modelSelect.value || storedModel;
} else {
modelSelect.style.display = 'block';
refreshModelsBtn.style.display = 'block';
modelInputManual.style.display = 'none';
}
};
// Provider Defaults access (assuming stored in doctorUI or passed in somehow)
// We will access app.Doctor.providerDefaults if available
providerSelect.onchange = () => {
const defaults = doctorUI.providerDefaults || {};
baseUrlInput.value = defaults[providerSelect.value] || "";
if (baseUrlInput.value && !manualToggle.checked) {
loadModels();
}
};
// Auto-load
if (currentBaseUrl && !manualToggle.checked) {
loadModels();
}
// Privacy Mode Translations
const privacySelect = settingsPanel.querySelector('#doctor-privacy-select');
const updatePrivacyTranslations = (lang) => {
// We can fetch from API or rely on getUIText if we refresh UI text.
// doctorUI.loadUIText() reloads and updates this.uiText.
// We can subscribe to that or just re-render?
// doctor.js implementation fetched directly.
fetch(`/doctor/ui_text?lang=${lang}`)
.then(res => res.json())
.then(text => {
const map = text.text || text; // Handle structure
const pLabel = settingsPanel.querySelector('#doctor-privacy-label');
const pHint = settingsPanel.querySelector('#doctor-privacy-hint');
const opts = {
none: settingsPanel.querySelector('#privacy-none-option'),
basic: settingsPanel.querySelector('#privacy-basic-option'),
strict: settingsPanel.querySelector('#privacy-strict-option')
};
if (map.privacy_mode) pLabel.textContent = map.privacy_mode;
if (map.privacy_mode_hint) pHint.textContent = map.privacy_mode_hint;
if (map.privacy_mode_none) opts.none.textContent = map.privacy_mode_none;
if (map.privacy_mode_basic) opts.basic.textContent = map.privacy_mode_basic;
if (map.privacy_mode_strict) opts.strict.textContent = map.privacy_mode_strict;
})
.catch(e => console.error(e));
};
const saveBtn = settingsPanel.querySelector('#doctor-save-settings-btn');
const autoOpenToggle = settingsPanel.querySelector('#doctor-auto-open-toggle');
saveBtn.onclick = async () => {
const langSelect = settingsPanel.querySelector('#doctor-language-select');
const modelValue = manualToggle.checked ? modelInputManual.value : modelSelect.value;
const providerVal = providerSelect.value;
const baseUrlVal = baseUrlInput.value;
const apiKeyVal = apiKeyInput.value;
const privacyVal = privacySelect.value;
const autoOpenVal = autoOpenToggle.checked;
try {
setDoctorSetting("Doctor.General.Language", langSelect.value, app);
setDoctorSetting("Doctor.LLM.Provider", providerVal, app);
setDoctorSetting("Doctor.LLM.BaseUrl", baseUrlVal, app);
// S8: API key is session-only — store in memory, NOT in ComfyUI settings.
setRuntimeApiKey(apiKeyVal);
setDoctorSetting("Doctor.LLM.Model", modelValue, app);
setDoctorSetting("Doctor.Privacy.Mode", privacyVal, app);
// F17: Save auto-open setting and apply immediately
setDoctorSetting("Doctor.Behavior.AutoOpenOnError", autoOpenVal, app);
doctorUI.autoOpenOnError = autoOpenVal;
// Reload UI Text for new language (which also updates privacy text eventually)
// doctorUI.updateUILanguage() -> will update info card etc.
// We need to trigger language sync.
await DoctorAPI.setLanguage(langSelect.value);
// Manually update privacy fields now for immediate feedback or rely on reload?
updatePrivacyTranslations(langSelect.value);
// Ideally, reload the whole doctor UI or re-fetch uiText.
// doctorUI.loadUIText() handles it.
// Since doctorUI.language is what it uses, update it.
doctorUI.language = langSelect.value;
await doctorUI.loadUIText(); // Reloads and updates UI
// Visual Feedback
const originalText = saveBtn.textContent;
saveBtn.textContent = '✅ Saved, please refresh the UI to apply changes';
setTimeout(() => {
saveBtn.textContent = originalText;
}, 4000);
} catch (e) {
console.error('Failed to save settings:', e);
saveBtn.textContent = '❌ Error';
setTimeout(() => {
saveBtn.textContent = '💾 Save Settings';
}, 2000);
}
};
// ---- S8: Advanced Key Store Interaction ----
const keystoreSection = settingsPanel.querySelector('#doctor-key-store-section');
const keystoreProviderSelect = settingsPanel.querySelector('#doctor-keystore-provider');
const keystoreKeyInput = settingsPanel.querySelector('#doctor-keystore-key');
const keystoreAdminInput = settingsPanel.querySelector('#doctor-keystore-admin');
const keystoreSaveBtn = settingsPanel.querySelector('#doctor-keystore-save-btn');
const keystoreDeleteBtn = settingsPanel.querySelector('#doctor-keystore-delete-btn');
const keystoreStatus = settingsPanel.querySelector('#doctor-keystore-status');
const keystoreGrid = settingsPanel.querySelector('#doctor-keystore-providers-grid');
const SOURCE_BADGES = {
env: { label: 'ENV', color: '#4caf50', bg: 'rgba(76,175,80,0.15)' },
server_store: { label: 'Server', color: '#2196f3', bg: 'rgba(33,150,243,0.15)' },
none: { label: 'None', color: '#888', bg: 'rgba(136,136,136,0.1)' },
};
const replaceWithPlainText = (container, text, { tagName = 'span', color, fontSize } = {}) => {
if (!container) return null;
const element = document.createElement(tagName);
if (color) element.style.color = color;
if (fontSize) element.style.fontSize = fontSize;
// SECURITY: backend, exception, provider, and localized status values stay text-only.
element.textContent = String(text ?? '');
container.replaceChildren(element);
return element;
};
const renderKeystoreStatus = (text, color) => {
replaceWithPlainText(keystoreStatus, text, { color });
};
const renderProvidersGrid = (providers) => {
if (!keystoreGrid || !providers) return;
const entries = Object.entries(providers);
if (!entries.length) {
replaceWithPlainText(
keystoreGrid,
doctorUI.getUIText('keystore_no_providers') || 'No providers configured.',
{ tagName: 'div', color: '#666', fontSize: '11px' }
);
return;
}
const title = document.createElement('div');
title.style.fontSize = '11px';
title.style.color = '#aaa';
title.style.marginBottom = '4px';
title.textContent = String(doctorUI.getUIText('keystore_provider_status') || 'Provider Status:');
const grid = document.createElement('div');
grid.style.display = 'grid';
grid.style.gridTemplateColumns = '1fr 1fr';
grid.style.gap = '4px';
for (const [id, info] of entries) {
const badge = SOURCE_BADGES[info?.source] || SOURCE_BADGES.none;
const row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '4px';
row.style.padding = '3px 6px';
row.style.background = badge.bg;
row.style.borderRadius = '3px';
row.style.fontSize = '11px';
const providerName = document.createElement('span');
providerName.style.color = '#ccc';
providerName.textContent = String(id);
const sourceBadge = document.createElement('span');
sourceBadge.style.color = badge.color;
sourceBadge.style.fontWeight = '600';
sourceBadge.textContent = badge.label;
row.append(providerName, sourceBadge);
grid.appendChild(row);
}
keystoreGrid.replaceChildren(title, grid);
};
const loadKeystoreStatus = async () => {
try {
const adminToken = keystoreAdminInput?.value?.trim() || '';
const result = await DoctorAPI.getSecretsStatus(adminToken);
if (result.success) {
renderProvidersGrid(result.providers);
} else {
replaceWithPlainText(
keystoreGrid,
`Failed: ${result.error || result.message || 'unknown'}`,
{ tagName: 'div', color: '#e53935', fontSize: '11px' }
);
}
} catch (e) {
replaceWithPlainText(
keystoreGrid,
`Error: ${e?.message ?? e}`,
{ tagName: 'div', color: '#e53935', fontSize: '11px' }
);
}
};
// Auto-load status when section is opened
if (keystoreSection) {
keystoreSection.addEventListener('toggle', () => {
if (keystoreSection.open) loadKeystoreStatus();
});
}
if (keystoreSaveBtn) {
keystoreSaveBtn.onclick = async () => {
const provider = keystoreProviderSelect?.value;
const key = keystoreKeyInput?.value?.trim();
const adminToken = keystoreAdminInput?.value?.trim() || '';
if (!provider || !key) {
renderKeystoreStatus(
doctorUI.getUIText('keystore_status_required') || 'Provider and API Key are required.',
'#f0ad4e'
);
return;
}
keystoreSaveBtn.disabled = true;
keystoreSaveBtn.textContent = doctorUI.getUIText('keystore_saving_btn') || '⏳ Saving...';
try {
const result = await DoctorAPI.saveSecret(provider, key, adminToken);
if (result.success) {
const successText = doctorUI.getUIText('keystore_save_success') || '✅ Saved {0} key to server.';
renderKeystoreStatus(successText.replace('{0}', provider), '#4caf50');
keystoreKeyInput.value = '';
await loadKeystoreStatus();
} else {
renderKeystoreStatus(
`❌ ${result.error || result.message || doctorUI.getUIText('keystore_save_failed') || 'Save failed'}`,
'#e53935'
);
}
} catch (e) {
renderKeystoreStatus(`❌ ${e?.message ?? e}`, '#e53935');
}
keystoreSaveBtn.disabled = false;
keystoreSaveBtn.textContent = doctorUI.getUIText('keystore_save_btn') || '💾 Save to Server';
};
}
if (keystoreDeleteBtn) {
keystoreDeleteBtn.onclick = async () => {
const provider = keystoreProviderSelect?.value;
const adminToken = keystoreAdminInput?.value?.trim() || '';
if (!provider) return;
const confirmMsg = doctorUI.getUIText('keystore_delete_confirm') || 'Delete server-stored key for "{0}"?';
if (!confirm(confirmMsg.replace('{0}', provider))) return;
keystoreDeleteBtn.disabled = true;
keystoreDeleteBtn.textContent = doctorUI.getUIText('keystore_deleting_btn') || '⏳ Deleting...';
try {
const result = await DoctorAPI.clearSecret(provider, adminToken);
if (result.success) {
const successText = doctorUI.getUIText('keystore_delete_success') || '✅ Deleted {0} key.';
renderKeystoreStatus(successText.replace('{0}', provider), '#4caf50');
await loadKeystoreStatus();
} else {
renderKeystoreStatus(
`❌ ${result.error || result.message || doctorUI.getUIText('keystore_delete_failed') || 'Delete failed'}`,
'#e53935'
);
}
} catch (e) {
renderKeystoreStatus(`❌ ${e?.message ?? e}`, '#e53935');
}
keystoreDeleteBtn.disabled = false;
keystoreDeleteBtn.textContent = doctorUI.getUIText('keystore_delete_btn') || '🗑️ Delete';
};
}
}