@@ -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"]