diff --git a/public/s/js/admin.js b/public/s/js/admin.js index 566bf8d..d037113 100644 --- a/public/s/js/admin.js +++ b/public/s/js/admin.js @@ -186,6 +186,9 @@ postid: postid }); if (res.success) { + if (window.invalidateItemCache) { + window.invalidateItemCache(postid); + } // New state is the logical opposite of what it was before the API call const isNowFav = !wasAlreadyFav; diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 4e7930b..d63c16a 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -4145,6 +4145,7 @@ window.cancelAnimFrame = (function () { .then(r => r.json()) .then(data => { if (data.success) { + if (window.invalidateItemCache) window.invalidateItemCache(id); const newOc = data.is_oc; ocBtn.dataset.isOc = newOc; ocBtn.setAttribute('title', newOc ? 'Remove OC status' : 'Mark as OC'); @@ -4486,7 +4487,13 @@ window.cancelAnimFrame = (function () { .then(r => r.json()) .then(res => { toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1); - + + // Evict cached item HTML immediately on success so navigating back fetches fresh content, + // even if the user already navigated to a different item while this request was in flight. + if (res && res.success && window.invalidateItemCache) { + window.invalidateItemCache(postid); + } + // Verify we are still on the same post (prevent dynamic/PJAX page leaks) const currentIdStr = window.getCurrentItemId(); const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null; @@ -4502,10 +4509,6 @@ window.cancelAnimFrame = (function () { if (window.renderTags) { window.renderTags(res.tags); } - // Evict the cached item HTML so navigating back fetches fresh content - if (window.invalidateItemCache) { - window.invalidateItemCache(postid); - } } else { revert(); window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error'); @@ -7148,14 +7151,21 @@ window.cancelAnimFrame = (function () { window.loadPageAjax = loadPageAjax; window.loadItemAjax = loadItemAjax; - // Invalidate all itemCacheMap entries that contain a given item ID. - // Call this after mutating an item (e.g. adding a tag) so the next + // Invalidate all itemCacheMap entries that match a given item ID or slug. + // Call this after mutating an item (e.g. rating, favorite, tags, OC) so the next // AJAX navigation fetches fresh HTML instead of serving stale cache. window.invalidateItemCache = (itemId) => { - if (!itemId) return; - const needle = `/${itemId}`; - for (const key of itemCacheMap.keys()) { - if (key.includes(needle)) { + if (itemId === undefined || itemId === null || itemId === '') return; + const strId = String(itemId).trim(); + for (const [key, val] of itemCacheMap.entries()) { + const urlPath = key.split('?')[0]; + const segments = urlPath.split('/').filter(Boolean); + const lastSegment = segments[segments.length - 1]; + if (lastSegment === strId) { + itemCacheMap.delete(key); + continue; + } + if (val && typeof val.html === 'string' && val.html.includes(`data-item-id="${strId}"`)) { itemCacheMap.delete(key); } } @@ -8697,6 +8707,10 @@ class NotificationSystem { return; } + if (window.invalidateItemCache) { + window.invalidateItemCache(data.item_id); + } + // Check if we are currently viewing this item const currentIdStr = window.getCurrentItemId(); const currentId = currentIdStr ? parseInt(currentIdStr, 10) : null; @@ -8780,6 +8794,10 @@ class NotificationSystem { handleFavoritesUpdate(data) { if (!data || !data.item_id) return; + if (window.invalidateItemCache) { + window.invalidateItemCache(data.item_id); + } + // Check if we are currently viewing this item const currentIdStr = window.getCurrentItemId(); if (!currentIdStr || parseInt(currentIdStr, 10) !== parseInt(data.item_id, 10)) return; diff --git a/public/s/js/user.js b/public/s/js/user.js index 8cfc8b7..e0431db 100644 --- a/public/s/js/user.js +++ b/public/s/js/user.js @@ -175,6 +175,9 @@ postid: postid }); if (res.success) { + if (window.invalidateItemCache) { + window.invalidateItemCache(postid); + } // New state is the logical opposite of what it was before the API call const isNowFav = !wasAlreadyFav; diff --git a/src/inc/lib.mjs b/src/inc/lib.mjs index f2dacbc..bc9fffa 100644 --- a/src/inc/lib.mjs +++ b/src/inc/lib.mjs @@ -200,27 +200,27 @@ export default new class { const tagged = +(await db` select count(*) as total from "items" - where id in (select item_id from tags_assign group by item_id) and active = true + where id in (select item_id from tags_assign group by item_id) and active = true and is_deleted = false `)[0].total; const untagged = +(await db` select count(*) as total from "items" - where not exists (select 1 from tags_assign where item_id = items.id) and active = true + where not exists (select 1 from tags_assign where item_id = items.id) and active = true and is_deleted = false `)[0].total; const sfw = +(await db` select count(*) as total from "items" - where id in (select item_id from tags_assign where tag_id = 1 group by item_id) and active = true + where id in (select item_id from tags_assign where tag_id = 1 group by item_id) and active = true and is_deleted = false `)[0].total; const nsfw = +(await db` select count(*) as total from "items" - where id in (select item_id from tags_assign where tag_id = 2 group by item_id) and active = true + where id in (select item_id from tags_assign where tag_id = 2 group by item_id) and active = true and is_deleted = false `)[0].total; const nsfl = cfg.enable_nsfl ? +(await db` select count(*) as total from "items" - where id in (select item_id from tags_assign where tag_id = ${cfg.nsfl_tag_id || 3} group by item_id) and active = true + where id in (select item_id from tags_assign where tag_id = ${cfg.nsfl_tag_id || 3} group by item_id) and active = true and is_deleted = false `)[0].total : 0; const deleted = +(await db` select count(*) as total diff --git a/src/inc/routes/admin.mjs b/src/inc/routes/admin.mjs index cf78a8b..3a73198 100644 --- a/src/inc/routes/admin.mjs +++ b/src/inc/routes/admin.mjs @@ -845,12 +845,16 @@ export default (router, tpl) => { await fs.unlink(path.join(cfg.paths.ca, `${item.id}.webp`)).catch(() => { }); } + await db`DELETE FROM tags_assign WHERE item_id = ${item.id}`; await db`UPDATE items SET is_deleted = true, is_purged = true, active = false WHERE id = ${item.id}`; count++; } catch (e) { console.error(`[CLEANUP] Failed to delete item ${item.id}:`, e.message); } } + + // Clean up any remaining orphan tag assignments for deleted or inactive items + await db`DELETE FROM tags_assign WHERE item_id IN (SELECT id FROM items WHERE is_deleted = true OR active = false)`; } // Log it in audit diff --git a/src/inc/routes/scroller.mjs b/src/inc/routes/scroller.mjs index 8d616db..5c051d7 100644 --- a/src/inc/routes/scroller.mjs +++ b/src/inc/routes/scroller.mjs @@ -115,10 +115,13 @@ export default (router, tpl) => { try { const slug = '%' + lib.slugify(q) + '%'; const rows = await db` - SELECT t.tag, t.normalized, COUNT(ta.item_id) as uses + SELECT t.tag, t.normalized, COUNT(DISTINCT items.id) as uses FROM tags t JOIN tags_assign ta ON ta.tag_id = t.id + JOIN items ON items.id = ta.item_id WHERE t.id > 2 + AND items.active = true + AND items.is_deleted = false AND lower(t.normalized) ILIKE ${slug} GROUP BY t.tag, t.normalized ORDER BY uses DESC diff --git a/src/inc/routes/tag_image.mjs b/src/inc/routes/tag_image.mjs index a9916d5..05ab9bc 100644 --- a/src/inc/routes/tag_image.mjs +++ b/src/inc/routes/tag_image.mjs @@ -33,6 +33,7 @@ export async function regenerateTagImage(tag, mode) { ${modeFilter} WHERE (t.tag = ${tag} OR t.normalized = ${tag}) AND i.active = true + AND i.is_deleted = false AND COALESCE(i.visibility, 0) = 0 ORDER BY RANDOM() LIMIT 3 diff --git a/src/inc/routes/toptags.mjs b/src/inc/routes/toptags.mjs index 5ffb589..7fc55a6 100644 --- a/src/inc/routes/toptags.mjs +++ b/src/inc/routes/toptags.mjs @@ -51,6 +51,7 @@ export default (router, tpl) => { JOIN tags_assign ta ON t.id = ta.tag_id JOIN items ON items.id = ta.item_id WHERE items.active = true + AND items.is_deleted = false AND COALESCE(items.visibility, 0) = 0 AND t.id NOT IN (1, 2) AND ${db.unsafe(modequery)} @@ -75,6 +76,7 @@ export default (router, tpl) => { JOIN items ON items.id = ta.item_id WHERE t.normalized LIKE '%' || ${tag.normalized} || '%' AND items.active = true + AND items.is_deleted = false AND COALESCE(items.visibility, 0) = 0 AND ${db.unsafe(modequery)} ${restrictedFilter} diff --git a/src/inc/routes/user_tags.mjs b/src/inc/routes/user_tags.mjs index 023b02e..e72f924 100644 --- a/src/inc/routes/user_tags.mjs +++ b/src/inc/routes/user_tags.mjs @@ -51,6 +51,7 @@ export default (router, tpl) => { JOIN tags_assign ta ON t.id = ta.tag_id JOIN items ON items.id = ta.item_id WHERE items.active = true + AND items.is_deleted = false AND COALESCE(items.visibility, 0) = 0 AND t.id NOT IN (1, 2) AND ta.user_id = ${userId} @@ -76,6 +77,7 @@ export default (router, tpl) => { JOIN items ON items.id = ta.item_id WHERE t.normalized LIKE '%' || ${tag.normalized} || '%' AND items.active = true + AND items.is_deleted = false AND COALESCE(items.visibility, 0) = 0 AND ta.user_id = ${userId} AND ${db.unsafe(modequery)}