added upload indication

This commit is contained in:
MEGASOL\simon.adams
2025-08-27 21:07:10 +02:00
parent 20deb5616b
commit b714134f2e
6 changed files with 109 additions and 18 deletions

24
.dockerignore Normal file
View File

@@ -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

View File

@@ -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

25
.gitignore vendored
View File

@@ -1 +1,26 @@
*.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

View File

@@ -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)

View File

@@ -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.
"""

View File

@@ -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';