730 lines
26 KiB
Python
730 lines
26 KiB
Python
"""WESP-shaped admin endpoints for copied static UI (/api/admin/*)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from fastapi.responses import FileResponse
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import func, select
|
|
|
|
from app.core.config import settings
|
|
from app.core.server_logging import ensure_server_log_file
|
|
from app.core.database import session_scope
|
|
from app.core.dependencies import require_superuser
|
|
from app.core.audit_log import read_audit_events, write_audit_event
|
|
from app.modules.admin.service import (
|
|
create_admin_user,
|
|
delete_admin_user,
|
|
get_diagnostics_report,
|
|
get_server_log_tail,
|
|
patch_user,
|
|
reset_user_password,
|
|
)
|
|
from app.modules.zootech.orchestrator_system_metrics import collect_orchestrator_system_metrics
|
|
from app.modules.sync.models import Enterprise, FarmHub, SyncConflict, SyncOutbox
|
|
from app.modules.sync.service import get_sync_metrics
|
|
from app.modules.users import repository as users_repository
|
|
from app.modules.users.models import User
|
|
from app.modules.zootech.catalog_models import ZootechFeedDispenser, ZootechFeedingPeriod
|
|
from app.modules.zootech.models import ZootechComponent, ZootechIngredient, ZootechRecipe
|
|
|
|
router = APIRouter(prefix="/admin", tags=["zootech-wesp-admin"])
|
|
|
|
|
|
def _wesp_error(message: str, code: int = 400) -> None:
|
|
raise HTTPException(status_code=code, detail={"status": "error", "message": message})
|
|
|
|
|
|
def _count_zootech(model) -> int:
|
|
with session_scope() as db:
|
|
return (
|
|
db.scalar(select(func.count()).select_from(model).where(model.is_deleted.is_(False))) or 0
|
|
)
|
|
|
|
|
|
def _sync_queue_stats() -> dict[str, int]:
|
|
with session_scope() as db:
|
|
total = db.scalar(select(func.count()).select_from(SyncOutbox)) or 0
|
|
pending = (
|
|
db.scalar(
|
|
select(func.count()).select_from(SyncOutbox).where(SyncOutbox.status == "pending")
|
|
)
|
|
or 0
|
|
)
|
|
processing = (
|
|
db.scalar(
|
|
select(func.count())
|
|
.select_from(SyncOutbox)
|
|
.where(SyncOutbox.status == "processing")
|
|
)
|
|
or 0
|
|
)
|
|
failed = (
|
|
db.scalar(
|
|
select(func.count()).select_from(SyncOutbox).where(SyncOutbox.status == "failed")
|
|
)
|
|
or 0
|
|
)
|
|
return {
|
|
"total": int(total),
|
|
"pending": int(pending),
|
|
"processing": int(processing),
|
|
"failed": int(failed),
|
|
}
|
|
|
|
|
|
def _user_to_wesp(user: User) -> dict[str, Any]:
|
|
return {
|
|
"id": user.id,
|
|
"login": user.email,
|
|
"is_superuser": bool(user.is_superuser),
|
|
"lab_access": False,
|
|
"created_at": user.created_at.isoformat() if user.created_at else None,
|
|
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
|
|
}
|
|
|
|
|
|
class WespUiActivityIn(BaseModel):
|
|
text: str = Field(min_length=1)
|
|
level: str = "ok"
|
|
|
|
|
|
class WespUserCreateIn(BaseModel):
|
|
login: str = Field(min_length=1)
|
|
password: str = Field(min_length=8)
|
|
is_superuser: bool = False
|
|
|
|
|
|
class WespUserPatchIn(BaseModel):
|
|
password: str | None = None
|
|
is_superuser: bool | None = None
|
|
lab_access: bool | None = None
|
|
|
|
|
|
class WespNetworkPatchIn(BaseModel):
|
|
local_hostname: str | None = None
|
|
public_base_url: str | None = None
|
|
mdns_enabled: bool | None = None
|
|
|
|
|
|
class WespOrchestratorSyncPatchIn(BaseModel):
|
|
upstream_url: str | None = None
|
|
hub_site_id: str | None = None
|
|
api_key: str | None = None
|
|
|
|
|
|
@router.get("/users")
|
|
def wesp_list_users(_admin: User = Depends(require_superuser)):
|
|
users, _total = users_repository.list_users(page=1, limit=500)
|
|
payload = {"status": "success", "users": [_user_to_wesp(u) for u in users]}
|
|
return payload
|
|
|
|
|
|
@router.post("/users", status_code=201)
|
|
def wesp_create_user(payload: WespUserCreateIn, admin: User = Depends(require_superuser)):
|
|
email = payload.login.strip()
|
|
if "@" not in email:
|
|
_wesp_error("Используйте email в поле логина (admin@compton.example)")
|
|
try:
|
|
create_admin_user(
|
|
admin,
|
|
email=email,
|
|
password=payload.password,
|
|
role="admin" if payload.is_superuser else "user",
|
|
is_superuser=payload.is_superuser,
|
|
status="active",
|
|
)
|
|
except ValueError as exc:
|
|
if str(exc) == "USER_EXISTS":
|
|
_wesp_error("Пользователь с таким логином уже существует", 409)
|
|
_wesp_error(str(exc))
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.patch("/users/{user_id}")
|
|
def wesp_patch_user(user_id: str, payload: WespUserPatchIn, admin: User = Depends(require_superuser)):
|
|
if payload.password:
|
|
try:
|
|
reset_user_password(admin, user_id, payload.password)
|
|
except ValueError as exc:
|
|
_wesp_error(str(exc), 404 if str(exc) == "USER_NOT_FOUND" else 400)
|
|
return {"status": "success"}
|
|
if payload.is_superuser is not None:
|
|
try:
|
|
patch_user(admin, user_id, None, None, payload.is_superuser)
|
|
except ValueError as exc:
|
|
_wesp_error(str(exc), 404 if str(exc) == "USER_NOT_FOUND" else 400)
|
|
return {"status": "success"}
|
|
if payload.lab_access is not None:
|
|
return {"status": "success"}
|
|
_wesp_error("Нет полей для обновления")
|
|
|
|
|
|
@router.delete("/users/{user_id}")
|
|
def wesp_delete_user(user_id: str, admin: User = Depends(require_superuser)):
|
|
try:
|
|
delete_admin_user(admin, user_id)
|
|
except ValueError as exc:
|
|
_wesp_error(str(exc), 404 if str(exc) == "USER_NOT_FOUND" else 400)
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/summary")
|
|
def wesp_admin_summary(_admin: User = Depends(require_superuser)):
|
|
queue = _sync_queue_stats()
|
|
with session_scope() as db:
|
|
enterprises = list(db.scalars(select(Enterprise).order_by(Enterprise.name)))
|
|
hub_count = db.scalar(select(func.count()).select_from(FarmHub)) or 0
|
|
enterprise_ids = [e.id for e in enterprises]
|
|
|
|
conflicts_pending = 0
|
|
for enterprise_id in enterprise_ids:
|
|
conflicts_pending += int(get_sync_metrics(enterprise_id).get("conflicts_pending") or 0)
|
|
|
|
payload = {
|
|
"status": "success",
|
|
"generated_at": datetime.now(UTC).isoformat(),
|
|
"app": {
|
|
"config_mode": settings.app_env,
|
|
"debug": settings.app_env != "production",
|
|
"version": "1.0.0-orchestrator",
|
|
"role": "server",
|
|
"first_launch_at": None,
|
|
"warranty_until": None,
|
|
"warranty_days_remaining": None,
|
|
},
|
|
"databases": {
|
|
"recipes": {"engine": "postgresql", "path": "orchestrator"},
|
|
"reports": {"engine": "postgresql", "path": "orchestrator"},
|
|
},
|
|
"machine": {"platform": "orchestrator"},
|
|
"counts": {
|
|
"users": users_repository.count_users(),
|
|
"recipes": _count_zootech(ZootechRecipe),
|
|
"ingredients": _count_zootech(ZootechIngredient),
|
|
"components": _count_zootech(ZootechComponent),
|
|
"feed_dispensers": _count_zootech(ZootechFeedDispenser),
|
|
"feeding_periods": _count_zootech(ZootechFeedingPeriod),
|
|
},
|
|
"sync": {
|
|
"client_role": "server",
|
|
"connection": {
|
|
"role": "server",
|
|
"server_url": settings.public_base_url.rstrip("/"),
|
|
"configured": True,
|
|
},
|
|
"max_concurrent": 4,
|
|
"queue": queue,
|
|
"client_poll_interval_sec": 14,
|
|
"first_sync_banner": False,
|
|
"initial_sync_progress": {"active": False},
|
|
"guarantees": {
|
|
"master_data": "Orchestrator — источник правды для предприятий и хабов.",
|
|
"reports": "Отчёты принимаются через sync API.",
|
|
},
|
|
"state": {"hub_count": int(hub_count), "enterprise_count": len(enterprise_ids)},
|
|
"queue_errors": [],
|
|
},
|
|
"security": {
|
|
"mac_lock_enabled": False,
|
|
"calibration_public": False,
|
|
"device_token_configured": False,
|
|
"kiosk_enforce_paired_only": False,
|
|
},
|
|
"auto_update": {"enabled": False, "note": "Обновления управляются деплоем orchestrator."},
|
|
"hardware": {"simulation_mode": True, "note": "Железо недоступно на orchestrator."},
|
|
}
|
|
return payload
|
|
|
|
|
|
@router.get("/system-metrics")
|
|
def wesp_system_metrics(_admin: User = Depends(require_superuser)):
|
|
metrics = collect_orchestrator_system_metrics()
|
|
return {"status": "success", "metrics": metrics}
|
|
|
|
|
|
@router.get("/factory-reset/preview")
|
|
def wesp_factory_reset_preview(_admin: User = Depends(require_superuser)):
|
|
return {
|
|
"status": "success",
|
|
"confirm_phrase": "СБРОС ДАННЫХ",
|
|
"preserved_note": "Сброс data/ недоступен на orchestrator — данные в PostgreSQL и S3.",
|
|
"files": [],
|
|
"directories": [],
|
|
}
|
|
|
|
|
|
@router.get("/network-settings")
|
|
def wesp_network_settings_get(_admin: User = Depends(require_superuser)):
|
|
public_url = settings.public_base_url.rstrip("/")
|
|
return {
|
|
"status": "success",
|
|
"network": {
|
|
"local_hostname": "orchestrator.local",
|
|
"default_local_hostname": "orchestrator.local",
|
|
"public_base_url": public_url,
|
|
"mdns_enabled": False,
|
|
"local_hostname_locked_by_env": True,
|
|
"public_base_url_locked_by_env": False,
|
|
"mdns_enabled_locked_by_env": True,
|
|
"detected": {
|
|
"listen_port": 5173,
|
|
"listen_host": "0.0.0.0",
|
|
"os_hostname": "orchestrator",
|
|
"mdns_available": False,
|
|
"mdns_active": False,
|
|
},
|
|
"suggested_public_url": public_url,
|
|
"sync_url_by_ip": public_url,
|
|
},
|
|
}
|
|
|
|
|
|
@router.patch("/network-settings")
|
|
def wesp_network_settings_patch(_payload: WespNetworkPatchIn, _admin: User = Depends(require_superuser)):
|
|
return {"status": "success", "message": "Сетевые настройки orchestrator задаются через PUBLIC_BASE_URL / nginx."}
|
|
|
|
|
|
@router.get("/peripheral-events")
|
|
def wesp_peripheral_events(
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
component: str | None = None,
|
|
exclude_component: str | None = None,
|
|
_admin: User = Depends(require_superuser),
|
|
):
|
|
_ = (limit, component, exclude_component)
|
|
return {"status": "success", "events": []}
|
|
|
|
|
|
@router.get("/sync-diagnostics")
|
|
def wesp_sync_diagnostics(_admin: User = Depends(require_superuser)):
|
|
queue = _sync_queue_stats()
|
|
with session_scope() as db:
|
|
enterprises = list(db.scalars(select(Enterprise).order_by(Enterprise.name)))
|
|
hubs = list(db.scalars(select(FarmHub).order_by(FarmHub.created_at.desc())))
|
|
conflicts_total = db.scalar(select(func.count()).select_from(SyncConflict)) or 0
|
|
conflicts_pending = (
|
|
db.scalar(
|
|
select(func.count())
|
|
.select_from(SyncConflict)
|
|
.where(SyncConflict.status == "pending")
|
|
)
|
|
or 0
|
|
)
|
|
hub_rows = [
|
|
{
|
|
"farm_hub_id": h.id,
|
|
"name": h.name,
|
|
"hub_site_id": h.hub_site_id,
|
|
"status": h.status,
|
|
"last_seen": h.last_seen.isoformat() if h.last_seen else None,
|
|
"enterprise_id": h.enterprise_id,
|
|
}
|
|
for h in hubs
|
|
]
|
|
enterprise_rows = [{"id": e.id, "name": e.name, "slug": e.slug} for e in enterprises]
|
|
|
|
for row in hub_rows:
|
|
row.update(get_sync_metrics(row.pop("enterprise_id")))
|
|
|
|
payload = {
|
|
"status": "success",
|
|
"role": "server",
|
|
"queues": {
|
|
"summary": queue,
|
|
"by_table": {},
|
|
"stuck_processing": [],
|
|
"processing_missing_processed_at": [],
|
|
"failed_recent": [],
|
|
"high_retry_pending": [],
|
|
},
|
|
"clients": {"recent_deliveries": []},
|
|
"conflicts": {
|
|
"summary": {"total": int(conflicts_total), "pending": int(conflicts_pending)},
|
|
"recent": [],
|
|
},
|
|
"report_push": {
|
|
"scope": "orchestrator",
|
|
"note": "Push отчётов обрабатывается через sync engine.",
|
|
"total": 0,
|
|
},
|
|
"engine_state": {
|
|
"enterprises": enterprise_rows,
|
|
"hubs": hub_rows,
|
|
},
|
|
"initial_sync_progress": {"active": False},
|
|
"actions_capabilities": {
|
|
"requeue_stuck": False,
|
|
"restart_local_sync": False,
|
|
"refresh_diagnostics": True,
|
|
"retry_report_push": False,
|
|
},
|
|
}
|
|
return payload
|
|
|
|
|
|
@router.get("/orchestrator-sync")
|
|
def wesp_orchestrator_sync_get(_admin: User = Depends(require_superuser)):
|
|
with session_scope() as db:
|
|
hub = db.scalar(select(FarmHub).order_by(FarmHub.created_at.desc()))
|
|
hub_site_id = hub.hub_site_id if hub else ""
|
|
return {
|
|
"status": "success",
|
|
"orchestrator_sync": {
|
|
"upstream_url": settings.public_base_url.rstrip("/"),
|
|
"hub_site_id": hub_site_id,
|
|
},
|
|
}
|
|
|
|
|
|
@router.patch("/orchestrator-sync")
|
|
def wesp_orchestrator_sync_patch(_payload: WespOrchestratorSyncPatchIn, _admin: User = Depends(require_superuser)):
|
|
return {
|
|
"status": "success",
|
|
"message": "Параметры hub↔orchestrator настраиваются через pairing, не из этой формы.",
|
|
}
|
|
|
|
|
|
@router.get("/activity-feed")
|
|
def wesp_activity_feed(limit: int = Query(default=200, ge=1, le=1000), _admin: User = Depends(require_superuser)):
|
|
entries: list[dict[str, Any]] = []
|
|
for event in read_audit_events(limit=limit):
|
|
ts_raw = event.get("timestamp")
|
|
ts_ms = 0
|
|
if ts_raw:
|
|
try:
|
|
ts_ms = int(datetime.fromisoformat(str(ts_raw).replace("Z", "+00:00")).timestamp() * 1000)
|
|
except ValueError:
|
|
ts_ms = int(time.time() * 1000)
|
|
details = event.get("details") or {}
|
|
text = str(details.get("text") or event.get("action") or "")
|
|
level = str(details.get("level") or "ok")
|
|
if level not in {"ok", "err", "warn"}:
|
|
level = "ok"
|
|
entries.append({"ts": ts_ms, "level": level, "text": text})
|
|
return {"status": "success", "entries": entries, "meta": "orchestrator-audit"}
|
|
|
|
|
|
@router.post("/ui-activity")
|
|
def wesp_ui_activity(payload: WespUiActivityIn, admin: User = Depends(require_superuser)):
|
|
level = payload.level if payload.level in {"ok", "err", "warn"} else "ok"
|
|
write_audit_event(
|
|
action="ui.activity",
|
|
actor_user_id=admin.id,
|
|
actor_email=admin.email,
|
|
details={"text": payload.text, "level": level},
|
|
)
|
|
return {"status": "success"}
|
|
|
|
|
|
@router.get("/diagnostics/report")
|
|
def wesp_diagnostics_report(_admin: User = Depends(require_superuser)):
|
|
report = get_diagnostics_report()
|
|
return {"status": "success", **report}
|
|
|
|
|
|
def _security_settings_payload() -> dict[str, Any]:
|
|
return {
|
|
"mac_lock_enabled": False,
|
|
"mac_lock_enabled_locked_by_env": True,
|
|
"allowed_mac_addresses": "",
|
|
"allowed_mac_addresses_locked_by_env": True,
|
|
"kiosk_enforce_paired_only": False,
|
|
"kiosk_enforce_paired_only_locked_by_env": True,
|
|
"calibration_public": False,
|
|
"calibration_public_locked_by_env": True,
|
|
"device_token_configured": False,
|
|
"device_token_locked_by_env": True,
|
|
"device_token_header": "X-Device-Token",
|
|
"device_token_header_locked_by_env": True,
|
|
"kiosk_pair_token_ttl_seconds": 300,
|
|
"kiosk_pair_token_ttl_seconds_locked_by_env": True,
|
|
"kiosk_auth_cookie_days": 18250,
|
|
"kiosk_auth_cookie_days_locked_by_env": True,
|
|
"session_remember_days": 7,
|
|
"session_remember_days_locked_by_env": True,
|
|
"llm_autostart": False,
|
|
"llm_autostart_locked_by_env": True,
|
|
"admin_llm_enabled": False,
|
|
"admin_llm_enabled_locked_by_env": True,
|
|
"llm_chat_db_context": False,
|
|
"llm_chat_db_context_locked_by_env": True,
|
|
"llm_tools_enabled": False,
|
|
"llm_tools_enabled_locked_by_env": True,
|
|
}
|
|
|
|
|
|
def _auto_update_payload() -> dict[str, Any]:
|
|
return {
|
|
"enabled": False,
|
|
"auto_install": False,
|
|
"gitea_url": "",
|
|
"gitea_owner": "",
|
|
"gitea_repo": "",
|
|
"repository_url": "",
|
|
"check_interval_sec": 3600,
|
|
"note": "Автообновление недоступно на orchestrator.",
|
|
}
|
|
|
|
|
|
def _hub_only_stub(message: str, **extra: Any) -> dict[str, Any]:
|
|
return {"status": "success", "available": False, "orchestrator": True, "message": message, **extra}
|
|
|
|
|
|
def _patch_ok(message: str, **extra: Any) -> dict[str, Any]:
|
|
return {"status": "success", "message": message, **extra}
|
|
|
|
|
|
@router.get("/hardware-status")
|
|
def wesp_hardware_status(_admin: User = Depends(require_superuser)):
|
|
return {
|
|
"status": "success",
|
|
"config": {"simulation_mode": True, "read_interval": 0.05, "samples_per_read": 3},
|
|
"peripherals": {},
|
|
"scales": {"available": False, "error": "Железо недоступно на orchestrator (Docker)."},
|
|
}
|
|
|
|
|
|
@router.get("/hardware-metrics-history")
|
|
def wesp_hardware_metrics_history(_admin: User = Depends(require_superuser)):
|
|
return {"status": "success", "points": [], "path": None, "retention_days": 0}
|
|
|
|
|
|
@router.patch("/hardware/simulation")
|
|
def wesp_hardware_simulation_patch(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Симуляция железа недоступна на orchestrator.")
|
|
|
|
|
|
@router.put("/hardware/simulation/weight")
|
|
@router.patch("/hardware/simulation/weight")
|
|
def wesp_hardware_simulation_weight(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Симуляция весов недоступна на orchestrator.")
|
|
|
|
|
|
@router.get("/llm/status")
|
|
def wesp_llm_status(_admin: User = Depends(require_superuser)):
|
|
return {
|
|
"status": "success",
|
|
"enabled": False,
|
|
"llm_base_url": "",
|
|
"model": "",
|
|
"assistant_dir": "",
|
|
"llm_autostart": False,
|
|
"llm_autostart_locked_by_env": True,
|
|
"admin_llm_enabled_locked_by_env": True,
|
|
"llm_chat_db_context": False,
|
|
"llm_chat_db_context_locked_by_env": True,
|
|
"llm_tools_enabled": False,
|
|
"llm_tools_enabled_locked_by_env": True,
|
|
"llm_reachable": False,
|
|
"model_present": False,
|
|
"llm_error": "LLM недоступен на orchestrator.",
|
|
}
|
|
|
|
|
|
@router.get("/llm/activity")
|
|
def wesp_llm_activity(_admin: User = Depends(require_superuser)):
|
|
return {"status": "success", "entries": []}
|
|
|
|
|
|
@router.post("/llm/chat")
|
|
@router.post("/llm/ping")
|
|
@router.post("/llm/diagnostics")
|
|
@router.post("/llm/summary")
|
|
def wesp_llm_actions(_admin: User = Depends(require_superuser)):
|
|
return {"status": "error", "message": "LLM недоступен на orchestrator."}
|
|
|
|
|
|
@router.patch("/security-settings")
|
|
def wesp_security_settings_patch(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok(
|
|
"Настройки безопасности hub недоступны на orchestrator.",
|
|
security=_security_settings_payload(),
|
|
)
|
|
|
|
|
|
@router.patch("/auto-update-settings")
|
|
def wesp_auto_update_settings_patch(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok(
|
|
"Автообновление недоступно на orchestrator.",
|
|
auto_update=_auto_update_payload(),
|
|
)
|
|
|
|
|
|
@router.patch("/gitea-secrets")
|
|
def wesp_gitea_secrets_patch(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Gitea secrets недоступны на orchestrator.", auto_update=_auto_update_payload())
|
|
|
|
|
|
@router.get("/sync-settings")
|
|
def wesp_sync_settings_get(_admin: User = Depends(require_superuser)):
|
|
return {
|
|
"status": "success",
|
|
"connection": {"role": "server", "server_url": settings.public_base_url.rstrip("/"), "configured": True},
|
|
"state": {},
|
|
}
|
|
|
|
|
|
@router.patch("/sync-settings")
|
|
def wesp_sync_settings_patch(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Sync settings hub-client недоступны на orchestrator (роль server).")
|
|
|
|
|
|
@router.post("/sync-actions/requeue-stuck")
|
|
@router.post("/sync-actions/restart-local-sync")
|
|
@router.post("/sync-actions/refresh-runtime")
|
|
@router.post("/sync-actions/retry-report-push")
|
|
def wesp_sync_actions(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Действие sync hub-client недоступно на orchestrator.")
|
|
|
|
|
|
@router.get("/kiosk-full-setup/status")
|
|
def wesp_kiosk_full_setup_status(_admin: User = Depends(require_superuser)):
|
|
return _hub_only_stub(
|
|
"Kiosk setup только на Raspberry Pi hub.",
|
|
running_as_root=False,
|
|
full_setup_ready=False,
|
|
platform_ready=False,
|
|
kiosk_enabled=False,
|
|
chromium_found=False,
|
|
)
|
|
|
|
|
|
@router.post("/kiosk-full-setup")
|
|
def wesp_kiosk_full_setup_post(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("Kiosk setup только на Raspberry Pi hub.", 400)
|
|
|
|
|
|
@router.get("/pi-platform/status")
|
|
def wesp_pi_platform_status(_admin: User = Depends(require_superuser)):
|
|
return _hub_only_stub("Pi platform setup только на hub.", running_as_root=False, platform_ready=False)
|
|
|
|
|
|
@router.post("/pi-platform/apply")
|
|
def wesp_pi_platform_apply(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("Pi platform setup только на hub.", 400)
|
|
|
|
|
|
@router.get("/pi-boot/rainbow-splash/status")
|
|
def wesp_pi_rainbow_status(_admin: User = Depends(require_superuser)):
|
|
return _hub_only_stub("Pi boot config только на hub.", config_found=False, disable_splash=True)
|
|
|
|
|
|
@router.post("/pi-boot/rainbow-splash")
|
|
def wesp_pi_rainbow_post(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("Pi boot config только на hub.", 400)
|
|
|
|
|
|
@router.get("/plymouth/status")
|
|
def wesp_plymouth_status(_admin: User = Depends(require_superuser)):
|
|
return _hub_only_stub("Plymouth только на hub.", installed=False, running=False)
|
|
|
|
|
|
@router.post("/plymouth/install")
|
|
def wesp_plymouth_install(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("Plymouth только на hub.", 400)
|
|
|
|
|
|
@router.get("/kiosk-boot/status")
|
|
def wesp_kiosk_boot_status(_admin: User = Depends(require_superuser)):
|
|
return _hub_only_stub("Kiosk boot только на hub.", configured=False)
|
|
|
|
|
|
@router.post("/kiosk-boot")
|
|
def wesp_kiosk_boot_post(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("Kiosk boot только на hub.", 400)
|
|
|
|
|
|
@router.post("/factory-reset")
|
|
def wesp_factory_reset_post(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("Factory reset data/ недоступен на orchestrator.", 400)
|
|
|
|
|
|
@router.post("/service-control")
|
|
def wesp_service_control(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("Управление systemd недоступно на orchestrator.", 400)
|
|
|
|
|
|
@router.get("/backup.sqlite")
|
|
@router.post("/restore.sqlite")
|
|
def wesp_sqlite_backup(_admin: User = Depends(require_superuser)):
|
|
_wesp_error("SQLite backup/restore только на hub.", 400)
|
|
|
|
|
|
@router.get("/client-uploaded-logs")
|
|
def wesp_client_uploaded_logs(_admin: User = Depends(require_superuser)):
|
|
return {"status": "success", "root": "", "clients": []}
|
|
|
|
|
|
@router.get("/client-uploaded-logs/tail")
|
|
def wesp_client_uploaded_logs_tail(
|
|
client_id: str | None = None,
|
|
file: str | None = None,
|
|
lines: int = Query(default=300, ge=1, le=2000),
|
|
_admin: User = Depends(require_superuser),
|
|
):
|
|
_ = (client_id, file, lines)
|
|
return {
|
|
"status": "error",
|
|
"message": "Логи клиентов (POST /api/sync/client-log) на orchestrator пока не настроены.",
|
|
"lines": [],
|
|
}
|
|
|
|
|
|
@router.get("/client-uploaded-logs/download")
|
|
def wesp_client_uploaded_logs_download(
|
|
client_id: str | None = None,
|
|
file: str | None = None,
|
|
_admin: User = Depends(require_superuser),
|
|
):
|
|
_ = (client_id, file)
|
|
_wesp_error("Логи клиентов на orchestrator пока не настроены.", 404)
|
|
|
|
|
|
@router.get("/server-log")
|
|
def wesp_server_log(lines: int = Query(default=200, ge=1, le=1000), _admin: User = Depends(require_superuser)):
|
|
log_path = ensure_server_log_file()
|
|
tail = get_server_log_tail(lines=lines)
|
|
payload = {"status": "success", "path": str(log_path), "lines": tail.get("lines") or []}
|
|
return payload
|
|
|
|
|
|
@router.get("/server-log/download")
|
|
def wesp_server_log_download(_admin: User = Depends(require_superuser)):
|
|
log_path = ensure_server_log_file()
|
|
return FileResponse(
|
|
log_path,
|
|
media_type="text/plain; charset=utf-8",
|
|
filename=log_path.name,
|
|
)
|
|
|
|
|
|
@router.get("/diagnostics/settings")
|
|
def wesp_diagnostics_settings_get(_admin: User = Depends(require_superuser)):
|
|
return {"status": "success", "targets": [], "extra_targets": []}
|
|
|
|
|
|
@router.patch("/diagnostics/settings")
|
|
def wesp_diagnostics_settings_patch(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Diagnostics settings сохранены локально недоступны на orchestrator.")
|
|
|
|
|
|
@router.post("/diagnostics/network-run")
|
|
@router.post("/diagnostics/hx711-sample")
|
|
@router.post("/diagnostics/gpio-blink")
|
|
@router.post("/diagnostics/traceroute-by-hosts")
|
|
def wesp_diagnostics_actions(_admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Диагностика железа/сети hub недоступна на orchestrator.")
|
|
|
|
|
|
@router.patch("/sync-clients/{node_id}/ip")
|
|
def wesp_sync_client_ip(_node_id: str, _admin: User = Depends(require_superuser)):
|
|
return _patch_ok("Sync client IP управляется через hub sync API.")
|
|
|