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
|
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(
|
def render_html5_editor(
|
||||||
*,
|
*,
|
||||||
project_id: str,
|
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>
|
<div class="panel-title">Authoring · {len(change_list)}</div>
|
||||||
{_authoring_changes_summary(change_list)}
|
{_authoring_changes_summary(change_list)}
|
||||||
|
{_authoring_recent_change(change_list)}
|
||||||
<div class="review-list">{body}</div>
|
<div class="review-list">{body}</div>
|
||||||
{render_html5_authoring_change_detail(project_id, None)}
|
{render_html5_authoring_change_detail(project_id, None)}
|
||||||
</div>
|
</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>'
|
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:
|
def _topbar(project_id: str, project_nav: str) -> str:
|
||||||
return f"""
|
return f"""
|
||||||
<header class="topbar" data-html5-topbar>
|
<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:
|
def _authoring_result_summary(state: str, diff: Iterable[object], checks: Iterable[object]) -> str:
|
||||||
diff_list = list(diff)
|
diff_list = list(diff)
|
||||||
check_list = list(checks)
|
check_list = list(checks)
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
from html import escape
|
||||||
|
from typing import Iterable
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from api_server.html5 import _enum_text, _metric, _page
|
||||||
|
|
||||||
|
|
||||||
|
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-сервером и обновляется SSE без 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_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 _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 ""
|
||||||
@@ -51,7 +51,6 @@ from api_server.html5 import (
|
|||||||
render_html5_metadata_preview_result,
|
render_html5_metadata_preview_result,
|
||||||
render_html5_object_context,
|
render_html5_object_context,
|
||||||
render_html5_object_report,
|
render_html5_object_report,
|
||||||
render_html5_operation_detail,
|
|
||||||
render_html5_project_setup,
|
render_html5_project_setup,
|
||||||
render_html5_project_rows,
|
render_html5_project_rows,
|
||||||
render_html5_project_report,
|
render_html5_project_report,
|
||||||
@@ -59,15 +58,18 @@ from api_server.html5 import (
|
|||||||
render_html5_symbol_detail,
|
render_html5_symbol_detail,
|
||||||
render_html5_import_check,
|
render_html5_import_check,
|
||||||
render_html5_import_job,
|
render_html5_import_job,
|
||||||
render_html5_operation_rows,
|
|
||||||
render_html5_operation_summary,
|
|
||||||
render_html5_operations,
|
|
||||||
render_html5_settings_panel,
|
render_html5_settings_panel,
|
||||||
render_html5_setup_summary,
|
render_html5_setup_summary,
|
||||||
render_html5_source,
|
render_html5_source,
|
||||||
render_html5_status,
|
render_html5_status,
|
||||||
render_html5_symbols,
|
render_html5_symbols,
|
||||||
)
|
)
|
||||||
|
from api_server.html5_operations import (
|
||||||
|
render_html5_operation_detail,
|
||||||
|
render_html5_operation_rows,
|
||||||
|
render_html5_operation_summary,
|
||||||
|
render_html5_operations,
|
||||||
|
)
|
||||||
from impact_engine import object_impact, routine_impact
|
from impact_engine import object_impact, routine_impact
|
||||||
from incremental_indexer import rebuild_changed_file
|
from incremental_indexer import rebuild_changed_file
|
||||||
from integration_topology import IntegrationKind, build_integration_topology
|
from integration_topology import IntegrationKind, build_integration_topology
|
||||||
|
|||||||
@@ -28,6 +28,20 @@ def create_authoring_session(client: TestClient, project_id: str, task_id: str,
|
|||||||
assert session.status_code == 200
|
assert session.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def assert_html5_contract(text: str, *markers: str, full_page: bool = False) -> None:
|
||||||
|
assert "__next" not in text
|
||||||
|
assert "unpkg.com" not in text
|
||||||
|
assert 'hx-trigger="every' not in text
|
||||||
|
if full_page:
|
||||||
|
assert "<!doctype html>" in text
|
||||||
|
assert "/html5/assets/htmx.min.js" in text
|
||||||
|
assert "/html5/assets/htmx-ext-sse.js" in text
|
||||||
|
else:
|
||||||
|
assert "<html" not in text
|
||||||
|
for marker in markers:
|
||||||
|
assert marker in text
|
||||||
|
|
||||||
|
|
||||||
def test_cors_allows_lan_panel_origin():
|
def test_cors_allows_lan_panel_origin():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
response = client.options(
|
response = client.options(
|
||||||
@@ -245,6 +259,51 @@ def test_html5_server_rendered_project_editor(tmp_path: Path):
|
|||||||
assert "sse" in sse_asset.text
|
assert "sse" in sse_asset.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_html5_contracts_are_server_rendered_and_stable(tmp_path: Path):
|
||||||
|
client = TestClient(app)
|
||||||
|
project_id = f"html5-contract-{uuid4()}"
|
||||||
|
module = tmp_path / "contract_module.bsl"
|
||||||
|
module.write_text(
|
||||||
|
"Процедура ПроверитьКонтракт()\n"
|
||||||
|
" Сообщить(\"contract\");\n"
|
||||||
|
"КонецПроцедуры\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
||||||
|
assert indexed.status_code == 200
|
||||||
|
settings = client.post(
|
||||||
|
f"/projects/{project_id}/settings",
|
||||||
|
json={"name": "HTML5 Contract", "structure_source": "XML_DUMP"},
|
||||||
|
)
|
||||||
|
assert settings.status_code == 200
|
||||||
|
|
||||||
|
full_pages = [
|
||||||
|
("/html5", ("data-html5-page=\"projects\"", "data-html5-projects")),
|
||||||
|
(f"/html5/projects/{project_id}/editor", ("data-html5-page=\"editor\"", "data-html5-editor")),
|
||||||
|
(f"/html5/projects/{project_id}/setup", ("data-html5-page=\"setup\"", "data-html5-setup-summary")),
|
||||||
|
("/html5/operations", ("data-html5-page=\"operations\"", "data-html5-operations-filter")),
|
||||||
|
]
|
||||||
|
for path, markers in full_pages:
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_html5_contract(response.text, *markers, full_page=True)
|
||||||
|
|
||||||
|
partials = [
|
||||||
|
(f"/html5/projects/{project_id}/symbols", {"q": "Проверить"}, ("data-html5-symbol",)),
|
||||||
|
(f"/html5/projects/{project_id}/report", {}, ("data-html5-project-report", "data-html5-project-summary")),
|
||||||
|
(f"/html5/projects/{project_id}/review", {}, ("data-html5-review", "data-html5-review-summary")),
|
||||||
|
(f"/html5/projects/{project_id}/flowchart", {}, ("data-html5-flowchart",)),
|
||||||
|
(f"/html5/projects/{project_id}/authoring/changes", {}, ("data-html5-authoring-changes", "data-html5-authoring-summary")),
|
||||||
|
(f"/html5/projects/{project_id}/setup/summary", {}, ("data-html5-setup-summary",)),
|
||||||
|
("/html5/operations/jobs", {}, ("data-html5-operation",)),
|
||||||
|
("/html5/operations/summary", {}, ("data-html5-operations-summary",)),
|
||||||
|
]
|
||||||
|
for path, params, markers in partials:
|
||||||
|
response = client.get(path, params=params)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert_html5_contract(response.text, *markers)
|
||||||
|
|
||||||
|
|
||||||
def test_html5_project_index_creates_project_with_fragment():
|
def test_html5_project_index_creates_project_with_fragment():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
project_id = f"html5-created-{uuid4()}"
|
project_id = f"html5-created-{uuid4()}"
|
||||||
@@ -2778,11 +2837,13 @@ def test_authoring_context_and_completion_preview(tmp_path: Path):
|
|||||||
assert "text/html" in html5_changes.headers["content-type"]
|
assert "text/html" in html5_changes.headers["content-type"]
|
||||||
assert "data-html5-authoring-changes" in html5_changes.text
|
assert "data-html5-authoring-changes" in html5_changes.text
|
||||||
assert "data-html5-authoring-summary" in html5_changes.text
|
assert "data-html5-authoring-summary" in html5_changes.text
|
||||||
|
assert "data-html5-authoring-recent-change" in html5_changes.text
|
||||||
assert "workspace" in html5_changes.text
|
assert "workspace" in html5_changes.text
|
||||||
assert "+3 / -0" in html5_changes.text
|
assert "+3 / -0" in html5_changes.text
|
||||||
assert "data-html5-authoring-detail" in html5_changes.text
|
assert "data-html5-authoring-detail" in html5_changes.text
|
||||||
assert 'hx-target="[data-html5-authoring-detail]"' in html5_changes.text
|
assert 'hx-target="[data-html5-authoring-detail]"' in html5_changes.text
|
||||||
assert apply_payload["change_id"] in html5_changes.text
|
assert apply_payload["change_id"] in html5_changes.text
|
||||||
|
assert apply_payload["version"]["version_id"] in html5_changes.text
|
||||||
assert "<html" not in html5_changes.text
|
assert "<html" not in html5_changes.text
|
||||||
|
|
||||||
rollback = client.get(
|
rollback = client.get(
|
||||||
|
|||||||
Reference in New Issue
Block a user