Files
cross-py-builder/ctrl/workers.py
T
Brett Williams 0e89919342 Add controller with web UI and retire CLI
- Add ctrl/ package: Flask app (waitress), SQLite job store, background
  scheduler that probes configured workers, dispatches via /upload and
  /checkout_git, fetches artifacts, and serves a single-page UI
- Supporting API: /api/jobs (create/list/detail/cancel), /api/workers,
  /api/capabilities, artifact download, and SSE log stream
- Workers keep the existing HTTP API; add /progress/<job_id> endpoint and
  per-step build tracking for live status
- Retire agent_manager.py and the cross-py-builder CLI; controller UI is
  the primary frontend
- Record design in DESIGN.md; add waitress dependency

Note: /progress is exposed on the worker but the synchronous /upload and
/checkout endpoints block until a build completes, so per-step worker
progress is not yet relayed live in the UI. Live streaming needs an
async-start worker endpoint as a follow-up.
2026-08-30 21:55:46 -05:00

51 lines
1.5 KiB
Python

import ctrl.settings as settings
import requests
def parse_worker(spec):
"""'host:port' -> {'url': 'http://host:port', 'host': host, 'port': port}."""
host, _, port = spec.strip().rpartition(":")
if not host or not port.isdigit():
raise ValueError(f"Invalid worker spec: {spec!r} (expected host:port)")
return {"url": f"http://{host}:{port}", "host": host, "port": int(port)}
def configured_workers():
return [parse_worker(s) for s in settings.WORKERS]
def probe_worker(url, timeout=None):
"""Return a normalized worker status dict, or None if unreachable."""
timeout = timeout or settings.WORKER_STATUS_TIMEOUT
try:
resp = requests.get(f"{url}/status", timeout=timeout)
resp.raise_for_status()
info = resp.json()
except (requests.RequestException, ValueError):
return None
info["url"] = url
info["online"] = True
return info
def probe_all():
workers = []
for w in configured_workers():
status = probe_worker(w["url"])
if status:
workers.append(status)
else:
workers.append({"url": w["url"], "host": w["host"], "port": w["port"], "online": False})
return workers
def matches(job, worker):
if worker.get("status") != "ready":
return False
if job.get("os_req") and job["os_req"].lower() not in str(worker.get("os", "")).lower():
return False
if job.get("cpu_req") and job["cpu_req"].lower() not in str(worker.get("cpu", "")).lower():
return False
return True