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:
@@ -21,6 +21,8 @@
|
||||
* Provides: collectQueueBooks(), renderPhotoQueue(), triggerPhoto()
|
||||
*/
|
||||
|
||||
/* exported collectQueueBooks, renderPhotoQueue, triggerPhoto */
|
||||
|
||||
// ── Photo Queue ──────────────────────────────────────────────────────────────
|
||||
function collectQueueBooks(node, type) {
|
||||
const books = [];
|
||||
@@ -29,9 +31,9 @@ function collectQueueBooks(node, type) {
|
||||
if (n.identification_status !== 'user_approved') books.push(n);
|
||||
return;
|
||||
}
|
||||
if (t === 'room') n.cabinets.forEach(c => collect(c, 'cabinet'));
|
||||
if (t === 'cabinet') n.shelves.forEach(s => collect(s, 'shelf'));
|
||||
if (t === 'shelf') n.books.forEach(b => collect(b, 'book'));
|
||||
if (t === 'room') n.cabinets.forEach((c) => collect(c, 'cabinet'));
|
||||
if (t === 'cabinet') n.shelves.forEach((s) => collect(s, 'shelf'));
|
||||
if (t === 'shelf') n.books.forEach((b) => collect(b, 'book'));
|
||||
}
|
||||
collect(node, type);
|
||||
return books;
|
||||
@@ -40,8 +42,12 @@ function collectQueueBooks(node, type) {
|
||||
function renderPhotoQueue() {
|
||||
const el = document.getElementById('photo-queue-overlay');
|
||||
if (!el) return;
|
||||
if (!_photoQueue) { el.style.display = 'none'; el.innerHTML = ''; return; }
|
||||
const {books, index, processing} = _photoQueue;
|
||||
if (!_photoQueue) {
|
||||
el.style.display = 'none';
|
||||
el.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const { books, index, processing } = _photoQueue;
|
||||
el.style.display = 'flex';
|
||||
if (index >= books.length) {
|
||||
el.innerHTML = `<div class="pq-hdr">
|
||||
@@ -79,8 +85,8 @@ function renderPhotoQueue() {
|
||||
const gphoto = document.getElementById('gphoto');
|
||||
|
||||
function triggerPhoto(type, id) {
|
||||
S._photoTarget = {type, id};
|
||||
if (/Android|iPhone|iPad/i.test(navigator.userAgent)) gphoto.setAttribute('capture','environment');
|
||||
S._photoTarget = { type, id };
|
||||
if (/Android|iPhone|iPad/i.test(navigator.userAgent)) gphoto.setAttribute('capture', 'environment');
|
||||
else gphoto.removeAttribute('capture');
|
||||
gphoto.value = '';
|
||||
gphoto.click();
|
||||
@@ -89,20 +95,22 @@ function triggerPhoto(type, id) {
|
||||
gphoto.addEventListener('change', async () => {
|
||||
const file = gphoto.files[0];
|
||||
if (!file || !S._photoTarget) return;
|
||||
const {type, id} = S._photoTarget;
|
||||
const { type, id } = S._photoTarget;
|
||||
S._photoTarget = null;
|
||||
const fd = new FormData();
|
||||
fd.append('image', file, file.name); // HD — no client-side compression
|
||||
fd.append('image', file, file.name); // HD — no client-side compression
|
||||
const urls = {
|
||||
room: `/api/rooms/${id}/photo`,
|
||||
room: `/api/rooms/${id}/photo`,
|
||||
cabinet: `/api/cabinets/${id}/photo`,
|
||||
shelf: `/api/shelves/${id}/photo`,
|
||||
book: `/api/books/${id}/photo`,
|
||||
shelf: `/api/shelves/${id}/photo`,
|
||||
book: `/api/books/${id}/photo`,
|
||||
};
|
||||
try {
|
||||
const res = await req('POST', urls[type], fd, true);
|
||||
const key = type==='book' ? 'image_filename' : 'photo_filename';
|
||||
walkTree(n=>{ if(n.id===id) n[key]=res[key]; });
|
||||
const key = type === 'book' ? 'image_filename' : 'photo_filename';
|
||||
walkTree((n) => {
|
||||
if (n.id === id) n[key] = res[key];
|
||||
});
|
||||
// Photo queue mode: process and advance without full re-render
|
||||
if (_photoQueue && type === 'book') {
|
||||
_photoQueue.processing = true;
|
||||
@@ -111,8 +119,12 @@ gphoto.addEventListener('change', async () => {
|
||||
if (book && book.identification_status !== 'user_approved') {
|
||||
try {
|
||||
const br = await req('POST', `/api/books/${id}/process`);
|
||||
walkTree(n => { if (n.id === id) Object.assign(n, br); });
|
||||
} catch { /* continue queue on process error */ }
|
||||
walkTree((n) => {
|
||||
if (n.id === id) Object.assign(n, br);
|
||||
});
|
||||
} catch {
|
||||
/* continue queue on process error */
|
||||
}
|
||||
}
|
||||
_photoQueue.processing = false;
|
||||
_photoQueue.index++;
|
||||
@@ -127,12 +139,24 @@ gphoto.addEventListener('change', async () => {
|
||||
if (book && book.identification_status !== 'user_approved') {
|
||||
try {
|
||||
const br = await req('POST', `/api/books/${id}/process`);
|
||||
walkTree(n => { if(n.id===id) Object.assign(n, br); });
|
||||
walkTree((n) => {
|
||||
if (n.id === id) Object.assign(n, br);
|
||||
});
|
||||
toast(`Photo saved · Identified (${br.identification_status})`);
|
||||
render();
|
||||
} catch { toast('Photo saved'); }
|
||||
} else { toast('Photo saved'); }
|
||||
} else { toast('Photo saved'); }
|
||||
} else { toast('Photo saved'); }
|
||||
} catch(err) { toast('Upload failed: '+err.message); }
|
||||
} catch {
|
||||
toast('Photo saved');
|
||||
}
|
||||
} else {
|
||||
toast('Photo saved');
|
||||
}
|
||||
} else {
|
||||
toast('Photo saved');
|
||||
}
|
||||
} else {
|
||||
toast('Photo saved');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('Upload failed: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user