Add HTML5 operations monitor
This commit is contained in:
@@ -70,6 +70,54 @@ def render_html5_project_rows(projects: Iterable[object]) -> str:
|
|||||||
return project_rows
|
return project_rows
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_operations(jobs: Iterable[object]) -> str:
|
||||||
|
job_list = list(jobs)
|
||||||
|
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">
|
||||||
|
<div class="section-title">
|
||||||
|
<h2>Очередь</h2>
|
||||||
|
<a class="button" href="/html5">Проекты</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table data-html5-operations>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Job</th><th>Проект</th><th>Статус</th><th>Stage</th><th>Сообщение</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
data-html5-operations-body
|
||||||
|
hx-get="/html5/operations/jobs"
|
||||||
|
hx-trigger="every 3s"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>{render_html5_operation_rows(job_list)}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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="5" class="muted">Фоновые операции пока не запускались</td></tr>'
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def render_html5_editor(
|
def render_html5_editor(
|
||||||
*,
|
*,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
@@ -474,6 +522,29 @@ 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>
|
||||||
|
</tr>"""
|
||||||
|
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ from api_server.html5 import (
|
|||||||
render_html5_project_rows,
|
render_html5_project_rows,
|
||||||
render_html5_import_check,
|
render_html5_import_check,
|
||||||
render_html5_import_job,
|
render_html5_import_job,
|
||||||
|
render_html5_operation_rows,
|
||||||
|
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,
|
||||||
@@ -1612,6 +1614,22 @@ async def html5_delete_project(project_id: str, request: Request) -> Response:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/html5/operations")
|
||||||
|
async def html5_operations() -> Response:
|
||||||
|
return Response(
|
||||||
|
render_html5_operations(_html5_operation_jobs()),
|
||||||
|
media_type="text/html; charset=utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/html5/operations/jobs")
|
||||||
|
async def html5_operation_jobs() -> Response:
|
||||||
|
return Response(
|
||||||
|
render_html5_operation_rows(_html5_operation_jobs()),
|
||||||
|
media_type="text/html; charset=utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/html5/projects/{project_id}/editor")
|
@app.get("/html5/projects/{project_id}/editor")
|
||||||
async def html5_project_editor(project_id: str, q: str = "") -> Response:
|
async def html5_project_editor(project_id: str, q: str = "") -> Response:
|
||||||
try:
|
try:
|
||||||
@@ -7837,6 +7855,10 @@ def _current_import_source(project_id: str) -> ImportSourceKind:
|
|||||||
return ImportSourceKind.XML_DUMP
|
return ImportSourceKind.XML_DUMP
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_operation_jobs() -> list[OperationJob]:
|
||||||
|
return sorted(_operations.jobs.values(), key=lambda job: job.updated_at, reverse=True)[:50]
|
||||||
|
|
||||||
|
|
||||||
def _project_summaries() -> list[ProjectSummaryResponse]:
|
def _project_summaries() -> list[ProjectSummaryResponse]:
|
||||||
project_ids = set(_project_setup.keys())
|
project_ids = set(_project_setup.keys())
|
||||||
stored_snapshots = _storage.list_snapshot_refs()
|
stored_snapshots = _storage.list_snapshot_refs()
|
||||||
|
|||||||
@@ -287,6 +287,35 @@ def test_html5_project_setup_renders_server_fragments():
|
|||||||
assert "<html" not in summary.text
|
assert "<html" not in summary.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_html5_operations_renders_job_monitor_fragments():
|
||||||
|
client = TestClient(app)
|
||||||
|
project_id = f"html5-ops-{uuid4()}"
|
||||||
|
saved = client.post(
|
||||||
|
f"/projects/{project_id}/settings",
|
||||||
|
json={"name": "HTML5 Ops", "structure_source": "XML_DUMP"},
|
||||||
|
)
|
||||||
|
assert saved.status_code == 200
|
||||||
|
|
||||||
|
job = client.post(f"/html5/projects/{project_id}/setup/import-job")
|
||||||
|
assert job.status_code == 200
|
||||||
|
|
||||||
|
page = client.get("/html5/operations")
|
||||||
|
assert page.status_code == 200
|
||||||
|
assert "text/html" in page.headers["content-type"]
|
||||||
|
assert 'data-html5-page="operations"' in page.text
|
||||||
|
assert "data-html5-operations-body" in page.text
|
||||||
|
assert 'hx-get="/html5/operations/jobs"' in page.text
|
||||||
|
assert project_id in page.text
|
||||||
|
assert "__next" not in page.text
|
||||||
|
|
||||||
|
rows = client.get("/html5/operations/jobs")
|
||||||
|
assert rows.status_code == 200
|
||||||
|
assert "text/html" in rows.headers["content-type"]
|
||||||
|
assert "data-html5-operation" in rows.text
|
||||||
|
assert project_id in rows.text
|
||||||
|
assert "<html" not in rows.text
|
||||||
|
|
||||||
|
|
||||||
def test_project_setup_mock_import_indexes_project():
|
def test_project_setup_mock_import_indexes_project():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
project_id = f"setup-import-{uuid4()}"
|
project_id = f"setup-import-{uuid4()}"
|
||||||
|
|||||||
Reference in New Issue
Block a user