added upload indication
This commit is contained in:
24
.dockerignore
Normal file
24
.dockerignore
Normal 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
|
||||||
@@ -3,12 +3,12 @@ HOST=0.0.0.0
|
|||||||
PORT=8080
|
PORT=8080
|
||||||
|
|
||||||
# Immich connection
|
# Immich connection
|
||||||
IMMICH_BASE_URL=http://127.0.0.1:2283//api
|
IMMICH_BASE_URL=http://127.0.0.1:2283/api
|
||||||
IMMICH_API_KEY=ADD-YOUR-API-KEY
|
IMMICH_API_KEY=ADD-YOUR-API-KEY #key needs asset.upload (default functions)
|
||||||
MAX_CONCURRENT=3
|
MAX_CONCURRENT=3
|
||||||
|
|
||||||
# Optional admin token to allow UI-based config updates
|
# Optional: Album name for auto-adding uploads (creates if doesn't exist)
|
||||||
CONFIG_TOKEN=change-me
|
IMMICH_ALBUM_NAME=dead-drop #key needs album.create,album.read,albumAsset.create (extended functions)
|
||||||
|
|
||||||
# Data path inside the container
|
# Data path inside the container
|
||||||
STATE_DB=/data/state.db
|
STATE_DB=/data/state.db
|
||||||
25
.gitignore
vendored
25
.gitignore
vendored
@@ -1 +1,26 @@
|
|||||||
*.env
|
*.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
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ version: "3.9"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich-drop:
|
immich-drop:
|
||||||
image: ttlequals0/immich-drop:latest
|
image: ghcr.io/nasogaa/immich-drop:latest
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
container_name: immich-drop
|
container_name: immich-drop
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -165,11 +165,12 @@ PORT=8080
|
|||||||
|
|
||||||
# Immich connection (include /api)
|
# Immich connection (include /api)
|
||||||
IMMICH_BASE_URL=http://REPLACE_ME:2283/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
|
MAX_CONCURRENT=3
|
||||||
|
|
||||||
# Optional: Album name for auto-adding uploads (creates if doesn't exist)
|
# 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
|
# Local dedupe cache
|
||||||
STATE_DB=./data/state.db # local dev -> ./state.db (data folder is created in docker image)
|
STATE_DB=./data/state.db # local dev -> ./state.db (data folder is created in docker image)
|
||||||
|
|||||||
@@ -435,6 +435,7 @@ async def api_upload(
|
|||||||
|
|
||||||
return await do_upload()
|
return await do_upload()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
"""
|
||||||
import uvicorn
|
Note: Do not run this module directly. Use `python main.py` from
|
||||||
uvicorn.run("backend.main:app", host=HOST, port=PORT, reload=True)
|
project root, which starts `uvicorn app.app:app` with reload.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ const sessionId = (crypto && crypto.randomUUID) ? crypto.randomUUID() : (Math.ra
|
|||||||
let items = [];
|
let items = [];
|
||||||
let socket;
|
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 ---
|
// --- Dark mode ---
|
||||||
function initDarkMode() {
|
function initDarkMode() {
|
||||||
const stored = localStorage.getItem('theme');
|
const stored = localStorage.getItem('theme');
|
||||||
@@ -87,9 +91,24 @@ function openSocket(){
|
|||||||
const { item_id, status, progress, message } = msg;
|
const { item_id, status, progress, message } = msg;
|
||||||
const it = items.find(x => x.id===item_id);
|
const it = items.find(x => x.id===item_id);
|
||||||
if(!it) return;
|
if(!it) return;
|
||||||
it.status = status;
|
// If we've already finalized this item, ignore late/regressive updates
|
||||||
if(typeof progress==='number') it.progress = progress;
|
if (FINAL_STATES.has(it.status)) return;
|
||||||
if(message) it.message = message;
|
|
||||||
|
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();
|
render();
|
||||||
};
|
};
|
||||||
socket.onclose = () => setTimeout(openSocket, 2000);
|
socket.onclose = () => setTimeout(openSocket, 2000);
|
||||||
@@ -118,6 +137,15 @@ async function runQueue(){
|
|||||||
next.status='error';
|
next.status='error';
|
||||||
next.message = body.error || 'Upload failed';
|
next.message = body.error || 'Upload failed';
|
||||||
render();
|
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){
|
}catch(err){
|
||||||
next.status='error';
|
next.status='error';
|
||||||
@@ -141,6 +169,20 @@ const pingStatus = document.getElementById('pingStatus');
|
|||||||
const banner = document.getElementById('topBanner');
|
const banner = document.getElementById('topBanner');
|
||||||
const btnTheme = document.getElementById('btnTheme');
|
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 ---
|
// --- Connection test with ephemeral banner ---
|
||||||
btnPing.onclick = async () => {
|
btnPing.onclick = async () => {
|
||||||
pingStatus.textContent = 'checking…';
|
pingStatus.textContent = 'checking…';
|
||||||
@@ -154,9 +196,7 @@ btnPing.onclick = async () => {
|
|||||||
if(j.album_name) {
|
if(j.album_name) {
|
||||||
bannerText += ` | Uploading to album: "${j.album_name}"`;
|
bannerText += ` | Uploading to album: "${j.album_name}"`;
|
||||||
}
|
}
|
||||||
banner.textContent = bannerText;
|
showBanner(bannerText, 'ok');
|
||||||
banner.classList.remove('hidden');
|
|
||||||
setTimeout(() => banner.classList.add('hidden'), 4000);
|
|
||||||
}
|
}
|
||||||
}catch{
|
}catch{
|
||||||
pingStatus.textContent = 'No connection';
|
pingStatus.textContent = 'No connection';
|
||||||
|
|||||||
Reference in New Issue
Block a user