diff options
| author | Ken D'Ambrosio <ken@jots.org> | 2026-05-22 22:50:35 +0000 |
|---|---|---|
| committer | Ken D'Ambrosio <ken@jots.org> | 2026-05-22 22:50:35 +0000 |
| commit | d32b5e99afc6f0cffefa594510cda0e4f414db75 (patch) | |
| tree | b4c24a1a7264bcbde72c0fff906e7bf380c18a02 /views/admin | |
| parent | de80b9871ebe1497c672f3c7c7bb5467dabcb83a (diff) | |
Speed up update.rb and fix UI always forcing full rescan
- update.rb: skip exiftool on images marked exif_absent (set after first
failed attempt); prevents repeated slow scans of old photos with no EXIF
- update.rb: explicit directory argument now implies force — passing a path
always rescans that subtree regardless of sentinel mtime
- app.rb: /admin/update no longer hardcodes --force; sentinel-based skipping
is used by default, making UI updates finish in seconds instead of minutes
- admin/album.erb: add "Force rescan all" checkbox to Run Update button;
checked state passes force=1 to the server and restores --force behavior
- README.md, DESIGN.md: document sentinel skipping, exif_absent flag, and
explicit-directory force behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'views/admin')
| -rw-r--r-- | views/admin/album.erb | 122 |
1 files changed, 121 insertions, 1 deletions
diff --git a/views/admin/album.erb b/views/admin/album.erb index 15a043f..14a8f07 100644 --- a/views/admin/album.erb +++ b/views/admin/album.erb @@ -140,6 +140,9 @@ <section class="admin-update"> <h2>Update</h2> <p class="update-hint">Scans this album for new/removed files, extracts EXIF data, and generates missing thumbnails.</p> + <label class="update-force-label"> + <input type="checkbox" id="update-force"> Force rescan all (slow) + </label> <button id="update-btn" class="btn" onclick="startUpdate()">Run Update</button> <div id="update-panel" class="update-panel hidden"> <div id="update-status" class="update-status"></div> @@ -147,6 +150,31 @@ </div> </section> + <section class="admin-upload"> + <h2>Upload</h2> + <p class="update-hint">Add photos or videos. To create a new sub-album, fill in a name below. Up to 2 GB per file (reverse proxy may impose a lower limit).</p> + <div class="form-row" style="max-width:380px"> + <label>New sub-album name <small style="color:var(--text-dim)">(optional — leave blank to add to this album)</small> + <input type="text" id="upload-subalbum" placeholder="e.g. 2024-12-Hawaii" autocomplete="off"> + </label> + </div> + <div class="upload-file-row"> + <input type="file" id="upload-files" multiple + accept="image/*,video/*,audio/*,.heic,.heif,.flac,.wav,.m4a,.aac" + style="display:none"> + <button type="button" class="btn" onclick="document.getElementById('upload-files').click()">Choose Files</button> + <span id="upload-file-count" class="upload-file-count">No files chosen</span> + <button type="button" id="upload-btn" class="btn" onclick="startUpload()" disabled>Upload</button> + </div> + <div id="upload-panel" class="upload-panel hidden"> + <div class="upload-progress-wrap"> + <div id="upload-progress-bar" class="upload-progress-bar"></div> + </div> + <div id="upload-status" class="update-status"></div> + <pre id="upload-log" class="update-log"></pre> + </div> + </section> + <script> async function startUpdate() { const btn = document.getElementById('update-btn'); @@ -162,10 +190,11 @@ status.className = 'update-status running'; panel.classList.remove('hidden'); + const force = document.getElementById('update-force').checked; const res = await fetch('/admin/update', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ rel }) + body: new URLSearchParams({ rel, force: force ? '1' : '' }) }); const { job_id } = await res.json(); @@ -193,5 +222,96 @@ } }, 1500); } + document.getElementById('upload-files').addEventListener('change', function () { + const n = this.files.length; + document.getElementById('upload-file-count').textContent = + n > 0 ? `${n} file${n !== 1 ? 's' : ''} chosen` : 'No files chosen'; + document.getElementById('upload-btn').disabled = n === 0; + }); + + function startUpload() { + const fileInput = document.getElementById('upload-files'); + if (fileInput.files.length === 0) return; + + const subAlbum = document.getElementById('upload-subalbum').value.trim(); + const btn = document.getElementById('upload-btn'); + const panel = document.getElementById('upload-panel'); + const progressBar = document.getElementById('upload-progress-bar'); + const status = document.getElementById('upload-status'); + const log = document.getElementById('upload-log'); + const rel = <%= @rel.to_json %>; + + btn.disabled = true; + panel.classList.remove('hidden'); + log.textContent = ''; + progressBar.style.width = '0%'; + status.textContent = 'Uploading…'; + status.className = 'update-status running'; + + const fd = new FormData(); + fd.append('rel', rel); + if (subAlbum) fd.append('new_album_name', subAlbum); + for (const f of fileInput.files) fd.append('files[]', f); + + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + progressBar.style.width = Math.round(e.loaded / e.total * 100) + '%'; + } + }); + xhr.addEventListener('load', () => { + progressBar.style.width = '100%'; + if (xhr.status !== 200) { + status.textContent = 'Upload failed ✗'; + status.className = 'update-status error'; + btn.disabled = false; + return; + } + let resp; + try { resp = JSON.parse(xhr.responseText); } catch (e) { + status.textContent = 'Upload failed ✗'; + status.className = 'update-status error'; + btn.disabled = false; + return; + } + const saved = resp.saved; + status.textContent = `${saved} file${saved !== 1 ? 's' : ''} saved — running update…`; + pollUploadJob(resp.job_id, resp.album_rel, btn, status, log); + }); + xhr.addEventListener('error', () => { + status.textContent = 'Network error ✗'; + status.className = 'update-status error'; + btn.disabled = false; + }); + xhr.open('POST', '/admin/upload'); + xhr.send(fd); + } + + function pollUploadJob(jobId, albumRel, btn, status, log) { + let seen = 0; + const poll = setInterval(async () => { + const r = await fetch('/admin/update/' + jobId); + const data = await r.json(); + const fresh = data.lines.slice(seen); + if (fresh.length) { + log.textContent += fresh.join('\n') + '\n'; + log.scrollTop = log.scrollHeight; + seen = data.lines.length; + } + if (data.status !== 'running') { + clearInterval(poll); + btn.disabled = false; + if (data.status === 'done') { + status.innerHTML = + `Done ✓ <a href="/admin/edit/${albumRel}" class="btn btn-sm">Edit album</a>` + + ` <a href="/browse/${albumRel}" class="btn btn-sm">View</a>`; + status.className = 'update-status done'; + } else { + status.textContent = 'Error ✗'; + status.className = 'update-status error'; + } + } + }, 1500); + } </script> </div> |
