From f830c4290fd0c2d2962935978bae06706e57a054 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 17 May 2026 00:54:46 +0300 Subject: [PATCH] Add HTML5 focused object review --- services/api-server/src/api_server/html5.py | 15 ++++- services/api-server/src/api_server/main.py | 64 ++++++++++++++++++++- services/api-server/tests/test_api.py | 3 + 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py index 4decd8b..3187d16 100644 --- a/services/api-server/src/api_server/html5.py +++ b/services/api-server/src/api_server/html5.py @@ -385,7 +385,14 @@ def render_html5_object_report( """ -def render_html5_review(project_id: str, findings: list[dict] | None) -> str: +def render_html5_review( + project_id: str, + findings: list[dict] | None, + *, + title: str = "Review", + oob: bool = False, +) -> str: + oob_attr = ' hx-swap-oob="outerHTML"' if oob else "" if findings is None: return f"""
str: hx-get="/html5/projects/{quote(project_id)}/review" hx-trigger="load" hx-swap="outerHTML" + {oob_attr} > -
Review
+
{escape(title)}

Сервер готовит findings.

""" @@ -410,8 +418,9 @@ def render_html5_review(project_id: str, findings: list[dict] | None) -> str: hx-get="/html5/projects/{quote(project_id)}/review" hx-trigger="every 20s" hx-swap="outerHTML" + {oob_attr} > -
Review · {len(findings)}
+
{escape(title)} · {len(findings)}
{body}
""" diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index d5a8d0c..badc319 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -1770,6 +1770,7 @@ async def html5_project_object_context(project_id: str, object_name: str) -> Res knowledge = _knowledge_for_object_context(schema, impact, ui) source_node = _source_node_for_object_context(project_id, impact) symbol_references = await project_symbol_references(project_id, schema.object.lineage_id, direction="both") + focused_findings = _review_for_object_context(project_id, schema, impact, ui) object_context = render_html5_object_context( project_id, schema, @@ -1794,8 +1795,9 @@ async def html5_project_object_context(project_id: str, object_name: str) -> Res integrations=integrations, oob=True, ) + review_context = render_html5_review(project_id, focused_findings, title="Review объекта", oob=True) return Response( - object_context + flowchart_context + source_context + symbol_context + report_context, + object_context + flowchart_context + source_context + symbol_context + report_context + review_context, media_type="text/html; charset=utf-8", ) @@ -8262,6 +8264,66 @@ def _source_node_for_object_context( return None +def _review_for_object_context( + project_id: str, + schema: ObjectSchemaResponse, + impact: ObjectImpactResponse, + ui: ObjectUiResponse, +) -> list[dict]: + snapshot = _project_snapshot_or_404(project_id) + nodes_by_lineage = {node.lineage_id: node for node in snapshot.nodes} + names: set[str] = set() + lineages: set[str] = set() + + def add_node(node: NamedNode | None) -> None: + if node is None: + return + lineages.add(node.lineage_id) + names.add(node.name.casefold()) + names.add(node.qualified_name.casefold()) + + add_node(schema.object) + for attribute in schema.attributes: + add_node(attribute) + for section in schema.tabular_sections: + add_node(section.tabular_section) + for column in section.columns: + add_node(column) + for group in [ + impact.modules, + impact.routines, + impact.forms, + impact.commands, + impact.roles, + impact.jobs, + impact.callees, + impact.query_tables, + impact.writes, + ]: + for node in group: + add_node(node) + for form in ui.forms: + add_node(form.form) + for node in [*form.commands, *form.elements, *form.command_handlers.values()]: + add_node(node) + + source_paths = { + node.source_ref.source_path + for lineage_id in lineages + if (node := nodes_by_lineage.get(lineage_id)) is not None and node.source_ref is not None + } + focused: list[dict] = [] + for finding in get_review_payload(snapshot): + source_path = finding.get("source_path") + haystack = " ".join( + str(finding.get(key) or "").casefold() + for key in ["title", "message", "source_path"] + ) + if (source_path and source_path in source_paths) or any(name and name in haystack for name in names): + focused.append(finding) + return focused + + def _current_import_source(project_id: str) -> ImportSourceKind: setup = _project_setup_response(project_id) if setup.current_source is not None: diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index b89c7be..61144f7 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -337,6 +337,9 @@ def test_html5_object_context_fragment(tmp_path: Path): assert "data-html5-project-report" in context.text assert "Отчет объекта" in context.text assert "server focused summary" in context.text + assert "data-html5-review" in context.text + assert "Review объекта" in context.text + assert "External integration endpoint" in context.text assert "1 signals" in context.text assert "1 errors" in context.text assert "125.0 ms" in context.text