From 03f1af0301d769b667e7ed1e4fe816e4cff25f37 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 16 May 2026 21:18:43 +0300 Subject: [PATCH] Add htmx HTML5 fragments --- services/api-server/src/api_server/html5.py | 67 +++++++++++++++------ services/api-server/src/api_server/main.py | 23 ++++++- services/api-server/tests/test_api.py | 20 ++++++ 3 files changed, 92 insertions(+), 18 deletions(-) diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py index f698861..3e9a136 100644 --- a/services/api-server/src/api_server/html5.py +++ b/services/api-server/src/api_server/html5.py @@ -9,7 +9,8 @@ from sir import NodeKind, SirSnapshot def render_html5_index(projects: Iterable[object]) -> str: - project_rows = "\n".join(_project_row(project) for project in projects) + project_list = list(projects) + project_rows = "\n".join(_project_row(project) for project in project_list) if not project_rows: project_rows = 'Проекты пока не настроены' return _page( @@ -23,7 +24,7 @@ def render_html5_index(projects: Iterable[object]) -> str:

Основной HTML собирает API-сервер. Браузер получает готовую страницу без React/Next runtime.

- {len(list(projects))} + {len(project_list)} проектов
@@ -83,23 +84,15 @@ def render_html5_editor( NodeKind.DATA_PROCESSOR, } ] + tree_nodes = objects[:120] or modules[:120] selected_module = modules[0] if modules else None - query = q.strip().lower() - search_results = [ - node for node in snapshot.nodes - if query and query in (node.qualified_name or node.name).lower() - ][:30] - if not query: - search_results = modules[:12] or snapshot.nodes[:12] - - source_text = _node_source_text(selected_module) content = f"""
{_topbar(project_id, project_nav)}
@@ -107,12 +100,21 @@ def render_html5_editor(

HTML5 editor

{escape(selected_module.qualified_name if selected_module else project_id)}

- -
{escape(source_text)}
+ {render_html5_source(selected_module)}
@@ -150,6 +152,29 @@ def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str: ) +def html5_symbol_results(snapshot: SirSnapshot, q: str) -> list[object]: + query = q.strip().lower() + if not query: + modules = [node for node in snapshot.nodes if node.kind == NodeKind.MODULE] + return (modules[:12] or snapshot.nodes[:12]) + return [ + node for node in snapshot.nodes + if query in (node.qualified_name or node.name).lower() + ][:30] + + +def render_html5_symbols(snapshot: SirSnapshot, q: str) -> str: + results = html5_symbol_results(snapshot, q) + if not results: + return '

Нет результатов

' + return "".join(_symbol_result(node) for node in results) + + +def render_html5_source(node: object | None) -> str: + name = "source" if node is None else getattr(node, "qualified_name", None) or getattr(node, "name", "source") + return f'
{escape(_node_source_text(node))}
' + + def _page(title: str, body: str) -> str: return f""" @@ -196,11 +221,19 @@ def _topbar(project_id: str, project_nav: str) -> str: """ -def _tree_item(node: object) -> str: +def _tree_item(project_id: str, node: object) -> str: name = getattr(node, "qualified_name", None) or getattr(node, "name", "") kind = getattr(node, "kind", "") kind_value = str(kind.value if hasattr(kind, "value") else kind) - return f'{escape(str(name))}{escape(kind_value)}' + lineage_id = str(getattr(node, "lineage_id", "")) + return ( + f'' + f'{escape(str(name))}{escape(kind_value)}' + ) def _symbol_result(node: object) -> str: diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index 6b27fab..e08c1a3 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -35,7 +35,7 @@ from fastapi.responses import PlainTextResponse, Response, StreamingResponse from neo4j import AsyncGraphDatabase from pydantic import BaseModel, Field -from api_server.html5 import render_html5_editor, render_html5_index, render_html5_status +from api_server.html5 import render_html5_editor, render_html5_index, render_html5_source, render_html5_status, render_html5_symbols from impact_engine import object_impact, routine_impact from incremental_indexer import rebuild_changed_file from integration_topology import IntegrationKind, build_integration_topology @@ -1618,6 +1618,27 @@ async def html5_project_events(project_id: str, once: bool = False) -> Streaming ) +@app.get("/html5/projects/{project_id}/symbols") +async def html5_project_symbols(project_id: str, q: str = "") -> Response: + snapshot = _project_snapshot_or_404(project_id) + return Response( + render_html5_symbols(snapshot, q), + media_type="text/html; charset=utf-8", + ) + + +@app.get("/html5/projects/{project_id}/source/{lineage_id}") +async def html5_project_source(project_id: str, lineage_id: str) -> Response: + snapshot = _project_snapshot_or_404(project_id) + node = _find_snapshot_node(snapshot, lineage_id) + if node is None: + raise HTTPException(status_code=404, detail=f"Lineage not found: {lineage_id}") + return Response( + render_html5_source(node), + media_type="text/html; charset=utf-8", + ) + + @app.get("/version") async def version() -> dict[str, str]: return {"name": "sfera", "version": "0.1.0"} diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index f98f900..5e60419 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -107,6 +107,10 @@ def test_html5_server_rendered_project_editor(tmp_path: Path): assert 'data-html5-page="editor"' in editor.text assert "data-html5-editor" in editor.text assert "data-html5-symbol-results" in editor.text + assert 'hx-get="/html5/projects/' in editor.text + assert 'hx-target="[data-html5-symbol-results]"' in editor.text + assert 'hx-target="[data-html5-source]"' in editor.text + assert 'hx-swap="outerHTML"' in editor.text assert "hx-ext=\"sse\"" in editor.text assert f"sse-connect=\"/html5/projects/{project_id}/events\"" in editor.text assert "htmx.org" in editor.text @@ -121,6 +125,22 @@ def test_html5_server_rendered_project_editor(tmp_path: Path): assert "data:" in first_chunk assert project_id in first_chunk + symbols = client.get(f"/html5/projects/{project_id}/symbols", params={"q": "Проверить"}) + assert symbols.status_code == 200 + assert "text/html" in symbols.headers["content-type"] + assert 'data-html5-symbol' in symbols.text + assert "Проверить" in symbols.text + assert "