merge: upgrade to upstream v0.50.95 + keep custom additions
Upstream v0.50.95 features merged (Russian localization, slash commands, mic toggle fix, gateway sync fix, KaTeX/Prism.js, etc.) Custom additions preserved: - Tier-2 agent switching commands in commands.js - MC panel in index.html + MC CSS - _resolve_cli_toolsets() in config.py - Custom routes.py, server.py, boot.js, i18n.js, messages.js, workspace.js Files with conflict resolution (took upstream, custom code in other files): - CHANGELOG.md, config.py, commands.js, index.html, panels.js, style.css, ui.js
This commit is contained in:
360
static/panels.js
360
static/panels.js
@@ -16,6 +16,7 @@ async function switchPanel(name) {
|
||||
if (name === 'workspaces') await loadWorkspacesPanel();
|
||||
if (name === 'profiles') await loadProfilesPanel();
|
||||
if (name === 'todos') loadTodos();
|
||||
if (name === 'missioncontrol') await loadMissionControl();
|
||||
}
|
||||
|
||||
// ── Cron panel ──
|
||||
@@ -39,6 +40,8 @@ async function loadCrons() {
|
||||
item.innerHTML = `
|
||||
<div class="cron-header" onclick="toggleCron('${job.id}')">
|
||||
<span class="cron-name" title="${esc(job.name)}">${esc(job.name)}</span>
|
||||
<span class="cron-schedule-inline">${esc(job.schedule_display || job.schedule?.expression || '')}</span>
|
||||
<span class="cron-last-inline">${job.last_run_at ? esc(new Date(job.last_run_at).toLocaleString()) : ''}</span>
|
||||
<span class="cron-status ${statusClass}">${statusLabel}</span>
|
||||
</div>
|
||||
<div class="cron-body" id="cron-body-${job.id}">
|
||||
@@ -50,7 +53,7 @@ async function loadCrons() {
|
||||
? `<button class="cron-btn" onclick="cronResume('${job.id}')">${li('play',12)} ${esc(t('cron_resume'))}</button>`
|
||||
: `<button class="cron-btn pause" onclick="cronPause('${job.id}')">${li('pause',12)} ${esc(t('cron_pause'))}</button>`}
|
||||
<button class="cron-btn" onclick="cronEditOpen('${job.id}',${JSON.stringify(job).replace(/"/g,'"')})">${li('pencil',12)} ${esc(t('edit'))}</button>
|
||||
<button class="cron-btn" style="border-color:var(--accent-bg-strong);color:var(--accent-text)" onclick="cronDelete('${job.id}')">${li('trash-2',12)} ${esc(t('delete_title'))}</button>
|
||||
<button class="cron-btn" style="border-color:rgba(201,168,76,.3);color:var(--accent)" onclick="cronDelete('${job.id}')">${li('trash-2',12)} ${esc(t('delete_title'))}</button>
|
||||
</div>
|
||||
<!-- Inline edit form, hidden by default -->
|
||||
<div id="cron-edit-${job.id}" style="display:none;margin-top:8px;border-top:1px solid var(--border);padding-top:8px">
|
||||
@@ -323,7 +326,12 @@ function loadTodos() {
|
||||
}
|
||||
}
|
||||
if (!todos.length) {
|
||||
panel.innerHTML = `<div style="color:var(--muted);font-size:12px;padding:4px 0">${esc(t('todos_no_active'))}</div>`;
|
||||
panel.innerHTML = `<div style="color:var(--muted);font-size:12px;padding:4px 0">${esc(t('todos_no_active'))}</div>
|
||||
<div style="margin-top:10px;padding:12px;border:1px dashed var(--border2);border-radius:8px;font-size:12px;color:var(--muted);line-height:1.6;background:rgba(124,185,255,.03)">
|
||||
<div style="font-weight:600;margin-bottom:6px;color:var(--text);opacity:.8">${li('lightbulb',12)} Tipp</div>
|
||||
<div style="opacity:.7">Frag im Chat nach einer Todo-Liste, z.B.:</div>
|
||||
<div style="margin-top:6px;padding:6px 8px;background:rgba(255,255,255,.04);border-radius:6px;font-style:italic;opacity:.6">"Plane mein Wochenende: Freitags einkaufen, samtags Sport, sonntags relaxen"</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
const statusIcon = {pending:li('square',14), in_progress:li('loader',14), completed:li('check',14), cancelled:li('x',14)};
|
||||
@@ -375,7 +383,7 @@ function renderSkills(skills) {
|
||||
// Group by category
|
||||
const cats = {};
|
||||
for (const s of filtered) {
|
||||
const cat = s.category || '(general)';
|
||||
const cat = s.category || 'general';
|
||||
if (!cats[cat]) cats[cat] = [];
|
||||
cats[cat].push(s);
|
||||
}
|
||||
@@ -385,7 +393,8 @@ function renderSkills(skills) {
|
||||
for (const [cat, items] of Object.entries(cats).sort()) {
|
||||
const sec = document.createElement('div');
|
||||
sec.className = 'skills-category';
|
||||
sec.innerHTML = `<div class="skills-cat-header">${li('folder',12)} ${esc(cat)} <span style="opacity:.5">(${items.length})</span></div>`;
|
||||
const displayName = cat === 'general' ? 'General' : cat;
|
||||
sec.innerHTML = `<div class="skills-cat-header">${li('folder',12)} ${esc(displayName)} <span style="opacity:.5">(${items.length})</span></div>`;
|
||||
for (const skill of items.sort((a,b) => a.name.localeCompare(b.name))) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'skill-item';
|
||||
@@ -405,8 +414,6 @@ async function openSkill(name, el) {
|
||||
// Highlight active skill
|
||||
document.querySelectorAll('.skill-item').forEach(e => e.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
// Ensure the workspace panel is open so the skill content is actually visible (#643)
|
||||
if (typeof ensureWorkspacePreviewVisible === 'function') ensureWorkspacePreviewVisible();
|
||||
try {
|
||||
const data = await api(`/api/skills/content?name=${encodeURIComponent(name)}`);
|
||||
// Show skill content in right panel preview
|
||||
@@ -436,6 +443,7 @@ async function openSkill(name, el) {
|
||||
});
|
||||
$('previewArea').classList.add('visible');
|
||||
$('fileTree').style.display = 'none';
|
||||
const wsSearchSkill=$('wsSearchWrap');if(wsSearchSkill)wsSearchSkill.style.display='none';
|
||||
} catch(e) { setStatus(t('skill_load_failed') + e.message); }
|
||||
}
|
||||
|
||||
@@ -451,12 +459,39 @@ async function openSkillFile(skillName, filePath) {
|
||||
$('previewMd').innerHTML = renderMd(data.content || '');
|
||||
} else {
|
||||
showPreview('code');
|
||||
$('previewCode').textContent = data.content || '';
|
||||
if (['yml','yaml'].includes(ext)) {
|
||||
$('previewCode').textContent = '';
|
||||
$('previewCode').className = 'preview-code hl-yaml';
|
||||
$('previewCode').innerHTML = highlightYAML(data.content || '');
|
||||
} else {
|
||||
$('previewCode').className = 'preview-code';
|
||||
$('previewCode').textContent = data.content || '';
|
||||
}
|
||||
requestAnimationFrame(() => highlightCode());
|
||||
}
|
||||
} catch(e) { setStatus(t('skill_file_load_failed') + e.message); }
|
||||
}
|
||||
|
||||
// ── YAML syntax highlighter with line numbers ──
|
||||
function highlightYAML(text) {
|
||||
const lines = text.split('\n');
|
||||
return lines.map(raw => {
|
||||
// Escape HTML first
|
||||
let line = esc(raw);
|
||||
// Highlight full-line comments
|
||||
if (/^\s*#/.test(line)) {
|
||||
return '<span class="code-line"><span class="hl-comment">' + line + '</span></span>';
|
||||
}
|
||||
// Highlight inline comments (after a value)
|
||||
line = line.replace(/(#.*)$/, '<span class="hl-comment">$1</span>');
|
||||
// Highlight strings (double or single quoted)
|
||||
line = line.replace(/("[^&]*?"|'[^&]*?')/g, '<span class="hl-string">$1</span>');
|
||||
// Highlight key: value pairs — key before the colon
|
||||
line = line.replace(/^(\s*)([\w._-]+)(:)/, '$1<span class="hl-key">$2</span><span class="hl-value">$3</span>');
|
||||
return '<span class="code-line">' + line + '</span>';
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// ── Skill create/edit form ──
|
||||
let _editingSkillName = null;
|
||||
|
||||
@@ -549,9 +584,7 @@ function syncWorkspaceDisplays(){
|
||||
const composerLabel=$('composerWorkspaceLabel');
|
||||
const composerDropdown=$('composerWsDropdown');
|
||||
if(!hasSession && composerDropdown) composerDropdown.classList.remove('open');
|
||||
// Only show workspace label once boot has finished to prevent
|
||||
// flash of "No workspace" before the saved session finishes loading.
|
||||
if(composerLabel) composerLabel.textContent=S._bootReady?label:'';
|
||||
if(composerLabel) composerLabel.textContent=label;
|
||||
if(composerChip){
|
||||
composerChip.disabled=!hasSession;
|
||||
composerChip.title=hasSession?ws:t('no_workspace');
|
||||
@@ -819,11 +852,10 @@ async function loadProfilesPanel() {
|
||||
: `<span class="profile-opt-badge stopped" title="${esc(t('profile_gateway_stopped'))}"></span>`;
|
||||
const isActive = p.name === data.active;
|
||||
const activeBadge = isActive ? `<span style="color:var(--link);font-size:10px;font-weight:600;margin-left:6px">${esc(t('profile_active'))}</span>` : '';
|
||||
const defaultBadge = p.is_default ? ` <span style="opacity:.5">${esc(t('profile_default_label'))}</span>` : '';
|
||||
card.innerHTML = `
|
||||
<div class="profile-card-header">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div class="profile-card-name${isActive ? ' is-active' : ''}">${gwDot}${esc(p.name)}${defaultBadge}${activeBadge}</div>
|
||||
<div class="profile-card-name${isActive ? ' is-active' : ''}">${gwDot}${esc(p.name)}${p.is_default ? ' <span style="opacity:.5">(default)</span>' : ''}${activeBadge}</div>
|
||||
${meta.length ? `<div class="profile-card-meta">${esc(meta.join(' \u00b7 '))}</div>` : `<div class="profile-card-meta">${esc(t('profile_no_configuration'))}</div>`}
|
||||
</div>
|
||||
<div class="profile-card-actions">
|
||||
@@ -834,7 +866,7 @@ async function loadProfilesPanel() {
|
||||
panel.appendChild(card);
|
||||
}
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<div style="color:var(--accent);font-size:12px;padding:12px">${esc(t('error_prefix'))}${esc(e.message)}</div>`;
|
||||
panel.innerHTML = `<div style="color:var(--accent);font-size:12px;padding:12px">Error: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,8 +884,7 @@ function renderProfileDropdown(data) {
|
||||
if (p.skill_count) meta.push(t('profile_skill_count', p.skill_count));
|
||||
const gwDot = `<span class="profile-opt-badge ${p.gateway_running ? 'running' : 'stopped'}"></span>`;
|
||||
const checkmark = p.name === active ? ' <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--link)" stroke-width="3" style="vertical-align:-1px"><polyline points="20 6 9 17 4 12"/></svg>' : '';
|
||||
const defaultBadge = p.is_default ? ` <span style="opacity:.5;font-weight:400">${esc(t('profile_default_label'))}</span>` : '';
|
||||
opt.innerHTML = `<div class="profile-opt-name">${gwDot}${esc(p.name)}${defaultBadge}${checkmark}</div>` +
|
||||
opt.innerHTML = `<div class="profile-opt-name">${gwDot}${esc(p.name)}${p.is_default ? ' <span style="opacity:.5;font-weight:400">(default)</span>' : ''}${checkmark}</div>` +
|
||||
(meta.length ? `<div class="profile-opt-meta">${esc(meta.join(' \u00b7 '))}</div>` : '');
|
||||
opt.onclick = async () => {
|
||||
closeProfileDropdown();
|
||||
@@ -1034,6 +1065,97 @@ async function deleteProfile(name) {
|
||||
} catch (e) { showToast(t('delete_failed') + e.message); }
|
||||
}
|
||||
|
||||
// ── Gateway panel ──
|
||||
let _gatewaysCache = null;
|
||||
|
||||
async function loadGatewaysPanel() {
|
||||
const panel = $('gatewaysPanel');
|
||||
if (!panel) return;
|
||||
try {
|
||||
const data = await api('/api/gateways');
|
||||
_gatewaysCache = data;
|
||||
panel.innerHTML = '';
|
||||
if (!data.gateways || !data.gateways.length) {
|
||||
panel.innerHTML = `<div style="padding:16px;color:var(--muted);font-size:12px">${esc(t('gateways_no_gateways'))}</div>`;
|
||||
return;
|
||||
}
|
||||
for (const gw of data.gateways) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'profile-card';
|
||||
const statusColor = gw.running ? 'var(--link)' : 'var(--muted)';
|
||||
const statusDot = gw.running
|
||||
? `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--link);margin-right:6px"></span>`
|
||||
: `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--muted);margin-right:6px"></span>`;
|
||||
const statusText = gw.running ? esc(t('gateway_running')) : esc(t('gateway_stopped'));
|
||||
const typeMeta = gw.type ? `<span style="opacity:.7">${esc(gw.type)}</span>` : '';
|
||||
card.innerHTML = `
|
||||
<div class="profile-card-header">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div class="profile-card-name">${statusDot}${esc(gw.name)}</div>
|
||||
<div class="profile-card-meta">${statusText}${typeMeta ? ' · ' + typeMeta : ''}</div>
|
||||
</div>
|
||||
<div class="profile-card-actions">
|
||||
${gw.running
|
||||
? `<button class="ws-action-btn danger" onclick="stopGateway('${esc(gw.name)}')" title="${esc(t('gateway_stop_title'))}">${esc(t('gateway_stop'))}</button>`
|
||||
: `<button class="ws-action-btn" onclick="startGateway('${esc(gw.name)}')" title="${esc(t('gateway_start_title'))}">${esc(t('gateway_start'))}</button>`
|
||||
}
|
||||
<button class="ws-action-btn" onclick="restartGateway('${esc(gw.name)}')" title="${esc(t('gateway_restart_title'))}">${esc(t('gateway_restart'))}</button>
|
||||
</div>
|
||||
</div>
|
||||
${gw.info ? `<div style="font-size:11px;color:var(--muted);padding:4px 0 8px">${esc(gw.info)}</div>` : ''}`;
|
||||
panel.appendChild(card);
|
||||
}
|
||||
// Add gateway button
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.className = 'cron-btn run';
|
||||
addBtn.style.cssText = 'width:100%;margin-top:8px;padding:6px';
|
||||
addBtn.innerHTML = `+ ${esc(t('gateway_add'))}`;
|
||||
addBtn.onclick = () => showAddGatewayDialog();
|
||||
panel.appendChild(addBtn);
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<div style="color:var(--accent);font-size:12px;padding:12px">Error: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function startGateway(name) {
|
||||
try {
|
||||
await api('/api/gateway/start', { method: 'POST', body: JSON.stringify({ name }) });
|
||||
showToast(t('gateway_started', name));
|
||||
await loadGatewaysPanel();
|
||||
} catch (e) { showToast(t('gateway_start_failed') + e.message); }
|
||||
}
|
||||
|
||||
async function stopGateway(name) {
|
||||
try {
|
||||
await api('/api/gateway/stop', { method: 'POST', body: JSON.stringify({ name }) });
|
||||
showToast(t('gateway_stopped_msg', name));
|
||||
await loadGatewaysPanel();
|
||||
} catch (e) { showToast(t('gateway_stop_failed') + e.message); }
|
||||
}
|
||||
|
||||
async function restartGateway(name) {
|
||||
try {
|
||||
await api('/api/gateway/restart', { method: 'POST', body: JSON.stringify({ name }) });
|
||||
showToast(t('gateway_restarted', name));
|
||||
await loadGatewaysPanel();
|
||||
} catch (e) { showToast(t('gateway_restart_failed') + e.message); }
|
||||
}
|
||||
|
||||
async function showAddGatewayDialog() {
|
||||
const name = await showPromptDialog({
|
||||
title: t('gateway_add_title'),
|
||||
message: t('gateway_add_message'),
|
||||
confirmLabel: t('gateway_add'),
|
||||
placeholder: 'telegram'
|
||||
});
|
||||
if (!name) return;
|
||||
try {
|
||||
await api('/api/gateway/add', { method: 'POST', body: JSON.stringify({ name, type: 'telegram' }) });
|
||||
showToast(t('gateway_added', name));
|
||||
await loadGatewaysPanel();
|
||||
} catch (e) { showToast(t('gateway_add_failed') + e.message); }
|
||||
}
|
||||
|
||||
// ── Memory panel ──
|
||||
async function loadMemory(force) {
|
||||
const panel = $('memoryPanel');
|
||||
@@ -1041,6 +1163,17 @@ async function loadMemory(force) {
|
||||
const data = await api('/api/memory');
|
||||
_memoryData = data; // cache for edit form
|
||||
const fmtTime = ts => ts ? new Date(ts*1000).toLocaleString() : '';
|
||||
// Mask sensitive data (API tokens, passwords, keys) in displayed memory
|
||||
function maskSensitive(md) {
|
||||
if (!md) return md;
|
||||
return md
|
||||
.replace(/([A-Za-z0-9]{32,})\.(?:[A-Za-z0-9+\/=]{10,})/g, '***REDACTED***') // API tokens like Z.ai
|
||||
.replace(/([Aa]pi[_\s]?[Tt]oken[:\s]*)\S{20,}/g, '$1***REDACTED***') // API Token: xxx
|
||||
.replace(/([Aa]PI[_\s]?[Kk]ey[:\s]*)\S{20,}/g, '$1***REDACTED***') // API Key: xxx
|
||||
.replace(/([Pp]assword[:\s]*)\S{8,}/g, '$1***REDACTED***') // Password: xxx
|
||||
.replace(/([Tt]oken[:\s]*)[a-f0-9]{32,}/g, '$1***REDACTED***') // token: hex
|
||||
.replace(/([Ss]ecret[:\s]*)\S{16,}/g, '$1***REDACTED***'); // secret: xxx
|
||||
}
|
||||
panel.innerHTML = `
|
||||
<div class="memory-section">
|
||||
<div class="memory-section-title">
|
||||
@@ -1048,7 +1181,7 @@ async function loadMemory(force) {
|
||||
<span class="memory-mtime">${fmtTime(data.memory_mtime)}</span>
|
||||
</div>
|
||||
${data.memory
|
||||
? `<div class="memory-content preview-md">${renderMd(data.memory)}</div>`
|
||||
? `<div class="memory-content preview-md">${renderMd(maskSensitive(data.memory))}</div>`
|
||||
: `<div class="memory-empty">${esc(t('no_notes_yet'))}</div>`}
|
||||
</div>
|
||||
<div class="memory-section">
|
||||
@@ -1057,7 +1190,7 @@ async function loadMemory(force) {
|
||||
<span class="memory-mtime">${fmtTime(data.user_mtime)}</span>
|
||||
</div>
|
||||
${data.user
|
||||
? `<div class="memory-content preview-md">${renderMd(data.user)}</div>`
|
||||
? `<div class="memory-content preview-md">${renderMd(maskSensitive(data.user))}</div>`
|
||||
: `<div class="memory-empty">${esc(t('no_profile_yet'))}</div>`}
|
||||
</div>`;
|
||||
} catch(e) { panel.innerHTML = `<div style="color:var(--accent);font-size:12px">${esc(t('error_prefix'))}${esc(e.message)}</div>`; }
|
||||
@@ -1074,14 +1207,13 @@ document.addEventListener('drop',e=>{e.preventDefault();dragCounter=0;wrap.class
|
||||
|
||||
let _settingsDirty = false;
|
||||
let _settingsThemeOnOpen = null; // track theme at open time for discard revert
|
||||
let _settingsSkinOnOpen = null; // track skin at open time for discard revert
|
||||
let _settingsSection = 'conversation';
|
||||
|
||||
function switchSettingsSection(name){
|
||||
const section=(name==='appearance'||name==='preferences'||name==='system')?name:'conversation';
|
||||
const section=(name==='preferences'||name==='system'||name==='gateways')?name:'conversation';
|
||||
_settingsSection=section;
|
||||
const map={conversation:'Conversation',appearance:'Appearance',preferences:'Preferences',system:'System'};
|
||||
['conversation','appearance','preferences','system'].forEach(key=>{
|
||||
const map={conversation:'Conversation',preferences:'Preferences',system:'System',gateways:'Gateways'};
|
||||
['conversation','preferences','system','gateways'].forEach(key=>{
|
||||
const tab=$('settingsTab'+map[key]);
|
||||
const pane=$('settingsPane'+map[key]);
|
||||
const active=key===section;
|
||||
@@ -1119,8 +1251,7 @@ function toggleSettings(){
|
||||
if(!overlay) return;
|
||||
if(overlay.style.display==='none'){
|
||||
_settingsDirty = false;
|
||||
_settingsThemeOnOpen = localStorage.getItem('hermes-theme') || 'dark';
|
||||
_settingsSkinOnOpen = localStorage.getItem('hermes-skin') || 'default';
|
||||
_settingsThemeOnOpen = localStorage.getItem('hermes-theme') || document.documentElement.dataset.theme || 'dark';
|
||||
_settingsSection = 'conversation';
|
||||
overlay.style.display='';
|
||||
loadSettingsPanel();
|
||||
@@ -1160,10 +1291,7 @@ function _revertSettingsPreview(){
|
||||
if(_settingsThemeOnOpen){
|
||||
localStorage.setItem('hermes-theme', _settingsThemeOnOpen);
|
||||
if(typeof _applyTheme==='function') _applyTheme(_settingsThemeOnOpen);
|
||||
}
|
||||
if(_settingsSkinOnOpen){
|
||||
localStorage.setItem('hermes-skin', _settingsSkinOnOpen);
|
||||
if(typeof _applySkin==='function') _applySkin(_settingsSkinOnOpen);
|
||||
else document.documentElement.dataset.theme = _settingsThemeOnOpen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1198,16 +1326,6 @@ function _markSettingsDirty(){
|
||||
async function loadSettingsPanel(){
|
||||
try{
|
||||
const settings=await api('/api/settings');
|
||||
// Hydrate appearance controls first so a slow /api/models request
|
||||
// cannot overwrite an in-progress theme/skin selection.
|
||||
const themeSel=$('settingsTheme');
|
||||
const themeVal=settings.theme||'dark';
|
||||
if(themeSel) themeSel.value=themeVal;
|
||||
if(typeof _syncThemePicker==='function') _syncThemePicker(themeVal);
|
||||
const skinVal=(settings.skin||'default').toLowerCase();
|
||||
const skinSel=$('settingsSkin');
|
||||
if(skinSel) skinSel.value=skinVal;
|
||||
if(typeof _buildSkinPicker==='function') _buildSkinPicker(skinVal);
|
||||
const resolvedLanguage=(typeof resolvePreferredLocale==='function')
|
||||
? resolvePreferredLocale(settings.language, localStorage.getItem('hermes-lang'))
|
||||
: (settings.language || localStorage.getItem('hermes-lang') || 'en');
|
||||
@@ -1239,6 +1357,9 @@ async function loadSettingsPanel(){
|
||||
// Send key preference
|
||||
const sendKeySel=$('settingsSendKey');
|
||||
if(sendKeySel){sendKeySel.value=settings.send_key||'enter';sendKeySel.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
// Theme preference
|
||||
const themeSel=$('settingsTheme');
|
||||
if(themeSel){themeSel.value=settings.theme||'dark';themeSel.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
// Language preference — populate from LOCALES bundle
|
||||
const langSel=$('settingsLanguage');
|
||||
if(langSel){
|
||||
@@ -1280,6 +1401,7 @@ async function loadSettingsPanel(){
|
||||
}catch(e){}
|
||||
_syncHermesPanelSessionActions();
|
||||
switchSettingsSection(_settingsSection);
|
||||
if(_settingsSection==='gateways') loadGatewaysPanel();
|
||||
}catch(e){
|
||||
showToast(t('settings_load_failed')+e.message);
|
||||
}
|
||||
@@ -1293,7 +1415,7 @@ function _setSettingsAuthButtonsVisible(active){
|
||||
}
|
||||
|
||||
function _applySavedSettingsUi(saved, body, opts){
|
||||
const {sendKey,showTokenUsage,showCliSessions,theme,skin,language}=opts;
|
||||
const {sendKey,showTokenUsage,showCliSessions,theme,language}=opts;
|
||||
window._sendKey=sendKey||'enter';
|
||||
window._showTokenUsage=showTokenUsage;
|
||||
window._showCliSessions=showCliSessions;
|
||||
@@ -1311,7 +1433,6 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
_setSettingsAuthButtonsVisible(!!saved.auth_enabled);
|
||||
_settingsDirty=false;
|
||||
_settingsThemeOnOpen=theme;
|
||||
_settingsSkinOnOpen=skin||'default';
|
||||
const bar=$('settingsUnsavedBar');
|
||||
if(bar) bar.style.display='none';
|
||||
renderMessages();
|
||||
@@ -1326,14 +1447,12 @@ async function saveSettings(andClose){
|
||||
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
|
||||
const pw=($('settingsPassword')||{}).value;
|
||||
const theme=($('settingsTheme')||{}).value||'dark';
|
||||
const skin=($('settingsSkin')||{}).value||'default';
|
||||
const language=($('settingsLanguage')||{}).value||'en';
|
||||
const body={};
|
||||
if(model) body.default_model=model;
|
||||
|
||||
if(sendKey) body.send_key=sendKey;
|
||||
body.theme=theme;
|
||||
body.skin=skin;
|
||||
body.language=language;
|
||||
body.show_token_usage=showTokenUsage;
|
||||
body.show_cli_sessions=showCliSessions;
|
||||
@@ -1349,7 +1468,7 @@ async function saveSettings(andClose){
|
||||
if(pw && pw.trim()){
|
||||
try{
|
||||
const saved=await api('/api/settings',{method:'POST',body:JSON.stringify({...body,_set_password:pw.trim()})});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,language});
|
||||
showToast(t(saved.auth_just_enabled?'settings_saved_pw':'settings_saved_pw_updated'));
|
||||
_hideSettingsPanel();
|
||||
return;
|
||||
@@ -1357,7 +1476,7 @@ async function saveSettings(andClose){
|
||||
}
|
||||
try{
|
||||
const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language});
|
||||
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,language});
|
||||
showToast(t('settings_saved'));
|
||||
_hideSettingsPanel();
|
||||
}catch(e){
|
||||
@@ -1368,7 +1487,7 @@ async function saveSettings(andClose){
|
||||
async function signOut(){
|
||||
try{
|
||||
await api('/api/auth/logout',{method:'POST',body:'{}'});
|
||||
window.location.href='login';
|
||||
window.location.href='/login';
|
||||
}catch(e){
|
||||
showToast(t('sign_out_failed')+e.message);
|
||||
}
|
||||
@@ -1492,4 +1611,157 @@ function dismissErrorBanner(){
|
||||
if(banner) banner.style.display='none';
|
||||
}
|
||||
|
||||
// ── Mission Control panel ──
|
||||
let _mcInterval = null;
|
||||
|
||||
async function loadMissionControl() {
|
||||
clearInterval(_mcInterval);
|
||||
await refreshMC();
|
||||
_mcInterval = setInterval(refreshMC, 15000);
|
||||
}
|
||||
|
||||
async function refreshMC() {
|
||||
const [statusRes, tasksRes, feedRes, prioritiesRes] = await Promise.all([
|
||||
api('/api/mc/status'),
|
||||
api('/api/mc/tasks'),
|
||||
api('/api/mc/feed?limit=10'),
|
||||
api('/api/mc/priorities'),
|
||||
]);
|
||||
|
||||
const status = statusRes;
|
||||
const tasks = tasksRes.tasks || [];
|
||||
const feed = feedRes.feed || [];
|
||||
const priorities = prioritiesRes.priorities || [];
|
||||
|
||||
// ── Health Badge ──
|
||||
const h = status.dashboard_health || 'empty';
|
||||
const healthColors = { healthy: '#4caf50', active: '#ff9800', warning: '#ff5722', empty: '#9e9e9e', ok: '#4caf50' };
|
||||
const healthLabels = { healthy: 'Healthy', active: 'Active', warning: 'Warning', empty: 'No Data', ok: 'OK' };
|
||||
const $hb = $('mcHealthBadge');
|
||||
$hb.textContent = healthLabels[h] || 'Unknown';
|
||||
$hb.style.color = healthColors[h] || '#9e9e9e';
|
||||
$hb.style.background = (healthColors[h] || '#9e9e9e') + '22';
|
||||
|
||||
// ── Stats Cards ──
|
||||
const totalTasks = tasks.length;
|
||||
const doneTasks = tasks.filter(t => t.status === 'done').length;
|
||||
const progressPct = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0;
|
||||
|
||||
$('mcTasksCount').textContent = `${doneTasks}/${totalTasks}`;
|
||||
$('mcTasksLabel').textContent = `${doneTasks} done · ${totalTasks - doneTasks} open`;
|
||||
$('mcPrioritiesCount').textContent = priorities.length;
|
||||
$('mcPrioritiesLabel').textContent = `${priorities.filter(p => p.done).length} done`;
|
||||
|
||||
// ── Progress Bar ──
|
||||
$('mcProgressBar').style.width = progressPct + '%';
|
||||
$('mcProgressPct').textContent = progressPct + '%';
|
||||
|
||||
// ── Priority Filters ──
|
||||
const priorityMap = {};
|
||||
priorities.forEach(p => { priorityMap[p.id] = p; });
|
||||
|
||||
const priorityEmoji = { 1: '🔴', 2: '🟠', 3: '🟡', 4: '🟢' };
|
||||
const pfEl = $('mcPriorityFilters');
|
||||
pfEl.innerHTML = [
|
||||
`<button onclick="filterMCTasks('all')" style="background:var(--input-bg);border:1px solid var(--border);border-radius:20px;padding:3px 10px;font-size:10px;cursor:pointer;color:var(--text)" id="mcFilterAll">All</button>`,
|
||||
...priorities.map(p =>
|
||||
`<button onclick="filterMCTasks(${p.id})" style="background:var(--input-bg);border:1px solid var(--border);border-radius:20px;padding:3px 10px;font-size:10px;cursor:pointer;color:var(--text);display:flex;align-items:center;gap:3px" id="mcFilter${p.id}">${priorityEmoji[p.id] || '•'} ${esc(p.name)}</button>`
|
||||
)
|
||||
].join('');
|
||||
|
||||
// ── Tasks List ──
|
||||
const listEl = $('mcTasksList');
|
||||
const activeFilter = window._mcPriorityFilter || 'all';
|
||||
const filteredTasks = activeFilter === 'all' ? tasks : tasks.filter(t => t.priority === activeFilter);
|
||||
|
||||
if (filteredTasks.length === 0) {
|
||||
listEl.innerHTML = '<div style="color:var(--muted);font-size:11px;text-align:center;padding:20px">No tasks yet.<br><span style="font-size:10px">Add one above ↑</span></div>';
|
||||
} else {
|
||||
const statusMeta = {
|
||||
backlog: { icon: '○', color: 'var(--muted)', label: 'Backlog' },
|
||||
progress: { icon: '◐', color: '#ff9800', label: 'In Progress' },
|
||||
done: { icon: '●', color: '#4caf50', label: 'Done' }
|
||||
};
|
||||
listEl.innerHTML = filteredTasks.map(t => {
|
||||
const meta = statusMeta[t.status] || statusMeta.backlog;
|
||||
const p = priorityMap[t.priority] || { name: 'Unknown', color: '#808080' };
|
||||
const emoji = priorityEmoji[t.priority] || '•';
|
||||
return `<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:6px">
|
||||
<div style="display:flex;align-items:flex-start;gap:8px">
|
||||
<span style="font-size:16px;flex-shrink:0;cursor:pointer" onclick="toggleMCTask(${t.id},'${t.status}')" title="Toggle status">${meta.icon}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:12px;color:var(--text);${t.status === 'done' ? 'text-decoration:line-through;opacity:0.5' : ''};word-break:break-word">${esc(t.title)}</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
|
||||
<span style="font-size:10px;${t.status === 'done' ? 'color:#4caf50' : t.status === 'progress' ? 'color:#ff9800' : 'color:var(--muted)'}">${meta.label}</span>
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:${p.color};flex-shrink:0"></span>
|
||||
<span style="font-size:10px;color:var(--muted)">${emoji} ${esc(p.name)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<select onchange="updateMCTask(${t.id}, this.value)" style="background:var(--input-bg);border:1px solid var(--border);border-radius:6px;padding:3px 6px;font-size:9px;color:var(--text);cursor:pointer;flex-shrink:0">
|
||||
<option value="backlog" ${t.status === 'backlog' ? 'selected' : ''}>○ Backlog</option>
|
||||
<option value="progress" ${t.status === 'progress' ? 'selected' : ''}>◐ Progress</option>
|
||||
<option value="done" ${t.status === 'done' ? 'selected' : ''}>● Done</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Feed ──
|
||||
const feedEl = $('mcFeed');
|
||||
if (feed.length === 0) {
|
||||
feedEl.innerHTML = '<div style="opacity:0.5;font-size:10px">No recent activity</div>';
|
||||
} else {
|
||||
feedEl.innerHTML = feed.map(f => {
|
||||
const d = new Date(f.timestamp);
|
||||
const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
return `<div style="padding:2px 0;border-bottom:1px solid var(--border)">
|
||||
<span style="color:var(--text);font-size:10px">${esc(f.event)}</span>
|
||||
<span style="opacity:0.5;font-size:9px;margin-left:6px">${time}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMCTask(id, currentStatus) {
|
||||
const next = currentStatus === 'done' ? 'backlog' : currentStatus === 'backlog' ? 'progress' : 'done';
|
||||
updateMCTask(id, next);
|
||||
}
|
||||
|
||||
function filterMCTasks(priorityId) {
|
||||
window._mcPriorityFilter = priorityId;
|
||||
refreshMC();
|
||||
}
|
||||
|
||||
async function createMCTask() {
|
||||
const title = $('mcNewTaskTitle').value.trim();
|
||||
if (!title) return;
|
||||
const priority = parseInt($('mcNewTaskPriority').value);
|
||||
const status = $('mcNewTaskStatus').value;
|
||||
$('mcNewTaskTitle').value = '';
|
||||
await api('/api/mc/task/create', { title, priority, status });
|
||||
await refreshMC();
|
||||
}
|
||||
|
||||
async function updateMCTask(id, status) {
|
||||
await api('/api/mc/task/update', { id, status });
|
||||
await refreshMC();
|
||||
}
|
||||
|
||||
// ── Priority Management ──
|
||||
async function createMCPriority() {
|
||||
const name = prompt('Priority name:');
|
||||
if (!name) return;
|
||||
const color = prompt('Color (hex, e.g. #ff0000):', '#808080');
|
||||
if (!color) return;
|
||||
await api('/api/mc/priority/create', { name, color });
|
||||
await refreshMC();
|
||||
}
|
||||
|
||||
async function deleteMCPriority(id) {
|
||||
if (!confirm('Delete this priority?')) return;
|
||||
await api('/api/mc/priority/delete', { id });
|
||||
await refreshMC();
|
||||
}
|
||||
|
||||
// Event wiring
|
||||
|
||||
Reference in New Issue
Block a user