Add HTML5 setup SSE updates
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-17 02:51:17 +03:00
parent 8a1c0da0ea
commit 65c82c4fed
3 changed files with 61 additions and 5 deletions
+11 -5
View File
@@ -1038,7 +1038,13 @@ def render_html5_project_setup(*, project_id: str, projects: Iterable[object], s
sources = getattr(setup, "import_sources", []) or []
source_cards = "".join(_import_source_card(source) for source in sources)
content = f"""
<main class="workspace setup-workspace" data-html5-page="setup" data-project-id="{escape(project_id)}">
<main
class="workspace setup-workspace"
data-html5-page="setup"
data-project-id="{escape(project_id)}"
hx-ext="sse"
sse-connect="/html5/projects/{quote(project_id)}/setup/events"
>
{_topbar(project_id, project_nav)}
<section class="setup-layout">
<aside class="panel">
@@ -1187,7 +1193,7 @@ def render_html5_import_check(project_id: str, check: object | None = None) -> s
def render_html5_import_job(project_id: str, job: object | None = None) -> str:
if job is None:
return f"""
<div class="import-job" data-html5-import-job>
<div class="import-job" data-html5-import-job sse-swap="setup-import-job" hx-swap="outerHTML">
<div class="panel-title flush">Фоновый импорт</div>
<p class="muted padded">Фоновая задача импорта для проекта {escape(project_id)} еще не запускалась.</p>
</div>
@@ -1199,14 +1205,14 @@ def render_html5_import_job(project_id: str, job: object | None = None) -> str:
source = str(payload.get("source") or "")
stage = str(payload.get("stage") or "")
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
poll = ' hx-trigger="every 2s" hx-swap="outerHTML"' if status in {"QUEUED", "RUNNING"} else ""
logs_html = "".join(f"<li>{escape(str(item))}</li>" for item in logs[-6:])
return f"""
<div
class="import-job"
data-html5-import-job
hx-get="/html5/projects/{quote(project_id)}/setup/jobs/{quote(job_id)}"
{poll}
sse-swap="setup-import-job"
hx-swap="outerHTML"
>
<div class="panel-title flush">Фоновый импорт</div>
<div class="check-head">
@@ -1231,7 +1237,7 @@ def render_html5_setup_summary(project_id: str, setup: object) -> str:
class="setup-summary"
data-html5-setup-summary
hx-get="/html5/projects/{quote(project_id)}/setup/summary"
hx-trigger="every 5s"
sse-swap="setup-summary"
hx-swap="outerHTML"
>
<div class="section-title">
@@ -2021,6 +2021,24 @@ async def html5_project_setup_summary(project_id: str) -> Response:
)
@app.get("/html5/projects/{project_id}/setup/events")
async def html5_project_setup_events(project_id: str, once: bool = False) -> StreamingResponse:
async def stream_setup():
while True:
setup = _project_setup_response(project_id)
yield _html5_sse_event("setup-summary", render_html5_setup_summary(project_id, setup))
yield _html5_sse_event("setup-import-job", render_html5_import_job(project_id, _html5_latest_import_job(project_id)))
if once:
break
await asyncio.sleep(2)
return StreamingResponse(
stream_setup(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
@app.post("/html5/projects/{project_id}/setup/source")
async def html5_project_setup_source(project_id: str, request: Request) -> Response:
form = await _html5_form_data(request)
@@ -8392,6 +8410,19 @@ def _html5_operation_jobs() -> list[OperationJob]:
return sorted(_operations.jobs.values(), key=lambda job: job.updated_at, reverse=True)[:50]
def _html5_latest_import_job(project_id: str) -> OperationJob | None:
jobs = [
job
for job in _operations.jobs.values()
if job.payload.get("project_id") == project_id and _operation_value(getattr(job, "kind", "")) == "SERVER_IMPORT"
]
return max(jobs, key=lambda job: job.updated_at) if jobs else None
def _operation_value(value: object) -> str:
return str(getattr(value, "value", value))
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"
+19
View File
@@ -533,6 +533,11 @@ def test_html5_project_setup_renders_server_fragments():
assert "HTML5 Setup Demo" in setup.text
assert "data-html5-settings-panel" in setup.text
assert "data-html5-setup-summary" in setup.text
assert 'hx-ext="sse"' in setup.text
assert f'sse-connect="/html5/projects/{project_id}/setup/events"' in setup.text
assert 'sse-swap="setup-summary"' in setup.text
assert 'sse-swap="setup-import-job"' in setup.text
assert 'hx-trigger="every 5s"' not in setup.text
assert f'hx-get="/html5/projects/{project_id}/setup/summary"' in setup.text
assert f'hx-post="/html5/projects/{project_id}/setup/settings"' in setup.text
assert f'hx-post="/html5/projects/{project_id}/setup/source"' in setup.text
@@ -582,6 +587,8 @@ def test_html5_project_setup_renders_server_fragments():
assert "data-html5-import-job" in import_job.text
assert "SERVER_IMPORT" not in import_job.text
assert "hx-get" in import_job.text
assert 'sse-swap="setup-import-job"' in import_job.text
assert 'hx-trigger="every 2s"' not in import_job.text
assert "<html" not in import_job.text
jobs = client.get("/operations/jobs", params={"project_id": project_id, "kind": "SERVER_IMPORT"})
@@ -591,6 +598,8 @@ def test_html5_project_setup_renders_server_fragments():
assert job_fragment.status_code == 200
assert "data-html5-import-job" in job_fragment.text
assert job_id in job_fragment.text
assert 'sse-swap="setup-import-job"' in job_fragment.text
assert 'hx-trigger="every 2s"' not in job_fragment.text
assert "<html" not in job_fragment.text
html5_import = client.post(f"/html5/projects/{project_id}/setup/import")
@@ -611,8 +620,18 @@ def test_html5_project_setup_renders_server_fragments():
assert "data-html5-setup-summary" in summary.text
assert "INDEXED" in summary.text
assert "mock_indexed" in summary.text
assert 'sse-swap="setup-summary"' in summary.text
assert 'hx-trigger="every 5s"' not in summary.text
assert "<html" not in summary.text
with client.stream("GET", f"/html5/projects/{project_id}/setup/events?once=1") as events:
first_chunk = next(events.iter_text())
assert "event: setup-summary" in first_chunk
assert "event: setup-import-job" in first_chunk
assert "data-html5-setup-summary" in first_chunk
assert "data-html5-import-job" in first_chunk
assert project_id in first_chunk
def test_html5_operations_renders_job_monitor_fragments():
client = TestClient(app)