`;
}).join('');
// Tapping a workload name inside an event filters the timeline to it.
body.querySelectorAll('.oumta-tl-workload').forEach(el => {
el.addEventListener('click', () => setTimelineFilter(el.dataset.wlid));
});
}
function setTimelineFilter(workloadId) {
state.timelineFilter = workloadId || null;
renderTimeline();
}
/** Render the workload quick-pick bar. "all" + distinct workloads seen in
* the current timeline, capped at N inline pills — overflow collapses
* into a dropdown. Deduplicated by workloadId, ranked by most recent
* event so the pills you care about stay visible. */
function renderTimelineFilters() {
const host = $('oumta-timeline-filters');
if (!host) return;
// In workload-detail scope (opened via a card click) the filter bar is
// redundant — we're already showing one workload's events.
if (state.data.scopeWorkload) { host.innerHTML = ''; return; }
const seen = new Map(); // workloadId -> earliest-seen index (= recency)
state.data.timeline.forEach((e, i) => {
if (e.workloadId && !seen.has(e.workloadId)) seen.set(e.workloadId, i);
});
const workloads = [...seen.keys()];
if (!workloads.length) { host.innerHTML = ''; return; }
const INLINE_LIMIT = 6;
const inline = workloads.slice(0, INLINE_LIMIT);
const overflow = workloads.slice(INLINE_LIMIT);
const active = state.timelineFilter;
const pill = (id, label, isActive) =>
``;
const overflowActive = overflow.includes(active);
host.innerHTML = [
pill('', 'all', !active),
...inline.map(id => pill(id, workloadShort(id), id === active)),
overflow.length ? `
${overflow.map(id => ``).join('')}
` : '',
].filter(Boolean).join('');
host.querySelectorAll('.oumta-tl-pill[data-wlid]').forEach(btn => {
btn.addEventListener('click', () => setTimelineFilter(btn.dataset.wlid || null));
});
const moreBtn = host.querySelector('[data-tl-more]');
const moreMenu = host.querySelector('.oumta-tl-more-menu');
if (moreBtn && moreMenu) {
moreBtn.addEventListener('click', (e) => {
e.stopPropagation();
moreMenu.classList.toggle('hidden');
});
moreMenu.querySelectorAll('button[data-wlid]').forEach(btn => {
btn.addEventListener('click', () => {
setTimelineFilter(btn.dataset.wlid || null);
moreMenu.classList.add('hidden');
});
});
document.addEventListener('click', (e) => {
if (!moreMenu.contains(e.target) && e.target !== moreBtn) moreMenu.classList.add('hidden');
}, { once: true });
}
}
// ── Workload → detail-as-timeline navigation ──────────────────
async function openWorkload(workloadId) {
state.data.scopeWorkload = workloadId;
const [nsSlug, name] = workloadId.replace('deployment/', '').split('/');
// Pull workload-scoped timeline — this REPLACES the generic one for rendering.
try {
const res = await api(`/api/workloads/${nsSlug}/${name}/timeline`);
state.data.timeline = res.items || [];
} catch (e) { setLive('error', e.message); }
switchView('timeline');
}
$('oumta-timeline-back')?.addEventListener('click', () => {
state.data.scopeWorkload = null;
// Restore the global timeline
api('/api/timeline').then(r => {
state.data.timeline = r.items || [];
renderTimeline();
}).catch(e => setLive('error', e.message));
});
// ── Realtime (SSE) with polling fallback ──────────────────────
//
// Plan: open an EventSource at /api/events?namespace=… — backend
// not implemented yet, so we go straight to polling. When SSE is available
// we can try to connect and fall back on `error`.
function startLive() {
stopLive();
// TODO(api): attempt SSE first:
// state.sseHandle = new EventSource(url);
// state.sseHandle.onmessage = ev => applyLiveEvent(JSON.parse(ev.data));
// state.sseHandle.onerror = () => { stopLive(); startPolling(); setLive('polling'); };
// state.sseHandle.onopen = () => setLive('live');
startPolling();
}
function stopLive() {
if (state.pollHandle) { clearInterval(state.pollHandle); state.pollHandle = null; }
if (state.sseHandle) { state.sseHandle.close(); state.sseHandle = null; }
setLive('off');
}
function startPolling() {
const ms = (CONFIG.pollIntervalSec || 15) * 1000;
state.pollHandle = setInterval(refresh, ms);
setLive('polling');
}
function setLive(mode, detail = '') {
state.liveMode = mode;
const el = $('oumta-live-indicator');
const lbl = $('oumta-live-label');
if (el) el.dataset.state = mode;
if (lbl) {
lbl.textContent = ({
live: 'live',
polling: 'polling',
error: 'error' + (detail ? ' — ' + detail : ''),
off: 'off',
})[mode] || mode;
}
}
// ── Header buttons ───────────────────────────────────────────
$('oumta-close')?.addEventListener('click', hide);
$('oumta-refresh')?.addEventListener('click', refresh);
$('oumta-wl-search')?.addEventListener('input', renderWorkloads);
$('oumta-wl-status')?.addEventListener('change', renderWorkloads);
$('oumta-wl-timerange')?.addEventListener('change', () => {
// TODO(api): forward to /api/workloads?since= once the backend accepts it.
renderWorkloads();
});
// ESC to close
window.addEventListener('keydown', e => {
if (e.key === 'Escape' && isOpen()) {
e.preventDefault(); e.stopPropagation();
hide();
}
}, { capture: true });
// ── Inject header button into hyprcrd top bar ────────────────
function injectHeaderButton() {
const tasksBtn = document.getElementById('tasks-btn');
if (!tasksBtn || document.getElementById('oumta-btn')) return;
const btn = document.createElement('button');
btn.id = 'oumta-btn';
btn.className = 'flex items-center gap-2 px-2.5 py-1.5 text-xs border border-t-border text-t-muted hover:border-t-accent hover:text-t-accent transition-colors';
btn.innerHTML = 'Oumta';
btn.addEventListener('click', toggle);
tasksBtn.parentNode.insertBefore(btn, tasksBtn);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectHeaderButton);
} else {
injectHeaderButton();
}
// ── Helpers ─────────────────────────────────────────────────
function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
function escapeAttr(s) { return escapeHtml(s); }
function formatTime(iso) {
try {
const d = new Date(iso);
return d.toLocaleString();
} catch { return iso; }
}
// ── Boot ────────────────────────────────────────────────────
h?.registerPlugin?.({ name: 'oumta-dashboard', version: '0.1.0' });