diff --git a/lib/collectionWatch.js b/lib/collectionWatch.js index 8061f58..5cce293 100644 --- a/lib/collectionWatch.js +++ b/lib/collectionWatch.js @@ -45,7 +45,8 @@ async function list() { (SELECT COUNT(*)::int FROM watched_collection_items i WHERE i.watch_id = w.id AND i.status IN ('imported', 'partial')) AS imported_count, (SELECT COUNT(*)::int FROM watched_collection_items i WHERE i.watch_id = w.id AND i.status = 'partial') AS partial_count, (SELECT COUNT(*)::int FROM watched_collection_items i WHERE i.watch_id = w.id AND i.status = 'error') AS error_count, - COALESCE(r.recent, '[]'::json) AS recent + COALESCE(r.recent, '[]'::json) AS recent, + COALESCE(q.pending, '[]'::json) AS pending FROM watched_collections w LEFT JOIN projects p ON p.id = w.project_id LEFT JOIN LATERAL ( @@ -54,6 +55,13 @@ async function list() { ORDER BY i.seen_at DESC) AS recent FROM (SELECT * FROM watched_collection_items WHERE watch_id = w.id ORDER BY seen_at DESC LIMIT 5) i ) r ON true + -- Every model still waiting for its files, not just the five most recent. + LEFT JOIN LATERAL ( + SELECT json_agg(json_build_object('model_id', i.model_id, 'title', i.title) ORDER BY i.seen_at DESC) AS pending + FROM (SELECT * FROM watched_collection_items + WHERE watch_id = w.id AND status = 'partial' AND model_id IS NOT NULL + ORDER BY seen_at DESC LIMIT 200) i + ) q ON true ORDER BY w.id`); return rows; } @@ -88,8 +96,14 @@ async function add({ url, aiTag = false, intervalHours = 6 }) { `INSERT INTO watched_collections (site, source_id, url, title, project_id, ai_tag, interval_hours, total, listed, last_status) VALUES ('makerworld', $1, $2, $3, $4, $5, $6, $7, $8, 'checking') RETURNING id`, [sourceId, url, collection.title, project.id, Boolean(aiTag), hours, collection.total, collection.designs.length]); - checkNow(watch.id).catch((err) => console.warn(`Watch ${watch.id} first check failed:`, err.message)); - return get(watch.id); + // The collection was just read, so import straight from it rather than + // fetching the whole thing again. + const added = await get(watch.id); + checking.add(watch.id); + importFound(added, collection) + .catch((err) => console.warn(`Watch ${watch.id} first import failed:`, err.message)) + .finally(() => checking.delete(watch.id)); + return added; } async function update(id, { enabled, aiTag, intervalHours }) { diff --git a/public/app.js b/public/app.js index a4b9e2a..569e9fe 100644 --- a/public/app.js +++ b/public/app.js @@ -2204,6 +2204,12 @@ function watchHtml(w) { ? `` : escapeHtml(i.title || i.design_id)}${i.error ? ` ${escapeHtml(i.error)}` : ''}${(i.error || i.status === 'partial') && i.model_id ? ` ` : ''}`).join('')}` : ''} + ${(w.pending || []).length ? `
+ ${plural(w.partial_count || w.pending.length, 'model')} still without files +
${w.pending.map((i) => ` + + `).join('')}
+
` : ''} `; } @@ -2230,7 +2236,10 @@ async function refreshWatches() { list.innerHTML = watches.length ? watches.map(watchHtml).join('') : '

Nothing watched yet. Paste a MakerWorld collection link above.

'; - if (watches.some((w) => w.last_status === 'checking')) watchPoll = setTimeout(refreshWatches, 5000); + // Keep the page current even between checks: one may start from the schedule, + // another tab, or another browser. refreshWatches stops itself when you leave. + const checking = watches.some((w) => w.last_status === 'checking'); + watchPoll = setTimeout(refreshWatches, checking ? 5000 : 20000); } async function handleAddWatch(e) { diff --git a/public/styles.css b/public/styles.css index 3c4bf57..56c7a9f 100644 --- a/public/styles.css +++ b/public/styles.css @@ -464,6 +464,9 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .watch-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px; } .watch-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: 8px; } .watch-actions .btn-sm { align-self: auto; } +.watch-pending summary { cursor: pointer; font-size: 13px; color: var(--text-muted); } +.watch-pending summary:hover { color: var(--text); } +.watch-pending .watch-recent { margin-top: 6px; } .watch-meta { font-family: var(--mono); font-size: 12px; color: var(--text-dim); } .watch-recent { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--text-muted); max-height: 180px; overflow-y: auto; } #watchStatus.is-error { color: var(--danger); }