From 9af071df6c008ed308821a85257a05c8f0b394db Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 02:44:58 -0500 Subject: [PATCH] Retheme admin page, improve save feedback, add monitor nav link Admin UI: - Full retro-brutalist theme matching main UI - JetBrains Mono headings, amber accent, 2px borders - Animated dot-grid background + scanlines - Square toggles (was rounded) - Backdrop-filter blur on cards - Nav bar with links to Team, Lab, Logs, Monitor Save feedback: - Every save now verifies the API response (checks d.ok) - Toast shows what was saved: "ollama provider saved / Enabled" - Toast shows details: "Cloud models saved / 3 models configured" - Toast shows timeout details: "Timeouts saved / Global: 300s, 2 overrides" - Failed saves show red toast with error message - Toast fade-out animation Co-Authored-By: Claude Opus 4.6 (1M context) --- llm_team_ui.py | 179 +++++++++++++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 67 deletions(-) diff --git a/llm_team_ui.py b/llm_team_ui.py index a3e6d6f..163cb0a 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -2046,71 +2046,90 @@ ADMIN_HTML = r""" LLM Team - Admin + + +

LLM Team Admin

- +
Providers
@@ -2290,19 +2309,23 @@ function renderTimeouts() { } async function updateProvider(name) { - const prov = {}; - const en = document.getElementById(name+'-enabled'); + var prov = {}; + var en = document.getElementById(name+'-enabled'); if (en) prov.enabled = en.checked; - const url = document.getElementById(name+'-url'); + var url = document.getElementById(name+'-url'); if (url) prov.base_url = url.value; - const to = document.getElementById(name+'-timeout'); + var to = document.getElementById(name+'-timeout'); if (to) prov.timeout = parseInt(to.value) || 120; - const key = document.getElementById(name+'-key'); + var key = document.getElementById(name+'-key'); if (key && key.value) prov.api_key = key.value; - const body = {providers: {}}; + var body = {providers: {}}; body.providers[name] = prov; - await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)}); - toast('Saved'); + try { + var r = await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)}); + var d = await r.json(); + if (d.ok) toast(name + ' provider saved', true, en ? (prov.enabled ? 'Enabled' : 'Disabled') : ''); + else toast('Save failed: ' + (d.error || 'unknown'), false); + } catch(e) { toast('Save failed: ' + e.message, false); } } async function testProvider(name) { @@ -2317,13 +2340,17 @@ async function testProvider(name) { async function toggleOllama(name, enabled) { config.disabled_models = config.disabled_models || []; if (enabled) { - config.disabled_models = config.disabled_models.filter(m => m !== name); + config.disabled_models = config.disabled_models.filter(function(m) { return m !== name; }); } else { if (!config.disabled_models.includes(name)) config.disabled_models.push(name); } - await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, - body:JSON.stringify({disabled_models: config.disabled_models})}); - toast('Model ' + (enabled ? 'enabled' : 'disabled')); + try { + var r = await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify({disabled_models: config.disabled_models})}); + var d = await r.json(); + if (d.ok) toast(name + ' ' + (enabled ? 'enabled' : 'disabled'), true); + else toast('Failed to save model state', false); + } catch(e) { toast('Save error: ' + e.message, false); } } function toggleCloud(idx, enabled) { @@ -2338,9 +2365,13 @@ function removeCloud(idx) { } async function saveCloudModels() { - await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, - body:JSON.stringify({cloud_models: config.cloud_models})}); - toast('Saved'); + try { + var r = await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify({cloud_models: config.cloud_models})}); + var d = await r.json(); + if (d.ok) toast('Cloud models saved', true, (config.cloud_models||[]).length + ' models configured'); + else toast('Save failed', false); + } catch(e) { toast('Save error: ' + e.message, false); } } function showAddCloud() { document.getElementById('add-cloud-modal').style.display = ''; } @@ -2400,12 +2431,17 @@ async function addOR(id, name) { } async function saveTimeouts() { - const g = parseInt(document.getElementById('global-timeout').value) || 300; + var g = parseInt(document.getElementById('global-timeout').value) || 300; config.timeouts = config.timeouts || {}; config.timeouts.global = g; - await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, - body:JSON.stringify({timeouts: config.timeouts})}); - toast('Saved'); + try { + var r = await fetch('/api/admin/config', {method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify({timeouts: config.timeouts})}); + var d = await r.json(); + var perCount = Object.keys((config.timeouts||{}).per_model||{}).length; + if (d.ok) toast('Timeouts saved', true, 'Global: ' + g + 's' + (perCount ? ', ' + perCount + ' overrides' : '')); + else toast('Save failed', false); + } catch(e) { toast('Save error: ' + e.message, false); } } function setModelTimeout(name, val) { @@ -2478,15 +2514,24 @@ async function removeAllowIP(ip) { toast('Removed ' + ip); } -function toast(msg, ok=true) { - const t = document.createElement('div'); +function toast(msg, ok=true, detail) { + var t = document.createElement('div'); t.className = 'toast ' + (ok ? 'ok' : 'err'); - t.textContent = msg; + t.textContent = ok ? '✓ ' + msg : '✗ ' + msg; + if (detail) { + var d = document.createElement('div'); + d.className = 'toast-detail'; + d.textContent = detail; + t.appendChild(d); + } document.body.appendChild(t); - setTimeout(() => t.remove(), 3000); + setTimeout(function() { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(function() { t.remove(); }, 300); }, 3000); } loadConfig(); + +// Background grid +!function(){var c=document.getElementById('bg-grid');if(!c)return;var x=c.getContext('2d');function resize(){c.width=window.innerWidth;c.height=window.innerHeight}resize();window.addEventListener('resize',resize);var t=0;function draw(){x.clearRect(0,0,c.width,c.height);var s=50,ox=(t*0.2)%s,oy=(t*0.1)%s;x.fillStyle='rgba(226,181,90,0.025)';for(var gx=-s+ox;gx