diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py
index 812e992..9f9f3e1 100644
--- a/services/api-server/src/api_server/html5.py
+++ b/services/api-server/src/api_server/html5.py
@@ -322,6 +322,7 @@ def render_html5_object_context(
impact: object | None,
access: object | None = None,
ui: object | None = None,
+ runtime: Iterable[object] | None = None,
) -> str:
if schema is None or impact is None:
return f"""
@@ -341,6 +342,7 @@ def render_html5_object_context(
jobs = getattr(impact, "jobs", []) or []
grants = getattr(access, "grants", []) if access is not None else []
ui_forms = getattr(ui, "forms", []) if ui is not None else []
+ runtime_items = list(runtime or [])
return f"""
Object context
@@ -355,12 +357,14 @@ def render_html5_object_context(
{_metric("Routines", len(routines))}
{_metric("Forms", len(ui_forms) or len(forms))}
{_metric("Roles", len(grants) or len(roles))}
+ {_metric("Runtime", len(runtime_items))}
{''.join(_named_node_item("attr", item) for item in attributes[:6]) or '
Реквизиты не найдены
'}
{''.join(_tabular_section_item(item) for item in sections[:4])}
{''.join(_ui_form_item(item) for item in ui_forms[:4])}
{''.join(_role_access_item(item) for item in grants[:6])}
+ {''.join(_runtime_summary_item(item) for item in runtime_items[:6])}
{''.join(_named_node_item("routine", item) for item in routines[:6])}
{''.join(_named_node_item("job", item) for item in jobs[:4])}
@@ -1423,6 +1427,21 @@ def _ui_form_item(form_semantics: object) -> str:
"""
+def _runtime_summary_item(item: object) -> str:
+ node = getattr(item, "node", None)
+ name = getattr(node, "qualified_name", None) or getattr(node, "name", "runtime")
+ signal_count = getattr(item, "signal_count", 0)
+ error_count = getattr(item, "error_count", 0)
+ max_duration = getattr(item, "max_duration_ms", None)
+ duration_text = f" · max {max_duration} ms" if max_duration is not None else ""
+ return f"""
+
+ {escape(str(name))}
+ {escape(str(signal_count))} signals · {escape(str(error_count))} errors{escape(duration_text)}
+
+ """
+
+
def _authoring_diff_item(line: object) -> str:
kind = str(getattr(line, "kind", ""))
text = str(getattr(line, "text", ""))
diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py
index 3884e83..d1abfd7 100644
--- a/services/api-server/src/api_server/main.py
+++ b/services/api-server/src/api_server/main.py
@@ -1747,8 +1747,9 @@ async def html5_project_object_context(project_id: str, object_name: str) -> Res
impact = await get_object_impact(project_id, object_name)
access = await get_object_access(project_id, object_name)
ui = await get_object_ui(project_id, object_name)
+ runtime = _runtime_for_object_context(project_id, impact)
return Response(
- render_html5_object_context(project_id, schema, impact, access, ui),
+ render_html5_object_context(project_id, schema, impact, access, ui, runtime),
media_type="text/html; charset=utf-8",
)
@@ -8112,6 +8113,35 @@ def _html5_metadata_tabular_sections(raw: str) -> list[dict]:
return sections
+def _runtime_for_object_context(project_id: str, impact: ObjectImpactResponse) -> list[RuntimeSummaryResponse]:
+ lineages = {
+ item.lineage_id
+ for group in [
+ [impact.object],
+ impact.modules,
+ impact.routines,
+ impact.forms,
+ impact.commands,
+ impact.jobs,
+ impact.writes,
+ ]
+ for item in group
+ if item is not None
+ }
+ snapshot = _project_snapshot_or_404(project_id)
+ overlay = _overlays.get(project_id, RuntimeOverlay(project_id=project_id))
+ return [
+ RuntimeSummaryResponse(
+ node=_named_node(item.node),
+ signal_count=item.signal_count,
+ error_count=item.error_count,
+ max_duration_ms=item.max_duration_ms,
+ )
+ for item in summarize_runtime(snapshot, overlay)
+ if item.node.lineage_id in lineages
+ ]
+
+
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 41ac53b..db4c130 100644
--- a/services/api-server/tests/test_api.py
+++ b/services/api-server/tests/test_api.py
@@ -254,6 +254,20 @@ def test_html5_object_context_fragment(tmp_path: Path):
client = TestClient(app)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
+ snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
+ handler = next(node for node in snapshot["nodes"] if node["name"] == "ПровестиКоманда")
+ signal = client.post(
+ f"/projects/{project_id}/runtime/signals",
+ json={
+ "signal": {
+ "signal_id": "html5-runtime.1",
+ "lineage_id": handler["lineage_id"],
+ "kind": "ERROR",
+ "duration_ms": 125.0,
+ }
+ },
+ )
+ assert signal.status_code == 200
editor = client.get(f"/html5/projects/{project_id}/editor")
assert editor.status_code == 200
@@ -271,6 +285,9 @@ def test_html5_object_context_fragment(tmp_path: Path):
assert "ФормаДокумента" in context.text
assert "Провести" in context.text
assert "ПровестиКоманда" in context.text
+ assert "1 signals" in context.text
+ assert "1 errors" in context.text
+ assert "125.0 ms" in context.text
assert "Роль.Менеджер" in context.text
assert "read, write, post" in context.text or "post, read, write" in context.text
assert "