"""HTML views for the Gitea issue↔PR linkage console (#645, Phase 3). Read-only renderer over :mod:`webui.linkage_loader`. The page's job is to make three things impossible to misread: * **why** an edge exists — every link carries its evidence badge, so a bare ``#N`` mention never looks like a closing claim; * **what was not loaded** — a partial inventory, an unfocused thread, or an unavailable handoff source renders as an explicit qualifier, never as an affirmative "none"; * **that nothing here mutates** — there is no review, merge, or edit control, and the deep link out to Gitea appears only under the admin reveal opt-in. """ from __future__ import annotations from html import escape from typing import Sequence from webui.layout import render_page from webui.linkage_loader import ( EVIDENCE_BRANCH, EVIDENCE_CLOSES, EVIDENCE_DESCRIPTIONS, EVIDENCE_ORDER, EVIDENCE_REFERENCE, HANDOFF_LOADED, HANDOFF_NOT_LOADED, HandoffSummary, LinkageNode, LinkageSnapshot, ) _EVIDENCE_CSS = { EVIDENCE_CLOSES: "badge-health-ok", EVIDENCE_BRANCH: "badge-health-skipped", EVIDENCE_REFERENCE: "badge-health-unproven", } _EVIDENCE_LABEL = { EVIDENCE_CLOSES: "closes", EVIDENCE_BRANCH: "branch", EVIDENCE_REFERENCE: "mention", } def _badge(text: str, css: str) -> str: return f'{escape(text)}' def _labels(names: Sequence[str]) -> str: if not names: return '' return " ".join(_badge(name, "badge-health-skipped") for name in names) def _ref(node: LinkageNode) -> str: """Render an item reference, hyperlinked only when deep links are revealed.""" label = f"#{node.number}" if node.deep_link: return f'{escape(label)}' return f"{escape(label)}" def _scope_card(snapshot: LinkageSnapshot) -> str: focus = ( "none" if snapshot.focus is None else f"{snapshot.focus[0]}#{snapshot.focus[1]}" ) completeness = ( _badge("complete", "badge-health-ok") if snapshot.inventory_complete else _badge("partial", "badge-health-degraded") ) links_note = ( "Every linkage edge below is a claim about this loaded window only." if snapshot.inventory_complete else ( "Pagination did not complete for this window, so an empty link list " "means none found in what was loaded — not that no link exists." ) ) deep_links = ( _badge("enabled", "badge-health-ok") if snapshot.deep_links_enabled else _badge("hidden", "badge-health-skipped") ) return f"""

Scope

Project{escape(snapshot.project_id or "—")}
Repository{escape(snapshot.repo_label or "—")}
Item state{escape(snapshot.state_scope)}
Focused thread{escape(focus)}
Inventory{completeness}
Gitea deep links{deep_links}

{links_note}

""" def _error_card(snapshot: LinkageSnapshot) -> str: if snapshot.ok and not snapshot.fetch_error: return "" return ( '
Linkage unavailable: ' f"{escape(snapshot.fetch_error or 'the linkage read did not complete')}. " "No linkage table is rendered: an empty table would read as " "no issue is linked to any PR, which this read cannot claim." "
" ) def _contested_card(snapshot: LinkageSnapshot) -> str: if not snapshot.contested_issues: return "" refs = ", ".join(f"#{number}" for number in snapshot.contested_issues) return ( '
' f"Contested issues: {refs}. More than one PR in this " "window claims each of these — duplicate work or a superseded PR. " "Resolution stays in Gitea and the workflow; this console only reports it." "
" ) def _evidence_badges(evidence: Sequence[str]) -> str: return " ".join( _badge( _EVIDENCE_LABEL.get(name, name), _EVIDENCE_CSS.get(name, "badge-health-skipped"), ) for name in EVIDENCE_ORDER if name in evidence ) def _handoff_inline(handoff: HandoffSummary) -> str: type_css = "badge-health-ok" if handoff.cth_type_known else "badge-health-degraded" return ( f'{_badge(handoff.cth_type or "—", type_css)}' f'
' f'{escape(handoff.status or "—")} → {escape(handoff.next_owner or "—")}
' ) def _handoff_cell(node: LinkageNode) -> str: """Render the handoff column, distinguishing 'none found' from 'not loaded'.""" status = node.handoff_status if status.state == HANDOFF_LOADED: if node.handoff is None: return 'no canonical handoff on this thread' return _handoff_inline(node.handoff) if status.state == HANDOFF_NOT_LOADED: return ( f'{_badge("not loaded", "badge-health-skipped")}' f'
' f'{escape(status.reason or "")}
' ) return ( f'{_badge("unavailable", "badge-health-degraded")}' f'
' f'{escape(status.reason or "")}
' ) def _issue_rows(snapshot: LinkageSnapshot) -> str: rows = [] for node in snapshot.issues: if node.linked_prs: linked = ", ".join(f"#{number}" for number in node.linked_prs) if node.contested: linked += " " + _badge("contested", "badge-blocked") elif node.links_authoritative: linked = 'none' else: # The distinction an operator needs: nothing found in a window that # was not fully loaded is not the same as nothing existing. linked = 'none found (partial inventory)' rows.append( "" f"{_ref(node)}" f"{escape(node.title)}" f"{escape(node.state or '—')}" f"{_labels(node.labels)}" f"{linked}" f"{_handoff_cell(node)}" "" ) if not rows: return 'No issues in the loaded window.' return "".join(rows) def _pr_rows(snapshot: LinkageSnapshot) -> str: rows = [] for node in snapshot.prs: if node.links: linked = "".join( f"
#{link.issue_number} " f"{_evidence_badges(link.evidence)}
" for link in node.links ) if node.ambiguous: linked += _badge("ambiguous", "badge-blocked") if node.contested: linked += " " + _badge("contested", "badge-blocked") elif node.links_authoritative: linked = 'no issue reference' else: linked = 'none found (partial inventory)' rows.append( "" f"{_ref(node)}" f"{escape(node.title)}" f"{escape(node.state or '—')}" f"{_labels(node.labels)}" f"{linked}" f"{_handoff_cell(node)}" "" ) if not rows: return ( 'No pull requests in the loaded ' "window." ) return "".join(rows) def _focus_card(snapshot: LinkageSnapshot) -> str: """Render the focused thread's latest canonical handoff, when one was loaded.""" if snapshot.focus is None: return f"""

Canonical handoff

{escape(snapshot.handoff_status.reason or "")} Add ?issue=N or ?pr=N to load the latest Canonical Thread Handoff for one thread.

""" kind, number = snapshot.focus target = f"{kind} #{number}" if not snapshot.handoff_status.loaded: return f"""

Canonical handoff — {escape(target)}

{_badge("unavailable", "badge-health-degraded")} {escape(snapshot.handoff_status.reason or "handoff source did not run")}. This is not evidence that the thread carries no handoff.

""" handoff = next( ( node.handoff for node in (snapshot.issues + snapshot.prs) if node.kind == kind and node.number == number and node.handoff ), None, ) if handoff is None: return f"""

Canonical handoff — {escape(target)}

Comments loaded; no Canonical Thread Handoff comment found on this thread.

""" type_css = "badge-health-ok" if handoff.cth_type_known else "badge-health-degraded" unknown_note = ( "" if handoff.cth_type_known else ( '

The comment\'s heading is not a declared CTH type, ' "so it is reported as unrecognized rather than republished.

" ) ) return f"""

Canonical handoff — {escape(target)}

{_badge(handoff.cth_type or "—", type_css)} by {escape(handoff.author or "unknown")} at {escape(handoff.created_at or "unknown")}

{unknown_note}
Status{escape(handoff.status or "—")}
Next owner{escape(handoff.next_owner or "—")}
Current blocker{escape(handoff.current_blocker or "—")}
Decision{escape(handoff.decision or "—")}
Next action{escape(handoff.next_action or "—")}

Full event history: /api/v1/timeline

""" def _legend_card(snapshot: LinkageSnapshot) -> str: items = "".join( f"
  • {_badge(_EVIDENCE_LABEL[name], _EVIDENCE_CSS[name])} — " f"{escape(EVIDENCE_DESCRIPTIONS[name])}
  • " for name in EVIDENCE_ORDER ) reveal_note = ( "Gitea deep links are shown because the " "GITEA_MCP_REVEAL_ENDPOINTS admin opt-in is set." if snapshot.deep_links_enabled else ( "Gitea deep links are withheld. Set " "GITEA_MCP_REVEAL_ENDPOINTS=1 server-side to reveal " "them; item numbers stay usable without them." ) ) return f"""

    How an edge was found

    {reveal_note}

    Read-only surface: no issue or PR editing, no review, and no merge. JSON export: /api/v1/gitea/linkage

    """ def render_linkage_page(snapshot: LinkageSnapshot) -> str: """Render the full HTML page for the Gitea linkage console.""" if not snapshot.ok: return render_page( title="Gitea linkage", body_html=f"""

    Gitea issue and PR linkage

    Phase 3 read-only linkage console (#645).

    {_error_card(snapshot)} {_scope_card(snapshot)}""", ) body = f"""

    Gitea issue and PR linkage

    Phase 3 read-only linkage console (#645). Gitea remains the source of truth; this page reads it and never writes to it.

    {_scope_card(snapshot)} {_contested_card(snapshot)}

    Issues → pull requests

    {_issue_rows(snapshot)}
    IssueTitleStateLabels Linked PRsLatest handoff

    Pull requests → issues

    {_pr_rows(snapshot)}
    PRTitleStateLabels Linked issuesLatest handoff
    {_focus_card(snapshot)} {_legend_card(snapshot)} """ return render_page(title="Gitea linkage", body_html=body)