Split HTML5 setup renderer
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-17 11:08:32 +03:00
parent 0f8141d5f9
commit 624dc5d7f0
3 changed files with 321 additions and 312 deletions
-307
View File
@@ -962,238 +962,6 @@ def render_html5_authoring_rollback_result(project_id: str, result: object | Non
"""
def render_html5_project_setup(*, project_id: str, projects: Iterable[object], setup: object) -> str:
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
name = _setup_name(setup)
sources = getattr(setup, "import_sources", []) or []
source_cards = "".join(_import_source_card(source) for source in sources)
content = f"""
<main
class="workspace setup-workspace"
data-html5-page="setup"
data-project-id="{escape(project_id)}"
hx-ext="sse"
sse-connect="/html5/projects/{quote(project_id)}/setup/events"
>
{_topbar(project_id, project_nav)}
<section class="setup-layout">
<aside class="panel">
<div class="panel-title">Проект</div>
<div class="setup-card">
<p class="eyebrow">HTML5 setup</p>
<h1>{escape(name)}</h1>
<p class="muted">{escape(project_id)}</p>
<div class="setup-actions">
<a class="button" href="/html5/projects/{quote(project_id)}/editor">Открыть IDE</a>
<a class="button" href="/project-settings?project={quote(project_id)}">Legacy setup</a>
</div>
</div>
<div class="panel-title">Источники</div>
<div class="source-list">{source_cards or '<p class="muted padded">Источники импорта не настроены</p>'}</div>
</aside>
<section class="panel setup-main">
{render_html5_settings_panel(project_id, setup)}
{render_html5_setup_actions(project_id, setup)}
{render_html5_setup_summary(project_id, setup)}
</section>
</section>
</main>
"""
return _page(f"SFERA HTML5 setup - {project_id}", content)
def render_html5_settings_panel(project_id: str, setup: object, saved: bool = False) -> str:
settings = getattr(setup, "settings", None)
name = str(getattr(settings, "name", "") or "")
platform_version = str(getattr(settings, "platform_version", "") or "")
compatibility_mode = str(getattr(settings, "compatibility_mode", "") or "")
notice = '<span class="saved">Сохранено</span>' if saved else ""
return f"""
<div class="settings-panel" data-html5-settings-panel>
<div class="panel-title flush">Базовые настройки {notice}</div>
<form
class="settings-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/settings"
hx-post="/html5/projects/{quote(project_id)}/setup/settings"
hx-target="[data-html5-settings-panel]"
hx-swap="outerHTML"
>
<label>Название<input name="name" value="{escape(name)}" /></label>
<label>Платформа<input name="platform_version" value="{escape(platform_version)}" placeholder="8.3.24" /></label>
<label>Совместимость<input name="compatibility_mode" value="{escape(compatibility_mode)}" placeholder="8.3.20" /></label>
<button type="submit">Сохранить настройки</button>
</form>
</div>
"""
def render_html5_setup_actions(project_id: str, setup: object) -> str:
sources = getattr(setup, "import_sources", []) or []
current_source = _enum_text(getattr(setup, "current_source", None) or "")
source_options = "".join(_source_option(source, current_source) for source in sources)
if not source_options:
source_options = f'<option value="{escape(current_source or "XML_DUMP")}">{escape(current_source or "XML_DUMP")}</option>'
return f"""
<div class="setup-actions-panel" data-html5-setup-actions>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/source"
hx-post="/html5/projects/{quote(project_id)}/setup/source"
hx-target="[data-html5-setup-summary]"
hx-swap="outerHTML"
>
<label>Источник</label>
<select name="source">{source_options}</select>
<button type="submit">Сохранить</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/check"
hx-post="/html5/projects/{quote(project_id)}/setup/check"
hx-target="[data-html5-import-check]"
hx-swap="outerHTML"
>
<button type="submit">Проверить</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/import-job"
hx-post="/html5/projects/{quote(project_id)}/setup/import-job"
hx-target="[data-html5-import-job]"
hx-swap="outerHTML"
>
<button type="submit">Импорт в фоне</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/import"
hx-post="/html5/projects/{quote(project_id)}/setup/import"
hx-target="[data-html5-setup-summary]"
hx-swap="outerHTML"
>
<button type="submit">Запустить импорт</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/reindex"
hx-post="/html5/projects/{quote(project_id)}/setup/reindex"
hx-target="[data-html5-setup-summary]"
hx-swap="outerHTML"
>
<button type="submit">Переиндексировать</button>
</form>
</div>
{render_html5_import_check(project_id)}
{render_html5_import_job(project_id)}
"""
def render_html5_import_check(project_id: str, check: object | None = None) -> str:
if check is None:
return f"""
<div class="import-check" data-html5-import-check>
<div class="panel-title flush">Проверка импорта</div>
<p class="muted padded">Запустите server-side preflight перед импортом проекта {escape(project_id)}.</p>
</div>
"""
status = str(getattr(check, "status", "UNKNOWN"))
source = _enum_text(getattr(check, "source", ""))
ready = bool(getattr(check, "ready", False))
checks = getattr(check, "checks", []) or []
items = "".join(_preflight_item(item) for item in checks)
return f"""
<div class="import-check" data-html5-import-check>
<div class="panel-title flush">Проверка импорта</div>
<div class="check-head">
<strong>{escape(status)}</strong>
<span>{escape(source)}</span>
<small>{'ready' if ready else 'needs attention'}</small>
</div>
<div class="check-list">{items or '<p class="muted padded">Проверки не вернули результатов</p>'}</div>
</div>
"""
def render_html5_import_job(project_id: str, job: object | None = None) -> str:
if job is None:
return f"""
<div class="import-job" data-html5-import-job sse-swap="setup-import-job" hx-swap="outerHTML">
<div class="panel-title flush">Фоновый импорт</div>
<p class="muted padded">Фоновая задача импорта для проекта {escape(project_id)} еще не запускалась.</p>
</div>
"""
job_id = str(getattr(job, "job_id", ""))
status = _enum_text(getattr(job, "status", "unknown"))
payload = getattr(job, "payload", {}) or {}
message = str(payload.get("message") or "")
source = str(payload.get("source") or "")
stage = str(payload.get("stage") or "")
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
logs_html = "".join(f"<li>{escape(str(item))}</li>" for item in logs[-6:])
return f"""
<div
class="import-job"
data-html5-import-job
hx-get="/html5/projects/{quote(project_id)}/setup/jobs/{quote(job_id)}"
sse-swap="setup-import-job"
hx-swap="outerHTML"
>
<div class="panel-title flush">Фоновый импорт</div>
<div class="check-head">
<strong>{escape(status)}</strong>
<span>{escape(source)}</span>
<small>{escape(stage or job_id)}</small>
</div>
<p class="muted padded">{escape(message or "Ожидание обновления статуса")}</p>
<ul class="job-log">{logs_html or '<li>Лог пока пустой</li>'}</ul>
</div>
"""
def render_html5_setup_summary(project_id: str, setup: object) -> str:
status = _enum_text(getattr(setup, "status", "unknown"))
message = str(getattr(setup, "message", ""))
current_source = _enum_text(getattr(setup, "current_source", None) or "не выбран")
last_import = getattr(setup, "last_import", None)
history = getattr(setup, "import_history", []) or []
return f"""
<div
class="setup-summary"
data-html5-setup-summary
hx-get="/html5/projects/{quote(project_id)}/setup/summary"
sse-swap="setup-summary"
hx-swap="outerHTML"
>
<div class="section-title">
<div>
<p class="eyebrow">Server-rendered status</p>
<h2>{escape(status)}</h2>
</div>
<span class="status-pill">{escape(current_source)}</span>
</div>
<p class="lead compact-lead">{escape(message)}</p>
<dl class="setup-metrics">
{_metric("Объекты", _import_value(last_import, "object_count"))}
{_metric("Модули", _import_value(last_import, "module_count"))}
{_metric("Формы", _import_value(last_import, "form_count"))}
{_metric("Роли", _import_value(last_import, "role_count"))}
</dl>
<div class="panel-title flush">Последняя загрузка</div>
{_last_import_block(last_import)}
<div class="panel-title flush">История</div>
<div class="history-list">
{''.join(_history_item(item) for item in history[:6]) or '<p class="muted padded">История импорта пока пустая</p>'}
</div>
</div>
"""
def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str:
return (
f'<span>project: {escape(project_id)}</span>'
@@ -1374,91 +1142,16 @@ def _topbar(project_id: str, project_nav: str) -> str:
</header>"""
def _setup_name(setup: object) -> str:
settings = getattr(setup, "settings", None)
return str(getattr(settings, "name", None) or getattr(setup, "project_id", "SFERA Project"))
def _enum_text(value: object) -> str:
if value is None:
return ""
return str(value.value if hasattr(value, "value") else value)
def _import_value(import_summary: object | None, field: str) -> int | str:
if import_summary is None:
return "0"
return getattr(import_summary, field, 0)
def _metric(label: str, value: object) -> str:
return f"<div><dt>{escape(label)}</dt><dd>{escape(str(value))}</dd></div>"
def _last_import_block(import_summary: object | None) -> str:
if import_summary is None:
return '<p class="muted padded">Загрузка еще не выполнялась</p>'
source = _enum_text(getattr(import_summary, "source", ""))
status = str(getattr(import_summary, "status", ""))
source_path = str(getattr(import_summary, "source_path", "") or "source path unavailable")
runtime = str(getattr(import_summary, "runtime_mode", "") or "runtime unavailable")
return f"""
<div class="setup-detail" data-html5-last-import>
<strong>{escape(status)}</strong>
<span>{escape(source)} · {escape(runtime)}</span>
<small>{escape(source_path)}</small>
</div>
"""
def _history_item(item: object) -> str:
source = _enum_text(getattr(item, "source", ""))
status = str(getattr(item, "status", ""))
objects = getattr(item, "object_count", 0)
modules = getattr(item, "module_count", 0)
return f"""
<article class="history-item" data-html5-import-history>
<strong>{escape(status)}</strong>
<span>{escape(source)}</span>
<small>{escape(str(objects))} objects · {escape(str(modules))} modules</small>
</article>
"""
def _import_source_card(source: object) -> str:
kind = _enum_text(getattr(source, "kind", ""))
title = str(getattr(source, "title", kind))
description = str(getattr(source, "description", ""))
readiness = str(getattr(source, "readiness", ""))
return f"""
<article class="source-card" data-html5-import-source="{escape(kind)}">
<strong>{escape(title)}</strong>
<span>{escape(kind)}</span>
<small>{escape(readiness or description)}</small>
</article>
"""
def _source_option(source: object, current_source: str) -> str:
kind = _enum_text(getattr(source, "kind", ""))
title = str(getattr(source, "title", kind))
selected = " selected" if kind == current_source else ""
return f'<option value="{escape(kind)}"{selected}>{escape(title)} · {escape(kind)}</option>'
def _preflight_item(item: object) -> str:
title = str(getattr(item, "title", "Check"))
status = str(getattr(item, "status", "UNKNOWN"))
message = str(getattr(item, "message", ""))
return f"""
<article class="check-item" data-html5-preflight-check="{escape(status)}">
<strong>{escape(title)}</strong>
<span>{escape(status)}</span>
<small>{escape(message)}</small>
</article>
"""
def _review_summary(findings: list[dict]) -> str:
severities = Counter(str(item.get("severity") or item.get("level") or "INFO") for item in findings)
titles = Counter(str(item.get("title") or item.get("code") or "Finding") for item in findings)
@@ -0,0 +1,314 @@
from __future__ import annotations
from html import escape
from typing import Iterable
from urllib.parse import quote
from api_server.html5 import _enum_text, _metric, _page, _project_link, _topbar
def render_html5_project_setup(*, project_id: str, projects: Iterable[object], setup: object) -> str:
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
name = _setup_name(setup)
sources = getattr(setup, "import_sources", []) or []
source_cards = "".join(_import_source_card(source) for source in sources)
content = f"""
<main
class="workspace setup-workspace"
data-html5-page="setup"
data-project-id="{escape(project_id)}"
hx-ext="sse"
sse-connect="/html5/projects/{quote(project_id)}/setup/events"
>
{_topbar(project_id, project_nav)}
<section class="setup-layout">
<aside class="panel">
<div class="panel-title">Проект</div>
<div class="setup-card">
<p class="eyebrow">HTML5 setup</p>
<h1>{escape(name)}</h1>
<p class="muted">{escape(project_id)}</p>
<div class="setup-actions">
<a class="button" href="/html5/projects/{quote(project_id)}/editor">Открыть IDE</a>
<a class="button" href="/project-settings?project={quote(project_id)}">Legacy setup</a>
</div>
</div>
<div class="panel-title">Источники</div>
<div class="source-list">{source_cards or '<p class="muted padded">Источники импорта не настроены</p>'}</div>
</aside>
<section class="panel setup-main">
{render_html5_settings_panel(project_id, setup)}
{render_html5_setup_actions(project_id, setup)}
{render_html5_setup_summary(project_id, setup)}
</section>
</section>
</main>
"""
return _page(f"SFERA HTML5 setup - {project_id}", content)
def render_html5_settings_panel(project_id: str, setup: object, saved: bool = False) -> str:
settings = getattr(setup, "settings", None)
name = str(getattr(settings, "name", "") or "")
platform_version = str(getattr(settings, "platform_version", "") or "")
compatibility_mode = str(getattr(settings, "compatibility_mode", "") or "")
notice = '<span class="saved">Сохранено</span>' if saved else ""
return f"""
<div class="settings-panel" data-html5-settings-panel>
<div class="panel-title flush">Базовые настройки {notice}</div>
<form
class="settings-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/settings"
hx-post="/html5/projects/{quote(project_id)}/setup/settings"
hx-target="[data-html5-settings-panel]"
hx-swap="outerHTML"
>
<label>Название<input name="name" value="{escape(name)}" /></label>
<label>Платформа<input name="platform_version" value="{escape(platform_version)}" placeholder="8.3.24" /></label>
<label>Совместимость<input name="compatibility_mode" value="{escape(compatibility_mode)}" placeholder="8.3.20" /></label>
<button type="submit">Сохранить настройки</button>
</form>
</div>
"""
def render_html5_setup_actions(project_id: str, setup: object) -> str:
sources = getattr(setup, "import_sources", []) or []
current_source = _enum_text(getattr(setup, "current_source", None) or "")
source_options = "".join(_source_option(source, current_source) for source in sources)
if not source_options:
source_options = f'<option value="{escape(current_source or "XML_DUMP")}">{escape(current_source or "XML_DUMP")}</option>'
return f"""
<div class="setup-actions-panel" data-html5-setup-actions>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/source"
hx-post="/html5/projects/{quote(project_id)}/setup/source"
hx-target="[data-html5-setup-summary]"
hx-swap="outerHTML"
>
<label>Источник</label>
<select name="source">{source_options}</select>
<button type="submit">Сохранить</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/check"
hx-post="/html5/projects/{quote(project_id)}/setup/check"
hx-target="[data-html5-import-check]"
hx-swap="outerHTML"
>
<button type="submit">Проверить</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/import-job"
hx-post="/html5/projects/{quote(project_id)}/setup/import-job"
hx-target="[data-html5-import-job]"
hx-swap="outerHTML"
>
<button type="submit">Импорт в фоне</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/import"
hx-post="/html5/projects/{quote(project_id)}/setup/import"
hx-target="[data-html5-setup-summary]"
hx-swap="outerHTML"
>
<button type="submit">Запустить импорт</button>
</form>
<form
class="inline-form"
method="post"
action="/html5/projects/{quote(project_id)}/setup/reindex"
hx-post="/html5/projects/{quote(project_id)}/setup/reindex"
hx-target="[data-html5-setup-summary]"
hx-swap="outerHTML"
>
<button type="submit">Переиндексировать</button>
</form>
</div>
{render_html5_import_check(project_id)}
{render_html5_import_job(project_id)}
"""
def render_html5_import_check(project_id: str, check: object | None = None) -> str:
if check is None:
return f"""
<div class="import-check" data-html5-import-check>
<div class="panel-title flush">Проверка импорта</div>
<p class="muted padded">Запустите server-side preflight перед импортом проекта {escape(project_id)}.</p>
</div>
"""
status = str(getattr(check, "status", "UNKNOWN"))
source = _enum_text(getattr(check, "source", ""))
ready = bool(getattr(check, "ready", False))
checks = getattr(check, "checks", []) or []
items = "".join(_preflight_item(item) for item in checks)
return f"""
<div class="import-check" data-html5-import-check>
<div class="panel-title flush">Проверка импорта</div>
<div class="check-head">
<strong>{escape(status)}</strong>
<span>{escape(source)}</span>
<small>{'ready' if ready else 'needs attention'}</small>
</div>
<div class="check-list">{items or '<p class="muted padded">Проверки не вернули результатов</p>'}</div>
</div>
"""
def render_html5_import_job(project_id: str, job: object | None = None) -> str:
if job is None:
return f"""
<div class="import-job" data-html5-import-job sse-swap="setup-import-job" hx-swap="outerHTML">
<div class="panel-title flush">Фоновый импорт</div>
<p class="muted padded">Фоновая задача импорта для проекта {escape(project_id)} еще не запускалась.</p>
</div>
"""
job_id = str(getattr(job, "job_id", ""))
status = _enum_text(getattr(job, "status", "unknown"))
payload = getattr(job, "payload", {}) or {}
message = str(payload.get("message") or "")
source = str(payload.get("source") or "")
stage = str(payload.get("stage") or "")
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
logs_html = "".join(f"<li>{escape(str(item))}</li>" for item in logs[-6:])
return f"""
<div
class="import-job"
data-html5-import-job
hx-get="/html5/projects/{quote(project_id)}/setup/jobs/{quote(job_id)}"
sse-swap="setup-import-job"
hx-swap="outerHTML"
>
<div class="panel-title flush">Фоновый импорт</div>
<div class="check-head">
<strong>{escape(status)}</strong>
<span>{escape(source)}</span>
<small>{escape(stage or job_id)}</small>
</div>
<p class="muted padded">{escape(message or "Ожидание обновления статуса")}</p>
<ul class="job-log">{logs_html or '<li>Лог пока пустой</li>'}</ul>
</div>
"""
def render_html5_setup_summary(project_id: str, setup: object) -> str:
status = _enum_text(getattr(setup, "status", "unknown"))
message = str(getattr(setup, "message", ""))
current_source = _enum_text(getattr(setup, "current_source", None) or "не выбран")
last_import = getattr(setup, "last_import", None)
history = getattr(setup, "import_history", []) or []
return f"""
<div
class="setup-summary"
data-html5-setup-summary
hx-get="/html5/projects/{quote(project_id)}/setup/summary"
sse-swap="setup-summary"
hx-swap="outerHTML"
>
<div class="section-title">
<div>
<p class="eyebrow">Server-rendered status</p>
<h2>{escape(status)}</h2>
</div>
<span class="status-pill">{escape(current_source)}</span>
</div>
<p class="lead compact-lead">{escape(message)}</p>
<dl class="setup-metrics">
{_metric("Объекты", _import_value(last_import, "object_count"))}
{_metric("Модули", _import_value(last_import, "module_count"))}
{_metric("Формы", _import_value(last_import, "form_count"))}
{_metric("Роли", _import_value(last_import, "role_count"))}
</dl>
<div class="panel-title flush">Последняя загрузка</div>
{_last_import_block(last_import)}
<div class="panel-title flush">История</div>
<div class="history-list">
{''.join(_history_item(item) for item in history[:6]) or '<p class="muted padded">История импорта пока пустая</p>'}
</div>
</div>
"""
def _setup_name(setup: object) -> str:
settings = getattr(setup, "settings", None)
return str(getattr(settings, "name", None) or getattr(setup, "project_id", "SFERA Project"))
def _import_value(import_summary: object | None, field: str) -> int | str:
if import_summary is None:
return "0"
return getattr(import_summary, field, 0)
def _last_import_block(import_summary: object | None) -> str:
if import_summary is None:
return '<p class="muted padded">Загрузка еще не выполнялась</p>'
source = _enum_text(getattr(import_summary, "source", ""))
status = str(getattr(import_summary, "status", ""))
source_path = str(getattr(import_summary, "source_path", "") or "source path unavailable")
runtime = str(getattr(import_summary, "runtime_mode", "") or "runtime unavailable")
return f"""
<div class="setup-detail" data-html5-last-import>
<strong>{escape(status)}</strong>
<span>{escape(source)} · {escape(runtime)}</span>
<small>{escape(source_path)}</small>
</div>
"""
def _history_item(item: object) -> str:
source = _enum_text(getattr(item, "source", ""))
status = str(getattr(item, "status", ""))
objects = getattr(item, "object_count", 0)
modules = getattr(item, "module_count", 0)
return f"""
<article class="history-item" data-html5-import-history>
<strong>{escape(status)}</strong>
<span>{escape(source)}</span>
<small>{escape(str(objects))} objects · {escape(str(modules))} modules</small>
</article>
"""
def _import_source_card(source: object) -> str:
kind = _enum_text(getattr(source, "kind", ""))
title = str(getattr(source, "title", kind))
description = str(getattr(source, "description", ""))
readiness = str(getattr(source, "readiness", ""))
return f"""
<article class="source-card" data-html5-import-source="{escape(kind)}">
<strong>{escape(title)}</strong>
<span>{escape(kind)}</span>
<small>{escape(readiness or description)}</small>
</article>
"""
def _source_option(source: object, current_source: str) -> str:
kind = _enum_text(getattr(source, "kind", ""))
title = str(getattr(source, "title", kind))
selected = " selected" if kind == current_source else ""
return f'<option value="{escape(kind)}"{selected}>{escape(title)} · {escape(kind)}</option>'
def _preflight_item(item: object) -> str:
title = str(getattr(item, "title", "Check"))
status = str(getattr(item, "status", "UNKNOWN"))
message = str(getattr(item, "message", ""))
return f"""
<article class="check-item" data-html5-preflight-check="{escape(status)}">
<strong>{escape(title)}</strong>
<span>{escape(status)}</span>
<small>{escape(message)}</small>
</article>
"""
+7 -5
View File
@@ -51,15 +51,10 @@ from api_server.html5 import (
render_html5_metadata_preview_result,
render_html5_object_context,
render_html5_object_report,
render_html5_project_setup,
render_html5_project_rows,
render_html5_project_report,
render_html5_review,
render_html5_symbol_detail,
render_html5_import_check,
render_html5_import_job,
render_html5_settings_panel,
render_html5_setup_summary,
render_html5_source,
render_html5_status,
render_html5_symbols,
@@ -70,6 +65,13 @@ from api_server.html5_operations import (
render_html5_operation_summary,
render_html5_operations,
)
from api_server.html5_setup import (
render_html5_import_check,
render_html5_import_job,
render_html5_project_setup,
render_html5_settings_panel,
render_html5_setup_summary,
)
from impact_engine import object_impact, routine_impact
from incremental_indexer import rebuild_changed_file
from integration_topology import IntegrationKind, build_integration_topology