feat: add lease and collision visibility to web UI (Closes #433)
Adds read-only /leases and /api/leases routes for issue claim inventory, reviewer PR lease comments, duplicate PR/branch warnings, and local issue lock visibility without acquire/release mutations. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -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