diff --git a/dashboard/app.py b/dashboard/app.py index 968ff46..e80de18 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -44,13 +44,15 @@ def _is_image(p: Path) -> bool: return p.suffix.lower() in EXT_IMAGES and p.exists() -def _thumb(path: str): - """Return thumbnail URL WITHOUT generating (lazy — /thumbs/ generates on first hit).""" - src = Path(path) - if not src.exists(): +def _thumb(path: str, sha: str = None): + """Return thumbnail URL keyed by sha256 (the image's unique identity). + + Never uses filename — duplicate filenames across folders caused wrong + thumbnails/full-size images. sha256 is unique per image content. + """ + if not sha: return None - key = src.stem + "_" + str(abs(hash(str(src))))[:8] + ".jpg" - return f"/thumbs/{key}" + return f"/thumbs/{sha}.jpg" def _load_item(row: sqlite3.Row) -> dict: @@ -61,7 +63,7 @@ def _load_item(row: sqlite3.Row) -> dict: "name": p.name, "status": row["status"], "source": row["source"], - "thumb": _thumb(row["path"]) if _is_image(p) else None, + "thumb": _thumb(row["path"], row["sha256"]) if _is_image(p) else None, "exists": p.exists(), "size_mb": round(p.stat().st_size / 1e6, 1) if p.exists() else None, "flag_reason": row["flag_reason"] if "flag_reason" in row.keys() else None, @@ -229,39 +231,31 @@ def full_file(sha: str): @app.get("/thumbs/{name}") def thumb_file(name: str): - """Serve thumbnail; generate on first request (cached after).""" + """Serve thumbnail keyed by sha256; generate on first request (cached).""" f = THUMB_DIR / name - if not f.exists(): - # lazy generate — reconstruct source path from key (stem is orig filename) - THUMB_DIR.mkdir(parents=True, exist_ok=True) - # find the source image: key = _.jpg - stem = name.rsplit("_", 1)[0] - src = _find_source(stem) - if not src: - raise HTTPException(404) - try: - with Image.open(src) as im: - im.convert("RGB") - im.thumbnail(THUMB_SIZE) - im.save(f, "JPEG", quality=70) - except Exception: - raise HTTPException(404) + if f.exists(): + return FileResponse(f) + # key = .jpg — resolve exact image via the DB (unique identity) + sha = name.rsplit(".", 1)[0] + conn = _conn() + row = conn.execute("SELECT path FROM image_hashes WHERE sha256=?", (sha,)).fetchone() + conn.close() + if not row: + raise HTTPException(404) + src = Path(row["path"]) + if not src.exists(): + raise HTTPException(404) + THUMB_DIR.mkdir(parents=True, exist_ok=True) + try: + with Image.open(src) as im: + im.convert("RGB") + im.thumbnail(THUMB_SIZE) + im.save(f, "JPEG", quality=70) + except Exception: + raise HTTPException(404) return FileResponse(f) -def _find_source(stem: str): - """Find the original image for a thumbnail key (by filename stem).""" - import os - - for root_dir in ("/mnt/ubuntu_storage_3TB/archive/03_photos", - "/mnt/data/01_keep", "/mnt/data/02_review", "/mnt/data/03_delete"): - for dirpath, _, files in os.walk(root_dir): - for fn in files: - if fn.rsplit(".", 1)[0] == stem: - return Path(dirpath) / fn - return None - - @app.get("/upload", response_class=HTMLResponse) def upload_page(request: Request): """Upload page — drop files/archives into the incoming folder.""" diff --git a/dashboard/templates/_card.html b/dashboard/templates/_card.html index 5ebc3e0..2a49ea4 100644 --- a/dashboard/templates/_card.html +++ b/dashboard/templates/_card.html @@ -1,9 +1,9 @@ {% set sha8 = item["sha256"][:8] %} - diff --git a/dashboard/templates/review.html b/dashboard/templates/review.html index 218efbd..671975e 100644 --- a/dashboard/templates/review.html +++ b/dashboard/templates/review.html @@ -143,6 +143,25 @@ refresh(); }); + async function cardAction(action, btn) { + const card = btn.closest('.card'); + const sha = card.dataset.sha; + try { + const resp = await fetch(`/review/${sha}/${action}`, { method: 'POST' }); + if (resp.ok) { + card.remove(); + updateCount(); + showToast(action === 'approve' ? 'Approved' : action === 'reject' ? 'Rejected' : 'Reset'); + // if reset, refresh the page (item returns to a different view) + if (action === 'reset') location.reload(); + } else { + showToast('Action failed', true); + } + } catch (e) { + showToast('Action failed: ' + e, true); + } + } + async function bulk(action) { const shas = getSelected(); if (shas.length === 0) return;