Add maintenance menu (top-right ⚙) with generate missing PDF thumbnails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 00:41:40 +00:00
parent 451b04ad03
commit 8e0f7eb4d8
5 changed files with 111 additions and 0 deletions

View File

@@ -1940,6 +1940,33 @@ async function init() {
btn.addEventListener('click', () => navigate('/' + btn.dataset.section)); 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(); }); window.addEventListener('popstate', () => { parse_url(); render(); });
await load_all(); await load_all();

View File

@@ -17,6 +17,12 @@
<button class="nav-btn" data-section="grids">Grids</button> <button class="nav-btn" data-section="grids">Grids</button>
<button class="nav-btn" data-section="templates">Templates</button> <button class="nav-btn" data-section="templates">Templates</button>
</nav> </nav>
<div class="maint-menu" id="maint-menu">
<button class="maint-toggle" id="maint-toggle" title="Maintenance"></button>
<div class="maint-dropdown" id="maint-dropdown" hidden>
<button class="maint-item" id="maint-gen-thumbs">Generate missing PDF thumbnails</button>
</div>
</div>
</header> </header>
<main id="main"></main> <main id="main"></main>
<script type="module" src="/app.mjs"></script> <script type="module" src="/app.mjs"></script>

View File

@@ -59,6 +59,9 @@ export async function upload_pdf(file, display_name) {
return data; return data;
} }
// Maintenance
export const maintenance_pdf_thumbs = () => req('POST', '/api/maintenance/pdf-thumbs');
// Grid images // Grid images
export const get_grids = () => req('GET', '/api/grid-images'); export const get_grids = () => req('GET', '/api/grid-images');
export const get_grid = (id) => req('GET', `/api/grid-images/${id}`); export const get_grid = (id) => req('GET', `/api/grid-images/${id}`);

View File

@@ -92,6 +92,62 @@ nav {
background: rgba(91, 156, 246, 0.12); 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 ===== */
#main { #main {

View File

@@ -544,6 +544,25 @@ app.delete('/api/pdfs/:id', (req, res) => {
ok(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 // SPA fallback — serve index.html for any non-API, non-asset path
const INDEX_HTML = new URL('./public/index.html', import.meta.url).pathname; const INDEX_HTML = new URL('./public/index.html', import.meta.url).pathname;
app.get('/{*path}', (req, res) => res.sendFile(INDEX_HTML)); app.get('/{*path}', (req, res) => res.sendFile(INDEX_HTML));