diff --git a/image/app/static/app.js b/image/app/static/app.js index c059169..44f421f 100644 --- a/image/app/static/app.js +++ b/image/app/static/app.js @@ -556,13 +556,13 @@ async function renderSpeechModels() { let statusPill; if (status === 'in_sync') { - statusPill = `patches in sync`; + statusPill = `patches in sync`; } else if (status === 'drift') { - statusPill = `spark-control has newer patches`; + statusPill = `spark-control has newer patches`; } else if (status === 'missing') { - statusPill = `patches missing in container`; + statusPill = `patches missing in container`; } else { - statusPill = `unknown`; + statusPill = `unknown`; } const asrLoaded = !!health.asr_loaded; @@ -599,12 +599,12 @@ async function renderSpeechModels() {
Parakeet ASR ${asrModel} - ${asrLoaded ? 'loaded' : 'not loaded'} + ${asrLoaded ? 'loaded' : 'not loaded'}
Sortformer diarizer ${diarModel} - ${diarLoaded ? 'loaded' : 'not loaded'} + ${diarLoaded ? 'loaded' : 'not loaded'}
${fileRows}
@@ -768,6 +768,64 @@ function renderHealth(status) { function renderBanner(status) { el('#setup-banner').classList.toggle('hidden', !!status.configured); + // Dashboard tabs share the same "configured" gate as the rest of the + // body — hidden until SSH is set up, then visible. + const tabs = el('#dashboard-tabs'); + if (tabs) tabs.classList.toggle('hidden', !status.configured); +} + +// ===================== dashboard tabs (LLM / Audio) ===================== + +const TABS_STORAGE_KEY = 'sparkcontrol.dashboard.activeTab'; + +function setupDashboardTabs() { + const buttons = $$('.dashboard-tab'); + if (!buttons.length) return; + + // Restore the last-selected tab, default to "llm" + let saved; + try { saved = localStorage.getItem(TABS_STORAGE_KEY); } catch {} + const initial = saved === 'audio' || saved === 'llm' ? saved : 'llm'; + + function selectTab(name) { + buttons.forEach((b) => { + const active = b.dataset.tab === name; + b.classList.toggle('active', active); + b.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + $$('.tab-content').forEach((c) => { + c.classList.toggle('active', c.id === `tab-${name}`); + }); + try { localStorage.setItem(TABS_STORAGE_KEY, name); } catch {} + } + + buttons.forEach((b) => { + b.addEventListener('click', () => selectTab(b.dataset.tab)); + }); + selectTab(initial); +} + +// ===================== collapsible endpoint card ===================== + +const ENDPOINT_COLLAPSED_KEY = 'sparkcontrol.endpoint.collapsed'; + +function setupEndpointCollapse() { + const panel = el('#endpoint-panel'); + const btn = el('#ep-collapse'); + if (!panel || !btn) return; + // Default: collapsed (most of the time you don't need to see endpoint details) + let collapsed = true; + try { + const v = localStorage.getItem(ENDPOINT_COLLAPSED_KEY); + if (v === 'false') collapsed = false; + else if (v === 'true') collapsed = true; + } catch {} + panel.classList.toggle('collapsed', collapsed); + btn.addEventListener('click', () => { + const nowCollapsed = !panel.classList.contains('collapsed'); + panel.classList.toggle('collapsed', nowCollapsed); + try { localStorage.setItem(ENDPOINT_COLLAPSED_KEY, nowCollapsed ? 'true' : 'false'); } catch {} + }); } function renderSwapPanel() { @@ -1800,6 +1858,8 @@ async function init() { a.classList.remove('hidden'); } } catch {} + setupDashboardTabs(); + setupEndpointCollapse(); await loadModels(); await pollStatus(); await renderServices(); diff --git a/image/app/static/index.html b/image/app/static/index.html index 141e1fc..8accfe4 100644 --- a/image/app/static/index.html +++ b/image/app/static/index.html @@ -44,8 +44,14 @@ -