No images match the filter.
{% endfor %}diff --git a/dashboard/app.py b/dashboard/app.py index 5d6de60..2238eac 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -96,25 +96,34 @@ def index(request: Request): @app.get("/review", response_class=HTMLResponse) -def review(request: Request, source: str = None, status: str = None): +def review(request: Request, source: str = None, status: str = None, page: int = 1): conn = _conn() + per_page = 200 q = "SELECT sha256, path, status, source FROM image_hashes WHERE 1=1" + count_q = "SELECT COUNT(*) FROM image_hashes WHERE 1=1" params = [] if source: q += " AND source=?" + count_q += " AND source=?" params.append(source) if status: q += " AND status=?" + count_q += " AND status=?" params.append(status) else: q += " AND status IN ('scanned','review')" - q += " ORDER BY added_at DESC LIMIT 300" - rows = conn.execute(q, params).fetchall() + count_q += " AND status IN ('scanned','review')" + total = conn.execute(count_q, params).fetchone()[0] + pages = max(1, (total + per_page - 1) // per_page) + page = max(1, min(page, pages)) + q += " ORDER BY added_at DESC LIMIT ? OFFSET ?" + rows = conn.execute(q, params + [per_page, (page - 1) * per_page]).fetchall() conn.close() items = [_load_item(r) for r in rows] return templates.TemplateResponse( request, "review.html", - {"items": items, "source": source, "status": status}, + {"items": items, "source": source, "status": status, + "page": page, "pages": pages, "total": total}, ) @@ -231,6 +240,47 @@ def thumb_file(name: str): return FileResponse(f) +@app.get("/upload", response_class=HTMLResponse) +def upload_page(request: Request): + """Upload page β drop files/archives into the incoming folder.""" + return templates.TemplateResponse(request, "upload.html", {}) + + +@app.post("/upload") +async def upload(request: Request): + """Receive uploaded files β save to /mnt/data/takeout/incoming/.""" + import uuid + + from starlette.datastructures import UploadFile + + form = await request.form() + incoming = STAGING / "takeout" / "incoming" + incoming.mkdir(parents=True, exist_ok=True) + saved = [] + for field in form.values(): + if isinstance(field, UploadFile) and field.filename: + # sanitize: keep name but avoid path traversal + name = Path(field.filename).name + dest = incoming / f"{uuid.uuid4().hex[:8]}_{name}" + with open(dest, "wb") as f: + while chunk := await field.read(1024 * 1024): + f.write(chunk) + saved.append(dest.name) + # notify + try: + import sys + if str(BASE.parent) not in sys.path: + sys.path.insert(0, str(BASE.parent)) + import apprise_helper + apprise_helper.notify( + "π₯ photo-pipeline: upload received", + f"{len(saved)} file(s) saved to incoming. Watch flow will process them.", + ) + except Exception: + pass + return {"saved": len(saved), "files": saved} + + if __name__ == "__main__": import uvicorn diff --git a/dashboard/templates/review.html b/dashboard/templates/review.html index 1cc25b3..94a6ca3 100644 --- a/dashboard/templates/review.html +++ b/dashboard/templates/review.html @@ -41,6 +41,9 @@ .none { color: #666; font-style: italic; padding: 2rem; text-align: center; } .selall { display: inline-flex; align-items: center; gap: .35rem; background: #222; border: 1px solid #444; border-radius: 6px; padding: .4rem .8rem; cursor: pointer; } .toast { position: fixed; bottom: 1.2rem; right: 1.2rem; background: #1d4; color: #031; padding: .7rem 1rem; border-radius: 8px; display: none; z-index: 50; font-size: .9rem; box-shadow: 0 2px 12px rgba(0,0,0,.5); } + .pager { display: flex; gap: 1rem; align-items: center; justify-content: center; padding: 1.5rem; } + .pager a { color: #6cf; text-decoration: none; padding: .4rem .8rem; background: #222; border: 1px solid #444; border-radius: 6px; } + .pager span { color: #999; }
@@ -72,7 +75,7 @@ -{{ items|length }} images
+{{ total }} images Β· page {{ page }}/{{ pages }}
No images match the filter.
{% endfor %}