diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 86f3867..8f945b5 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -49,3 +49,40 @@ OTB Cloud is now operating as a live multi-profile, dual-GPU media processing/st ### Recommended Next Step Proceed to Android app update: - allow app-side selection for images, videos, or both + +## [v1.1.0-alpha4.2] - Storage structure + Android alignment + +### ✅ Storage restructuring +- Unified storage under: + - originals/images + - originals/video +- Eliminated previous split: + - devices/phone/images + - devices/phone/video + +### ✅ Android upload route fix +- MIME-based routing now writes to: + - originals/images + - originals/video +- DB paths now match filesystem paths + +### ✅ Device browser fix +- Browser rooted correctly at: + - devices//originals +- Folder discovery now correctly shows: + - images + - video + +### 🔧 Data repair +- Migrated existing device 37 file records: + - images → originals/images + - video → originals/video + +### ⚠️ Notes +- Previous mismatch between DB paths and filesystem caused invisible folders +- Permission issues on new directories corrected (otbcloud ownership) + +### 🧠 Next +- Improve Android upload stability (OOM handling) +- Add per-user GPU fairness scheduling (queued) + diff --git a/app/main/routes.py b/app/main/routes.py index d93b7d8..9764599 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1418,8 +1418,8 @@ def android_upload(): if not files: return jsonify({"ok": False, "error": "no_files"}), 400 - upload_base = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / row["tenant_slug"] / row["relative_path"] / "originals" - upload_base.mkdir(parents=True, exist_ok=True) + base_path = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / row["tenant_slug"] / row["relative_path"] / "originals" + base_path.mkdir(parents=True, exist_ok=True) uploaded_count = 0 @@ -1428,6 +1428,16 @@ def android_upload(): original_filename = incoming.filename or "upload.bin" stored_name = _stored_name(original_filename) + mime = (incoming.mimetype or "").lower() + + if mime.startswith("video/"): + subdir = "video" + else: + subdir = "images" + + upload_base = base_path / subdir + upload_base.mkdir(parents=True, exist_ok=True) + target_path = upload_base / stored_name incoming.save(target_path) @@ -1439,8 +1449,8 @@ def android_upload(): else: basename, extension = original_filename, "" - relative_path = f"{row['relative_path']}/originals/{stored_name}" - directory_path = f"{row['relative_path']}/originals" + relative_path = f"{row['relative_path']}/originals/{subdir}/{stored_name}" + directory_path = f"{row['relative_path']}/originals/{subdir}" cur.execute( """ @@ -1637,11 +1647,41 @@ from app.services.video_jobs import create_video_job, list_jobs_for_tenant @bp.route("/workshop/") def workshop(device_id): - return render_template("cloud/workshop.html", device_id=device_id) + from app.db import get_db + + db = get_db() + + with db.cursor() as cur: + cur.execute( + """ + SELECT COUNT(*) AS queued_jobs + FROM video_jobs + WHERE status = 'queued' + """ + ) + qrow = cur.fetchone() or {"queued_jobs": 0} + + cur.execute( + """ + SELECT COUNT(DISTINCT tenant_id) AS active_users + FROM video_jobs + WHERE status IN ('queued', 'processing') + """ + ) + arow = cur.fetchone() or {"active_users": 0} + + return render_template( + "cloud/workshop.html", + device_id=device_id, + queued_jobs=qrow["queued_jobs"] or 0, + active_users=arow["active_users"] or 0, + ) @bp.route("/api/video/enqueue", methods=["POST"]) def video_enqueue(): - data = request.json + from uuid import uuid4 + + data = request.json or {} tenant = session.get("tenant") or 'def' device_id = data.get("device_id") files = data.get("files", []) @@ -1651,6 +1691,7 @@ def video_enqueue(): if not profiles: profiles = ["default"] + batch_id = uuid4().hex job_ids = [] for f in files: @@ -1660,15 +1701,12 @@ def video_enqueue(): device_id=device_id, source_file_id=f, profile=profile, - rotation_override=rotation_override + rotation_override=rotation_override, + batch_id=batch_id ) job_ids.append(job_id) - return jsonify({"status": "ok", "jobs": job_ids}) - - - - + return jsonify({"status": "ok", "jobs": job_ids, "batch_id": batch_id}) @bp.route("/video-jobs") @@ -2045,3 +2083,27 @@ def video_jobs(): jobs = list_jobs_for_tenant(tenant) return jsonify(jobs) +@bp.route("/api/video/queue-summary") +def video_queue_summary(): + from app.db import get_db + + db = get_db() + cur = db.cursor() + + cur.execute("SELECT COUNT(*) AS c FROM video_jobs WHERE status='queued'") + row1 = cur.fetchone() + queue_count = row1["c"] if isinstance(row1, dict) else row1[0] + + cur.execute(""" + SELECT COUNT(DISTINCT tenant_id) AS c + FROM video_jobs + WHERE status IN ('queued','processing') + """) + row2 = cur.fetchone() + active_users = row2["c"] if isinstance(row2, dict) else row2[0] + + return { + "queue_count": queue_count, + "active_users": active_users + } +