diff options
| author | dax <me@dax.ist> | 2026-08-05 20:14:45 +0100 |
|---|---|---|
| committer | dax <me@dax.ist> | 2026-08-05 20:14:45 +0100 |
| commit | 994cdbec9dd6ae6b965b56f4f6816a1dfef8bcc9 (patch) | |
| tree | 0d7b0d1c5bec8d34b5ebe731af34ce3e8b1706ca /public | |
| parent | d2a8b2abc908311fc249454086d0d0eacfe861e1 (diff) | |
staging: local drop-folder (watch/) auto-import with startup recovery + failed batches; admin local-files panel; upload 413 pre-checkHEADmain
Diffstat (limited to 'public')
| -rw-r--r-- | public/admin.css | 60 | ||||
| -rw-r--r-- | public/admin.html | 17 | ||||
| -rw-r--r-- | public/admin.js | 53 |
3 files changed, 127 insertions, 3 deletions
diff --git a/public/admin.css b/public/admin.css index 8f95d63..8756359 100644 --- a/public/admin.css +++ b/public/admin.css @@ -159,6 +159,66 @@ button.danger:hover { border-color: var(--danger); background: rgba(252, 165, 16 margin: 0 0 8px; } +.local { + background: var(--panel); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 10px; + padding: 14px; + display: grid; + gap: 10px; +} +.local h2 { + font-size: 14px; + font-weight: 500; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--muted); + margin: 0; +} +.local-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } +.local-head h2 { margin: 0; } +.local-path { + margin: 0; + font-size: 12px; + color: var(--ring); + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + overflow-wrap: anywhere; +} +.local-hint { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.5; } +.local-hint code { + display: inline-block; + margin-top: 4px; + font-size: 12px; + color: var(--ink); + background: var(--panel-2); + border: 1px solid rgba(255, 255, 255, .1); + border-radius: 6px; + padding: 3px 8px; + overflow-wrap: anywhere; +} +.st-file { + display: flex; + justify-content: space-between; + gap: 10px; + padding: 6px 8px; + background: var(--panel-2); + border: 1px solid rgba(255, 255, 255, .06); + border-radius: 8px; + font-size: 13px; +} +.st-file .n { color: var(--ink); overflow-wrap: anywhere; } +.st-file .s { color: var(--muted); white-space: nowrap; } +.st-log-item { + padding: 6px 8px; + font-size: 12px; + border-radius: 8px; + background: var(--panel-2); + border: 1px solid rgba(255, 255, 255, .06); +} +.st-log-item.ok { color: var(--ok); } +.st-log-item.bad { color: var(--danger); } +#st-log { display: grid; gap: 6px; } + .results { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); diff --git a/public/admin.html b/public/admin.html index 9fe1b5a..c296bea 100644 --- a/public/admin.html +++ b/public/admin.html @@ -5,7 +5,7 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="color-scheme" content="dark"> <title>pathways · admin</title> - <link rel="stylesheet" href="/admin.css?v=4"> + <link rel="stylesheet" href="/admin.css?v=5"> </head> <body> <main id="app"> @@ -73,6 +73,19 @@ <p class="im-status" id="im-status"></p> </div> <p class="err" id="im-err"></p> + <div class="local"> + <div class="local-head"> + <h2>local drop folder</h2> + <button id="st-scan">import now</button> + </div> + <p class="local-path" id="st-path"></p> + <p class="local-hint">copy photos into this folder on the server — they import automatically (polled every ~15s).<br> + <code>scp photo.jpg dax@<host>:/srv/pathways/staging/watch/</code></p> + <div id="st-files"></div> + <p class="facets" id="st-none" hidden>no files staged yet</p> + <h2>recent imports</h2> + <div id="st-log"></div> + </div> <div class="recent" id="im-recent" hidden> <h2>recently added</h2> <div class="results" id="im-new"></div> @@ -87,6 +100,6 @@ </section> </main> <div id="toast"></div> - <script src="/admin.js?v=4"></script> + <script src="/admin.js?v=5"></script> </body> </html> diff --git a/public/admin.js b/public/admin.js index 90dd6b0..5291620 100644 --- a/public/admin.js +++ b/public/admin.js @@ -99,6 +99,13 @@ function fmtUptime(s) { return h ? `${h} h ${m} m` : `${m} m` } +function fmtSize(n) { + if (n >= 1e9) return (n / 1e9).toFixed(1) + " GB" + if (n >= 1e6) return (n / 1e6).toFixed(1) + " MB" + if (n >= 1e3) return Math.round(n / 1e3) + " KB" + return n + " B" +} + let debounceTimer = 0 function debounce(fn, ms) { clearTimeout(debounceTimer) @@ -294,6 +301,7 @@ async function enterDash() { try { await loadStatus() await loadRecent() + await loadStaging().catch(() => {}) } catch (err) { if (views.dash.hidden) return; } } @@ -316,6 +324,34 @@ async function loadRecent() { $("#im-recent").hidden = data.photos.length === 0 } +async function loadStaging() { + const d = await api("/api/admin/staging") + $("#st-path").textContent = d.watch + const filesEl = $("#st-files") + filesEl.replaceChildren() + for (const f of d.files) { + const row = document.createElement("div") + row.className = "st-file" + const n = document.createElement("span") + n.className = "n" + n.textContent = f.name + const s = document.createElement("span") + s.className = "s" + s.textContent = fmtSize(f.size) + " · " + fmtAge(Date.now() - f.mtime) + row.append(n, s) + filesEl.appendChild(row) + } + $("#st-none").hidden = d.files.length > 0 + const logEl = $("#st-log") + logEl.replaceChildren() + for (const r of d.imports) { + const row = document.createElement("div") + row.className = "st-log-item " + (r.ok ? "ok" : "bad") + row.textContent = `${new Date(r.at).toLocaleString()} — ${r.ok ? `imported ${r.count} ${r.count === 1 ? "photo" : "photos"}` : "failed: " + (r.error || "unknown error")}` + logEl.appendChild(row) + } +} + function setImProgress(pct, text) { $("#im-progress").hidden = false $("#im-status").textContent = text @@ -360,6 +396,7 @@ async function importFiles(list) { toast(`imported ${data.imported} ${data.imported === 1 ? "photo" : "photos"}`) await loadStatus() await loadRecent() + await loadStaging().catch(() => {}) } catch (err) { $("#im-err").textContent = err.message } finally { @@ -378,10 +415,24 @@ async function init() { $("#tab-status").hidden = tab !== "status" $("#tab-import").hidden = tab !== "import" $("#tab-graph").hidden = tab !== "graph" - if (tab === "import") loadRecent().catch((e2) => toast(e2.message)) + if (tab === "import") { + loadRecent().catch((e2) => toast(e2.message)) + loadStaging().catch((e2) => toast(e2.message)) + } }) }) + $("#st-scan").addEventListener("click", async () => { + const btn = $("#st-scan") + btn.disabled = true + try { + await api("/api/admin/staging", { method: "POST" }) + await loadStaging() + toast("scan complete") + } catch (err) { toast(err.message) } + btn.disabled = false + }) + $("#setup-form").addEventListener("submit", async (e) => { e.preventDefault() const err = $("#setup-err") |
