Initial commit

Photo-based book cataloger with AI identification.
Room → Cabinet → Shelf → Book hierarchy; FastAPI + SQLite backend;
vanilla JS SPA; OpenAI-compatible plugin system for boundary
detection, text recognition, and archive search.
This commit is contained in:
night
2026-03-09 14:11:11 +03:00
committed by Petr Polezhaev
commit 5d5f26c8ae
64 changed files with 8605 additions and 0 deletions

82
static/js/init.js Normal file
View File

@@ -0,0 +1,82 @@
/*
* 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, _batchPollTimer (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(), startBatchPolling(),
* 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 span
const hb = document.getElementById('main-hdr-btns');
if (hb) hb.innerHTML = mainHeaderBtns();
const bb = document.getElementById('main-hdr-batch');
if (bb) bb.innerHTML = vBatchBtn();
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;
_plugins = cfg.plugins || [];
} catch { window._grabPx = 14; window._confidenceThreshold = 0.8; }
}
function startBatchPolling() {
if (_batchPollTimer) clearInterval(_batchPollTimer);
_batchPollTimer = setInterval(async () => {
try {
const st = await req('GET', '/api/batch/status');
_batchState = st;
const bb = document.getElementById('main-hdr-batch');
if (bb) bb.innerHTML = vBatchBtn();
if (!st.running) {
clearInterval(_batchPollTimer); _batchPollTimer = null;
toast(`Batch: ${st.done} done, ${st.errors} errors`);
await loadTree();
}
} catch { /* ignore poll errors */ }
}, 2000);
}
async function loadTree() {
S.tree = await req('GET','/api/tree');
render();
}
// ── Init ──────────────────────────────────────────────────────────────────────
Promise.all([loadConfig(), loadTree()]);