Split HTML5 setup renderer
This commit is contained in:
@@ -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:
|
def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str:
|
||||||
return (
|
return (
|
||||||
f'<span>project: {escape(project_id)}</span>'
|
f'<span>project: {escape(project_id)}</span>'
|
||||||
@@ -1374,91 +1142,16 @@ def _topbar(project_id: str, project_nav: str) -> str:
|
|||||||
</header>"""
|
</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:
|
def _enum_text(value: object) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
return str(value.value if hasattr(value, "value") else value)
|
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:
|
def _metric(label: str, value: object) -> str:
|
||||||
return f"<div><dt>{escape(label)}</dt><dd>{escape(str(value))}</dd></div>"
|
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:
|
def _review_summary(findings: list[dict]) -> str:
|
||||||
severities = Counter(str(item.get("severity") or item.get("level") or "INFO") for item in findings)
|
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)
|
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>
|
||||||
|
"""
|
||||||
@@ -51,15 +51,10 @@ from api_server.html5 import (
|
|||||||
render_html5_metadata_preview_result,
|
render_html5_metadata_preview_result,
|
||||||
render_html5_object_context,
|
render_html5_object_context,
|
||||||
render_html5_object_report,
|
render_html5_object_report,
|
||||||
render_html5_project_setup,
|
|
||||||
render_html5_project_rows,
|
render_html5_project_rows,
|
||||||
render_html5_project_report,
|
render_html5_project_report,
|
||||||
render_html5_review,
|
render_html5_review,
|
||||||
render_html5_symbol_detail,
|
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_source,
|
||||||
render_html5_status,
|
render_html5_status,
|
||||||
render_html5_symbols,
|
render_html5_symbols,
|
||||||
@@ -70,6 +65,13 @@ from api_server.html5_operations import (
|
|||||||
render_html5_operation_summary,
|
render_html5_operation_summary,
|
||||||
render_html5_operations,
|
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 impact_engine import object_impact, routine_impact
|
||||||
from incremental_indexer import rebuild_changed_file
|
from incremental_indexer import rebuild_changed_file
|
||||||
from integration_topology import IntegrationKind, build_integration_topology
|
from integration_topology import IntegrationKind, build_integration_topology
|
||||||
|
|||||||
Reference in New Issue
Block a user