Browse Source

alpha4.2: storage structure unified (originals/images + originals/video), browser fix, android path alignment

master
Don Kingdon 2 weeks ago
parent
commit
04028cfbda
  1. 37
      PROJECT_STATE.md
  2. 86
      app/main/routes.py

37
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 ### Recommended Next Step
Proceed to Android app update: Proceed to Android app update:
- allow app-side selection for images, videos, or both - 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/<device>/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)

86
app/main/routes.py

@ -1418,8 +1418,8 @@ def android_upload():
if not files: if not files:
return jsonify({"ok": False, "error": "no_files"}), 400 return jsonify({"ok": False, "error": "no_files"}), 400
upload_base = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / row["tenant_slug"] / row["relative_path"] / "originals" base_path = Path(current_app.config["STORAGE_ROOT"]) / "tenants" / row["tenant_slug"] / row["relative_path"] / "originals"
upload_base.mkdir(parents=True, exist_ok=True) base_path.mkdir(parents=True, exist_ok=True)
uploaded_count = 0 uploaded_count = 0
@ -1428,6 +1428,16 @@ def android_upload():
original_filename = incoming.filename or "upload.bin" original_filename = incoming.filename or "upload.bin"
stored_name = _stored_name(original_filename) 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 target_path = upload_base / stored_name
incoming.save(target_path) incoming.save(target_path)
@ -1439,8 +1449,8 @@ def android_upload():
else: else:
basename, extension = original_filename, "" basename, extension = original_filename, ""
relative_path = f"{row['relative_path']}/originals/{stored_name}" relative_path = f"{row['relative_path']}/originals/{subdir}/{stored_name}"
directory_path = f"{row['relative_path']}/originals" directory_path = f"{row['relative_path']}/originals/{subdir}"
cur.execute( cur.execute(
""" """
@ -1637,11 +1647,41 @@ from app.services.video_jobs import create_video_job, list_jobs_for_tenant
@bp.route("/workshop/<int:device_id>") @bp.route("/workshop/<int:device_id>")
def workshop(device_id): 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"]) @bp.route("/api/video/enqueue", methods=["POST"])
def video_enqueue(): def video_enqueue():
data = request.json from uuid import uuid4
data = request.json or {}
tenant = session.get("tenant") or 'def' tenant = session.get("tenant") or 'def'
device_id = data.get("device_id") device_id = data.get("device_id")
files = data.get("files", []) files = data.get("files", [])
@ -1651,6 +1691,7 @@ def video_enqueue():
if not profiles: if not profiles:
profiles = ["default"] profiles = ["default"]
batch_id = uuid4().hex
job_ids = [] job_ids = []
for f in files: for f in files:
@ -1660,15 +1701,12 @@ def video_enqueue():
device_id=device_id, device_id=device_id,
source_file_id=f, source_file_id=f,
profile=profile, profile=profile,
rotation_override=rotation_override rotation_override=rotation_override,
batch_id=batch_id
) )
job_ids.append(job_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") @bp.route("/video-jobs")
@ -2045,3 +2083,27 @@ def video_jobs():
jobs = list_jobs_for_tenant(tenant) jobs = list_jobs_for_tenant(tenant)
return jsonify(jobs) 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
}

Loading…
Cancel
Save