/* * init.js * Application bootstrap: full render, partial detail re-render, config and * tree loading, batch-status polling, and the initial Promise.all boot call. * * render() is the single source of truth for full repaints — it replaces * #app innerHTML, re-attaches editables, reinitialises Sortable instances, * and (on desktop) schedules the boundary canvas setup. * * renderDetail() does a cheaper in-place update of the right panel only, * used during plugin runs and field edits to avoid re-rendering the sidebar. * * Depends on: S, _plugins, _batchState, _batchWs (state.js); * req, toast (api.js / helpers.js); isDesktop (helpers.js); * vApp, vDetailBody, mainTitle, mainHeaderBtns, vBatchBtn * (tree-render.js / detail-render.js); * attachEditables, initSortables (editing.js); * setupDetailCanvas (canvas-boundary.js) * Provides: render(), renderDetail(), loadConfig(), connectBatchWs(), * loadTree() */ /* exported render, renderDetail, connectBatchWs, connectAiLogWs, loadTree */ // ── Full re-render ──────────────────────────────────────────────────────────── function render() { if (document.activeElement?.contentEditable === 'true') return; const sy = window.scrollY; document.getElementById('app').innerHTML = vApp(); window.scrollTo(0, sy); attachEditables(); initSortables(); if (isDesktop()) requestAnimationFrame(setupDetailCanvas); } // ── Right-panel partial re-render ───────────────────────────────────────────── // Used during plugin runs and field edits to avoid re-rendering the sidebar. function renderDetail() { const body = document.getElementById('main-body'); if (body) body.innerHTML = vDetailBody(); const t = document.getElementById('main-title'); if (t) t.innerHTML = mainTitle(); // innerHTML: mainTitle() returns an HTML string const hb = document.getElementById('main-hdr-btns'); if (hb) hb.innerHTML = mainHeaderBtns(); attachEditables(); // pick up the new editable span in the header requestAnimationFrame(setupDetailCanvas); } // ── Data loading ────────────────────────────────────────────────────────────── async function loadConfig() { try { const cfg = await req('GET', '/api/config'); window._grabPx = cfg.boundary_grab_px ?? 14; window._confidenceThreshold = cfg.confidence_threshold ?? 0.8; window._aiLogMax = cfg.ai_log_max_entries ?? 100; _plugins = cfg.plugins || []; } catch { window._grabPx = 14; window._confidenceThreshold = 0.8; window._aiLogMax = 100; } } function connectBatchWs() { if (_batchWs) { _batchWs.close(); _batchWs = null; } const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${proto}//${location.host}/ws/batch`); _batchWs = ws; ws.onmessage = async (ev) => { const st = JSON.parse(ev.data); _batchState = st; const bb = document.getElementById('main-hdr-batch'); if (bb) bb.innerHTML = vBatchBtn(); if (!st.running) { ws.close(); _batchWs = null; toast(`Batch: ${st.done} done, ${st.errors} errors`); await loadTree(); } }; ws.onerror = () => { _batchWs = null; }; ws.onclose = () => { _batchWs = null; }; } function connectAiLogWs() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${proto}//${location.host}/ws/ai-log`); _aiLogWs = ws; ws.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.type === 'snapshot') { _aiLog = msg.entries || []; } else if (msg.type === 'update') { const entry = msg.entry; const idx = _aiLog.findIndex((e) => e.id === entry.id); if (idx >= 0) { _aiLog[idx] = entry; } else { _aiLog.push(entry); const max = window._aiLogMax ?? 100; if (_aiLog.length > max) _aiLog.splice(0, _aiLog.length - max); } } else if (msg.type === 'entity_update') { const etype = msg.entity_type.slice(0, -1); // "books" → "book" walkTree((n) => { if (n.id === msg.entity_id) Object.assign(n, msg.data); }); if (S.selected && S.selected.type === etype && S.selected.id === msg.entity_id) { renderDetail(); } else { render(); // update sidebar badges } return; // skip AI indicator update — not a log entry } // Update header AI indicator const hdr = document.getElementById('hdr-ai-indicator'); if (hdr) { const running = _aiLog.filter((e) => e.status === 'running').length; hdr.innerHTML = running > 0 ? vAiIndicator(running) : ''; } // Update root detail panel if shown if (!S.selected) renderDetail(); }; ws.onerror = () => {}; ws.onclose = () => { // Reconnect after a short delay setTimeout(connectAiLogWs, 3000); }; } async function loadTree() { S.tree = await req('GET', '/api/tree'); render(); } // ── Init ────────────────────────────────────────────────────────────────────── // Image popup: close when clicking the overlay background or the × button. (function () { const popup = document.getElementById('img-popup'); const closeBtn = document.getElementById('img-popup-close'); if (popup) { popup.addEventListener('click', (e) => { if (e.target === popup) popup.classList.remove('open'); }); } if (closeBtn) { closeBtn.addEventListener('click', () => popup && popup.classList.remove('open')); } })(); Promise.all([loadConfig(), loadTree()]).then(() => connectAiLogWs());