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:
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
19
server.mjs
19
server.mjs
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user