Fix admin model management: route ordering, LiteLLM built-ins, auto-select
- Root cause: PUT /config/:key(*) wildcard was registered before /config/models/toggle and /config/models/default, intercepting them and returning "value is required" (body had modelId not value) - Fix: move all model-specific PUT routes before the wildcard - LiteLLM: return empty built-in list with discovery hint (hardcoded models don't match user's proxy — must use Search API) - After adding a discovered model: auto-select it in the default dropdown - GET /config/models now returns defaultModel so dropdown pre-selects it
This commit is contained in:
parent
9ec4cbf6b1
commit
e4daa7590c
2 changed files with 113 additions and 64 deletions
|
|
@ -616,7 +616,7 @@
|
||||||
var badge = document.getElementById('admin-model-provider-badge');
|
var badge = document.getElementById('admin-model-provider-badge');
|
||||||
if (badge) badge.textContent = (data.provider || 'unknown').toUpperCase();
|
if (badge) badge.textContent = (data.provider || 'unknown').toUpperCase();
|
||||||
|
|
||||||
// Default model selector
|
// Default model selector — built-ins + custom models
|
||||||
var defaultSel = document.getElementById('admin-default-model');
|
var defaultSel = document.getElementById('admin-default-model');
|
||||||
if (defaultSel) {
|
if (defaultSel) {
|
||||||
defaultSel.innerHTML = '';
|
defaultSel.innerHTML = '';
|
||||||
|
|
@ -626,20 +626,25 @@
|
||||||
opt.textContent = m.name + (m.enabled === false ? ' (disabled)' : '');
|
opt.textContent = m.name + (m.enabled === false ? ' (disabled)' : '');
|
||||||
defaultSel.appendChild(opt);
|
defaultSel.appendChild(opt);
|
||||||
});
|
});
|
||||||
// Also add custom models
|
|
||||||
(data.custom || []).forEach(function(m) {
|
(data.custom || []).forEach(function(m) {
|
||||||
var opt = document.createElement('option');
|
var opt = document.createElement('option');
|
||||||
opt.value = m.id;
|
opt.value = m.id;
|
||||||
opt.textContent = m.name + ' (custom)';
|
opt.textContent = m.name;
|
||||||
defaultSel.appendChild(opt);
|
defaultSel.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
// Pre-select the saved default
|
||||||
|
if (data.defaultModel) defaultSel.value = data.defaultModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Built-in models with toggle
|
// Built-in models with toggle
|
||||||
var container = document.getElementById('admin-builtin-models');
|
var container = document.getElementById('admin-builtin-models');
|
||||||
if (container) {
|
if (container) {
|
||||||
if (data.models.length === 0) {
|
if (data.litellmHint) {
|
||||||
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No built-in models for this provider</p>';
|
container.innerHTML = '<p style="color:var(--g500);font-size:13px;padding:8px;background:var(--g50);border-radius:6px;">' +
|
||||||
|
'<i class="fas fa-info-circle"></i> LiteLLM has no built-in model list — your available models depend on your proxy configuration. ' +
|
||||||
|
'Use <strong>Search API</strong> below to discover what models your LiteLLM proxy exposes, then add them.</p>';
|
||||||
|
} else if (data.models.length === 0) {
|
||||||
|
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No built-in models for this provider.</p>';
|
||||||
} else {
|
} else {
|
||||||
container.innerHTML = data.models.map(function(m) {
|
container.innerHTML = data.models.map(function(m) {
|
||||||
var checked = m.enabled !== false ? 'checked' : '';
|
var checked = m.enabled !== false ? 'checked' : '';
|
||||||
|
|
@ -736,9 +741,33 @@
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast('Added: ' + name, 'success');
|
showToast('Added: ' + name + ' — now select it as default and click Set Default', 'success');
|
||||||
if (btn) { btn.textContent = 'Added'; btn.disabled = true; btn.style.background = 'var(--green)'; }
|
if (btn) { btn.textContent = 'Added'; btn.disabled = true; btn.style.background = 'var(--green)'; }
|
||||||
loadAdminModels(); // Refresh custom list
|
// Refresh model lists, then auto-select the newly added model
|
||||||
|
fetch('/api/admin/config/models', { headers: getAuthHeaders() })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(refreshed) {
|
||||||
|
if (!refreshed.success) return;
|
||||||
|
var defaultSel = document.getElementById('admin-default-model');
|
||||||
|
if (defaultSel) {
|
||||||
|
defaultSel.innerHTML = '';
|
||||||
|
refreshed.models.forEach(function(m) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = m.id;
|
||||||
|
opt.textContent = m.name + (m.enabled === false ? ' (disabled)' : '');
|
||||||
|
defaultSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
(refreshed.custom || []).forEach(function(m) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = m.id;
|
||||||
|
opt.textContent = m.name;
|
||||||
|
defaultSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
// Auto-select the model just added
|
||||||
|
defaultSel.value = id;
|
||||||
|
}
|
||||||
|
renderCustomModels(refreshed.custom || []);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || 'Failed', 'error');
|
showToast(data.error || 'Failed', 'error');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,37 +48,6 @@ router.get('/config', async function(req, res) {
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── PUT update a single config entry ──────────────────────────────────────
|
|
||||||
router.put('/config/:key(*)', async function(req, res) {
|
|
||||||
try {
|
|
||||||
var key = req.params.key;
|
|
||||||
var value = req.body.value;
|
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return res.status(400).json({ error: 'value is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security: only allow known key prefixes
|
|
||||||
var allowed = ['announcement.', 'feature.', 'email.', 'prompt.', 'registration_enabled', 'site.', 'smtp.', 'models.'];
|
|
||||||
var isAllowed = allowed.some(function(p) { return key === p || key.startsWith(p); });
|
|
||||||
if (!isAllowed) {
|
|
||||||
return res.status(400).json({ error: 'Unknown config key' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.setSetting(key, String(value));
|
|
||||||
|
|
||||||
// Update in-memory prompt immediately if it's a prompt key
|
|
||||||
if (key.startsWith('prompt.')) {
|
|
||||||
var promptKey = key.replace('prompt.', '');
|
|
||||||
PROMPTS.updatePrompt(promptKey, String(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.audit(req.user.id, 'admin_config_update', 'Updated config: ' + key, req, { category: 'admin' });
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST send test email ───────────────────────────────────────────────────
|
// ── POST send test email ───────────────────────────────────────────────────
|
||||||
router.post('/config/test-email', async function(req, res) {
|
router.post('/config/test-email', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -192,6 +161,7 @@ router.get('/config/smtp/status', async function(req, res) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── PUT update SMTP settings ─────────────────────────────────────────────
|
// ── PUT update SMTP settings ─────────────────────────────────────────────
|
||||||
|
// NOTE: This must come BEFORE the wildcard PUT /config/:key(*) below
|
||||||
router.put('/config/smtp', async function(req, res) {
|
router.put('/config/smtp', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var { host, port, user, pass, from, secure } = req.body;
|
var { host, port, user, pass, from, secure } = req.body;
|
||||||
|
|
@ -203,7 +173,7 @@ router.put('/config/smtp', async function(req, res) {
|
||||||
await db.setSetting('smtp.from', (from || user || '').trim());
|
await db.setSetting('smtp.from', (from || user || '').trim());
|
||||||
await db.setSetting('smtp.secure', String(secure === true || secure === 'true'));
|
await db.setSetting('smtp.secure', String(secure === true || secure === 'true'));
|
||||||
if (pass && pass.trim()) {
|
if (pass && pass.trim()) {
|
||||||
await db.setSetting('smtp.pass', pass.trim()); // stored as-is; consider encrypting at rest
|
await db.setSetting('smtp.pass', pass.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.audit(req.user.id, 'admin_smtp_update', 'Updated SMTP settings', req, { category: 'admin' });
|
logger.audit(req.user.id, 'admin_smtp_update', 'Updated SMTP settings', req, { category: 'admin' });
|
||||||
|
|
@ -223,21 +193,28 @@ router.delete('/config/smtp', async function(req, res) {
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MODEL ROUTES — All must come BEFORE the wildcard PUT /config/:key(*)
|
||||||
|
// The wildcard uses :key(*) which matches slashes, so it would intercept
|
||||||
|
// /config/models/toggle and /config/models/default if registered after it.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
// ── GET models config ────────────────────────────────────────────────────
|
// ── GET models config ────────────────────────────────────────────────────
|
||||||
router.get('/config/models', async function(req, res) {
|
router.get('/config/models', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var { OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS, VERTEX_MODELS, LITELLM_MODELS, activeProvider } = require('../utils/models');
|
var { OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS, VERTEX_MODELS, activeProvider } = require('../utils/models');
|
||||||
var providerModels;
|
var providerModels;
|
||||||
switch (activeProvider) {
|
switch (activeProvider) {
|
||||||
case 'bedrock': providerModels = BEDROCK_MODELS; break;
|
case 'bedrock': providerModels = BEDROCK_MODELS; break;
|
||||||
case 'azure': providerModels = AZURE_MODELS; break;
|
case 'azure': providerModels = AZURE_MODELS; break;
|
||||||
case 'vertex': providerModels = VERTEX_MODELS; break;
|
case 'vertex': providerModels = VERTEX_MODELS; break;
|
||||||
case 'litellm': providerModels = LITELLM_MODELS; break;
|
case 'litellm': providerModels = []; break; // LiteLLM: no built-ins — use discover
|
||||||
default: providerModels = OPENROUTER_MODELS;
|
default: providerModels = OPENROUTER_MODELS;
|
||||||
}
|
}
|
||||||
|
|
||||||
var disabledRaw = await db.getSetting('models.disabled') || '[]';
|
var disabledRaw = await db.getSetting('models.disabled') || '[]';
|
||||||
var customRaw = await db.getSetting('models.custom') || '[]';
|
var customRaw = await db.getSetting('models.custom') || '[]';
|
||||||
|
var defaultModel = await db.getSetting('models.default') || '';
|
||||||
var disabled, custom;
|
var disabled, custom;
|
||||||
try { disabled = JSON.parse(disabledRaw); } catch(e) { disabled = []; }
|
try { disabled = JSON.parse(disabledRaw); } catch(e) { disabled = []; }
|
||||||
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
||||||
|
|
@ -246,11 +223,18 @@ router.get('/config/models', async function(req, res) {
|
||||||
return Object.assign({}, m, { enabled: !disabled.includes(m.id) });
|
return Object.assign({}, m, { enabled: !disabled.includes(m.id) });
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, provider: activeProvider, models: models, custom: custom });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
provider: activeProvider,
|
||||||
|
models: models,
|
||||||
|
custom: custom,
|
||||||
|
defaultModel: defaultModel,
|
||||||
|
litellmHint: activeProvider === 'litellm'
|
||||||
|
});
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── PUT update model enabled/disabled state ──────────────────────────────
|
// ── PUT toggle model enabled/disabled ────────────────────────────────────
|
||||||
router.put('/config/models/toggle', async function(req, res) {
|
router.put('/config/models/toggle', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var { modelId, enabled } = req.body;
|
var { modelId, enabled } = req.body;
|
||||||
|
|
@ -273,7 +257,7 @@ router.put('/config/models/toggle', async function(req, res) {
|
||||||
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
||||||
var enabledCount = allModels.filter(function(m) { return !disabled.includes(m.id); }).length + custom.length;
|
var enabledCount = allModels.filter(function(m) { return !disabled.includes(m.id); }).length + custom.length;
|
||||||
if (enabledCount === 0) {
|
if (enabledCount === 0) {
|
||||||
return res.status(400).json({ error: 'Cannot disable all models. At least one model must remain enabled.' });
|
return res.status(400).json({ error: 'Cannot disable all models. At least one must remain enabled.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,22 +267,32 @@ router.put('/config/models/toggle', async function(req, res) {
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── POST add custom model ────────────────────────────────────────────────
|
// ── PUT set default model ─────────────────────────────────────────────────
|
||||||
|
router.put('/config/models/default', async function(req, res) {
|
||||||
|
try {
|
||||||
|
var { modelId } = req.body;
|
||||||
|
if (!modelId) return res.status(400).json({ error: 'modelId required' });
|
||||||
|
await db.setSetting('models.default', modelId.trim());
|
||||||
|
logger.audit(req.user.id, 'admin_model_default', 'Set default model: ' + modelId, req, { category: 'admin' });
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── POST add custom model (manual entry) ─────────────────────────────────
|
||||||
router.post('/config/models/custom', async function(req, res) {
|
router.post('/config/models/custom', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var { id, name, cost, category } = req.body;
|
var { id, name, cost, category } = req.body;
|
||||||
if (!id || !name) return res.status(400).json({ error: 'id and name required' });
|
if (!id || !name) return res.status(400).json({ error: 'id and name required' });
|
||||||
|
|
||||||
// Validate model ID format (provider/model-name)
|
|
||||||
var trimmedId = id.trim();
|
var trimmedId = id.trim();
|
||||||
if (trimmedId.length > 200) return res.status(400).json({ error: 'Model ID too long (max 200 chars)' });
|
if (trimmedId.length > 200) return res.status(400).json({ error: 'Model ID too long (max 200 chars)' });
|
||||||
if (!/^[a-zA-Z0-9._\-\/\:]+$/.test(trimmedId)) {
|
if (!/^[a-zA-Z0-9._\-\/\:]+$/.test(trimmedId)) {
|
||||||
return res.status(400).json({ error: 'Invalid model ID format. Use only letters, numbers, dots, dashes, slashes, and colons.' });
|
return res.status(400).json({ error: 'Invalid model ID format. Use only letters, numbers, dots, dashes, slashes, and colons.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if model ID conflicts with a built-in model
|
// Check if model ID conflicts with a built-in model (all providers)
|
||||||
var { OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS } = require('../utils/models');
|
var { OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS, VERTEX_MODELS } = require('../utils/models');
|
||||||
var allBuiltIn = [].concat(OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS);
|
var allBuiltIn = [].concat(OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS, VERTEX_MODELS);
|
||||||
if (allBuiltIn.find(function(m) { return m.id === trimmedId; })) {
|
if (allBuiltIn.find(function(m) { return m.id === trimmedId; })) {
|
||||||
return res.status(400).json({ error: 'Model ID conflicts with a built-in model. Use the toggle to enable/disable built-in models.' });
|
return res.status(400).json({ error: 'Model ID conflicts with a built-in model. Use the toggle to enable/disable built-in models.' });
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +304,6 @@ router.post('/config/models/custom', async function(req, res) {
|
||||||
var custom;
|
var custom;
|
||||||
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
||||||
|
|
||||||
// Prevent duplicates
|
|
||||||
var existing = custom.find(function(m) { return m.id === trimmedId; });
|
var existing = custom.find(function(m) { return m.id === trimmedId; });
|
||||||
custom = custom.filter(function(m) { return m.id !== trimmedId; });
|
custom = custom.filter(function(m) { return m.id !== trimmedId; });
|
||||||
custom.push({ id: trimmedId, name: name.trim().substring(0, 100), cost: (cost || '?').substring(0, 20), category: cat, tag: 'CUSTOM' });
|
custom.push({ id: trimmedId, name: name.trim().substring(0, 100), cost: (cost || '?').substring(0, 20), category: cat, tag: 'CUSTOM' });
|
||||||
|
|
@ -321,7 +314,7 @@ router.post('/config/models/custom', async function(req, res) {
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── DELETE custom model ──────────────────────────────────────────────────
|
// ── DELETE custom model ───────────────────────────────────────────────────
|
||||||
router.delete('/config/models/custom/:modelId(*)', async function(req, res) {
|
router.delete('/config/models/custom/:modelId(*)', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var modelId = req.params.modelId;
|
var modelId = req.params.modelId;
|
||||||
|
|
@ -330,11 +323,12 @@ router.delete('/config/models/custom/:modelId(*)', async function(req, res) {
|
||||||
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
|
||||||
custom = custom.filter(function(m) { return m.id !== modelId; });
|
custom = custom.filter(function(m) { return m.id !== modelId; });
|
||||||
await db.setSetting('models.custom', JSON.stringify(custom));
|
await db.setSetting('models.custom', JSON.stringify(custom));
|
||||||
|
logger.audit(req.user.id, 'admin_model_delete', 'Removed custom model: ' + modelId, req, { category: 'admin' });
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── GET discover models from provider API ─────────────────────────────
|
// ── GET discover models from provider API ─────────────────────────────────
|
||||||
router.get('/config/models/discover', async function(req, res) {
|
router.get('/config/models/discover', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var { discoverModels } = require('../utils/ai');
|
var { discoverModels } = require('../utils/ai');
|
||||||
|
|
@ -349,7 +343,7 @@ router.get('/config/models/discover', async function(req, res) {
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── POST add discovered model to enabled list ─────────────────────────
|
// ── POST add discovered model to custom list ──────────────────────────────
|
||||||
router.post('/config/models/add-discovered', async function(req, res) {
|
router.post('/config/models/add-discovered', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var { id, name, cost, category } = req.body;
|
var { id, name, cost, category } = req.body;
|
||||||
|
|
@ -370,17 +364,43 @@ router.post('/config/models/add-discovered', async function(req, res) {
|
||||||
|
|
||||||
await db.setSetting('models.custom', JSON.stringify(custom));
|
await db.setSetting('models.custom', JSON.stringify(custom));
|
||||||
logger.audit(req.user.id, 'admin_model_discover_add', 'Added discovered model: ' + trimmedId, req, { category: 'admin' });
|
logger.audit(req.user.id, 'admin_model_discover_add', 'Added discovered model: ' + trimmedId, req, { category: 'admin' });
|
||||||
res.json({ success: true });
|
res.json({ success: true, id: trimmedId });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── PUT set default model ─────────────────────────────────────────────
|
// ============================================================
|
||||||
router.put('/config/models/default', async function(req, res) {
|
// WILDCARD CONFIG — Must come AFTER all specific model routes above
|
||||||
|
// :key(*) matches slashes, so it would intercept /config/models/toggle
|
||||||
|
// and /config/models/default if registered before them.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// ── PUT update a single config entry ──────────────────────────────────────
|
||||||
|
router.put('/config/:key(*)', async function(req, res) {
|
||||||
try {
|
try {
|
||||||
var { modelId } = req.body;
|
var key = req.params.key;
|
||||||
if (!modelId) return res.status(400).json({ error: 'modelId required' });
|
var value = req.body.value;
|
||||||
await db.setSetting('models.default', modelId.trim());
|
|
||||||
logger.audit(req.user.id, 'admin_model_default', 'Set default model: ' + modelId, req, { category: 'admin' });
|
if (value === undefined || value === null) {
|
||||||
|
return res.status(400).json({ error: 'value is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: only allow known key prefixes
|
||||||
|
var allowed = ['announcement.', 'feature.', 'email.', 'prompt.', 'registration_enabled', 'site.', 'smtp.', 'models.'];
|
||||||
|
var isAllowed = allowed.some(function(p) { return key === p || key.startsWith(p); });
|
||||||
|
if (!isAllowed) {
|
||||||
|
return res.status(400).json({ error: 'Unknown config key' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.setSetting(key, String(value));
|
||||||
|
|
||||||
|
// Update in-memory prompt immediately if it's a prompt key
|
||||||
|
if (key.startsWith('prompt.')) {
|
||||||
|
var promptKey = key.replace('prompt.', '');
|
||||||
|
PROMPTS.updatePrompt(promptKey, String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.audit(req.user.id, 'admin_config_update', 'Updated config: ' + key, req, { category: 'admin' });
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue