Add htmx HTML5 fragments
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-16 21:18:43 +03:00
parent 567b517699
commit 03f1af0301
3 changed files with 92 additions and 18 deletions
+50 -17
View File
@@ -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 = '<tr><td colspan="4" class="muted">Проекты пока не настроены</td></tr>'
return _page(
@@ -23,7 +24,7 @@ def render_html5_index(projects: Iterable[object]) -> str:
<p class="lead">Основной HTML собирает API-сервер. Браузер получает готовую страницу без React/Next runtime.</p>
</div>
<div class="hero-metrics">
<strong>{len(list(projects))}</strong>
<strong>{len(project_list)}</strong>
<span>проектов</span>
</div>
</section>
@@ -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"""
<main class="workspace" data-html5-page="editor" data-project-id="{escape(project_id)}">
{_topbar(project_id, project_nav)}
<section class="layout">
<aside class="panel tree" data-html5-tree>
<div class="panel-title">Дерево объектов</div>
<nav>{''.join(_tree_item(node) for node in objects[:120]) or '<p class="muted">Объекты не найдены</p>'}</nav>
<nav>{''.join(_tree_item(project_id, node) for node in tree_nodes) or '<p class="muted">Объекты не найдены</p>'}</nav>
</aside>
<section class="editor" data-html5-editor>
<div class="editor-head">
@@ -107,12 +100,21 @@ def render_html5_editor(
<p class="eyebrow">HTML5 editor</p>
<h1>{escape(selected_module.qualified_name if selected_module else project_id)}</h1>
</div>
<form class="search" action="/html5/projects/{quote(project_id)}/editor" method="get" data-html5-search>
<form
class="search"
action="/html5/projects/{quote(project_id)}/editor"
method="get"
data-html5-search
hx-get="/html5/projects/{quote(project_id)}/symbols"
hx-target="[data-html5-symbol-results]"
hx-swap="innerHTML"
hx-push-url="false"
>
<input name="q" value="{escape(q)}" placeholder="Найти символ на сервере" />
<button type="submit">Найти</button>
</form>
</div>
<pre class="code" data-html5-source>{escape(source_text)}</pre>
{render_html5_source(selected_module)}
</section>
<aside class="panel inspector" data-html5-inspector>
<div class="panel-title">Серверный контекст</div>
@@ -126,7 +128,7 @@ def render_html5_editor(
<ul class="compact">{''.join(f'<li><span>{escape(kind)}</span><b>{count}</b></li>' for kind, count in counts.most_common(10))}</ul>
<div class="panel-title">Результаты</div>
<div data-html5-symbol-results>
{''.join(_symbol_result(node) for node in search_results) or '<p class="muted">Нет результатов</p>'}
{render_html5_symbols(snapshot, q)}
</div>
</aside>
</section>
@@ -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 '<p class="muted">Нет результатов</p>'
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'<pre class="code" data-html5-source data-html5-source-name="{escape(str(name))}">{escape(_node_source_text(node))}</pre>'
def _page(title: str, body: str) -> str:
return f"""<!doctype html>
<html lang="ru">
@@ -196,11 +221,19 @@ def _topbar(project_id: str, project_nav: str) -> str:
</header>"""
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'<a class="tree-item" href="#{quote(str(name))}" data-html5-node-kind="{escape(kind_value)}"><span>{escape(str(name))}</span><small>{escape(kind_value)}</small></a>'
lineage_id = str(getattr(node, "lineage_id", ""))
return (
f'<a class="tree-item" href="#{quote(str(name))}" '
f'data-html5-node-kind="{escape(kind_value)}" '
f'data-html5-lineage-id="{escape(lineage_id)}" '
f'hx-get="/html5/projects/{quote(project_id)}/source/{quote(lineage_id, safe="")}" '
'hx-target="[data-html5-source]" hx-swap="outerHTML">'
f'<span>{escape(str(name))}</span><small>{escape(kind_value)}</small></a>'
)
def _symbol_result(node: object) -> str:
+22 -1
View File
@@ -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"}