From 44459279900622501bfe7b877e946220b1817ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E6=98=AF=E5=B0=8F=E4=B8=80=E7=81=B0?= Date: Tue, 26 May 2026 12:11:02 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E9=83=A8=E7=BD=B2=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=AE=89=E8=A3=85=20Web=20=E9=85=8D=E7=BD=AE=E5=86=99?= =?UTF-8?q?=E5=85=A5=20sudo=20=E5=8A=A9=E6=89=8B=20/=20Install=20Web=20con?= =?UTF-8?q?fig-write=20sudo=20helper=20in=20deploy=20scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ogscope-config-write 与 ogscope-config sudoers / Add config-write script and sudoers - install/board-update/network-init 自动安装并规范化 env 640 权限 / Auto-install on deploy paths - API 与配置页区分 direct/sudo 可写状态 / API and config UI expose direct vs sudo write access Co-authored-by: Cursor --- ogscope/web/api/system/config_files.py | 107 ++++++++++++++++++ ogscope/web/api/system/routes.py | 61 +--------- scripts/board-update.sh | 2 + scripts/install-min.sh | 2 + scripts/install.sh | 2 + scripts/mirror.sh | 36 ++++++ scripts/ogscope-config-write.sh | 53 +++++++++ scripts/ogscope-network-init.sh | 67 ++++++++++- tests/unit/test_config_files.py | 43 +++++++ web/spa/src/apps/system/pages/ConfigPage.tsx | 20 +++- .../analysis-lab/assets/system-B6miqBWl.js | 78 ------------- .../analysis-lab/assets/system-DQXiDxh6.js | 78 +++++++++++++ web/static/analysis-lab/system.html | 2 +- 13 files changed, 412 insertions(+), 139 deletions(-) create mode 100644 ogscope/web/api/system/config_files.py create mode 100755 scripts/ogscope-config-write.sh create mode 100644 tests/unit/test_config_files.py delete mode 100644 web/static/analysis-lab/assets/system-B6miqBWl.js create mode 100644 web/static/analysis-lab/assets/system-DQXiDxh6.js diff --git a/ogscope/web/api/system/config_files.py b/ogscope/web/api/system/config_files.py new file mode 100644 index 0000000..6818c7e --- /dev/null +++ b/ogscope/web/api/system/config_files.py @@ -0,0 +1,107 @@ +"""Web 配置 env 文件读写辅助 / Helpers for Web-managed env config files.""" + +from __future__ import annotations + +import grp +import os +import subprocess +from pathlib import Path + +CONFIG_WRITE_SCRIPT = Path("/usr/local/bin/ogscope-config-write") +CONFIG_SUDOERS = Path("/etc/sudoers.d/ogscope-config") +CONFIG_FILE_MODE = "640" + + +def config_file_group(path: Path) -> str: + """读取目标文件属组,供 chown 使用 / Group name for chown on target file.""" + if path.exists(): + try: + return grp.getgrgid(path.stat().st_gid).gr_name + except KeyError: + pass + return os.environ.get("USER", "ogscope") + + +def config_write_access() -> dict[str, bool]: + """评估 sudo 写入能力 / Assess sudo-backed config write access.""" + via_sudo = CONFIG_WRITE_SCRIPT.is_file() and CONFIG_SUDOERS.is_file() + return { + "writable_via_sudo": via_sudo, + } + + +def read_config_file_payload(path: Path) -> dict: + """读取配置文件内容与写入能力 / Read config file and write capability flags.""" + exists = path.exists() + access = config_write_access() + if not exists: + parent_writable = os.access(path.parent, os.W_OK) + return { + "path": str(path), + "exists": False, + "writable": parent_writable or access["writable_via_sudo"], + **access, + "content": "", + "error": "file not found", + } + try: + content = path.read_text(encoding="utf-8") + direct = os.access(path, os.W_OK) + writable = direct or access["writable_via_sudo"] + return { + "path": str(path), + "exists": True, + "writable": writable, + "writable_direct": direct, + "writable_via_sudo": access["writable_via_sudo"], + "content": content, + "error": None, + } + except OSError as exc: + return { + "path": str(path), + "exists": True, + "writable": access["writable_via_sudo"], + **access, + "content": "", + "error": str(exc), + } + + +def write_config_file(path: Path, content: str) -> None: + """写入 env 配置文件(必要时 sudo)/ Write env config, using sudo helper when needed.""" + path.parent.mkdir(parents=True, exist_ok=True) + try: + path.write_text(content, encoding="utf-8") + return + except OSError: + pass + + if not CONFIG_WRITE_SCRIPT.is_file(): + raise RuntimeError( + "failed to write config file; install ogscope-config-write via install.sh " + "or ogscope-network-init.sh ensure-config" + ) + + group = config_file_group(path) + proc = subprocess.run( + [ + "sudo", + "-n", + str(CONFIG_WRITE_SCRIPT), + str(path), + CONFIG_FILE_MODE, + group, + ], + input=content, + text=True, + capture_output=True, + check=False, + ) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout or "").strip() + raise RuntimeError( + "failed to write config file via sudo; run " + "sudo ./scripts/ogscope-network-init.sh ensure-config " + f"({detail})" + ) diff --git a/ogscope/web/api/system/routes.py b/ogscope/web/api/system/routes.py index 9513f9e..38f166e 100644 --- a/ogscope/web/api/system/routes.py +++ b/ogscope/web/api/system/routes.py @@ -3,8 +3,6 @@ """ from pathlib import Path -import os -import subprocess from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field @@ -14,6 +12,7 @@ from ogscope.domain.system.services import system_info_service from ogscope.platform.hardware_plane.runtime import get_hardware_plane_client from ogscope.web.api.models.schemas import SystemInfo +from ogscope.web.api.system.config_files import read_config_file_payload, write_config_file router = APIRouter() @@ -59,64 +58,14 @@ def _validate_env_content(content: str) -> None: def _read_config_file(path: Path) -> dict: - exists = path.exists() - writable = os.access(path if exists else path.parent, os.W_OK) - if not exists: - return { - "path": str(path), - "exists": False, - "writable": writable, - "content": "", - "error": "file not found", - } - try: - content = path.read_text(encoding="utf-8") - return { - "path": str(path), - "exists": True, - "writable": writable, - "content": content, - "error": None, - } - except OSError as exc: - return { - "path": str(path), - "exists": True, - "writable": writable, - "content": "", - "error": str(exc), - } + return read_config_file_payload(path) def _write_config_file(path: Path, content: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) try: - path.write_text(content, encoding="utf-8") - return - except OSError: - pass - - proc = subprocess.run( - ["sudo", "-n", "tee", str(path)], - input=content, - text=True, - capture_output=True, - check=False, - ) - if proc.returncode != 0: - raise HTTPException( - status_code=500, - detail=( - "failed to write config file; grant write permission " - "or allow sudo tee without password" - ), - ) - subprocess.run( - ["sudo", "-n", "chmod", "640", str(path)], - capture_output=True, - text=True, - check=False, - ) + write_config_file(path, content) + except RuntimeError as exc: + raise HTTPException(status_code=500, detail=str(exc)) from exc @router.get("/system/info", response_model=SystemInfo) diff --git a/scripts/board-update.sh b/scripts/board-update.sh index 93c53b1..3f90317 100755 --- a/scripts/board-update.sh +++ b/scripts/board-update.sh @@ -136,6 +136,8 @@ fi sudo chown "root:${USER}" "${OGSCOPE_ENV_FILE}" 2>/dev/null || true sudo chmod 640 "${OGSCOPE_ENV_FILE}" 2>/dev/null || true +ogscope_install_config_write_artifacts "${PROJECT_DIR}" "${USER}" + chmod +x "${PROJECT_DIR}/scripts/ogscope-network-boot.sh" 2>/dev/null || true ogscope_sync_network_boot_unit_if_needed "${PROJECT_DIR}" diff --git a/scripts/install-min.sh b/scripts/install-min.sh index 8fe88d3..68a2905 100644 --- a/scripts/install-min.sh +++ b/scripts/install-min.sh @@ -121,6 +121,8 @@ fi sudo chown "root:${USER}" "${OGSCOPE_ENV_FILE}" 2>/dev/null || true sudo chmod 640 "${OGSCOPE_ENV_FILE}" 2>/dev/null || true +ogscope_install_config_write_artifacts "${PROJECT_DIR}" "${USER}" + echo "⚙️ 写入 systemd 服务 / Writing systemd service..." sudo tee "${SERVICE_PATH}" >/dev/null </dev/null || true fi +ogscope_install_config_write_artifacts "${PROJECT_DIR}" "${USER}" + # ExecStart 使用 poetry env info --path(与 virtualenvs.in-project=true 时即项目 .venv),勿手写 ~/.virtualenvs/ # ExecStart uses poetry env path (project .venv when in-project=true); do not hardcode ~/.virtualenvs/ echo "⚙️ 写入 systemd: ${SERVICE_PATH}" diff --git a/scripts/mirror.sh b/scripts/mirror.sh index 7f0b9ee..be4d1e9 100644 --- a/scripts/mirror.sh +++ b/scripts/mirror.sh @@ -412,6 +412,41 @@ ogscope_report_plate_solve_database_status() { echo "⚠️ Plate solving needs default_database.npz under data/plate_solve/; see docs/development/plate-solve-data.md" } +# 增量更新:同步网络相关工件(与近期 wifi-nm / systemd 文档一致) +# Board update: sync network artifacts (matches wifi-nm + systemd docs) +# 参数 / Args: $1 = 项目根目录绝对路径 / absolute project root +# $2 = 服务用户名(可选,默认 $USER)/ service user (optional, default $USER) +# 环境 / Env: OGSCOPE_SKIP_NETWORK_SYNC=1 跳过;需 sudo(免密或交互)/ skip; requires sudo +ogscope_install_config_write_artifacts() { + local project_dir="${1:?}" + local run_user="${2:-${USER:-}}" + local src="${project_dir}/scripts/ogscope-config-write.sh" + local dst="/usr/local/bin/ogscope-config-write" + local sudoers="/etc/sudoers.d/ogscope-config" + + if [ ! -f "${src}" ]; then + echo "⚠️ 未找到 ${src},跳过 config-write / Missing config-write script" + return 0 + fi + + echo "📝 安装 Web 配置写入助手 / Installing config-write helper → ${dst} ..." + sudo install -m 755 "${src}" "${dst}" + + if [ -z "${run_user}" ]; then + echo "⚠️ 未设置服务用户,跳过 ogscope-config sudoers / No service user; skip config sudoers" + return 0 + fi + + umask 077 + sudo tee "${sudoers}.tmp" >/dev/null </dev/null; then sudo env OGSCOPE_SERVICE_USER="${USER}" "${init_script}" ensure-systemd \ || echo "⚠️ ensure-systemd 失败;可手动: sudo env OGSCOPE_SERVICE_USER=\$USER ${init_script} ensure-systemd" + ogscope_install_config_write_artifacts "${project_dir}" "${USER}" else echo "⚠️ 无法免密 sudo,未运行 ensure-systemd;若 Web WiFi 异常请手动执行上述命令(见 docs/development/wifi-nm.md)" echo "⚠️ Non-interactive sudo unavailable; run ensure-systemd manually if WiFi/API issues" diff --git a/scripts/ogscope-config-write.sh b/scripts/ogscope-config-write.sh new file mode 100755 index 0000000..d4a9635 --- /dev/null +++ b/scripts/ogscope-config-write.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# 受控写入 /etc/ogscope/*.env(供 Web 配置 API 经 sudo 调用) +# Controlled write to /etc/ogscope/*.env (invoked via sudo from Web config API) +# +# 用法 / Usage: +# echo "KEY=value" | sudo -n ogscope-config-write /etc/ogscope/ogscope.env [mode] [group] +# +set -euo pipefail + +die() { + echo "ogscope-config-write: $*" >&2 + exit 1 +} + +dest="${1:-}" +mode="${2:-640}" +group="${3:-}" + +ALLOWED=( + "/etc/ogscope/ogscope.env" + "/etc/ogscope/network.env" +) + +[[ -n "${dest}" ]] || die "missing destination path" + +allowed=0 +for path in "${ALLOWED[@]}"; do + if [[ "${dest}" == "${path}" ]]; then + allowed=1 + break + fi +done +[[ "${allowed}" -eq 1 ]] || die "destination not allowed: ${dest}" + +if [[ ! "${mode}" =~ ^[0-7]{3,4}$ ]]; then + die "invalid mode: ${mode}" +fi + +if [[ -z "${group}" ]]; then + if [[ -e "${dest}" ]]; then + group="$(stat -c '%G' "${dest}" 2>/dev/null || true)" + fi + group="${group:-ogscope}" +fi + +mkdir -p "$(dirname "${dest}")" +tmp="$(mktemp "${dest}.tmp.XXXXXX")" +trap 'rm -f "${tmp}"' EXIT +cat >"${tmp}" +chown "root:${group}" "${tmp}" +chmod "${mode}" "${tmp}" +mv -f "${tmp}" "${dest}" +trap - EXIT diff --git a/scripts/ogscope-network-init.sh b/scripts/ogscope-network-init.sh index 478e424..4aee0bb 100755 --- a/scripts/ogscope-network-init.sh +++ b/scripts/ogscope-network-init.sh @@ -30,6 +30,10 @@ SWITCH_SRC="${SCRIPT_DIR}/ogscope-wifi-switch.sh" SWITCH_DST="/usr/local/bin/ogscope-wifi-switch" SUDOERS_D="/etc/sudoers.d/ogscope-wifi" SUDOERS_NMCLI="/etc/sudoers.d/ogscope-nmcli" +SUDOERS_CONFIG="/etc/sudoers.d/ogscope-config" +CONFIG_WRITE_SRC="${SCRIPT_DIR}/ogscope-config-write.sh" +CONFIG_WRITE_DST="/usr/local/bin/ogscope-config-write" +OGSCOPE_ENV_FILE="${ENV_DIR}/ogscope.env" # systemd drop-in:老部署主 unit 可能无 EnvironmentFile / Drop-in for units missing EnvironmentFile SYSTEMD_DROPIN_DIR="/etc/systemd/system/ogscope.service.d" SYSTEMD_NETWORK_ENV_CONF="${SYSTEMD_DROPIN_DIR}/ogscope-network-env.conf" @@ -104,8 +108,47 @@ write_sudoers_nmcli() { ok "已写入 ${SUDOERS_NMCLI}(免密 ${nmcli_bin},Web「激活」已保存 WiFi 等)" } +install_config_write_script() { + if [[ ! -f "${CONFIG_WRITE_SRC}" ]]; then + die "未找到 ${CONFIG_WRITE_SRC} / Config write script missing" + fi + install -m 755 "${CONFIG_WRITE_SRC}" "${CONFIG_WRITE_DST}" + ok "已安装 ${CONFIG_WRITE_DST}" +} + +write_sudoers_config() { + local run_user="${OGSCOPE_SERVICE_USER:-${SUDO_USER:-}}" + if [[ -z "${run_user}" ]]; then + info "未设置 OGSCOPE_SERVICE_USER/SUDO_USER,跳过 config sudoers / Skipping config sudoers" + return 0 + fi + umask 077 + cat >"${SUDOERS_CONFIG}.tmp" </dev/null || true + chmod 640 "${f}" 2>/dev/null || true + done + ok "已规范化 ogscope.env / network.env 权限为 root:${run_user} 640" +} + write_network_env() { local suffix="$1" + local run_user="${OGSCOPE_SERVICE_USER:-${SUDO_USER:-}}" umask 077 cat >"${ENV_FILE}.tmp" </dev/null || true + fi + chmod 640 "${ENV_FILE}.tmp" mv "${ENV_FILE}.tmp" "${ENV_FILE}" ok "已写入 ${ENV_FILE}" } @@ -252,6 +298,9 @@ cmd_init() { ensure_ogscope_systemd_network_env write_sudoers write_sudoers_nmcli + install_config_write_script + write_sudoers_config + normalize_config_env_permissions set_hostname_avahi "${suffix}" ok "init 完成。请 systemctl restart ogscope 并连接热点 OGScope_${suffix} / init done" @@ -276,9 +325,20 @@ cmd_ensure_systemd() { fi ensure_ogscope_systemd_network_env write_sudoers_nmcli + install_config_write_script + write_sudoers_config + normalize_config_env_permissions info "请执行: sudo systemctl restart ogscope / Please run: sudo systemctl restart ogscope" } +cmd_ensure_config() { + require_root + install_config_write_script + write_sudoers_config + normalize_config_env_permissions + ok "config-write 与 sudoers 已就绪 / config-write and sudoers ready" +} + cmd_diag() { info "=== OGScope 网络诊断 / Network diagnostics ===" command -v nmcli >/dev/null && ok "nmcli: OK" || echo "❌ nmcli 缺失" @@ -287,6 +347,8 @@ cmd_diag() { [[ -f "${SWITCH_DST}" ]] && ok "切换脚本: ${SWITCH_DST}" || echo "⚠️ 无 ${SWITCH_DST}" [[ -f "${SUDOERS_D}" ]] && ok "sudoers: ${SUDOERS_D}" || echo "⚠️ 无 sudoers" [[ -f "${SUDOERS_NMCLI}" ]] && ok "sudoers nmcli: ${SUDOERS_NMCLI}" || echo "⚠️ 无 ${SUDOERS_NMCLI}(Web 激活 WiFi 可能报 Not authorized)" + [[ -f "${SUDOERS_CONFIG}" ]] && ok "sudoers config: ${SUDOERS_CONFIG}" || echo "⚠️ 无 ${SUDOERS_CONFIG}(Web 配置页可能无法保存)" + [[ -x "${CONFIG_WRITE_DST}" ]] && ok "config-write: ${CONFIG_WRITE_DST}" || echo "⚠️ 无 ${CONFIG_WRITE_DST}" command -v avahi-daemon >/dev/null && ok "avahi-daemon 已安装" || echo "⚠️ avahi-daemon 未安装" if command -v nmcli >/dev/null; then nmcli connection show "${AP_NAME}" >/dev/null 2>&1 && ok "连接 ${AP_NAME} 存在" || echo "⚠️ 无 ${AP_NAME}" @@ -346,9 +408,10 @@ main() { init) cmd_init "${1:-}" ;; diag) cmd_diag ;; ensure-systemd) cmd_ensure_systemd ;; + ensure-config) cmd_ensure_config ;; reset) cmd_reset "${1:-}" ;; *) - echo "Usage: sudo $0 init [--yes] | diag | ensure-systemd | reset [--yes]" >&2 + echo "Usage: sudo $0 init [--yes] | diag | ensure-systemd | ensure-config | reset [--yes]" >&2 exit 1 ;; esac diff --git a/tests/unit/test_config_files.py b/tests/unit/test_config_files.py new file mode 100644 index 0000000..f2c0540 --- /dev/null +++ b/tests/unit/test_config_files.py @@ -0,0 +1,43 @@ +"""配置 env 文件读写辅助测试 / Tests for config env file helpers.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ogscope.web.api.system import config_files as mod + + +@pytest.mark.unit +def test_read_config_file_payload_marks_sudo_writable( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + env_path = tmp_path / "ogscope.env" + env_path.write_text("OGSCOPE_PORT=8000\n", encoding="utf-8") + monkeypatch.setattr(mod, "CONFIG_WRITE_SCRIPT", tmp_path / "write.sh") + monkeypatch.setattr(mod, "CONFIG_SUDOERS", tmp_path / "sudoers") + mod.CONFIG_WRITE_SCRIPT.write_text("#!/bin/sh\n", encoding="utf-8") + mod.CONFIG_SUDOERS.write_text("ogscope ALL=(ALL) NOPASSWD: /usr/local/bin/ogscope-config-write\n") + + payload = mod.read_config_file_payload(env_path) + + assert payload["exists"] is True + assert payload["writable_via_sudo"] is True + assert payload["writable"] is True + + +@pytest.mark.unit +def test_read_config_file_payload_not_writable_without_sudoers( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, +) -> None: + env_path = tmp_path / "ogscope.env" + env_path.write_text("OGSCOPE_PORT=8000\n", encoding="utf-8") + monkeypatch.setattr(mod, "CONFIG_WRITE_SCRIPT", tmp_path / "missing-write.sh") + monkeypatch.setattr(mod, "CONFIG_SUDOERS", tmp_path / "missing-sudoers") + monkeypatch.setattr(mod.os, "access", lambda _path, _mode: False) + + payload = mod.read_config_file_payload(env_path) + + assert payload["writable_via_sudo"] is False + assert payload["writable"] is False diff --git a/web/spa/src/apps/system/pages/ConfigPage.tsx b/web/spa/src/apps/system/pages/ConfigPage.tsx index e04e64e..6f115e9 100644 --- a/web/spa/src/apps/system/pages/ConfigPage.tsx +++ b/web/spa/src/apps/system/pages/ConfigPage.tsx @@ -8,6 +8,8 @@ type ConfigFileItem = { path: string; exists: boolean; writable: boolean; + writable_direct?: boolean; + writable_via_sudo?: boolean; content: string; error?: string | null; }; @@ -288,6 +290,21 @@ export function ConfigPage() { return label ? (isZh ? label.zh : label.en) : fileId; }; + const writableHint = (file: ConfigFileItem) => { + if (!file.writable) { + return isZh + ? "不可写:请运行 sudo ./scripts/ogscope-network-init.sh ensure-config" + : "Not writable: run sudo ./scripts/ogscope-network-init.sh ensure-config"; + } + if (file.writable_via_sudo && !file.writable_direct) { + return isZh ? "可写(经 sudo 助手)" : "Writable (via sudo helper)"; + } + if (file.writable_direct) { + return isZh ? "可写(直接)" : "Writable (direct)"; + } + return isZh ? "可写" : "Writable"; + }; + return (
@@ -360,8 +377,7 @@ export function ConfigPage() {

{activeFile.path}

- {isZh ? "可写" : "Writable"}: {String(activeFile.writable)} · {isZh ? "存在" : "Exists"}:{" "} - {String(activeFile.exists)} + {writableHint(activeFile)} · {isZh ? "存在" : "Exists"}: {String(activeFile.exists)}

diff --git a/web/static/analysis-lab/assets/system-B6miqBWl.js b/web/static/analysis-lab/assets/system-B6miqBWl.js deleted file mode 100644 index 3ce1222..0000000 --- a/web/static/analysis-lab/assets/system-B6miqBWl.js +++ /dev/null @@ -1,78 +0,0 @@ -import{j as e,r as a,a as $e,R as Me}from"./client-D1ZVDB-N.js";import{u as pe,C as Ee,r as K,a as U,S as Pe,b as Le}from"./http-ChPtkS1w.js";import{c as q,u as J,T as Re,I as Te}from"./index-CutgeBjy.js";import{R as he}from"./refresh-cw-BkMjDReH.js";/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const me=q("Activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Oe=q("Bolt",[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",key:"yt0hxn"}],["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Ae=q("BookOpen",[["path",{d:"M12 7v14",key:"1akyts"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",key:"ruj8y"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Se=q("Cpu",[["rect",{width:"16",height:"16",x:"4",y:"4",rx:"2",key:"14l7u7"}],["rect",{width:"6",height:"6",x:"9",y:"9",rx:"1",key:"5aljv4"}],["path",{d:"M15 2v2",key:"13l42r"}],["path",{d:"M15 20v2",key:"15mkzm"}],["path",{d:"M2 15h2",key:"1gxd5l"}],["path",{d:"M2 9h2",key:"1bbxkp"}],["path",{d:"M20 15h2",key:"19e6y8"}],["path",{d:"M20 9h2",key:"19tzq7"}],["path",{d:"M9 2v2",key:"165o2o"}],["path",{d:"M9 20v2",key:"i2bqo8"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Fe=q("HardDrive",[["line",{x1:"22",x2:"2",y1:"12",y2:"12",key:"1y58io"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16",key:"sgf278"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16",key:"1l4acy"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Ie=q("LayoutDashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const ze=q("Network",[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1",key:"4q2zg0"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1",key:"8cvhb9"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1",key:"1egb70"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3",key:"1jsf9p"}],["path",{d:"M12 12V8",key:"2874zd"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const De=q("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const _e=q("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const qe=q("Settings",[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const ge=q("Sparkles",[["path",{d:"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z",key:"4pj2yx"}],["path",{d:"M20 3v4",key:"1olli1"}],["path",{d:"M22 5h-4",key:"1gvqau"}],["path",{d:"M4 17v2",key:"vumght"}],["path",{d:"M5 18H3",key:"zchphs"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const He=q("Thermometer",[["path",{d:"M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z",key:"17jzev"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Ue=q("Touchpad",[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2",key:"18n3k1"}],["path",{d:"M2 14h20",key:"myj16y"}],["path",{d:"M12 20v-6",key:"1rm09r"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const Be=q("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/** - * @license lucide-react v0.460.0 - ISC - * - * This source code is licensed under the ISC license. - * See the LICENSE file in the root directory of this source tree. - */const fe=q("Wifi",[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0",key:"1x1e6c"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}]]),V=s=>`flex items-center gap-3 rounded-lg px-3 py-2.5 font-headline text-sm tracking-tight transition-colors ${s?"border-r-2 border-primary bg-white/5 font-semibold text-primary":"text-on-surface-variant hover:bg-white/5 hover:text-on-surface"}`;function We({route:s,allowNetworkRoute:o,onRouteChange:n,children:t}){const{t:c,locale:d,setLocale:N}=J(),{info:m}=pe(),f=(m==null?void 0:m.cpu_usage)!=null?Number(m.cpu_usage).toFixed(1):"—",y=(m==null?void 0:m.memory_usage)!=null?Number(m.memory_usage).toFixed(1):"—",g=(m==null?void 0:m.temperature)!=null?Number(m.temperature).toFixed(1):"—",S=(m==null?void 0:m.wifi_quality)!=null&&!Number.isNaN(Number(m.wifi_quality))?`${Number(m.wifi_quality).toFixed(0)}%`:"—",v={overview:c("sys.shell.top.overview"),network:c("sys.shell.top.network"),sensors:c("sys.shell.top.sensors"),hmi:c("sys.shell.top.hmi"),power:c("sys.shell.top.power"),config:c("sys.shell.top.config")},p=V(!1),$=(M,R)=>{const _=window.open(M,R);_&&_.focus()};return e.jsxs("div",{className:"flex h-full min-h-0 flex-col bg-background text-on-surface md:flex-row",children:[e.jsxs("aside",{className:"glass-panel z-50 flex w-full shrink-0 flex-col border-b border-outline-variant/20 bg-surface-container-low/80 backdrop-blur-xl md:fixed md:left-0 md:top-0 md:h-full md:w-64 md:border-b-0 md:border-r md:border-white/5",children:[e.jsxs("div",{className:"p-5",children:[e.jsxs("div",{className:"mb-8 flex items-center gap-3",children:[e.jsx("div",{className:"primary-gradient flex h-10 w-10 items-center justify-center rounded-lg shadow-lg",children:e.jsx(ge,{className:"h-5 w-5 text-on-primary-container"})}),e.jsxs("div",{children:[e.jsx("h1",{className:"font-headline text-lg font-bold tracking-widest text-primary",children:"OGScope"}),e.jsx("p",{className:"font-mono text-[10px] uppercase tracking-widest text-on-surface-variant",children:c("sys.shell.subtitle")})]})]}),e.jsxs("nav",{className:"flex flex-col gap-0.5",children:[e.jsxs("button",{type:"button",className:V(s==="overview"),onClick:()=>n("overview"),children:[e.jsx(Ie,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.overview")})]}),o&&e.jsxs("button",{type:"button",className:V(s==="network"),onClick:()=>n("network"),children:[e.jsx(ze,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.network")})]}),e.jsxs("a",{href:"/debug/camera",className:p,onClick:M=>{M.preventDefault(),$("/debug/camera","ogscopeCameraConsole")},children:[e.jsx(Ee,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.camera")})]}),e.jsxs("a",{href:"/debug/analysis",className:p,onClick:M=>{M.preventDefault(),$("/debug/analysis","ogscopeAnalysisConsole")},children:[e.jsx(ge,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.analysis")})]}),e.jsxs("button",{type:"button",className:V(s==="sensors"),onClick:()=>n("sensors"),children:[e.jsx(me,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.sensors")})]}),e.jsxs("button",{type:"button",className:V(s==="power"),onClick:()=>n("power"),children:[e.jsx(Oe,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.power")})]}),e.jsxs("button",{type:"button",className:V(s==="hmi"),onClick:()=>n("hmi"),children:[e.jsx(Ue,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.hmi")})]}),e.jsxs("button",{type:"button",className:V(s==="config"),onClick:()=>n("config"),children:[e.jsx(qe,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.config")})]})]})]}),e.jsx("div",{className:"mt-auto hidden p-5 md:block",children:e.jsxs("div",{className:"rounded-xl border border-white/5 bg-surface-container-low p-3",children:[e.jsx("p",{className:"truncate text-xs font-semibold text-on-surface",children:c("sys.shell.workbench")}),e.jsxs("p",{className:"font-mono text-[10px] text-on-surface-variant",children:[c("sys.shell.node"),": OGSCOPE_PI_ZERO_2W"]})]})})]}),e.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1 flex-col md:ml-64",children:[e.jsxs("header",{className:"sticky top-0 z-40 flex h-14 shrink-0 items-center justify-between border-b border-white/5 bg-neutral-950/80 px-4 backdrop-blur-md md:px-8",children:[e.jsx("div",{className:"flex min-w-0 items-center gap-3",children:e.jsx("span",{className:"hidden truncate border-b-2 border-primary pb-0.5 font-mono text-xs uppercase tracking-wider text-primary sm:inline",children:v[s]})}),e.jsxs("div",{className:"flex flex-wrap items-center justify-end gap-3 sm:gap-4",children:[e.jsxs("div",{className:"mr-2 flex gap-1 text-[10px]",children:[e.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${d==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>N("zh"),children:c("lang.zh")}),e.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${d==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>N("en"),children:c("lang.en")})]}),e.jsxs("div",{className:"flex flex-wrap items-center justify-end gap-3 font-mono text-[10px] uppercase tracking-wider text-on-surface-variant sm:gap-4",children:[e.jsxs("span",{className:"flex items-center gap-1 text-primary",children:[e.jsx(Se,{className:"h-3.5 w-3.5"})," CPU ",f,"%"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(me,{className:"h-3.5 w-3.5"})," MEM ",y,"%"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"text-xs",children:"°C"})," ",g]}),e.jsxs("span",{className:"flex items-center gap-1 text-secondary",children:[e.jsx(fe,{className:"h-3.5 w-3.5"})," ",S]})]})]})]}),e.jsx("main",{className:"og-scrollbar min-h-0 flex-1 overflow-auto p-4 md:p-6",children:t})]})]})}async function Ke(){return await K("/api/dev/system/hardware-plane/status",{cache:"no-store"})}async function Xe(s){return await K("/api/dev/system/hardware-plane/command",{method:"POST",body:JSON.stringify(s)})}async function Ge(s){var t;const o=new URLSearchParams;s!=null&&s.service&&o.set("service",s.service),o.set("since_seconds",String(s.sinceSeconds)),o.set("limit",String(s.limit)),(t=s==null?void 0:s.levels)!=null&&t.length&&o.set("levels",s.levels.join(","));const n=o.toString();return await K(`/api/dev/debug/logs/systemd${n?`?${n}`:""}`,{cache:"no-store"})}function Ze(s){const o=Math.max(0,parseInt(String(s??0),10)||0),n=Math.floor(o/86400),t=Math.floor(o%86400/3600),c=Math.floor(o%3600/60);return n>0?`${n}d ${t}h`:t>0?`${t}h ${c}m`:`${c}m`}function de(s,o=1){return s==null||Number.isNaN(Number(s))?"—":Number(s).toFixed(o)}function ye(s){return s==="ERROR"?"text-error":s==="WARN"?"text-amber-300":"text-primary"}function Je(s){if(!s)return"--:--:--";const o=new Date(s);return Number.isNaN(o.getTime())?"--:--:--":o.toLocaleTimeString()}function Ye(){const{t:n}=J(),{info:t,error:c}=pe(),[d,N]=a.useState(!1),[m,f]=a.useState(["INFO","WARN","ERROR"]),[y,g]=a.useState([]),[S,v]=a.useState(null),[p,$]=a.useState(!1),[M,R]=a.useState(!0),_=a.useRef(null),E=a.useRef(new Set),h=de(t==null?void 0:t.cpu_usage),T=de(t==null?void 0:t.memory_usage),O=de(t==null?void 0:t.temperature),j=(t==null?void 0:t.load_average_1m)!=null?String(t.load_average_1m):"—",C=(t==null?void 0:t.wifi_quality)!=null&&!Number.isNaN(Number(t.wifi_quality))?`${Number(t.wifi_quality).toFixed(0)}%`:"—",w=(t==null?void 0:t.wifi_signal_dbm)!=null&&!Number.isNaN(Number(t.wifi_signal_dbm))?`${Number(t.wifi_signal_dbm).toFixed(0)} dBm`:"—",P=async()=>{if(m.length===0){g([]);return}$(!0);try{v(null);const i=await Ge({service:"ogscope",sinceSeconds:1200,limit:240,levels:m});g(k=>{const L=new Set(k.map(I=>`${I.ts??""}::${I.level}::${I.source}::${I.message}`)),A=[...k];for(const I of i.items){const B=`${I.ts??""}::${I.level}::${I.source}::${I.message}`;L.has(B)||(L.add(B),A.push(I))}return A.length<=300?A:A.slice(A.length-300)})}catch(i){v(i instanceof Error?i.message:String(i))}finally{$(!1)}};a.useEffect(()=>{if(!d)return;P();const i=window.setInterval(()=>{document.hidden||P()},4e3);return()=>window.clearInterval(i)},[d,m.join(",")]),a.useEffect(()=>{const i=_.current;if(!i)return;const k=()=>{const L=i.scrollHeight-i.scrollTop-i.clientHeight;R(L<=24)};return k(),i.addEventListener("scroll",k),()=>i.removeEventListener("scroll",k)},[]),a.useEffect(()=>{if(!_.current||y.length===0)return;const k=new Set(y.map(A=>`${A.ts??""}::${A.level}::${A.source}::${A.message}`));let L=!1;k.forEach(A=>{E.current.has(A)||(L=!0)}),E.current=k,M&&L&&requestAnimationFrame(()=>{_.current&&(_.current.scrollTop=_.current.scrollHeight)})},[y,M]);const z=a.useMemo(()=>new Set(m),[m]);return e.jsxs("div",{className:"mx-auto max-w-7xl space-y-6",children:[e.jsxs("header",{className:"mb-1",children:[e.jsxs("div",{className:"flex items-center gap-2 text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.breadcrumb.console")}),e.jsx("span",{children:"/"}),e.jsx("span",{className:"text-primary",children:n("sys.overview.breadcrumb.module")})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:n("sys.overview.title")}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:n("sys.overview.subtitle")})]}),c&&e.jsx("div",{className:"rounded-lg border border-error/40 bg-error-container/20 px-3 py-2 text-sm text-on-error-container",children:c}),e.jsxs("section",{className:"grid grid-cols-12 gap-4",children:[e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.cpu")}),e.jsx(Se,{className:"h-4 w-4 text-primary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[h,"%"]}),e.jsx("div",{className:"mt-3 h-1.5 w-full overflow-hidden rounded bg-surface-container-high",children:e.jsx("div",{className:"h-full bg-primary",style:{width:`${Math.min(Number(h)||0,100)}%`}})})]}),e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.mem")}),e.jsx(me,{className:"h-4 w-4 text-secondary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[T,"%"]}),e.jsx("div",{className:"mt-3 h-1.5 w-full overflow-hidden rounded bg-surface-container-high",children:e.jsx("div",{className:"h-full bg-secondary",style:{width:`${Math.min(Number(T)||0,100)}%`}})})]}),e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.temp")}),e.jsx(He,{className:"h-4 w-4 text-primary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[O,"°C"]}),e.jsx("div",{className:"mt-3 text-xs text-on-surface-variant",children:n("sys.overview.tempState")})]}),e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.wifi")}),e.jsx(fe,{className:"h-4 w-4 text-primary"})]}),e.jsx("div",{className:"text-3xl font-bold text-on-surface",children:C}),e.jsx("div",{className:"mt-3 text-xs text-on-surface-variant",children:w})]})]}),e.jsxs("section",{className:"grid grid-cols-12 gap-4",children:[e.jsxs("article",{className:"col-span-12 rounded-xl border border-white/5 bg-surface-container-low p-6 lg:col-span-8",children:[e.jsxs("div",{className:"mb-6 flex items-center justify-between",children:[e.jsxs("div",{children:[e.jsx("h3",{className:"font-headline text-lg font-bold",children:n("sys.overview.wifiSummary")}),e.jsxs("p",{className:"text-xs text-on-surface-variant",children:[n("sys.overview.iface"),": ",String((t==null?void 0:t.wifi_interface)??"wlan0")]})]}),e.jsx("span",{className:"rounded border border-primary/30 bg-primary/10 px-2 py-1 text-[10px] uppercase tracking-widest text-primary",children:n("sys.overview.linkActive")})]}),e.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.signal")}),e.jsx("p",{className:"mt-1 font-mono text-2xl font-bold",children:w})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.quality")}),e.jsx("p",{className:"mt-1 font-mono text-2xl font-bold",children:C})]})]})]}),e.jsxs("aside",{className:"col-span-12 space-y-4 lg:col-span-4",children:[e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.metric.uptime")}),e.jsx("p",{className:"mt-1 text-xl font-bold",children:Ze(t==null?void 0:t.uptime_seconds)})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.metric.load")}),e.jsx("p",{className:"mt-1 text-xl font-bold",children:j})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.metric.storage")}),e.jsx(Fe,{className:"h-4 w-4 text-on-surface-variant"})]}),e.jsx("p",{className:"mt-2 text-sm text-on-surface-variant",children:n("sys.overview.storageComingSoon")})]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-lowest p-4",children:[e.jsxs("div",{className:"mb-3 flex flex-wrap items-center justify-between gap-3 border-b border-outline-variant/20 pb-2",children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("span",{className:"text-[10px] uppercase tracking-widest text-primary",children:n("sys.logs.title")}),e.jsxs("span",{className:"text-[10px] text-on-surface-variant",children:[n("sys.logs.kernel"),": ",String((t==null?void 0:t.os)??"—")]})]}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsxs("label",{className:"inline-flex items-center gap-2 text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:d,onChange:i=>N(i.target.checked)}),n("sys.logs.liveToggle")]}),e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-on-surface-variant hover:border-primary hover:text-on-surface",onClick:()=>void P(),children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(he,{className:"h-3.5 w-3.5"})," ",n("sys.logs.refresh")]})})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-2 text-xs",children:[["INFO","WARN","ERROR"].map(i=>e.jsxs("label",{className:"inline-flex items-center gap-1.5 text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:z.has(i),onChange:k=>{f(L=>k.target.checked?Array.from(new Set([...L,i])):L.filter(A=>A!==i))}}),e.jsx("span",{className:ye(i),children:i})]},i)),!d&&e.jsxs("span",{className:"inline-flex items-center gap-1 rounded border border-outline-variant/30 px-2 py-0.5 text-[11px] text-on-surface-variant",children:[e.jsx(Be,{className:"h-3.5 w-3.5"})," ",n("sys.logs.liveOffHint")]})]}),S&&e.jsx("div",{className:"mb-2 rounded border border-error/40 bg-error-container/20 px-2 py-1 text-xs text-on-error-container",children:S}),e.jsxs("div",{ref:_,className:"og-scrollbar max-h-72 space-y-1 overflow-auto font-mono text-[11px] text-on-surface-variant",children:[p&&y.length===0&&e.jsx("div",{children:n("sys.logs.loading")}),!p&&y.length===0&&e.jsx("div",{children:n("sys.logs.empty")}),y.map((i,k)=>e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsxs("span",{className:"shrink-0 text-primary",children:["[",Je(i.ts),"]"]}),e.jsx("span",{className:`shrink-0 ${ye(i.level)}`,children:i.level}),e.jsx("span",{className:"shrink-0 text-on-surface/80",children:i.source}),e.jsx("span",{className:"min-w-0 break-words text-on-surface",children:i.message})]},`${i.ts||"ts"}-${k}`))]})]})]})}function ie(){return typeof window.OGSCOPE_HTTP_PORT=="number"?window.OGSCOPE_HTTP_PORT:8e3}function Ve(s){const o=Math.max(0,parseInt(String(s??0),10)||0),n=Math.floor(o/86400),t=Math.floor(o%86400/3600),c=Math.floor(o%3600/60);return n>0?`${n}天 ${t}小时`:t>0?`${t}小时 ${c}分`:`${c}分`}function Qe(){const{info:s,error:o}=pe(),[n,t]=a.useState("加载中..."),[c,d]=a.useState(""),[N,m]=a.useState(""),[f,y]=a.useState([]),[g,S]=a.useState(!1),[v,p]=a.useState([]),[$,M]=a.useState(!1),[R,_]=a.useState(""),[E,h]=a.useState(""),[T,O]=a.useState(!1),[j,C]=a.useState(""),[w,P]=a.useState(`http://192.168.4.1:${ie()}`),[z,i]=a.useState("OGScope_xxxx"),[k,L]=a.useState(null),[A,I]=a.useState("—"),B=a.useCallback(r=>{const x=r.mode||"unknown",b=r.active_connection||"-",H=r.wireless_interface||"wlan0",D=r.ap_ipv4||"-",Z=r.configured?"是":"否",Y=r.message?`,消息: ${r.message}`:"";r.ap_url_hint&&P(r.ap_url_hint),r.ap_ssid&&i(r.ap_ssid);const te=ie();if(r.mdns_hostname_hint){const oe=`http://${r.mdns_hostname_hint}:${te}/debug`;L(oe),I(oe)}else if(r.device_id_suffix){const oe=`http://${`ogscope-${r.device_id_suffix}.local`}:${te}/debug`;L(oe),I(oe)}else L(null),I("未提供");t(`模式: ${x} | 活动连接: ${b} | 接口: ${H} | AP地址: ${D} | 已配置: ${Z}${Y}`)},[]),X=a.useCallback(async()=>{const r=await K("/api/network/wifi",{cache:"no-store"});B(r)},[B]);a.useEffect(()=>{X().catch(r=>t(`获取状态失败: ${r.message}`))},[X]);const Q=async r=>{t(`正在切换到 ${r.toUpperCase()}...`);const x=await K("/api/network/wifi",{method:"POST",body:JSON.stringify({mode:r})});B(x)},ae=async()=>{S(!0),m("扫描中..."),y([]);try{const r=await K("/api/network/wifi/scan",{cache:"no-store"}),x=r.networks||[],b=r.hint?` ${r.hint}`:"";y(x),m(`扫描到 ${x.length} 个网络${b}`.trim())}catch(r){m(`扫描失败: ${r instanceof Error?r.message:String(r)}`)}finally{S(!1)}},ee=async r=>{const x=window.prompt(`输入密码: ${r}`,"");if(x!==null){d(""),t("正在连接..."),O(!0);try{const b=await K("/api/network/wifi/sta/connect",{method:"POST",body:JSON.stringify({ssid:r,password:x||null})});B(b);const H=b.mode||"unknown";d(H==="sta"?`连接成功,当前连接: ${b.active_connection||"—"}`:`连接请求已提交,当前模式: ${H},连接: ${b.active_connection||"—"}`),window.setTimeout(()=>void X().catch(()=>{}),2500)}catch(b){t(`连接失败: ${b instanceof Error?b.message:String(b)}`),d(`错误详情: ${b instanceof Error?b.message:String(b)}`)}finally{O(!1)}}},se=async()=>{const r=R.trim();if(!r){window.alert("请输入 SSID");return}d(""),t("正在连接..."),O(!0);try{const x=await K("/api/network/wifi/sta/connect",{method:"POST",body:JSON.stringify({ssid:r,password:E||null})});B(x);const b=x.mode||"unknown";d(b==="sta"?`连接成功,当前连接: ${x.active_connection||"—"}`:`连接请求已提交,当前模式: ${b},连接: ${x.active_connection||"—"}`),window.setTimeout(()=>void X().catch(()=>{}),2500)}catch(x){t(`连接失败: ${x instanceof Error?x.message:String(x)}`),d(`错误详情: ${x instanceof Error?x.message:String(x)}`)}finally{O(!1)}},ne=async()=>{M(!0);try{const r=await K("/api/network/wifi/profiles",{cache:"no-store"});p(r.profiles||[])}catch(r){p([]),window.alert(r instanceof Error?r.message:String(r))}finally{M(!1)}},u=async r=>{try{await K("/api/network/wifi/profile/activate",{method:"POST",body:JSON.stringify({connection_name:r})}),t("已发送激活请求")}catch(x){window.alert(x instanceof Error?x.message:String(x))}};async function F(r,x){const b=`http://${r}:${x}/health`;try{const H=new AbortController,D=window.setTimeout(()=>H.abort(),700),Z=await fetch(b,{signal:H.signal,mode:"cors"});if(window.clearTimeout(D),!Z.ok)return null;const Y=await Z.json();if(Y&&Y.status==="healthy")return`http://${r}:${x}`}catch{}return null}const G=async()=>{const r=ie();C("扫描中...");const x=["192.168.0","192.168.1","192.168.31","10.0.0"];for(const b of x){const H=[];for(let D=1;D<255;D++)H.push(`${b}.${D}`);for(let D=0;DF(ce,r)))).find(Boolean);if(te){C(`已找到设备: ${te}`),window.location.href=`${te}/debug`;return}}}C("未找到设备")},l=a.useMemo(()=>{if(!s)return[];const r=s.wifi_quality!=null&&!Number.isNaN(Number(s.wifi_quality))?`${Number(s.wifi_quality).toFixed(1)}%`:"—",x=s.wifi_signal_dbm!=null&&!Number.isNaN(Number(s.wifi_signal_dbm))?`${Number(s.wifi_signal_dbm).toFixed(0)} dBm (${String(s.wifi_interface??"?")})`:"—";return[["平台",String(s.platform??"—")],["系统",String(s.os??"—")],["CPU 占用",`${Number(s.cpu_usage??0).toFixed(1)}%`],["内存占用",`${Number(s.memory_usage??0).toFixed(1)}%`],["CPU 温度",`${Number(s.temperature??0).toFixed(1)} °C`],["运行时长",Ve(s.uptime_seconds)],["1 分钟负载",String(s.load_average_1m??"—")],["WiFi 质量",r],["WiFi 信号",x]]},[s]);return e.jsxs("div",{className:"mx-auto max-w-7xl space-y-6",children:[e.jsxs("header",{children:[e.jsxs("div",{className:"flex items-center gap-2 text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:[e.jsx("span",{children:"Console"}),e.jsx("span",{children:"/"}),e.jsx("span",{className:"text-primary",children:"System_Network_Debug"})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:"NETWORK TERMINAL"}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:"WiFi 管理、模式切换、发现与恢复工具"})]}),o&&e.jsx("div",{className:"rounded-lg border border-error/40 bg-error-container/20 px-3 py-2 text-sm text-on-error-container",children:o}),e.jsxs("section",{className:"grid grid-cols-12 gap-6",children:[e.jsxs("div",{className:"col-span-12 space-y-6 lg:col-span-8",children:[e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-outline-variant/20 bg-surface-container-high px-4 py-3",children:[e.jsxs("div",{className:"flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-primary",children:[e.jsx(fe,{className:"h-4 w-4"}),"WiFi Control"]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-xs hover:border-primary",onClick:()=>void Q("sta").catch(r=>t(String(r))),children:"STA"}),e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-xs hover:border-primary",onClick:()=>void Q("ap").catch(r=>t(String(r))),children:"AP"}),e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 p-1.5 hover:border-primary",onClick:()=>void X().catch(r=>t(String(r))),children:e.jsx(he,{className:"h-3.5 w-3.5"})})]})]}),e.jsxs("div",{className:"space-y-3 p-4",children:[e.jsx("p",{className:"rounded border border-outline-variant/20 bg-surface-container-low p-3 font-mono text-xs text-on-surface",children:n}),e.jsxs("div",{className:"grid gap-2 text-xs text-on-surface-variant md:grid-cols-2",children:[e.jsxs("p",{children:["AP 地址:",e.jsx("span",{className:"font-mono text-on-surface",children:w})]}),e.jsxs("p",{children:["mDNS:"," ",k?e.jsx("a",{className:"font-mono text-primary underline",href:k,children:A}):e.jsx("span",{className:"font-mono text-on-surface",children:A})]})]})]})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between",children:[e.jsx("h3",{className:"text-sm font-semibold uppercase tracking-wider",children:"Scan Results"}),e.jsx("button",{type:"button",disabled:g,className:"rounded bg-primary-container px-3 py-1.5 text-xs font-medium text-on-primary-container disabled:opacity-50",onClick:()=>void ae(),children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(_e,{className:"h-3.5 w-3.5"})," 扫描 WiFi"]})})]}),e.jsx("p",{className:"mb-3 text-xs text-on-surface-variant",children:N||"由设备端 NetworkManager 执行扫描"}),e.jsx("div",{className:"overflow-x-auto",children:e.jsxs("table",{className:"w-full text-left text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-outline-variant/30 text-xs uppercase tracking-wider text-on-surface-variant",children:[e.jsx("th",{className:"p-2",children:"SSID"}),e.jsx("th",{className:"p-2",children:"信号"}),e.jsx("th",{className:"p-2",children:"安全"}),e.jsx("th",{className:"p-2 text-right",children:"操作"})]})}),e.jsx("tbody",{children:f.map(r=>e.jsxs("tr",{className:"border-b border-outline-variant/10",children:[e.jsx("td",{className:"p-2 font-mono",children:r.ssid}),e.jsx("td",{className:"p-2",children:r.signal??"—"}),e.jsx("td",{className:"p-2",children:r.security??"—"}),e.jsx("td",{className:"p-2 text-right",children:e.jsx("button",{type:"button",disabled:T,className:"rounded border border-primary/40 px-2 py-1 text-xs text-primary disabled:opacity-50",onClick:()=>void ee(r.ssid),children:"Connect"})})]},r.ssid))})]})})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:"Known Networks"}),e.jsx("button",{type:"button",disabled:$,className:"rounded border border-outline-variant/40 px-3 py-1.5 text-xs disabled:opacity-50",onClick:()=>void ne(),children:"刷新已保存网络"}),e.jsxs("table",{className:"mt-3 w-full text-left text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-outline-variant/30 text-xs uppercase tracking-wider text-on-surface-variant",children:[e.jsx("th",{className:"p-2",children:"连接名"}),e.jsx("th",{className:"p-2",children:"SSID"}),e.jsx("th",{className:"p-2",children:"自动连接"}),e.jsx("th",{className:"p-2 text-right",children:"操作"})]})}),e.jsx("tbody",{children:v.map(r=>e.jsxs("tr",{className:"border-b border-outline-variant/10",children:[e.jsx("td",{className:"p-2",children:r.connection_name}),e.jsx("td",{className:"p-2",children:r.ssid}),e.jsx("td",{className:"p-2",children:r.autoconnect?"是":"否"}),e.jsx("td",{className:"p-2 text-right",children:e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-xs hover:border-primary",onClick:()=>void u(r.connection_name),children:"Activate"})})]},r.connection_name))})]})]})]}),e.jsxs("aside",{className:"col-span-12 space-y-6 lg:col-span-4",children:[e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:"System Monitor"}),e.jsx("div",{className:"grid gap-2",children:l.map(([r,x])=>e.jsxs("div",{className:"flex justify-between border-b border-outline-variant/10 pb-1 text-xs",children:[e.jsx("span",{className:"text-on-surface-variant",children:r}),e.jsx("span",{className:"font-mono text-on-surface",children:x})]},r))})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:"Manual Connect"}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("label",{className:"block text-xs text-on-surface-variant",children:["SSID",e.jsx("input",{className:"mt-1 w-full rounded border border-outline-variant/40 bg-surface-container-low px-2 py-1.5 text-sm",value:R,onChange:r=>_(r.target.value)})]}),e.jsxs("label",{className:"block text-xs text-on-surface-variant",children:["Password",e.jsx("input",{type:"password",className:"mt-1 w-full rounded border border-outline-variant/40 bg-surface-container-low px-2 py-1.5 text-sm",value:E,onChange:r=>h(r.target.value)})]}),e.jsx("button",{type:"button",disabled:T,className:"w-full rounded bg-primary-container px-4 py-2 text-sm font-medium text-on-primary-container disabled:opacity-50",onClick:()=>void se(),children:"连接并切换 STA"})]}),e.jsx("p",{className:"mt-2 text-xs text-on-surface-variant",children:c})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:"WiFi 引导"}),e.jsxs("ol",{className:"list-decimal space-y-1 pl-4 text-xs text-on-surface-variant",children:[e.jsxs("li",{children:["连接热点 ",e.jsx("strong",{children:z}),",密码 ",e.jsx("code",{children:"ogscopeadmin"})]}),e.jsxs("li",{children:["浏览器打开 ",e.jsxs("span",{className:"font-mono",children:["http://192.168.4.1:",ie()]})]}),e.jsx("li",{children:"扫描 WiFi 或手动填写 SSID 连接"})]})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:"Find Device"}),e.jsx("p",{className:"mb-3 text-xs text-on-surface-variant",children:"扫描常见网段并探测 /health"}),e.jsx("button",{type:"button",className:"w-full rounded border border-outline-variant/40 px-3 py-2 text-sm hover:border-primary",onClick:()=>void G(),children:"扫描局域网"}),e.jsx("p",{className:"mt-2 text-xs text-on-surface-variant",children:j})]})]})]})]})}function es(s){var t,c;const o=(c=(t=s==null?void 0:s.data)==null?void 0:t.services)==null?void 0:c.hmi;if(!o||typeof o!="object")return null;const n=o.display;return!n||typeof n!="object"?null:n}function ss(s){try{return JSON.stringify(s,null,2)}catch{return String(s)}}function ts(){var j,C;const{t:s}=J(),[o,n]=a.useState(null),[t,c]=a.useState(null),[d,N]=a.useState(!1),[m,f]=a.useState(null),[y,g]=a.useState(null),[S,v]=a.useState(40),[p,$]=a.useState(80),[M,R]=a.useState(200),_=a.useCallback(async()=>{try{c(null);const w=await Ke();n(w)}catch(w){n(null),c(w instanceof Error?w.message:String(w))}},[]);a.useEffect(()=>{_()},[_]);const E=a.useCallback(async(w,P={})=>{var z;N(!0),g(null);try{const i=await Xe({target:"hmi",action:w,payload:P,timeout_ms:8e3});if(f(i),!i.success){const A=((z=i.error)==null?void 0:z.message)??"RPC failed";g(A);return}const k=i.data,L=k==null?void 0:k.result;L&&L.accepted===!1&&L.message?g(L.message):g(null),await _()}catch(i){f(null),g(i instanceof Error?i.message:String(i))}finally{N(!1)}},[_]),h=es(o),T=(C=(j=o==null?void 0:o.data)==null?void 0:j.services)==null?void 0:C.hmi,O=typeof(T==null?void 0:T.screen_on)=="boolean"?T.screen_on:void 0;return e.jsxs("div",{className:"mx-auto max-w-6xl space-y-6",children:[e.jsxs("header",{children:[e.jsx("div",{className:"text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:s("sys.placeholder.breadcrumb")}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:s("sys.hmi.title")}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:s("sys.hmi.desc")})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:s("sys.hmi.status.section")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 bg-surface-container px-3 py-1.5 text-xs font-medium text-on-surface hover:bg-surface-container-high",onClick:()=>void _(),disabled:d,children:s("sys.hmi.status.refresh")})]}),t?e.jsx("p",{className:"mt-2 text-sm text-error",children:t}):e.jsxs("dl",{className:"mt-3 grid gap-2 font-mono text-[11px] text-on-surface-variant sm:grid-cols-2",children:[e.jsxs("div",{children:[e.jsx("dt",{className:"text-on-surface-variant",children:s("sys.hmi.status.displayEnabled")}),e.jsx("dd",{className:"text-on-surface",children:(h==null?void 0:h.enabled)===!0?"true":"false"})]}),e.jsxs("div",{children:[e.jsx("dt",{children:s("sys.hmi.status.spidev")}),e.jsx("dd",{className:h!=null&&h.spidev_present?"text-primary":"text-error",children:h!=null&&h.spidev_present?s("sys.hmi.status.yes"):s("sys.hmi.status.no")})]}),e.jsxs("div",{children:[e.jsx("dt",{children:s("sys.hmi.status.resolution")}),e.jsxs("dd",{children:[(h==null?void 0:h.width)??"—"," × ",(h==null?void 0:h.height)??"—"," · DC GPIO ",(h==null?void 0:h.dc_pin)??"—"]})]}),e.jsxs("div",{children:[e.jsx("dt",{children:s("sys.hmi.status.driver")}),e.jsx("dd",{children:h!=null&&h.driver_open?s("sys.hmi.status.open"):s("sys.hmi.status.closed")})]}),e.jsxs("div",{children:[e.jsx("dt",{children:s("sys.hmi.status.screenOutput")}),e.jsx("dd",{children:O===void 0?"—":s(O?"sys.hmi.status.on":"sys.hmi.status.off")})]}),e.jsxs("div",{className:"sm:col-span-2",children:[e.jsx("dt",{children:s("sys.hmi.status.lastPattern")}),e.jsx("dd",{children:(h==null?void 0:h.last_pattern)??"—"})]}),h!=null&&h.last_error?e.jsxs("div",{className:"sm:col-span-2",children:[e.jsx("dt",{children:s("sys.hmi.status.lastError")}),e.jsx("dd",{className:"text-error",children:h.last_error})]}):null]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:s("sys.hmi.actions.section")}),e.jsx("p",{className:"mt-1 text-[11px] text-on-surface-variant",children:s("sys.hmi.actions.hint")}),e.jsxs("div",{className:"mt-4 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",className:"rounded-lg bg-primary px-3 py-2 text-xs font-semibold text-on-primary hover:opacity-90 disabled:opacity-50",disabled:d,onClick:()=>void E("display.test_pattern",{pattern:"smoke"}),children:s("sys.hmi.actions.smoke")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 bg-surface-container px-3 py-2 text-xs font-medium text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("display.test_pattern",{pattern:"colorbars"}),children:s("sys.hmi.actions.colorbars")})]}),e.jsxs("div",{className:"mt-6 flex flex-wrap items-end gap-3",children:[e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:["R",e.jsx("input",{type:"number",min:0,max:255,className:"w-20 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:S,onChange:w=>v(Number(w.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:["G",e.jsx("input",{type:"number",min:0,max:255,className:"w-20 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:p,onChange:w=>$(Number(w.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:["B",e.jsx("input",{type:"number",min:0,max:255,className:"w-20 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:M,onChange:w=>R(Number(w.target.value))})]}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 bg-surface-container px-3 py-2 text-xs font-medium text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("display.test_pattern",{pattern:"fill",r:S,g:p,b:M}),children:s("sys.hmi.actions.fill")})]}),e.jsxs("div",{className:"mt-6 flex flex-wrap gap-2 border-t border-outline-variant/20 pt-4",children:[e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("screen.set",{on:!0}),children:s("sys.hmi.actions.screenOn")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("screen.set",{on:!1}),children:s("sys.hmi.actions.screenOff")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("display.release"),children:s("sys.hmi.actions.release")})]}),y?e.jsx("p",{className:"mt-3 text-sm text-error",children:y}):null,e.jsxs("details",{className:"mt-4",children:[e.jsx("summary",{className:"cursor-pointer text-[11px] text-on-surface-variant",children:s("sys.hmi.rawJson")}),e.jsx("pre",{className:"mt-2 max-h-64 overflow-auto rounded border border-outline-variant/30 bg-surface-container p-2 font-mono text-[10px] text-on-surface",children:m?ss(m):"—"})]})]})]})}const rs={sensors:{titleKey:"sys.placeholder.sensors.title",descKey:"sys.placeholder.sensors.desc",blocks:["sys.placeholder.sensors.block1","sys.placeholder.sensors.block2","sys.placeholder.sensors.block3"]},hmi:{titleKey:"sys.placeholder.hmi.title",descKey:"sys.placeholder.hmi.desc",blocks:["sys.placeholder.hmi.block1","sys.placeholder.hmi.block2","sys.placeholder.hmi.block3"]},power:{titleKey:"sys.placeholder.power.title",descKey:"sys.placeholder.power.desc",blocks:["sys.placeholder.power.block1","sys.placeholder.power.block2","sys.placeholder.power.block3"]}};function ve({scope:s}){const{t:o}=J(),n=rs[s];return e.jsxs("div",{className:"mx-auto max-w-6xl space-y-6",children:[e.jsxs("header",{children:[e.jsx("div",{className:"text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:o("sys.placeholder.breadcrumb")}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:o(n.titleKey)}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:o(n.descKey)})]}),e.jsx("section",{className:"grid grid-cols-12 gap-4",children:n.blocks.map(t=>e.jsxs("article",{className:"col-span-12 rounded-xl border border-dashed border-outline-variant/40 bg-surface-container/60 p-5 md:col-span-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-primary",children:o("sys.placeholder.block")}),e.jsx("h3",{className:"mt-2 text-lg font-semibold",children:o(t)}),e.jsx("p",{className:"mt-2 text-sm text-on-surface-variant",children:o("sys.placeholder.desc")})]},t))}),e.jsx("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:e.jsx("p",{className:"font-mono text-xs text-on-surface-variant",children:o("sys.placeholder.status")})})]})}const W=62,re=4;function as(s){const{rollDeg:o,pitchDeg:n,yawRateDps:t}=s,{t:c}=J(),d=`rotateX(${-n}deg) rotateY(${o}deg)`;return e.jsxs("div",{className:"mt-5 rounded-xl border border-teal-500/25 bg-black/25 p-4 ring-1 ring-teal-500/10",children:[e.jsx("p",{className:"text-center text-[11px] font-semibold text-teal-200/95",children:c("sys.sensors.gyro.att3dTitle")}),e.jsx("p",{className:"mx-auto mt-1 max-w-xl text-center text-[10px] leading-relaxed text-on-surface-variant",children:c("sys.sensors.gyro.att3dDesc")}),e.jsxs("div",{className:"mt-4 flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between xl:gap-8",children:[e.jsxs("div",{className:"flex min-w-0 flex-1 flex-col items-center gap-4",children:[e.jsx("div",{className:"flex shrink-0 items-center justify-center py-2",style:{perspective:"820px"},children:e.jsxs("div",{className:"relative h-[188px] w-[220px]",style:{transformStyle:"preserve-3d",transform:d},children:[e.jsxs("div",{className:"absolute left-[10px] top-[18px] h-[152px] w-[200px] rounded-2xl border-2 border-teal-400/75 bg-gradient-to-br from-slate-600/90 via-slate-800/95 to-slate-950 shadow-[0_20px_50px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.08)] transition-transform duration-150 ease-out",style:{transformStyle:"preserve-3d"},children:[e.jsx("div",{className:"pointer-events-none absolute inset-x-0 top-2 flex justify-center",children:e.jsx("span",{className:"rounded bg-black/35 px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.2em] text-teal-100",children:"TOP"})}),e.jsx("div",{className:"absolute bottom-2 left-2 font-mono text-[9px] text-slate-400",children:"MPU-6050"})]}),e.jsxs("div",{className:"pointer-events-none absolute left-[110px] top-[94px] h-0 w-0",style:{transformStyle:"preserve-3d"},children:[e.jsx("div",{className:"absolute rounded-full bg-white/90 shadow-[0_0_6px_rgba(255,255,255,0.6)]",style:{width:7,height:7,left:-3.5,top:-3.5,transform:"translateZ(0.5px)"}}),e.jsx("div",{className:"absolute bg-amber-400 shadow-md ring-1 ring-amber-200/35",style:{width:W,height:re,left:0,top:-re/2,transformOrigin:"0 50%"}}),e.jsx("span",{className:"absolute whitespace-nowrap font-mono text-[10px] font-bold text-amber-200",style:{left:W+4,top:-8},children:c("sys.sensors.gyro.axisXLabel")}),e.jsx("div",{className:"absolute bg-sky-400 shadow-md ring-1 ring-sky-200/30",style:{width:W,height:re,left:0,top:-re/2,transformOrigin:"0 50%",transform:"rotateZ(90deg)"}}),e.jsx("span",{className:"absolute whitespace-nowrap font-mono text-[10px] font-bold text-sky-200",style:{left:-6,top:-W-16},children:c("sys.sensors.gyro.axisYLabel")}),e.jsx("div",{className:"absolute bg-violet-400 shadow-md ring-1 ring-violet-200/35",style:{width:W,height:re,left:0,top:-re/2,transformOrigin:"0 50%",transform:"rotateY(-90deg)"}}),e.jsx("span",{className:"absolute whitespace-nowrap font-mono text-[10px] font-bold text-violet-200",style:{left:W*.35,top:-W*.45,transform:"translateZ(28px)"},children:c("sys.sensors.gyro.axisZLabel")}),e.jsx("div",{className:"absolute bg-amber-900/55",style:{width:W*.45,height:2,left:-W*.45,top:-1,transformOrigin:"100% 50%"}}),e.jsx("div",{className:"absolute bg-sky-900/50",style:{width:W*.45,height:2,left:0,top:-1,transformOrigin:"0 50%",transform:`rotateZ(90deg) translateX(${-W*.45}px)`}}),e.jsx("div",{className:"absolute bg-violet-900/45",style:{width:W*.4,height:2,left:0,top:-1,transformOrigin:"0 50%",transform:`rotateY(-90deg) translateX(${-W*.4}px)`}})]})]})}),e.jsx("p",{className:"max-w-md text-center text-[10px] leading-snug text-on-surface-variant",children:c("sys.sensors.gyro.att3dBodyAxes")})]}),e.jsxs("div",{className:"flex w-full max-w-[220px] shrink-0 flex-col items-center gap-2 self-center xl:self-start",children:[e.jsx("p",{className:"text-center text-[10px] font-medium text-on-surface-variant",children:c("sys.sensors.gyro.att3dRefTitle")}),e.jsx("div",{className:"flex items-center justify-center rounded-lg border border-outline-variant/30 bg-slate-950/50 px-4 py-5 ring-1 ring-white/5",children:e.jsx("div",{style:{perspective:"280px"},children:e.jsx("div",{className:"relative h-[100px] w-[100px]",style:{transformStyle:"preserve-3d",transform:"rotateX(58deg) rotateZ(-42deg)"},children:e.jsxs("div",{className:"absolute left-1/2 top-1/2 h-0 w-0",style:{transformStyle:"preserve-3d"},children:[e.jsx("div",{className:"absolute bg-amber-400/95",style:{width:44,height:3,left:0,top:-1.5,transformOrigin:"0 50%"}}),e.jsx("div",{className:"absolute bg-sky-400/95",style:{width:44,height:3,left:0,top:-1.5,transformOrigin:"0 50%",transform:"rotateZ(90deg)"}}),e.jsx("div",{className:"absolute bg-violet-400/95",style:{width:44,height:3,left:0,top:-1.5,transformOrigin:"0 50%",transform:"rotateY(-90deg)"}}),e.jsx("div",{className:"absolute rounded-full bg-white/80",style:{width:5,height:5,left:-2.5,top:-2.5}})]})})})}),e.jsx("p",{className:"text-center text-[9px] leading-relaxed text-on-surface-variant/85",children:c("sys.sensors.gyro.att3dRefDesc")})]}),e.jsxs("div",{className:"grid w-full min-w-[200px] max-w-md grid-cols-3 gap-3 font-mono text-[11px] lg:max-w-lg xl:max-w-[340px]",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/80 px-2 py-2 text-center",children:[e.jsx("p",{className:"text-[9px] uppercase tracking-wider text-on-surface-variant",children:c("sys.sensors.gyro.roll")}),e.jsxs("p",{className:"mt-1 text-lg font-bold tabular-nums text-teal-200",children:[o.toFixed(1),"°"]})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/80 px-2 py-2 text-center",children:[e.jsx("p",{className:"text-[9px] uppercase tracking-wider text-on-surface-variant",children:c("sys.sensors.gyro.pitch")}),e.jsxs("p",{className:"mt-1 text-lg font-bold tabular-nums text-teal-200",children:[n.toFixed(1),"°"]})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/80 px-2 py-2 text-center",children:[e.jsx("p",{className:"text-[9px] uppercase tracking-wider text-on-surface-variant",children:c("sys.sensors.gyro.yawRate")}),e.jsx("p",{className:"mt-1 text-lg font-bold tabular-nums text-amber-200/95",children:t.toFixed(2)}),e.jsx("p",{className:"text-[9px] text-on-surface-variant",children:"°/s"})]})]})]})]})}const xe=250;function ue(s){const{label:o,value:n,maxAbs:t,unit:c}=s,d=Math.max(-1,Math.min(1,n/t)),N=d>=0?50:50+d*50,m=Math.abs(d)*50;return e.jsxs("div",{className:"space-y-1",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-2",children:[e.jsx("span",{className:"text-[11px] font-medium text-on-surface",children:o}),e.jsxs("span",{className:"font-mono text-xs tabular-nums text-sky-100",children:[n.toFixed(2),e.jsxs("span",{className:"text-on-surface-variant",children:[" ",c]})]})]}),e.jsxs("div",{className:"relative h-5 w-full overflow-hidden rounded-md bg-slate-900/80 ring-1 ring-slate-600/50",children:[e.jsx("div",{className:"absolute left-1/2 top-0 z-10 h-full w-px -translate-x-px bg-slate-500/90"}),e.jsx("div",{className:"absolute top-1 h-3 rounded-sm bg-gradient-to-r from-emerald-600 to-sky-500 shadow-sm",style:{left:`${N}%`,width:`${Math.max(m,d===0?0:.8)}%`}})]})]})}function ns(s){const{bus:o,addr:n}=s,{t}=J(),[c,d]=a.useState(!1),[N,m]=a.useState(null),[f,y]=a.useState(null),[g,S]=a.useState(null),[v,p]=a.useState(null),[$,M]=a.useState(null),[R,_]=a.useState(null),[E,h]=a.useState(!1),T=a.useRef(null),O=a.useCallback(async()=>{var C;d(!0),m(null);try{const w=new URLSearchParams({bus:String(o),addr:String(n)}),P=await U(`/api/debug/sensors/mpu6050/imu-sample?${w.toString()}`);if(!P.success){y(null),S(null),p(null),M(null),_(null),m(((C=P.sample)==null?void 0:C.error)||P.error||t("sys.sensors.gyro.errUnknown"));return}y(P.gyro_dps??null),S(P.gyro_raw??null),p(P.tilt_deg??null),M(P.yaw_rate_dps??null),_(P.accel_g??null)}catch(w){y(null),S(null),p(null),M(null),_(null),m(w instanceof Error?w.message:String(w))}finally{d(!1)}},[n,o,t]);a.useEffect(()=>{if(!E){T.current&&(clearInterval(T.current),T.current=null);return}return T.current=setInterval(()=>void O(),500),()=>{T.current&&clearInterval(T.current)}},[E,O]);const j=v!=null&&$!=null&&Number.isFinite(v.roll)&&Number.isFinite(v.pitch);return e.jsxs("div",{className:"mt-6 rounded-xl border border-emerald-600/35 bg-gradient-to-br from-slate-900/40 to-surface-container/70 p-4",children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-semibold text-on-surface",children:t("sys.sensors.gyro.title")}),e.jsx("p",{className:"mt-1 max-w-xl text-[11px] leading-snug text-on-surface-variant",children:t("sys.sensors.gyro.subtitle")})]}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("label",{className:"flex cursor-pointer items-center gap-1.5 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:E,onChange:C=>h(C.target.checked)}),t("sys.sensors.gyro.live")]}),e.jsx("button",{type:"button",disabled:c,className:"rounded-lg bg-emerald-800/90 px-3 py-1.5 text-xs font-medium text-emerald-50 hover:bg-emerald-700 disabled:opacity-50",onClick:()=>void O(),children:t(c?"sys.sensors.gyro.loading":"sys.sensors.gyro.btn")})]})]}),j&&e.jsx(as,{rollDeg:v.roll,pitchDeg:v.pitch,yawRateDps:$}),f&&e.jsxs("div",{className:"mt-5 grid gap-4 md:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-emerald-500/20 bg-black/20 px-3 py-3 text-center md:col-span-1",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"ωx"}),e.jsx("p",{className:"font-mono text-3xl font-bold tabular-nums text-emerald-200",children:f.x.toFixed(2)}),e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"°/s"})]}),e.jsxs("div",{className:"rounded-lg border border-emerald-500/20 bg-black/20 px-3 py-3 text-center md:col-span-1",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"ωy"}),e.jsx("p",{className:"font-mono text-3xl font-bold tabular-nums text-emerald-200",children:f.y.toFixed(2)}),e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"°/s"})]}),e.jsxs("div",{className:"rounded-lg border border-emerald-500/20 bg-black/20 px-3 py-3 text-center md:col-span-1",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"ωz"}),e.jsx("p",{className:"font-mono text-3xl font-bold tabular-nums text-emerald-200",children:f.z.toFixed(2)}),e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"°/s"})]})]}),f&&e.jsxs("div",{className:"mt-5 space-y-4",children:[e.jsx("p",{className:"text-[10px] font-medium uppercase tracking-wider text-on-surface-variant",children:t("sys.sensors.gyro.barsTitle")}),e.jsx(ue,{label:"X",value:f.x,maxAbs:xe,unit:"°/s"}),e.jsx(ue,{label:"Y",value:f.y,maxAbs:xe,unit:"°/s"}),e.jsx(ue,{label:"Z",value:f.z,maxAbs:xe,unit:"°/s"})]}),R&&e.jsxs("p",{className:"mt-3 text-center font-mono text-[10px] text-on-surface-variant",children:["g — X:",R.x.toFixed(3)," Y:",R.y.toFixed(3)," Z:",R.z.toFixed(3)]}),g&&e.jsxs("div",{className:"mt-4 rounded border border-outline-variant/25 bg-surface-container/50 px-3 py-2",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:t("sys.sensors.gyro.rawBlock")}),e.jsxs("p",{className:"mt-1 font-mono text-[11px] tabular-nums text-on-surface",children:["raw X=",g.x," · Y=",g.y," · Z=",g.z]})]}),!f&&!N&&!c&&e.jsx("p",{className:"mt-4 text-[11px] text-on-surface-variant",children:t("sys.sensors.gyro.hint")}),N&&e.jsx("p",{className:"mt-3 rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 font-mono text-[11px] text-amber-100",children:N})]})}const be=3.4,Ce=7,os=3;function ls(s){const o=(s%360+360)%360;return o<1||o>359?"cardN":Math.abs(o-90)<1?"cardE":Math.abs(o-180)<1?"cardS":Math.abs(o-270)<1?"cardW":null}function is(){const s=[],o=Ce*360;for(let n=0;n<=o;n+=5){const t=n%360,c=n%30===0,d=!c&&n%10===0,N=ls(t),m=!N&&c&&t%90!==0?t:void 0;s.push({x:n*be,deg:n,h:c?"maj":d?"mid":"min",degLabel:m,cardKey:N??void 0})}return s}const cs=is(),je=Ce*360*be;function ds(s,o){const n=(s%360+360)%360;let c=(o%360+360)%360-n;return c=(c+180)%360-180,s+c}function xs(s){const{bus:o,addr:n}=s,{t}=J(),[c,d]=a.useState(!1),[N,m]=a.useState(null),[f,y]=a.useState(null),[g,S]=a.useState(null),[v,p]=a.useState(!1),$=a.useRef(null),M=a.useRef(0),R=a.useRef(null),[_,E]=a.useState(320);a.useEffect(()=>{const j=R.current;if(!j)return;const C=new ResizeObserver(()=>{E(Math.max(200,j.clientWidth))});return C.observe(j),E(Math.max(200,j.clientWidth)),()=>C.disconnect()},[]),a.useEffect(()=>{y(null),S(null),m(null)},[o,n]);const h=a.useCallback(async()=>{const j=++M.current;d(!0);try{const C=new URLSearchParams({bus:String(o),addr:String(n)}),w=await U(`/api/debug/sensors/magnetometer/sample?${C.toString()}`);if(j!==M.current)return;if(!w.success){y(null),S(null),m(w.error||t("sys.sensors.compass.err"));return}m(null);const P=w.heading_deg??null;P!=null&&y(z=>z==null?os*360+P:ds(z,P)),S(w.field_ut??null)}catch(C){if(j!==M.current)return;y(null),S(null),m(C instanceof Error?C.message:String(C))}finally{j===M.current&&d(!1)}},[n,o,t]);a.useEffect(()=>{if(!v){$.current&&(clearInterval($.current),$.current=null);return}return $.current=setInterval(()=>{h()},850),()=>{$.current&&clearInterval($.current)}},[v,h]);const T=a.useMemo(()=>{if(f==null)return 0;const j=f*be;return _/2-j},[f,_]),O=f!=null?(f%360+360)%360:null;return e.jsxs("div",{className:"rounded-xl border border-sky-500/30 bg-gradient-to-b from-slate-900/90 via-slate-900/70 to-surface-container/90 p-4",children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-semibold text-on-surface",children:t("sys.sensors.compass.title")}),e.jsx("p",{className:"mt-1 max-w-xl text-[11px] leading-snug text-on-surface-variant",children:t("sys.sensors.compass.desc")})]}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("label",{className:"flex cursor-pointer items-center gap-1.5 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:v,onChange:j=>p(j.target.checked)}),t("sys.sensors.compass.live")]}),e.jsx("button",{type:"button",disabled:c,className:"rounded-lg bg-sky-700/80 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-600 disabled:opacity-50",onClick:()=>void h(),children:t(c?"sys.sensors.compass.loading":"sys.sensors.compass.btn")})]})]}),e.jsxs("div",{className:"mt-5",children:[e.jsx("p",{className:"mb-2 text-center text-[10px] uppercase tracking-[0.2em] text-sky-300/90",children:t("sys.sensors.compass.tapeCaption")}),e.jsxs("div",{ref:R,className:"relative mx-auto h-[112px] w-full max-w-3xl overflow-hidden rounded-lg ring-1 ring-sky-500/25",style:{maskImage:"linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%)",WebkitMaskImage:"linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%)"},children:[e.jsx("div",{className:"pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center",children:e.jsxs("div",{className:"flex flex-col items-center",children:[e.jsx("div",{className:"h-0 w-0 border-x-[9px] border-x-transparent border-b-[12px] border-b-amber-400 drop-shadow"}),e.jsx("div",{className:"h-[88px] w-0.5 rounded-full bg-gradient-to-b from-amber-300/95 to-sky-400/40"})]})}),e.jsx("div",{className:"absolute bottom-0 left-0 top-0 will-change-transform",style:{width:je,transform:`translateX(${T}px)`,transition:"transform 0.42s cubic-bezier(0.22, 0.95, 0.28, 1)"},children:e.jsxs("div",{className:"relative h-full",style:{width:je,background:"linear-gradient(180deg, rgba(15,23,42,0.2) 0%, rgba(30,41,59,0.85) 40%, rgba(15,23,42,0.95) 100%)"},children:[e.jsx("div",{className:"absolute left-0 right-0 top-8 h-px bg-slate-600/60"}),cs.map(j=>{const C=j.h==="maj"?22:j.h==="mid"?14:8;return e.jsxs("div",{className:"absolute flex flex-col items-center",style:{left:j.x,transform:"translateX(-50%)",top:32},children:[(j.cardKey||j.degLabel!=null)&&e.jsx("span",{className:`mb-0.5 whitespace-nowrap font-mono ${j.cardKey?"text-[13px] font-bold text-sky-200":"text-[10px] font-medium text-slate-400"}`,style:{marginTop:-18},children:j.cardKey?t(`sys.sensors.compass.${j.cardKey}`):String(j.degLabel)}),e.jsx("div",{className:`w-px rounded-full ${j.h==="maj"?"bg-sky-300/90":j.h==="mid"?"bg-slate-500/85":"bg-slate-600/50"}`,style:{height:C}})]},j.deg)}),e.jsx("div",{className:"absolute bottom-6 left-0 right-0 h-px bg-slate-600/40"})]})})]})]}),e.jsxs("div",{className:"mt-5 flex flex-col gap-4 md:flex-row md:items-start md:justify-center md:gap-8",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/90 px-6 py-4 text-center md:min-w-[200px]",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:t("sys.sensors.compass.headingLabel")}),e.jsx("p",{className:"font-mono text-4xl font-bold tabular-nums text-sky-200",children:O!=null?`${O.toFixed(1)}°`:"—"}),e.jsx("p",{className:"mt-1 text-[10px] text-on-surface-variant",children:t("sys.sensors.compass.headingHint")})]}),g&&e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-4 py-3 font-mono text-[11px] text-on-surface md:max-w-md",children:[e.jsx("p",{className:"mb-1 text-[10px] text-on-surface-variant",children:"µT (X / Y / Z)"}),e.jsxs("p",{className:"tabular-nums",children:[g.x.toFixed(2)," · ",g.y.toFixed(2)," · ",g.z.toFixed(2)]})]})]}),N&&e.jsx("p",{className:"mt-3 rounded border border-amber-500/40 bg-amber-500/10 px-3 py-2 font-mono text-[11px] text-amber-100",children:N}),e.jsx("p",{className:"mt-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:t("sys.sensors.compass.footnote")})]})}function Ne(s){try{return JSON.stringify(s,null,2)}catch{return String(s)}}function us(){const{t:s}=J(),[o,n]=a.useState(1),[t,c]=a.useState(12),[d,N]=a.useState(!0),[m,f]=a.useState(!1),[y,g]=a.useState(null),[S,v]=a.useState(null),[p,$]=a.useState(null),[M,R]=a.useState(null),[_,E]=a.useState(1),[h,T]=a.useState(104),[O,j]=a.useState(!0),[C,w]=a.useState(!1),[P,z]=a.useState(null),[i,k]=a.useState(null),L=a.useCallback(async()=>{f(!0),g(null);try{const u=new URLSearchParams({bus:String(o),addr:String(t),i2cdetect:d?"true":"false"}),F=await U(`/api/debug/sensors/magnetometer/selftest?${u.toString()}`);v(F)}catch(u){v(null),g(u instanceof Error?u.message:String(u))}finally{f(!1)}},[t,o,d]),A=a.useCallback(async()=>{f(!0),g(null);try{const u=new URLSearchParams({addr:String(t)}),F=await U(`/api/debug/sensors/magnetometer/probe-buses?${u.toString()}`);v(F)}catch(u){v(null),g(u instanceof Error?u.message:String(u))}finally{f(!1)}},[t]),I=a.useCallback(async()=>{f(!0),g(null);try{const u=new URLSearchParams({bus:String(o),addr:String(t)}),F=await U(`/api/debug/sensors/magnetometer/calibration/start?${u.toString()}`,{method:"POST"});v(F),R("已开始方向校准,请缓慢旋转设备 5-15 秒。");const G=await U(`/api/debug/sensors/magnetometer/calibration/status?${u.toString()}`);$(G)}catch(u){v(null),g(u instanceof Error?u.message:String(u))}finally{f(!1)}},[t,o]),B=a.useCallback(async()=>{f(!0),g(null);try{const u=new URLSearchParams({bus:String(o),addr:String(t)}),F=await U(`/api/debug/sensors/magnetometer/calibration/commit?${u.toString()}`,{method:"POST"});v(F),R("已保存并锁定方向校准。");const G=await U(`/api/debug/sensors/magnetometer/calibration/status?${u.toString()}`);$(G)}catch(u){v(null),g(u instanceof Error?u.message:String(u))}finally{f(!1)}},[t,o]),X=a.useCallback(async()=>{f(!0),g(null);try{const u=new URLSearchParams({bus:String(o),addr:String(t)}),F=await U(`/api/debug/sensors/magnetometer/calibration/reset?${u.toString()}`,{method:"POST"});v(F),R("已重置到自动模式。");const G=await U(`/api/debug/sensors/magnetometer/calibration/status?${u.toString()}`);$(G)}catch(u){v(null),g(u instanceof Error?u.message:String(u))}finally{f(!1)}},[t,o]),Q=a.useCallback(async()=>{f(!0),g(null);try{const u=new URLSearchParams({bus:String(o),addr:String(t)}),F=await U(`/api/debug/sensors/magnetometer/calibration/status?${u.toString()}`);v(F),$(F)}catch(u){v(null),g(u instanceof Error?u.message:String(u))}finally{f(!1)}},[t,o]);a.useEffect(()=>{const u=new URLSearchParams({bus:String(o),addr:String(t)});U(`/api/debug/sensors/magnetometer/calibration/status?${u.toString()}`).then(F=>$(F)).catch(()=>{})},[t,o]),a.useEffect(()=>{if((p==null?void 0:p.mode)!=="recording")return;const u=setInterval(()=>{const F=new URLSearchParams({bus:String(o),addr:String(t)});U(`/api/debug/sensors/magnetometer/calibration/status?${F.toString()}`).then(G=>$(G)).catch(()=>{})},900);return()=>clearInterval(u)},[p==null?void 0:p.mode,o,t]);const ae=a.useMemo(()=>{const u=(p==null?void 0:p.mode)??"auto";return s(u==="recording"?"sys.sensors.mag.calModeRecording":u==="locked"?"sys.sensors.mag.calModeLocked":"sys.sensors.mag.calModeAuto")},[p==null?void 0:p.mode,s]),ee=Number((p==null?void 0:p.samples)??0),se=(p==null?void 0:p.mode)==="recording"&&ee>=10,ne=a.useCallback(async()=>{w(!0),z(null);try{const u=new URLSearchParams({bus:String(_),addr:String(h),i2cdetect:O?"true":"false"}),F=await U(`/api/debug/sensors/mpu6050/selftest?${u.toString()}`);k(F)}catch(u){k(null),z(u instanceof Error?u.message:String(u))}finally{w(!1)}},[h,_,O]);return e.jsxs("div",{className:"mx-auto max-w-6xl space-y-6",children:[e.jsxs("header",{children:[e.jsx("div",{className:"text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:s("sys.placeholder.breadcrumb")}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:s("sys.sensors.title")}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:s("sys.sensors.desc")})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:s("sys.sensors.mag.section")}),e.jsx("p",{className:"mt-1 text-[11px] text-on-surface-variant",children:s("sys.sensors.mag.note")}),e.jsxs("div",{className:"mt-4 flex flex-wrap items-end gap-4",children:[e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[s("sys.sensors.mag.bus"),e.jsx("input",{type:"number",min:0,max:32,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:o,onChange:u=>n(Number(u.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[s("sys.sensors.mag.addr"),e.jsx("input",{type:"number",min:1,max:127,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:t,onChange:u=>c(Number(u.target.value))})]}),e.jsxs("label",{className:"flex items-center gap-2 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:d,onChange:u=>N(u.target.checked)}),s("sys.sensors.mag.i2cdetect")]})]}),e.jsx(xs,{bus:o,addr:t}),e.jsxs("div",{className:"mt-6 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",disabled:m,className:"rounded-lg bg-primary px-4 py-2 text-sm font-medium text-on-primary hover:opacity-90 disabled:opacity-50",onClick:()=>void L(),children:s(m?"sys.sensors.mag.running":"sys.sensors.mag.btnSelftest")}),e.jsx("button",{type:"button",disabled:m,className:"rounded-lg border border-outline-variant/40 px-4 py-2 text-sm text-on-surface hover:bg-surface-container/80 disabled:opacity-50",onClick:()=>void A(),children:s("sys.sensors.mag.btnProbe")})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",disabled:m||(p==null?void 0:p.mode)==="recording",className:"rounded-lg border border-amber-400/40 px-3 py-1.5 text-xs text-on-surface hover:bg-amber-500/10 disabled:opacity-50",onClick:()=>void I(),children:s("sys.sensors.mag.btnCalStart")}),e.jsx("button",{type:"button",disabled:m||!se,className:"rounded-lg border border-emerald-400/40 px-3 py-1.5 text-xs text-on-surface hover:bg-emerald-500/10 disabled:opacity-50",onClick:()=>void B(),children:s("sys.sensors.mag.btnCalCommit")}),e.jsx("button",{type:"button",disabled:m,className:"rounded-lg border border-rose-400/40 px-3 py-1.5 text-xs text-on-surface hover:bg-rose-500/10 disabled:opacity-50",onClick:()=>void X(),children:s("sys.sensors.mag.btnCalReset")}),e.jsx("button",{type:"button",disabled:m,className:"rounded-lg border border-outline-variant/40 px-3 py-1.5 text-xs text-on-surface hover:bg-surface-container/80 disabled:opacity-50",onClick:()=>void Q(),children:s("sys.sensors.mag.btnCalStatus")})]}),e.jsxs("div",{className:"mt-3 rounded-lg border border-outline-variant/30 bg-surface-container/60 px-3 py-2 text-[11px] text-on-surface",children:[e.jsxs("p",{className:"font-medium",children:[s("sys.sensors.mag.calStatusPrefix")," ",ae]}),e.jsxs("p",{className:"mt-1 text-on-surface-variant",children:[s("sys.sensors.mag.calSamplesPrefix")," ",ee,(p==null?void 0:p.mode)==="recording"?" / 10+":""]}),(p==null?void 0:p.span_xyz)&&e.jsxs("p",{className:"mt-1 font-mono text-[10px] text-on-surface-variant",children:["span xyz: ",Number(p.span_xyz.x??0).toFixed(1)," /"," ",Number(p.span_xyz.y??0).toFixed(1)," /"," ",Number(p.span_xyz.z??0).toFixed(1)]}),(p==null?void 0:p.mode)==="recording"&&e.jsx("p",{className:"mt-1 text-amber-200",children:s("sys.sensors.mag.calRecordingHint")}),(p==null?void 0:p.mode)==="locked"&&p.locked&&e.jsxs("p",{className:"mt-1 text-emerald-200",children:[s("sys.sensors.mag.calLockedHint")," axes=",String(p.locked.axes_pair??"xy")]}),M&&e.jsx("p",{className:"mt-1 text-sky-200",children:M})]}),y&&e.jsx("p",{className:"mt-3 rounded border border-red-500/40 bg-red-500/10 px-3 py-2 font-mono text-xs text-red-200",children:y}),S&&e.jsxs("details",{className:"mt-4 rounded-lg border border-outline-variant/30 bg-surface-container/50",children:[e.jsx("summary",{className:"cursor-pointer px-3 py-2 text-[11px] text-on-surface-variant",children:s("sys.sensors.jsonToggle")}),e.jsx("pre",{className:"max-h-[320px] overflow-auto border-t border-outline-variant/20 p-3 font-mono text-[11px] leading-relaxed text-on-surface",children:Ne(S)})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:s("sys.sensors.mpu.section")}),e.jsx("p",{className:"mt-1 text-[11px] text-on-surface-variant",children:s("sys.sensors.mpu.note")}),e.jsxs("div",{className:"mt-4 flex flex-wrap items-end gap-4",children:[e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[s("sys.sensors.mag.bus"),e.jsx("input",{type:"number",min:0,max:32,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:_,onChange:u=>E(Number(u.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[s("sys.sensors.mpu.addr"),e.jsx("input",{type:"number",min:1,max:127,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:h,onChange:u=>T(Number(u.target.value))})]}),e.jsxs("label",{className:"flex items-center gap-2 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:O,onChange:u=>j(u.target.checked)}),s("sys.sensors.mag.i2cdetect")]})]}),e.jsx(ns,{bus:_,addr:h}),e.jsx("div",{className:"mt-4 flex flex-wrap gap-2",children:e.jsx("button",{type:"button",disabled:C,className:"rounded-lg bg-primary px-4 py-2 text-sm font-medium text-on-primary hover:opacity-90 disabled:opacity-50",onClick:()=>void ne(),children:s(C?"sys.sensors.mpu.running":"sys.sensors.mpu.btnSelftest")})}),P&&e.jsx("p",{className:"mt-3 rounded border border-red-500/40 bg-red-500/10 px-3 py-2 font-mono text-xs text-red-200",children:P}),i&&e.jsxs("details",{className:"mt-4 rounded-lg border border-outline-variant/30 bg-surface-container/50",children:[e.jsx("summary",{className:"cursor-pointer px-3 py-2 text-[11px] text-on-surface-variant",children:s("sys.sensors.jsonToggle")}),e.jsx("pre",{className:"max-h-[320px] overflow-auto border-t border-outline-variant/20 p-3 font-mono text-[11px] leading-relaxed text-on-surface",children:Ne(i)})]})]})]})}const ms={ogscope:{zh:"主配置 ogscope.env",en:"Primary ogscope.env"},network:{zh:"网络 network.env",en:"Network network.env"}};function ps(s){const o=[];let n=0;return s.split(/\r?\n/).forEach((c,d)=>{const N=c.trim();if(!N||N.startsWith("#"))return;const m=N.startsWith("export ")?N.slice(7):N,f=m.indexOf("=");if(f<=0){n+=1;return}const y=m.slice(0,f).trim();if(!y){n+=1;return}const g=m.slice(f+1);o.push({id:`env-${d}-${y}`,key:y,value:g})}),{entries:o,unsupportedLines:n}}function we(s){const o=s.map(n=>({key:n.key.trim(),value:n.value})).filter(n=>n.key.length>0).map(n=>`${n.key}=${n.value}`);return o.length>0?`${o.join(` -`)} -`:""}function le(s,o){return s==="both"?!0:s===o}function hs(){const{locale:s}=J(),[o,n]=a.useState([]),[t,c]=a.useState(null),[d,N]=a.useState(""),[m,f]=a.useState(""),[y,g]=a.useState("form"),[S,v]=a.useState([]),[p,$]=a.useState(0),[M,R]=a.useState(!1),[_,E]=a.useState(""),[h,T]=a.useState(""),[O,j]=a.useState(""),[C,w]=a.useState(!0),[P,z]=a.useState(""),i=s==="zh",k=a.useMemo(()=>o.find(l=>l.file_id===d)??null,[d,o]),L=a.useMemo(()=>{const l=new Map;for(const r of(t==null?void 0:t.sections)??[])for(const x of r.entries)l.set(x.key.toUpperCase(),x);for(const r of(t==null?void 0:t.network_only)??[])l.set(r.key.toUpperCase(),r);return l},[t]),A=a.useMemo(()=>{if(!d)return[];const l=new Set(S.map(x=>x.key.trim().toUpperCase()).filter(Boolean)),r=[];for(const x of(t==null?void 0:t.sections)??[])for(const b of x.entries)le(b.scope,d)&&(l.has(b.key.toUpperCase())||r.push(b));for(const x of(t==null?void 0:t.network_only)??[])le(x.scope,d)&&(l.has(x.key.toUpperCase())||r.push(x));return r.sort((x,b)=>x.key.localeCompare(b.key))},[d,t,S]),I=a.useMemo(()=>{const l=O.trim().toLowerCase();return l?S.filter(r=>{const x=r.key.toLowerCase(),b=L.get(r.key.trim().toUpperCase());return`${x} ${r.value} ${(b==null?void 0:b.zh)??""} ${(b==null?void 0:b.en)??""}`.toLowerCase().includes(l)}):S},[S,L,O]),B=l=>{const r=ps(l);v(r.entries),$(r.unsupportedLines)},X=async()=>{var l,r;R(!0),E("");try{const[x,b]=await Promise.all([K("/api/dev/system/config/files",{cache:"no-store"}),K("/api/dev/system/config/catalog",{cache:"no-store"})]);n(x.files??[]),c(b);const H=((r=(l=x.files)==null?void 0:l[0])==null?void 0:r.file_id)??"";N(D=>{var Z;return D&&((Z=x.files)!=null&&Z.some(Y=>Y.file_id===D))?D:H})}catch(x){E(x instanceof Error?x.message:String(x))}finally{R(!1)}},Q=async()=>{if(!d)return;if(y==="form"&&p>0){E(i?"当前文件包含无法表单化的行,请切换到「原始文本」模式编辑后再保存。":"This file has lines not supported by form mode. Switch to Raw mode before saving.");return}const l=y==="form"?we(S):m;R(!0),E(""),T("");try{const r=await K("/api/dev/system/config/files",{method:"POST",body:JSON.stringify({file_id:d,content:l})});T(i?`保存成功:${r.message??d};请重启 ogscope 服务使配置生效`:`Saved: ${r.message??d}; restart ogscope to apply changes`),await X()}catch(r){E(r instanceof Error?r.message:String(r))}finally{R(!1)}};a.useEffect(()=>{X()},[]),a.useEffect(()=>{if(!k){f(""),v([]),$(0),z("");return}const l=k.content??"";f(l),B(l),z("")},[k]);const ae=()=>{v(l=>[...l,{id:`env-new-${Date.now()}-${l.length}`,key:"",value:""}])},ee=()=>{const l=A.find(r=>r.key===P);l&&(v(r=>[...r,{id:`env-catalog-${Date.now()}-${l.key}`,key:l.key,value:l.default??""}]),z(""))},se=(l,r)=>{v(x=>x.map(b=>b.id===l?{...b,...r}:b))},ne=l=>{v(r=>r.filter(x=>x.id!==l))},u=l=>{const r=L.get(l.trim().toUpperCase());return r?i?r.zh:r.en:i?"暂无释义(可查阅 deploy/*.env.example)":"No hint (see deploy/*.env.example)"},F=l=>{const r=L.get(l.trim().toUpperCase());return r!=null&&r.default?i?`默认:${r.default}`:`Default: ${r.default}`:""},G=l=>{const r=ms[l];return r?i?r.zh:r.en:l};return e.jsxs("div",{className:"mx-auto max-w-7xl space-y-6",children:[e.jsxs("header",{children:[e.jsxs("div",{className:"flex items-center gap-2 text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:[e.jsx("span",{children:"Console"}),e.jsx("span",{children:"/"}),e.jsx("span",{className:"text-primary",children:i?"配置管理":"Config Manager"})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:i?"环境配置管理":"Environment Config Manager"}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:i?"编辑 /etc/ogscope 下的 ogscope.env 与 network.env。保存后请重启 ogscope 服务;配置项说明来自服务端目录 API。":"Edit ogscope.env and network.env under /etc/ogscope. Restart ogscope after saving; hints come from the server catalog API."})]}),_&&e.jsx("div",{className:"rounded-lg border border-error/40 bg-error-container/20 px-3 py-2 text-sm text-on-error-container",children:_}),h&&e.jsx("div",{className:"rounded-lg border border-primary/30 bg-primary/10 px-3 py-2 text-sm",children:h}),e.jsxs("section",{className:"grid grid-cols-12 gap-4",children:[e.jsxs("aside",{className:"col-span-12 space-y-2 rounded-xl border border-outline-variant/20 bg-surface-container p-3 lg:col-span-3",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("p",{className:"text-xs uppercase tracking-wider text-on-surface-variant",children:i?"配置文件":"Config Files"}),e.jsx("button",{type:"button",onClick:()=>void X(),disabled:M,children:e.jsxs("span",{className:"inline-flex items-center gap-1 text-xs",children:[e.jsx(he,{className:"h-3.5 w-3.5"})," ",i?"刷新":"Refresh"]})})]}),o.map(l=>e.jsxs("button",{type:"button",onClick:()=>N(l.file_id),className:`w-full rounded-lg border px-3 py-2 text-left text-sm ${l.file_id===d?"border-primary bg-primary/10 text-on-surface":"border-outline-variant/30 bg-surface-container-low text-on-surface-variant"}`,children:[e.jsx("div",{className:"font-medium",children:G(l.file_id)}),e.jsx("div",{className:"mt-1 truncate font-mono text-[11px]",children:l.path})]},l.file_id)),(t==null?void 0:t.env_files)&&e.jsxs("div",{className:"mt-3 rounded border border-outline-variant/20 bg-surface-container-low px-2 py-2 text-[11px] text-on-surface-variant",children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:i?"配置路径":"Config paths"}),Object.entries(t.env_files).map(([l,r])=>e.jsxs("p",{className:"font-mono",children:[l,": ",r]},l))]})]}),e.jsxs("div",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 lg:col-span-9",children:[!k&&e.jsx("p",{className:"text-sm text-on-surface-variant",children:i?"暂无可编辑配置文件":"No editable config files."}),k&&e.jsxs("div",{className:"space-y-3",children:[e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("p",{className:"font-mono text-xs text-on-surface",children:k.path}),e.jsxs("p",{className:"text-xs text-on-surface-variant",children:[i?"可写":"Writable",": ",String(k.writable)," · ",i?"存在":"Exists",":"," ",String(k.exists)]})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{type:"button",onClick:()=>{y==="raw"&&B(m),g("form")},className:`rounded px-2 py-1 text-xs ${y==="form"?"bg-primary-container text-on-primary-container":"border border-outline-variant/30 text-on-surface-variant"}`,children:i?"表单模式":"Form"}),e.jsx("button",{type:"button",onClick:()=>{y==="form"&&f(we(S)),g("raw")},className:`rounded px-2 py-1 text-xs ${y==="raw"?"bg-primary-container text-on-primary-container":"border border-outline-variant/30 text-on-surface-variant"}`,children:i?"原始文本":"Raw"})]}),e.jsx("button",{type:"button",onClick:()=>void Q(),disabled:M||!k.writable,children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(Pe,{className:"h-3.5 w-3.5"}),i?"保存并提示重启":"Save"]})})]}),k.error&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-2 py-1 text-xs text-on-error-container",children:k.error}),y==="form"?e.jsxs("div",{className:"space-y-3",children:[p>0&&e.jsx("div",{className:"rounded border border-warning/40 bg-warning/10 px-2 py-1 text-xs text-on-surface",children:i?`检测到 ${p} 行无法转换为键值表单。请切到「原始文本」模式处理。`:`${p} line(s) cannot be represented in key-value form. Use Raw mode.`}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("div",{className:"relative min-w-[200px] flex-1",children:[e.jsx(_e,{className:"pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-on-surface-variant"}),e.jsx("input",{className:"w-full rounded border border-outline-variant/30 bg-surface-container-low py-1.5 pl-8 pr-2 text-xs outline-none focus:border-primary",placeholder:i?"搜索键名、值或释义…":"Search keys, values, or hints…",value:O,onChange:l=>j(l.target.value)})]}),e.jsxs("select",{className:"max-w-xs flex-1 rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5 text-xs outline-none focus:border-primary",value:P,onChange:l=>z(l.target.value),children:[e.jsx("option",{value:"",children:i?"从目录添加配置项…":"Add from catalog…"}),A.map(l=>e.jsx("option",{value:l.key,children:l.key},l.key))]}),e.jsx("button",{type:"button",disabled:!P,onClick:ee,className:"rounded border border-outline-variant/30 px-2 py-1.5 text-xs disabled:opacity-50",children:i?"添加":"Add"})]}),e.jsxs("div",{className:"max-h-[420px] overflow-auto pr-1",children:[e.jsxs("table",{className:"w-full border-separate border-spacing-y-2",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"text-left text-[11px] uppercase tracking-wide text-on-surface-variant",children:[e.jsx("th",{children:i?"配置项":"Key"}),e.jsx("th",{children:i?"值":"Value"}),e.jsx("th",{children:i?"释义":"Meaning"}),e.jsx("th",{children:i?"操作":"Action"})]})}),e.jsx("tbody",{children:I.map(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"pr-2 align-top",children:e.jsx("input",{className:"w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1 font-mono text-xs outline-none focus:border-primary",placeholder:"OGSCOPE_PORT",value:l.key,onChange:r=>se(l.id,{key:r.target.value})})}),e.jsx("td",{className:"pr-2 align-top",children:e.jsx("input",{className:"w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1 font-mono text-xs outline-none focus:border-primary",placeholder:i?"变量值":"Value",value:l.value,onChange:r=>se(l.id,{value:r.target.value})})}),e.jsxs("td",{className:"pr-2 align-top text-xs text-on-surface-variant",children:[e.jsx("p",{children:u(l.key)}),F(l.key)&&e.jsx("p",{className:"mt-0.5 font-mono text-[10px] text-on-surface-variant/80",children:F(l.key)})]}),e.jsx("td",{className:"align-top",children:e.jsx("button",{type:"button",onClick:()=>ne(l.id),children:e.jsx(Re,{className:"h-3.5 w-3.5 text-on-surface-variant"})})})]},l.id))})]}),S.length===0&&e.jsx("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-2 text-xs text-on-surface-variant",children:i?"当前没有可编辑变量,可从目录添加或手动新增。":"No variables yet. Add from catalog or manually."}),S.length>0&&I.length===0&&e.jsx("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-2 text-xs text-on-surface-variant",children:i?"无匹配项,请调整搜索条件。":"No matches for the current search."})]}),e.jsx("div",{className:"flex flex-wrap gap-2",children:e.jsx("button",{type:"button",onClick:ae,children:e.jsxs("span",{className:"inline-flex items-center gap-1 text-xs",children:[e.jsx(De,{className:"h-3.5 w-3.5"}),i?"空白行":"Blank row"]})})}),e.jsxs("details",{open:C,onToggle:l=>w(l.target.open),className:"rounded border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-xs text-on-surface-variant",children:[e.jsx("summary",{className:"cursor-pointer font-medium text-on-surface",children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(Ae,{className:"h-3.5 w-3.5"}),i?"配置目录(按模块)":"Config catalog (by section)"]})}),e.jsxs("div",{className:"mt-3 space-y-4",children:[((t==null?void 0:t.sections)??[]).map(l=>{const r=l.entries.filter(x=>d?le(x.scope,d):!0);return r.length===0?null:e.jsxs("div",{children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:i?l.title_zh:l.title_en}),e.jsx("div",{className:"space-y-1",children:r.map(x=>e.jsxs("p",{children:[e.jsx("span",{className:"font-mono text-[11px] text-on-surface",children:x.key}),x.default!=null&&x.default!==""&&e.jsxs("span",{className:"ml-1 font-mono text-[10px] text-on-surface-variant/80",children:["(= ",x.default,")"]})," — ",e.jsx("span",{children:i?x.zh:x.en})]},x.key))})]},l.id)}),((t==null?void 0:t.network_only)??[]).filter(l=>d?le(l.scope,d):!0).length>0&&e.jsxs("div",{children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:i?"仅 network.env / 脚本":"network.env / scripts only"}),e.jsx("div",{className:"space-y-1",children:((t==null?void 0:t.network_only)??[]).filter(l=>d?le(l.scope,d):!0).map(l=>e.jsxs("p",{children:[e.jsx("span",{className:"font-mono text-[11px] text-on-surface",children:l.key})," — ",e.jsx("span",{children:i?l.zh:l.en})]},l.key))})]})]})]})]}):e.jsx("textarea",{className:"h-[460px] w-full rounded-lg border border-outline-variant/30 bg-neutral-950 p-3 font-mono text-xs text-on-surface outline-none focus:border-primary",spellCheck:!1,value:m,onChange:l=>f(l.target.value)})]})]})]})]})}const fs=new Set(["overview","network","sensors","hmi","power","config"]);function ke(){const s=window.location.hash.replace(/^#\/?/,"").trim().toLowerCase();return fs.has(s)?s:"overview"}function bs(s){window.location.hash=`/${s}`}function gs(){const[s,o]=a.useState(()=>ke()),[n,t]=a.useState(!0);a.useEffect(()=>{const d=()=>o(ke());return window.addEventListener("hashchange",d),()=>window.removeEventListener("hashchange",d)},[]),a.useEffect(()=>{(async()=>{var d;try{const N=await fetch("/api",{cache:"no-store"});if(!N.ok)return;const m=await N.json();t(!!((d=m.endpoints)!=null&&d.network))}catch{}})()},[]);const c=a.useMemo(()=>s==="network"?n?e.jsx(Qe,{}):e.jsx(ve,{scope:"network"}):s==="sensors"?e.jsx(us,{}):s==="hmi"?e.jsx(ts,{}):s==="config"?e.jsx(hs,{}):s==="power"?e.jsx(ve,{scope:"power"}):e.jsx(Ye,{}),[n,s]);return e.jsx(We,{route:s,allowNetworkRoute:n,onRouteChange:d=>{d==="network"&&!n||d!==s&&bs(d)},children:c})}$e.createRoot(document.getElementById("root")).render(e.jsx(Me.StrictMode,{children:e.jsx(Te,{children:e.jsx(Le,{children:e.jsx(gs,{})})})})); diff --git a/web/static/analysis-lab/assets/system-DQXiDxh6.js b/web/static/analysis-lab/assets/system-DQXiDxh6.js new file mode 100644 index 0000000..7380d3d --- /dev/null +++ b/web/static/analysis-lab/assets/system-DQXiDxh6.js @@ -0,0 +1,78 @@ +import{j as e,r as a,a as $e,R as Me}from"./client-D1ZVDB-N.js";import{u as pe,C as Ee,r as W,a as q,S as Pe,b as Le}from"./http-ChPtkS1w.js";import{c as H,u as Z,T as Re,I as Te}from"./index-CutgeBjy.js";import{R as he}from"./refresh-cw-BkMjDReH.js";/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const me=H("Activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Oe=H("Bolt",[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",key:"yt0hxn"}],["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ae=H("BookOpen",[["path",{d:"M12 7v14",key:"1akyts"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",key:"ruj8y"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Se=H("Cpu",[["rect",{width:"16",height:"16",x:"4",y:"4",rx:"2",key:"14l7u7"}],["rect",{width:"6",height:"6",x:"9",y:"9",rx:"1",key:"5aljv4"}],["path",{d:"M15 2v2",key:"13l42r"}],["path",{d:"M15 20v2",key:"15mkzm"}],["path",{d:"M2 15h2",key:"1gxd5l"}],["path",{d:"M2 9h2",key:"1bbxkp"}],["path",{d:"M20 15h2",key:"19e6y8"}],["path",{d:"M20 9h2",key:"19tzq7"}],["path",{d:"M9 2v2",key:"165o2o"}],["path",{d:"M9 20v2",key:"i2bqo8"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Fe=H("HardDrive",[["line",{x1:"22",x2:"2",y1:"12",y2:"12",key:"1y58io"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16",key:"sgf278"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16",key:"1l4acy"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ie=H("LayoutDashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ze=H("Network",[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1",key:"4q2zg0"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1",key:"8cvhb9"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1",key:"1egb70"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3",key:"1jsf9p"}],["path",{d:"M12 12V8",key:"2874zd"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const De=H("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const _e=H("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const He=H("Settings",[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ge=H("Sparkles",[["path",{d:"M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z",key:"4pj2yx"}],["path",{d:"M20 3v4",key:"1olli1"}],["path",{d:"M22 5h-4",key:"1gvqau"}],["path",{d:"M4 17v2",key:"vumght"}],["path",{d:"M5 18H3",key:"zchphs"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const qe=H("Thermometer",[["path",{d:"M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z",key:"17jzev"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ue=H("Touchpad",[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2",key:"18n3k1"}],["path",{d:"M2 14h20",key:"myj16y"}],["path",{d:"M12 20v-6",key:"1rm09r"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Be=H("TriangleAlert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/** + * @license lucide-react v0.460.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fe=H("Wifi",[["path",{d:"M12 20h.01",key:"zekei9"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0",key:"dnpr2z"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0",key:"1x1e6c"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0",key:"1bycff"}]]),V=t=>`flex items-center gap-3 rounded-lg px-3 py-2.5 font-headline text-sm tracking-tight transition-colors ${t?"border-r-2 border-primary bg-white/5 font-semibold text-primary":"text-on-surface-variant hover:bg-white/5 hover:text-on-surface"}`;function We({route:t,allowNetworkRoute:o,onRouteChange:n,children:r}){const{t:c,locale:d,setLocale:j}=Z(),{info:u}=pe(),f=(u==null?void 0:u.cpu_usage)!=null?Number(u.cpu_usage).toFixed(1):"—",g=(u==null?void 0:u.memory_usage)!=null?Number(u.memory_usage).toFixed(1):"—",b=(u==null?void 0:u.temperature)!=null?Number(u.temperature).toFixed(1):"—",k=(u==null?void 0:u.wifi_quality)!=null&&!Number.isNaN(Number(u.wifi_quality))?`${Number(u.wifi_quality).toFixed(0)}%`:"—",y={overview:c("sys.shell.top.overview"),network:c("sys.shell.top.network"),sensors:c("sys.shell.top.sensors"),hmi:c("sys.shell.top.hmi"),power:c("sys.shell.top.power"),config:c("sys.shell.top.config")},m=V(!1),$=(M,R)=>{const S=window.open(M,R);S&&S.focus()};return e.jsxs("div",{className:"flex h-full min-h-0 flex-col bg-background text-on-surface md:flex-row",children:[e.jsxs("aside",{className:"glass-panel z-50 flex w-full shrink-0 flex-col border-b border-outline-variant/20 bg-surface-container-low/80 backdrop-blur-xl md:fixed md:left-0 md:top-0 md:h-full md:w-64 md:border-b-0 md:border-r md:border-white/5",children:[e.jsxs("div",{className:"p-5",children:[e.jsxs("div",{className:"mb-8 flex items-center gap-3",children:[e.jsx("div",{className:"primary-gradient flex h-10 w-10 items-center justify-center rounded-lg shadow-lg",children:e.jsx(ge,{className:"h-5 w-5 text-on-primary-container"})}),e.jsxs("div",{children:[e.jsx("h1",{className:"font-headline text-lg font-bold tracking-widest text-primary",children:"OGScope"}),e.jsx("p",{className:"font-mono text-[10px] uppercase tracking-widest text-on-surface-variant",children:c("sys.shell.subtitle")})]})]}),e.jsxs("nav",{className:"flex flex-col gap-0.5",children:[e.jsxs("button",{type:"button",className:V(t==="overview"),onClick:()=>n("overview"),children:[e.jsx(Ie,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.overview")})]}),o&&e.jsxs("button",{type:"button",className:V(t==="network"),onClick:()=>n("network"),children:[e.jsx(ze,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.network")})]}),e.jsxs("a",{href:"/debug/camera",className:m,onClick:M=>{M.preventDefault(),$("/debug/camera","ogscopeCameraConsole")},children:[e.jsx(Ee,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.camera")})]}),e.jsxs("a",{href:"/debug/analysis",className:m,onClick:M=>{M.preventDefault(),$("/debug/analysis","ogscopeAnalysisConsole")},children:[e.jsx(ge,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.analysis")})]}),e.jsxs("button",{type:"button",className:V(t==="sensors"),onClick:()=>n("sensors"),children:[e.jsx(me,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.sensors")})]}),e.jsxs("button",{type:"button",className:V(t==="power"),onClick:()=>n("power"),children:[e.jsx(Oe,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.power")})]}),e.jsxs("button",{type:"button",className:V(t==="hmi"),onClick:()=>n("hmi"),children:[e.jsx(Ue,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.hmi")})]}),e.jsxs("button",{type:"button",className:V(t==="config"),onClick:()=>n("config"),children:[e.jsx(He,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:c("sys.shell.nav.config")})]})]})]}),e.jsx("div",{className:"mt-auto hidden p-5 md:block",children:e.jsxs("div",{className:"rounded-xl border border-white/5 bg-surface-container-low p-3",children:[e.jsx("p",{className:"truncate text-xs font-semibold text-on-surface",children:c("sys.shell.workbench")}),e.jsxs("p",{className:"font-mono text-[10px] text-on-surface-variant",children:[c("sys.shell.node"),": OGSCOPE_PI_ZERO_2W"]})]})})]}),e.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1 flex-col md:ml-64",children:[e.jsxs("header",{className:"sticky top-0 z-40 flex h-14 shrink-0 items-center justify-between border-b border-white/5 bg-neutral-950/80 px-4 backdrop-blur-md md:px-8",children:[e.jsx("div",{className:"flex min-w-0 items-center gap-3",children:e.jsx("span",{className:"hidden truncate border-b-2 border-primary pb-0.5 font-mono text-xs uppercase tracking-wider text-primary sm:inline",children:y[t]})}),e.jsxs("div",{className:"flex flex-wrap items-center justify-end gap-3 sm:gap-4",children:[e.jsxs("div",{className:"mr-2 flex gap-1 text-[10px]",children:[e.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${d==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>j("zh"),children:c("lang.zh")}),e.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${d==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>j("en"),children:c("lang.en")})]}),e.jsxs("div",{className:"flex flex-wrap items-center justify-end gap-3 font-mono text-[10px] uppercase tracking-wider text-on-surface-variant sm:gap-4",children:[e.jsxs("span",{className:"flex items-center gap-1 text-primary",children:[e.jsx(Se,{className:"h-3.5 w-3.5"})," CPU ",f,"%"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(me,{className:"h-3.5 w-3.5"})," MEM ",g,"%"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"text-xs",children:"°C"})," ",b]}),e.jsxs("span",{className:"flex items-center gap-1 text-secondary",children:[e.jsx(fe,{className:"h-3.5 w-3.5"})," ",k]})]})]})]}),e.jsx("main",{className:"og-scrollbar min-h-0 flex-1 overflow-auto p-4 md:p-6",children:r})]})]})}async function Ke(){return await W("/api/dev/system/hardware-plane/status",{cache:"no-store"})}async function Xe(t){return await W("/api/dev/system/hardware-plane/command",{method:"POST",body:JSON.stringify(t)})}async function Ge(t){var r;const o=new URLSearchParams;t!=null&&t.service&&o.set("service",t.service),o.set("since_seconds",String(t.sinceSeconds)),o.set("limit",String(t.limit)),(r=t==null?void 0:t.levels)!=null&&r.length&&o.set("levels",t.levels.join(","));const n=o.toString();return await W(`/api/dev/debug/logs/systemd${n?`?${n}`:""}`,{cache:"no-store"})}function Ze(t){const o=Math.max(0,parseInt(String(t??0),10)||0),n=Math.floor(o/86400),r=Math.floor(o%86400/3600),c=Math.floor(o%3600/60);return n>0?`${n}d ${r}h`:r>0?`${r}h ${c}m`:`${c}m`}function de(t,o=1){return t==null||Number.isNaN(Number(t))?"—":Number(t).toFixed(o)}function ye(t){return t==="ERROR"?"text-error":t==="WARN"?"text-amber-300":"text-primary"}function Je(t){if(!t)return"--:--:--";const o=new Date(t);return Number.isNaN(o.getTime())?"--:--:--":o.toLocaleTimeString()}function Ye(){const{t:n}=Z(),{info:r,error:c}=pe(),[d,j]=a.useState(!1),[u,f]=a.useState(["INFO","WARN","ERROR"]),[g,b]=a.useState([]),[k,y]=a.useState(null),[m,$]=a.useState(!1),[M,R]=a.useState(!0),S=a.useRef(null),E=a.useRef(new Set),h=de(r==null?void 0:r.cpu_usage),T=de(r==null?void 0:r.memory_usage),O=de(r==null?void 0:r.temperature),v=(r==null?void 0:r.load_average_1m)!=null?String(r.load_average_1m):"—",_=(r==null?void 0:r.wifi_quality)!=null&&!Number.isNaN(Number(r.wifi_quality))?`${Number(r.wifi_quality).toFixed(0)}%`:"—",N=(r==null?void 0:r.wifi_signal_dbm)!=null&&!Number.isNaN(Number(r.wifi_signal_dbm))?`${Number(r.wifi_signal_dbm).toFixed(0)} dBm`:"—",P=async()=>{if(u.length===0){b([]);return}$(!0);try{y(null);const i=await Ge({service:"ogscope",sinceSeconds:1200,limit:240,levels:u});b(w=>{const L=new Set(w.map(I=>`${I.ts??""}::${I.level}::${I.source}::${I.message}`)),A=[...w];for(const I of i.items){const U=`${I.ts??""}::${I.level}::${I.source}::${I.message}`;L.has(U)||(L.add(U),A.push(I))}return A.length<=300?A:A.slice(A.length-300)})}catch(i){y(i instanceof Error?i.message:String(i))}finally{$(!1)}};a.useEffect(()=>{if(!d)return;P();const i=window.setInterval(()=>{document.hidden||P()},4e3);return()=>window.clearInterval(i)},[d,u.join(",")]),a.useEffect(()=>{const i=S.current;if(!i)return;const w=()=>{const L=i.scrollHeight-i.scrollTop-i.clientHeight;R(L<=24)};return w(),i.addEventListener("scroll",w),()=>i.removeEventListener("scroll",w)},[]),a.useEffect(()=>{if(!S.current||g.length===0)return;const w=new Set(g.map(A=>`${A.ts??""}::${A.level}::${A.source}::${A.message}`));let L=!1;w.forEach(A=>{E.current.has(A)||(L=!0)}),E.current=w,M&&L&&requestAnimationFrame(()=>{S.current&&(S.current.scrollTop=S.current.scrollHeight)})},[g,M]);const z=a.useMemo(()=>new Set(u),[u]);return e.jsxs("div",{className:"mx-auto max-w-7xl space-y-6",children:[e.jsxs("header",{className:"mb-1",children:[e.jsxs("div",{className:"flex items-center gap-2 text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.breadcrumb.console")}),e.jsx("span",{children:"/"}),e.jsx("span",{className:"text-primary",children:n("sys.overview.breadcrumb.module")})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:n("sys.overview.title")}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:n("sys.overview.subtitle")})]}),c&&e.jsx("div",{className:"rounded-lg border border-error/40 bg-error-container/20 px-3 py-2 text-sm text-on-error-container",children:c}),e.jsxs("section",{className:"grid grid-cols-12 gap-4",children:[e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.cpu")}),e.jsx(Se,{className:"h-4 w-4 text-primary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[h,"%"]}),e.jsx("div",{className:"mt-3 h-1.5 w-full overflow-hidden rounded bg-surface-container-high",children:e.jsx("div",{className:"h-full bg-primary",style:{width:`${Math.min(Number(h)||0,100)}%`}})})]}),e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.mem")}),e.jsx(me,{className:"h-4 w-4 text-secondary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[T,"%"]}),e.jsx("div",{className:"mt-3 h-1.5 w-full overflow-hidden rounded bg-surface-container-high",children:e.jsx("div",{className:"h-full bg-secondary",style:{width:`${Math.min(Number(T)||0,100)}%`}})})]}),e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.temp")}),e.jsx(qe,{className:"h-4 w-4 text-primary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[O,"°C"]}),e.jsx("div",{className:"mt-3 text-xs text-on-surface-variant",children:n("sys.overview.tempState")})]}),e.jsxs("article",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 md:col-span-3",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between text-[10px] uppercase tracking-wider text-on-surface-variant",children:[e.jsx("span",{children:n("sys.overview.metric.wifi")}),e.jsx(fe,{className:"h-4 w-4 text-primary"})]}),e.jsx("div",{className:"text-3xl font-bold text-on-surface",children:_}),e.jsx("div",{className:"mt-3 text-xs text-on-surface-variant",children:N})]})]}),e.jsxs("section",{className:"grid grid-cols-12 gap-4",children:[e.jsxs("article",{className:"col-span-12 rounded-xl border border-white/5 bg-surface-container-low p-6 lg:col-span-8",children:[e.jsxs("div",{className:"mb-6 flex items-center justify-between",children:[e.jsxs("div",{children:[e.jsx("h3",{className:"font-headline text-lg font-bold",children:n("sys.overview.wifiSummary")}),e.jsxs("p",{className:"text-xs text-on-surface-variant",children:[n("sys.overview.iface"),": ",String((r==null?void 0:r.wifi_interface)??"wlan0")]})]}),e.jsx("span",{className:"rounded border border-primary/30 bg-primary/10 px-2 py-1 text-[10px] uppercase tracking-widest text-primary",children:n("sys.overview.linkActive")})]}),e.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.signal")}),e.jsx("p",{className:"mt-1 font-mono text-2xl font-bold",children:N})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.quality")}),e.jsx("p",{className:"mt-1 font-mono text-2xl font-bold",children:_})]})]})]}),e.jsxs("aside",{className:"col-span-12 space-y-4 lg:col-span-4",children:[e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.metric.uptime")}),e.jsx("p",{className:"mt-1 text-xl font-bold",children:Ze(r==null?void 0:r.uptime_seconds)})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.metric.load")}),e.jsx("p",{className:"mt-1 text-xl font-bold",children:v})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:n("sys.overview.metric.storage")}),e.jsx(Fe,{className:"h-4 w-4 text-on-surface-variant"})]}),e.jsx("p",{className:"mt-2 text-sm text-on-surface-variant",children:n("sys.overview.storageComingSoon")})]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-lowest p-4",children:[e.jsxs("div",{className:"mb-3 flex flex-wrap items-center justify-between gap-3 border-b border-outline-variant/20 pb-2",children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("span",{className:"text-[10px] uppercase tracking-widest text-primary",children:n("sys.logs.title")}),e.jsxs("span",{className:"text-[10px] text-on-surface-variant",children:[n("sys.logs.kernel"),": ",String((r==null?void 0:r.os)??"—")]})]}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsxs("label",{className:"inline-flex items-center gap-2 text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:d,onChange:i=>j(i.target.checked)}),n("sys.logs.liveToggle")]}),e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-on-surface-variant hover:border-primary hover:text-on-surface",onClick:()=>void P(),children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(he,{className:"h-3.5 w-3.5"})," ",n("sys.logs.refresh")]})})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-2 text-xs",children:[["INFO","WARN","ERROR"].map(i=>e.jsxs("label",{className:"inline-flex items-center gap-1.5 text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:z.has(i),onChange:w=>{f(L=>w.target.checked?Array.from(new Set([...L,i])):L.filter(A=>A!==i))}}),e.jsx("span",{className:ye(i),children:i})]},i)),!d&&e.jsxs("span",{className:"inline-flex items-center gap-1 rounded border border-outline-variant/30 px-2 py-0.5 text-[11px] text-on-surface-variant",children:[e.jsx(Be,{className:"h-3.5 w-3.5"})," ",n("sys.logs.liveOffHint")]})]}),k&&e.jsx("div",{className:"mb-2 rounded border border-error/40 bg-error-container/20 px-2 py-1 text-xs text-on-error-container",children:k}),e.jsxs("div",{ref:S,className:"og-scrollbar max-h-72 space-y-1 overflow-auto font-mono text-[11px] text-on-surface-variant",children:[m&&g.length===0&&e.jsx("div",{children:n("sys.logs.loading")}),!m&&g.length===0&&e.jsx("div",{children:n("sys.logs.empty")}),g.map((i,w)=>e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsxs("span",{className:"shrink-0 text-primary",children:["[",Je(i.ts),"]"]}),e.jsx("span",{className:`shrink-0 ${ye(i.level)}`,children:i.level}),e.jsx("span",{className:"shrink-0 text-on-surface/80",children:i.source}),e.jsx("span",{className:"min-w-0 break-words text-on-surface",children:i.message})]},`${i.ts||"ts"}-${w}`))]})]})]})}function le(){return typeof window.OGSCOPE_HTTP_PORT=="number"?window.OGSCOPE_HTTP_PORT:8e3}function Ve(t){const o=Math.max(0,parseInt(String(t??0),10)||0),n=Math.floor(o/86400),r=Math.floor(o%86400/3600),c=Math.floor(o%3600/60);return n>0?`${n}天 ${r}小时`:r>0?`${r}小时 ${c}分`:`${c}分`}function Qe(){const{info:t,error:o}=pe(),[n,r]=a.useState("加载中..."),[c,d]=a.useState(""),[j,u]=a.useState(""),[f,g]=a.useState([]),[b,k]=a.useState(!1),[y,m]=a.useState([]),[$,M]=a.useState(!1),[R,S]=a.useState(""),[E,h]=a.useState(""),[T,O]=a.useState(!1),[v,_]=a.useState(""),[N,P]=a.useState(`http://192.168.4.1:${le()}`),[z,i]=a.useState("OGScope_xxxx"),[w,L]=a.useState(null),[A,I]=a.useState("—"),U=a.useCallback(s=>{const l=s.mode||"unknown",p=s.active_connection||"-",C=s.wireless_interface||"wlan0",D=s.ap_ipv4||"-",G=s.configured?"是":"否",J=s.message?`,消息: ${s.message}`:"";s.ap_url_hint&&P(s.ap_url_hint),s.ap_ssid&&i(s.ap_ssid);const Y=le();if(s.mdns_hostname_hint){const ne=`http://${s.mdns_hostname_hint}:${Y}/debug`;L(ne),I(ne)}else if(s.device_id_suffix){const ne=`http://${`ogscope-${s.device_id_suffix}.local`}:${Y}/debug`;L(ne),I(ne)}else L(null),I("未提供");r(`模式: ${l} | 活动连接: ${p} | 接口: ${C} | AP地址: ${D} | 已配置: ${G}${J}`)},[]),K=a.useCallback(async()=>{const s=await W("/api/network/wifi",{cache:"no-store"});U(s)},[U]);a.useEffect(()=>{K().catch(s=>r(`获取状态失败: ${s.message}`))},[K]);const Q=async s=>{r(`正在切换到 ${s.toUpperCase()}...`);const l=await W("/api/network/wifi",{method:"POST",body:JSON.stringify({mode:s})});U(l)},re=async()=>{k(!0),u("扫描中..."),g([]);try{const s=await W("/api/network/wifi/scan",{cache:"no-store"}),l=s.networks||[],p=s.hint?` ${s.hint}`:"";g(l),u(`扫描到 ${l.length} 个网络${p}`.trim())}catch(s){u(`扫描失败: ${s instanceof Error?s.message:String(s)}`)}finally{k(!1)}},ee=async s=>{const l=window.prompt(`输入密码: ${s}`,"");if(l!==null){d(""),r("正在连接..."),O(!0);try{const p=await W("/api/network/wifi/sta/connect",{method:"POST",body:JSON.stringify({ssid:s,password:l||null})});U(p);const C=p.mode||"unknown";d(C==="sta"?`连接成功,当前连接: ${p.active_connection||"—"}`:`连接请求已提交,当前模式: ${C},连接: ${p.active_connection||"—"}`),window.setTimeout(()=>void K().catch(()=>{}),2500)}catch(p){r(`连接失败: ${p instanceof Error?p.message:String(p)}`),d(`错误详情: ${p instanceof Error?p.message:String(p)}`)}finally{O(!1)}}},se=async()=>{const s=R.trim();if(!s){window.alert("请输入 SSID");return}d(""),r("正在连接..."),O(!0);try{const l=await W("/api/network/wifi/sta/connect",{method:"POST",body:JSON.stringify({ssid:s,password:E||null})});U(l);const p=l.mode||"unknown";d(p==="sta"?`连接成功,当前连接: ${l.active_connection||"—"}`:`连接请求已提交,当前模式: ${p},连接: ${l.active_connection||"—"}`),window.setTimeout(()=>void K().catch(()=>{}),2500)}catch(l){r(`连接失败: ${l instanceof Error?l.message:String(l)}`),d(`错误详情: ${l instanceof Error?l.message:String(l)}`)}finally{O(!1)}},ae=async()=>{M(!0);try{const s=await W("/api/network/wifi/profiles",{cache:"no-store"});m(s.profiles||[])}catch(s){m([]),window.alert(s instanceof Error?s.message:String(s))}finally{M(!1)}},x=async s=>{try{await W("/api/network/wifi/profile/activate",{method:"POST",body:JSON.stringify({connection_name:s})}),r("已发送激活请求")}catch(l){window.alert(l instanceof Error?l.message:String(l))}};async function F(s,l){const p=`http://${s}:${l}/health`;try{const C=new AbortController,D=window.setTimeout(()=>C.abort(),700),G=await fetch(p,{signal:C.signal,mode:"cors"});if(window.clearTimeout(D),!G.ok)return null;const J=await G.json();if(J&&J.status==="healthy")return`http://${s}:${l}`}catch{}return null}const X=async()=>{const s=le();_("扫描中...");const l=["192.168.0","192.168.1","192.168.31","10.0.0"];for(const p of l){const C=[];for(let D=1;D<255;D++)C.push(`${p}.${D}`);for(let D=0;DF(ce,s)))).find(Boolean);if(Y){_(`已找到设备: ${Y}`),window.location.href=`${Y}/debug`;return}}}_("未找到设备")},ie=a.useMemo(()=>{if(!t)return[];const s=t.wifi_quality!=null&&!Number.isNaN(Number(t.wifi_quality))?`${Number(t.wifi_quality).toFixed(1)}%`:"—",l=t.wifi_signal_dbm!=null&&!Number.isNaN(Number(t.wifi_signal_dbm))?`${Number(t.wifi_signal_dbm).toFixed(0)} dBm (${String(t.wifi_interface??"?")})`:"—";return[["平台",String(t.platform??"—")],["系统",String(t.os??"—")],["CPU 占用",`${Number(t.cpu_usage??0).toFixed(1)}%`],["内存占用",`${Number(t.memory_usage??0).toFixed(1)}%`],["CPU 温度",`${Number(t.temperature??0).toFixed(1)} °C`],["运行时长",Ve(t.uptime_seconds)],["1 分钟负载",String(t.load_average_1m??"—")],["WiFi 质量",s],["WiFi 信号",l]]},[t]);return e.jsxs("div",{className:"mx-auto max-w-7xl space-y-6",children:[e.jsxs("header",{children:[e.jsxs("div",{className:"flex items-center gap-2 text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:[e.jsx("span",{children:"Console"}),e.jsx("span",{children:"/"}),e.jsx("span",{className:"text-primary",children:"System_Network_Debug"})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:"NETWORK TERMINAL"}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:"WiFi 管理、模式切换、发现与恢复工具"})]}),o&&e.jsx("div",{className:"rounded-lg border border-error/40 bg-error-container/20 px-3 py-2 text-sm text-on-error-container",children:o}),e.jsxs("section",{className:"grid grid-cols-12 gap-6",children:[e.jsxs("div",{className:"col-span-12 space-y-6 lg:col-span-8",children:[e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container",children:[e.jsxs("div",{className:"flex items-center justify-between border-b border-outline-variant/20 bg-surface-container-high px-4 py-3",children:[e.jsxs("div",{className:"flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-primary",children:[e.jsx(fe,{className:"h-4 w-4"}),"WiFi Control"]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-xs hover:border-primary",onClick:()=>void Q("sta").catch(s=>r(String(s))),children:"STA"}),e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-xs hover:border-primary",onClick:()=>void Q("ap").catch(s=>r(String(s))),children:"AP"}),e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 p-1.5 hover:border-primary",onClick:()=>void K().catch(s=>r(String(s))),children:e.jsx(he,{className:"h-3.5 w-3.5"})})]})]}),e.jsxs("div",{className:"space-y-3 p-4",children:[e.jsx("p",{className:"rounded border border-outline-variant/20 bg-surface-container-low p-3 font-mono text-xs text-on-surface",children:n}),e.jsxs("div",{className:"grid gap-2 text-xs text-on-surface-variant md:grid-cols-2",children:[e.jsxs("p",{children:["AP 地址:",e.jsx("span",{className:"font-mono text-on-surface",children:N})]}),e.jsxs("p",{children:["mDNS:"," ",w?e.jsx("a",{className:"font-mono text-primary underline",href:w,children:A}):e.jsx("span",{className:"font-mono text-on-surface",children:A})]})]})]})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between",children:[e.jsx("h3",{className:"text-sm font-semibold uppercase tracking-wider",children:"Scan Results"}),e.jsx("button",{type:"button",disabled:b,className:"rounded bg-primary-container px-3 py-1.5 text-xs font-medium text-on-primary-container disabled:opacity-50",onClick:()=>void re(),children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(_e,{className:"h-3.5 w-3.5"})," 扫描 WiFi"]})})]}),e.jsx("p",{className:"mb-3 text-xs text-on-surface-variant",children:j||"由设备端 NetworkManager 执行扫描"}),e.jsx("div",{className:"overflow-x-auto",children:e.jsxs("table",{className:"w-full text-left text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-outline-variant/30 text-xs uppercase tracking-wider text-on-surface-variant",children:[e.jsx("th",{className:"p-2",children:"SSID"}),e.jsx("th",{className:"p-2",children:"信号"}),e.jsx("th",{className:"p-2",children:"安全"}),e.jsx("th",{className:"p-2 text-right",children:"操作"})]})}),e.jsx("tbody",{children:f.map(s=>e.jsxs("tr",{className:"border-b border-outline-variant/10",children:[e.jsx("td",{className:"p-2 font-mono",children:s.ssid}),e.jsx("td",{className:"p-2",children:s.signal??"—"}),e.jsx("td",{className:"p-2",children:s.security??"—"}),e.jsx("td",{className:"p-2 text-right",children:e.jsx("button",{type:"button",disabled:T,className:"rounded border border-primary/40 px-2 py-1 text-xs text-primary disabled:opacity-50",onClick:()=>void ee(s.ssid),children:"Connect"})})]},s.ssid))})]})})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:"Known Networks"}),e.jsx("button",{type:"button",disabled:$,className:"rounded border border-outline-variant/40 px-3 py-1.5 text-xs disabled:opacity-50",onClick:()=>void ae(),children:"刷新已保存网络"}),e.jsxs("table",{className:"mt-3 w-full text-left text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-outline-variant/30 text-xs uppercase tracking-wider text-on-surface-variant",children:[e.jsx("th",{className:"p-2",children:"连接名"}),e.jsx("th",{className:"p-2",children:"SSID"}),e.jsx("th",{className:"p-2",children:"自动连接"}),e.jsx("th",{className:"p-2 text-right",children:"操作"})]})}),e.jsx("tbody",{children:y.map(s=>e.jsxs("tr",{className:"border-b border-outline-variant/10",children:[e.jsx("td",{className:"p-2",children:s.connection_name}),e.jsx("td",{className:"p-2",children:s.ssid}),e.jsx("td",{className:"p-2",children:s.autoconnect?"是":"否"}),e.jsx("td",{className:"p-2 text-right",children:e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 px-2 py-1 text-xs hover:border-primary",onClick:()=>void x(s.connection_name),children:"Activate"})})]},s.connection_name))})]})]})]}),e.jsxs("aside",{className:"col-span-12 space-y-6 lg:col-span-4",children:[e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:"System Monitor"}),e.jsx("div",{className:"grid gap-2",children:ie.map(([s,l])=>e.jsxs("div",{className:"flex justify-between border-b border-outline-variant/10 pb-1 text-xs",children:[e.jsx("span",{className:"text-on-surface-variant",children:s}),e.jsx("span",{className:"font-mono text-on-surface",children:l})]},s))})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:"Manual Connect"}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("label",{className:"block text-xs text-on-surface-variant",children:["SSID",e.jsx("input",{className:"mt-1 w-full rounded border border-outline-variant/40 bg-surface-container-low px-2 py-1.5 text-sm",value:R,onChange:s=>S(s.target.value)})]}),e.jsxs("label",{className:"block text-xs text-on-surface-variant",children:["Password",e.jsx("input",{type:"password",className:"mt-1 w-full rounded border border-outline-variant/40 bg-surface-container-low px-2 py-1.5 text-sm",value:E,onChange:s=>h(s.target.value)})]}),e.jsx("button",{type:"button",disabled:T,className:"w-full rounded bg-primary-container px-4 py-2 text-sm font-medium text-on-primary-container disabled:opacity-50",onClick:()=>void se(),children:"连接并切换 STA"})]}),e.jsx("p",{className:"mt-2 text-xs text-on-surface-variant",children:c})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:"WiFi 引导"}),e.jsxs("ol",{className:"list-decimal space-y-1 pl-4 text-xs text-on-surface-variant",children:[e.jsxs("li",{children:["连接热点 ",e.jsx("strong",{children:z}),",密码 ",e.jsx("code",{children:"ogscopeadmin"})]}),e.jsxs("li",{children:["浏览器打开 ",e.jsxs("span",{className:"font-mono",children:["http://192.168.4.1:",le()]})]}),e.jsx("li",{children:"扫描 WiFi 或手动填写 SSID 连接"})]})]}),e.jsxs("div",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h3",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:"Find Device"}),e.jsx("p",{className:"mb-3 text-xs text-on-surface-variant",children:"扫描常见网段并探测 /health"}),e.jsx("button",{type:"button",className:"w-full rounded border border-outline-variant/40 px-3 py-2 text-sm hover:border-primary",onClick:()=>void X(),children:"扫描局域网"}),e.jsx("p",{className:"mt-2 text-xs text-on-surface-variant",children:v})]})]})]})]})}function es(t){var r,c;const o=(c=(r=t==null?void 0:t.data)==null?void 0:r.services)==null?void 0:c.hmi;if(!o||typeof o!="object")return null;const n=o.display;return!n||typeof n!="object"?null:n}function ss(t){try{return JSON.stringify(t,null,2)}catch{return String(t)}}function ts(){var v,_;const{t}=Z(),[o,n]=a.useState(null),[r,c]=a.useState(null),[d,j]=a.useState(!1),[u,f]=a.useState(null),[g,b]=a.useState(null),[k,y]=a.useState(40),[m,$]=a.useState(80),[M,R]=a.useState(200),S=a.useCallback(async()=>{try{c(null);const N=await Ke();n(N)}catch(N){n(null),c(N instanceof Error?N.message:String(N))}},[]);a.useEffect(()=>{S()},[S]);const E=a.useCallback(async(N,P={})=>{var z;j(!0),b(null);try{const i=await Xe({target:"hmi",action:N,payload:P,timeout_ms:8e3});if(f(i),!i.success){const A=((z=i.error)==null?void 0:z.message)??"RPC failed";b(A);return}const w=i.data,L=w==null?void 0:w.result;L&&L.accepted===!1&&L.message?b(L.message):b(null),await S()}catch(i){f(null),b(i instanceof Error?i.message:String(i))}finally{j(!1)}},[S]),h=es(o),T=(_=(v=o==null?void 0:o.data)==null?void 0:v.services)==null?void 0:_.hmi,O=typeof(T==null?void 0:T.screen_on)=="boolean"?T.screen_on:void 0;return e.jsxs("div",{className:"mx-auto max-w-6xl space-y-6",children:[e.jsxs("header",{children:[e.jsx("div",{className:"text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:t("sys.placeholder.breadcrumb")}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:t("sys.hmi.title")}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:t("sys.hmi.desc")})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:t("sys.hmi.status.section")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 bg-surface-container px-3 py-1.5 text-xs font-medium text-on-surface hover:bg-surface-container-high",onClick:()=>void S(),disabled:d,children:t("sys.hmi.status.refresh")})]}),r?e.jsx("p",{className:"mt-2 text-sm text-error",children:r}):e.jsxs("dl",{className:"mt-3 grid gap-2 font-mono text-[11px] text-on-surface-variant sm:grid-cols-2",children:[e.jsxs("div",{children:[e.jsx("dt",{className:"text-on-surface-variant",children:t("sys.hmi.status.displayEnabled")}),e.jsx("dd",{className:"text-on-surface",children:(h==null?void 0:h.enabled)===!0?"true":"false"})]}),e.jsxs("div",{children:[e.jsx("dt",{children:t("sys.hmi.status.spidev")}),e.jsx("dd",{className:h!=null&&h.spidev_present?"text-primary":"text-error",children:h!=null&&h.spidev_present?t("sys.hmi.status.yes"):t("sys.hmi.status.no")})]}),e.jsxs("div",{children:[e.jsx("dt",{children:t("sys.hmi.status.resolution")}),e.jsxs("dd",{children:[(h==null?void 0:h.width)??"—"," × ",(h==null?void 0:h.height)??"—"," · DC GPIO ",(h==null?void 0:h.dc_pin)??"—"]})]}),e.jsxs("div",{children:[e.jsx("dt",{children:t("sys.hmi.status.driver")}),e.jsx("dd",{children:h!=null&&h.driver_open?t("sys.hmi.status.open"):t("sys.hmi.status.closed")})]}),e.jsxs("div",{children:[e.jsx("dt",{children:t("sys.hmi.status.screenOutput")}),e.jsx("dd",{children:O===void 0?"—":t(O?"sys.hmi.status.on":"sys.hmi.status.off")})]}),e.jsxs("div",{className:"sm:col-span-2",children:[e.jsx("dt",{children:t("sys.hmi.status.lastPattern")}),e.jsx("dd",{children:(h==null?void 0:h.last_pattern)??"—"})]}),h!=null&&h.last_error?e.jsxs("div",{className:"sm:col-span-2",children:[e.jsx("dt",{children:t("sys.hmi.status.lastError")}),e.jsx("dd",{className:"text-error",children:h.last_error})]}):null]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:t("sys.hmi.actions.section")}),e.jsx("p",{className:"mt-1 text-[11px] text-on-surface-variant",children:t("sys.hmi.actions.hint")}),e.jsxs("div",{className:"mt-4 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",className:"rounded-lg bg-primary px-3 py-2 text-xs font-semibold text-on-primary hover:opacity-90 disabled:opacity-50",disabled:d,onClick:()=>void E("display.test_pattern",{pattern:"smoke"}),children:t("sys.hmi.actions.smoke")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 bg-surface-container px-3 py-2 text-xs font-medium text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("display.test_pattern",{pattern:"colorbars"}),children:t("sys.hmi.actions.colorbars")})]}),e.jsxs("div",{className:"mt-6 flex flex-wrap items-end gap-3",children:[e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:["R",e.jsx("input",{type:"number",min:0,max:255,className:"w-20 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:k,onChange:N=>y(Number(N.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:["G",e.jsx("input",{type:"number",min:0,max:255,className:"w-20 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:m,onChange:N=>$(Number(N.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:["B",e.jsx("input",{type:"number",min:0,max:255,className:"w-20 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:M,onChange:N=>R(Number(N.target.value))})]}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 bg-surface-container px-3 py-2 text-xs font-medium text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("display.test_pattern",{pattern:"fill",r:k,g:m,b:M}),children:t("sys.hmi.actions.fill")})]}),e.jsxs("div",{className:"mt-6 flex flex-wrap gap-2 border-t border-outline-variant/20 pt-4",children:[e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("screen.set",{on:!0}),children:t("sys.hmi.actions.screenOn")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("screen.set",{on:!1}),children:t("sys.hmi.actions.screenOff")}),e.jsx("button",{type:"button",className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs text-on-surface hover:bg-surface-container-high disabled:opacity-50",disabled:d,onClick:()=>void E("display.release"),children:t("sys.hmi.actions.release")})]}),g?e.jsx("p",{className:"mt-3 text-sm text-error",children:g}):null,e.jsxs("details",{className:"mt-4",children:[e.jsx("summary",{className:"cursor-pointer text-[11px] text-on-surface-variant",children:t("sys.hmi.rawJson")}),e.jsx("pre",{className:"mt-2 max-h-64 overflow-auto rounded border border-outline-variant/30 bg-surface-container p-2 font-mono text-[10px] text-on-surface",children:u?ss(u):"—"})]})]})]})}const rs={sensors:{titleKey:"sys.placeholder.sensors.title",descKey:"sys.placeholder.sensors.desc",blocks:["sys.placeholder.sensors.block1","sys.placeholder.sensors.block2","sys.placeholder.sensors.block3"]},hmi:{titleKey:"sys.placeholder.hmi.title",descKey:"sys.placeholder.hmi.desc",blocks:["sys.placeholder.hmi.block1","sys.placeholder.hmi.block2","sys.placeholder.hmi.block3"]},power:{titleKey:"sys.placeholder.power.title",descKey:"sys.placeholder.power.desc",blocks:["sys.placeholder.power.block1","sys.placeholder.power.block2","sys.placeholder.power.block3"]}};function ve({scope:t}){const{t:o}=Z(),n=rs[t];return e.jsxs("div",{className:"mx-auto max-w-6xl space-y-6",children:[e.jsxs("header",{children:[e.jsx("div",{className:"text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:o("sys.placeholder.breadcrumb")}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:o(n.titleKey)}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:o(n.descKey)})]}),e.jsx("section",{className:"grid grid-cols-12 gap-4",children:n.blocks.map(r=>e.jsxs("article",{className:"col-span-12 rounded-xl border border-dashed border-outline-variant/40 bg-surface-container/60 p-5 md:col-span-4",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-primary",children:o("sys.placeholder.block")}),e.jsx("h3",{className:"mt-2 text-lg font-semibold",children:o(r)}),e.jsx("p",{className:"mt-2 text-sm text-on-surface-variant",children:o("sys.placeholder.desc")})]},r))}),e.jsx("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:e.jsx("p",{className:"font-mono text-xs text-on-surface-variant",children:o("sys.placeholder.status")})})]})}const B=62,te=4;function as(t){const{rollDeg:o,pitchDeg:n,yawRateDps:r}=t,{t:c}=Z(),d=`rotateX(${-n}deg) rotateY(${o}deg)`;return e.jsxs("div",{className:"mt-5 rounded-xl border border-teal-500/25 bg-black/25 p-4 ring-1 ring-teal-500/10",children:[e.jsx("p",{className:"text-center text-[11px] font-semibold text-teal-200/95",children:c("sys.sensors.gyro.att3dTitle")}),e.jsx("p",{className:"mx-auto mt-1 max-w-xl text-center text-[10px] leading-relaxed text-on-surface-variant",children:c("sys.sensors.gyro.att3dDesc")}),e.jsxs("div",{className:"mt-4 flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between xl:gap-8",children:[e.jsxs("div",{className:"flex min-w-0 flex-1 flex-col items-center gap-4",children:[e.jsx("div",{className:"flex shrink-0 items-center justify-center py-2",style:{perspective:"820px"},children:e.jsxs("div",{className:"relative h-[188px] w-[220px]",style:{transformStyle:"preserve-3d",transform:d},children:[e.jsxs("div",{className:"absolute left-[10px] top-[18px] h-[152px] w-[200px] rounded-2xl border-2 border-teal-400/75 bg-gradient-to-br from-slate-600/90 via-slate-800/95 to-slate-950 shadow-[0_20px_50px_rgba(0,0,0,0.55),inset_0_1px_0_rgba(255,255,255,0.08)] transition-transform duration-150 ease-out",style:{transformStyle:"preserve-3d"},children:[e.jsx("div",{className:"pointer-events-none absolute inset-x-0 top-2 flex justify-center",children:e.jsx("span",{className:"rounded bg-black/35 px-2 py-0.5 text-[9px] font-bold uppercase tracking-[0.2em] text-teal-100",children:"TOP"})}),e.jsx("div",{className:"absolute bottom-2 left-2 font-mono text-[9px] text-slate-400",children:"MPU-6050"})]}),e.jsxs("div",{className:"pointer-events-none absolute left-[110px] top-[94px] h-0 w-0",style:{transformStyle:"preserve-3d"},children:[e.jsx("div",{className:"absolute rounded-full bg-white/90 shadow-[0_0_6px_rgba(255,255,255,0.6)]",style:{width:7,height:7,left:-3.5,top:-3.5,transform:"translateZ(0.5px)"}}),e.jsx("div",{className:"absolute bg-amber-400 shadow-md ring-1 ring-amber-200/35",style:{width:B,height:te,left:0,top:-te/2,transformOrigin:"0 50%"}}),e.jsx("span",{className:"absolute whitespace-nowrap font-mono text-[10px] font-bold text-amber-200",style:{left:B+4,top:-8},children:c("sys.sensors.gyro.axisXLabel")}),e.jsx("div",{className:"absolute bg-sky-400 shadow-md ring-1 ring-sky-200/30",style:{width:B,height:te,left:0,top:-te/2,transformOrigin:"0 50%",transform:"rotateZ(90deg)"}}),e.jsx("span",{className:"absolute whitespace-nowrap font-mono text-[10px] font-bold text-sky-200",style:{left:-6,top:-B-16},children:c("sys.sensors.gyro.axisYLabel")}),e.jsx("div",{className:"absolute bg-violet-400 shadow-md ring-1 ring-violet-200/35",style:{width:B,height:te,left:0,top:-te/2,transformOrigin:"0 50%",transform:"rotateY(-90deg)"}}),e.jsx("span",{className:"absolute whitespace-nowrap font-mono text-[10px] font-bold text-violet-200",style:{left:B*.35,top:-B*.45,transform:"translateZ(28px)"},children:c("sys.sensors.gyro.axisZLabel")}),e.jsx("div",{className:"absolute bg-amber-900/55",style:{width:B*.45,height:2,left:-B*.45,top:-1,transformOrigin:"100% 50%"}}),e.jsx("div",{className:"absolute bg-sky-900/50",style:{width:B*.45,height:2,left:0,top:-1,transformOrigin:"0 50%",transform:`rotateZ(90deg) translateX(${-B*.45}px)`}}),e.jsx("div",{className:"absolute bg-violet-900/45",style:{width:B*.4,height:2,left:0,top:-1,transformOrigin:"0 50%",transform:`rotateY(-90deg) translateX(${-B*.4}px)`}})]})]})}),e.jsx("p",{className:"max-w-md text-center text-[10px] leading-snug text-on-surface-variant",children:c("sys.sensors.gyro.att3dBodyAxes")})]}),e.jsxs("div",{className:"flex w-full max-w-[220px] shrink-0 flex-col items-center gap-2 self-center xl:self-start",children:[e.jsx("p",{className:"text-center text-[10px] font-medium text-on-surface-variant",children:c("sys.sensors.gyro.att3dRefTitle")}),e.jsx("div",{className:"flex items-center justify-center rounded-lg border border-outline-variant/30 bg-slate-950/50 px-4 py-5 ring-1 ring-white/5",children:e.jsx("div",{style:{perspective:"280px"},children:e.jsx("div",{className:"relative h-[100px] w-[100px]",style:{transformStyle:"preserve-3d",transform:"rotateX(58deg) rotateZ(-42deg)"},children:e.jsxs("div",{className:"absolute left-1/2 top-1/2 h-0 w-0",style:{transformStyle:"preserve-3d"},children:[e.jsx("div",{className:"absolute bg-amber-400/95",style:{width:44,height:3,left:0,top:-1.5,transformOrigin:"0 50%"}}),e.jsx("div",{className:"absolute bg-sky-400/95",style:{width:44,height:3,left:0,top:-1.5,transformOrigin:"0 50%",transform:"rotateZ(90deg)"}}),e.jsx("div",{className:"absolute bg-violet-400/95",style:{width:44,height:3,left:0,top:-1.5,transformOrigin:"0 50%",transform:"rotateY(-90deg)"}}),e.jsx("div",{className:"absolute rounded-full bg-white/80",style:{width:5,height:5,left:-2.5,top:-2.5}})]})})})}),e.jsx("p",{className:"text-center text-[9px] leading-relaxed text-on-surface-variant/85",children:c("sys.sensors.gyro.att3dRefDesc")})]}),e.jsxs("div",{className:"grid w-full min-w-[200px] max-w-md grid-cols-3 gap-3 font-mono text-[11px] lg:max-w-lg xl:max-w-[340px]",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/80 px-2 py-2 text-center",children:[e.jsx("p",{className:"text-[9px] uppercase tracking-wider text-on-surface-variant",children:c("sys.sensors.gyro.roll")}),e.jsxs("p",{className:"mt-1 text-lg font-bold tabular-nums text-teal-200",children:[o.toFixed(1),"°"]})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/80 px-2 py-2 text-center",children:[e.jsx("p",{className:"text-[9px] uppercase tracking-wider text-on-surface-variant",children:c("sys.sensors.gyro.pitch")}),e.jsxs("p",{className:"mt-1 text-lg font-bold tabular-nums text-teal-200",children:[n.toFixed(1),"°"]})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/80 px-2 py-2 text-center",children:[e.jsx("p",{className:"text-[9px] uppercase tracking-wider text-on-surface-variant",children:c("sys.sensors.gyro.yawRate")}),e.jsx("p",{className:"mt-1 text-lg font-bold tabular-nums text-amber-200/95",children:r.toFixed(2)}),e.jsx("p",{className:"text-[9px] text-on-surface-variant",children:"°/s"})]})]})]})]})}const xe=250;function ue(t){const{label:o,value:n,maxAbs:r,unit:c}=t,d=Math.max(-1,Math.min(1,n/r)),j=d>=0?50:50+d*50,u=Math.abs(d)*50;return e.jsxs("div",{className:"space-y-1",children:[e.jsxs("div",{className:"flex items-baseline justify-between gap-2",children:[e.jsx("span",{className:"text-[11px] font-medium text-on-surface",children:o}),e.jsxs("span",{className:"font-mono text-xs tabular-nums text-sky-100",children:[n.toFixed(2),e.jsxs("span",{className:"text-on-surface-variant",children:[" ",c]})]})]}),e.jsxs("div",{className:"relative h-5 w-full overflow-hidden rounded-md bg-slate-900/80 ring-1 ring-slate-600/50",children:[e.jsx("div",{className:"absolute left-1/2 top-0 z-10 h-full w-px -translate-x-px bg-slate-500/90"}),e.jsx("div",{className:"absolute top-1 h-3 rounded-sm bg-gradient-to-r from-emerald-600 to-sky-500 shadow-sm",style:{left:`${j}%`,width:`${Math.max(u,d===0?0:.8)}%`}})]})]})}function ns(t){const{bus:o,addr:n}=t,{t:r}=Z(),[c,d]=a.useState(!1),[j,u]=a.useState(null),[f,g]=a.useState(null),[b,k]=a.useState(null),[y,m]=a.useState(null),[$,M]=a.useState(null),[R,S]=a.useState(null),[E,h]=a.useState(!1),T=a.useRef(null),O=a.useCallback(async()=>{var _;d(!0),u(null);try{const N=new URLSearchParams({bus:String(o),addr:String(n)}),P=await q(`/api/debug/sensors/mpu6050/imu-sample?${N.toString()}`);if(!P.success){g(null),k(null),m(null),M(null),S(null),u(((_=P.sample)==null?void 0:_.error)||P.error||r("sys.sensors.gyro.errUnknown"));return}g(P.gyro_dps??null),k(P.gyro_raw??null),m(P.tilt_deg??null),M(P.yaw_rate_dps??null),S(P.accel_g??null)}catch(N){g(null),k(null),m(null),M(null),S(null),u(N instanceof Error?N.message:String(N))}finally{d(!1)}},[n,o,r]);a.useEffect(()=>{if(!E){T.current&&(clearInterval(T.current),T.current=null);return}return T.current=setInterval(()=>void O(),500),()=>{T.current&&clearInterval(T.current)}},[E,O]);const v=y!=null&&$!=null&&Number.isFinite(y.roll)&&Number.isFinite(y.pitch);return e.jsxs("div",{className:"mt-6 rounded-xl border border-emerald-600/35 bg-gradient-to-br from-slate-900/40 to-surface-container/70 p-4",children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-semibold text-on-surface",children:r("sys.sensors.gyro.title")}),e.jsx("p",{className:"mt-1 max-w-xl text-[11px] leading-snug text-on-surface-variant",children:r("sys.sensors.gyro.subtitle")})]}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("label",{className:"flex cursor-pointer items-center gap-1.5 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:E,onChange:_=>h(_.target.checked)}),r("sys.sensors.gyro.live")]}),e.jsx("button",{type:"button",disabled:c,className:"rounded-lg bg-emerald-800/90 px-3 py-1.5 text-xs font-medium text-emerald-50 hover:bg-emerald-700 disabled:opacity-50",onClick:()=>void O(),children:r(c?"sys.sensors.gyro.loading":"sys.sensors.gyro.btn")})]})]}),v&&e.jsx(as,{rollDeg:y.roll,pitchDeg:y.pitch,yawRateDps:$}),f&&e.jsxs("div",{className:"mt-5 grid gap-4 md:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-emerald-500/20 bg-black/20 px-3 py-3 text-center md:col-span-1",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"ωx"}),e.jsx("p",{className:"font-mono text-3xl font-bold tabular-nums text-emerald-200",children:f.x.toFixed(2)}),e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"°/s"})]}),e.jsxs("div",{className:"rounded-lg border border-emerald-500/20 bg-black/20 px-3 py-3 text-center md:col-span-1",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"ωy"}),e.jsx("p",{className:"font-mono text-3xl font-bold tabular-nums text-emerald-200",children:f.y.toFixed(2)}),e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"°/s"})]}),e.jsxs("div",{className:"rounded-lg border border-emerald-500/20 bg-black/20 px-3 py-3 text-center md:col-span-1",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"ωz"}),e.jsx("p",{className:"font-mono text-3xl font-bold tabular-nums text-emerald-200",children:f.z.toFixed(2)}),e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:"°/s"})]})]}),f&&e.jsxs("div",{className:"mt-5 space-y-4",children:[e.jsx("p",{className:"text-[10px] font-medium uppercase tracking-wider text-on-surface-variant",children:r("sys.sensors.gyro.barsTitle")}),e.jsx(ue,{label:"X",value:f.x,maxAbs:xe,unit:"°/s"}),e.jsx(ue,{label:"Y",value:f.y,maxAbs:xe,unit:"°/s"}),e.jsx(ue,{label:"Z",value:f.z,maxAbs:xe,unit:"°/s"})]}),R&&e.jsxs("p",{className:"mt-3 text-center font-mono text-[10px] text-on-surface-variant",children:["g — X:",R.x.toFixed(3)," Y:",R.y.toFixed(3)," Z:",R.z.toFixed(3)]}),b&&e.jsxs("div",{className:"mt-4 rounded border border-outline-variant/25 bg-surface-container/50 px-3 py-2",children:[e.jsx("p",{className:"text-[10px] text-on-surface-variant",children:r("sys.sensors.gyro.rawBlock")}),e.jsxs("p",{className:"mt-1 font-mono text-[11px] tabular-nums text-on-surface",children:["raw X=",b.x," · Y=",b.y," · Z=",b.z]})]}),!f&&!j&&!c&&e.jsx("p",{className:"mt-4 text-[11px] text-on-surface-variant",children:r("sys.sensors.gyro.hint")}),j&&e.jsx("p",{className:"mt-3 rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 font-mono text-[11px] text-amber-100",children:j})]})}const be=3.4,Ce=7,os=3;function ls(t){const o=(t%360+360)%360;return o<1||o>359?"cardN":Math.abs(o-90)<1?"cardE":Math.abs(o-180)<1?"cardS":Math.abs(o-270)<1?"cardW":null}function is(){const t=[],o=Ce*360;for(let n=0;n<=o;n+=5){const r=n%360,c=n%30===0,d=!c&&n%10===0,j=ls(r),u=!j&&c&&r%90!==0?r:void 0;t.push({x:n*be,deg:n,h:c?"maj":d?"mid":"min",degLabel:u,cardKey:j??void 0})}return t}const cs=is(),je=Ce*360*be;function ds(t,o){const n=(t%360+360)%360;let c=(o%360+360)%360-n;return c=(c+180)%360-180,t+c}function xs(t){const{bus:o,addr:n}=t,{t:r}=Z(),[c,d]=a.useState(!1),[j,u]=a.useState(null),[f,g]=a.useState(null),[b,k]=a.useState(null),[y,m]=a.useState(!1),$=a.useRef(null),M=a.useRef(0),R=a.useRef(null),[S,E]=a.useState(320);a.useEffect(()=>{const v=R.current;if(!v)return;const _=new ResizeObserver(()=>{E(Math.max(200,v.clientWidth))});return _.observe(v),E(Math.max(200,v.clientWidth)),()=>_.disconnect()},[]),a.useEffect(()=>{g(null),k(null),u(null)},[o,n]);const h=a.useCallback(async()=>{const v=++M.current;d(!0);try{const _=new URLSearchParams({bus:String(o),addr:String(n)}),N=await q(`/api/debug/sensors/magnetometer/sample?${_.toString()}`);if(v!==M.current)return;if(!N.success){g(null),k(null),u(N.error||r("sys.sensors.compass.err"));return}u(null);const P=N.heading_deg??null;P!=null&&g(z=>z==null?os*360+P:ds(z,P)),k(N.field_ut??null)}catch(_){if(v!==M.current)return;g(null),k(null),u(_ instanceof Error?_.message:String(_))}finally{v===M.current&&d(!1)}},[n,o,r]);a.useEffect(()=>{if(!y){$.current&&(clearInterval($.current),$.current=null);return}return $.current=setInterval(()=>{h()},850),()=>{$.current&&clearInterval($.current)}},[y,h]);const T=a.useMemo(()=>{if(f==null)return 0;const v=f*be;return S/2-v},[f,S]),O=f!=null?(f%360+360)%360:null;return e.jsxs("div",{className:"rounded-xl border border-sky-500/30 bg-gradient-to-b from-slate-900/90 via-slate-900/70 to-surface-container/90 p-4",children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-semibold text-on-surface",children:r("sys.sensors.compass.title")}),e.jsx("p",{className:"mt-1 max-w-xl text-[11px] leading-snug text-on-surface-variant",children:r("sys.sensors.compass.desc")})]}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("label",{className:"flex cursor-pointer items-center gap-1.5 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:y,onChange:v=>m(v.target.checked)}),r("sys.sensors.compass.live")]}),e.jsx("button",{type:"button",disabled:c,className:"rounded-lg bg-sky-700/80 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-600 disabled:opacity-50",onClick:()=>void h(),children:r(c?"sys.sensors.compass.loading":"sys.sensors.compass.btn")})]})]}),e.jsxs("div",{className:"mt-5",children:[e.jsx("p",{className:"mb-2 text-center text-[10px] uppercase tracking-[0.2em] text-sky-300/90",children:r("sys.sensors.compass.tapeCaption")}),e.jsxs("div",{ref:R,className:"relative mx-auto h-[112px] w-full max-w-3xl overflow-hidden rounded-lg ring-1 ring-sky-500/25",style:{maskImage:"linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%)",WebkitMaskImage:"linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%)"},children:[e.jsx("div",{className:"pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center",children:e.jsxs("div",{className:"flex flex-col items-center",children:[e.jsx("div",{className:"h-0 w-0 border-x-[9px] border-x-transparent border-b-[12px] border-b-amber-400 drop-shadow"}),e.jsx("div",{className:"h-[88px] w-0.5 rounded-full bg-gradient-to-b from-amber-300/95 to-sky-400/40"})]})}),e.jsx("div",{className:"absolute bottom-0 left-0 top-0 will-change-transform",style:{width:je,transform:`translateX(${T}px)`,transition:"transform 0.42s cubic-bezier(0.22, 0.95, 0.28, 1)"},children:e.jsxs("div",{className:"relative h-full",style:{width:je,background:"linear-gradient(180deg, rgba(15,23,42,0.2) 0%, rgba(30,41,59,0.85) 40%, rgba(15,23,42,0.95) 100%)"},children:[e.jsx("div",{className:"absolute left-0 right-0 top-8 h-px bg-slate-600/60"}),cs.map(v=>{const _=v.h==="maj"?22:v.h==="mid"?14:8;return e.jsxs("div",{className:"absolute flex flex-col items-center",style:{left:v.x,transform:"translateX(-50%)",top:32},children:[(v.cardKey||v.degLabel!=null)&&e.jsx("span",{className:`mb-0.5 whitespace-nowrap font-mono ${v.cardKey?"text-[13px] font-bold text-sky-200":"text-[10px] font-medium text-slate-400"}`,style:{marginTop:-18},children:v.cardKey?r(`sys.sensors.compass.${v.cardKey}`):String(v.degLabel)}),e.jsx("div",{className:`w-px rounded-full ${v.h==="maj"?"bg-sky-300/90":v.h==="mid"?"bg-slate-500/85":"bg-slate-600/50"}`,style:{height:_}})]},v.deg)}),e.jsx("div",{className:"absolute bottom-6 left-0 right-0 h-px bg-slate-600/40"})]})})]})]}),e.jsxs("div",{className:"mt-5 flex flex-col gap-4 md:flex-row md:items-start md:justify-center md:gap-8",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/30 bg-surface-container/90 px-6 py-4 text-center md:min-w-[200px]",children:[e.jsx("p",{className:"text-[10px] uppercase tracking-widest text-on-surface-variant",children:r("sys.sensors.compass.headingLabel")}),e.jsx("p",{className:"font-mono text-4xl font-bold tabular-nums text-sky-200",children:O!=null?`${O.toFixed(1)}°`:"—"}),e.jsx("p",{className:"mt-1 text-[10px] text-on-surface-variant",children:r("sys.sensors.compass.headingHint")})]}),b&&e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-4 py-3 font-mono text-[11px] text-on-surface md:max-w-md",children:[e.jsx("p",{className:"mb-1 text-[10px] text-on-surface-variant",children:"µT (X / Y / Z)"}),e.jsxs("p",{className:"tabular-nums",children:[b.x.toFixed(2)," · ",b.y.toFixed(2)," · ",b.z.toFixed(2)]})]})]}),j&&e.jsx("p",{className:"mt-3 rounded border border-amber-500/40 bg-amber-500/10 px-3 py-2 font-mono text-[11px] text-amber-100",children:j}),e.jsx("p",{className:"mt-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:r("sys.sensors.compass.footnote")})]})}function Ne(t){try{return JSON.stringify(t,null,2)}catch{return String(t)}}function us(){const{t}=Z(),[o,n]=a.useState(1),[r,c]=a.useState(12),[d,j]=a.useState(!0),[u,f]=a.useState(!1),[g,b]=a.useState(null),[k,y]=a.useState(null),[m,$]=a.useState(null),[M,R]=a.useState(null),[S,E]=a.useState(1),[h,T]=a.useState(104),[O,v]=a.useState(!0),[_,N]=a.useState(!1),[P,z]=a.useState(null),[i,w]=a.useState(null),L=a.useCallback(async()=>{f(!0),b(null);try{const x=new URLSearchParams({bus:String(o),addr:String(r),i2cdetect:d?"true":"false"}),F=await q(`/api/debug/sensors/magnetometer/selftest?${x.toString()}`);y(F)}catch(x){y(null),b(x instanceof Error?x.message:String(x))}finally{f(!1)}},[r,o,d]),A=a.useCallback(async()=>{f(!0),b(null);try{const x=new URLSearchParams({addr:String(r)}),F=await q(`/api/debug/sensors/magnetometer/probe-buses?${x.toString()}`);y(F)}catch(x){y(null),b(x instanceof Error?x.message:String(x))}finally{f(!1)}},[r]),I=a.useCallback(async()=>{f(!0),b(null);try{const x=new URLSearchParams({bus:String(o),addr:String(r)}),F=await q(`/api/debug/sensors/magnetometer/calibration/start?${x.toString()}`,{method:"POST"});y(F),R("已开始方向校准,请缓慢旋转设备 5-15 秒。");const X=await q(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);$(X)}catch(x){y(null),b(x instanceof Error?x.message:String(x))}finally{f(!1)}},[r,o]),U=a.useCallback(async()=>{f(!0),b(null);try{const x=new URLSearchParams({bus:String(o),addr:String(r)}),F=await q(`/api/debug/sensors/magnetometer/calibration/commit?${x.toString()}`,{method:"POST"});y(F),R("已保存并锁定方向校准。");const X=await q(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);$(X)}catch(x){y(null),b(x instanceof Error?x.message:String(x))}finally{f(!1)}},[r,o]),K=a.useCallback(async()=>{f(!0),b(null);try{const x=new URLSearchParams({bus:String(o),addr:String(r)}),F=await q(`/api/debug/sensors/magnetometer/calibration/reset?${x.toString()}`,{method:"POST"});y(F),R("已重置到自动模式。");const X=await q(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);$(X)}catch(x){y(null),b(x instanceof Error?x.message:String(x))}finally{f(!1)}},[r,o]),Q=a.useCallback(async()=>{f(!0),b(null);try{const x=new URLSearchParams({bus:String(o),addr:String(r)}),F=await q(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);y(F),$(F)}catch(x){y(null),b(x instanceof Error?x.message:String(x))}finally{f(!1)}},[r,o]);a.useEffect(()=>{const x=new URLSearchParams({bus:String(o),addr:String(r)});q(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`).then(F=>$(F)).catch(()=>{})},[r,o]),a.useEffect(()=>{if((m==null?void 0:m.mode)!=="recording")return;const x=setInterval(()=>{const F=new URLSearchParams({bus:String(o),addr:String(r)});q(`/api/debug/sensors/magnetometer/calibration/status?${F.toString()}`).then(X=>$(X)).catch(()=>{})},900);return()=>clearInterval(x)},[m==null?void 0:m.mode,o,r]);const re=a.useMemo(()=>{const x=(m==null?void 0:m.mode)??"auto";return t(x==="recording"?"sys.sensors.mag.calModeRecording":x==="locked"?"sys.sensors.mag.calModeLocked":"sys.sensors.mag.calModeAuto")},[m==null?void 0:m.mode,t]),ee=Number((m==null?void 0:m.samples)??0),se=(m==null?void 0:m.mode)==="recording"&&ee>=10,ae=a.useCallback(async()=>{N(!0),z(null);try{const x=new URLSearchParams({bus:String(S),addr:String(h),i2cdetect:O?"true":"false"}),F=await q(`/api/debug/sensors/mpu6050/selftest?${x.toString()}`);w(F)}catch(x){w(null),z(x instanceof Error?x.message:String(x))}finally{N(!1)}},[h,S,O]);return e.jsxs("div",{className:"mx-auto max-w-6xl space-y-6",children:[e.jsxs("header",{children:[e.jsx("div",{className:"text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:t("sys.placeholder.breadcrumb")}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:t("sys.sensors.title")}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:t("sys.sensors.desc")})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:t("sys.sensors.mag.section")}),e.jsx("p",{className:"mt-1 text-[11px] text-on-surface-variant",children:t("sys.sensors.mag.note")}),e.jsxs("div",{className:"mt-4 flex flex-wrap items-end gap-4",children:[e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[t("sys.sensors.mag.bus"),e.jsx("input",{type:"number",min:0,max:32,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:o,onChange:x=>n(Number(x.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[t("sys.sensors.mag.addr"),e.jsx("input",{type:"number",min:1,max:127,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:r,onChange:x=>c(Number(x.target.value))})]}),e.jsxs("label",{className:"flex items-center gap-2 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:d,onChange:x=>j(x.target.checked)}),t("sys.sensors.mag.i2cdetect")]})]}),e.jsx(xs,{bus:o,addr:r}),e.jsxs("div",{className:"mt-6 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",disabled:u,className:"rounded-lg bg-primary px-4 py-2 text-sm font-medium text-on-primary hover:opacity-90 disabled:opacity-50",onClick:()=>void L(),children:t(u?"sys.sensors.mag.running":"sys.sensors.mag.btnSelftest")}),e.jsx("button",{type:"button",disabled:u,className:"rounded-lg border border-outline-variant/40 px-4 py-2 text-sm text-on-surface hover:bg-surface-container/80 disabled:opacity-50",onClick:()=>void A(),children:t("sys.sensors.mag.btnProbe")})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",disabled:u||(m==null?void 0:m.mode)==="recording",className:"rounded-lg border border-amber-400/40 px-3 py-1.5 text-xs text-on-surface hover:bg-amber-500/10 disabled:opacity-50",onClick:()=>void I(),children:t("sys.sensors.mag.btnCalStart")}),e.jsx("button",{type:"button",disabled:u||!se,className:"rounded-lg border border-emerald-400/40 px-3 py-1.5 text-xs text-on-surface hover:bg-emerald-500/10 disabled:opacity-50",onClick:()=>void U(),children:t("sys.sensors.mag.btnCalCommit")}),e.jsx("button",{type:"button",disabled:u,className:"rounded-lg border border-rose-400/40 px-3 py-1.5 text-xs text-on-surface hover:bg-rose-500/10 disabled:opacity-50",onClick:()=>void K(),children:t("sys.sensors.mag.btnCalReset")}),e.jsx("button",{type:"button",disabled:u,className:"rounded-lg border border-outline-variant/40 px-3 py-1.5 text-xs text-on-surface hover:bg-surface-container/80 disabled:opacity-50",onClick:()=>void Q(),children:t("sys.sensors.mag.btnCalStatus")})]}),e.jsxs("div",{className:"mt-3 rounded-lg border border-outline-variant/30 bg-surface-container/60 px-3 py-2 text-[11px] text-on-surface",children:[e.jsxs("p",{className:"font-medium",children:[t("sys.sensors.mag.calStatusPrefix")," ",re]}),e.jsxs("p",{className:"mt-1 text-on-surface-variant",children:[t("sys.sensors.mag.calSamplesPrefix")," ",ee,(m==null?void 0:m.mode)==="recording"?" / 10+":""]}),(m==null?void 0:m.span_xyz)&&e.jsxs("p",{className:"mt-1 font-mono text-[10px] text-on-surface-variant",children:["span xyz: ",Number(m.span_xyz.x??0).toFixed(1)," /"," ",Number(m.span_xyz.y??0).toFixed(1)," /"," ",Number(m.span_xyz.z??0).toFixed(1)]}),(m==null?void 0:m.mode)==="recording"&&e.jsx("p",{className:"mt-1 text-amber-200",children:t("sys.sensors.mag.calRecordingHint")}),(m==null?void 0:m.mode)==="locked"&&m.locked&&e.jsxs("p",{className:"mt-1 text-emerald-200",children:[t("sys.sensors.mag.calLockedHint")," axes=",String(m.locked.axes_pair??"xy")]}),M&&e.jsx("p",{className:"mt-1 text-sky-200",children:M})]}),g&&e.jsx("p",{className:"mt-3 rounded border border-red-500/40 bg-red-500/10 px-3 py-2 font-mono text-xs text-red-200",children:g}),k&&e.jsxs("details",{className:"mt-4 rounded-lg border border-outline-variant/30 bg-surface-container/50",children:[e.jsx("summary",{className:"cursor-pointer px-3 py-2 text-[11px] text-on-surface-variant",children:t("sys.sensors.jsonToggle")}),e.jsx("pre",{className:"max-h-[320px] overflow-auto border-t border-outline-variant/20 p-3 font-mono text-[11px] leading-relaxed text-on-surface",children:Ne(k)})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container-low p-4",children:[e.jsx("p",{className:"text-xs font-medium text-on-surface",children:t("sys.sensors.mpu.section")}),e.jsx("p",{className:"mt-1 text-[11px] text-on-surface-variant",children:t("sys.sensors.mpu.note")}),e.jsxs("div",{className:"mt-4 flex flex-wrap items-end gap-4",children:[e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[t("sys.sensors.mag.bus"),e.jsx("input",{type:"number",min:0,max:32,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:S,onChange:x=>E(Number(x.target.value))})]}),e.jsxs("label",{className:"flex flex-col gap-1 text-[11px] text-on-surface-variant",children:[t("sys.sensors.mpu.addr"),e.jsx("input",{type:"number",min:1,max:127,className:"w-24 rounded border border-outline-variant/40 bg-surface-container px-2 py-1 font-mono text-sm text-on-surface",value:h,onChange:x=>T(Number(x.target.value))})]}),e.jsxs("label",{className:"flex items-center gap-2 text-[11px] text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:O,onChange:x=>v(x.target.checked)}),t("sys.sensors.mag.i2cdetect")]})]}),e.jsx(ns,{bus:S,addr:h}),e.jsx("div",{className:"mt-4 flex flex-wrap gap-2",children:e.jsx("button",{type:"button",disabled:_,className:"rounded-lg bg-primary px-4 py-2 text-sm font-medium text-on-primary hover:opacity-90 disabled:opacity-50",onClick:()=>void ae(),children:t(_?"sys.sensors.mpu.running":"sys.sensors.mpu.btnSelftest")})}),P&&e.jsx("p",{className:"mt-3 rounded border border-red-500/40 bg-red-500/10 px-3 py-2 font-mono text-xs text-red-200",children:P}),i&&e.jsxs("details",{className:"mt-4 rounded-lg border border-outline-variant/30 bg-surface-container/50",children:[e.jsx("summary",{className:"cursor-pointer px-3 py-2 text-[11px] text-on-surface-variant",children:t("sys.sensors.jsonToggle")}),e.jsx("pre",{className:"max-h-[320px] overflow-auto border-t border-outline-variant/20 p-3 font-mono text-[11px] leading-relaxed text-on-surface",children:Ne(i)})]})]})]})}const ms={ogscope:{zh:"主配置 ogscope.env",en:"Primary ogscope.env"},network:{zh:"网络 network.env",en:"Network network.env"}};function ps(t){const o=[];let n=0;return t.split(/\r?\n/).forEach((c,d)=>{const j=c.trim();if(!j||j.startsWith("#"))return;const u=j.startsWith("export ")?j.slice(7):j,f=u.indexOf("=");if(f<=0){n+=1;return}const g=u.slice(0,f).trim();if(!g){n+=1;return}const b=u.slice(f+1);o.push({id:`env-${d}-${g}`,key:g,value:b})}),{entries:o,unsupportedLines:n}}function we(t){const o=t.map(n=>({key:n.key.trim(),value:n.value})).filter(n=>n.key.length>0).map(n=>`${n.key}=${n.value}`);return o.length>0?`${o.join(` +`)} +`:""}function oe(t,o){return t==="both"?!0:t===o}function hs(){const{locale:t}=Z(),[o,n]=a.useState([]),[r,c]=a.useState(null),[d,j]=a.useState(""),[u,f]=a.useState(""),[g,b]=a.useState("form"),[k,y]=a.useState([]),[m,$]=a.useState(0),[M,R]=a.useState(!1),[S,E]=a.useState(""),[h,T]=a.useState(""),[O,v]=a.useState(""),[_,N]=a.useState(!0),[P,z]=a.useState(""),i=t==="zh",w=a.useMemo(()=>o.find(s=>s.file_id===d)??null,[d,o]),L=a.useMemo(()=>{const s=new Map;for(const l of(r==null?void 0:r.sections)??[])for(const p of l.entries)s.set(p.key.toUpperCase(),p);for(const l of(r==null?void 0:r.network_only)??[])s.set(l.key.toUpperCase(),l);return s},[r]),A=a.useMemo(()=>{if(!d)return[];const s=new Set(k.map(p=>p.key.trim().toUpperCase()).filter(Boolean)),l=[];for(const p of(r==null?void 0:r.sections)??[])for(const C of p.entries)oe(C.scope,d)&&(s.has(C.key.toUpperCase())||l.push(C));for(const p of(r==null?void 0:r.network_only)??[])oe(p.scope,d)&&(s.has(p.key.toUpperCase())||l.push(p));return l.sort((p,C)=>p.key.localeCompare(C.key))},[d,r,k]),I=a.useMemo(()=>{const s=O.trim().toLowerCase();return s?k.filter(l=>{const p=l.key.toLowerCase(),C=L.get(l.key.trim().toUpperCase());return`${p} ${l.value} ${(C==null?void 0:C.zh)??""} ${(C==null?void 0:C.en)??""}`.toLowerCase().includes(s)}):k},[k,L,O]),U=s=>{const l=ps(s);y(l.entries),$(l.unsupportedLines)},K=async()=>{var s,l;R(!0),E("");try{const[p,C]=await Promise.all([W("/api/dev/system/config/files",{cache:"no-store"}),W("/api/dev/system/config/catalog",{cache:"no-store"})]);n(p.files??[]),c(C);const D=((l=(s=p.files)==null?void 0:s[0])==null?void 0:l.file_id)??"";j(G=>{var J;return G&&((J=p.files)!=null&&J.some(Y=>Y.file_id===G))?G:D})}catch(p){E(p instanceof Error?p.message:String(p))}finally{R(!1)}},Q=async()=>{if(!d)return;if(g==="form"&&m>0){E(i?"当前文件包含无法表单化的行,请切换到「原始文本」模式编辑后再保存。":"This file has lines not supported by form mode. Switch to Raw mode before saving.");return}const s=g==="form"?we(k):u;R(!0),E(""),T("");try{const l=await W("/api/dev/system/config/files",{method:"POST",body:JSON.stringify({file_id:d,content:s})});T(i?`保存成功:${l.message??d};请重启 ogscope 服务使配置生效`:`Saved: ${l.message??d}; restart ogscope to apply changes`),await K()}catch(l){E(l instanceof Error?l.message:String(l))}finally{R(!1)}};a.useEffect(()=>{K()},[]),a.useEffect(()=>{if(!w){f(""),y([]),$(0),z("");return}const s=w.content??"";f(s),U(s),z("")},[w]);const re=()=>{y(s=>[...s,{id:`env-new-${Date.now()}-${s.length}`,key:"",value:""}])},ee=()=>{const s=A.find(l=>l.key===P);s&&(y(l=>[...l,{id:`env-catalog-${Date.now()}-${s.key}`,key:s.key,value:s.default??""}]),z(""))},se=(s,l)=>{y(p=>p.map(C=>C.id===s?{...C,...l}:C))},ae=s=>{y(l=>l.filter(p=>p.id!==s))},x=s=>{const l=L.get(s.trim().toUpperCase());return l?i?l.zh:l.en:i?"暂无释义(可查阅 deploy/*.env.example)":"No hint (see deploy/*.env.example)"},F=s=>{const l=L.get(s.trim().toUpperCase());return l!=null&&l.default?i?`默认:${l.default}`:`Default: ${l.default}`:""},X=s=>{const l=ms[s];return l?i?l.zh:l.en:s},ie=s=>s.writable?s.writable_via_sudo&&!s.writable_direct?i?"可写(经 sudo 助手)":"Writable (via sudo helper)":s.writable_direct?i?"可写(直接)":"Writable (direct)":i?"可写":"Writable":i?"不可写:请运行 sudo ./scripts/ogscope-network-init.sh ensure-config":"Not writable: run sudo ./scripts/ogscope-network-init.sh ensure-config";return e.jsxs("div",{className:"mx-auto max-w-7xl space-y-6",children:[e.jsxs("header",{children:[e.jsxs("div",{className:"flex items-center gap-2 text-[10px] uppercase tracking-[0.14em] text-on-surface-variant",children:[e.jsx("span",{children:"Console"}),e.jsx("span",{children:"/"}),e.jsx("span",{className:"text-primary",children:i?"配置管理":"Config Manager"})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:i?"环境配置管理":"Environment Config Manager"}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:i?"编辑 /etc/ogscope 下的 ogscope.env 与 network.env。保存后请重启 ogscope 服务;配置项说明来自服务端目录 API。":"Edit ogscope.env and network.env under /etc/ogscope. Restart ogscope after saving; hints come from the server catalog API."})]}),S&&e.jsx("div",{className:"rounded-lg border border-error/40 bg-error-container/20 px-3 py-2 text-sm text-on-error-container",children:S}),h&&e.jsx("div",{className:"rounded-lg border border-primary/30 bg-primary/10 px-3 py-2 text-sm",children:h}),e.jsxs("section",{className:"grid grid-cols-12 gap-4",children:[e.jsxs("aside",{className:"col-span-12 space-y-2 rounded-xl border border-outline-variant/20 bg-surface-container p-3 lg:col-span-3",children:[e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("p",{className:"text-xs uppercase tracking-wider text-on-surface-variant",children:i?"配置文件":"Config Files"}),e.jsx("button",{type:"button",onClick:()=>void K(),disabled:M,children:e.jsxs("span",{className:"inline-flex items-center gap-1 text-xs",children:[e.jsx(he,{className:"h-3.5 w-3.5"})," ",i?"刷新":"Refresh"]})})]}),o.map(s=>e.jsxs("button",{type:"button",onClick:()=>j(s.file_id),className:`w-full rounded-lg border px-3 py-2 text-left text-sm ${s.file_id===d?"border-primary bg-primary/10 text-on-surface":"border-outline-variant/30 bg-surface-container-low text-on-surface-variant"}`,children:[e.jsx("div",{className:"font-medium",children:X(s.file_id)}),e.jsx("div",{className:"mt-1 truncate font-mono text-[11px]",children:s.path})]},s.file_id)),(r==null?void 0:r.env_files)&&e.jsxs("div",{className:"mt-3 rounded border border-outline-variant/20 bg-surface-container-low px-2 py-2 text-[11px] text-on-surface-variant",children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:i?"配置路径":"Config paths"}),Object.entries(r.env_files).map(([s,l])=>e.jsxs("p",{className:"font-mono",children:[s,": ",l]},s))]})]}),e.jsxs("div",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 lg:col-span-9",children:[!w&&e.jsx("p",{className:"text-sm text-on-surface-variant",children:i?"暂无可编辑配置文件":"No editable config files."}),w&&e.jsxs("div",{className:"space-y-3",children:[e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("p",{className:"font-mono text-xs text-on-surface",children:w.path}),e.jsxs("p",{className:"text-xs text-on-surface-variant",children:[ie(w)," · ",i?"存在":"Exists",": ",String(w.exists)]})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{type:"button",onClick:()=>{g==="raw"&&U(u),b("form")},className:`rounded px-2 py-1 text-xs ${g==="form"?"bg-primary-container text-on-primary-container":"border border-outline-variant/30 text-on-surface-variant"}`,children:i?"表单模式":"Form"}),e.jsx("button",{type:"button",onClick:()=>{g==="form"&&f(we(k)),b("raw")},className:`rounded px-2 py-1 text-xs ${g==="raw"?"bg-primary-container text-on-primary-container":"border border-outline-variant/30 text-on-surface-variant"}`,children:i?"原始文本":"Raw"})]}),e.jsx("button",{type:"button",onClick:()=>void Q(),disabled:M||!w.writable,children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(Pe,{className:"h-3.5 w-3.5"}),i?"保存并提示重启":"Save"]})})]}),w.error&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-2 py-1 text-xs text-on-error-container",children:w.error}),g==="form"?e.jsxs("div",{className:"space-y-3",children:[m>0&&e.jsx("div",{className:"rounded border border-warning/40 bg-warning/10 px-2 py-1 text-xs text-on-surface",children:i?`检测到 ${m} 行无法转换为键值表单。请切到「原始文本」模式处理。`:`${m} line(s) cannot be represented in key-value form. Use Raw mode.`}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs("div",{className:"relative min-w-[200px] flex-1",children:[e.jsx(_e,{className:"pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-on-surface-variant"}),e.jsx("input",{className:"w-full rounded border border-outline-variant/30 bg-surface-container-low py-1.5 pl-8 pr-2 text-xs outline-none focus:border-primary",placeholder:i?"搜索键名、值或释义…":"Search keys, values, or hints…",value:O,onChange:s=>v(s.target.value)})]}),e.jsxs("select",{className:"max-w-xs flex-1 rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5 text-xs outline-none focus:border-primary",value:P,onChange:s=>z(s.target.value),children:[e.jsx("option",{value:"",children:i?"从目录添加配置项…":"Add from catalog…"}),A.map(s=>e.jsx("option",{value:s.key,children:s.key},s.key))]}),e.jsx("button",{type:"button",disabled:!P,onClick:ee,className:"rounded border border-outline-variant/30 px-2 py-1.5 text-xs disabled:opacity-50",children:i?"添加":"Add"})]}),e.jsxs("div",{className:"max-h-[420px] overflow-auto pr-1",children:[e.jsxs("table",{className:"w-full border-separate border-spacing-y-2",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"text-left text-[11px] uppercase tracking-wide text-on-surface-variant",children:[e.jsx("th",{children:i?"配置项":"Key"}),e.jsx("th",{children:i?"值":"Value"}),e.jsx("th",{children:i?"释义":"Meaning"}),e.jsx("th",{children:i?"操作":"Action"})]})}),e.jsx("tbody",{children:I.map(s=>e.jsxs("tr",{children:[e.jsx("td",{className:"pr-2 align-top",children:e.jsx("input",{className:"w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1 font-mono text-xs outline-none focus:border-primary",placeholder:"OGSCOPE_PORT",value:s.key,onChange:l=>se(s.id,{key:l.target.value})})}),e.jsx("td",{className:"pr-2 align-top",children:e.jsx("input",{className:"w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1 font-mono text-xs outline-none focus:border-primary",placeholder:i?"变量值":"Value",value:s.value,onChange:l=>se(s.id,{value:l.target.value})})}),e.jsxs("td",{className:"pr-2 align-top text-xs text-on-surface-variant",children:[e.jsx("p",{children:x(s.key)}),F(s.key)&&e.jsx("p",{className:"mt-0.5 font-mono text-[10px] text-on-surface-variant/80",children:F(s.key)})]}),e.jsx("td",{className:"align-top",children:e.jsx("button",{type:"button",onClick:()=>ae(s.id),children:e.jsx(Re,{className:"h-3.5 w-3.5 text-on-surface-variant"})})})]},s.id))})]}),k.length===0&&e.jsx("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-2 text-xs text-on-surface-variant",children:i?"当前没有可编辑变量,可从目录添加或手动新增。":"No variables yet. Add from catalog or manually."}),k.length>0&&I.length===0&&e.jsx("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-2 text-xs text-on-surface-variant",children:i?"无匹配项,请调整搜索条件。":"No matches for the current search."})]}),e.jsx("div",{className:"flex flex-wrap gap-2",children:e.jsx("button",{type:"button",onClick:re,children:e.jsxs("span",{className:"inline-flex items-center gap-1 text-xs",children:[e.jsx(De,{className:"h-3.5 w-3.5"}),i?"空白行":"Blank row"]})})}),e.jsxs("details",{open:_,onToggle:s=>N(s.target.open),className:"rounded border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-xs text-on-surface-variant",children:[e.jsx("summary",{className:"cursor-pointer font-medium text-on-surface",children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(Ae,{className:"h-3.5 w-3.5"}),i?"配置目录(按模块)":"Config catalog (by section)"]})}),e.jsxs("div",{className:"mt-3 space-y-4",children:[((r==null?void 0:r.sections)??[]).map(s=>{const l=s.entries.filter(p=>d?oe(p.scope,d):!0);return l.length===0?null:e.jsxs("div",{children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:i?s.title_zh:s.title_en}),e.jsx("div",{className:"space-y-1",children:l.map(p=>e.jsxs("p",{children:[e.jsx("span",{className:"font-mono text-[11px] text-on-surface",children:p.key}),p.default!=null&&p.default!==""&&e.jsxs("span",{className:"ml-1 font-mono text-[10px] text-on-surface-variant/80",children:["(= ",p.default,")"]})," — ",e.jsx("span",{children:i?p.zh:p.en})]},p.key))})]},s.id)}),((r==null?void 0:r.network_only)??[]).filter(s=>d?oe(s.scope,d):!0).length>0&&e.jsxs("div",{children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:i?"仅 network.env / 脚本":"network.env / scripts only"}),e.jsx("div",{className:"space-y-1",children:((r==null?void 0:r.network_only)??[]).filter(s=>d?oe(s.scope,d):!0).map(s=>e.jsxs("p",{children:[e.jsx("span",{className:"font-mono text-[11px] text-on-surface",children:s.key})," — ",e.jsx("span",{children:i?s.zh:s.en})]},s.key))})]})]})]})]}):e.jsx("textarea",{className:"h-[460px] w-full rounded-lg border border-outline-variant/30 bg-neutral-950 p-3 font-mono text-xs text-on-surface outline-none focus:border-primary",spellCheck:!1,value:u,onChange:s=>f(s.target.value)})]})]})]})]})}const fs=new Set(["overview","network","sensors","hmi","power","config"]);function ke(){const t=window.location.hash.replace(/^#\/?/,"").trim().toLowerCase();return fs.has(t)?t:"overview"}function bs(t){window.location.hash=`/${t}`}function gs(){const[t,o]=a.useState(()=>ke()),[n,r]=a.useState(!0);a.useEffect(()=>{const d=()=>o(ke());return window.addEventListener("hashchange",d),()=>window.removeEventListener("hashchange",d)},[]),a.useEffect(()=>{(async()=>{var d;try{const j=await fetch("/api",{cache:"no-store"});if(!j.ok)return;const u=await j.json();r(!!((d=u.endpoints)!=null&&d.network))}catch{}})()},[]);const c=a.useMemo(()=>t==="network"?n?e.jsx(Qe,{}):e.jsx(ve,{scope:"network"}):t==="sensors"?e.jsx(us,{}):t==="hmi"?e.jsx(ts,{}):t==="config"?e.jsx(hs,{}):t==="power"?e.jsx(ve,{scope:"power"}):e.jsx(Ye,{}),[n,t]);return e.jsx(We,{route:t,allowNetworkRoute:n,onRouteChange:d=>{d==="network"&&!n||d!==t&&bs(d)},children:c})}$e.createRoot(document.getElementById("root")).render(e.jsx(Me.StrictMode,{children:e.jsx(Te,{children:e.jsx(Le,{children:e.jsx(gs,{})})})})); diff --git a/web/static/analysis-lab/system.html b/web/static/analysis-lab/system.html index d4bf4a2..93c894d 100644 --- a/web/static/analysis-lab/system.html +++ b/web/static/analysis-lab/system.html @@ -4,7 +4,7 @@ OGScope 系统调试控制台 - + From 186e90324c9351a4cf30a940ad892fb6200d3fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E6=98=AF=E5=B0=8F=E4=B8=80=E7=81=B0?= Date: Tue, 26 May 2026 12:20:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20CI=20=E7=9A=84?= =?UTF-8?q?=20Ruff/Black=20=E4=B8=8E=20streaming=20=E5=8D=95=E6=B5=8B=20/?= =?UTF-8?q?=20Fix=20CI=20Ruff,=20Black,=20and=20streaming=20unit=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 全仓 black 格式化与 ruff import/风格修复 / Run black and ruff across ogscope and tests - 补全 MJPEG streaming 测试 FakeSettings.shared_preview_fps / Add shared_preview_fps to fake settings Co-authored-by: Cursor --- .../plate_solve/centroid_quality.py | 12 +++-- ogscope/algorithms/plate_solve/solver.py | 4 +- ogscope/config_catalog.py | 4 +- ogscope/core/application/core_service.py | 46 ++++++++++------ ogscope/domain/__init__.py | 1 - ogscope/domain/analysis/__init__.py | 1 - ogscope/domain/analysis/services.py | 5 +- ogscope/domain/camera/__init__.py | 1 - ogscope/domain/camera/services.py | 41 +++++++++----- ogscope/domain/camera/sidecar.py | 1 - ogscope/domain/camera/stream_limiter.py | 1 - ogscope/domain/camera/streaming.py | 1 - ogscope/domain/network/__init__.py | 1 - ogscope/domain/network/nmcli_services.py | 1 - ogscope/domain/network/services.py | 19 ++++--- ogscope/domain/shared/filesystem.py | 1 - ogscope/domain/system/__init__.py | 1 - ogscope/domain/system/services.py | 5 +- ogscope/platform/adapters/debug_services.py | 1 - ogscope/platform/hardware/ak09911_i2c.py | 12 +++-- ogscope/platform/hardware/camera.py | 4 +- ogscope/platform/hardware/st7796_spi.py | 40 +++++++++++++- ogscope/platform/hardware_plane/__init__.py | 1 - ogscope/platform/hardware_plane/client.py | 5 +- ogscope/platform/hardware_plane/daemon.py | 5 +- ogscope/platform/hardware_plane/registry.py | 3 +- ogscope/platform/hardware_plane/runtime.py | 5 +- .../hardware_plane/services/__init__.py | 1 - .../platform/hardware_plane/services/base.py | 5 +- .../hardware_plane/services/camera_service.py | 1 - .../platform/hardware_plane/services/hmi.py | 8 ++- .../hardware_plane/services/sensor_hub.py | 1 - .../hardware_plane/transport/__init__.py | 1 - .../hardware_plane/transport/jsonrpc_uds.py | 11 ++-- ogscope/web/api/analysis/routes.py | 6 ++- ogscope/web/api/analysis/services.py | 4 +- ogscope/web/api/core/routes.py | 8 ++- ogscope/web/api/debug/magnetometer_service.py | 54 ++++++++++++------- ogscope/web/api/system/routes.py | 5 +- ogscope/web/app.py | 17 ++++-- ogscope/web/mjpeg_stream_limiter.py | 5 +- tests/conftest.py | 6 ++- tests/unit/test_analysis_api.py | 4 +- tests/unit/test_config_catalog.py | 4 +- tests/unit/test_config_files.py | 7 ++- tests/unit/test_core_contract_api.py | 8 ++- tests/unit/test_debug_camera_api.py | 4 +- tests/unit/test_dev_contract_api.py | 1 - tests/unit/test_domain_camera_sidecar.py | 1 - tests/unit/test_domain_camera_streaming.py | 10 ++-- tests/unit/test_domain_shared_filesystem.py | 1 - tests/unit/test_hardware_plane.py | 9 ++-- tests/unit/test_system_wifi_parse.py | 1 - tests/unit/test_wifi_switch.py | 17 ++++-- 54 files changed, 280 insertions(+), 142 deletions(-) diff --git a/ogscope/algorithms/plate_solve/centroid_quality.py b/ogscope/algorithms/plate_solve/centroid_quality.py index f2c0b88..b6fd5b6 100644 --- a/ogscope/algorithms/plate_solve/centroid_quality.py +++ b/ogscope/algorithms/plate_solve/centroid_quality.py @@ -67,7 +67,9 @@ def _reject_dense_clusters( return kept, removed, removed_pts -def _point_line_dist(points_yx: np.ndarray, p0: np.ndarray, p1: np.ndarray) -> np.ndarray: +def _point_line_dist( + points_yx: np.ndarray, p0: np.ndarray, p1: np.ndarray +) -> np.ndarray: """点到线段距离(像素)/ Distance from points to segment.""" # segment vector vx = p1[1] - p0[1] @@ -221,9 +223,11 @@ def filter_centroids_yx( ) n1 = int(xy3.shape[0]) - rejected_pts = np.concatenate( - [dense_removed, line_removed], axis=0 - ) if (dense_removed.size > 0 or line_removed.size > 0) else np.empty((0, 2), dtype=np.float64) + rejected_pts = ( + np.concatenate([dense_removed, line_removed], axis=0) + if (dense_removed.size > 0 or line_removed.size > 0) + else np.empty((0, 2), dtype=np.float64) + ) quality: dict[str, Any] = { "level": lv, "flags": flags, diff --git a/ogscope/algorithms/plate_solve/solver.py b/ogscope/algorithms/plate_solve/solver.py index 1ea366b..a878276 100644 --- a/ogscope/algorithms/plate_solve/solver.py +++ b/ogscope/algorithms/plate_solve/solver.py @@ -344,9 +344,7 @@ def solve( sorted_stars = sorted(stars, key=lambda s: s.flux, reverse=True) cyx = np.array([[s.y, s.x] for s in sorted_stars], dtype=np.float64) overlay = ( - _make_solve_overlay( - {}, cyx, None, (height, width), (height, width) - ) + _make_solve_overlay({}, cyx, None, (height, width), (height, width)) if len(cyx) > 0 else None ) diff --git a/ogscope/config_catalog.py b/ogscope/config_catalog.py index c0d2836..70a2aa9 100644 --- a/ogscope/config_catalog.py +++ b/ogscope/config_catalog.py @@ -10,7 +10,9 @@ ConfigFileScope = Literal["ogscope", "network", "both"] -_CATALOG_SECTIONS: tuple[tuple[str, str, str, ConfigFileScope, tuple[str, ...]], ...] = ( +_CATALOG_SECTIONS: tuple[ + tuple[str, str, str, ConfigFileScope, tuple[str, ...]], ... +] = ( ( "basic", "基础", diff --git a/ogscope/core/application/core_service.py b/ogscope/core/application/core_service.py index 7fe36f7..50a0715 100644 --- a/ogscope/core/application/core_service.py +++ b/ogscope/core/application/core_service.py @@ -17,8 +17,8 @@ stream_state_domain_service, ) from ogscope.domain.system.services import system_info_service -from ogscope.platform.hardware_plane.runtime import get_hardware_plane_client from ogscope.platform.hardware.wifi_switch import wifi_switch_service +from ogscope.platform.hardware_plane.runtime import get_hardware_plane_client @dataclass(slots=True) @@ -106,7 +106,9 @@ async def get_system_status(self) -> dict[str, Any]: wifi_raw = wifi_switch_service.get_status() hardware_client = get_hardware_plane_client() hw_status_resp = await hardware_client.status_get() - hw_status_data = hw_status_resp.get("data", {}) if hw_status_resp.get("success") else {} + hw_status_data = ( + hw_status_resp.get("data", {}) if hw_status_resp.get("success") else {} + ) camera_service_status = ( hw_status_data.get("services", {}).get("camera", {}) if isinstance(hw_status_data, dict) @@ -150,15 +152,21 @@ async def get_system_status(self) -> dict[str, Any]: "version": __version__, "capabilities": capability_map(), "hardware_plane": { - "started": bool(hw_status_data.get("started", False)) - if isinstance(hw_status_data, dict) - else False, - "metrics": hw_status_data.get("metrics", {}) - if isinstance(hw_status_data, dict) - else {}, - "services": hw_status_data.get("services", {}) - if isinstance(hw_status_data, dict) - else {}, + "started": ( + bool(hw_status_data.get("started", False)) + if isinstance(hw_status_data, dict) + else False + ), + "metrics": ( + hw_status_data.get("metrics", {}) + if isinstance(hw_status_data, dict) + else {} + ), + "services": ( + hw_status_data.get("services", {}) + if isinstance(hw_status_data, dict) + else {} + ), }, "system": system, "camera": {"success": True, **camera_status}, @@ -178,9 +186,15 @@ async def get_camera_status(self) -> dict[str, Any]: status = await camera_domain_service.get_status() normalized = self._normalize_camera_status(status) if hp_camera: - normalized["connected"] = bool(hp_camera.get("connected", normalized["connected"])) - normalized["streaming"] = bool(hp_camera.get("streaming", normalized["streaming"])) - normalized["recording"] = bool(hp_camera.get("recording", normalized["recording"])) + normalized["connected"] = bool( + hp_camera.get("connected", normalized["connected"]) + ) + normalized["streaming"] = bool( + hp_camera.get("streaming", normalized["streaming"]) + ) + normalized["recording"] = bool( + hp_camera.get("recording", normalized["recording"]) + ) return {"success": True, **normalized} async def start_camera(self) -> dict[str, Any]: @@ -222,7 +236,9 @@ async def tune_camera(self, payload: dict[str, Any]) -> dict[str, Any]: applied["auto_exposure"] = bool(auto_exposure) if payload.get("exposure_us") is not None: - await camera_domain_service.update_settings({"exposure": payload["exposure_us"]}) + await camera_domain_service.update_settings( + {"exposure": payload["exposure_us"]} + ) applied["exposure_us"] = int(payload["exposure_us"]) if payload.get("analogue_gain") is not None: diff --git a/ogscope/domain/__init__.py b/ogscope/domain/__init__.py index 58bf2dc..dcf5dab 100644 --- a/ogscope/domain/__init__.py +++ b/ogscope/domain/__init__.py @@ -1,4 +1,3 @@ """ 领域层聚合导出 / Domain layer package exports. """ - diff --git a/ogscope/domain/analysis/__init__.py b/ogscope/domain/analysis/__init__.py index 22d9b98..b83e152 100644 --- a/ogscope/domain/analysis/__init__.py +++ b/ogscope/domain/analysis/__init__.py @@ -1,4 +1,3 @@ from ogscope.domain.analysis.services import analysis_domain_service __all__ = ["analysis_domain_service"] - diff --git a/ogscope/domain/analysis/services.py b/ogscope/domain/analysis/services.py index 5579329..11ab5eb 100644 --- a/ogscope/domain/analysis/services.py +++ b/ogscope/domain/analysis/services.py @@ -29,7 +29,9 @@ def resolve_upload_file_response(path: Path) -> tuple[Path, str]: return path, media or "application/octet-stream" @staticmethod - def parse_frame_upload_payload(payload: str) -> tuple[dict[str, Any], dict[str, Any]]: + def parse_frame_upload_payload( + payload: str, + ) -> tuple[dict[str, Any], dict[str, Any]]: obj = json.loads(payload) if not isinstance(obj, dict): raise ValueError("payload 必须为 JSON 对象 / payload must be a JSON object") @@ -51,4 +53,3 @@ def parse_frame_upload_payload(payload: str) -> tuple[dict[str, Any], dict[str, analysis_domain_service = AnalysisDomainService() __all__ = ["analysis_domain_service", "AnalysisDomainService"] - diff --git a/ogscope/domain/camera/__init__.py b/ogscope/domain/camera/__init__.py index 0c2d1e4..99dca18 100644 --- a/ogscope/domain/camera/__init__.py +++ b/ogscope/domain/camera/__init__.py @@ -5,4 +5,3 @@ ) __all__ = ["DebugCameraService", "DebugFileService", "DebugPresetService"] - diff --git a/ogscope/domain/camera/services.py b/ogscope/domain/camera/services.py index 2c0e29a..8e1261d 100644 --- a/ogscope/domain/camera/services.py +++ b/ogscope/domain/camera/services.py @@ -8,13 +8,12 @@ import time from typing import Any -from fastapi import HTTPException from fastapi.responses import Response from starlette.requests import Request -from ogscope.platform.adapters.debug_services import get_debug_services_module from ogscope.config import get_settings from ogscope.domain.camera.stream_limiter import get_mjpeg_stream_limiter +from ogscope.platform.adapters.debug_services import get_debug_services_module logger = logging.getLogger(__name__) @@ -173,11 +172,15 @@ async def get_runtime_overrides(): @staticmethod async def clear_runtime_overrides(): - return await _debug_services_module().DebugCameraService.clear_runtime_overrides() + return ( + await _debug_services_module().DebugCameraService.clear_runtime_overrides() + ) @staticmethod async def apply_runtime_overrides_as_defaults(): - return await _debug_services_module().DebugCameraService.apply_runtime_overrides_as_defaults() + return ( + await _debug_services_module().DebugCameraService.apply_runtime_overrides_as_defaults() + ) @staticmethod async def start_camera(): @@ -229,7 +232,9 @@ async def set_fps(fps: int): @staticmethod async def update_settings(settings: dict[str, Any]): - return await _debug_services_module().DebugCameraService.update_settings(settings) + return await _debug_services_module().DebugCameraService.update_settings( + settings + ) @staticmethod async def set_auto_exposure_mode(enabled: bool): @@ -251,19 +256,27 @@ async def get_image_quality(): @staticmethod async def apply_night_mode_preset(): - return await _debug_services_module().DebugCameraService.apply_night_mode_preset() + return ( + await _debug_services_module().DebugCameraService.apply_night_mode_preset() + ) @staticmethod async def save_current_settings_backup(): - return await _debug_services_module().DebugCameraService.save_current_settings_backup() + return ( + await _debug_services_module().DebugCameraService.save_current_settings_backup() + ) @staticmethod async def restore_settings_backup(): - return await _debug_services_module().DebugCameraService.restore_settings_backup() + return ( + await _debug_services_module().DebugCameraService.restore_settings_backup() + ) @staticmethod async def set_color_mode(color_mode: str): - return await _debug_services_module().DebugCameraService.set_color_mode(color_mode) + return await _debug_services_module().DebugCameraService.set_color_mode( + color_mode + ) @staticmethod async def set_white_balance(mode: str, gain_r: float, gain_b: float): @@ -307,11 +320,16 @@ async def save_preset(payload: dict[str, Any]): @staticmethod async def apply_preset(preset_name: str): - return await _debug_services_module().DebugPresetService.apply_preset(preset_name) + return await _debug_services_module().DebugPresetService.apply_preset( + preset_name + ) @staticmethod async def delete_preset(preset_name: str): - return await _debug_services_module().DebugPresetService.delete_preset(preset_name) + return await _debug_services_module().DebugPresetService.delete_preset( + preset_name + ) + __all__ = [ "DebugCameraService", @@ -323,4 +341,3 @@ async def delete_preset(preset_name: str): "file_domain_service", "stream_state_domain_service", ] - diff --git a/ogscope/domain/camera/sidecar.py b/ogscope/domain/camera/sidecar.py index 4462386..fadd41c 100644 --- a/ogscope/domain/camera/sidecar.py +++ b/ogscope/domain/camera/sidecar.py @@ -41,4 +41,3 @@ def merge_capture_sidecar_into_info( if key not in capture_info: capture_info[key] = value info.update(capture_info) - diff --git a/ogscope/domain/camera/stream_limiter.py b/ogscope/domain/camera/stream_limiter.py index 740cab4..a952f48 100644 --- a/ogscope/domain/camera/stream_limiter.py +++ b/ogscope/domain/camera/stream_limiter.py @@ -52,4 +52,3 @@ def get_mjpeg_stream_limiter() -> MjpegStreamLimiter: if _limiter is None: _limiter = MjpegStreamLimiter(get_settings().stream_max_mjpeg_clients) return _limiter - diff --git a/ogscope/domain/camera/streaming.py b/ogscope/domain/camera/streaming.py index a2462f3..b8665a5 100644 --- a/ogscope/domain/camera/streaming.py +++ b/ogscope/domain/camera/streaming.py @@ -95,4 +95,3 @@ async def frame_generator(): frame_generator(), media_type=f"multipart/x-mixed-replace; boundary={boundary}", ) - diff --git a/ogscope/domain/network/__init__.py b/ogscope/domain/network/__init__.py index a3d1b76..87bcde1 100644 --- a/ogscope/domain/network/__init__.py +++ b/ogscope/domain/network/__init__.py @@ -1,4 +1,3 @@ from ogscope.domain.network.services import wifi_domain_service __all__ = ["wifi_domain_service"] - diff --git a/ogscope/domain/network/nmcli_services.py b/ogscope/domain/network/nmcli_services.py index f9d54bb..c1f1ace 100644 --- a/ogscope/domain/network/nmcli_services.py +++ b/ogscope/domain/network/nmcli_services.py @@ -403,4 +403,3 @@ async def _sta_rollback_loop() -> None: raise except Exception as e: logger.error("STA 回滚失败 / Rollback to AP failed: {}", e) - diff --git a/ogscope/domain/network/services.py b/ogscope/domain/network/services.py index d05e84d..1218ab9 100644 --- a/ogscope/domain/network/services.py +++ b/ogscope/domain/network/services.py @@ -8,8 +8,8 @@ import subprocess from ogscope.config import get_settings -from ogscope.platform.hardware.wifi_switch import wifi_switch_service from ogscope.domain.network import nmcli_services as net_impl +from ogscope.platform.hardware.wifi_switch import wifi_switch_service class WifiDomainService: @@ -27,7 +27,9 @@ def build_wifi_status() -> dict: ap_connection = data.get("AP_CONNECTION", settings.wifi_ap_connection) ap_ipv4 = data.get("AP_IPV4") or None ap_url_hint = ( - f"http://{settings.wifi_ap_url_host}:{settings.port}" if mode == "ap" else None + f"http://{settings.wifi_ap_url_host}:{settings.port}" + if mode == "ap" + else None ) message = data.get("error") suffix = settings.device_id_suffix or None @@ -58,7 +60,9 @@ async def switch_mode(self, mode: str) -> dict: async def scan_wifi(self): settings = get_settings() - return await asyncio.to_thread(net_impl.nmcli_wifi_scan, settings.wifi_interface) + return await asyncio.to_thread( + net_impl.nmcli_wifi_scan, settings.wifi_interface + ) async def list_profiles(self): settings = get_settings() @@ -68,7 +72,9 @@ async def connect_sta(self, ssid: str, password: str) -> dict: settings = get_settings() if not wifi_switch_service.is_configured(): raise RuntimeError("wifi_not_configured") - await asyncio.to_thread(net_impl.nmcli_modify_sta_to_ssid, settings, ssid, password) + await asyncio.to_thread( + net_impl.nmcli_modify_sta_to_ssid, settings, ssid, password + ) await asyncio.to_thread(wifi_switch_service.switch, "sta") net_impl.schedule_sta_rollback_watch() return self.build_wifi_status() @@ -83,7 +89,9 @@ async def activate_profile(self, connection_name: str) -> dict: if name == settings.wifi_sta_connection: await asyncio.to_thread(wifi_switch_service.switch, "sta") else: - await asyncio.to_thread(net_impl.nm_down_if_exists, settings.wifi_ap_connection) + await asyncio.to_thread( + net_impl.nm_down_if_exists, settings.wifi_ap_connection + ) await asyncio.to_thread(net_impl.nmcli_activate_connection, settings, name) net_impl.schedule_sta_rollback_watch() return self.build_wifi_status() @@ -115,4 +123,3 @@ async def activate_profile(self, connection_name: str) -> dict: "TimeoutExpired", "CalledProcessError", ] - diff --git a/ogscope/domain/shared/filesystem.py b/ogscope/domain/shared/filesystem.py index c5fff0b..7e2ae19 100644 --- a/ogscope/domain/shared/filesystem.py +++ b/ogscope/domain/shared/filesystem.py @@ -39,4 +39,3 @@ def ensure_safe_basename(filename: str) -> str: if "/" in safe_name or "\\" in safe_name: raise ValueError("invalid filename") return safe_name - diff --git a/ogscope/domain/system/__init__.py b/ogscope/domain/system/__init__.py index 3925486..77c8228 100644 --- a/ogscope/domain/system/__init__.py +++ b/ogscope/domain/system/__init__.py @@ -1,4 +1,3 @@ from ogscope.domain.system.services import system_info_service __all__ = ["system_info_service"] - diff --git a/ogscope/domain/system/services.py b/ogscope/domain/system/services.py index 7e5cad1..de9a238 100644 --- a/ogscope/domain/system/services.py +++ b/ogscope/domain/system/services.py @@ -225,7 +225,9 @@ def read_systemd_logs( rt = item.get("__REALTIME_TIMESTAMP") try: if rt is not None: - ts = dt.datetime.fromtimestamp(int(str(rt)) / 1_000_000, tz=dt.timezone.utc) + ts = dt.datetime.fromtimestamp( + int(str(rt)) / 1_000_000, tz=dt.timezone.utc + ) ts_iso = ts.isoformat() except (ValueError, TypeError): ts_iso = None @@ -256,4 +258,3 @@ def _journal_priority_to_level(priority: str | int | None) -> str: system_info_service = SystemInfoService() __all__ = ["SystemInfoService", "system_info_service", "read_systemd_logs"] - diff --git a/ogscope/platform/adapters/debug_services.py b/ogscope/platform/adapters/debug_services.py index 10fa692..5b37dd6 100644 --- a/ogscope/platform/adapters/debug_services.py +++ b/ogscope/platform/adapters/debug_services.py @@ -10,4 +10,3 @@ def get_debug_services_module(): """延迟加载调试实现模块 / Lazy load debug implementation module.""" return importlib.import_module("ogscope.web.api.debug.services") - diff --git a/ogscope/platform/hardware/ak09911_i2c.py b/ogscope/platform/hardware/ak09911_i2c.py index 040196d..27fbfb7 100644 --- a/ogscope/platform/hardware/ak09911_i2c.py +++ b/ogscope/platform/hardware/ak09911_i2c.py @@ -204,7 +204,9 @@ def _measure_body_smbus(smbus: Any, addr7: int) -> Ak09911Measurement: time.sleep(0.006 * (read_try + 1)) if data is None: if last_io is not None: - raise RuntimeError(_err_ctx("hxl_read", last_io, addr7).to_text()) from last_io + raise RuntimeError( + _err_ctx("hxl_read", last_io, addr7).to_text() + ) from last_io raise RuntimeError("hxl_read unknown error") try: _ = smbus.read_byte_data(addr7, REG_ST2) @@ -215,7 +217,9 @@ def _measure_body_smbus(smbus: Any, addr7: int) -> Ak09911Measurement: try: _ = smbus.read_byte_data(addr7, REG_ST2) except OSError as exc2: - raise RuntimeError(_err_ctx("st2_read", exc2, addr7).to_text()) from exc2 + raise RuntimeError( + _err_ctx("st2_read", exc2, addr7).to_text() + ) from exc2 else: raise RuntimeError(_err_ctx("st2_read", exc, addr7).to_text()) from exc hx, hy, hz = _combine_hxl_6(data) @@ -303,7 +307,9 @@ def measure_heading_with_cad_fallback( ) -def measure_single(bus: int, addr7: int) -> tuple[Ak09911Measurement | None, str | None]: +def measure_single( + bus: int, addr7: int +) -> tuple[Ak09911Measurement | None, str | None]: path = ensure_i2c_dev_node(bus) if path is None: return None, f"missing {i2c_dev_path(bus)}" diff --git a/ogscope/platform/hardware/camera.py b/ogscope/platform/hardware/camera.py index 737662e..9d11270 100644 --- a/ogscope/platform/hardware/camera.py +++ b/ogscope/platform/hardware/camera.py @@ -771,9 +771,7 @@ def set_flip(self, flip_horizontal: bool, flip_vertical: bool) -> bool: return False self.flip_horizontal = bool(flip_horizontal) self.flip_vertical = bool(flip_vertical) - logger.info( - f"图像镜像: 水平={self.flip_horizontal}, 垂直={self.flip_vertical}" - ) + logger.info(f"图像镜像: 水平={self.flip_horizontal}, 垂直={self.flip_vertical}") return True def set_sampling_mode(self, mode: str) -> bool: diff --git a/ogscope/platform/hardware/st7796_spi.py b/ogscope/platform/hardware/st7796_spi.py index 59f4394..ec0ee59 100644 --- a/ogscope/platform/hardware/st7796_spi.py +++ b/ogscope/platform/hardware/st7796_spi.py @@ -102,8 +102,44 @@ def _init_sequence(self) -> None: d(0xC2, [0xA7]) d(0xC5, [0x18]) time.sleep(0.12) - d(0xE0, [0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B]) - d(0xE1, [0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B]) + d( + 0xE0, + [ + 0xF0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x15, + 0x2F, + 0x54, + 0x42, + 0x3C, + 0x17, + 0x14, + 0x18, + 0x1B, + ], + ) + d( + 0xE1, + [ + 0xE0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x03, + 0x2B, + 0x43, + 0x42, + 0x3B, + 0x16, + 0x14, + 0x17, + 0x1B, + ], + ) time.sleep(0.12) d(0xF0, [0x3C]) d(0xF0, [0x69]) diff --git a/ogscope/platform/hardware_plane/__init__.py b/ogscope/platform/hardware_plane/__init__.py index b2f9f9b..e96ef1a 100644 --- a/ogscope/platform/hardware_plane/__init__.py +++ b/ogscope/platform/hardware_plane/__init__.py @@ -21,4 +21,3 @@ "start_hardware_plane", "stop_hardware_plane", ] - diff --git a/ogscope/platform/hardware_plane/client.py b/ogscope/platform/hardware_plane/client.py index bd05246..cdd18e2 100644 --- a/ogscope/platform/hardware_plane/client.py +++ b/ogscope/platform/hardware_plane/client.py @@ -43,7 +43,9 @@ def __init__( self._daemon = daemon self._default_timeout_ms = max(50, int(default_timeout_ms)) self._remote_sensor_transport = remote_sensor_transport - self._remote_sensor_enabled = bool(remote_sensor_enabled and remote_sensor_transport) + self._remote_sensor_enabled = bool( + remote_sensor_enabled and remote_sensor_transport + ) self._runtime_profile = dict(runtime_profile or {}) async def _call( @@ -108,4 +110,3 @@ async def event_subscribe(self, topic: str) -> dict[str, Any]: def runtime_profile(self) -> dict[str, Any]: """运行时角色信息 / Runtime role profile.""" return dict(self._runtime_profile) - diff --git a/ogscope/platform/hardware_plane/daemon.py b/ogscope/platform/hardware_plane/daemon.py index 6fa2c0c..3dbabb4 100644 --- a/ogscope/platform/hardware_plane/daemon.py +++ b/ogscope/platform/hardware_plane/daemon.py @@ -195,9 +195,7 @@ async def handle_call( code=PlaneErrorCode.UNAVAILABLE, message="local sensor service is disabled; use delegated sensor backend", ) - return ok_payload( - {"sensor": await sensor_hub.read(sensor_name)} - ) + return ok_payload({"sensor": await sensor_hub.read(sensor_name)}) if method == PlaneMethod.DEVICE_COMMAND.value: target = str(params.get("target", "")) action = str(params.get("action", "")) @@ -257,4 +255,3 @@ async def status(self) -> dict[str, Any]: def metrics(self) -> dict[str, Any]: return self._metrics.to_dict() - diff --git a/ogscope/platform/hardware_plane/registry.py b/ogscope/platform/hardware_plane/registry.py index e8a0eb0..124f7dc 100644 --- a/ogscope/platform/hardware_plane/registry.py +++ b/ogscope/platform/hardware_plane/registry.py @@ -55,9 +55,8 @@ def update_state(self, name: str, state: CapabilityState) -> None: def list_records(self) -> list[CapabilityRecord]: """列出所有能力 / List all capabilities.""" with self._lock: - return [record for record in self._records.values()] + return list(self._records.values()) def as_dict_list(self) -> list[dict[str, Any]]: """字典列表表示 / Dict-list representation.""" return [record.to_dict() for record in self.list_records()] - diff --git a/ogscope/platform/hardware_plane/runtime.py b/ogscope/platform/hardware_plane/runtime.py index 0c4ecfd..72ff19c 100644 --- a/ogscope/platform/hardware_plane/runtime.py +++ b/ogscope/platform/hardware_plane/runtime.py @@ -73,7 +73,9 @@ def _ensure_runtime(settings: Settings) -> None: ) remote_sensor_transport = None if profile["subordinate_mode"]: - remote_sensor_transport = JsonRpcUdsClient(str(settings.hardware_plane_remote_uds_socket)) + remote_sensor_transport = JsonRpcUdsClient( + str(settings.hardware_plane_remote_uds_socket) + ) _client = HardwarePlaneClient( _daemon, default_timeout_ms=settings.hardware_plane_rpc_timeout_ms, @@ -125,4 +127,3 @@ async def stop_hardware_plane() -> None: """停止硬件平面 / Stop hardware plane.""" daemon = get_hardware_plane_daemon() await daemon.stop() - diff --git a/ogscope/platform/hardware_plane/services/__init__.py b/ogscope/platform/hardware_plane/services/__init__.py index 6bf89ec..1bd2ec1 100644 --- a/ogscope/platform/hardware_plane/services/__init__.py +++ b/ogscope/platform/hardware_plane/services/__init__.py @@ -7,4 +7,3 @@ from ogscope.platform.hardware_plane.services.sensor_hub import SensorHubService __all__ = ["CameraPlaneService", "HmiService", "SensorHubService"] - diff --git a/ogscope/platform/hardware_plane/services/base.py b/ogscope/platform/hardware_plane/services/base.py index ab24826..4702048 100644 --- a/ogscope/platform/hardware_plane/services/base.py +++ b/ogscope/platform/hardware_plane/services/base.py @@ -21,6 +21,7 @@ async def stop(self) -> None: async def status(self) -> dict[str, Any]: """读取服务状态 / Read service status.""" - async def command(self, action: str, payload: dict[str, Any] | None = None) -> dict[str, Any]: + async def command( + self, action: str, payload: dict[str, Any] | None = None + ) -> dict[str, Any]: """执行命令 / Execute command.""" - diff --git a/ogscope/platform/hardware_plane/services/camera_service.py b/ogscope/platform/hardware_plane/services/camera_service.py index a687fa2..7ea4364 100644 --- a/ogscope/platform/hardware_plane/services/camera_service.py +++ b/ogscope/platform/hardware_plane/services/camera_service.py @@ -88,4 +88,3 @@ async def command( frame.pop("payload", None) return frame return {"accepted": False, "message": f"unsupported action: {action}"} - diff --git a/ogscope/platform/hardware_plane/services/hmi.py b/ogscope/platform/hardware_plane/services/hmi.py index 2b129e5..7fd629d 100644 --- a/ogscope/platform/hardware_plane/services/hmi.py +++ b/ogscope/platform/hardware_plane/services/hmi.py @@ -73,9 +73,13 @@ def _ensure_display_sync(self) -> Any: "display_disabled:在环境变量或 .env 中设置 OGSCOPE_DISPLAY_ENABLED=true" ) if settings.display_type.lower() != "st7796": - raise RuntimeError(f"unsupported display_type: {settings.display_type!r} (expected st7796)") + raise RuntimeError( + f"unsupported display_type: {settings.display_type!r} (expected st7796)" + ) if sys.platform != "linux": - raise RuntimeError("ST7796 仅支持 Linux(树莓派)/ ST7796 requires Linux (Raspberry Pi)") + raise RuntimeError( + "ST7796 仅支持 Linux(树莓派)/ ST7796 requires Linux (Raspberry Pi)" + ) if self._display is not None: return self._display from ogscope.platform.hardware.st7796_spi import ST7796SPI diff --git a/ogscope/platform/hardware_plane/services/sensor_hub.py b/ogscope/platform/hardware_plane/services/sensor_hub.py index 5eea6b8..67770e1 100644 --- a/ogscope/platform/hardware_plane/services/sensor_hub.py +++ b/ogscope/platform/hardware_plane/services/sensor_hub.py @@ -63,4 +63,3 @@ async def command( self._running = True return {"accepted": True, "message": "sensor hub restarted"} return {"accepted": False, "message": f"unsupported action: {action}"} - diff --git a/ogscope/platform/hardware_plane/transport/__init__.py b/ogscope/platform/hardware_plane/transport/__init__.py index 3b55b41..1e20b7c 100644 --- a/ogscope/platform/hardware_plane/transport/__init__.py +++ b/ogscope/platform/hardware_plane/transport/__init__.py @@ -8,4 +8,3 @@ ) __all__ = ["JsonRpcUdsServer", "JsonRpcUdsClient"] - diff --git a/ogscope/platform/hardware_plane/transport/jsonrpc_uds.py b/ogscope/platform/hardware_plane/transport/jsonrpc_uds.py index 7515534..35c5197 100644 --- a/ogscope/platform/hardware_plane/transport/jsonrpc_uds.py +++ b/ogscope/platform/hardware_plane/transport/jsonrpc_uds.py @@ -96,13 +96,18 @@ async def call( "method": method, "params": params or {}, } - writer.write((json.dumps(request, ensure_ascii=False) + "\n").encode("utf-8")) + writer.write( + (json.dumps(request, ensure_ascii=False) + "\n").encode("utf-8") + ) await asyncio.wait_for(writer.drain(), timeout=budget_s) line = await asyncio.wait_for(reader.readline(), timeout=budget_s) if not line: - return {"success": False, "error": {"message": "empty response"}, "data": {}} + return { + "success": False, + "error": {"message": "empty response"}, + "data": {}, + } return json.loads(line.decode("utf-8", errors="ignore")) finally: writer.close() await writer.wait_closed() - diff --git a/ogscope/web/api/analysis/routes.py b/ogscope/web/api/analysis/routes.py index 8ff2314..114b812 100644 --- a/ogscope/web/api/analysis/routes.py +++ b/ogscope/web/api/analysis/routes.py @@ -8,6 +8,7 @@ from fastapi.responses import FileResponse, PlainTextResponse from ogscope.domain.analysis.services import analysis_domain_service +from ogscope.web.api.analysis.services import analysis_service from ogscope.web.api.models.schemas import ( AnalysisBatchSolveRequest, AnalysisExperimentCreate, @@ -19,7 +20,6 @@ AnalysisSolveVideoFrameRequest, ImportFromDebugRequest, ) -from ogscope.web.api.analysis.services import analysis_service router = APIRouter() @@ -287,7 +287,9 @@ async def solve_uploaded_frame( """上传单帧 JPEG/PNG 并解算 / Solve a single uploaded frame (multipart).""" try: raw = await file.read() - payload_dict, extras = analysis_domain_service.parse_frame_upload_payload(payload) + payload_dict, extras = analysis_domain_service.parse_frame_upload_payload( + payload + ) data = AnalysisSolveImageRequest.model_validate(payload_dict) return await analysis_service.solve_uploaded_frame( image_bytes=raw, diff --git a/ogscope/web/api/analysis/services.py b/ogscope/web/api/analysis/services.py index f14fc70..b372cc0 100644 --- a/ogscope/web/api/analysis/services.py +++ b/ogscope/web/api/analysis/services.py @@ -1317,7 +1317,9 @@ async def solve_video_frame( ) loop = asyncio.get_running_loop() - cr_frame = self._clamp_centroid_rejection_level(body.centroid_rejection_level) + cr_frame = self._clamp_centroid_rejection_level( + body.centroid_rejection_level + ) def _run() -> dict[str, Any]: return self._solve_bgr_to_row( diff --git a/ogscope/web/api/core/routes.py b/ogscope/web/api/core/routes.py index 4a53330..a0dc471 100644 --- a/ogscope/web/api/core/routes.py +++ b/ogscope/web/api/core/routes.py @@ -25,7 +25,9 @@ "/core/v1/analysis/start", response_model=CoreAnalysisControlResponse, ) -async def core_start_analysis(body: CoreStartAnalysisRequest) -> CoreAnalysisControlResponse: +async def core_start_analysis( + body: CoreStartAnalysisRequest, +) -> CoreAnalysisControlResponse: """开始分析(Core 标准契约)/ Start analysis (Core contract).""" try: data = await core_contract_service.start_analysis( @@ -113,7 +115,9 @@ async def core_camera_stop() -> CoreCameraControlResponse: async def core_camera_tune(payload: CoreCameraTuneRequest) -> CoreCameraControlResponse: """微调相机参数(Core 标准契约)/ Tune camera settings (Core contract).""" try: - data = await core_contract_service.tune_camera(payload.model_dump(exclude_none=True)) + data = await core_contract_service.tune_camera( + payload.model_dump(exclude_none=True) + ) return CoreCameraControlResponse(**data) except Exception as exc: # noqa: BLE001 raise HTTPException(status_code=500, detail=str(exc)) from exc diff --git a/ogscope/web/api/debug/magnetometer_service.py b/ogscope/web/api/debug/magnetometer_service.py index 3e15859..8788bbf 100644 --- a/ogscope/web/api/debug/magnetometer_service.py +++ b/ogscope/web/api/debug/magnetometer_service.py @@ -35,7 +35,12 @@ def _smbus_read_wia(bus: int, addr7: int) -> dict[str, Any]: try: from smbus2 import SMBus except ImportError: - return {"ok": False, "error": "smbus2 not installed", "wia1": None, "wia2": None} + return { + "ok": False, + "error": "smbus2 not installed", + "wia1": None, + "wia2": None, + } path = f"/dev/i2c-{bus}" if not os.path.exists(path): @@ -60,9 +65,7 @@ def _read() -> tuple[int | None, int | None, str | None]: wia1, wia2, err = _read() if err: return {"ok": False, "error": err, "wia1": None, "wia2": None} - match = ( - wia1 == _AKM_WIA1 and wia2 is not None and int(wia2) in _KNOWN_WIA2 - ) + match = wia1 == _AKM_WIA1 and wia2 is not None and int(wia2) in _KNOWN_WIA2 return { "ok": True, "error": None, @@ -96,6 +99,7 @@ def _smbus_read_wia_first_matching( class MagnetometerDebugService: """AK09911 系列探针与总线扫描 / AK09911 family probe and bus scan.""" + _xy_calib: dict[tuple[int, int], dict[str, Any]] = {} _heading_mode: dict[tuple[int, int], str] = {} _heading_locked: dict[tuple[int, int], dict[str, Any]] = {} @@ -226,14 +230,22 @@ async def calibration_commit(*, bus: int = 1, addr7: int = 0x0C) -> dict[str, An d = ((d + 180.0) % 360.0) - 180.0 unwrapped += d prev_u = unwrapped - trend = unwrapped - ((math.degrees(math.atan2( - *MagnetometerDebugService._pair_values( - axes, - float(hx_hist[0]) - cx, - float(hy_hist[0]) - cy, - float(hz_hist[0]) - cz, + trend = unwrapped - ( + ( + math.degrees( + math.atan2( + *MagnetometerDebugService._pair_values( + axes, + float(hx_hist[0]) - cx, + float(hy_hist[0]) - cy, + float(hz_hist[0]) - cz, + ) + ) + ) + + 360.0 ) - )) + 360.0) % 360.0) + % 360.0 + ) sign = 1 if trend >= 0 else -1 locked = { @@ -284,7 +296,11 @@ async def calibration_status(*, bus: int = 1, addr7: int = 0x0C) -> dict[str, An "addr_7bit": int(k[1]), "addr_7bit_hex": f"0x{int(k[1]):02x}", "samples": int(float(st.get("samples", 0.0))), - "span_xyz": {"x": round(span_x, 3), "y": round(span_y, 3), "z": round(span_z, 3)}, + "span_xyz": { + "x": round(span_x, 3), + "y": round(span_y, 3), + "z": round(span_z, 3), + }, "locked": locked, } @@ -416,9 +432,7 @@ async def probe_address_on_buses( _smbus_read_wia_first_matching, b, addr7 ) results.append({"bus": b, "addr_7bit_used": int(used), **w}) - any_ok = any( - r.get("ok") and r.get("matches_ak099xx") for r in results - ) + any_ok = any(r.get("ok") and r.get("matches_ak099xx") for r in results) return { "success": any_ok, "addr_7bit": int(addr7), @@ -530,9 +544,8 @@ async def sample_heading(*, bus: int = 1, addr7: int = 0x0C) -> dict[str, Any]: lz = hz - float(c.get("z", cz)) la, lb = MagnetometerDebugService._pair_values(axes_locked, lx, ly, lz) heading_deg_locked = ( - (math.degrees(math.atan2(sign_locked * la, lb)) + offset_locked + 360.0) - % 360.0 - ) + math.degrees(math.atan2(sign_locked * la, lb)) + offset_locked + 360.0 + ) % 360.0 heading_deg = heading_deg_locked heading_source = f"locked_{axes_locked}" return { @@ -548,7 +561,9 @@ async def sample_heading(*, bus: int = 1, addr7: int = 0x0C) -> dict[str, Any]: "heading_raw_deg": round(heading_raw_deg, 2), "heading_calibrated_deg": round(heading_cal_deg, 2), "heading_auto_deg": round(heading_auto_deg, 2), - "heading_locked_deg": None if heading_deg_locked is None else round(heading_deg_locked, 2), + "heading_locked_deg": ( + None if heading_deg_locked is None else round(heading_deg_locked, 2) + ), "heading_source": heading_source, "heading_axes_auto": auto_axes, "heading_mode": mode, @@ -580,4 +595,3 @@ async def sample_heading(*, bus: int = 1, addr7: int = 0x0C) -> dict[str, Any]: "calibration_samples": int(float(st.get("samples", 0.0))), "calibration_locked": locked, } - diff --git a/ogscope/web/api/system/routes.py b/ogscope/web/api/system/routes.py index 38f166e..278513e 100644 --- a/ogscope/web/api/system/routes.py +++ b/ogscope/web/api/system/routes.py @@ -12,7 +12,10 @@ from ogscope.domain.system.services import system_info_service from ogscope.platform.hardware_plane.runtime import get_hardware_plane_client from ogscope.web.api.models.schemas import SystemInfo -from ogscope.web.api.system.config_files import read_config_file_payload, write_config_file +from ogscope.web.api.system.config_files import ( + read_config_file_payload, + write_config_file, +) router = APIRouter() diff --git a/ogscope/web/app.py b/ogscope/web/app.py index 6f5c290..4dfe82a 100644 --- a/ogscope/web/app.py +++ b/ogscope/web/app.py @@ -101,7 +101,9 @@ async def _warm_solver() -> None: "相机自动启动失败,将按需延迟启动 / Camera auto-start failed, fallback to lazy start: {}", e, ) - phase_elapsed_ms = int((asyncio.get_running_loop().time() - phase_p0_started) * 1000) + phase_elapsed_ms = int( + (asyncio.get_running_loop().time() - phase_p0_started) * 1000 + ) logger.info("启动阶段完成 / Startup phases ready in {} ms", phase_elapsed_ms) try: @@ -141,8 +143,12 @@ async def _warm_solver() -> None: logger.warning( "硬件平面停止超时或异常 / Hardware plane stop timeout or error: {}", e ) - shutdown_elapsed_ms = int((asyncio.get_running_loop().time() - shutdown_started) * 1000) - logger.info("退出阶段完成 / Shutdown cleanup finished in {} ms", shutdown_elapsed_ms) + shutdown_elapsed_ms = int( + (asyncio.get_running_loop().time() - shutdown_started) * 1000 + ) + logger.info( + "退出阶段完成 / Shutdown cleanup finished in {} ms", shutdown_elapsed_ms + ) # API 文档分组标签 / API documentation group tags @@ -268,6 +274,7 @@ async def _guard_subordinate_dev_routes(request: Request, call_next): ) return await call_next(request) + # 挂载静态文件 / Mount static files if bool(hardware_profile["enable_ui"]) and settings.static_dir.exists(): app.mount("/static", StaticFiles(directory=str(settings.static_dir)), name="static") @@ -438,7 +445,9 @@ def _filtered_openapi_schema(*, mode: str) -> dict: filtered_paths: dict[str, dict] = {} if mode == "core": filtered_paths = { - path: data for path, data in paths.items() if path.startswith("/api/core/v1/") + path: data + for path, data in paths.items() + if path.startswith("/api/core/v1/") } elif mode == "dev": filtered_paths = { diff --git a/ogscope/web/mjpeg_stream_limiter.py b/ogscope/web/mjpeg_stream_limiter.py index 0c2371b..44bd961 100644 --- a/ogscope/web/mjpeg_stream_limiter.py +++ b/ogscope/web/mjpeg_stream_limiter.py @@ -2,6 +2,9 @@ MJPEG 长连接并发限制 / Concurrent MJPEG stream limiter """ -from ogscope.domain.camera.stream_limiter import MjpegStreamLimiter, get_mjpeg_stream_limiter +from ogscope.domain.camera.stream_limiter import ( + MjpegStreamLimiter, + get_mjpeg_stream_limiter, +) __all__ = ["MjpegStreamLimiter", "get_mjpeg_stream_limiter"] diff --git a/tests/conftest.py b/tests/conftest.py index b147c19..115c6f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,9 +19,9 @@ def client(): @pytest.fixture def temp_debug_dir(monkeypatch, tmp_path: Path): """将调试目录重定向到临时目录,避免污染用户目录。 / Redirect the debug directory to a temporary directory to avoid polluting the user directory.""" - from ogscope.web.api.debug import services as debug_services from ogscope.domain import shared as domain_shared_pkg from ogscope.domain.shared import filesystem as shared_fs + from ogscope.web.api.debug import services as debug_services debug_root = tmp_path / "dev_captures" debug_root.mkdir(parents=True, exist_ok=True) @@ -32,7 +32,9 @@ def temp_debug_dir(monkeypatch, tmp_path: Path): if hasattr(debug_services, "DEBUG_CAPTURES_DIR"): monkeypatch.setattr(debug_services, "DEBUG_CAPTURES_DIR", debug_root) if hasattr(domain_shared_pkg, "filesystem"): - monkeypatch.setattr(domain_shared_pkg.filesystem, "DEV_CAPTURES_DIR", debug_root) + monkeypatch.setattr( + domain_shared_pkg.filesystem, "DEV_CAPTURES_DIR", debug_root + ) monkeypatch.setattr(debug_services, "is_recording", False) monkeypatch.setattr(debug_services, "recording_task", None) monkeypatch.setattr(debug_services, "recording_stem", None) diff --git a/tests/unit/test_analysis_api.py b/tests/unit/test_analysis_api.py index ed9e2d1..a59895a 100644 --- a/tests/unit/test_analysis_api.py +++ b/tests/unit/test_analysis_api.py @@ -219,7 +219,9 @@ def test_analysis_list_presets_and_batch( ) assert exp.status_code == 200 - el = client.get("/api/dev/analysis/experiments", params={"page": 1, "page_size": 10}) + el = client.get( + "/api/dev/analysis/experiments", params={"page": 1, "page_size": 10} + ) assert el.status_code == 200 assert el.json()["total"] >= 1 diff --git a/tests/unit/test_config_catalog.py b/tests/unit/test_config_catalog.py index ac3d922..b3f8429 100644 --- a/tests/unit/test_config_catalog.py +++ b/tests/unit/test_config_catalog.py @@ -12,9 +12,7 @@ def test_build_config_catalog_includes_new_preview_fields() -> None: catalog = build_config_catalog() keys = { - entry["key"] - for section in catalog["sections"] - for entry in section["entries"] + entry["key"] for section in catalog["sections"] for entry in section["entries"] } assert "OGSCOPE_SHARED_PREVIEW_FPS" in keys assert "OGSCOPE_PREVIEW_JPEG_QUALITY" in keys diff --git a/tests/unit/test_config_files.py b/tests/unit/test_config_files.py index f2c0540..7a8306c 100644 --- a/tests/unit/test_config_files.py +++ b/tests/unit/test_config_files.py @@ -18,7 +18,9 @@ def test_read_config_file_payload_marks_sudo_writable( monkeypatch.setattr(mod, "CONFIG_WRITE_SCRIPT", tmp_path / "write.sh") monkeypatch.setattr(mod, "CONFIG_SUDOERS", tmp_path / "sudoers") mod.CONFIG_WRITE_SCRIPT.write_text("#!/bin/sh\n", encoding="utf-8") - mod.CONFIG_SUDOERS.write_text("ogscope ALL=(ALL) NOPASSWD: /usr/local/bin/ogscope-config-write\n") + mod.CONFIG_SUDOERS.write_text( + "ogscope ALL=(ALL) NOPASSWD: /usr/local/bin/ogscope-config-write\n" + ) payload = mod.read_config_file_payload(env_path) @@ -29,7 +31,8 @@ def test_read_config_file_payload_marks_sudo_writable( @pytest.mark.unit def test_read_config_file_payload_not_writable_without_sudoers( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, ) -> None: env_path = tmp_path / "ogscope.env" env_path.write_text("OGSCOPE_PORT=8000\n", encoding="utf-8") diff --git a/tests/unit/test_core_contract_api.py b/tests/unit/test_core_contract_api.py index e6afa0d..9fb96a8 100644 --- a/tests/unit/test_core_contract_api.py +++ b/tests/unit/test_core_contract_api.py @@ -118,11 +118,15 @@ async def _fake_stop_camera(): monkeypatch.setattr( core_service.core_contract_service, "get_camera_status", _fake_camera_status ) - monkeypatch.setattr(core_service.core_contract_service, "tune_camera", _fake_camera_tune) + monkeypatch.setattr( + core_service.core_contract_service, "tune_camera", _fake_camera_tune + ) monkeypatch.setattr( core_service.core_contract_service, "start_camera", _fake_start_camera ) - monkeypatch.setattr(core_service.core_contract_service, "stop_camera", _fake_stop_camera) + monkeypatch.setattr( + core_service.core_contract_service, "stop_camera", _fake_stop_camera + ) monkeypatch.setattr( core_service.core_contract_service, "get_stream_status", _fake_stream_status ) diff --git a/tests/unit/test_debug_camera_api.py b/tests/unit/test_debug_camera_api.py index 2fb7e74..fe0623f 100644 --- a/tests/unit/test_debug_camera_api.py +++ b/tests/unit/test_debug_camera_api.py @@ -236,7 +236,9 @@ def test_debug_camera_update_settings_success(client, fake_camera_env): @pytest.mark.unit def test_debug_camera_auto_exposure_switch_success(client, fake_camera_env): - response = client.post("/api/dev/debug/camera/auto-exposure", params={"enabled": False}) + response = client.post( + "/api/dev/debug/camera/auto-exposure", params={"enabled": False} + ) assert response.status_code == 200 body = response.json() assert body["success"] is True diff --git a/tests/unit/test_dev_contract_api.py b/tests/unit/test_dev_contract_api.py index e0ca105..ab79da3 100644 --- a/tests/unit/test_dev_contract_api.py +++ b/tests/unit/test_dev_contract_api.py @@ -58,4 +58,3 @@ def test_dev_hardware_plane_metrics_include_profile(client) -> None: def test_legacy_debug_path_not_exposed(client) -> None: resp = client.get("/api/debug/camera/status") assert resp.status_code in {404, 405} - diff --git a/tests/unit/test_domain_camera_sidecar.py b/tests/unit/test_domain_camera_sidecar.py index 281827b..7857a99 100644 --- a/tests/unit/test_domain_camera_sidecar.py +++ b/tests/unit/test_domain_camera_sidecar.py @@ -39,4 +39,3 @@ def test_merge_capture_sidecar_does_not_override_existing_fields() -> None: merge_capture_sidecar_into_info(info, capture_info) assert info["resolution"] == "640x480" - diff --git a/tests/unit/test_domain_camera_streaming.py b/tests/unit/test_domain_camera_streaming.py index 5a11548..c8d7827 100644 --- a/tests/unit/test_domain_camera_streaming.py +++ b/tests/unit/test_domain_camera_streaming.py @@ -35,7 +35,9 @@ async def release(self) -> None: @pytest.mark.unit @pytest.mark.asyncio -async def test_build_camera_mjpeg_stream_rejects_when_limit_reached(monkeypatch) -> None: +async def test_build_camera_mjpeg_stream_rejects_when_limit_reached( + monkeypatch, +) -> None: limiter = _FakeLimiter(can_acquire=False) monkeypatch.setattr(streaming_mod, "get_mjpeg_stream_limiter", lambda: limiter) @@ -60,10 +62,13 @@ async def test_build_camera_mjpeg_stream_yields_frame_and_releases(monkeypatch) class _FakeSettings: stream_mjpeg_frame_fetch_timeout_ms = 1000 + shared_preview_fps = 8 monkeypatch.setattr(streaming_mod, "get_settings", lambda: _FakeSettings()) - async def _fake_get_stream_frame_bytes(fmt: str, quality: int, *, since_frame_id: int): + async def _fake_get_stream_frame_bytes( + fmt: str, quality: int, *, since_frame_id: int + ): _ = fmt, quality, since_frame_id return 200, b"abc", 1 @@ -87,4 +92,3 @@ async def _fake_get_stream_frame_bytes(fmt: str, quality: int, *, since_frame_id assert b"Content-Type: image/jpeg" in first_chunk await body_iter.aclose() assert limiter.released is True - diff --git a/tests/unit/test_domain_shared_filesystem.py b/tests/unit/test_domain_shared_filesystem.py index c945ba0..77e430a 100644 --- a/tests/unit/test_domain_shared_filesystem.py +++ b/tests/unit/test_domain_shared_filesystem.py @@ -34,4 +34,3 @@ def test_ensure_safe_basename_accepts_valid_names(name: str) -> None: def test_ensure_safe_basename_rejects_invalid_names(name: str) -> None: with pytest.raises(ValueError): ensure_safe_basename(name) - diff --git a/tests/unit/test_hardware_plane.py b/tests/unit/test_hardware_plane.py index cc866cd..5314e18 100644 --- a/tests/unit/test_hardware_plane.py +++ b/tests/unit/test_hardware_plane.py @@ -38,7 +38,9 @@ async def test_hardware_plane_daemon_minimal_methods() -> None: @pytest.mark.unit @pytest.mark.asyncio -async def test_hardware_plane_daemon_subordinate_profile_disables_local_services() -> None: +async def test_hardware_plane_daemon_subordinate_profile_disables_local_services() -> ( + None +): daemon = HardwarePlaneDaemon( enable_local_sensors=False, enable_hmi=False, @@ -62,7 +64,9 @@ async def test_hardware_plane_daemon_subordinate_profile_disables_local_services @pytest.mark.asyncio async def test_jsonrpc_uds_sensor_read_roundtrip(tmp_path: Path) -> None: _ = tmp_path - socket_path = Path("/tmp") / f"external-sensor-{os.getpid()}-{int(time.time() * 1000)}.sock" + socket_path = ( + Path("/tmp") / f"external-sensor-{os.getpid()}-{int(time.time() * 1000)}.sock" + ) async def _handler(method: str, params: dict[str, object]) -> dict[str, object]: if method != "sensor.read": @@ -108,4 +112,3 @@ def test_runtime_profile_subordinate_disables_ui_hmi_local_sensors() -> None: assert profile["enable_hmi"] is False assert profile["enable_ui"] is True assert profile["enable_local_sensors"] is False - diff --git a/tests/unit/test_system_wifi_parse.py b/tests/unit/test_system_wifi_parse.py index f3e206e..9b5246a 100644 --- a/tests/unit/test_system_wifi_parse.py +++ b/tests/unit/test_system_wifi_parse.py @@ -4,7 +4,6 @@ import pytest -from ogscope.web.api.system import services as system_services from ogscope.web.api.system.services import SystemInfoService _WIRELESS_SAMPLE = """Inter-| sta-| Quality | Discarded packets diff --git a/tests/unit/test_wifi_switch.py b/tests/unit/test_wifi_switch.py index 0769de6..8a05c78 100644 --- a/tests/unit/test_wifi_switch.py +++ b/tests/unit/test_wifi_switch.py @@ -10,7 +10,10 @@ import pytest from ogscope.config import Settings -from ogscope.platform.hardware.wifi_switch import WifiSwitchService, _parse_status_output +from ogscope.platform.hardware.wifi_switch import ( + WifiSwitchService, + _parse_status_output, +) @pytest.mark.unit @@ -119,7 +122,9 @@ async def _fake_switch_mode(mode: str): _ = mode return network_routes.wifi_domain_service.build_wifi_status() - monkeypatch.setattr(network_routes.wifi_domain_service, "switch_mode", _fake_switch_mode) + monkeypatch.setattr( + network_routes.wifi_domain_service, "switch_mode", _fake_switch_mode + ) response = client.get("/api/network/wifi") assert response.status_code == 200 @@ -139,7 +144,9 @@ def test_network_wifi_scan_api(client, monkeypatch) -> None: async def _fake_scan_wifi(): return [{"ssid": "Home", "signal": 80, "security": "WPA2"}], None - monkeypatch.setattr(network_routes.wifi_domain_service, "scan_wifi", _fake_scan_wifi) + monkeypatch.setattr( + network_routes.wifi_domain_service, "scan_wifi", _fake_scan_wifi + ) response = client.get("/api/network/wifi/scan") assert response.status_code == 200 @@ -163,7 +170,9 @@ async def _fake_profiles(): } ] - monkeypatch.setattr(network_routes.wifi_domain_service, "list_profiles", _fake_profiles) + monkeypatch.setattr( + network_routes.wifi_domain_service, "list_profiles", _fake_profiles + ) response = client.get("/api/network/wifi/profiles") assert response.status_code == 200 data = response.json()