Add per-request AI logging, DB batch queue, WS entity updates, and UI polish
- log_thread.py: thread-safe ContextVar bridge so executor threads can log
individual LLM calls and archive searches back to the event loop
- ai_log.py: init_thread_logging(), notify_entity_update(); WS now pushes
entity_update messages when book data changes after any plugin or batch run
- batch.py: replace batch_pending.json with batch_queue SQLite table;
run_batch_consumer() reads queue dynamically so new books can be added
while batch is running; add_to_queue() deduplicates
- migrate.py: fix _migrate_v1 (clear-on-startup bug); add _migrate_v2 for
batch_queue table
- _client.py / archive.py / identification.py: wrap each LLM API call and
archive search with log_thread start/finish entries
- api.py: POST /api/batch returns {already_running, added}; notify_entity_update
after identify pipeline
- models.default.yaml: strengthen ai_identify confidence-scoring instructions;
warn against placeholder data
- detail-render.js: book log entries show clickable ID + spine thumbnail;
book spine/title images open full-screen popup
- events.js: batch-start handles already_running+added; open-img-popup action
- init.js: entity_update WS handler; image popup close listeners
- overlays.css / index.html: full-screen image popup overlay
- eslint.config.js: add new globals; fix no-redeclare/no-unused-vars for
multi-file global architecture; all lint errors resolved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,54 +12,84 @@
|
||||
* Provides: attachEditables(), initSortables()
|
||||
*/
|
||||
|
||||
/* exported attachEditables, initSortables */
|
||||
|
||||
// ── SortableJS instances (destroyed and recreated on each render) ─────────────
|
||||
let _sortables = [];
|
||||
|
||||
// ── Inline name editing ──────────────────────────────────────────────────────
|
||||
function attachEditables() {
|
||||
document.querySelectorAll('[contenteditable=true]').forEach(el => {
|
||||
document.querySelectorAll('[contenteditable=true]').forEach((el) => {
|
||||
el.dataset.orig = el.textContent.trim();
|
||||
el.addEventListener('keydown', e => {
|
||||
if (e.key==='Enter') { e.preventDefault(); el.blur(); }
|
||||
if (e.key==='Escape') { el.textContent=el.dataset.orig; el.blur(); }
|
||||
el.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
el.blur();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
el.textContent = el.dataset.orig;
|
||||
el.blur();
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
el.addEventListener('blur', async () => {
|
||||
const val = el.textContent.trim();
|
||||
if (!val||val===el.dataset.orig) { if(!val) el.textContent=el.dataset.orig; return; }
|
||||
const newName = val.replace(/^[🏠📚]\s*/u,'').trim();
|
||||
const {type, id} = el.dataset;
|
||||
const url = {room:`/api/rooms/${id}`,cabinet:`/api/cabinets/${id}`,shelf:`/api/shelves/${id}`}[type];
|
||||
if (!val || val === el.dataset.orig) {
|
||||
if (!val) el.textContent = el.dataset.orig;
|
||||
return;
|
||||
}
|
||||
const newName = val.replace(/^[🏠📚]\s*/u, '').trim();
|
||||
const { type, id } = el.dataset;
|
||||
const url = { room: `/api/rooms/${id}`, cabinet: `/api/cabinets/${id}`, shelf: `/api/shelves/${id}` }[type];
|
||||
if (!url) return;
|
||||
try {
|
||||
await req('PUT', url, {name: newName});
|
||||
await req('PUT', url, { name: newName });
|
||||
el.dataset.orig = el.textContent.trim();
|
||||
walkTree(n=>{ if(n.id===id) n.name=newName; });
|
||||
walkTree((n) => {
|
||||
if (n.id === id) n.name = newName;
|
||||
});
|
||||
// Update sidebar label if editing from header (sidebar has non-editable nname spans)
|
||||
const sideLabel = document.querySelector(`.node[data-id="${id}"] .nname`);
|
||||
if (sideLabel && sideLabel !== el) {
|
||||
const prefix = type==='room' ? '🏠 ' : type==='cabinet' ? '📚 ' : '';
|
||||
const prefix = type === 'room' ? '🏠 ' : type === 'cabinet' ? '📚 ' : '';
|
||||
sideLabel.textContent = prefix + newName;
|
||||
}
|
||||
} catch(err) { el.textContent=el.dataset.orig; toast('Rename failed: '+err.message); }
|
||||
} catch (err) {
|
||||
el.textContent = el.dataset.orig;
|
||||
toast('Rename failed: ' + err.message);
|
||||
}
|
||||
});
|
||||
el.addEventListener('click', e=>e.stopPropagation());
|
||||
el.addEventListener('click', (e) => e.stopPropagation());
|
||||
});
|
||||
}
|
||||
|
||||
// ── SortableJS drag-and-drop ─────────────────────────────────────────────────
|
||||
function initSortables() {
|
||||
_sortables.forEach(s=>{ try{s.destroy();}catch(_){} });
|
||||
_sortables.forEach((s) => {
|
||||
try {
|
||||
s.destroy();
|
||||
} catch (_) {
|
||||
// ignore destroy errors on stale instances
|
||||
}
|
||||
});
|
||||
_sortables = [];
|
||||
document.querySelectorAll('.sortable-list').forEach(el => {
|
||||
document.querySelectorAll('.sortable-list').forEach((el) => {
|
||||
const type = el.dataset.type;
|
||||
_sortables.push(Sortable.create(el, {
|
||||
handle:'.drag-h', animation:120, ghostClass:'drag-ghost',
|
||||
onEnd: async () => {
|
||||
const ids = [...el.querySelectorAll(':scope > .node')].map(n=>n.dataset.id);
|
||||
try { await req('PATCH',`/api/${type}/reorder`,{ids}); }
|
||||
catch(err) { toast('Reorder failed'); await loadTree(); }
|
||||
},
|
||||
}));
|
||||
_sortables.push(
|
||||
Sortable.create(el, {
|
||||
handle: '.drag-h',
|
||||
animation: 120,
|
||||
ghostClass: 'drag-ghost',
|
||||
onEnd: async () => {
|
||||
const ids = [...el.querySelectorAll(':scope > .node')].map((n) => n.dataset.id);
|
||||
try {
|
||||
await req('PATCH', `/api/${type}/reorder`, { ids });
|
||||
} catch (_err) {
|
||||
toast('Reorder failed');
|
||||
await loadTree();
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user