Add HTML5 contracts and operations renderer split
This commit is contained in:
@@ -95,145 +95,6 @@ def render_html5_project_rows(projects: Iterable[object]) -> str:
|
||||
return project_rows
|
||||
|
||||
|
||||
def render_html5_operations(
|
||||
jobs: Iterable[object],
|
||||
*,
|
||||
project_id: str = "",
|
||||
status: str = "",
|
||||
kind: str = "",
|
||||
) -> str:
|
||||
job_list = list(jobs)
|
||||
filter_query = _operation_filter_query(project_id=project_id, status=status, kind=kind)
|
||||
return _page(
|
||||
"SFERA HTML5 operations",
|
||||
f"""
|
||||
<main class="shell" data-html5-page="operations">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<p class="eyebrow">SFERA HTML5</p>
|
||||
<h1>Операции сервера</h1>
|
||||
<p class="lead">Очередь фоновых задач отрисовывается API-сервером и обновляется htmx polling без React runtime.</p>
|
||||
</div>
|
||||
<div class="hero-metrics">
|
||||
<strong>{len(job_list)}</strong>
|
||||
<span>jobs</span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="band" hx-ext="sse" sse-connect="/html5/operations/events{filter_query}">
|
||||
<div class="section-title">
|
||||
<h2>Очередь</h2>
|
||||
<a class="button" href="/html5">Проекты</a>
|
||||
</div>
|
||||
{_operation_filter_form(project_id=project_id, status=status, kind=kind)}
|
||||
<div data-html5-operations-summary-stream sse-swap="operations-summary" hx-swap="innerHTML">
|
||||
{render_html5_operation_summary(job_list)}
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table data-html5-operations>
|
||||
<thead>
|
||||
<tr><th>Job</th><th>Проект</th><th>Статус</th><th>Stage</th><th>Сообщение</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody
|
||||
data-html5-operations-body
|
||||
sse-swap="operations-jobs"
|
||||
hx-swap="innerHTML"
|
||||
>{render_html5_operation_rows(job_list)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{render_html5_operation_detail(None)}
|
||||
</section>
|
||||
</main>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def render_html5_operation_summary(jobs: Iterable[object]) -> str:
|
||||
job_list = list(jobs)
|
||||
counts = Counter(_enum_text(getattr(job, "status", "unknown")) for job in job_list)
|
||||
running = counts.get("RUNNING", 0)
|
||||
queued = counts.get("QUEUED", 0)
|
||||
succeeded = counts.get("SUCCEEDED", 0)
|
||||
failed = counts.get("FAILED", 0)
|
||||
return f"""
|
||||
<div
|
||||
class="ops-summary"
|
||||
data-html5-operations-summary
|
||||
>
|
||||
{_metric("Всего", len(job_list))}
|
||||
{_metric("В работе", running)}
|
||||
{_metric("В очереди", queued)}
|
||||
{_metric("Успешно", succeeded)}
|
||||
{_metric("Ошибки", failed)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def render_html5_operation_rows(jobs: Iterable[object]) -> str:
|
||||
rows = "\n".join(_operation_row(job) for job in jobs)
|
||||
if not rows:
|
||||
return '<tr><td colspan="6" class="muted">Фоновые операции пока не запускались</td></tr>'
|
||||
return rows
|
||||
|
||||
|
||||
def render_html5_operation_detail(job: object | None) -> str:
|
||||
if job is None:
|
||||
return """
|
||||
<div class="operation-detail" data-html5-operation-detail>
|
||||
<div class="panel-title flush">Детали операции</div>
|
||||
<p class="muted padded">Выберите job в таблице, чтобы сервер отрисовал payload, result и логи.</p>
|
||||
</div>
|
||||
"""
|
||||
job_id = str(getattr(job, "job_id", ""))
|
||||
kind = str(getattr(job, "kind", ""))
|
||||
status = _enum_text(getattr(job, "status", ""))
|
||||
payload = getattr(job, "payload", {}) or {}
|
||||
result = getattr(job, "result", {}) or {}
|
||||
error = str(getattr(job, "error", "") or "")
|
||||
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
|
||||
return f"""
|
||||
<div class="operation-detail" data-html5-operation-detail data-html5-operation-detail-id="{escape(job_id)}">
|
||||
<div class="panel-title flush">Детали операции</div>
|
||||
<article class="setup-detail">
|
||||
<strong>{escape(kind)} · {escape(status)}</strong>
|
||||
<span>{escape(job_id)}</span>
|
||||
<small>{escape(error or "no error")}</small>
|
||||
</article>
|
||||
<div class="report-grid">
|
||||
{_metric("Payload keys", len(payload))}
|
||||
{_metric("Result keys", len(result))}
|
||||
{_metric("Logs", len(logs))}
|
||||
{_metric("Updated", str(getattr(job, "updated_at", ""))[:19])}
|
||||
</div>
|
||||
<pre class="code">{escape(_compact_mapping(payload))}</pre>
|
||||
<pre class="code">{escape(_compact_mapping(result))}</pre>
|
||||
<ul class="job-log">{"".join(f"<li>{escape(str(item))}</li>" for item in logs[-8:]) or "<li>Лог пока пустой</li>"}</ul>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _operation_filter_form(*, project_id: str, status: str, kind: str) -> str:
|
||||
return f"""
|
||||
<form class="ops-filter" data-html5-operations-filter method="get" action="/html5/operations">
|
||||
<input name="project_id" value="{escape(project_id)}" placeholder="project_id" />
|
||||
<input name="status" value="{escape(status)}" placeholder="status" />
|
||||
<input name="kind" value="{escape(kind)}" placeholder="kind" />
|
||||
<button type="submit">Фильтр</button>
|
||||
<a class="button" href="/html5/operations">Сброс</a>
|
||||
</form>
|
||||
"""
|
||||
|
||||
|
||||
def _operation_filter_query(*, project_id: str, status: str, kind: str) -> str:
|
||||
params = []
|
||||
if project_id:
|
||||
params.append(f"project_id={quote(project_id)}")
|
||||
if status:
|
||||
params.append(f"status={quote(status)}")
|
||||
if kind:
|
||||
params.append(f"kind={quote(kind)}")
|
||||
return f"?{'&'.join(params)}" if params else ""
|
||||
|
||||
|
||||
def render_html5_editor(
|
||||
*,
|
||||
project_id: str,
|
||||
@@ -749,6 +610,7 @@ def render_html5_authoring_changes(project_id: str, changes: Iterable[object] |
|
||||
>
|
||||
<div class="panel-title">Authoring · {len(change_list)}</div>
|
||||
{_authoring_changes_summary(change_list)}
|
||||
{_authoring_recent_change(change_list)}
|
||||
<div class="review-list">{body}</div>
|
||||
{render_html5_authoring_change_detail(project_id, None)}
|
||||
</div>
|
||||
@@ -1500,44 +1362,6 @@ def _project_link(project: object, active_project_id: str) -> str:
|
||||
return f'<a class="project-link{active}" href="/html5/projects/{quote(project_id)}/editor">{escape(name)}</a>'
|
||||
|
||||
|
||||
def _operation_row(job: object) -> str:
|
||||
job_id = str(getattr(job, "job_id", ""))
|
||||
kind = str(getattr(job, "kind", ""))
|
||||
status = _enum_text(getattr(job, "status", ""))
|
||||
payload = getattr(job, "payload", {}) or {}
|
||||
project_id = str(payload.get("project_id") or "")
|
||||
stage = str(payload.get("stage") or "")
|
||||
message = str(payload.get("message") or getattr(job, "error", "") or "")
|
||||
project_link = (
|
||||
f'<a href="/html5/projects/{quote(project_id)}/setup">{escape(project_id)}</a>'
|
||||
if project_id
|
||||
else '<span class="muted">-</span>'
|
||||
)
|
||||
return f"""
|
||||
<tr data-html5-operation="{escape(job_id)}">
|
||||
<td><strong>{escape(kind)}</strong><small>{escape(job_id)}</small></td>
|
||||
<td>{project_link}</td>
|
||||
<td>{escape(status)}</td>
|
||||
<td>{escape(stage or "-")}</td>
|
||||
<td>{escape(message or "-")}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
hx-get="/html5/operations/jobs/{quote(job_id)}/detail"
|
||||
hx-target="[data-html5-operation-detail]"
|
||||
hx-swap="outerHTML"
|
||||
>Открыть</button>
|
||||
</td>
|
||||
</tr>"""
|
||||
|
||||
|
||||
def _compact_mapping(value: dict) -> str:
|
||||
if not value:
|
||||
return "{}"
|
||||
rows = [f"{key}: {value[key]}" for key in sorted(value)[:20]]
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def _topbar(project_id: str, project_nav: str) -> str:
|
||||
return f"""
|
||||
<header class="topbar" data-html5-topbar>
|
||||
@@ -1821,6 +1645,27 @@ def _authoring_changes_summary(changes: Iterable[object]) -> str:
|
||||
"""
|
||||
|
||||
|
||||
def _authoring_recent_change(changes: Iterable[object]) -> str:
|
||||
change_list = list(changes)
|
||||
if not change_list:
|
||||
return ""
|
||||
latest = change_list[0]
|
||||
change_id = str(getattr(latest, "change_id", ""))
|
||||
status = str(getattr(latest, "status", "") or "UNKNOWN")
|
||||
target = getattr(latest, "target", None)
|
||||
target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
|
||||
version = getattr(latest, "version", None)
|
||||
version_id = str(getattr(latest, "version_id", "") or getattr(version, "version_id", "") or "version unavailable")
|
||||
approved_by = str(getattr(latest, "approved_by", "") or "not approved")
|
||||
return f"""
|
||||
<article class="authoring-change" data-html5-authoring-recent-change="{escape(change_id)}">
|
||||
<strong>{escape(status)} · {escape(str(target_name))}</strong>
|
||||
<span>{escape(version_id)}</span>
|
||||
<small>{escape(approved_by)} · {escape(change_id)}</small>
|
||||
</article>
|
||||
"""
|
||||
|
||||
|
||||
def _authoring_result_summary(state: str, diff: Iterable[object], checks: Iterable[object]) -> str:
|
||||
diff_list = list(diff)
|
||||
check_list = list(checks)
|
||||
|
||||
Reference in New Issue
Block a user