| """ |
| delivery.decisions.render β render a DecisionQueue to Markdown / HTML. |
| |
| Triage-focused: the table foregrounds urgency / status / owner so an |
| operations owner can scan the queue and decide what to pick up next. |
| """ |
| from __future__ import annotations |
|
|
| from ..evidence.render import _esc, _fmt_pct, _fmt_score |
| from .queue import STATUS_ORDER, DecisionQueue, DecisionQueueItem |
|
|
|
|
| def _status_summary_line(queue: DecisionQueue) -> str: |
| parts = [f"{queue.status_counts.get(s, 0)} {s}" for s in STATUS_ORDER] |
| return ", ".join(parts) |
|
|
|
|
| |
|
|
| def _filter_phrase(queue: DecisionQueue) -> str: |
| if queue.status_filter is None: |
| return "all statuses" |
| return f"status = {queue.status_filter}" |
|
|
|
|
| def render_markdown(queue: DecisionQueue) -> str: |
| lines = [ |
| f"# Decision Queue β `{queue.tenant_id}`", |
| "", |
| f"- **Filter:** {_filter_phrase(queue)}", |
| f"- **Counts:** {_status_summary_line(queue)}", |
| f"- **Returned:** {len(queue.items)}", |
| "", |
| ] |
| if not queue.items: |
| lines += [ |
| "No decisions match the current filter.", |
| "", |
| ] |
| return "\n".join(lines).rstrip() + "\n" |
| lines += [ |
| "| Urgency | Status | Entity | Recommendation | Owner | Confidence | Run | Updated |", |
| "|---:|---|---|---|---|---:|---|---|", |
| ] |
| for it in queue.items: |
| lines.append( |
| f"| {_fmt_score(it.urgency)} " |
| f"| {it.status} " |
| f"| `{it.entity_id}` ({it.entity_type}) " |
| f"| {it.recommendation} " |
| f"| {it.owner or 'β'} " |
| f"| {_fmt_pct(it.confidence)} " |
| f"| `{it.run_id}` " |
| f"| {it.updated_at or 'β'} |" |
| ) |
| lines.append("") |
| return "\n".join(lines).rstrip() + "\n" |
|
|
|
|
| |
|
|
| _HTML_HEAD = ( |
| "<!doctype html><html><head><meta charset=\"utf-8\">" |
| "<title>Decision Queue</title>" |
| "<style>" |
| "body{font-family:-apple-system,Segoe UI,Arial,sans-serif;" |
| "max-width:1100px;margin:32px auto;padding:0 16px;line-height:1.5;color:#222}" |
| "h1{font-size:1.6em;margin-bottom:0.2em}" |
| "table{border-collapse:collapse;width:100%;margin:0.6em 0}" |
| "th,td{border:1px solid #ddd;padding:6px 8px;text-align:left;font-size:0.94em}" |
| "th{background:#f4f4f4}" |
| ".status{display:inline-block;padding:1px 8px;border-radius:3px;" |
| "font-size:0.85em;font-weight:600;color:#fff}" |
| ".status-open{background:#a00}" |
| ".status-in_progress{background:#d35400}" |
| ".status-resolved{background:#2c8a3e}" |
| ".status-dismissed{background:#777}" |
| ".meta{color:#555;font-size:0.93em}" |
| ".counts{margin-bottom:1em}" |
| ".counts span{margin-right:12px}" |
| "code{background:#f4f4f4;padding:1px 4px;border-radius:3px}" |
| "</style></head><body>" |
| ) |
| _HTML_FOOT = "</body></html>" |
|
|
|
|
| def _status_badge(status: str) -> str: |
| cls = status if status in STATUS_ORDER else "open" |
| return f'<span class="status status-{_esc(cls)}">{_esc(status)}</span>' |
|
|
|
|
| def _counts_html(queue: DecisionQueue) -> str: |
| spans = [ |
| f"<span><strong>{queue.status_counts.get(s, 0)}</strong> {_esc(s)}</span>" |
| for s in STATUS_ORDER |
| ] |
| return '<p class="counts">' + "".join(spans) + "</p>" |
|
|
|
|
| def _row_html(it: DecisionQueueItem) -> str: |
| return ( |
| "<tr>" |
| f"<td>{_esc(_fmt_score(it.urgency))}</td>" |
| f"<td>{_status_badge(it.status)}</td>" |
| f"<td><code>{_esc(it.entity_id)}</code> " |
| f'<span class="meta">({_esc(it.entity_type)})</span></td>' |
| f"<td>{_esc(it.recommendation)}</td>" |
| f"<td>{_esc(it.owner or 'β')}</td>" |
| f"<td>{_esc(_fmt_pct(it.confidence))}</td>" |
| f"<td><code>{_esc(it.run_id)}</code></td>" |
| f"<td>{_esc(it.updated_at or 'β')}</td>" |
| "</tr>" |
| ) |
|
|
|
|
| def render_html(queue: DecisionQueue) -> str: |
| parts = [ |
| _HTML_HEAD, |
| f"<h1>Decision Queue — <code>{_esc(queue.tenant_id)}</code></h1>", |
| '<p class="meta">' |
| f"Filter: {_esc(_filter_phrase(queue))} · " |
| f"returned {len(queue.items)}" |
| "</p>", |
| _counts_html(queue), |
| ] |
| if not queue.items: |
| parts.append("<p>No decisions match the current filter.</p>") |
| else: |
| parts.append( |
| "<table><thead><tr>" |
| "<th>Urgency</th><th>Status</th><th>Entity</th>" |
| "<th>Recommendation</th><th>Owner</th><th>Confidence</th>" |
| "<th>Run</th><th>Updated</th>" |
| "</tr></thead><tbody>" |
| ) |
| for it in queue.items: |
| parts.append(_row_html(it)) |
| parts.append("</tbody></table>") |
| parts.append(_HTML_FOOT) |
| return "".join(parts) |
|
|