Files
Void-Homelab/public/components/topbar.js
root e2be462ecb fix(dross): collapse shell to 2 columns; topbar ◆ summons Dross
Removing #rightrail left a dead 360px grid column that narrowed #main.
Shell grid is now sidebar+main; the topbar ◆ (was Toggle-companion-rail)
now dispatches dross-toggle to open/close the floating bubble. Remaining
.rail-* CSS + chrome.js toggleRail are dead no-ops (minor cleanup later).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:34:39 +10:00

87 lines
3.5 KiB
JavaScript

// Topbar: brand, global search (Enter → /search?q=), capture button stub,
// pending bell with badge, user/agent toggle stub.
import { el, mount, clear } from '../dom.js';
import { navigate } from '../router.js';
import { on } from '../state.js';
import { toggleSidebar } from './chrome.js';
import { api } from '../api.js';
// Cluster health → topbar pill. Returns [status, label, title].
function classifyCluster(c) {
if (!c || c.error) return ['unknown', 'cluster ?', 'Cluster status unavailable'];
if (!c.quorate) return ['down', 'no quorum', 'Cluster has LOST quorum'];
if ((c.nodes_online ?? 0) < (c.nodes_total ?? 0)) return ['down', 'node down', `${c.nodes_online}/${c.nodes_total} nodes online`];
if (c.ha && c.ha.services_error > 0) return ['warn', 'HA issue', `${c.ha.services_error} HA service(s) in error`];
return ['ok', 'healthy', `Quorate · ${c.nodes_online}/${c.nodes_total} nodes · HA ok`];
}
function startClusterHealth(pill, labelEl) {
async function tick() {
let c = null;
try { c = await api.get('/api/cluster'); } catch { c = { error: 'fetch' }; }
const [status, label, title] = classifyCluster(c);
pill.className = 'icon-btn cluster-health status-' + status;
pill.title = title;
labelEl.textContent = label;
}
tick();
setInterval(tick, 30000);
}
function captureModal() {
const root = document.getElementById('modal-root');
mount(root,
el('div', { class: 'modal-backdrop', onclick: (e) => { if (e.target.classList.contains('modal-backdrop')) clear(root); } },
el('div', { class: 'modal' },
el('h2', {}, 'Universal Capture'),
el('p', { class: 'muted' },
'Drag a URL, paste a YouTube link, drop a PDF. ' +
'Plan 3 wires the capture queue + workers — this surface is here so the UX shape is honest about what is coming.'
),
el('div', { class: 'actions' },
el('button', { class: 'ghost', onclick: () => clear(root) }, 'Close')
)
)
)
);
}
export function renderTopbar(root) {
const searchInput = el('input', {
type: 'text',
placeholder: 'Search … (Enter to search)',
onkeydown: (e) => {
if (e.key === 'Enter' && e.target.value.trim()) {
navigate('/search?q=' + encodeURIComponent(e.target.value.trim()));
}
}
});
const bell = el('button', { class: 'icon-btn', onclick: () => navigate('/inbox') }, 'Inbox');
const chLabel = el('span', { class: 'ch-label' }, '…');
const clusterPill = el('button', { class: 'icon-btn cluster-health status-unknown', title: 'Cluster health', onclick: () => navigate('/sacred-valley') },
el('span', { class: 'dot' }), chLabel);
mount(root,
el('button', { class: 'chrome-toggle', title: 'Toggle menu', onclick: toggleSidebar }, '☰'),
el('div', { class: 'brand' }, 'VOID'),
el('button', { class: 'icon-btn', onclick: captureModal }, '+ Capture'),
el('div', { class: 'topbar-search' }, searchInput),
el('div', { class: 'topbar-spacer' }),
clusterPill,
bell,
el('button', { class: 'chrome-toggle', title: 'Summon Dross', onclick: () => window.dispatchEvent(new CustomEvent('dross-toggle')) }, '◆'),
el('button', { class: 'icon-btn', onclick: () => alert('Agent-switching ships post-Plan-2.') }, 'Owner')
);
startClusterHealth(clusterPill, chLabel);
on('pending-count', (n) => {
const old = bell.querySelector('.badge');
if (old) old.remove();
if (n > 0) bell.appendChild(el('span', { class: 'badge' }, String(n)));
});
}