Files
Maven/cmd/mavweb/static/mavweb.js
kami 5fe8f228c1 feat(mavweb): /ecosystem page consuming Nexus/Praxis/Hexis + shell fixes
Add a read-only /ecosystem page that consumes the sibling services'
JSON APIs (Nexus entities, Praxis attention, Hexis capabilities),
fetched concurrently with honest per-panel error states. Siblings stay
headless — mavweb is their human surface (arch §16). Wired via mavweb
-nexus/-praxis/-hexis flags; mavweb joins the ecosystem compose network.

Fix mobile horizontal overflow across all pages: .content is a flex
child with default min-width:auto, so it refused to shrink below the
tables' intrinsic width. min-width:0 lets wide tables pan inside .scroll
instead of dragging the page sideways. Verified via CDP geometry check
(scrollWidth === clientWidth at 430px).

Also includes in-progress Ethos UI redesign, ecosystem deploy compose,
and planning docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:04:23 +04:00

208 lines
7.3 KiB
JavaScript

/* ═══════════════════════════════════════════════
mavweb — shared JS for Ethos workstation shell
Command palette, search, keyboard shortcuts,
inspector, connection status
═══════════════════════════════════════════════ */
(function() {
'use strict';
// ── Keyboard Shortcuts ──
const shortcuts = {
'ctrl+k': 'openPalette',
'ctrl+/': 'openSearch',
'escape': 'closeAll',
'alt+ArrowLeft': 'navigateBack',
'alt+ArrowRight': 'navigateForward',
};
document.addEventListener('keydown', function(e) {
const key = [];
if (e.ctrlKey || e.metaKey) key.push('ctrl');
if (e.altKey) key.push('alt');
if (e.shiftKey) key.push('shift');
key.push(e.key === ' ' ? 'Space' : e.key);
const combo = key.join('+').toLowerCase();
const action = shortcuts[combo];
if (action === 'openPalette') {
e.preventDefault();
openPalette();
} else if (action === 'openSearch') {
e.preventDefault();
openSearch();
} else if (action === 'closeAll') {
closePalette();
closeSearch();
closeInspector();
} else if (action === 'navigateBack') {
history.back();
} else if (action === 'navigateForward') {
history.forward();
}
});
// ── Command Palette ──
let paletteEl = null;
let paletteInput = null;
// Page entries for the command palette — uses ethos-icons.svg sprite
const palettePages = [
{ label: 'Dashboard', url: '/dash', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-grid"/></svg>' },
{ label: 'History', url: '/history', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-clock"/></svg>' },
{ label: 'Rule Trace', url: '/trace', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-wave"/></svg>' },
{ label: 'Notifications', url: '/notifications', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-bell"/></svg>' },
{ label: 'Voice', url: '/', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-mic"/></svg>' },
{ label: 'Tools', url: '/tools', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-settings"/></svg>' },
{ label: 'Passkey', url: '/auth/passkey', icon: '<svg width="14" height="14"><use href="/ethos-icons.svg#i-lock"/></svg>' },
];
function buildPalette() {
if (paletteEl) return;
paletteEl = document.createElement('div');
paletteEl.className = 'cmd-palette-overlay';
paletteEl.innerHTML =
'<div class=cmd-palette>' +
'<div class=cmd-palette-input>' +
'<svg width="14" height="14"><use href="/ethos-icons.svg#i-search"/></svg>' +
'<input type=text placeholder="Go to page or run action…" spellcheck=false autofocus>' +
'</div>' +
'<div class=cmd-palette-list id=paletteList></div>' +
'</div>';
document.body.appendChild(paletteEl);
paletteInput = paletteEl.querySelector('input');
paletteEl.addEventListener('click', function(e) {
if (e.target === paletteEl) closePalette();
});
paletteInput.addEventListener('keydown', function(e) {
if (e.key === 'Escape') { closePalette(); return; }
if (e.key === 'Enter') {
const selected = paletteEl.querySelector('.cmd-palette-item.selected');
if (selected) {
e.preventDefault();
closePalette();
window.location.href = selected.getAttribute('data-url') || selected.getAttribute('href');
}
return;
}
if (e.key === 'ArrowDown') { e.preventDefault(); moveSelection(1); return; }
if (e.key === 'ArrowUp') { e.preventDefault(); moveSelection(-1); return; }
});
paletteInput.addEventListener('input', filterPalette);
}
function moveSelection(dir) {
const items = paletteEl.querySelectorAll('.cmd-palette-item');
if (!items.length) return;
let idx = -1;
for (let i = 0; i < items.length; i++) {
if (items[i].classList.contains('selected')) { idx = i; items[i].classList.remove('selected'); break; }
}
let next = idx === -1 ? 0 : (idx + dir + items.length) % items.length;
items[next].classList.add('selected');
items[next].scrollIntoView({ block: 'nearest' });
}
function filterPalette() {
const q = paletteInput.value.toLowerCase();
const list = paletteEl.querySelector('#paletteList');
let html = '';
const all = palettePages;
for (let i = 0; i < all.length; i++) {
const p = all[i];
if (q && !p.label.toLowerCase().includes(q)) continue;
html += '<a href="' + p.url + '" class="cmd-palette-item' + (i === 0 ? ' selected' : '') + '" data-url="' + p.url + '">' +
'<span class=icon>' + p.icon + '</span>' +
'<span>' + p.label + '</span>' +
'</a>';
}
list.innerHTML = html;
}
window.__openPalette = function openPalette() {
buildPalette();
closeSearch();
paletteEl.classList.add('open');
setTimeout(function() { paletteInput.focus(); paletteInput.select(); }, 50);
filterPalette();
};
function closePalette() {
if (paletteEl) paletteEl.classList.remove('open');
}
window.closePalette = closePalette;
// ── Search (reuses command palette with search context) ──
window.__openSearch = function openSearch() {
openPalette();
if (paletteInput) {
paletteInput.placeholder = 'Search pages, tools, history…';
}
};
function closeSearch() {
// search uses the same palette overlay
if (paletteInput) paletteInput.placeholder = 'Go to page or run action…';
}
// ── Close all overlays ──
function closeAll() {
closePalette();
closeInspector();
}
// ── Inspector ──
window.openInspector = function openInspector(title, content) {
var inspector = document.getElementById('inspector');
var titleEl = document.getElementById('inspectorTitle');
var bodyEl = document.getElementById('inspectorBody');
if (!inspector || !titleEl || !bodyEl) return;
titleEl.textContent = title || 'Details';
bodyEl.innerHTML = content || '';
inspector.classList.add('open');
};
window.closeInspector = function closeInspector() {
var inspector = document.getElementById('inspector');
if (inspector) inspector.classList.remove('open');
};
// ── Connection Status ──
function updateConnStatus() {
var dot = document.getElementById('connDot');
if (!dot) return;
fetch('/api/ping')
.then(function(r) {
dot.className = 'dot ' + (r.ok ? 'online' : 'offline');
})
.catch(function() {
dot.className = 'dot offline';
});
}
// Check connection on load and every 60s
updateConnStatus();
setInterval(updateConnStatus, 60000);
// ── Inspector: row click to open detail ──
// Attach click handler to tables for rows with data attributes
document.addEventListener('click', function(e) {
var row = e.target.closest('tr.clickable');
if (!row) return;
var detailUrl = row.getAttribute('data-detail-url');
if (detailUrl) {
fetch(detailUrl)
.then(function(r) { return r.text(); })
.then(function(html) {
openInspector(row.getAttribute('data-detail-title') || 'Details', html);
})
.catch(function() {});
}
});
})();