feat: add final-report audit paste tool to web UI (Closes #431)
Expose /audit and /api/audit for local validator previews using final_report_validator and review schema checks. Operators can paste reports, auto-detect task kind, and get structured findings with suggested next prompts and issue-comment drafts. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""HTML views for final-report audit paste (#431)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
|
||||
from final_report_validator import FINAL_REPORT_TASK_KINDS
|
||||
from webui.audit_validator import audit_report
|
||||
from webui.layout import render_page
|
||||
|
||||
_TASK_KIND_OPTIONS = sorted(FINAL_REPORT_TASK_KINDS)
|
||||
|
||||
|
||||
def _escape(text: str) -> str:
|
||||
return html.escape(text, quote=True)
|
||||
|
||||
|
||||
def _render_findings(result: dict) -> str:
|
||||
findings = result.get("findings") or []
|
||||
if not findings:
|
||||
return '<p class="muted">No validator findings — report structure looks complete for the selected task kind.</p>'
|
||||
rows = []
|
||||
for finding in findings:
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td><code>{_escape(str(finding.get('rule_id', '')))}</code></td>"
|
||||
f"<td>{_escape(str(finding.get('severity', '')))}</td>"
|
||||
f"<td>{_escape(str(finding.get('field', '')))}</td>"
|
||||
f"<td>{_escape(str(finding.get('reason', '')))}</td>"
|
||||
f"<td>{_escape(str(finding.get('safe_next_action', '')))}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
return (
|
||||
'<table class="detail audit-findings">'
|
||||
"<thead><tr><th>Rule</th><th>Severity</th><th>Field</th>"
|
||||
"<th>Reason</th><th>Safe next action</th></tr></thead>"
|
||||
f"<tbody>{''.join(rows)}</tbody></table>"
|
||||
)
|
||||
|
||||
|
||||
def _render_task_kind_select(selected: str | None) -> str:
|
||||
options = ['<option value="">Auto-detect from report</option>']
|
||||
for kind in _TASK_KIND_OPTIONS:
|
||||
sel = ' selected' if selected == kind else ""
|
||||
options.append(f'<option value="{_escape(kind)}"{sel}>{_escape(kind)}</option>')
|
||||
return f'<select id="task_kind" name="task_kind">{"".join(options)}</select>'
|
||||
|
||||
|
||||
def render_audit_page(
|
||||
*,
|
||||
report_text: str = "",
|
||||
task_kind: str | None = None,
|
||||
result: dict | None = None,
|
||||
) -> str:
|
||||
result_html = ""
|
||||
if result is not None:
|
||||
grade = _escape(str(result.get("grade", "")))
|
||||
inferred = _escape(str(result.get("inferred_task_kind", "")))
|
||||
safe_next = _escape(str(result.get("safe_next_action", "")))
|
||||
suggested_prompt = result.get("suggested_next_prompt") or ""
|
||||
suggested_comment = result.get("suggested_issue_comment") or ""
|
||||
result_html = (
|
||||
'<section class="audit-result">'
|
||||
f"<h3>Validator preview</h3>"
|
||||
f'<p class="meta">Task kind: <code>{inferred}</code> · '
|
||||
f'Grade: <strong>{grade}</strong> · '
|
||||
f"Blocked: <code>{result.get('blocked')}</code> · "
|
||||
f"Downgraded: <code>{result.get('downgraded')}</code></p>"
|
||||
f"<p><strong>Safe next action:</strong> {safe_next}</p>"
|
||||
f"{_render_findings(result)}"
|
||||
)
|
||||
if suggested_prompt:
|
||||
result_html += (
|
||||
"<h4>Suggested next prompt</h4>"
|
||||
f'<pre class="prompt-text">{_escape(suggested_prompt)}</pre>'
|
||||
)
|
||||
if suggested_comment:
|
||||
result_html += (
|
||||
"<h4>Suggested issue/PR comment</h4>"
|
||||
f'<pre class="prompt-text">{_escape(suggested_comment)}</pre>'
|
||||
)
|
||||
result_html += "</section>"
|
||||
|
||||
body = (
|
||||
"<h2>Report audit</h2>"
|
||||
"<p>Paste an LLM final report for local validator preview. "
|
||||
"Uses <code>final_report_validator</code> and review schema checks — "
|
||||
"does not post to Gitea or store reports server-side.</p>"
|
||||
'<form method="post" action="/audit" class="audit-form">'
|
||||
"<label for=\"task_kind\">Task kind</label>"
|
||||
f"{_render_task_kind_select(task_kind)}"
|
||||
'<label for="report_text">Final report</label>'
|
||||
f'<textarea id="report_text" name="report_text" rows="18" '
|
||||
f'placeholder="Paste final report markdown here…">{_escape(report_text)}</textarea>'
|
||||
'<button type="submit" class="copy-btn">Run validator preview</button>'
|
||||
"</form>"
|
||||
f"{result_html}"
|
||||
"<p><a href=\"/api/audit\">JSON API</a> (POST <code>report_text</code>, "
|
||||
"optional <code>task_kind</code>)</p>"
|
||||
)
|
||||
return render_page(title="Audit", body_html=body, extra_head=AUDIT_PAGE_STYLES)
|
||||
|
||||
|
||||
AUDIT_PAGE_STYLES = """
|
||||
<style>
|
||||
.audit-form label {
|
||||
display: block;
|
||||
margin: 0.75rem 0 0.35rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.audit-form select,
|
||||
.audit-form textarea {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 0.65rem;
|
||||
}
|
||||
.audit-form textarea {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
resize: vertical;
|
||||
}
|
||||
.audit-result {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
table.audit-findings td:nth-child(4),
|
||||
table.audit-findings td:nth-child(5) {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
Reference in New Issue
Block a user