from __future__ import annotations from collections import Counter from html import escape from typing import Iterable from urllib.parse import quote from api_server.html5 import _enum_text, _metric, _page def render_html5_operations( jobs: Iterable[object], *, project_id: str = "", status: str = "", kind: str = "", ) -> str: job_list = list(jobs) filter_query = _operation_filter_query(project_id=project_id, status=status, kind=kind) return _page( "SFERA HTML5 operations", f"""

SFERA HTML5

Операции сервера

Очередь фоновых задач отрисовывается API-сервером и обновляется SSE без React runtime.

{len(job_list)} jobs

Очередь

Проекты
{_operation_filter_form(project_id=project_id, status=status, kind=kind)}
{render_html5_operation_summary(job_list)}
{render_html5_operation_rows(job_list)}
JobПроектСтатусStageСообщение
{render_html5_operation_detail(None)}
""", ) def render_html5_operation_summary(jobs: Iterable[object]) -> str: job_list = list(jobs) counts = Counter(_enum_text(getattr(job, "status", "unknown")) for job in job_list) running = counts.get("RUNNING", 0) queued = counts.get("QUEUED", 0) succeeded = counts.get("SUCCEEDED", 0) failed = counts.get("FAILED", 0) return f"""
{_metric("Всего", len(job_list))} {_metric("В работе", running)} {_metric("В очереди", queued)} {_metric("Успешно", succeeded)} {_metric("Ошибки", failed)}
""" def render_html5_operation_rows(jobs: Iterable[object]) -> str: rows = "\n".join(_operation_row(job) for job in jobs) if not rows: return 'Фоновые операции пока не запускались' return rows def render_html5_operation_detail(job: object | None) -> str: if job is None: return """
Детали операции

Выберите job в таблице, чтобы сервер отрисовал payload, result и логи.

""" job_id = str(getattr(job, "job_id", "")) kind = str(getattr(job, "kind", "")) status = _enum_text(getattr(job, "status", "")) payload = getattr(job, "payload", {}) or {} result = getattr(job, "result", {}) or {} error = str(getattr(job, "error", "") or "") logs = payload.get("logs") if isinstance(payload.get("logs"), list) else [] return f"""
Детали операции
{escape(kind)} · {escape(status)} {escape(job_id)} {escape(error or "no error")}
{_metric("Payload keys", len(payload))} {_metric("Result keys", len(result))} {_metric("Logs", len(logs))} {_metric("Updated", str(getattr(job, "updated_at", ""))[:19])}
{escape(_compact_mapping(payload))}
{escape(_compact_mapping(result))}
""" def filter_html5_operation_jobs( jobs: Iterable[object], *, project_id: str = "", status: str = "", kind: str = "", limit: int = 50, ) -> list[object]: normalized_project = project_id.strip().casefold() normalized_status = status.strip().casefold() normalized_kind = kind.strip().casefold() filtered = [] for job in jobs: payload = getattr(job, "payload", {}) or {} if normalized_project and str(payload.get("project_id") or "").casefold() != normalized_project: continue if normalized_status and _operation_value(getattr(job, "status", "")).casefold() != normalized_status: continue if normalized_kind and _operation_value(getattr(job, "kind", "")).casefold() != normalized_kind: continue filtered.append(job) return sorted(filtered, key=lambda job: getattr(job, "updated_at", ""), reverse=True)[:limit] def latest_html5_import_job(jobs: Iterable[object], project_id: str) -> object | None: import_jobs = [ job for job in jobs if (getattr(job, "payload", {}) or {}).get("project_id") == project_id and _operation_value(getattr(job, "kind", "")) == "SERVER_IMPORT" ] return max(import_jobs, key=lambda job: getattr(job, "updated_at", "")) if import_jobs else None def _operation_row(job: object) -> str: job_id = str(getattr(job, "job_id", "")) kind = str(getattr(job, "kind", "")) status = _enum_text(getattr(job, "status", "")) payload = getattr(job, "payload", {}) or {} project_id = str(payload.get("project_id") or "") stage = str(payload.get("stage") or "") message = str(payload.get("message") or getattr(job, "error", "") or "") project_link = ( f'{escape(project_id)}' if project_id else '-' ) return f""" {escape(kind)}{escape(job_id)} {project_link} {escape(status)} {escape(stage or "-")} {escape(message or "-")} """ def _compact_mapping(value: dict) -> str: if not value: return "{}" rows = [f"{key}: {value[key]}" for key in sorted(value)[:20]] return "\n".join(rows) def _operation_filter_form(*, project_id: str, status: str, kind: str) -> str: return f"""
Сброс
""" def _operation_filter_query(*, project_id: str, status: str, kind: str) -> str: params = [] if project_id: params.append(f"project_id={quote(project_id)}") if status: params.append(f"status={quote(status)}") if kind: params.append(f"kind={quote(kind)}") return f"?{'&'.join(params)}" if params else "" def _operation_value(value: object) -> str: return str(getattr(value, "value", value))