From 84303f208b2c5df3fc91c11c89740b48efeccede Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 16 May 2026 21:53:56 +0300 Subject: [PATCH] Add HTML5 operations monitor --- services/api-server/src/api_server/html5.py | 71 +++++++++++++++++++++ services/api-server/src/api_server/main.py | 22 +++++++ services/api-server/tests/test_api.py | 29 +++++++++ 3 files changed, 122 insertions(+) diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py index 804d589..ec284cc 100644 --- a/services/api-server/src/api_server/html5.py +++ b/services/api-server/src/api_server/html5.py @@ -70,6 +70,54 @@ def render_html5_project_rows(projects: Iterable[object]) -> str: return project_rows +def render_html5_operations(jobs: Iterable[object]) -> str: + job_list = list(jobs) + return _page( + "SFERA HTML5 operations", + f""" +
+
+
+

SFERA HTML5

+

Операции сервера

+

Очередь фоновых задач отрисовывается API-сервером и обновляется htmx polling без React runtime.

+
+
+ {len(job_list)} + jobs +
+
+
+
+

Очередь

+ Проекты +
+
+ + + + + {render_html5_operation_rows(job_list)} +
JobПроектСтатусStageСообщение
+
+
+
+ """, + ) + + +def render_html5_operation_rows(jobs: Iterable[object]) -> str: + rows = "\n".join(_operation_row(job) for job in jobs) + if not rows: + return 'Фоновые операции пока не запускались' + return rows + + def render_html5_editor( *, project_id: str, @@ -474,6 +522,29 @@ def _project_link(project: object, active_project_id: str) -> str: return f'{escape(name)}' +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'{escape(project_id)}' + if project_id + else '-' + ) + return f""" + + {escape(kind)}{escape(job_id)} + {project_link} + {escape(status)} + {escape(stage or "-")} + {escape(message or "-")} + """ + + def _topbar(project_id: str, project_nav: str) -> str: return f"""
diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index b308df0..c2e8dbd 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -42,6 +42,8 @@ from api_server.html5 import ( render_html5_project_rows, render_html5_import_check, render_html5_import_job, + render_html5_operation_rows, + render_html5_operations, render_html5_settings_panel, render_html5_setup_summary, 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") async def html5_project_editor(project_id: str, q: str = "") -> Response: try: @@ -7837,6 +7855,10 @@ def _current_import_source(project_id: str) -> ImportSourceKind: 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]: project_ids = set(_project_setup.keys()) stored_snapshots = _storage.list_snapshot_refs() diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index 3ec9c6d..218796e 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -287,6 +287,35 @@ def test_html5_project_setup_renders_server_fragments(): assert "