Show Windows Agent status on AI structure page
This commit is contained in:
@@ -13,6 +13,7 @@ def render_html5_ai_structure_page(
|
||||
projects: Iterable[object],
|
||||
result: dict | None = None,
|
||||
saved_credentials: dict[str, str] | None = None,
|
||||
agent_info: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
|
||||
return _page(
|
||||
@@ -30,6 +31,7 @@ def render_html5_ai_structure_page(
|
||||
</aside>
|
||||
<section class="panel setup-main">
|
||||
<div class="panel-title">Подготовка структуры</div>
|
||||
{render_html5_ai_structure_agent_panel(project_id, agent_info=agent_info)}
|
||||
{render_html5_ai_structure_form(project_id, saved_credentials=saved_credentials)}
|
||||
<p class="object-summary">Пути должны быть доступны серверу SFERA/API. Для docker-test используйте папку, смонтированную или доступную внутри контейнера; локальные диски Windows и закрытые SMB-папки без учетных данных сервер не увидит.</p>
|
||||
<div data-html5-ai-structure-result>{render_html5_ai_structure_result(result)}</div>
|
||||
@@ -40,6 +42,38 @@ def render_html5_ai_structure_page(
|
||||
)
|
||||
|
||||
|
||||
def render_html5_ai_structure_agent_panel(project_id: str, *, agent_info: dict[str, str] | None = None) -> str:
|
||||
info = agent_info or {}
|
||||
agent_id = str(info.get("agent_id") or "")
|
||||
status = str(info.get("status") or "not_configured")
|
||||
title = _agent_status_title(status)
|
||||
advice = _agent_status_advice(status, agent_id)
|
||||
details = []
|
||||
if agent_id:
|
||||
details.append(("Windows Agent", agent_id))
|
||||
if info.get("last_seen_at"):
|
||||
details.append(("Последний heartbeat", str(info["last_seen_at"])))
|
||||
if info.get("host"):
|
||||
details.append(("Хост", str(info["host"])))
|
||||
if info.get("version"):
|
||||
details.append(("Версия", str(info["version"])))
|
||||
detail_html = "".join(
|
||||
f'<article class="access-card"><strong>{escape(label)}</strong><small>{escape(value)}</small></article>'
|
||||
for label, value in details
|
||||
) or '<article class="access-card"><strong>Windows Agent</strong><small>Пока не выбран</small></article>'
|
||||
return f"""
|
||||
<section class="ai-agent-panel">
|
||||
<div class="access-plan-head">
|
||||
<span class="status-pill">{escape(title)}</span>
|
||||
<strong>Агент для CF/CFE</strong>
|
||||
</div>
|
||||
<p class="object-summary">{escape(advice)}</p>
|
||||
<div class="access-operations">{detail_html}</div>
|
||||
<p class="muted padded"><a class="button" href="/html5/projects/{quote(project_id)}/setup">Открыть setup проекта</a></p>
|
||||
</section>
|
||||
"""
|
||||
|
||||
|
||||
def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[str, str] | None = None) -> str:
|
||||
saved_credentials = saved_credentials or {}
|
||||
saved_username = str(saved_credentials.get("username") or "")
|
||||
@@ -203,3 +237,20 @@ def _artifact_text(value: object) -> str:
|
||||
"normalized_project.json": "Нормализованный проект",
|
||||
}
|
||||
return mapping.get(str(value or ""), str(value or ""))
|
||||
|
||||
|
||||
def _agent_status_title(status: str) -> str:
|
||||
mapping = {
|
||||
"online": "агент онлайн",
|
||||
"offline": "агент офлайн",
|
||||
"not_configured": "агент не настроен",
|
||||
}
|
||||
return mapping.get(status, status)
|
||||
|
||||
|
||||
def _agent_status_advice(status: str, agent_id: str) -> str:
|
||||
if status == "online":
|
||||
return f"Для бинарных .cf/.cfe будет использован Windows Agent {agent_id}. Можно запускать подготовку."
|
||||
if status == "offline":
|
||||
return f"Windows Agent {agent_id} выбран, но сейчас не отвечает. Обновите или запустите агент, затем повторите."
|
||||
return "Для прямого разбора .cf/.cfe нужен выбранный Windows Agent. Укажите его в настройках проекта."
|
||||
|
||||
@@ -25,11 +25,13 @@ def html5_ai_structure_page(
|
||||
project_id: str,
|
||||
project_summaries: Callable[[], Iterable[object]],
|
||||
load_credentials: Callable[[str], SmbCredentials | None] | None = None,
|
||||
load_agent_info: Callable[[str], dict[str, str]] | None = None,
|
||||
) -> str:
|
||||
return render_html5_ai_structure_page(
|
||||
project_id=project_id,
|
||||
projects=project_summaries(),
|
||||
saved_credentials=load_credentials(project_id) if load_credentials else None,
|
||||
agent_info=load_agent_info(project_id) if load_agent_info else None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -703,6 +703,24 @@ def _format_binary_file_list(paths: list[Path]) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def _ai_structure_agent_info(project_id: str) -> dict[str, str]:
|
||||
try:
|
||||
settings = _project_settings_or_404(project_id)
|
||||
except HTTPException:
|
||||
return {"status": "not_configured"}
|
||||
agent_id = _agent_id_for_source(settings, ImportSourceKind.CF_FILE)
|
||||
if not agent_id:
|
||||
return {"status": "not_configured"}
|
||||
status = _agent_status_with_liveness(_agent_statuses.get(agent_id, AgentStatus(agent_id=agent_id)))
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"status": str(status.status or "offline"),
|
||||
"last_seen_at": str(status.last_seen_at or ""),
|
||||
"host": str(status.host or ""),
|
||||
"version": str(status.version or ""),
|
||||
}
|
||||
|
||||
|
||||
def _cancel_stale_extension_install_jobs(project_id: str, selected_agent_id: str) -> None:
|
||||
now = _current_timestamp()
|
||||
for job in list(_agent_import_jobs.values()):
|
||||
@@ -1791,6 +1809,7 @@ async def html5_project_ai_structure(project_id: str) -> Response:
|
||||
project_id=project_id,
|
||||
project_summaries=_project_summaries,
|
||||
load_credentials=_load_ai_structure_smb_credentials,
|
||||
load_agent_info=_ai_structure_agent_info,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -2071,6 +2071,44 @@ def test_html5_ai_structure_reports_offline_agent_with_last_seen(tmp_path: Path)
|
||||
assert "Последний heartbeat" in queued.text
|
||||
|
||||
|
||||
def test_html5_ai_structure_page_shows_agent_status_panel():
|
||||
client = TestClient(app)
|
||||
project_id = f"ai-page-{uuid4()}"
|
||||
agent_id = f"win-agent-{uuid4()}"
|
||||
|
||||
settings = client.post(
|
||||
f"/projects/{project_id}/settings",
|
||||
json={"name": "AI Page", "structure_source": "CF_FILE", "agent": {"cf_agent_id": agent_id}},
|
||||
)
|
||||
assert settings.status_code == 200
|
||||
heartbeat = client.post("/agent/heartbeat", json={"agent_id": agent_id, "host": "test-host", "version": "0.2.31"})
|
||||
assert heartbeat.status_code == 200
|
||||
|
||||
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
||||
assert page.status_code == 200
|
||||
assert "Агент для CF/CFE" in page.text
|
||||
assert "агент онлайн" in page.text
|
||||
assert agent_id in page.text
|
||||
assert "test-host" in page.text
|
||||
assert "0.2.31" in page.text
|
||||
|
||||
|
||||
def test_html5_ai_structure_page_shows_missing_agent_hint():
|
||||
client = TestClient(app)
|
||||
project_id = f"ai-page-missing-{uuid4()}"
|
||||
|
||||
settings = client.post(
|
||||
f"/projects/{project_id}/settings",
|
||||
json={"name": "AI Page Missing", "structure_source": "CF_FILE"},
|
||||
)
|
||||
assert settings.status_code == 200
|
||||
|
||||
page = client.get(f"/html5/projects/{project_id}/ai-structure")
|
||||
assert page.status_code == 200
|
||||
assert "агент не настроен" in page.text
|
||||
assert "Укажите его в настройках проекта" in page.text
|
||||
|
||||
|
||||
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
|
||||
first = tmp_path / "first"
|
||||
second = tmp_path / "second"
|
||||
|
||||
Reference in New Issue
Block a user