From 8e0f7eb4d8fad0a0f9411eb1ddf84ec42ba7f086 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Sun, 22 Mar 2026 00:41:40 +0000 Subject: [PATCH] =?UTF-8?q?Add=20maintenance=20menu=20(top-right=20?= =?UTF-8?q?=E2=9A=99)=20with=20generate=20missing=20PDF=20thumbnails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- public/app.mjs | 27 ++++++++++++++++++++++ public/index.html | 6 +++++ public/lib/api.mjs | 3 +++ public/style.css | 56 ++++++++++++++++++++++++++++++++++++++++++++++ server.mjs | 19 ++++++++++++++++ 5 files changed, 111 insertions(+) diff --git a/public/app.mjs b/public/app.mjs index 7e319e3..2badb0a 100644 --- a/public/app.mjs +++ b/public/app.mjs @@ -1940,6 +1940,33 @@ async function init() { btn.addEventListener('click', () => navigate('/' + btn.dataset.section)); }); + // Maintenance menu + const maint_toggle = document.getElementById('maint-toggle'); + const maint_dropdown = document.getElementById('maint-dropdown'); + maint_toggle.addEventListener('click', (e) => { + e.stopPropagation(); + maint_dropdown.hidden = !maint_dropdown.hidden; + }); + document.addEventListener('click', () => { maint_dropdown.hidden = true; }); + + document.getElementById('maint-gen-thumbs').addEventListener('click', async () => { + maint_dropdown.hidden = true; + maint_toggle.textContent = '⏳'; + maint_toggle.disabled = true; + try { + const result = await api.maintenance_pdf_thumbs(); + const refreshed = await api.get_pdfs(); + all_pdfs = refreshed.pdfs; + alert(`Generated ${result.generated} thumbnail(s) of ${result.total} PDF(s).`); + render(); + } 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 3470641..6979e0b 100644 --- a/public/index.html +++ b/public/index.html @@ -17,6 +17,12 @@ +
+ + +
diff --git a/public/lib/api.mjs b/public/lib/api.mjs index e7bb470..936f699 100644 --- a/public/lib/api.mjs +++ b/public/lib/api.mjs @@ -59,6 +59,9 @@ export async function upload_pdf(file, display_name) { return data; } +// Maintenance +export const maintenance_pdf_thumbs = () => req('POST', '/api/maintenance/pdf-thumbs'); + // Grid images export const get_grids = () => req('GET', '/api/grid-images'); export const get_grid = (id) => req('GET', `/api/grid-images/${id}`); diff --git a/public/style.css b/public/style.css index 8c50d24..8539d35 100644 --- a/public/style.css +++ b/public/style.css @@ -92,6 +92,62 @@ nav { background: rgba(91, 156, 246, 0.12); } +/* ===== MAINTENANCE MENU ===== */ + +.maint-menu { + margin-left: auto; + position: relative; +} + +.maint-toggle { + background: none; + border: none; + color: var(--text-dim); + font-size: 1.2rem; + cursor: pointer; + padding: 0.3rem 0.5rem; + border-radius: 4px; + line-height: 1; + transition: color 0.1s, background 0.1s; +} + +.maint-toggle:hover { + color: var(--text); + background: var(--surface-raised); +} + +.maint-dropdown { + position: absolute; + right: 0; + top: calc(100% + 4px); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); + min-width: 240px; + z-index: 200; + padding: 0.3rem; +} + +.maint-item { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + color: var(--text); + font-family: inherit; + font-size: 0.9rem; + padding: 0.5rem 0.75rem; + cursor: pointer; + border-radius: 4px; + transition: background 0.1s; +} + +.maint-item:hover { + background: var(--surface-raised); +} + /* ===== MAIN ===== */ #main { diff --git a/server.mjs b/server.mjs index a53d3d9..29d6e89 100644 --- a/server.mjs +++ b/server.mjs @@ -544,6 +544,25 @@ app.delete('/api/pdfs/:id', (req, res) => { ok(res); }); +// --------------------------------------------------------------------------- +// Maintenance +// --------------------------------------------------------------------------- + +app.post('/api/maintenance/pdf-thumbs', (req, res) => { + const pdfs = list_pdfs(); + let generated = 0; + for (const pdf of pdfs) { + if (pdf.thumb_filename) continue; + const thumb_prefix = join('./data/pdfs', pdf.id + '-thumb'); + const thumb_file = generate_pdf_thumb(join('./data/pdfs', pdf.filename), thumb_prefix); + if (thumb_file) { + set_pdf({ ...pdf, thumb_filename: thumb_file }); + generated++; + } + } + ok(res, { generated, total: pdfs.length }); +}); + // SPA fallback — serve index.html for any non-API, non-asset path const INDEX_HTML = new URL('./public/index.html', import.meta.url).pathname; app.get('/{*path}', (req, res) => res.sendFile(INDEX_HTML));