Merge pull request 'feat: add lease and collision visibility to web UI (Closes #433)' (#454) from feat/issue-433-lease-visibility into master
This commit was merged in pull request #454.
This commit is contained in:
+11
-2
@@ -46,7 +46,8 @@ Optional environment variables:
|
||||
| `/runtime` | Stub — MCP runtime health (#430) |
|
||||
| `/audit` | Stub — report audit paste (#431) |
|
||||
| `/worktrees` | Stub — hygiene dashboard (#432) |
|
||||
| `/leases` | Stub — lease visibility (#433) |
|
||||
| `/leases` | Lease and collision visibility (#433) |
|
||||
| `/api/leases` | JSON lease/collision export |
|
||||
|
||||
All routes are GET-only. POST/PUT/PATCH/DELETE return `405` with
|
||||
`read-only-mvp`.
|
||||
@@ -82,8 +83,16 @@ credentials. The UI surfaces pagination proof (returned count, pages fetched,
|
||||
If credentials are missing or the fetch fails, the page shows an explicit error
|
||||
instead of an empty queue (fail closed).
|
||||
|
||||
## Lease visibility (#433)
|
||||
|
||||
`/leases` surfaces read-only lease and collision state: local issue lock file,
|
||||
in-progress claim inventory (#268), reviewer PR lease comments when present
|
||||
(`<!-- mcp-review-lease:v1 -->`, #407), duplicate open PRs per issue (#400),
|
||||
and duplicate local branches per issue. Links to collision-history backend
|
||||
issues (#267, #268, #400, #407) are included. No lease acquire/release from UI.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py -q
|
||||
pytest tests/test_webui_skeleton.py tests/test_webui_project_registry.py tests/test_webui_prompt_library.py tests/test_webui_queue_dashboard.py tests/test_webui_lease_visibility.py -q
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Tests for web UI lease visibility (#433)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from webui.app import create_app
|
||||
from webui.lease_loader import (
|
||||
_duplicate_branch_warnings,
|
||||
_duplicate_pr_warnings,
|
||||
load_lease_snapshot,
|
||||
parse_reviewer_lease_comment,
|
||||
snapshot_to_dict,
|
||||
)
|
||||
|
||||
|
||||
class TestLeaseLoader(unittest.TestCase):
|
||||
def test_parse_reviewer_lease_comment(self):
|
||||
body = (
|
||||
"<!-- mcp-review-lease:v1 -->\n"
|
||||
"pr: #42\n"
|
||||
"reviewer_identity: sysadmin\n"
|
||||
"profile: prgs-reviewer\n"
|
||||
"phase: validating\n"
|
||||
"expires_at: 2026-07-07T20:00:00Z\n"
|
||||
)
|
||||
parsed = parse_reviewer_lease_comment(body)
|
||||
self.assertIsNotNone(parsed)
|
||||
assert parsed is not None
|
||||
self.assertEqual(parsed["pr_number"], 42)
|
||||
self.assertEqual(parsed["phase"], "validating")
|
||||
|
||||
def test_duplicate_pr_warnings(self):
|
||||
raw_prs = [
|
||||
{"number": 1, "title": "Closes #99", "body": ""},
|
||||
{"number": 2, "title": "fixes #99", "body": ""},
|
||||
]
|
||||
warnings = _duplicate_pr_warnings(raw_prs)
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].issue_number, 99)
|
||||
self.assertEqual(warnings[0].pr_numbers, (1, 2))
|
||||
|
||||
def test_duplicate_branch_warnings(self):
|
||||
warnings = _duplicate_branch_warnings([
|
||||
"feat/issue-12-a",
|
||||
"feat/issue-12-b",
|
||||
"master",
|
||||
])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].issue_number, 12)
|
||||
|
||||
def test_load_snapshot_with_injected_fetch(self):
|
||||
def fetch_prs(_h, _o, _r, _a):
|
||||
return ([], None)
|
||||
|
||||
def fetch_issues(_h, _o, _r, _a):
|
||||
return ([], None)
|
||||
|
||||
snapshot = load_lease_snapshot(
|
||||
fetch_prs=fetch_prs,
|
||||
fetch_issues=fetch_issues,
|
||||
fetch_comments=lambda *_a, **_k: [],
|
||||
issue_lock_path=str(Path("/nonexistent/lock.json")),
|
||||
)
|
||||
data = snapshot_to_dict(snapshot)
|
||||
self.assertIn("claim_inventory", data)
|
||||
self.assertIn("collision_history", data)
|
||||
|
||||
|
||||
class TestLeaseRoutes(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = TestClient(create_app())
|
||||
|
||||
def test_leases_page_renders(self):
|
||||
response = self.client.get("/leases")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Leases", response.text)
|
||||
self.assertIn("Collision warnings", response.text)
|
||||
self.assertNotIn("child issue", response.text.lower())
|
||||
|
||||
def test_api_leases_json(self):
|
||||
response = self.client.get("/api/leases")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertIn("claim_inventory", data)
|
||||
self.assertIn("duplicate_prs", data)
|
||||
self.assertIn("reviewer_leases", data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -46,10 +46,14 @@ class TestWebuiSkeleton(unittest.TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Gitea-Tools", response.text)
|
||||
|
||||
def test_leases_is_implemented(self):
|
||||
response = self.client.get("/leases")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Leases", response.text)
|
||||
self.assertIn("Collision warnings", response.text)
|
||||
|
||||
def test_extra_stub_routes(self):
|
||||
for path in ("/worktrees", "/leases"):
|
||||
with self.subTest(path=path):
|
||||
self.assertEqual(self.client.get(path).status_code, 200)
|
||||
self.assertEqual(self.client.get("/worktrees").status_code, 200)
|
||||
|
||||
def test_post_is_rejected(self):
|
||||
response = self.client.post("/health")
|
||||
@@ -62,10 +66,10 @@ class TestWebuiSkeleton(unittest.TestCase):
|
||||
self.assertIn("Live queue", response.text)
|
||||
|
||||
def test_nav_links_on_all_pages(self):
|
||||
for path in ("/", "/queue", "/projects", "/prompts", "/runtime", "/audit"):
|
||||
for path in ("/", "/queue", "/projects", "/prompts", "/runtime", "/audit", "/leases"):
|
||||
with self.subTest(path=path):
|
||||
text = self.client.get(path).text
|
||||
for href in ("/queue", "/projects", "/prompts", "/runtime", "/audit"):
|
||||
for href in ("/queue", "/projects", "/prompts", "/runtime", "/audit", "/leases"):
|
||||
self.assertIn(f'href="{href}"', text)
|
||||
|
||||
|
||||
|
||||
+9
-4
@@ -14,6 +14,8 @@ from webui.project_registry import find_project, load_registry, registry_to_dict
|
||||
from webui.project_views import render_project_detail, render_projects_list
|
||||
from webui.prompt_library import find_prompt, library_to_dict
|
||||
from webui.prompt_views import render_prompt_detail, render_prompts_page
|
||||
from webui.lease_loader import load_lease_snapshot, snapshot_to_dict as lease_snapshot_to_dict
|
||||
from webui.lease_views import render_leases_page
|
||||
from webui.queue_loader import load_queue_snapshot, snapshot_to_dict
|
||||
from webui.queue_views import render_queue_page
|
||||
|
||||
@@ -141,10 +143,12 @@ async def worktrees(_request: Request) -> HTMLResponse:
|
||||
|
||||
|
||||
async def leases(_request: Request) -> HTMLResponse:
|
||||
return _stub_page(
|
||||
"Leases",
|
||||
"Lease visibility will show active issue and reviewer PR leases.",
|
||||
)
|
||||
snapshot = load_lease_snapshot()
|
||||
return HTMLResponse(render_leases_page(snapshot))
|
||||
|
||||
|
||||
async def api_leases(_request: Request) -> JSONResponse:
|
||||
return JSONResponse(lease_snapshot_to_dict(load_lease_snapshot()))
|
||||
|
||||
|
||||
async def method_not_allowed(request: Request, _exc: Exception) -> Response:
|
||||
@@ -175,6 +179,7 @@ def create_app() -> Starlette:
|
||||
Route("/audit", audit, methods=["GET"]),
|
||||
Route("/worktrees", worktrees, methods=["GET"]),
|
||||
Route("/leases", leases, methods=["GET"]),
|
||||
Route("/api/leases", api_leases, methods=["GET"]),
|
||||
],
|
||||
exception_handlers={405: method_not_allowed},
|
||||
)
|
||||
@@ -0,0 +1,330 @@
|
||||
"""Lease and collision visibility for the web UI (#433)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from gitea_auth import api_fetch_page, get_auth_header, repo_api_url
|
||||
from issue_claim_heartbeat import build_claim_inventory
|
||||
from merged_cleanup_reconcile import ISSUE_LOCK_FILE, read_issue_lock
|
||||
|
||||
from webui.project_registry import ProjectRecord, load_registry
|
||||
from webui.queue_loader import _extract_linked_issue, _fetch_issues, _fetch_prs
|
||||
|
||||
_REVIEWER_LEASE_MARKER = "<!-- mcp-review-lease:v1 -->"
|
||||
_REVIEWER_FIELD_RE = re.compile(
|
||||
r"^\s*([a-z_]+)\s*:\s*(.+?)\s*$",
|
||||
re.IGNORECASE | re.MULTILINE,
|
||||
)
|
||||
_ISSUE_BRANCH_RE = re.compile(r"issue-(\d+)", re.IGNORECASE)
|
||||
|
||||
_COLLISION_HISTORY = (
|
||||
{"number": 267, "title": "Author work leases"},
|
||||
{"number": 268, "title": "Issue claim heartbeat leases"},
|
||||
{"number": 400, "title": "Early duplicate-work detection"},
|
||||
{"number": 407, "title": "Per-PR reviewer leases"},
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CollisionWarning:
|
||||
kind: str
|
||||
message: str
|
||||
issue_number: int | None = None
|
||||
pr_numbers: tuple[int, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LeaseSnapshot:
|
||||
project_id: str
|
||||
repo_label: str
|
||||
issue_lock: dict[str, Any] | None
|
||||
claim_inventory: dict[str, Any]
|
||||
reviewer_leases: tuple[dict[str, Any], ...]
|
||||
duplicate_prs: tuple[CollisionWarning, ...]
|
||||
duplicate_branches: tuple[CollisionWarning, ...]
|
||||
collision_history: tuple[dict[str, Any], ...]
|
||||
fetch_error: str | None = None
|
||||
|
||||
|
||||
def _repo_root() -> str:
|
||||
override = (os.environ.get("WEBUI_REPO_ROOT") or "").strip()
|
||||
if override:
|
||||
return os.path.realpath(override)
|
||||
return os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
||||
def _host_from_url(remote_host: str) -> str:
|
||||
parsed = urlparse(remote_host.strip())
|
||||
return parsed.netloc or remote_host.strip().rstrip("/")
|
||||
|
||||
|
||||
def _parse_pr_ref(value: str | None) -> int | None:
|
||||
digits = re.sub(r"[^\d]", "", value or "")
|
||||
return int(digits) if digits.isdigit() else None
|
||||
|
||||
|
||||
def parse_reviewer_lease_comment(body: str) -> dict[str, Any] | None:
|
||||
text = body or ""
|
||||
if _REVIEWER_LEASE_MARKER not in text:
|
||||
return None
|
||||
fields: dict[str, str] = {}
|
||||
for match in _REVIEWER_FIELD_RE.finditer(text):
|
||||
fields[match.group(1).strip().lower()] = match.group(2).strip()
|
||||
if not fields:
|
||||
return None
|
||||
return {
|
||||
"pr_number": _parse_pr_ref(fields.get("pr")),
|
||||
"issue_number": _parse_pr_ref(fields.get("issue")),
|
||||
"reviewer_identity": fields.get("reviewer_identity"),
|
||||
"profile": fields.get("profile"),
|
||||
"phase": (fields.get("phase") or "").strip().lower() or None,
|
||||
"expires_at": fields.get("expires_at"),
|
||||
"blocker": fields.get("blocker"),
|
||||
}
|
||||
|
||||
|
||||
def _fetch_comments(
|
||||
host: str,
|
||||
org: str,
|
||||
repo: str,
|
||||
auth: str,
|
||||
*,
|
||||
issue_number: int,
|
||||
) -> list[dict]:
|
||||
url = f"{repo_api_url(host, org, repo)}/issues/{issue_number}/comments"
|
||||
comments: list[dict] = []
|
||||
page = 1
|
||||
while page <= 10:
|
||||
raw_page, meta = api_fetch_page(url, auth, page=page, limit=50)
|
||||
comments.extend(raw_page)
|
||||
if meta.get("is_final_page"):
|
||||
break
|
||||
page += 1
|
||||
return comments
|
||||
|
||||
|
||||
def _list_local_branch_names(project_root: str) -> list[str]:
|
||||
result = subprocess.run(
|
||||
["git", "-C", project_root, "branch", "--list", "--format=%(refname:short)"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
return [line.strip() for line in (result.stdout or "").splitlines() if line.strip()]
|
||||
|
||||
|
||||
def _duplicate_pr_warnings(raw_prs: list[dict]) -> list[CollisionWarning]:
|
||||
issue_to_prs: dict[int, list[int]] = {}
|
||||
for pr in raw_prs:
|
||||
linked = _extract_linked_issue(pr.get("title"), pr.get("body"))
|
||||
if linked is None:
|
||||
continue
|
||||
issue_to_prs.setdefault(linked, []).append(int(pr["number"]))
|
||||
warnings: list[CollisionWarning] = []
|
||||
for issue_number, pr_numbers in sorted(issue_to_prs.items()):
|
||||
if len(pr_numbers) > 1:
|
||||
warnings.append(
|
||||
CollisionWarning(
|
||||
kind="duplicate-pr",
|
||||
issue_number=issue_number,
|
||||
pr_numbers=tuple(sorted(pr_numbers)),
|
||||
message=(
|
||||
f"Issue #{issue_number} has {len(pr_numbers)} open PRs: "
|
||||
f"{', '.join(f'#{n}' for n in sorted(pr_numbers))} (#400)"
|
||||
),
|
||||
)
|
||||
)
|
||||
return warnings
|
||||
|
||||
|
||||
def _duplicate_branch_warnings(branch_names: list[str]) -> list[CollisionWarning]:
|
||||
issue_to_branches: dict[int, list[str]] = {}
|
||||
for name in branch_names:
|
||||
match = _ISSUE_BRANCH_RE.search(name)
|
||||
if not match:
|
||||
continue
|
||||
issue_num = int(match.group(1))
|
||||
issue_to_branches.setdefault(issue_num, []).append(name)
|
||||
warnings: list[CollisionWarning] = []
|
||||
for issue_number, branches in sorted(issue_to_branches.items()):
|
||||
if len(branches) > 1:
|
||||
warnings.append(
|
||||
CollisionWarning(
|
||||
kind="duplicate-branch",
|
||||
issue_number=issue_number,
|
||||
message=(
|
||||
f"Issue #{issue_number} has {len(branches)} local branches: "
|
||||
f"{', '.join(branches)}"
|
||||
),
|
||||
)
|
||||
)
|
||||
return warnings
|
||||
|
||||
|
||||
def _extract_reviewer_leases(
|
||||
raw_prs: list[dict],
|
||||
*,
|
||||
fetch_comments: Callable[[int], list[dict]],
|
||||
) -> list[dict[str, Any]]:
|
||||
leases: list[dict[str, Any]] = []
|
||||
for pr in raw_prs:
|
||||
pr_number = int(pr["number"])
|
||||
for comment in fetch_comments(pr_number):
|
||||
parsed = parse_reviewer_lease_comment(comment.get("body") or "")
|
||||
if not parsed:
|
||||
continue
|
||||
leases.append(
|
||||
{
|
||||
**parsed,
|
||||
"pr_number": parsed.get("pr_number") or pr_number,
|
||||
"comment_id": comment.get("id"),
|
||||
"author": (comment.get("user") or {}).get("login"),
|
||||
"created_at": comment.get("created_at"),
|
||||
}
|
||||
)
|
||||
active_phases = {"claimed", "validating", "approved", "request-changes", "merging"}
|
||||
return [lease for lease in leases if (lease.get("phase") or "") in active_phases]
|
||||
|
||||
|
||||
def load_lease_snapshot(
|
||||
*,
|
||||
project_id: str | None = None,
|
||||
project_root: str | None = None,
|
||||
issue_lock_path: str | None = None,
|
||||
fetch_prs: Callable | None = None,
|
||||
fetch_issues: Callable | None = None,
|
||||
fetch_comments: Callable[[str, str, str, str, int], list[dict]] | None = None,
|
||||
) -> LeaseSnapshot:
|
||||
registry = load_registry()
|
||||
project: ProjectRecord | None = None
|
||||
if project_id:
|
||||
project = next((p for p in registry.projects if p.id == project_id), None)
|
||||
else:
|
||||
project = registry.projects[0] if registry.projects else None
|
||||
|
||||
root = project_root or _repo_root()
|
||||
lock = read_issue_lock(issue_lock_path if issue_lock_path is not None else ISSUE_LOCK_FILE)
|
||||
|
||||
if project is None:
|
||||
return LeaseSnapshot(
|
||||
project_id=project_id or "",
|
||||
repo_label="",
|
||||
issue_lock=lock,
|
||||
claim_inventory={"entries": [], "counts": {}},
|
||||
reviewer_leases=(),
|
||||
duplicate_prs=(),
|
||||
duplicate_branches=(),
|
||||
collision_history=_COLLISION_HISTORY,
|
||||
fetch_error="project not found in registry",
|
||||
)
|
||||
|
||||
host = _host_from_url(project.remote_host)
|
||||
pr_fetch = fetch_prs or _fetch_prs
|
||||
issue_fetch = fetch_issues or _fetch_issues
|
||||
comment_fetch = fetch_comments or _fetch_comments
|
||||
using_live = fetch_prs is None or fetch_issues is None
|
||||
auth = get_auth_header(host) if using_live else "test-auth"
|
||||
|
||||
if using_live and not auth:
|
||||
return LeaseSnapshot(
|
||||
project_id=project.id,
|
||||
repo_label=f"{project.gitea_owner}/{project.repo_name}",
|
||||
issue_lock=lock,
|
||||
claim_inventory={"entries": [], "counts": {}},
|
||||
reviewer_leases=(),
|
||||
duplicate_prs=(),
|
||||
duplicate_branches=_duplicate_branch_warnings(_list_local_branch_names(root)),
|
||||
collision_history=_COLLISION_HISTORY,
|
||||
fetch_error=(
|
||||
f"Gitea credentials unavailable for {host}; "
|
||||
"remote lease artifacts cannot be loaded"
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
raw_prs, _ = pr_fetch(host, project.gitea_owner, project.repo_name, auth)
|
||||
raw_issues, _ = issue_fetch(host, project.gitea_owner, project.repo_name, auth)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return LeaseSnapshot(
|
||||
project_id=project.id,
|
||||
repo_label=f"{project.gitea_owner}/{project.repo_name}",
|
||||
issue_lock=lock,
|
||||
claim_inventory={"entries": [], "counts": {}},
|
||||
reviewer_leases=(),
|
||||
duplicate_prs=(),
|
||||
duplicate_branches=_duplicate_branch_warnings(_list_local_branch_names(root)),
|
||||
collision_history=_COLLISION_HISTORY,
|
||||
fetch_error=f"Gitea fetch failed: {exc}",
|
||||
)
|
||||
|
||||
comments_by_issue: dict[int, list[dict]] = {}
|
||||
for issue in raw_issues:
|
||||
if not any(lb.get("name") == "status:in-progress" for lb in issue.get("labels", [])):
|
||||
continue
|
||||
number = int(issue["number"])
|
||||
comments_by_issue[number] = comment_fetch(
|
||||
host, project.gitea_owner, project.repo_name, auth, issue_number=number
|
||||
)
|
||||
|
||||
branch_names = _list_local_branch_names(root)
|
||||
inventory = build_claim_inventory(
|
||||
issues=raw_issues,
|
||||
comments_by_issue=comments_by_issue,
|
||||
open_prs=raw_prs,
|
||||
branch_names=branch_names,
|
||||
)
|
||||
|
||||
def _pr_comments(pr_number: int) -> list[dict]:
|
||||
return comment_fetch(
|
||||
host, project.gitea_owner, project.repo_name, auth, issue_number=pr_number
|
||||
)
|
||||
|
||||
reviewer_leases = tuple(_extract_reviewer_leases(raw_prs, fetch_comments=_pr_comments))
|
||||
|
||||
return LeaseSnapshot(
|
||||
project_id=project.id,
|
||||
repo_label=f"{project.gitea_owner}/{project.repo_name}",
|
||||
issue_lock=lock,
|
||||
claim_inventory=inventory,
|
||||
reviewer_leases=reviewer_leases,
|
||||
duplicate_prs=tuple(_duplicate_pr_warnings(raw_prs)),
|
||||
duplicate_branches=tuple(_duplicate_branch_warnings(branch_names)),
|
||||
collision_history=_COLLISION_HISTORY,
|
||||
)
|
||||
|
||||
|
||||
def snapshot_to_dict(snapshot: LeaseSnapshot) -> dict[str, Any]:
|
||||
return {
|
||||
"project_id": snapshot.project_id,
|
||||
"repo_label": snapshot.repo_label,
|
||||
"issue_lock": snapshot.issue_lock,
|
||||
"claim_inventory": snapshot.claim_inventory,
|
||||
"reviewer_leases": list(snapshot.reviewer_leases),
|
||||
"duplicate_prs": [
|
||||
{
|
||||
"kind": w.kind,
|
||||
"message": w.message,
|
||||
"issue_number": w.issue_number,
|
||||
"pr_numbers": list(w.pr_numbers),
|
||||
}
|
||||
for w in snapshot.duplicate_prs
|
||||
],
|
||||
"duplicate_branches": [
|
||||
{
|
||||
"kind": w.kind,
|
||||
"message": w.message,
|
||||
"issue_number": w.issue_number,
|
||||
}
|
||||
for w in snapshot.duplicate_branches
|
||||
],
|
||||
"collision_history": list(snapshot.collision_history),
|
||||
"fetch_error": snapshot.fetch_error,
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"""HTML views for lease and collision visibility (#433)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
|
||||
from webui.layout import render_page
|
||||
from webui.lease_loader import LeaseSnapshot
|
||||
|
||||
|
||||
def _escape(text: str) -> str:
|
||||
return html.escape(text, quote=True)
|
||||
|
||||
|
||||
def _warnings_block(snapshot: LeaseSnapshot) -> str:
|
||||
warnings = list(snapshot.duplicate_prs) + list(snapshot.duplicate_branches)
|
||||
if not warnings:
|
||||
return "<p class='muted'>No duplicate PR/branch collisions detected in current inventory.</p>"
|
||||
items = "".join(f"<li>{_escape(w.message)}</li>" for w in warnings)
|
||||
return f"<ul class='collision-warnings'>{items}</ul>"
|
||||
|
||||
|
||||
def _lock_block(snapshot: LeaseSnapshot) -> str:
|
||||
lock = snapshot.issue_lock
|
||||
if not lock:
|
||||
return "<p class='muted'>No active local issue lock file.</p>"
|
||||
return (
|
||||
"<pre class='prompt-text'>"
|
||||
f"{_escape(json.dumps(lock, indent=2, sort_keys=True))}"
|
||||
"</pre>"
|
||||
)
|
||||
|
||||
|
||||
def _claims_table(snapshot: LeaseSnapshot) -> str:
|
||||
entries = snapshot.claim_inventory.get("entries") or []
|
||||
if not entries:
|
||||
return "<p class='muted'>No in-progress issue claims in fetched inventory.</p>"
|
||||
rows = []
|
||||
for entry in entries:
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td>#{entry.get('issue_number')}</td>"
|
||||
f"<td><span class='badge badge-{_escape(str(entry.get('status')))}'>"
|
||||
f"{_escape(str(entry.get('status')))}</span></td>"
|
||||
f"<td>{_escape(str(entry.get('linked_open_pr') or '—'))}</td>"
|
||||
f"<td>{entry.get('heartbeat_count', 0)}</td>"
|
||||
f"<td>{_escape(', '.join(entry.get('matching_branches') or []) or '—')}</td>"
|
||||
f"<td class='muted'>{_escape('; '.join(entry.get('reasons') or []))}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
return (
|
||||
"<table class='registry leases'>"
|
||||
"<thead><tr><th>Issue</th><th>Claim status</th><th>Open PR</th>"
|
||||
"<th>Heartbeats</th><th>Branches</th><th>Notes</th></tr></thead>"
|
||||
f"<tbody>{''.join(rows)}</tbody></table>"
|
||||
)
|
||||
|
||||
|
||||
def _reviewer_leases_table(snapshot: LeaseSnapshot) -> str:
|
||||
if not snapshot.reviewer_leases:
|
||||
return (
|
||||
"<p class='muted'>No active reviewer PR lease comments found "
|
||||
"(marker <code><!-- mcp-review-lease:v1 --></code>; see #407).</p>"
|
||||
)
|
||||
rows = []
|
||||
for lease in snapshot.reviewer_leases:
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td>#{lease.get('pr_number')}</td>"
|
||||
f"<td>{_escape(str(lease.get('reviewer_identity') or '—'))}</td>"
|
||||
f"<td><code>{_escape(str(lease.get('profile') or '—'))}</code></td>"
|
||||
f"<td>{_escape(str(lease.get('phase') or '—'))}</td>"
|
||||
f"<td><code>{_escape(str(lease.get('expires_at') or '—'))}</code></td>"
|
||||
"</tr>"
|
||||
)
|
||||
return (
|
||||
"<table class='registry leases'>"
|
||||
"<thead><tr><th>PR</th><th>Reviewer</th><th>Profile</th>"
|
||||
"<th>Phase</th><th>Expires</th></tr></thead>"
|
||||
f"<tbody>{''.join(rows)}</tbody></table>"
|
||||
)
|
||||
|
||||
|
||||
def _history_links(snapshot: LeaseSnapshot) -> str:
|
||||
items = "".join(
|
||||
f"<li><a href=\"https://gitea.prgs.cc/Scaled-Tech-Consulting/Gitea-Tools/issues/"
|
||||
f"{item['number']}\">#{item['number']}</a> — {_escape(item['title'])}</li>"
|
||||
for item in snapshot.collision_history
|
||||
)
|
||||
return f"<ul>{items}</ul>"
|
||||
|
||||
|
||||
LEASE_PAGE_STYLES = """
|
||||
<style>
|
||||
.collision-warnings li { color: #f0c4c4; margin: 0.35rem 0; }
|
||||
table.leases td:nth-child(6) { font-size: 0.85rem; }
|
||||
.badge-active, .badge-awaiting_review { color: #8fd19e; border-color: #3d6b4a; }
|
||||
.badge-stale, .badge-phantom, .badge-reclaimable { color: #f0a8a8; border-color: #7a3b3b; }
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
def render_leases_page(snapshot: LeaseSnapshot) -> str:
|
||||
error_block = ""
|
||||
if snapshot.fetch_error:
|
||||
error_block = (
|
||||
'<div class="stub"><p><strong>Partial load:</strong> '
|
||||
f"{_escape(snapshot.fetch_error)}</p></div>"
|
||||
)
|
||||
body = (
|
||||
"<h2>Leases & collisions</h2>"
|
||||
"<p>Read-only visibility for issue claims, reviewer PR leases, and "
|
||||
"duplicate-work risks. Does not acquire or release leases.</p>"
|
||||
f"{error_block}"
|
||||
f"<p class='meta'>Project: <code>{_escape(snapshot.repo_label)}</code></p>"
|
||||
"<h3>Collision warnings</h3>"
|
||||
f"{_warnings_block(snapshot)}"
|
||||
"<h3>Local issue lock</h3>"
|
||||
f"{_lock_block(snapshot)}"
|
||||
"<h3>In-progress issue claims (#268)</h3>"
|
||||
f"{_claims_table(snapshot)}"
|
||||
"<h3>Reviewer PR leases (#407)</h3>"
|
||||
f"{_reviewer_leases_table(snapshot)}"
|
||||
"<h3>Collision / lease history issues</h3>"
|
||||
f"{_history_links(snapshot)}"
|
||||
"<p><a href=\"/api/leases\">JSON API</a></p>"
|
||||
f"{LEASE_PAGE_STYLES}"
|
||||
)
|
||||
return render_page(title="Leases", body_html=body)
|
||||
Reference in New Issue
Block a user