From b714134f2ee3571b0f1acf5e08640a55e03655e5 Mon Sep 17 00:00:00 2001 From: "MEGASOL\\simon.adams" Date: Wed, 27 Aug 2025 21:07:10 +0200 Subject: [PATCH] added upload indication --- .dockerignore | 24 +++++++++++++++++++++++ .env.example | 10 +++++----- .gitignore | 27 ++++++++++++++++++++++++- README.md | 7 ++++--- app/app.py | 7 ++++--- frontend/app.js | 52 +++++++++++++++++++++++++++++++++++++++++++------ 6 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3148f64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +# Keep Docker build contexts lean and secrets out of images +.git +.gitignore +.venv +__pycache__/ +*.py[cod] +*.pyo +*.so +.mypy_cache/ +.pytest_cache/ +*.egg-info/ +dist/ +build/ +.eggs/ +.idea/ +.vscode/ +.DS_Store + +# Local data, screenshots, and env files +data/ +screenshot.png +.env +*.env +agent.md diff --git a/.env.example b/.env.example index 812636a..4edf91e 100644 --- a/.env.example +++ b/.env.example @@ -3,12 +3,12 @@ HOST=0.0.0.0 PORT=8080 # Immich connection -IMMICH_BASE_URL=http://127.0.0.1:2283//api -IMMICH_API_KEY=ADD-YOUR-API-KEY +IMMICH_BASE_URL=http://127.0.0.1:2283/api +IMMICH_API_KEY=ADD-YOUR-API-KEY #key needs asset.upload (default functions) MAX_CONCURRENT=3 -# Optional admin token to allow UI-based config updates -CONFIG_TOKEN=change-me +# Optional: Album name for auto-adding uploads (creates if doesn't exist) +IMMICH_ALBUM_NAME=dead-drop #key needs album.create,album.read,albumAsset.create (extended functions) # Data path inside the container -STATE_DB=/data/state.db \ No newline at end of file +STATE_DB=/data/state.db diff --git a/.gitignore b/.gitignore index 4f509e5..4ffe575 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,26 @@ -*.env \ No newline at end of file +*.env +agent.md + +# Python cache and build artifacts +__pycache__/ +app/__pycache__/* +*.py[cod] +*.pyo +*.so +.mypy_cache/ +.pytest_cache/ +*.egg-info/ +.eggs/ +build/ +dist/ + +# Virtual envs and IDE +.venv/ +.idea/ +.vscode/ +.DS_Store + +# Local data/dbs +data/*.db +data/*.sqlite +data/*.sqlite3 diff --git a/README.md b/README.md index 5924103..7a205f0 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ version: "3.9" services: immich-drop: - image: ttlequals0/immich-drop:latest + image: ghcr.io/nasogaa/immich-drop:latest pull_policy: always container_name: immich-drop restart: unless-stopped @@ -165,11 +165,12 @@ PORT=8080 # Immich connection (include /api) IMMICH_BASE_URL=http://REPLACE_ME:2283/api -IMMICH_API_KEY=REPLACE_ME +IMMICH_API_KEY=ADD-YOUR-API-KEY #key needs asset.upload (default functions) + MAX_CONCURRENT=3 # Optional: Album name for auto-adding uploads (creates if doesn't exist) -IMMICH_ALBUM_NAME=dead-drop +IMMICH_ALBUM_NAME=dead-drop #key needs album.create,album.read,albumAsset.create (extended functions) # Local dedupe cache STATE_DB=./data/state.db # local dev -> ./state.db (data folder is created in docker image) diff --git a/app/app.py b/app/app.py index 222e8bd..2fe1acd 100644 --- a/app/app.py +++ b/app/app.py @@ -435,6 +435,7 @@ async def api_upload( return await do_upload() -if __name__ == "__main__": - import uvicorn - uvicorn.run("backend.main:app", host=HOST, port=PORT, reload=True) +""" +Note: Do not run this module directly. Use `python main.py` from +project root, which starts `uvicorn app.app:app` with reload. +""" diff --git a/frontend/app.js b/frontend/app.js index 14f5579..28e202b 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -3,6 +3,10 @@ const sessionId = (crypto && crypto.randomUUID) ? crypto.randomUUID() : (Math.ra let items = []; let socket; +// Status precedence: never regress (e.g., uploading -> done shouldn't go back to uploading) +const STATUS_ORDER = { queued: 0, checking: 1, uploading: 2, duplicate: 3, done: 3, error: 4 }; +const FINAL_STATES = new Set(['done','duplicate','error']); + // --- Dark mode --- function initDarkMode() { const stored = localStorage.getItem('theme'); @@ -87,9 +91,24 @@ function openSocket(){ const { item_id, status, progress, message } = msg; const it = items.find(x => x.id===item_id); if(!it) return; - it.status = status; - if(typeof progress==='number') it.progress = progress; - if(message) it.message = message; + // If we've already finalized this item, ignore late/regressive updates + if (FINAL_STATES.has(it.status)) return; + + const cur = STATUS_ORDER[it.status] ?? 0; + const inc = STATUS_ORDER[status] ?? 0; + if (inc < cur) { + // ignore regressive status updates + } else { + it.status = status; + } + if (typeof progress==='number') { + // never decrease progress + it.progress = Math.max(it.progress || 0, progress); + } + if (message) it.message = message; + if (FINAL_STATES.has(it.status)) { + it.progress = 100; + } render(); }; socket.onclose = () => setTimeout(openSocket, 2000); @@ -118,6 +137,15 @@ async function runQueue(){ next.status='error'; next.message = body.error || 'Upload failed'; render(); + } else if (res.ok) { + // Fallback finalize on HTTP success in case WS final message is missed + const statusText = (body && body.status) ? String(body.status) : ''; + const isDuplicate = /duplicate/i.test(statusText); + next.status = isDuplicate ? 'duplicate' : 'done'; + next.message = statusText || (isDuplicate ? 'Duplicate' : 'Uploaded'); + next.progress = 100; + render(); + try { showBanner(isDuplicate ? `Duplicate: ${next.name}` : `Uploaded: ${next.name}`, isDuplicate ? 'warn' : 'ok'); } catch {} } }catch(err){ next.status='error'; @@ -141,6 +169,20 @@ const pingStatus = document.getElementById('pingStatus'); const banner = document.getElementById('topBanner'); const btnTheme = document.getElementById('btnTheme'); +// --- Simple banner helper --- +function showBanner(text, kind='ok'){ + if(!banner) return; + banner.textContent = text; + // reset classes and apply based on kind + banner.className = 'rounded-2xl p-3 text-center transition-colors ' + ( + kind==='ok' ? 'border border-green-200 bg-green-50 text-green-700 dark:bg-green-900 dark:border-green-700 dark:text-green-300' + : kind==='warn' ? 'border border-amber-200 bg-amber-50 text-amber-700 dark:bg-amber-900 dark:border-amber-700 dark:text-amber-300' + : 'border border-red-200 bg-red-50 text-red-700 dark:bg-red-900 dark:border-red-700 dark:text-red-300' + ); + banner.classList.remove('hidden'); + setTimeout(() => banner.classList.add('hidden'), 3000); +} + // --- Connection test with ephemeral banner --- btnPing.onclick = async () => { pingStatus.textContent = 'checking…'; @@ -154,9 +196,7 @@ btnPing.onclick = async () => { if(j.album_name) { bannerText += ` | Uploading to album: "${j.album_name}"`; } - banner.textContent = bannerText; - banner.classList.remove('hidden'); - setTimeout(() => banner.classList.add('hidden'), 4000); + showBanner(bannerText, 'ok'); } }catch{ pingStatus.textContent = 'No connection';