From 247390000a0fdf7eeedb89a2c5500c64f36df1c3 Mon Sep 17 00:00:00 2001 From: dax Date: Tue, 4 Aug 2026 18:53:03 +0100 Subject: show upload progress and indexing state; raise upload caps and timeouts --- public/app.css | 46 ++++++++++++++++++++++++++++++++++++++++++++++ public/app.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- public/index.html | 8 ++++++-- server.mjs | 10 +++++++--- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/public/app.css b/public/app.css index 24bb927..153c334 100644 --- a/public/app.css +++ b/public/app.css @@ -258,6 +258,52 @@ html, body { #toast.show { opacity: 1; visibility: visible; pointer-events: auto; } #toast.err { border-color: rgba(248, 113, 113, .6); color: #fca5a5; } +#progress { + position: fixed; + left: 50%; + bottom: 26px; + transform: translateX(-50%); + z-index: 40; + display: flex; + flex-direction: column; + align-items: center; + gap: 9px; + min-width: 280px; + max-width: 70vw; + padding: 13px 18px; + border: 1px solid rgba(255, 255, 255, .18); + border-radius: 10px; + background: rgba(14, 15, 19, .95); +} +#progress[hidden] { display: none; } +#progress-track { + width: 100%; + height: 6px; + border-radius: 3px; + background: rgba(255, 255, 255, .12); + overflow: hidden; +} +#progress-fill { + height: 100%; + width: 0%; + border-radius: 3px; + background: var(--ring); + transition: width .15s ease; +} +#progress.indeterminate #progress-fill { + width: 40%; + animation: progress-slide 1.1s linear infinite; +} +@keyframes progress-slide { + 0% { transform: translateX(-110%); } + 100% { transform: translateX(360%); } +} +#progress-text { + font-size: 12px; + letter-spacing: .04em; + color: var(--muted); +} + @media (prefers-reduced-motion: reduce) { #world { transition: none; } .tile.lock { animation: none; } diff --git a/public/app.js b/public/app.js index dfd6e6b..78584bf 100644 --- a/public/app.js +++ b/public/app.js @@ -8,6 +8,9 @@ const importEl = $("#import") const fileInput = $("#file") const dropOverlay = $("#drop") const toastEl = $("#toast") +const progressEl = $("#progress") +const progressFill = $("#progress-fill") +const progressText = $("#progress-text") addEventListener("error", (e) => { toast(`error: ${e.message || "script failed"}`, true) @@ -708,23 +711,64 @@ function plural(n) { return n === 1 ? "photo" : "photos" } +function setProgress(mode, pct, text) { + progressEl.hidden = false + progressText.textContent = text + if (mode === "indeterminate") { + progressEl.classList.add("indeterminate") + } else { + progressEl.classList.remove("indeterminate") + progressFill.style.width = pct + "%" + } +} + +function hideProgress() { + progressEl.hidden = true + progressEl.classList.remove("indeterminate") + progressFill.style.width = "0%" +} + +function uploadWithProgress(form, onPct, onUploaded) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open("POST", "/api/import") + xhr.upload.addEventListener("progress", (e) => { + if (e.lengthComputable) onPct(Math.min(100, Math.round((e.loaded / e.total) * 100))) + }) + xhr.upload.addEventListener("load", () => onUploaded()) + xhr.addEventListener("load", () => { + let data = {} + try { data = JSON.parse(xhr.responseText) } catch { /* ignore */ } + if (xhr.status >= 200 && xhr.status < 300) resolve(data) + else reject(new Error(data.error || `import failed (${xhr.status})`)) + }) + xhr.addEventListener("error", () => reject(new Error("upload failed (network error)"))) + xhr.send(form) + }) +} + async function importFiles(list) { const files = Array.from(list || []).filter((f) => f.size > 0) if (!files.length) return const form = new FormData() for (const f of files) form.append("photos", f, f.name) importEl.classList.add("busy") - toast(`importing ${files.length} ${plural(files.length)}...`) + setProgress("determinate", 0, `uploading ${files.length} ${plural(files.length)}… 0%`) try { - const r = await fetch("/api/import", { method: "POST", body: form }) - const data = await r.json().catch(() => ({})) - if (!r.ok || !data.ok) throw new Error(data.error || `import failed (${r.status})`) + const data = await uploadWithProgress( + form, + (pct) => setProgress("determinate", pct, `uploading ${files.length} ${plural(files.length)}… ${pct}%`), + () => setProgress("indeterminate", 100, "indexing…") + ) + if (!data.ok) throw new Error(data.error || "import failed") exitsCache.clear() exitPending.clear() + hideProgress() hideImportState() await beginWalk() toast(`imported ${data.imported} ${plural(data.imported)} - keep walking to find them`) } catch (err) { + hideProgress() toast(err.message || "import failed", true) } finally { importEl.classList.remove("busy") diff --git a/public/index.html b/public/index.html index 0f44c6b..ae56eea 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ pathways - +
@@ -23,7 +23,11 @@
drop photos to import
+
- + diff --git a/server.mjs b/server.mjs index e783acc..2e1d789 100644 --- a/server.mjs +++ b/server.mjs @@ -28,9 +28,9 @@ const PHOTOPRISM_USER = process.env.PHOTOPRISM_USER || "photoprism" const PHOTOPRISM_HOME = process.env.PHOTOPRISM_HOME || "/srv/photoprism" const ORIGINALS_PATH = process.env.PHOTOPRISM_ORIGINALS_PATH || "/srv/photoprism/originals" const STORAGE_PATH = process.env.PHOTOPRISM_STORAGE_PATH || "/srv/photoprism/storage" -const MAX_UPLOAD_BYTES = Number(process.env.MAX_UPLOAD_BYTES || 200 * 1024 * 1024) -const MAX_UPLOAD_FILES = Number(process.env.MAX_UPLOAD_FILES || 20) -const IMPORT_TIMEOUT_MS = Number(process.env.IMPORT_TIMEOUT_MS || 10 * 60 * 1000) +const MAX_UPLOAD_BYTES = Number(process.env.MAX_UPLOAD_BYTES || 1024 * 1024 * 1024) +const MAX_UPLOAD_FILES = Number(process.env.MAX_UPLOAD_FILES || 100) +const IMPORT_TIMEOUT_MS = Number(process.env.IMPORT_TIMEOUT_MS || 30 * 60 * 1000) const pool = mysql.createPool(DB) @@ -401,6 +401,10 @@ const server = http.createServer(async (req, res) => { } }) +server.requestTimeout = 0 +server.headersTimeout = 65_000 +server.keepAliveTimeout = 65_000 + server.listen(PORT, "127.0.0.1", () => { console.log(`pathways listening on 127.0.0.1:${PORT}`) mkdir(STAGING_ROOT, { recursive: true }).catch((err) => console.error("pathways: staging dir:", err.message)) -- cgit v1.3.1