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],
|
projects: Iterable[object],
|
||||||
result: dict | None = None,
|
result: dict | None = None,
|
||||||
saved_credentials: dict[str, str] | None = None,
|
saved_credentials: dict[str, str] | None = None,
|
||||||
|
agent_info: dict[str, str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
|
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
|
||||||
return _page(
|
return _page(
|
||||||
@@ -30,6 +31,7 @@ def render_html5_ai_structure_page(
|
|||||||
</aside>
|
</aside>
|
||||||
<section class="panel setup-main">
|
<section class="panel setup-main">
|
||||||
<div class="panel-title">Подготовка структуры</div>
|
<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)}
|
{render_html5_ai_structure_form(project_id, saved_credentials=saved_credentials)}
|
||||||
<p class="object-summary">Пути должны быть доступны серверу SFERA/API. Для docker-test используйте папку, смонтированную или доступную внутри контейнера; локальные диски Windows и закрытые SMB-папки без учетных данных сервер не увидит.</p>
|
<p class="object-summary">Пути должны быть доступны серверу SFERA/API. Для docker-test используйте папку, смонтированную или доступную внутри контейнера; локальные диски Windows и закрытые SMB-папки без учетных данных сервер не увидит.</p>
|
||||||
<div data-html5-ai-structure-result>{render_html5_ai_structure_result(result)}</div>
|
<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:
|
def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[str, str] | None = None) -> str:
|
||||||
saved_credentials = saved_credentials or {}
|
saved_credentials = saved_credentials or {}
|
||||||
saved_username = str(saved_credentials.get("username") or "")
|
saved_username = str(saved_credentials.get("username") or "")
|
||||||
@@ -203,3 +237,20 @@ def _artifact_text(value: object) -> str:
|
|||||||
"normalized_project.json": "Нормализованный проект",
|
"normalized_project.json": "Нормализованный проект",
|
||||||
}
|
}
|
||||||
return mapping.get(str(value or ""), str(value or ""))
|
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_id: str,
|
||||||
project_summaries: Callable[[], Iterable[object]],
|
project_summaries: Callable[[], Iterable[object]],
|
||||||
load_credentials: Callable[[str], SmbCredentials | None] | None = None,
|
load_credentials: Callable[[str], SmbCredentials | None] | None = None,
|
||||||
|
load_agent_info: Callable[[str], dict[str, str]] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
return render_html5_ai_structure_page(
|
return render_html5_ai_structure_page(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
projects=project_summaries(),
|
projects=project_summaries(),
|
||||||
saved_credentials=load_credentials(project_id) if load_credentials else None,
|
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
|
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:
|
def _cancel_stale_extension_install_jobs(project_id: str, selected_agent_id: str) -> None:
|
||||||
now = _current_timestamp()
|
now = _current_timestamp()
|
||||||
for job in list(_agent_import_jobs.values()):
|
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_id=project_id,
|
||||||
project_summaries=_project_summaries,
|
project_summaries=_project_summaries,
|
||||||
load_credentials=_load_ai_structure_smb_credentials,
|
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
|
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):
|
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
|
||||||
first = tmp_path / "first"
|
first = tmp_path / "first"
|
||||||
second = tmp_path / "second"
|
second = tmp_path / "second"
|
||||||
|
|||||||
Reference in New Issue
Block a user