From 1aa7350c4d141b372189528a595c8796aa835108 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Fri, 3 Apr 2026 03:08:38 +0000 Subject: [PATCH] Add maintenance: purge orphaned source image KV entries When a source image file is deleted without going through the API (e.g. the old bin delete bug), the KV entry remains and shows a broken image. The new maintenance action scans all source image entries, removes any whose file is missing on disk, and reports how many were cleaned up. Co-Authored-By: Claude Sonnet 4.6 --- public/app.mjs | 19 +++++++++++++++++++ public/index.html | 1 + public/lib/api.mjs | 3 ++- server.mjs | 14 +++++++++++++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/public/app.mjs b/public/app.mjs index 19e7471..4ee9202 100644 --- a/public/app.mjs +++ b/public/app.mjs @@ -2509,6 +2509,25 @@ async function init() { } }); + document.getElementById('maint-purge-sources').addEventListener('click', async () => { + maint_dropdown.hidden = true; + maint_toggle.textContent = '⏳'; + maint_toggle.disabled = true; + try { + const result = await api.maintenance_purge_missing_sources(); + if (result.removed.length > 0) { + all_sources = all_sources.filter(s => !result.removed.includes(s.id)); + render(); + } + alert(`Removed ${result.removed.length} orphaned entr${result.removed.length === 1 ? 'y' : 'ies'} of ${result.total} checked.`); + } catch (err) { + alert(`Error: ${err.message}`); + } finally { + maint_toggle.textContent = '⚙'; + maint_toggle.disabled = false; + } + }); + window.addEventListener('popstate', () => { parse_url(); render(); }); await load_all(); diff --git a/public/index.html b/public/index.html index 522f603..b2462ae 100644 --- a/public/index.html +++ b/public/index.html @@ -23,6 +23,7 @@ diff --git a/public/lib/api.mjs b/public/lib/api.mjs index edad102..458ef4d 100644 --- a/public/lib/api.mjs +++ b/public/lib/api.mjs @@ -86,7 +86,8 @@ export async function upload_bin(file, name) { } // Maintenance -export const maintenance_pdf_thumbs = () => req('POST', '/api/maintenance/pdf-thumbs'); +export const maintenance_pdf_thumbs = () => req('POST', '/api/maintenance/pdf-thumbs'); +export const maintenance_purge_missing_sources = () => req('POST', '/api/maintenance/purge-missing-sources'); // Grid images export const get_grids = () => req('GET', '/api/grid-images'); diff --git a/server.mjs b/server.mjs index bf428a0..ce30114 100644 --- a/server.mjs +++ b/server.mjs @@ -3,7 +3,7 @@ process.on('uncaughtException', (err) => { console.error('[uncaughtException import express from 'express'; import multer from 'multer'; -import { unlinkSync, mkdirSync } from 'node:fs'; +import { unlinkSync, mkdirSync, existsSync } from 'node:fs'; import { extname, join } from 'node:path'; import { execFileSync } from 'node:child_process'; import sharp from 'sharp'; @@ -634,6 +634,18 @@ app.delete('/api/pdfs/:id', (req, res) => { // Maintenance // --------------------------------------------------------------------------- +app.post('/api/maintenance/purge-missing-sources', (req, res) => { + const sources = list_source_images(); + const removed = []; + for (const src of sources) { + if (!existsSync(join('./data/images', src.id))) { + delete_source_image(src.id); + removed.push(src.id); + } + } + ok(res, { removed, total: sources.length }); +}); + app.post('/api/maintenance/pdf-thumbs', (req, res) => { const pdfs = list_pdfs(); let generated = 0;