""" Flask-based UI + proxy to the Android build-runner. Configuration: - BUILD_RUNNER_URL (env): base URL of your build-runner, e.g. https://runpod-host.example - DEFAULT_API_KEY (env): optional default API key to send to build-runner - PORT (env): port (default 8080) """ import os from flask import Flask, send_from_directory, request, jsonify, Response, redirect import requests BUILD_RUNNER = os.environ.get("BUILD_RUNNER_URL", "").rstrip("/") DEFAULT_API_KEY = os.environ.get("DEFAULT_API_KEY", "") if not BUILD_RUNNER: # App will still run locally; advise user to set BUILD_RUNNER_URL print("WARNING: BUILD_RUNNER_URL not set. UI will not be able to trigger builds until set.") app = Flask(__name__, static_folder="static", static_url_path="/static") def forward_headers(incoming): headers = {} # Accept an API key from the UI (header or form) or fallback to DEFAULT_API_KEY api_key = incoming.headers.get("X-API-KEY") or incoming.args.get("api_key") or incoming.form.get("api_key") or DEFAULT_API_KEY if api_key: headers["X-API-KEY"] = api_key return headers @app.route("/", methods=["GET"]) def index(): return send_from_directory("static", "index.html") # Proxy endpoints @app.route("/build", methods=["POST"]) def build(): if not BUILD_RUNNER: return jsonify({"error": "BUILD_RUNNER_URL not configured"}), 500 headers = forward_headers(request) # If UI sends a file (multipart), forward as-stream if request.content_type and request.content_type.startswith("multipart/"): files = {} data = {} # forward known fields for k in request.form: data[k] = request.form[k] if 'file' in request.files: f = request.files['file'] files['file'] = (f.filename, f.stream, f.mimetype) resp = requests.post(f"{BUILD_RUNNER}/build", headers=headers, data=data, files=files, timeout=300) else: # JSON body (repo_url etc) payload = request.get_json(force=False) or request.form.to_dict() resp = requests.post(f"{BUILD_RUNNER}/build", headers=headers, json=payload, timeout=300) return Response(resp.content, status=resp.status_code, content_type=resp.headers.get("content-type", "application/json")) @app.route("/status/", methods=["GET"]) def status(job_id): if not BUILD_RUNNER: return jsonify({"error": "BUILD_RUNNER_URL not configured"}), 500 headers = forward_headers(request) resp = requests.get(f"{BUILD_RUNNER}/status/{job_id}", headers=headers, timeout=30) return Response(resp.content, status=resp.status_code, content_type=resp.headers.get("content-type", "application/json")) @app.route("/logs/", methods=["GET"]) def logs(job_id): if not BUILD_RUNNER: return jsonify({"error": "BUILD_RUNNER_URL not configured"}), 500 headers = forward_headers(request) resp = requests.get(f"{BUILD_RUNNER}/logs/{job_id}", headers=headers, stream=True, timeout=300) def stream(): for chunk in resp.iter_content(chunk_size=4096): if chunk: yield chunk return Response(stream(), status=resp.status_code, content_type=resp.headers.get("content-type", "text/plain")) @app.route("/artifacts//", methods=["GET"]) def artifact(job_id, name): if not BUILD_RUNNER: return jsonify({"error": "BUILD_RUNNER_URL not configured"}), 500 headers = forward_headers(request) # The build-runner returns artifact URLs in status; we can redirect by trying to fetch the status and locating artifact URL. resp = requests.get(f"{BUILD_RUNNER}/status/{job_id}", headers=headers, timeout=10) if resp.status_code != 200: return Response(resp.content, status=resp.status_code, content_type=resp.headers.get("content-type", "application/json")) data = resp.json() for a in data.get("artifacts", []): if a.get("name") == name: url = a.get("url") # If url is a relative path on the runner, convert to full URL if url.startswith("/"): return redirect(BUILD_RUNNER + url) else: return redirect(url) return jsonify({"error": "artifact not found"}), 404 # Serve static assets under /static/* automatically # (Flask static_folder handles it) if __name__ == "__main__": port = int(os.environ.get("PORT", 8080)) app.run(host="0.0.0.0", port=port)