Files
Gitea-Tools/webui/worktree_views.py
T
sysadminandClaude Opus 4.8 276a71bd1a feat: add worktree hygiene dashboard to web UI (Closes #432)
Adds read-only /worktrees and /api/worktrees routes that scan branches/
directories, classify worktree state, flag missing preserved worktrees from
the issue lock, and surface the canonical cleanup prompt without deletion.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-07 15:26:22 -04:00

130 lines
4.3 KiB
Python

"""HTML views for branches/ worktree hygiene dashboard (#432)."""
from __future__ import annotations
import html
from webui.layout import render_page
from webui.worktree_scanner import HygieneSnapshot, WorktreeEntry
def _escape(text: str) -> str:
return html.escape(text, quote=True)
def _badge(classification: str) -> str:
return (
f'<span class="badge badge-{_escape(classification)}">'
f"{_escape(classification)}</span>"
)
def _entry_row(entry: WorktreeEntry) -> str:
branch = entry.branch or "—"
head = entry.head_sha[:12] if entry.head_sha else "—"
dirty = (
f"{entry.dirty_tracked} tracked"
+ (" + untracked" if entry.dirty_untracked else "")
if entry.dirty_tracked or entry.dirty_untracked
else "clean"
)
return (
"<tr>"
f"<td><code>{_escape(entry.rel_path)}</code></td>"
f"<td>{_badge(entry.classification)}</td>"
f"<td><code>{_escape(branch)}</code></td>"
f"<td><code>{_escape(head)}</code></td>"
f"<td>{_escape(dirty)}</td>"
f"<td>{'yes' if entry.detached else 'no'}</td>"
f"<td class='muted'>{_escape(entry.notes)}</td>"
"</tr>"
)
WORKTREE_PAGE_SCRIPT = """
<script>
document.querySelectorAll('.copy-cleanup-btn').forEach((btn) => {
btn.addEventListener('click', async () => {
const node = document.getElementById('cleanup-prompt');
if (!node) return;
try {
await navigator.clipboard.writeText(node.textContent || '');
const prior = btn.textContent;
btn.textContent = 'Copied';
setTimeout(() => { btn.textContent = prior; }, 1200);
} catch (_err) {
btn.textContent = 'Copy failed';
}
});
});
</script>
"""
WORKTREE_PAGE_STYLES = """
<style>
table.worktrees td:nth-child(7) { font-size: 0.85rem; }
.anomaly {
margin: 1rem 0;
padding: 0.75rem 0.9rem;
border: 1px solid #7a3b3b;
border-radius: 8px;
background: rgba(122, 59, 59, 0.15);
color: #f0c4c4;
}
.badge-active-pr { color: #9ec8f0; border-color: #3d5f7a; }
.badge-active-issue { color: #8fd19e; border-color: #3d6b4a; }
.badge-dirty { color: #e0c27a; border-color: #6b5730; }
.badge-stale-clean { color: #c9b8e8; border-color: #5a4a78; }
.badge-detached-review { color: #9ec8f0; border-color: #3d5f7a; }
.badge-unsafe-unknown { color: #f0a8a8; border-color: #7a3b3b; }
.badge-orphan { color: #f0a8a8; border-color: #7a3b3b; }
</style>
"""
def render_worktrees_page(snapshot: HygieneSnapshot) -> str:
error_block = ""
if snapshot.scan_error:
error_block = (
'<div class="stub"><p><strong>Partial scan:</strong> '
f"{_escape(snapshot.scan_error)}</p></div>"
)
anomaly_block = ""
if snapshot.anomalies:
items = "".join(f"<li>{_escape(text)}</li>" for text in snapshot.anomalies)
anomaly_block = (
'<section class="anomaly"><h3>Missing preserved worktree anomalies</h3>'
f"<ul>{items}</ul></section>"
)
if snapshot.entries:
rows = "".join(_entry_row(entry) for entry in snapshot.entries)
table = (
"<table class='registry worktrees'>"
"<thead><tr>"
"<th>Path</th><th>Class</th><th>Branch</th><th>HEAD</th>"
"<th>Dirty</th><th>Detached</th><th>Notes</th>"
"</tr></thead>"
f"<tbody>{rows}</tbody></table>"
)
else:
table = "<p class='muted'>No directories under <code>branches/</code>.</p>"
body = (
"<h2>Worktree hygiene</h2>"
"<p>Read-only scan of local <code>branches/</code> worktrees. "
"No deletion actions — copy the canonical cleanup prompt when merge is confirmed.</p>"
f"{error_block}"
f"{anomaly_block}"
f"<p class='meta'>Repo root: <code>{_escape(snapshot.repo_root)}</code> · "
f"entries: {len(snapshot.entries)}</p>"
f"{table}"
"<h3>Canonical cleanup prompt</h3>"
f'<pre class="prompt-text" id="cleanup-prompt">{_escape(snapshot.cleanup_prompt)}</pre>'
'<button type="button" class="copy-btn copy-cleanup-btn">Copy cleanup prompt</button>'
"<p><a href=\"/api/worktrees\">JSON API</a></p>"
f"{WORKTREE_PAGE_STYLES}"
f"{WORKTREE_PAGE_SCRIPT}"
)
return render_page(title="Worktrees", body_html=body)