From 5680af16899fbb3253279367148ed0572ed340d9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 27 Jul 2026 15:25:15 +0200 Subject: [PATCH 1/2] hackbot-ui: add local stub API with a sample run dataset A dependency-free stdlib HTTP server that stands in for hackbot-api during local UI development, so the dashboard can be exercised without Cloud SQL or GCP. Serves GET /agents, GET /runs (honoring agent/status/author/limit/offset) and GET /runs/{id} from a seeded set of runs spanning several agents, statuses, and authors (including unattributed automation runs). --- services/hackbot-ui/dev/stub_api.py | 130 ++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 services/hackbot-ui/dev/stub_api.py diff --git a/services/hackbot-ui/dev/stub_api.py b/services/hackbot-ui/dev/stub_api.py new file mode 100644 index 0000000000..a2c4b38022 --- /dev/null +++ b/services/hackbot-ui/dev/stub_api.py @@ -0,0 +1,130 @@ +"""Throwaway stand-in for hackbot-api, so the real hackbot-ui can be tried +locally without Cloud SQL / GCP. Serves just the endpoints the UI proxy calls: +GET /agents, GET /runs (with agent/status/author/limit/offset), GET /runs/{id}. +""" + +import json +from datetime import datetime, timedelta, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import parse_qs, urlparse + +DEV_USER = "sledru@mozilla.com" +AGENTS = [ + "bug-fix", + "autowebcompat-repro", + "build-repair", + "frontend-triage", + "test-plan-generator", +] +STATUSES = ["succeeded", "failed", "running", "pending", "timed_out"] +# author, agent, status, inputs +_SEED = [ + (DEV_USER, "bug-fix", "succeeded", {"bug_id": 1889001}), + (None, "bug-fix", "running", {"bug_id": 1889002, "revision_id": 412233}), + ("padenot@mozilla.com", "frontend-triage", "succeeded", {"bug_id": 1890777}), + (DEV_USER, "build-repair", "failed", {"git_commit": "a1b2c3d4e5f6a7b8"}), + (DEV_USER, "frontend-triage", "running", {"bug_id": 1891555}), + ("smujahid@mozilla.com", "test-plan-generator", "succeeded", {"feature_name": "WebGPU compute"}), + (None, "bug-fix", "timed_out", {"bug_id": 1888120, "revision_id": 410900}), + (DEV_USER, "autowebcompat-repro", "succeeded", {"bug_id": 1892003}), + ("mcastelluccio@mozilla.com", "bug-fix", "failed", {"bug_id": 1887654}), + (DEV_USER, "test-plan-generator", "pending", {"feature_name": "Cookie partitioning"}), + (DEV_USER, "bug-fix", "succeeded", {"bug_id": 1893100}), + ("smujahid@mozilla.com", "build-repair", "running", {"git_commit": "ffee00112233aabb"}), + ("mcastelluccio@mozilla.com", "bug-fix", "succeeded", {"bug_id": 1885000, "revision_id": 409001}), + (DEV_USER, "frontend-triage", "succeeded", {"bug_id": 1894222}), + ("padenot@mozilla.com", "autowebcompat-repro", "timed_out", {"bug_id": 1886777}), + (DEV_USER, "bug-fix", "failed", {"bug_id": 1895333}), + (DEV_USER, "bug-fix", "running", {"bug_id": 1896444}), + ("mcastelluccio@mozilla.com", "frontend-triage", "succeeded", {"bug_id": 1897555}), + (None, "bug-fix", "succeeded", {"bug_id": 1884321, "revision_id": 408222}), + (DEV_USER, "build-repair", "succeeded", {"git_commit": "0011223344556677"}), +] + + +def _build_runs(): + base = datetime(2026, 7, 27, 10, 0, tzinfo=timezone.utc) + runs = [] + for i, (author, agent, status, inputs) in enumerate(_SEED): + rid = f"{i:08x}-0000-4000-8000-{i:012x}" + created = (base - timedelta(minutes=17 * i)).isoformat() + err = None + if status == "failed": + err = "Agent exited non-zero: patch did not apply cleanly to tip." + elif status == "timed_out": + err = "Execution was cancelled or timed out" + runs.append( + { + "run_id": rid, + "agent": agent, + "status": status, + "inputs": inputs, + "author": author, + "created_at": created, + "updated_at": created, + "execution_name": None, + "results_prefix": f"results/{rid}/", + "summary": None, + "artifacts": [], + "error": err, + } + ) + return runs + + +RUNS = _build_runs() + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): # quieter console + pass + + def _json(self, payload, code=200): + body = json.dumps(payload).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + parsed = urlparse(self.path) + path = parsed.path + q = parse_qs(parsed.query) + + if path == "/agents": + return self._json( + [ + {"name": a, "description": f"{a} agent", "input_schema": {}} + for a in AGENTS + ] + ) + + if path.startswith("/runs/"): + rid = path[len("/runs/") :] + for r in RUNS: + if r["run_id"] == rid: + return self._json(r) + return self._json({"detail": "Run not found"}, 404) + + if path == "/runs": + items = RUNS + agent = q.get("agent", [None])[0] + status = q.get("status", [None])[0] + author = q.get("author", [None])[0] + if agent: + items = [r for r in items if r["agent"] == agent] + if status: + items = [r for r in items if r["status"] == status] + if author: + al = author.lower() + items = [r for r in items if (r["author"] or "").lower() == al] + offset = int(q.get("offset", ["0"])[0]) + limit = int(q.get("limit", ["50"])[0]) + return self._json(items[offset : offset + limit]) + + return self._json({"detail": "Not found"}, 404) + + +if __name__ == "__main__": + ThreadingHTTPServer(("127.0.0.1", 8080), Handler).serve_forever() From dff4f3a6a35058aae3ae67fd5cc049ad0e324ddd Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 27 Jul 2026 16:27:25 +0200 Subject: [PATCH 2/2] hackbot-ui: address review on the local stub API - Replace real @mozilla.com addresses in the seed data with example.com placeholders; make the "current user" identity overridable via HACKBOT_DEV_USER. - Return 400 instead of crashing on non-integer limit/offset. - Drop the unused STATUSES constant and reflow the docstring. --- ...ettings.local.json.tmp.430234.9e28f84d0377 | 41 +++++++++++++++++++ services/hackbot-ui/dev/stub_api.py | 41 ++++++++++++------- 2 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 .claude/settings.local.json.tmp.430234.9e28f84d0377 diff --git a/.claude/settings.local.json.tmp.430234.9e28f84d0377 b/.claude/settings.local.json.tmp.430234.9e28f84d0377 new file mode 100644 index 0000000000..c147ec38b5 --- /dev/null +++ b/.claude/settings.local.json.tmp.430234.9e28f84d0377 @@ -0,0 +1,41 @@ +{ + "permissions": { + "allow": [ + "Bash(uv run *)", + "Bash(PYTHONPATH=. uv run python -m pytest tests/test_list_runs_api.py -q)", + "Bash(npm run *)", + "Bash(echo \"exit=$?\")", + "Read(//tmp/**)", + "Bash(docker rm *)", + "Bash(rm -f /tmp/up.sql /tmp/down.sql /tmp/up.err /tmp/alembic_local.ini)", + "Bash(git check-ignore *)", + "Bash(curl -s \"http://localhost:3000/api/runs?limit=50&author=sledru@mozilla.com\")", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('count', len\\(d\\)\\); [print\\(' ', r['run_id'][:8], r['agent'], r['status'], r['author']\\) for r in d[:4]]\")", + "Bash(curl -s \"http://localhost:3000/api/runs?limit=50\")", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('count', len\\(d\\), '| distinct authors:', sorted\\({str\\(r['author']\\) for r in d}\\)\\)\")", + "Bash(curl -s \"http://localhost:3000/api/runs?limit=50&agent=bug-fix&status=failed\")", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('count', len\\(d\\)\\)\")", + "Bash(curl -s \"http://127.0.0.1:8080/runs?limit=50\")", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); from collections import Counter; c=Counter\\(str\\(r['author']\\) for r in d\\); print\\('all runs by author:', dict\\(c\\)\\)\")", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(sorted\\({str\\(r['author']\\) for r in d}\\)\\)\")", + "Bash(python3 stub_api.py)", + "Bash(python3 -c \"import sys,json; from collections import Counter; d=json.load\\(sys.stdin\\); print\\(dict\\(Counter\\(str\\(r['author']\\) for r in d\\)\\)\\)\")", + "Bash(python3 -c \"import sys,json; print\\(len\\(json.load\\(sys.stdin\\)\\)\\)\")", + "Bash(pkill -f stub_api.py)", + "Bash(pkill -f \"next dev\")", + "Bash(pkill -f \"next-server\")", + "Bash(rm -f services/hackbot-ui/.env.local)", + "Bash(npx tsc *)", + "Bash(echo \"tsc exit=$?\")", + "Bash(git commit -q -m 'hackbot: record and filter runs by author *)", + "Bash(git checkout *)", + "Bash(git add *)", + "Bash(git commit -q -m 'hackbot-ui: add local stub API with a sample run dataset *)", + "Bash(git push *)", + "Bash(gh pr *)", + "Bash(uvx ruff *)", + "Bash(python3 -c ' *)", + "Bash(git commit -q -m 'hackbot-ui: address review on the local stub API *)" + ] + } +} diff --git a/services/hackbot-ui/dev/stub_api.py b/services/hackbot-ui/dev/stub_api.py index a2c4b38022..729d25e0ec 100644 --- a/services/hackbot-ui/dev/stub_api.py +++ b/services/hackbot-ui/dev/stub_api.py @@ -1,14 +1,22 @@ -"""Throwaway stand-in for hackbot-api, so the real hackbot-ui can be tried -locally without Cloud SQL / GCP. Serves just the endpoints the UI proxy calls: -GET /agents, GET /runs (with agent/status/author/limit/offset), GET /runs/{id}. +"""Throwaway stand-in for hackbot-api. + +Lets the real hackbot-ui be tried locally without Cloud SQL / GCP. Serves just +the endpoints the UI proxy calls: GET /agents, GET /runs (with +agent/status/author/limit/offset), and GET /runs/{run_id}. """ import json +import os from datetime import datetime, timedelta, timezone from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import parse_qs, urlparse -DEV_USER = "sledru@mozilla.com" +# The identity the seeded "my runs" belong to. Override it with the address you +# sign in to the local UI with, so the "My runs" filter shows something. +DEV_USER = os.environ.get("HACKBOT_DEV_USER", "dev@example.com") +# Placeholder co-workers; deliberately fake so no real address is committed. +OTHER_USERS = ["alice@example.com", "bob@example.com", "carol@example.com"] +_ALICE, _BOB, _CAROL = OTHER_USERS AGENTS = [ "bug-fix", "autowebcompat-repro", @@ -16,27 +24,26 @@ "frontend-triage", "test-plan-generator", ] -STATUSES = ["succeeded", "failed", "running", "pending", "timed_out"] # author, agent, status, inputs _SEED = [ (DEV_USER, "bug-fix", "succeeded", {"bug_id": 1889001}), (None, "bug-fix", "running", {"bug_id": 1889002, "revision_id": 412233}), - ("padenot@mozilla.com", "frontend-triage", "succeeded", {"bug_id": 1890777}), + (_ALICE, "frontend-triage", "succeeded", {"bug_id": 1890777}), (DEV_USER, "build-repair", "failed", {"git_commit": "a1b2c3d4e5f6a7b8"}), (DEV_USER, "frontend-triage", "running", {"bug_id": 1891555}), - ("smujahid@mozilla.com", "test-plan-generator", "succeeded", {"feature_name": "WebGPU compute"}), + (_BOB, "test-plan-generator", "succeeded", {"feature_name": "WebGPU compute"}), (None, "bug-fix", "timed_out", {"bug_id": 1888120, "revision_id": 410900}), (DEV_USER, "autowebcompat-repro", "succeeded", {"bug_id": 1892003}), - ("mcastelluccio@mozilla.com", "bug-fix", "failed", {"bug_id": 1887654}), - (DEV_USER, "test-plan-generator", "pending", {"feature_name": "Cookie partitioning"}), + (_CAROL, "bug-fix", "failed", {"bug_id": 1887654}), + (DEV_USER, "test-plan-generator", "pending", {"feature_name": "Cookie jars"}), (DEV_USER, "bug-fix", "succeeded", {"bug_id": 1893100}), - ("smujahid@mozilla.com", "build-repair", "running", {"git_commit": "ffee00112233aabb"}), - ("mcastelluccio@mozilla.com", "bug-fix", "succeeded", {"bug_id": 1885000, "revision_id": 409001}), + (_BOB, "build-repair", "running", {"git_commit": "ffee00112233aabb"}), + (_CAROL, "bug-fix", "succeeded", {"bug_id": 1885000, "revision_id": 409001}), (DEV_USER, "frontend-triage", "succeeded", {"bug_id": 1894222}), - ("padenot@mozilla.com", "autowebcompat-repro", "timed_out", {"bug_id": 1886777}), + (_ALICE, "autowebcompat-repro", "timed_out", {"bug_id": 1886777}), (DEV_USER, "bug-fix", "failed", {"bug_id": 1895333}), (DEV_USER, "bug-fix", "running", {"bug_id": 1896444}), - ("mcastelluccio@mozilla.com", "frontend-triage", "succeeded", {"bug_id": 1897555}), + (_CAROL, "frontend-triage", "succeeded", {"bug_id": 1897555}), (None, "bug-fix", "succeeded", {"bug_id": 1884321, "revision_id": 408222}), (DEV_USER, "build-repair", "succeeded", {"git_commit": "0011223344556677"}), ] @@ -119,8 +126,12 @@ def do_GET(self): if author: al = author.lower() items = [r for r in items if (r["author"] or "").lower() == al] - offset = int(q.get("offset", ["0"])[0]) - limit = int(q.get("limit", ["50"])[0]) + try: + offset = int(q.get("offset", ["0"])[0]) + limit = int(q.get("limit", ["50"])[0]) + except ValueError: + # Don't take the server down over a typo'd query string. + return self._json({"detail": "limit/offset must be integers"}, 400) return self._json(items[offset : offset + limit]) return self._json({"detail": "Not found"}, 404)