From de7248db9ef9508b61e240b812b33a22c891d093 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 17 May 2026 02:35:06 +0300 Subject: [PATCH] Add HTML5 operations SSE updates --- services/api-server/src/api_server/html5.py | 12 +++++----- services/api-server/src/api_server/main.py | 25 ++++++++++++++++++++- services/api-server/tests/test_api.py | 15 +++++++++++-- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py index adbe2fe..6d59de9 100644 --- a/services/api-server/src/api_server/html5.py +++ b/services/api-server/src/api_server/html5.py @@ -112,12 +112,14 @@ def render_html5_operations(jobs: Iterable[object]) -> str: jobs -
+

Очередь

Проекты
- {render_html5_operation_summary(job_list)} +
+ {render_html5_operation_summary(job_list)} +
@@ -125,8 +127,7 @@ def render_html5_operations(jobs: Iterable[object]) -> str: {render_html5_operation_rows(job_list)}
@@ -148,9 +149,6 @@ def render_html5_operation_summary(jobs: Iterable[object]) -> str:
{_metric("Всего", len(job_list))} {_metric("В работе", running)} diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index e98e2c3..f204127 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -1653,6 +1653,24 @@ async def html5_operation_summary() -> Response: ) +@app.get("/html5/operations/events") +async def html5_operations_events(once: bool = False) -> StreamingResponse: + def stream_operations(): + while True: + jobs = _html5_operation_jobs() + yield _html5_sse_event("operations-summary", render_html5_operation_summary(jobs)) + yield _html5_sse_event("operations-jobs", render_html5_operation_rows(jobs)) + if once: + break + time.sleep(3) + + return StreamingResponse( + stream_operations(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + @app.get("/html5/projects/{project_id}/editor") async def html5_project_editor(project_id: str, q: str = "") -> Response: try: @@ -1683,7 +1701,7 @@ async def html5_project_events(project_id: str, once: bool = False) -> Streaming fragment = render_html5_status(project_id, snapshot) except HTTPException as error: fragment = f'project: {project_id}error: {error.detail}' - yield f"event: status\ndata: {fragment}\n\n" + yield _html5_sse_event("status", fragment) if once: break time.sleep(5) @@ -8357,6 +8375,11 @@ def _html5_operation_jobs() -> list[OperationJob]: return sorted(_operations.jobs.values(), key=lambda job: job.updated_at, reverse=True)[:50] +def _html5_sse_event(event: str, fragment: str) -> str: + data = "\n".join(f"data: {line}" for line in fragment.splitlines()) + return f"event: {event}\n{data}\n\n" + + 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 916da98..c39a7b3 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -619,11 +619,22 @@ def test_html5_operations_renders_job_monitor_fragments(): assert 'data-html5-page="operations"' in page.text assert "data-html5-operations-body" in page.text assert "data-html5-operations-summary" in page.text - assert 'hx-get="/html5/operations/jobs"' in page.text - assert 'hx-get="/html5/operations/summary"' in page.text + assert 'hx-ext="sse"' in page.text + assert 'sse-connect="/html5/operations/events"' in page.text + assert 'sse-swap="operations-summary"' in page.text + assert 'sse-swap="operations-jobs"' in page.text + assert 'hx-trigger="every 3s"' not in page.text assert project_id in page.text assert "__next" not in page.text + with client.stream("GET", "/html5/operations/events?once=1") as events: + first_chunk = next(events.iter_text()) + assert "event: operations-summary" in first_chunk + assert "event: operations-jobs" in first_chunk + assert "data-html5-operations-summary" in first_chunk + assert "data-html5-operation" in first_chunk + assert project_id in first_chunk + rows = client.get("/html5/operations/jobs") assert rows.status_code == 200 assert "text/html" in rows.headers["content-type"]