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
+
+
+
+
+
+
+
+ | Job | Проект | Статус | Stage | Сообщение |
+
+ {render_html5_operation_rows(job_list)}
+
+
+
+
+ """,
+ )
+
+
+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 "