'use strict'; const express = require('express'); const settings = require('./manager'); const Database = require('../storage/database'); function createRouter() { const router = express.Router(); function dbAvailable() { return Database && Database.db; } // ---- Global settings ---- router.get('/api/settings', async (req, res) => { try { const all = settings.getAll(); const registry = settings.getRegistry(); const result = registry.map(r => ({ ...r, value: all[r.key], })); res.json({ settings: result }); } catch (error) { console.error('API Error /api/settings:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/settings/:category', async (req, res) => { try { const values = settings.getAllByCategory(req.params.category); const registry = settings.getRegistry().filter(r => r.category === req.params.category); const result = registry.map(r => ({ ...r, value: values[r.key], })); res.json({ settings: result }); } catch (error) { res.status(500).json({ error: error.message }); } }); router.put('/api/settings/:key', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const { key } = req.params; const { value } = req.body; if (value === undefined) { return res.status(400).json({ error: 'Missing value' }); } const newValue = await settings.set(key, value); res.json({ key, value: newValue }); } catch (error) { console.error('API Error /api/settings/:key:', error); res.status(500).json({ error: error.message }); } }); // ---- Bot settings ---- router.get('/api/bot-settings', async (req, res) => { try { const names = settings.getBotNames(); const botReg = settings.getBotSettingsRegistry(); const bots = names.map(name => { const s = settings.getBotSettings(name); const flat = { name }; // Add metadata from registry for each key for (const br of botReg) { flat[br.key] = { value: s ? s[br.key] : null, type: br.type, label: br.label, description: br.description }; } return flat; }); res.json({ bots, registry: botReg }); } catch (error) { console.error('API Error /api/bot-settings:', error); res.status(500).json({ error: error.message }); } }); router.get('/api/bot-settings/:botName', async (req, res) => { try { const s = settings.getBotSettings(req.params.botName); if (!s) return res.status(404).json({ error: `Unknown bot: ${req.params.botName}` }); const botReg = settings.getBotSettingsRegistry(); const result = { name: req.params.botName }; for (const br of botReg) { result[br.key] = { value: s[br.key], type: br.type, label: br.label, description: br.description }; } res.json(result); } catch (error) { res.status(500).json({ error: error.message }); } }); router.put('/api/bot-settings/:botName/:key', async (req, res) => { if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' }); try { const { botName, key } = req.params; const { value } = req.body; if (value === undefined) return res.status(400).json({ error: 'Missing value' }); const newValue = await settings.setBotSetting(botName, key, value); res.json({ botName, key, value: newValue }); } catch (error) { console.error('API Error /api/bot-settings/:botName/:key:', error); res.status(500).json({ error: error.message }); } }); return router; } const webUI = { tabId: 'settings', tabLabel: 'Settings', tabOrder: 35, html: `
Select a category to view settings
`, css: ` .settings-layout{display:flex;gap:16px;min-height:400px} .settings-sidebar{width:180px;flex-shrink:0;display:flex;flex-direction:column;gap:4px} .settings-sidebar-btn{background:transparent;border:1px solid #374151;color:#9ca3af;padding:10px 14px;border-radius:6px;cursor:pointer;text-align:left;font-size:.9em;transition:all .2s} .settings-sidebar-btn:hover{border-color:#60a5fa;color:#e5e7eb} .settings-sidebar-btn.active{background:#1e40af;border-color:#60a5fa;color:#fff} .settings-main{flex:1;min-width:0} .settings-empty{padding:40px 20px;color:#6b7280;text-align:center;font-size:.9em} .settings-grid{display:flex;flex-direction:column;gap:12px} .settings-card{background:#111827;border:1px solid #374151;border-radius:8px;padding:14px 16px;transition:border-color .2s} .settings-card:hover{border-color:#60a5fa} .settings-card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px} .settings-card-label{font-weight:600;color:#e5e7eb;font-size:.95em} .settings-card-desc{color:#6b7280;font-size:.8em;margin-bottom:10px} .settings-card-key{color:#4b5563;font-size:.75em;font-family:monospace} .settings-card-body{display:flex;gap:8px;align-items:center} .settings-card-body input[type="text"], .settings-card-body input[type="number"], .settings-card-body input[type="password"]{flex:1;padding:8px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.9em} .settings-card-body input:focus{outline:none;border-color:#60a5fa} .settings-card-body textarea{flex:1;padding:8px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.85em;min-height:80px;resize:vertical;font-family:monospace} .settings-card-body textarea:focus{outline:none;border-color:#60a5fa} .settings-card-body input[type="checkbox"]{width:18px;height:18px;accent-color:#60a5fa} .settings-card-body select{flex:1;padding:8px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.9em} .settings-card-body select:focus{outline:none;border-color:#60a5fa} .settings-value-display{flex:1;padding:8px 10px;color:#9ca3af;font-size:.9em;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .settings-btn-save{padding:8px 16px;border-radius:6px;border:1px solid #059669;background:#065f46;color:#6ee7b7;cursor:pointer;font-size:.85em;white-space:nowrap;transition:all .2s} .settings-btn-save:hover{background:#059669;color:#fff} .settings-btn-save.saved{background:#059669;color:#fff} .settings-toast{position:fixed;bottom:20px;right:20px;background:#059669;color:#fff;padding:12px 20px;border-radius:8px;font-size:.9em;z-index:9999;opacity:0;transform:translateY(10px);transition:all .3s} .settings-toast.show{opacity:1;transform:translateY(0)} .settings-toast.error{background:#dc2626} .prompt-editor{margin-top:12px;border:1px solid #374151;border-radius:8px;overflow:hidden} .prompt-editor-layout{display:flex;min-height:300px} .prompt-editor-sidebar{width:180px;flex-shrink:0;background:#0f1729;border-right:1px solid #374151;display:flex;flex-direction:column} .prompt-editor-sidebar-header{padding:10px 12px;border-bottom:1px solid #374151;display:flex;justify-content:space-between;align-items:center} .prompt-editor-sidebar-title{color:#9ca3af;font-size:.75em;text-transform:uppercase;letter-spacing:.5px} .prompt-editor-sidebar-list{flex:1;overflow-y:auto;padding:4px} .prompt-editor-prompt-item{padding:8px 10px;border-radius:4px;cursor:pointer;color:#9ca3af;font-size:.85em;transition:all .15s;display:flex;justify-content:space-between;align-items:center} .prompt-editor-prompt-item:hover{background:#1e293b;color:#e5e7eb} .prompt-editor-prompt-item.active{background:#1e40af;color:#fff} .prompt-editor-prompt-item .prompt-delete-x{opacity:0;color:#ef4444;font-weight:bold;font-size:1.1em;padding:0 4px;transition:opacity .15s} .prompt-editor-prompt-item:hover .prompt-delete-x{opacity:.7} .prompt-editor-prompt-item .prompt-delete-x:hover{opacity:1} .prompt-editor-content{flex:1;display:flex;flex-direction:column;padding:12px} .prompt-editor-content-label{color:#9ca3af;font-size:.8em;margin-bottom:6px} .prompt-editor-content textarea{flex:1;min-height:250px;padding:10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.85em;font-family:monospace;resize:vertical;line-height:1.5} .prompt-editor-content textarea:focus{outline:none;border-color:#60a5fa} .prompt-editor-actions{display:flex;gap:8px;margin-top:8px;align-items:center} .prompt-editor-vars{font-size:.75em;color:#6b7280;margin-top:6px} .prompt-editor-vars code{color:#93c5fd;font-size:.85em} .prompt-editor-add-btn{padding:4px 8px;border-radius:4px;border:1px solid #374151;background:transparent;color:#9ca3af;cursor:pointer;font-size:.8em;transition:all .15s} .prompt-editor-add-btn:hover{border-color:#60a5fa;color:#e5e7eb} .bot-list{display:flex;flex-direction:column;gap:8px} .bot-card{background:#111827;border:1px solid #374151;border-radius:8px;overflow:hidden;transition:border-color .2s} .bot-card.expanded{border-color:#60a5fa} .bot-card-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;cursor:pointer;user-select:none;transition:background .15s} .bot-card-header:hover{background:#1e293b} .bot-card-name{font-weight:600;color:#e5e7eb;font-size:1em} .bot-card-summary{font-size:.8em;color:#6b7280;display:flex;gap:12px;flex-wrap:wrap} .bot-card-summary span{white-space:nowrap} .bot-card-summary .on{color:#6ee7b7} .bot-card-summary .off{color:#ef4444} .bot-card-arrow{color:#6b7280;transition:transform .2s;font-size:1.2em} .bot-card.expanded .bot-card-arrow{transform:rotate(180deg)} .bot-card-body{display:none;padding:0 16px 14px;border-top:1px solid #1f2937} .bot-card.expanded .bot-card-body{display:block} .bot-field{margin-top:10px} .bot-field-label{font-size:.8em;color:#9ca3af;margin-bottom:4px} .bot-field-row{display:flex;gap:8px;align-items:center} .bot-field-row input[type="text"], .bot-field-row input[type="number"], .bot-field-row input[type="password"]{flex:1;padding:6px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.85em} .bot-field-row input:focus{outline:none;border-color:#60a5fa} .bot-field-row textarea{flex:1;padding:6px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.8em;font-family:monospace;min-height:50px;resize:vertical} .bot-field-row textarea:focus{outline:none;border-color:#60a5fa} .bot-field-row input[type="checkbox"]{width:16px;height:16px;accent-color:#60a5fa} .bot-field-row .mini-save{padding:6px 12px;border-radius:6px;border:1px solid #059669;background:#065f46;color:#6ee7b7;cursor:pointer;font-size:.8em;white-space:nowrap;transition:all .2s} .bot-field-row .mini-save:hover{background:#059669;color:#fff} .bot-field-row .mini-save.saved{background:#059669;color:#fff} `, onTabActive: 'onSettingsTabActive', js: ` let settingsData = []; let activeCategory = null; let botData = []; let botRegistry = []; function onSettingsTabActive() { loadSettingsCategories(); } async function loadSettingsCategories() { try { const r = await fetch('/api/settings'); if (!r.ok) throw new Error('Failed to load settings'); const d = await r.json(); settingsData = d.settings || []; // Build category sidebar const cats = {}; settingsData.forEach(s => { if (!cats[s.category]) cats[s.category] = []; cats[s.category].push(s); }); // Ensure bots category exists cats['bots'] = cats['bots'] || []; const catOrder = ['ai', 'storage', 'server', 'farm', 'invites', 'general', 'bots']; let sidebarHtml = ''; const catNames = Object.keys(cats); catNames.sort((a, b) => { const ia = catOrder.indexOf(a), ib = catOrder.indexOf(b); if (ia >= 0 && ib >= 0) return ia - ib; if (ia >= 0) return -1; if (ib >= 0) return 1; return a.localeCompare(b); }); catNames.forEach(cat => { const count = cats[cat].length; const label = cat === 'bots' ? 'Bots' : cat.charAt(0).toUpperCase()+cat.slice(1); const activeClass = activeCategory === cat ? ' active' : (activeCategory === null && cat === catNames[0] ? ' active' : ''); sidebarHtml += ''; }); document.getElementById('settingsSidebar').innerHTML = sidebarHtml; if (!activeCategory) { activeCategory = catNames[0] || null; } if (activeCategory === 'bots') { await loadBotSettings(); } else { renderSettings(activeCategory); } } catch(e) { document.getElementById('settingsMain').innerHTML = '
Failed to load settings: '+escHtml(e.message)+'
'; } } async function switchSettingsCategory(cat) { activeCategory = cat; const btns = document.querySelectorAll('.settings-sidebar-btn'); btns.forEach(b => { b.classList.remove('active'); if (b.textContent.trim().startsWith(cat === 'bots' ? 'Bots' : cat.charAt(0).toUpperCase()+cat.slice(1))) b.classList.add('active'); }); if (cat === 'bots') { await loadBotSettings(); } else { renderSettings(cat); } } async function loadBotSettings() { try { const r = await fetch('/api/bot-settings'); if (!r.ok) throw new Error('Failed to load bot settings'); const d = await r.json(); botData = d.bots || []; botRegistry = d.registry || []; renderBotSettings(); } catch(e) { document.getElementById('settingsMain').innerHTML = '
Failed to load bot settings: '+escHtml(e.message)+'
'; } } function renderBotSettings() { if (!botData.length) { document.getElementById('settingsMain').innerHTML = '
No bots configured
'; return; } let html = '
'; botData.forEach(bot => { const autoConnect = bot.autoConnect?.value; const onDemand = bot.onDemand?.value; const isReady = bot.isReady ? ' (online)' : ''; let summaryParts = []; if (autoConnect) summaryParts.push('auto-connect'); else summaryParts.push('no auto-connect'); if (onDemand) summaryParts.push('on-demand'); if (bot.commands?.value && Array.isArray(bot.commands.value)) { summaryParts.push('cmds: '+escHtml(bot.commands.value.join(','))+''); } html += '
'+ '
'+ '
'+ '
'+escHtml(bot.name)+isReady+'
'+ '
'+summaryParts.join('')+'
'+ '
'+ '
'+ '
'+ '
'; botRegistry.forEach(br => { const field = bot[br.key]; if (!field) return; let val = field.value; if (br.type === 'boolean') val = val === true || val === 'true'; const displayVal = br.type === 'secret' ? (val ? '***' : '') : val; const isSecret = br.type === 'secret'; let inputHtml; if (br.type === 'boolean') { inputHtml = ''; } else if (br.type === 'json') { const jsonStr = val ? JSON.stringify(val, null, 2) : ''; inputHtml = ''; } else if (isSecret) { inputHtml = ''; } else if (br.type === 'number') { inputHtml = ''; } else { inputHtml = ''; } html += '
'+ '
'+escHtml(br.label)+' '+escHtml(br.key)+' '+escHtml(br.description ? '- '+br.description : '')+'
'+ '
'+inputHtml; if (br.type !== 'boolean') { html += ''; } html += '
'; }); html += '
'; }); html += '
'; document.getElementById('settingsMain').innerHTML = html; } function toggleBotCard(name) { const card = document.getElementById('botCard_'+name); if (!card) return; card.classList.toggle('expanded'); } async function saveBotSetting(botName, key, value) { const btn = document.getElementById('btn_bot_'+botName+'_'+key); try { const r = await fetch('/api/bot-settings/'+encodeURIComponent(botName)+'/'+encodeURIComponent(key), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: value }) }); if (!r.ok) { const err = await r.json().catch(() => ({ error: 'Unknown error' })); throw new Error(err.error || 'Failed to save'); } // Update local data const bot = botData.find(b => b.name === botName); if (bot && bot[key]) { bot[key].value = (key === 'password' && value) ? value : value; } if (btn) { btn.classList.add('saved'); btn.textContent = 'Saved!'; setTimeout(() => { btn.classList.remove('saved'); btn.textContent = 'Save'; }, 2000); } // If password field, clear it after save if (key === 'password') { const inp = document.getElementById('bot_'+botName+'_'+key); if (inp) inp.value = ''; } showToast('Saved '+botName+'.'+key); } catch(e) { showToast('Error: '+e.message, true); if (btn) { btn.style.borderColor = '#dc2626'; btn.textContent = 'Error'; setTimeout(() => { btn.style.borderColor = '#059669'; btn.textContent = 'Save'; }, 3000); } } } function renderSettings(category) { if (!category) { document.getElementById('settingsMain').innerHTML = '
Select a category
'; return; } const items = settingsData.filter(s => s.category === category); if (items.length === 0) { document.getElementById('settingsMain').innerHTML = '
No settings in this category
'; return; } let html = '
'; items.forEach(s => { const key = escHtml(s.key); const label = escHtml(s.label || s.key); const desc = escHtml(s.description || ''); const isSecret = s.type === 'secret'; let inputHtml = ''; if (s.key === 'ai.prompts') { inputHtml = buildPromptEditor(s); } else if (s.key === 'ai.promptName') { const prompts = getPromptsMap(); const names = Object.keys(prompts); if (names.length === 0) names.push('asshole'); inputHtml = ''; } else if (s.key === 'ai.prompCustom') { inputHtml = ''; } else if (s.type === 'boolean') { const checked = s.value === true ? ' checked' : ''; inputHtml = ''; } else if (isSecret) { inputHtml = ''; } else if (s.type === 'json') { const jsonStr = s.value ? JSON.stringify(s.value, null, 2) : ''; inputHtml = ''; } else if (s.type === 'number') { inputHtml = ''; } else { inputHtml = ''; } html += '
'+ '
'+ '
'+label+'
'+key+'
'+ '
'+ '
'+desc+'
'+ '
'+inputHtml; if (s.key === 'ai.prompts') { // Prompt editor handles its own save } else if (s.key === 'ai.promptName' || s.key === 'ai.prompCustom' || s.type !== 'boolean') { html += ''; } html += '
'; }); html += '
'; document.getElementById('settingsMain').innerHTML = html; const promptsItem = items.find(s => s.key === 'ai.prompts'); if (promptsItem) initPromptEditor(promptsItem); } async function saveSetting(key, value) { const btn = event && event.target ? event.target : null; try { const r = await fetch('/api/settings/'+encodeURIComponent(key), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: value }) }); if (!r.ok) { const err = await r.json().catch(() => ({ error: 'Unknown error' })); throw new Error(err.error || 'Failed to save'); } const d = await r.json(); const item = settingsData.find(s => s.key === key); if (item) item.value = d.value; if (btn) { btn.classList.add('saved'); btn.textContent = 'Saved!'; setTimeout(() => { btn.classList.remove('saved'); btn.textContent = 'Save'; }, 2000); } // Clear password field after save const isSecret = settingsData.find(s => s.key === key)?.type === 'secret'; if (isSecret) { const inp = document.getElementById('inp_'+key); if (inp) inp.value = ''; } showToast('Saved '+key); } catch(e) { showToast('Error: '+e.message, true); if (btn) { btn.style.borderColor = '#dc2626'; btn.style.background = '#7f1d1d'; btn.style.color = '#fca5a5'; btn.textContent = 'Error'; setTimeout(() => { btn.style.borderColor = '#059669'; btn.style.background = '#065f46'; btn.style.color = '#6ee7b7'; btn.textContent = 'Save'; }, 3000); } } } let selectedPromptName = null; function getPromptsMap() { const item = settingsData.find(s => s.key === 'ai.prompts'); if (item && item.value && typeof item.value === 'object' && !Array.isArray(item.value)) { return item.value; } return {}; } function buildPromptEditor(s) { return '
'+ '
'+ '
'+ '
'+ 'Prompts'+ ''+ '
'+ '
'+ '
'+ '
'+ ''+ ''+ '
'+ ''+ ''+ '
'+ '
Template variables: \${name} \${interval} \${currentPlayers} \${toolsDocs} \${memoryContext} \${timeInfo} \${custom}
'+ '
'+ '
'+ '
'; } function initPromptEditor(s) { const prompts = getPromptsMap(); renderPromptList(prompts); const names = Object.keys(prompts); if (names.length > 0) { selectPromptToEdit(names[0]); } } function renderPromptList(prompts) { const listEl = document.getElementById('promptEditorList'); if (!listEl) return; const names = Object.keys(prompts); listEl.innerHTML = names.map(name => '
'+ ''+escHtml(name)+''+ '×'+ '
' ).join(''); } function selectPromptToEdit(name) { selectedPromptName = name; const prompts = getPromptsMap(); document.getElementById('promptEditorActiveName').textContent = name; document.getElementById('promptEditorTextarea').value = prompts[name] || ''; document.getElementById('promptEditorSaved').textContent = ''; renderPromptList(prompts); } async function savePromptTemplate() { if (!selectedPromptName) return; const textarea = document.getElementById('promptEditorTextarea'); const template = textarea.value; const prompts = getPromptsMap(); prompts[selectedPromptName] = template; await saveSetting('ai.prompts', prompts); document.getElementById('promptEditorSaved').textContent = 'Saved!'; setTimeout(() => { document.getElementById('promptEditorSaved').textContent = ''; }, 2000); } async function addNewPrompt() { const name = prompt('New prompt name:'); if (!name || !name.trim()) return; const trimmed = name.trim(); const prompts = getPromptsMap(); if (prompts[trimmed]) { alert('Prompt "'+trimmed+'" already exists.'); return; } prompts[trimmed] = ''; await saveSetting('ai.prompts', prompts); selectedPromptName = trimmed; renderPromptList(prompts); selectPromptToEdit(trimmed); loadSettingsCategories(); } async function deletePrompt(name) { const prompts = getPromptsMap(); if (Object.keys(prompts).length <= 1) { alert('Cannot delete the last prompt.'); return; } if (!confirm('Delete prompt "'+name+'"?')) return; delete prompts[name]; await saveSetting('ai.prompts', prompts); if (selectedPromptName === name) { const remaining = Object.keys(prompts); selectedPromptName = remaining.length > 0 ? remaining[0] : null; } if (selectedPromptName) { selectPromptToEdit(selectedPromptName); } renderPromptList(prompts); loadSettingsCategories(); } function showToast(msg, isError) { let toast = document.getElementById('settingsToast'); if (!toast) { toast = document.createElement('div'); toast.id = 'settingsToast'; toast.className = 'settings-toast'; document.body.appendChild(toast); } toast.textContent = msg; toast.className = 'settings-toast' + (isError ? ' error' : ''); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); }, 3000); } `, }; module.exports = { createRouter, webUI };