diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py
index eec336f..90f7136 100644
--- a/services/api-server/src/api_server/html5.py
+++ b/services/api-server/src/api_server/html5.py
@@ -5,6 +5,11 @@ from html import escape
from typing import Iterable
from urllib.parse import quote
+from api_server.html5_authoring import (
+ render_html5_authoring_changes,
+ render_html5_authoring_preview,
+ render_html5_metadata_authoring,
+)
from sir import NodeKind, SirSnapshot
_HTML5_OBJECT_CONTEXT_KINDS = {
@@ -580,388 +585,6 @@ def render_html5_object_context(
"""
-def render_html5_authoring_changes(project_id: str, changes: Iterable[object] | None) -> str:
- if changes is None:
- return f"""
-
-
Authoring
-
Сервер загружает историю рабочих изменений.
-
- """
- change_list = list(changes)
- if not change_list:
- body = 'Изменений пока нет
'
- else:
- body = "".join(_authoring_change_item(change) for change in change_list[:12])
- return f"""
-
-
Authoring · {len(change_list)}
- {_authoring_changes_summary(change_list)}
- {_authoring_recent_change(change_list)}
-
{body}
- {render_html5_authoring_change_detail(project_id, None)}
-
- """
-
-
-def render_html5_authoring_preview(project_id: str, preview: object | None, error: str | None = None) -> str:
- if preview is None and error is None:
- return f"""
-
- """
- return render_html5_authoring_preview_result(project_id, preview, error)
-
-
-def render_html5_authoring_preview_result(project_id: str, preview: object | None = None, error: str | None = None) -> str:
- if preview is None and error is None:
- return '
'
- if error:
- return f"""
-
-
Preview result
-
{escape(error)}
-
- """
- allowed = bool(getattr(preview, "allowed", False))
- insert_text = str(getattr(preview, "insert_text", ""))
- checks = getattr(preview, "checks", []) or []
- diff = getattr(preview, "semantic_diff", []) or []
- context = getattr(preview, "context", None)
- object_node = getattr(context, "object", None)
- routine_node = getattr(context, "routine", None)
- object_name = getattr(object_node, "qualified_name", None) or getattr(object_node, "name", None) or "object unavailable"
- routine_name = getattr(routine_node, "name", None) or "routine unavailable"
- check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
- diff_rows = "".join(_authoring_diff_item(line) for line in diff[:8]) or 'Diff пустой
'
- return f"""
-
-
Preview result · {'allowed' if allowed else 'blocked'}
-
- {escape(str(object_name))}
- {escape(str(routine_name))}
- {escape(insert_text[:180] or "insert text unavailable")}
-
- {_authoring_result_summary("allowed" if allowed else "blocked", diff, checks)}
-
{check_rows}
-
{diff_rows}
-
- """
-
-
-def render_html5_authoring_diff_result(
- project_id: str,
- preview: object | None = None,
- error: str | None = None,
- request_payload: dict | None = None,
-) -> str:
- if preview is None and error is None:
- return '
'
- if error:
- return f"""
-
-
Diff preview
-
{escape(error)}
-
- """
- changed = bool(getattr(preview, "changed", False))
- added = getattr(preview, "added_lines", 0)
- removed = getattr(preview, "removed_lines", 0)
- target = getattr(preview, "target", None)
- target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
- checks = getattr(preview, "checks", []) or []
- diff = getattr(preview, "semantic_diff", []) or []
- version_preview = getattr(preview, "version_preview", None)
- next_version_id = str(getattr(version_preview, "next_version_id", ""))
- check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
- diff_rows = "".join(_authoring_diff_item(line) for line in diff[:12]) or 'Diff пустой
'
- apply_form = (
- _authoring_apply_change_set_form(project_id, request_payload or {}, next_version_id)
- if changed and next_version_id
- else ""
- )
- return f"""
-
-
Diff preview · {'changed' if changed else 'unchanged'}
-
- {escape(str(target_name))}
- +{escape(str(added))} / -{escape(str(removed))}
- {escape(next_version_id or "version preview unavailable")}
-
- {_authoring_result_summary("changed" if changed else "unchanged", diff, checks)}
-
{check_rows}
-
{diff_rows}
- {apply_form}
-
- """
-
-
-def render_html5_authoring_apply_result(project_id: str, result: object | None = None, error: str | None = None) -> str:
- if result is None and error is None:
- return '
'
- if error:
- return f"""
-
-
Apply change-set
-
{escape(error)}
-
- """
- status = str(getattr(result, "status", "UNKNOWN"))
- change_id = str(getattr(result, "change_id", ""))
- version = getattr(result, "version", None)
- version_id = str(getattr(version, "version_id", ""))
- return f"""
-
-
Apply change-set
-
- {escape(status)}
- {escape(change_id)}
- {escape(version_id)}
-
- {_authoring_apply_summary("change-set", status, change_id, version_id)}
-
Change-set применен в workspace для проекта {escape(project_id)}.
-
- """
-
-
-def render_html5_metadata_authoring(project_id: str) -> str:
- return f"""
-
- """
-
-
-def render_html5_metadata_preview_result(
- project_id: str,
- preview: object | None = None,
- error: str | None = None,
- request_payload: dict | None = None,
-) -> str:
- if preview is None and error is None:
- return '
'
- if error:
- return f"""
-
- """
- changed = bool(getattr(preview, "changed", False))
- added = getattr(preview, "added_lines", 0)
- target = getattr(preview, "target", None)
- target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
- checks = getattr(preview, "checks", []) or []
- diff = getattr(preview, "semantic_diff", []) or []
- version_preview = getattr(preview, "version_preview", None)
- next_version_id = str(getattr(version_preview, "next_version_id", ""))
- check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
- diff_rows = "".join(_authoring_diff_item(line) for line in diff[:12]) or 'Diff пустой
'
- apply_form = (
- _metadata_apply_form(project_id, request_payload or {}, next_version_id)
- if changed and next_version_id
- else ""
- )
- return f"""
-
- """
-
-
-def render_html5_metadata_apply_result(project_id: str, result: object | None = None, error: str | None = None) -> str:
- if result is None and error is None:
- return '
'
- if error:
- return f"""
-
- """
- status = str(getattr(result, "status", "UNKNOWN"))
- change_id = str(getattr(result, "change_id", ""))
- version = getattr(result, "version", None)
- version_id = str(getattr(version, "version_id", ""))
- return f"""
-
- """
-
-
-def render_html5_authoring_change_detail(project_id: str, preview: object | None) -> str:
- if preview is None:
- return f"""
-
-
Rollback preview
-
Выберите изменение, чтобы сервер рассчитал rollback diff для проекта {escape(project_id)}.
-
- """
- change_id = str(getattr(preview, "change_id", ""))
- original_version_id = str(getattr(preview, "original_version_id", ""))
- rollback_version_id = str(getattr(preview, "rollback_version_id", ""))
- target = getattr(preview, "target", None)
- target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
- diff = getattr(preview, "semantic_diff", []) or []
- checks = getattr(preview, "checks", []) or []
- apply_available = bool(getattr(preview, "apply_available", False))
- diff_rows = "".join(_authoring_diff_item(line) for line in diff[:12]) or 'Diff пустой
'
- check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
- apply_form = _authoring_rollback_form(project_id, change_id, rollback_version_id) if apply_available else ""
- return f"""
-
-
Rollback preview · {'ready' if apply_available else 'blocked'}
-
- {escape(str(target_name))}
- {escape(original_version_id)} -> {escape(rollback_version_id)}
- {escape(change_id)}
-
- {_authoring_detail_summary(diff, checks, apply_available)}
-
{check_rows}
-
{diff_rows}
- {apply_form}
-
- """
-
-
-def render_html5_authoring_rollback_result(project_id: str, result: object | None = None, error: str | None = None) -> str:
- if result is None and error is None:
- return '
'
- if error:
- return f"""
-
-
Rollback apply
-
{escape(error)}
-
- """
- status = str(getattr(result, "status", "UNKNOWN"))
- change_id = str(getattr(result, "change_id", ""))
- rollback_change_id = str(getattr(result, "rollback_change_id", ""))
- version = getattr(result, "version", None)
- version_id = str(getattr(version, "version_id", ""))
- return f"""
-
-
Rollback apply
-
- {escape(status)}
- {escape(rollback_change_id)}
- {escape(version_id)}
-
- {_authoring_apply_summary("rollback", status, rollback_change_id or change_id, version_id)}
-
Rollback применен в workspace для проекта {escape(project_id)}.
-
- """
-
-
def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str:
return (
f'project: {escape(project_id)} '
@@ -1297,113 +920,6 @@ def _symbol_reference_item(project_id: str, reference: object) -> str:
"""
-def _authoring_change_item(change: object) -> str:
- change_id = str(getattr(change, "change_id", ""))
- status = str(getattr(change, "status", ""))
- target = getattr(change, "target", None)
- target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
- approved_by = str(getattr(change, "approved_by", "") or "not approved")
- task_id = str(getattr(change, "task_id", "") or "no task")
- added = getattr(change, "added_lines", 0)
- removed = getattr(change, "removed_lines", 0)
- production = "production" if bool(getattr(change, "production_applied", False)) else "workspace"
- project_id = str(getattr(change, "project_id", ""))
- detail_attrs = (
- f'hx-get="/html5/projects/{quote(project_id)}/authoring/changes/{quote(change_id, safe="")}" '
- 'hx-target="[data-html5-authoring-detail]" hx-swap="outerHTML"'
- if change_id and project_id
- else ""
- )
- return f"""
-
- {escape(str(target_name))}
- {escape(status)} · +{escape(str(added))} / -{escape(str(removed))} · {escape(production)}
- {escape(task_id)} · {escape(approved_by)} · {escape(change_id)}
-
- """
-
-
-def _authoring_changes_summary(changes: Iterable[object]) -> str:
- change_list = list(changes)
- statuses = Counter(str(getattr(change, "status", "") or "UNKNOWN") for change in change_list)
- production = sum(1 for change in change_list if bool(getattr(change, "production_applied", False)))
- workspace = len(change_list) - production
- added = sum(int(getattr(change, "added_lines", 0) or 0) for change in change_list)
- removed = sum(int(getattr(change, "removed_lines", 0) or 0) for change in change_list)
- status_text = ", ".join(f"{name}: {count}" for name, count in sorted(statuses.items())) or "no changes"
- return f"""
-
- {escape(str(len(change_list)))} changes · {escape(status_text)} · {escape(str(workspace))} workspace · {escape(str(production))} production · +{escape(str(added))} / -{escape(str(removed))}
-
- """
-
-
-def _authoring_recent_change(changes: Iterable[object]) -> str:
- change_list = list(changes)
- if not change_list:
- return ""
- latest = change_list[0]
- change_id = str(getattr(latest, "change_id", ""))
- status = str(getattr(latest, "status", "") or "UNKNOWN")
- target = getattr(latest, "target", None)
- target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
- version = getattr(latest, "version", None)
- version_id = str(getattr(latest, "version_id", "") or getattr(version, "version_id", "") or "version unavailable")
- approved_by = str(getattr(latest, "approved_by", "") or "not approved")
- return f"""
-
- {escape(status)} · {escape(str(target_name))}
- {escape(version_id)}
- {escape(approved_by)} · {escape(change_id)}
-
- """
-
-
-def _authoring_result_summary(state: str, diff: Iterable[object], checks: Iterable[object]) -> str:
- diff_list = list(diff)
- check_list = list(checks)
- diff_kinds = Counter(str(getattr(line, "kind", "") or "CHANGE") for line in diff_list)
- check_statuses = Counter(str(getattr(check, "status", "") or "UNKNOWN") for check in check_list)
- added = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "ADD")
- removed = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "REMOVE")
- diff_text = ", ".join(f"{name}: {count}" for name, count in sorted(diff_kinds.items())) or "empty diff"
- check_text = ", ".join(f"{name}: {count}" for name, count in sorted(check_statuses.items())) or "no checks"
- return f"""
-
- {escape(state)} · {escape(str(len(diff_list)))} diff lines · +{escape(str(added))} / -{escape(str(removed))} · {escape(diff_text)} · {escape(check_text)}
-
- """
-
-
-def _authoring_detail_summary(diff: Iterable[object], checks: Iterable[object], apply_available: bool) -> str:
- diff_list = list(diff)
- check_list = list(checks)
- diff_kinds = Counter(str(getattr(line, "kind", "") or "CHANGE") for line in diff_list)
- check_statuses = Counter(str(getattr(check, "status", "") or "UNKNOWN") for check in check_list)
- added = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "ADD")
- removed = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "REMOVE")
- diff_text = ", ".join(f"{name}: {count}" for name, count in sorted(diff_kinds.items())) or "empty diff"
- check_text = ", ".join(f"{name}: {count}" for name, count in sorted(check_statuses.items())) or "no checks"
- state = "rollback ready" if apply_available else "rollback blocked"
- return f"""
-
- {escape(state)} · {escape(str(len(diff_list)))} diff lines · +{escape(str(added))} / -{escape(str(removed))} · {escape(diff_text)} · {escape(check_text)}
-
- """
-
-
-def _authoring_apply_summary(kind: str, status: str, change_id: str, version_id: str) -> str:
- return f"""
-
- {escape(kind)} · {escape(status or "UNKNOWN")} · {escape(change_id or "change unavailable")} · {escape(version_id or "version unavailable")}
-
- """
-
-
def _named_node_item(label: str, node: object) -> str:
name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
kind = getattr(node, "kind", label)
@@ -1759,109 +1275,6 @@ def _flowchart_node_item(project_id: str, node: object, depth: int) -> str:
"""
-def _authoring_diff_item(line: object) -> str:
- kind = str(getattr(line, "kind", ""))
- text = str(getattr(line, "text", ""))
- return f"""
-
- {escape(kind)}
- {escape(text)}
-
- """
-
-
-def _authoring_check_item(check: object) -> str:
- name = str(getattr(check, "name", "check"))
- status = str(getattr(check, "status", "UNKNOWN"))
- message = str(getattr(check, "message", ""))
- return f"""
-
- {escape(name)}
- {escape(status)}
- {escape(message)}
-
- """
-
-
-def _authoring_rollback_form(project_id: str, change_id: str, rollback_version_id: str) -> str:
- return f"""
-
-
-
-
-
-
- Apply rollback
-
- {render_html5_authoring_rollback_result(project_id)}
- """
-
-
-def _authoring_apply_change_set_form(project_id: str, payload: dict, next_version_id: str) -> str:
- return f"""
-
-
-
- {escape(str(payload.get("original_text") or ""))}
- {escape(str(payload.get("proposed_text") or ""))}
-
-
-
-
-
-
- Apply change-set
-
- {render_html5_authoring_apply_result(project_id)}
- """
-
-
-def _metadata_apply_form(project_id: str, payload: dict, next_version_id: str) -> str:
- return f"""
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Apply metadata draft
-
- {render_html5_metadata_apply_result(project_id)}
- """
-
-
def _node_source_text(node: object | None) -> str:
if node is None:
return "// Модуль не найден в snapshot.\n// HTML5 IDE показывает серверный fallback без клиентского JS."
diff --git a/services/api-server/src/api_server/html5_authoring.py b/services/api-server/src/api_server/html5_authoring.py
new file mode 100644
index 0000000..d71ccea
--- /dev/null
+++ b/services/api-server/src/api_server/html5_authoring.py
@@ -0,0 +1,596 @@
+from __future__ import annotations
+
+from collections import Counter
+from html import escape
+from typing import Iterable
+from urllib.parse import quote
+
+
+def render_html5_authoring_changes(project_id: str, changes: Iterable[object] | None) -> str:
+ if changes is None:
+ return f"""
+
+
Authoring
+
Сервер загружает историю рабочих изменений.
+
+ """
+ change_list = list(changes)
+ if not change_list:
+ body = 'Изменений пока нет
'
+ else:
+ body = "".join(_authoring_change_item(change) for change in change_list[:12])
+ return f"""
+
+
Authoring · {len(change_list)}
+ {_authoring_changes_summary(change_list)}
+ {_authoring_recent_change(change_list)}
+
{body}
+ {render_html5_authoring_change_detail(project_id, None)}
+
+ """
+
+
+def render_html5_authoring_preview(project_id: str, preview: object | None, error: str | None = None) -> str:
+ if preview is None and error is None:
+ return f"""
+
+ """
+ return render_html5_authoring_preview_result(project_id, preview, error)
+
+
+def render_html5_authoring_preview_result(project_id: str, preview: object | None = None, error: str | None = None) -> str:
+ if preview is None and error is None:
+ return '
'
+ if error:
+ return f"""
+
+
Preview result
+
{escape(error)}
+
+ """
+ allowed = bool(getattr(preview, "allowed", False))
+ insert_text = str(getattr(preview, "insert_text", ""))
+ checks = getattr(preview, "checks", []) or []
+ diff = getattr(preview, "semantic_diff", []) or []
+ context = getattr(preview, "context", None)
+ object_node = getattr(context, "object", None)
+ routine_node = getattr(context, "routine", None)
+ object_name = getattr(object_node, "qualified_name", None) or getattr(object_node, "name", None) or "object unavailable"
+ routine_name = getattr(routine_node, "name", None) or "routine unavailable"
+ check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
+ diff_rows = "".join(_authoring_diff_item(line) for line in diff[:8]) or 'Diff пустой
'
+ return f"""
+
+
Preview result · {'allowed' if allowed else 'blocked'}
+
+ {escape(str(object_name))}
+ {escape(str(routine_name))}
+ {escape(insert_text[:180] or "insert text unavailable")}
+
+ {_authoring_result_summary("allowed" if allowed else "blocked", diff, checks)}
+
{check_rows}
+
{diff_rows}
+
+ """
+
+
+def render_html5_authoring_diff_result(
+ project_id: str,
+ preview: object | None = None,
+ error: str | None = None,
+ request_payload: dict | None = None,
+) -> str:
+ if preview is None and error is None:
+ return '
'
+ if error:
+ return f"""
+
+
Diff preview
+
{escape(error)}
+
+ """
+ changed = bool(getattr(preview, "changed", False))
+ added = getattr(preview, "added_lines", 0)
+ removed = getattr(preview, "removed_lines", 0)
+ target = getattr(preview, "target", None)
+ target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
+ checks = getattr(preview, "checks", []) or []
+ diff = getattr(preview, "semantic_diff", []) or []
+ version_preview = getattr(preview, "version_preview", None)
+ next_version_id = str(getattr(version_preview, "next_version_id", ""))
+ check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
+ diff_rows = "".join(_authoring_diff_item(line) for line in diff[:12]) or 'Diff пустой
'
+ apply_form = (
+ _authoring_apply_change_set_form(project_id, request_payload or {}, next_version_id)
+ if changed and next_version_id
+ else ""
+ )
+ return f"""
+
+
Diff preview · {'changed' if changed else 'unchanged'}
+
+ {escape(str(target_name))}
+ +{escape(str(added))} / -{escape(str(removed))}
+ {escape(next_version_id or "version preview unavailable")}
+
+ {_authoring_result_summary("changed" if changed else "unchanged", diff, checks)}
+
{check_rows}
+
{diff_rows}
+ {apply_form}
+
+ """
+
+
+def render_html5_authoring_apply_result(project_id: str, result: object | None = None, error: str | None = None) -> str:
+ if result is None and error is None:
+ return '
'
+ if error:
+ return f"""
+
+
Apply change-set
+
{escape(error)}
+
+ """
+ status = str(getattr(result, "status", "UNKNOWN"))
+ change_id = str(getattr(result, "change_id", ""))
+ version = getattr(result, "version", None)
+ version_id = str(getattr(version, "version_id", ""))
+ return f"""
+
+
Apply change-set
+
+ {escape(status)}
+ {escape(change_id)}
+ {escape(version_id)}
+
+ {_authoring_apply_summary("change-set", status, change_id, version_id)}
+
Change-set применен в workspace для проекта {escape(project_id)}.
+
+ """
+
+
+def render_html5_metadata_authoring(project_id: str) -> str:
+ return f"""
+
+ """
+
+
+def render_html5_metadata_preview_result(
+ project_id: str,
+ preview: object | None = None,
+ error: str | None = None,
+ request_payload: dict | None = None,
+) -> str:
+ if preview is None and error is None:
+ return '
'
+ if error:
+ return f"""
+
+ """
+ changed = bool(getattr(preview, "changed", False))
+ added = getattr(preview, "added_lines", 0)
+ target = getattr(preview, "target", None)
+ target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
+ checks = getattr(preview, "checks", []) or []
+ diff = getattr(preview, "semantic_diff", []) or []
+ version_preview = getattr(preview, "version_preview", None)
+ next_version_id = str(getattr(version_preview, "next_version_id", ""))
+ check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
+ diff_rows = "".join(_authoring_diff_item(line) for line in diff[:12]) or 'Diff пустой
'
+ apply_form = (
+ _metadata_apply_form(project_id, request_payload or {}, next_version_id)
+ if changed and next_version_id
+ else ""
+ )
+ return f"""
+
+ """
+
+
+def render_html5_metadata_apply_result(project_id: str, result: object | None = None, error: str | None = None) -> str:
+ if result is None and error is None:
+ return '
'
+ if error:
+ return f"""
+
+ """
+ status = str(getattr(result, "status", "UNKNOWN"))
+ change_id = str(getattr(result, "change_id", ""))
+ version = getattr(result, "version", None)
+ version_id = str(getattr(version, "version_id", ""))
+ return f"""
+
+ """
+
+
+def render_html5_authoring_change_detail(project_id: str, preview: object | None) -> str:
+ if preview is None:
+ return f"""
+
+
Rollback preview
+
Выберите изменение, чтобы сервер рассчитал rollback diff для проекта {escape(project_id)}.
+
+ """
+ change_id = str(getattr(preview, "change_id", ""))
+ original_version_id = str(getattr(preview, "original_version_id", ""))
+ rollback_version_id = str(getattr(preview, "rollback_version_id", ""))
+ target = getattr(preview, "target", None)
+ target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
+ diff = getattr(preview, "semantic_diff", []) or []
+ checks = getattr(preview, "checks", []) or []
+ apply_available = bool(getattr(preview, "apply_available", False))
+ diff_rows = "".join(_authoring_diff_item(line) for line in diff[:12]) or 'Diff пустой
'
+ check_rows = "".join(_authoring_check_item(check) for check in checks[:8])
+ apply_form = _authoring_rollback_form(project_id, change_id, rollback_version_id) if apply_available else ""
+ return f"""
+
+
Rollback preview · {'ready' if apply_available else 'blocked'}
+
+ {escape(str(target_name))}
+ {escape(original_version_id)} -> {escape(rollback_version_id)}
+ {escape(change_id)}
+
+ {_authoring_detail_summary(diff, checks, apply_available)}
+
{check_rows}
+
{diff_rows}
+ {apply_form}
+
+ """
+
+
+def render_html5_authoring_rollback_result(project_id: str, result: object | None = None, error: str | None = None) -> str:
+ if result is None and error is None:
+ return '
'
+ if error:
+ return f"""
+
+
Rollback apply
+
{escape(error)}
+
+ """
+ status = str(getattr(result, "status", "UNKNOWN"))
+ change_id = str(getattr(result, "change_id", ""))
+ rollback_change_id = str(getattr(result, "rollback_change_id", ""))
+ version = getattr(result, "version", None)
+ version_id = str(getattr(version, "version_id", ""))
+ return f"""
+
+
Rollback apply
+
+ {escape(status)}
+ {escape(rollback_change_id)}
+ {escape(version_id)}
+
+ {_authoring_apply_summary("rollback", status, rollback_change_id or change_id, version_id)}
+
Rollback применен в workspace для проекта {escape(project_id)}.
+
+ """
+
+def _authoring_change_item(change: object) -> str:
+ change_id = str(getattr(change, "change_id", ""))
+ status = str(getattr(change, "status", ""))
+ target = getattr(change, "target", None)
+ target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
+ approved_by = str(getattr(change, "approved_by", "") or "not approved")
+ task_id = str(getattr(change, "task_id", "") or "no task")
+ added = getattr(change, "added_lines", 0)
+ removed = getattr(change, "removed_lines", 0)
+ production = "production" if bool(getattr(change, "production_applied", False)) else "workspace"
+ project_id = str(getattr(change, "project_id", ""))
+ detail_attrs = (
+ f'hx-get="/html5/projects/{quote(project_id)}/authoring/changes/{quote(change_id, safe="")}" '
+ 'hx-target="[data-html5-authoring-detail]" hx-swap="outerHTML"'
+ if change_id and project_id
+ else ""
+ )
+ return f"""
+
+ {escape(str(target_name))}
+ {escape(status)} · +{escape(str(added))} / -{escape(str(removed))} · {escape(production)}
+ {escape(task_id)} · {escape(approved_by)} · {escape(change_id)}
+
+ """
+
+
+def _authoring_changes_summary(changes: Iterable[object]) -> str:
+ change_list = list(changes)
+ statuses = Counter(str(getattr(change, "status", "") or "UNKNOWN") for change in change_list)
+ production = sum(1 for change in change_list if bool(getattr(change, "production_applied", False)))
+ workspace = len(change_list) - production
+ added = sum(int(getattr(change, "added_lines", 0) or 0) for change in change_list)
+ removed = sum(int(getattr(change, "removed_lines", 0) or 0) for change in change_list)
+ status_text = ", ".join(f"{name}: {count}" for name, count in sorted(statuses.items())) or "no changes"
+ return f"""
+
+ {escape(str(len(change_list)))} changes · {escape(status_text)} · {escape(str(workspace))} workspace · {escape(str(production))} production · +{escape(str(added))} / -{escape(str(removed))}
+
+ """
+
+
+def _authoring_recent_change(changes: Iterable[object]) -> str:
+ change_list = list(changes)
+ if not change_list:
+ return ""
+ latest = change_list[0]
+ change_id = str(getattr(latest, "change_id", ""))
+ status = str(getattr(latest, "status", "") or "UNKNOWN")
+ target = getattr(latest, "target", None)
+ target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
+ version = getattr(latest, "version", None)
+ version_id = str(getattr(latest, "version_id", "") or getattr(version, "version_id", "") or "version unavailable")
+ approved_by = str(getattr(latest, "approved_by", "") or "not approved")
+ return f"""
+
+ {escape(status)} · {escape(str(target_name))}
+ {escape(version_id)}
+ {escape(approved_by)} · {escape(change_id)}
+
+ """
+
+
+def _authoring_result_summary(state: str, diff: Iterable[object], checks: Iterable[object]) -> str:
+ diff_list = list(diff)
+ check_list = list(checks)
+ diff_kinds = Counter(str(getattr(line, "kind", "") or "CHANGE") for line in diff_list)
+ check_statuses = Counter(str(getattr(check, "status", "") or "UNKNOWN") for check in check_list)
+ added = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "ADD")
+ removed = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "REMOVE")
+ diff_text = ", ".join(f"{name}: {count}" for name, count in sorted(diff_kinds.items())) or "empty diff"
+ check_text = ", ".join(f"{name}: {count}" for name, count in sorted(check_statuses.items())) or "no checks"
+ return f"""
+
+ {escape(state)} · {escape(str(len(diff_list)))} diff lines · +{escape(str(added))} / -{escape(str(removed))} · {escape(diff_text)} · {escape(check_text)}
+
+ """
+
+
+def _authoring_detail_summary(diff: Iterable[object], checks: Iterable[object], apply_available: bool) -> str:
+ diff_list = list(diff)
+ check_list = list(checks)
+ diff_kinds = Counter(str(getattr(line, "kind", "") or "CHANGE") for line in diff_list)
+ check_statuses = Counter(str(getattr(check, "status", "") or "UNKNOWN") for check in check_list)
+ added = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "ADD")
+ removed = sum(1 for line in diff_list if str(getattr(line, "kind", "")) == "REMOVE")
+ diff_text = ", ".join(f"{name}: {count}" for name, count in sorted(diff_kinds.items())) or "empty diff"
+ check_text = ", ".join(f"{name}: {count}" for name, count in sorted(check_statuses.items())) or "no checks"
+ state = "rollback ready" if apply_available else "rollback blocked"
+ return f"""
+
+ {escape(state)} · {escape(str(len(diff_list)))} diff lines · +{escape(str(added))} / -{escape(str(removed))} · {escape(diff_text)} · {escape(check_text)}
+
+ """
+
+
+def _authoring_apply_summary(kind: str, status: str, change_id: str, version_id: str) -> str:
+ return f"""
+
+ {escape(kind)} · {escape(status or "UNKNOWN")} · {escape(change_id or "change unavailable")} · {escape(version_id or "version unavailable")}
+
+ """
+
+def _authoring_diff_item(line: object) -> str:
+ kind = str(getattr(line, "kind", ""))
+ text = str(getattr(line, "text", ""))
+ return f"""
+
+ {escape(kind)}
+ {escape(text)}
+
+ """
+
+
+def _authoring_check_item(check: object) -> str:
+ name = str(getattr(check, "name", "check"))
+ status = str(getattr(check, "status", "UNKNOWN"))
+ message = str(getattr(check, "message", ""))
+ return f"""
+
+ {escape(name)}
+ {escape(status)}
+ {escape(message)}
+
+ """
+
+
+def _authoring_rollback_form(project_id: str, change_id: str, rollback_version_id: str) -> str:
+ return f"""
+
+
+
+
+
+
+ Apply rollback
+
+ {render_html5_authoring_rollback_result(project_id)}
+ """
+
+
+def _authoring_apply_change_set_form(project_id: str, payload: dict, next_version_id: str) -> str:
+ return f"""
+
+
+
+ {escape(str(payload.get("original_text") or ""))}
+ {escape(str(payload.get("proposed_text") or ""))}
+
+
+
+
+
+
+ Apply change-set
+
+ {render_html5_authoring_apply_result(project_id)}
+ """
+
+
+def _metadata_apply_form(project_id: str, payload: dict, next_version_id: str) -> str:
+ return f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Apply metadata draft
+
+ {render_html5_metadata_apply_result(project_id)}
+ """
diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py
index 8db6242..3638438 100644
--- a/services/api-server/src/api_server/main.py
+++ b/services/api-server/src/api_server/main.py
@@ -38,17 +38,9 @@ from neo4j import AsyncGraphDatabase
from pydantic import BaseModel, Field
from api_server.html5 import (
- render_html5_authoring_apply_result,
- render_html5_authoring_changes,
- render_html5_authoring_change_detail,
- render_html5_authoring_diff_result,
- render_html5_authoring_preview_result,
- render_html5_authoring_rollback_result,
render_html5_editor,
render_html5_flowchart,
render_html5_index,
- render_html5_metadata_apply_result,
- render_html5_metadata_preview_result,
render_html5_object_context,
render_html5_object_report,
render_html5_project_rows,
@@ -59,6 +51,16 @@ from api_server.html5 import (
render_html5_status,
render_html5_symbols,
)
+from api_server.html5_authoring import (
+ render_html5_authoring_apply_result,
+ render_html5_authoring_changes,
+ render_html5_authoring_change_detail,
+ render_html5_authoring_diff_result,
+ render_html5_authoring_preview_result,
+ render_html5_authoring_rollback_result,
+ render_html5_metadata_apply_result,
+ render_html5_metadata_preview_result,
+)
from api_server.html5_operations import (
render_html5_operation_detail,
render_html5_operation_rows,